diff --git a/app/build.gradle b/app/build.gradle index 24d7ac0877..bef6eced1f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,8 +187,7 @@ dependencies { implementation libs.bundles.filemojicompat - implementation libs.bouncycastle - implementation libs.unified.push + implementation libs.bundles.unifiedpush implementation libs.bundles.xmldiff diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b3833ebceb..479d90efe3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -26,10 +26,6 @@ -keepattributes SourceFile,LineNumberTable -renamesourcefileattribute SourceFile -# Bouncy Castle -- Keep EC --keep class org.bouncycastle.jcajce.provider.asymmetric.EC$* { *; } --keep class org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi$EC - # Preference fragments can be referenced by name, ensure they remain # https://github.com/tuskyapp/Tusky/issues/3161 -keep class * extends androidx.preference.PreferenceFragmentCompat diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 86db206e8a..900cd2996f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -159,19 +159,12 @@ android:name=".receiver.SendStatusBroadcastReceiver" android:enabled="true" android:exported="false" /> - + - - - - - + - + - notificationService.clearNotificationsForAccount(account) + notificationHelper.clearNotificationsForAccount(account) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/SeveredRelationshipNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/SeveredRelationshipNotificationViewHolder.kt index 541585ee31..96fd7a26bd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/SeveredRelationshipNotificationViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/SeveredRelationshipNotificationViewHolder.kt @@ -16,7 +16,7 @@ package com.keylesspalace.tusky.components.notifications import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.databinding.ItemSeveredRelationshipNotificationBinding import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.viewdata.NotificationViewData @@ -37,7 +37,7 @@ class SeveredRelationshipNotificationViewHolder( val event = viewData.event!! val context = binding.root.context - binding.severedRelationshipText.text = NotificationService.severedRelationShipText( + binding.severedRelationshipText.text = NotificationHelper.severedRelationShipText( context, event, instanceName diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index fcc42e852d..2ee11ae8e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -18,7 +18,7 @@ package com.keylesspalace.tusky.components.preference import android.os.Bundle import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.entity.AccountEntity import com.keylesspalace.tusky.settings.PrefKeys @@ -36,7 +36,7 @@ class NotificationPreferencesFragment : BasePreferencesFragment() { lateinit var accountManager: AccountManager @Inject - lateinit var notificationService: NotificationService + lateinit var notificationHelper: NotificationHelper override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { val activeAccount = accountManager.activeAccount ?: return @@ -48,10 +48,10 @@ class NotificationPreferencesFragment : BasePreferencesFragment() { isChecked = activeAccount.notificationsEnabled setOnPreferenceChangeListener { _, newValue -> updateAccount { copy(notificationsEnabled = newValue as Boolean) } - if (notificationService.areNotificationsEnabledBySystem()) { - notificationService.enablePullNotifications() + if (notificationHelper.areNotificationsEnabledBySystem()) { + notificationHelper.enablePullNotifications() } else { - notificationService.disablePullNotifications() + notificationHelper.disablePullNotifications() } true } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt index d65d4381bb..ae722595a7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt @@ -43,7 +43,7 @@ class NotificationFetcher @Inject constructor( private val mastodonApi: MastodonApi, private val accountManager: AccountManager, private val eventHub: EventHub, - private val notificationService: NotificationService, + private val notificationHelper: NotificationHelper, ) { suspend fun fetchAndShow(accountId: Long?) { for (account in accountManager.accounts) { @@ -54,14 +54,14 @@ class NotificationFetcher @Inject constructor( if (account.notificationsEnabled) { try { val notifications = fetchNewNotifications(account) - .filter { notificationService.filterNotification(account, it.type) } + .filter { notificationHelper.filterNotification(account, it.type) } .sortedWith( compareBy({ it.id.length }, { it.id }) ) // oldest notifications first eventHub.dispatch(NewNotificationsEvent(account.accountId, notifications)) - notificationService.show(account, notifications) + notificationHelper.show(account, notifications) } catch (e: Exception) { Log.e(TAG, "Error while fetching notifications", e) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationService.kt b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.kt similarity index 94% rename from app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationService.kt rename to app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.kt index 9243ee0db1..b7cc554622 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.kt @@ -46,6 +46,7 @@ import com.keylesspalace.tusky.MainActivity.Companion.openNotificationIntent import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.entity.AccountEntity import com.keylesspalace.tusky.di.ApplicationScope @@ -56,7 +57,6 @@ import com.keylesspalace.tusky.entity.visibleNotificationTypes import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.CryptoUtil import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.unicodeWrap import com.keylesspalace.tusky.viewdata.buildDescription @@ -73,16 +73,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.unifiedpush.android.connector.UnifiedPush +import org.unifiedpush.android.connector.data.PushEndpoint +import org.unifiedpush.android.connector.ui.SelectDistributorDialogsBuilder +import org.unifiedpush.android.connector.ui.UnifiedPushFunctions import retrofit2.HttpException @Singleton -class NotificationService @Inject constructor( +class NotificationHelper @Inject constructor( private val notificationManager: NotificationManager, private val accountManager: AccountManager, private val api: MastodonApi, private val preferences: SharedPreferences, @ApplicationContext private val context: Context, @ApplicationScope private val applicationScope: CoroutineScope, + private val instanceInfoRepository: InstanceInfoRepository ) { private var workManager: WorkManager = WorkManager.getInstance(context) @@ -648,7 +652,7 @@ class NotificationService @Inject constructor( .putExtra(KEY_CITED_STATUS_ID, inReplyToId) .putExtra(KEY_VISIBILITY, replyVisibility) .putExtra(KEY_SPOILER, contentWarning) - .putExtra(KEY_MENTIONS, mentionedUsernames.toTypedArray()) + .putExtra(KEY_MENTIONS, mentionedUsernames.toTypedArray()) return PendingIntent.getBroadcast( context.applicationContext, @@ -768,13 +772,36 @@ class NotificationService @Inject constructor( // make sure this is done in any inconsistent case (is not too often and doesn't hurt). unregisterPushEndpoint(account) - UnifiedPush.registerAppWithDialog(activity, account.id.toString(), features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE)) - // Will lead to call of registerPushEndpoint() + val vapid = instanceInfoRepository.getUpdatedInstanceInfoOrFallback().vapidKey?.replace("=", "") + + val builder = SelectDistributorDialogsBuilder( + activity, + object : UnifiedPushFunctions { + override fun getAckDistributor(): String? = + UnifiedPush.getAckDistributor(activity) + + override fun getDistributors(): List = + UnifiedPush.getDistributors(activity) + + override fun register(instance: String) = + UnifiedPush.register(activity, instance, vapid = vapid) + + override fun saveDistributor(distributor: String) = + UnifiedPush.saveDistributor(activity, distributor) + + override fun tryUseDefaultDistributor(callback: (Boolean) -> Unit) = + UnifiedPush.tryUseDefaultDistributor(activity, callback) + } + ) + builder.instances = listOf(account.id.toString()) + builder.mayUseDefault = false + builder.mayUseCurrent = false + builder.run() } } private fun resetPushWhenDistributorIsMissing() { - val lastUsedPushProvider = preferences.getString(PrefKeys.LAST_USED_PUSH_PROVDER, null) + val lastUsedPushProvider = preferences.getString(PrefKeys.LAST_USED_PUSH_PROVIDER, null) // NOTE UnifiedPush.getSavedDistributor() cannot be used here as that is already null here if the // distributor was uninstalled. @@ -785,7 +812,7 @@ class NotificationService @Inject constructor( Log.w(TAG, "Previous push provider ($lastUsedPushProvider) uninstalled. Resetting all accounts.") preferences.edit { - remove(PrefKeys.LAST_USED_PUSH_PROVDER) + remove(PrefKeys.LAST_USED_PUSH_PROVIDER) } applicationScope.launch { @@ -836,7 +863,7 @@ class NotificationService @Inject constructor( unregisterPushEndpoint(account) // this probably does nothing (distributor to handle this is missing) - UnifiedPush.unregisterApp(context, account.id.toString()) + UnifiedPush.unregister(context, account.id.toString()) } fun fetchNotificationsOnPushMessage(account: AccountEntity) { @@ -857,26 +884,25 @@ class NotificationService @Inject constructor( private fun buildAlertSubscriptionData(account: AccountEntity): Map = buildAlertsMap(account).mapKeys { "data[alerts][${it.key}]" } - // Called by UnifiedPush callback in UnifiedPushBroadcastReceiver + // Called by UnifiedPush callback in UnifiedPushService suspend fun registerPushEndpoint( account: AccountEntity, - endpoint: String + endpoint: PushEndpoint ) = withContext(Dispatchers.IO) { - // Generate a prime256v1 key pair for WebPush - // Decryption is unimplemented for now, since Mastodon uses an old WebPush - // standard which does not send needed information for decryption in the payload - // This makes it not directly compatible with UnifiedPush - // As of now, we use it purely as a way to trigger a pull - val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1) - val auth = CryptoUtil.secureRandomBytesEncoded(16) + val pubKeySet = endpoint.pubKeySet + if (pubKeySet == null) { + Log.w(TAG, "cannot register push endpoint without public key") + return@withContext + } api.subscribePushNotifications( - "Bearer ${account.accessToken}", - account.domain, - endpoint, - keyPair.pubkey, - auth, - buildAlertSubscriptionData(account) + auth = "Bearer ${account.accessToken}", + domain = account.domain, + standard = true, + endpoint = endpoint.url, + keysP256DH = pubKeySet.pubKey, + keysAuth = pubKeySet.auth, + data = buildAlertSubscriptionData(account) ).onFailure { throwable -> Log.w(TAG, "Error setting push endpoint for account ${account.id}", throwable) disablePushNotificationsForAccount(account) @@ -885,11 +911,12 @@ class NotificationService @Inject constructor( accountManager.updateAccount(account) { copy( - pushPubKey = keyPair.pubkey, - pushPrivKey = keyPair.privKey, - pushAuth = auth, + pushPubKey = pubKeySet.pubKey, + // TODO + pushPrivKey = "", + pushAuth = pubKeySet.auth, pushServerKey = it.serverKey, - unifiedPushUrl = endpoint + unifiedPushUrl = endpoint.url ) } @@ -897,7 +924,7 @@ class NotificationService @Inject constructor( Log.d(TAG, "Saving distributor to preferences: $it") preferences.edit { - putString(PrefKeys.LAST_USED_PUSH_PROVDER, it) + putString(PrefKeys.LAST_USED_PUSH_PROVIDER, it) } // TODO once this is selected it cannot be changed (except by wiping the application or uninstalling the provider) @@ -949,7 +976,7 @@ class NotificationService @Inject constructor( } companion object { - const val TAG = "NotificationService" + const val TAG = "NotificationHelper" const val KEY_CITED_STATUS_ID: String = "KEY_CITED_STATUS_ID" const val KEY_MENTIONS: String = "KEY_MENTIONS" diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 660e90743d..5042942d22 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -65,7 +65,7 @@ }, // Note: Starting with version 54, database versions in Tusky are always even. // This is to reserve odd version numbers for use by forks. - version = 70, + version = 72, autoMigrations = { @AutoMigration(from = 48, to = 49), @AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class), diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt index 840bf12999..9a336827f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt @@ -45,7 +45,8 @@ data class InstanceEntity( val mastodonApiVersion: Int?, // ToDo: Remove this again when filter v1 support is dropped - @ColumnInfo(defaultValue = "false") val filterV2Supported: Boolean = false + @ColumnInfo(defaultValue = "false") val filterV2Supported: Boolean = false, + val vapidKey: String? ) @TypeConverters(Converters::class) @@ -72,4 +73,5 @@ data class InstanceInfoEntity( val maxFieldValueLength: Int?, val translationEnabled: Boolean?, val mastodonApiVersion: Int?, + val vapidKey: String? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index f75ea34dcb..cde8c16264 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -42,6 +42,7 @@ data class Instance( @JsonClass(generateAdapter = true) data class Configuration( val urls: Urls? = null, + val vapid: VapidKey? = null, val accounts: Accounts? = null, val statuses: Statuses? = null, @Json(name = "media_attachments") val mediaAttachments: MediaAttachments? = null, @@ -51,6 +52,11 @@ data class Instance( @JsonClass(generateAdapter = true) data class Urls(@Json(name = "streaming_api") val streamingApi: String? = null) + @JsonClass(generateAdapter = true) + data class VapidKey( + @Json(name = "public_key") val publicKey: String? = null + ) + @JsonClass(generateAdapter = true) data class Accounts( @Json(name = "max_featured_tags") val maxFeaturedTags: Int, diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 44e3c80bc2..48db67e4b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -675,7 +675,8 @@ interface MastodonApi { suspend fun subscribePushNotifications( @Header("Authorization") auth: String, @Header(DOMAIN_HEADER) domain: String, - @Field("subscription[endpoint]") endPoint: String, + @Field("subscription[standard]") standard: Boolean, + @Field("subscription[endpoint]") endpoint: String, @Field("subscription[keys][p256dh]") keysP256DH: String, @Field("subscription[keys][auth]") keysAuth: String, // The "data[alerts][]" fields to enable / disable notifications diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt index 07f63fbd9b..fe29146889 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt @@ -20,7 +20,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.ApplicationScope import com.keylesspalace.tusky.network.MastodonApi @@ -38,7 +38,7 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() { lateinit var accountManager: AccountManager @Inject - lateinit var notificationService: NotificationService + lateinit var notificationHelper: NotificationHelper @Inject @ApplicationScope @@ -46,7 +46,7 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Build.VERSION.SDK_INT < 28) return - if (!notificationService.arePushNotificationsAvailable()) return + if (!notificationHelper.arePushNotificationsAvailable()) return val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -64,7 +64,7 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() { accountManager.getAccountByIdentifier(accountIdentifier)?.let { account -> if (account.isPushNotificationsEnabled()) { externalScope.launch { - notificationService.updatePushSubscription(account) + notificationHelper.updatePushSubscription(account) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index d504cc153d..ce6d79d128 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -25,7 +25,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.service.SendStatusService @@ -43,20 +43,20 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { @SuppressLint("MissingPermission") override fun onReceive(context: Context, intent: Intent) { - if (intent.action == NotificationService.REPLY_ACTION) { - val serverNotificationId = intent.getStringExtra(NotificationService.KEY_SERVER_NOTIFICATION_ID) - val senderId = intent.getLongExtra(NotificationService.KEY_SENDER_ACCOUNT_ID, -1) + if (intent.action == NotificationHelper.REPLY_ACTION) { + val serverNotificationId = intent.getStringExtra(NotificationHelper.KEY_SERVER_NOTIFICATION_ID) + val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1) val senderIdentifier = intent.getStringExtra( - NotificationService.KEY_SENDER_ACCOUNT_IDENTIFIER + NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER )!! val senderFullName = intent.getStringExtra( - NotificationService.KEY_SENDER_ACCOUNT_FULL_NAME + NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME ) - val citedStatusId = intent.getStringExtra(NotificationService.KEY_CITED_STATUS_ID) + val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID) val visibility = - intent.getSerializableExtraCompat(NotificationService.KEY_VISIBILITY)!! - val spoiler = intent.getStringExtra(NotificationService.KEY_SPOILER).orEmpty() - val mentions = intent.getStringArrayExtra(NotificationService.KEY_MENTIONS).orEmpty() + intent.getSerializableExtraCompat(NotificationHelper.KEY_VISIBILITY)!! + val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER).orEmpty() + val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS).orEmpty() val account = accountManager.getAccountById(senderId) @@ -137,7 +137,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { private fun getReplyMessage(intent: Intent): CharSequence { val remoteInput = RemoteInput.getResultsFromIntent(intent) - return remoteInput?.getCharSequence(NotificationService.KEY_REPLY, "") ?: "" + return remoteInput?.getCharSequence(NotificationHelper.KEY_REPLY, "") ?: "" } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushService.kt similarity index 54% rename from app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt rename to app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushService.kt index bb8feb0436..e2776076de 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushService.kt @@ -15,60 +15,57 @@ package com.keylesspalace.tusky.receiver -import android.content.Context import android.util.Log -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.ApplicationScope -import com.keylesspalace.tusky.network.MastodonApi import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.unifiedpush.android.connector.MessagingReceiver +import org.unifiedpush.android.connector.FailedReason +import org.unifiedpush.android.connector.PushService +import org.unifiedpush.android.connector.data.PushEndpoint +import org.unifiedpush.android.connector.data.PushMessage @AndroidEntryPoint -class UnifiedPushBroadcastReceiver : MessagingReceiver() { +class UnifiedPushService : PushService() { @Inject lateinit var accountManager: AccountManager @Inject - lateinit var mastodonApi: MastodonApi - - @Inject - lateinit var notificationService: NotificationService + lateinit var notificationHelper: NotificationHelper @Inject @ApplicationScope lateinit var applicationScope: CoroutineScope - override fun onMessage(context: Context, message: ByteArray, instance: String) { - Log.d(TAG, "New message received for account $instance: #${message.size}") - val account = accountManager.getAccountById(instance.toLong()) - account?.let { - notificationService.fetchNotificationsOnPushMessage(it) + override fun onMessage(message: PushMessage, instance: String) { + Log.d(TAG, "New message received for account $instance: $message") + accountManager.getAccountById(instance.toLong())?.let { account -> + notificationHelper.fetchNotificationsOnPushMessage(account) } } - override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) { Log.d(TAG, "Endpoint available for account $instance: $endpoint") - accountManager.getAccountById(instance.toLong())?.let { - applicationScope.launch { notificationService.registerPushEndpoint(it, endpoint) } + accountManager.getAccountById(instance.toLong())?.let { account -> + applicationScope.launch { notificationHelper.registerPushEndpoint(account, endpoint) } } } - override fun onRegistrationFailed(context: Context, instance: String) = Unit + override fun onRegistrationFailed(reason: FailedReason, instance: String) = Unit - override fun onUnregistered(context: Context, instance: String) { + override fun onUnregistered(instance: String) { Log.d(TAG, "Endpoint unregistered for account $instance") - accountManager.getAccountById(instance.toLong())?.let { - // It's fine if the account does not exist anymore -- that means it has been logged out - // TODO its not: this is the Mastodon side and should be done (unregistered) - applicationScope.launch { notificationService.unregisterPushEndpoint(it) } + accountManager.getAccountById(instance.toLong())?.let { account -> + // It's fine if the account does not exist anymore -- that means it has been logged out, + // which removes the subscription anyway + applicationScope.launch { notificationHelper.unregisterPushEndpoint(account) } } } companion object { - const val TAG = "UnifiedPushBroadcastReceiver" + const val TAG = "UnifiedPushService" } } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index a1be2406c8..1ff08d9f84 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -55,7 +55,7 @@ object PrefKeys { // each preference a key for it to work. const val SCHEMA_VERSION: String = "schema_version" - const val LAST_USED_PUSH_PROVDER = "lastUsedPushProvider" + const val LAST_USED_PUSH_PROVIDER = "lastUsedPushProvider" const val APP_THEME = "appTheme" const val LANGUAGE = "language" diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt index e8e0db1e33..a946136c72 100644 --- a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt @@ -1,7 +1,7 @@ package com.keylesspalace.tusky.usecase import com.keylesspalace.tusky.components.drafts.DraftHelper -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.DatabaseCleaner import com.keylesspalace.tusky.db.entity.AccountEntity @@ -15,7 +15,7 @@ class LogoutUsecase @Inject constructor( private val accountManager: AccountManager, private val draftHelper: DraftHelper, private val shareShortcutHelper: ShareShortcutHelper, - private val notificationService: NotificationService, + private val notificationHelper: NotificationHelper, ) { /** @@ -35,7 +35,7 @@ class LogoutUsecase @Inject constructor( ) } - notificationService.disableNotificationsForAccount(account) + notificationHelper.disableNotificationsForAccount(account) // remove account from local AccountManager val otherAccountAvailable = accountManager.remove(account) != null diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt deleted file mode 100644 index d2fe7d5706..0000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* Copyright 2022 Tusky contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.util - -import android.util.Base64 -import java.security.KeyPairGenerator -import java.security.SecureRandom -import java.security.Security -import org.bouncycastle.jce.ECNamedCurveTable -import org.bouncycastle.jce.interfaces.ECPrivateKey -import org.bouncycastle.jce.interfaces.ECPublicKey -import org.bouncycastle.jce.provider.BouncyCastleProvider - -object CryptoUtil { - const val CURVE_PRIME256_V1 = "prime256v1" - - private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP - - init { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.addProvider(BouncyCastleProvider()) - } - - private fun secureRandomBytes(len: Int): ByteArray { - val ret = ByteArray(len) - SecureRandom.getInstance("SHA1PRNG").nextBytes(ret) - return ret - } - - fun secureRandomBytesEncoded(len: Int): String { - return Base64.encodeToString(secureRandomBytes(len), BASE64_FLAGS) - } - - data class EncodedKeyPair(val pubkey: String, val privKey: String) - - fun generateECKeyPair(curve: String): EncodedKeyPair { - val spec = ECNamedCurveTable.getParameterSpec(curve) - val gen = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) - gen.initialize(spec) - val keyPair = gen.genKeyPair() - val pubKey = keyPair.public as ECPublicKey - val privKey = keyPair.private as ECPrivateKey - val encodedPubKey = Base64.encodeToString(pubKey.q.getEncoded(false), BASE64_FLAGS) - val encodedPrivKey = Base64.encodeToString(privKey.d.toByteArray(), BASE64_FLAGS) - return EncodedKeyPair(encodedPubKey, encodedPrivKey) - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt index 7534b05b13..60826dfda8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt @@ -25,7 +25,7 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.systemnotifications.NotificationFetcher -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -34,9 +34,9 @@ class NotificationWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted params: WorkerParameters, private val notificationsFetcher: NotificationFetcher, - notificationService: NotificationService, + notificationHelper: NotificationHelper, ) : CoroutineWorker(appContext, params) { - val notification: Notification = notificationService.createWorkerNotification( + val notification: Notification = notificationHelper.createWorkerNotification( R.string.notification_notification_worker ) @@ -47,7 +47,7 @@ class NotificationWorker @AssistedInject constructor( } override suspend fun getForegroundInfo() = ForegroundInfo( - NotificationService.NOTIFICATION_ID_FETCH_NOTIFICATION, + NotificationHelper.NOTIFICATION_ID_FETCH_NOTIFICATION, notification ) diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt index 5c03bdb04b..47267a95f8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt @@ -25,7 +25,7 @@ import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.DatabaseCleaner import com.keylesspalace.tusky.util.deleteStaleCachedMedia @@ -39,9 +39,9 @@ class PruneCacheWorker @AssistedInject constructor( @Assisted workerParams: WorkerParameters, private val databaseCleaner: DatabaseCleaner, private val accountManager: AccountManager, - val notificationService: NotificationService, + val notificationHelper: NotificationHelper, ) : CoroutineWorker(appContext, workerParams) { - val notification: Notification = notificationService.createWorkerNotification( + val notification: Notification = notificationHelper.createWorkerNotification( R.string.notification_prune_cache ) @@ -57,7 +57,7 @@ class PruneCacheWorker @AssistedInject constructor( } override suspend fun getForegroundInfo() = ForegroundInfo( - NotificationService.NOTIFICATION_ID_PRUNE_CACHE, + NotificationHelper.NOTIFICATION_ID_PRUNE_CACHE, notification ) diff --git a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt index f9058f2fb0..8f8c735bef 100644 --- a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt @@ -13,7 +13,7 @@ import androidx.work.testing.WorkManagerTestInitHelper import at.connyduck.calladapter.networkresult.NetworkResult import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.components.accountlist.AccountListActivity -import com.keylesspalace.tusky.components.systemnotifications.NotificationService +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.entity.AccountEntity import com.keylesspalace.tusky.entity.Account @@ -99,7 +99,7 @@ class MainActivityTest { val notificationManager = context.getSystemService(NotificationManager::class.java) val shadowNotificationManager = shadowOf(notificationManager) - val notificationService = NotificationService( + val notificationHelper = NotificationHelper( notificationManager, mock { on { areNotificationsEnabled() } doReturn true @@ -110,10 +110,10 @@ class MainActivityTest { mock(), ) - notificationService.createNotificationChannelsForAccount(accountEntity) + notificationHelper.createNotificationChannelsForAccount(accountEntity) runInBackground { - val notification = notificationService.createBaseNotification( + val notification = notificationHelper.createBaseNotification( Notification( type = type, id = "id", @@ -167,7 +167,7 @@ class MainActivityTest { eventHub = eventHub, accountManager = accountManager, shareShortcutHelper = mock(), - notificationService = mock(), + notificationHelper = mock(), ) val testViewModelFactory = viewModelFactory { initializer { viewModel } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9e4ebd6d8..c3c52f04dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,8 @@ robolectric = "4.14.1" sparkbutton = "4.2.0" touchimageview = "3.7.1" turbine = "1.2.0" -unified-push = "2.4.0" +unified-push-connector = "3.0.9" +unified-push-connector-ui = "1.1.0" xmlwriter = "1.0.4" [plugins] @@ -127,7 +128,8 @@ robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectr sparkbutton = { module = "at.connyduck.sparkbutton:sparkbutton", version.ref = "sparkbutton" } touchimageview = { module = "com.github.MikeOrtiz:TouchImageView", version.ref = "touchimageview" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } -unified-push = { module = "com.github.UnifiedPush:android-connector", version.ref = "unified-push" } +unified-push-connector = { module = "org.unifiedpush.android:connector", version.ref = "unified-push-connector" } +unified-push-connector-ui = { module = "org.unifiedpush.android:connector-ui", version.ref = "unified-push-connector-ui" } xmlwriter = { module = "org.pageseeder.xmlwriter:pso-xmlwriter", version.ref = "xmlwriter" } [bundles] @@ -146,3 +148,4 @@ okhttp = ["okhttp-core", "okhttp-logging-interceptor"] retrofit = ["retrofit-core", "retrofit-converter-moshi"] room = ["androidx-room-ktx", "androidx-room-paging"] xmldiff = ["diffx", "xmlwriter"] +unifiedpush = ["unified-push-connector", "unified-push-connector-ui"] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 500ae4e912..4139686ab7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6387,12 +6387,12 @@ - - - - - - + + + + + + @@ -13068,6 +13068,22 @@ + + + + + + + + + + + + + + + + @@ -14505,6 +14521,14 @@ + + + + + + + + @@ -14573,6 +14597,11 @@ + + + + + @@ -14850,6 +14879,11 @@ + + + + + @@ -14866,6 +14900,14 @@ + + + + + + + + @@ -14900,6 +14942,11 @@ + + + + + @@ -20354,6 +20401,30 @@ + + + + + + + + + + + + + + + + + + + + + + + +