Push notifications support via UnifiedPush (#2303)

Fixes #793.

This is an implementation for push notifications based on UnifiedPush
for Tusky. No push gateway (other than UP itself) is needed, since
UnifiedPush is simple enough such that it can act as a catch-all
endpoint for WebPush messages. When a UnifiedPush distributor is present
on-device, we will by default register Tusky as a receiver; if no
UnifiedPush distributor is available, then pull notifications are used
as a fallback mechanism.

Because WebPush messages are encrypted, and Mastodon does not send the
keys and IV needed for decryption in the request body, for now the push
handler simply acts as a trigger for the pre-existing NotificationWorker
which is also used for pull notifications. Nevertheless, I have
implemented proper key generation and storage, just in case we would
like to implement full decryption support in the future when Mastodon
upgrades to the latest WebPush encryption scheme that includes all
information in the request body.

For users with existing accounts, push notifications will not be enabled
until all of the accounts have been re-logged in to grant the new push
OAuth scope. A small prompt will be shown (until dismissed) as a
Snackbar to explain to the user about this, and an option is added in
Account Preferences to facilitate re-login without deleting local drafts
and cache.
This commit is contained in:
Peter Cai 2022-05-17 13:32:09 -04:00 committed by GitHub
commit 9ec5d6e3b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1490 additions and 24 deletions

View file

@ -65,6 +65,10 @@ import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.components.notifications.disableAllNotifications
import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount
import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
import com.keylesspalace.tusky.components.search.SearchActivity
@ -242,12 +246,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setupTabs(showNotificationTab)
// Setup push notifications
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
NotificationHelper.enablePullNotifications(this)
} else {
NotificationHelper.disablePullNotifications(this)
}
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
@ -636,7 +634,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
// open LoginActivity to add new account
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true))
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN))
return false
}
// change Account
@ -666,6 +664,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
lifecycleScope.launch {
// Only disable UnifiedPush for this account -- do not call disableNotifications(),
// which unnecessarily disables it for all accounts and then re-enables it again at
// the next launch
disableUnifiedPushNotificationsForAccount(this@MainActivity, activeAccount)
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
cacheUpdater.clearForUser(activeAccount.id)
conversationRepository.deleteCacheForAccount(activeAccount.id)
@ -680,7 +682,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
NotificationHelper.disablePullNotifications(this@MainActivity)
}
val intent = if (newAccount == null) {
LoginActivity.getIntent(this@MainActivity, false)
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
} else {
Intent(this@MainActivity, MainActivity::class.java)
}
@ -714,6 +716,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
// Setup push notifications
showMigrationNoticeIfNecessary(this, binding.root, accountManager)
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
lifecycleScope.launch {
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
}
} else {
disableAllNotifications(this, accountManager)
}
accountLocked = me.locked
updateProfiles()