Improve push notifications (#4896)

Besides the refactoring these improvements:
* Track last push distributor and reset settings and subscription on any
incompatible change (ie. uninstall)
* Only update (push) notification settings on server if needed
* Allow to only fetch notifications for one account (the one for which a
push message was received)

This is (also) the revival of
https://github.com/tuskyapp/Tusky/pull/3642

It's not really well tested so far. (Ie. with two or more accounts or
two or more push providers.)
This commit is contained in:
UlrichKu 2025-02-20 12:27:06 +01:00 committed by GitHub
commit 6450af6edb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 274 additions and 127 deletions

View file

@ -27,6 +27,7 @@ import com.keylesspalace.tusky.appstore.NewNotificationsEvent
import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
@ -97,10 +98,10 @@ class MainViewModel @Inject constructor(
shareShortcutHelper.updateShortcuts()
setupNotifications()
setupNotifications(activeAccount)
},
{ throwable ->
Log.w(TAG, "Failed to fetch user info.", throwable)
Log.e(TAG, "Failed to fetch user info.", throwable)
}
)
}
@ -160,15 +161,24 @@ class MainViewModel @Inject constructor(
}
}
fun setupNotifications() {
notificationService.createNotificationChannelsForAccount(activeAccount)
fun setupNotifications(account: AccountEntity? = null) {
// TODO this is only called on full app (re) start; so changes in-between (push distributor uninstalled/subscription changed, or
// notifications fully disabled) will get unnoticed; and also an app restart cannot be easily triggered by the user.
if (notificationService.areNotificationsEnabled()) {
if (account != null) {
// TODO it's quite odd to separate channel creation (for an account) from the "is enabled by channels" question below
notificationService.createNotificationChannelsForAccount(account)
}
if (notificationService.areNotificationsEnabledBySystem()) {
viewModelScope.launch {
notificationService.enablePushNotificationsWithFallback()
notificationService.setupNotifications(account)
}
} else {
notificationService.disableAllNotifications()
viewModelScope.launch {
notificationService.disableAllNotifications()
}
}
}

View file

@ -68,13 +68,19 @@ class TuskyApplication : Application(), Configuration.Provider {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
val workManager = WorkManager.getInstance(this)
// Migrate shared preference keys and defaults from version to version.
val oldVersion = preferences.getInt(
PrefKeys.SCHEMA_VERSION,
NEW_INSTALL_SCHEMA_VERSION
)
if (oldVersion != SCHEMA_VERSION) {
// TODO SCHEMA_VERSION is outdated / not updated in code
if (oldVersion < 2025021701) {
// A new periodic work request is enqueued by unique name (and not tag anymore): stop the old one
workManager.cancelAllWorkByTag("pullNotifications")
}
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
}
@ -93,7 +99,7 @@ class TuskyApplication : Application(), Configuration.Provider {
val pruneCacheWorker = PeriodicWorkRequestBuilder<PruneCacheWorker>(12, TimeUnit.HOURS)
.setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build())
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
workManager.enqueueUniquePeriodicWork(
PruneCacheWorker.PERIODIC_WORK_TAG,
ExistingPeriodicWorkPolicy.KEEP,
pruneCacheWorker

View file

@ -40,7 +40,6 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val activeAccount = accountManager.activeAccount ?: return
val context = requireContext()
makePreferenceScreen {
switchPreference {
setTitle(R.string.pref_title_notifications_enabled)
@ -49,7 +48,7 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
isChecked = activeAccount.notificationsEnabled
setOnPreferenceChangeListener { _, newValue ->
updateAccount { copy(notificationsEnabled = newValue as Boolean) }
if (notificationService.areNotificationsEnabled()) {
if (notificationService.areNotificationsEnabledBySystem()) {
notificationService.enablePullNotifications()
} else {
notificationService.disablePullNotifications()

View file

@ -45,8 +45,12 @@ class NotificationFetcher @Inject constructor(
private val eventHub: EventHub,
private val notificationService: NotificationService,
) {
suspend fun fetchAndShow() {
suspend fun fetchAndShow(accountId: Long?) {
for (account in accountManager.accounts) {
if (accountId != null && account.id != accountId) {
continue
}
if (account.notificationsEnabled) {
try {
val notifications = fetchNewNotifications(account)
@ -137,7 +141,7 @@ class NotificationFetcher @Inject constructor(
// Save the newest notification ID in the marker.
notifications.firstOrNull()?.let {
val newMarkerId = notifications.first().id
Log.d(TAG, "updating notification marker for ${account.fullName} to: $newMarkerId")
Log.d(TAG, "Updating notification marker for ${account.fullName} to: $newMarkerId")
mastodonApi.updateMarkersWithAuth(
auth = authHeader,
domain = account.domain,

View file

@ -7,6 +7,7 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.os.Build
@ -23,12 +24,14 @@ import androidx.core.app.NotificationManagerCompat.NotificationWithIdAndTag
import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkRequest
import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.onFailure
import at.connyduck.calladapter.networkresult.onSuccess
import com.bumptech.glide.Glide
@ -42,9 +45,12 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.NotificationSubscribeResult
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
@ -56,24 +62,31 @@ import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.unifiedpush.android.connector.UnifiedPush
import retrofit2.HttpException
@Singleton
class NotificationService @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 var workManager: WorkManager = WorkManager.getInstance(context)
private var notificationId: Int = NOTIFICATION_ID_PRUNE_CACHE + 1
init {
createWorkerNotificationChannel()
}
fun areNotificationsEnabled(): Boolean {
fun areNotificationsEnabledBySystem(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// on Android >= O, notifications are enabled, if at least one channel is enabled
@ -94,6 +107,35 @@ class NotificationService @Inject constructor(
}
}
suspend fun setupNotifications(account: AccountEntity?) {
resetPushWhenDistributorIsMissing()
if (arePushNotificationsAvailable()) {
setupPushNotifications(account)
}
// At least as a fallback and otherwise as main source when there are no push distributors installed:
enablePullNotifications()
}
fun enablePullNotifications() {
val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(
NotificationWorker::class.java,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS,
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
workManager.enqueueUniquePeriodicWork(NOTIFICATION_PULL_NAME, ExistingPeriodicWorkPolicy.KEEP, workRequest)
Log.d(TAG, "Enabled pull checks with ${PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS / 60000} minutes interval.")
}
fun createNotificationChannelsForAccount(account: AccountEntity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
data class ChannelData(
@ -102,7 +144,6 @@ class NotificationService @Inject constructor(
@StringRes val description: Int,
)
// TODO REBLOG and esp. STATUS have very different names than the type itself.
val channelData = arrayOf(
ChannelData(
getChannelId(account, Notification.Type.MENTION)!!,
@ -175,44 +216,28 @@ class NotificationService @Inject constructor(
}
}
fun deleteNotificationChannelsForAccount(account: AccountEntity) {
private fun deleteNotificationChannelsForAccount(account: AccountEntity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.deleteNotificationChannelGroup(account.identifier)
}
}
fun enablePullNotifications() {
val workManager = WorkManager.getInstance(context)
workManager.cancelAllWorkByTag(NOTIFICATION_PULL_TAG)
// Periodic work requests are supposed to start running soon after being enqueued. In
// practice that may not be soon enough, so create and enqueue an expedited one-time
// request to get new notifications immediately.
val fetchNotifications: WorkRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
private fun enqueueOneTimeWorker(account: AccountEntity?) {
val oneTimeRequestBuilder = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
workManager.enqueue(fetchNotifications)
val workRequest: WorkRequest = PeriodicWorkRequest.Builder(
NotificationWorker::class.java,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
TimeUnit.MILLISECONDS,
)
.addTag(NOTIFICATION_PULL_TAG)
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
account?.let {
val data = Data.Builder()
data.putLong(NotificationWorker.KEY_ACCOUNT_ID, account.id)
oneTimeRequestBuilder.setInputData(data.build())
}
workManager.enqueue(workRequest)
Log.d(TAG, "Enabled pull checks with " + PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS + "ms interval.")
workManager.enqueue(oneTimeRequestBuilder.build())
}
fun disablePullNotifications() {
WorkManager.getInstance(context).cancelAllWorkByTag(NOTIFICATION_PULL_TAG)
workManager.cancelUniqueWork(NOTIFICATION_PULL_NAME)
Log.d(TAG, "Disabled pull checks.")
}
@ -265,7 +290,7 @@ class NotificationService @Inject constructor(
val summary = createSummaryNotification(account, type, notificationsForOneType) ?: continue
// NOTE Enqueue the summary first: Needed to avoid rate limit problems:
// ie. single notification is enqueued but that later summary one is filtered and thus no grouping
// ie. single notification is enqueued but later the summary one is filtered and thus no grouping
// takes place.
newNotifications.add(summary)
@ -388,7 +413,7 @@ class NotificationService @Inject constructor(
/**
* Create a notification that summarises the other notifications in this group.
*
* NOTE: We always create a summary notification (even for activeNotificationsForType.size() == 1):
* NOTE: We always create a summary notification (even for only one notification of that type):
* - No need to especially track the grouping
* - No need to change an existing single notification when there arrives another one of its group
* - Only the summary one will get announced
@ -737,78 +762,162 @@ class NotificationService @Inject constructor(
}
}
fun disableAllNotifications() {
suspend fun disableAllNotifications() {
disablePushNotificationsForAllAccounts()
disablePullNotifications()
}
suspend fun disableNotificationsForAccount(account: AccountEntity) {
disablePushNotificationsForAccount(account)
deleteNotificationChannelsForAccount(account)
if (!areNotificationsEnabledBySystem()) {
// TODO this is sort of a hack, it means: are there now no active accounts?
disablePullNotifications()
}
}
//
// Push notification section
//
fun isUnifiedPushAvailable(): Boolean =
fun arePushNotificationsAvailable(): Boolean =
UnifiedPush.getDistributors(context).isNotEmpty()
suspend fun enablePushNotificationsWithFallback() {
if (!isUnifiedPushAvailable()) {
// No distributors
enablePullNotifications()
return
private suspend fun setupPushNotifications(account: AccountEntity?) {
val relevantAccounts: List<AccountEntity> = if (account != null) {
listOf(account)
} else {
accountManager.accounts
}
accountManager.accounts.forEach {
relevantAccounts.forEach {
val notificationGroupEnabled = Build.VERSION.SDK_INT < 28 ||
notificationManager.getNotificationChannelGroup(it.identifier)?.isBlocked == false
val shouldEnable = it.notificationsEnabled && notificationGroupEnabled
if (shouldEnable) {
enableUnifiedPushNotificationsForAccount(it)
setupPushNotificationsForAccount(it)
Log.d(TAG, "Enabled push notifications for account ${it.id}.")
} else {
disableUnifiedPushNotificationsForAccount(it)
disablePushNotificationsForAccount(it)
Log.d(TAG, "Disabled push notifications for account ${it.id}.")
}
}
}
private suspend fun enableUnifiedPushNotificationsForAccount(account: AccountEntity) {
if (account.isPushNotificationsEnabled()) {
// Already registered, update the subscription to match notification settings
updateUnifiedPushSubscription(account)
private suspend fun setupPushNotificationsForAccount(account: AccountEntity) {
val currentSubscription = getActiveSubscription(account)
if (currentSubscription != null) {
val alertData = buildAlertsMap(account)
if (alertData != currentSubscription.alerts) {
// Update the subscription to match notification settings
updatePushSubscription(account)
} else {
Log.d(TAG, "Nothing to be done. Current push subscription matches for account ${account.id}.")
}
} else {
UnifiedPush.registerAppWithDialog(
context,
account.id.toString(),
features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE)
)
Log.d(TAG, "Trying to create a UnifiedPush subscription for account ${account.id}")
// When changing the local UP distributor this is necessary first to enable the following callbacks (i. e. onNewEndpoint);
// make sure this is done in any inconsistent case (is not too often and doesn't hurt).
unregisterPushEndpoint(account)
UnifiedPush.registerAppWithDialog(context, account.id.toString(), features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE))
// Will lead to call of registerPushEndpoint()
}
}
private fun disablePushNotificationsForAllAccounts() {
accountManager.accounts.forEach {
disableUnifiedPushNotificationsForAccount(it)
}
}
private fun resetPushWhenDistributorIsMissing() {
val lastUsedPushProvider = preferences.getString(PrefKeys.LAST_USED_PUSH_PROVDER, null)
// NOTE UnifiedPush.getSavedDistributor() cannot be used here as that is already null here if the
// distributor was uninstalled.
fun disableUnifiedPushNotificationsForAccount(account: AccountEntity) {
if (!account.isPushNotificationsEnabled()) {
// Not registered
if (lastUsedPushProvider.isNullOrEmpty() || UnifiedPush.getDistributors(context).contains(lastUsedPushProvider)) {
return
}
Log.w(TAG, "Previous push provider ($lastUsedPushProvider) uninstalled. Resetting all accounts.")
val editor = preferences.edit()
editor.remove(PrefKeys.LAST_USED_PUSH_PROVDER)
editor.apply()
applicationScope.launch {
accountManager.accounts.forEach {
// reset all accounts, also does resetPushSettingsInAccount()
unregisterPushEndpoint(it)
}
}
}
private suspend fun getActiveSubscription(account: AccountEntity): NotificationSubscribeResult? {
api.pushNotificationSubscription(
"Bearer ${account.accessToken}",
account.domain
).fold(
onSuccess = {
if (!account.matchesPushSubscription(it.endpoint)) {
Log.w(TAG, "Server push endpoint does not match previously registered one: ${it.endpoint} vs. ${account.unifiedPushUrl}")
return null
}
return it
},
onFailure = { throwable ->
if (throwable is HttpException && throwable.code() == 404) {
// this is alright; there is no subscription on the server
return null
}
Log.e(TAG, "Cannot get push subscription for account " + account.id + ": " + throwable.message, throwable)
return null
}
)
}
private suspend fun disablePushNotificationsForAllAccounts() {
accountManager.accounts.forEach {
disablePushNotificationsForAccount(it)
}
}
private suspend fun disablePushNotificationsForAccount(account: AccountEntity) {
if (!account.isPushNotificationsEnabled()) {
return
}
unregisterPushEndpoint(account)
// this probably does nothing (distributor to handle this is missing)
UnifiedPush.unregisterApp(context, account.id.toString())
}
private fun buildSubscriptionData(account: AccountEntity): Map<String, Boolean> =
fun fetchNotificationsOnPushMessage(account: AccountEntity) {
// TODO should there be a rate limit here? Ie. we could be silent (can we?) for another notification in a short timeframe.
Log.d(TAG, "Fetching notifications because of push for account ${account.id}")
enqueueOneTimeWorker(account)
}
private fun buildAlertsMap(account: AccountEntity): Map<String, Boolean> =
buildMap {
Notification.Type.visibleTypes.forEach {
put(
"data[alerts][${it.presentation}]",
filterNotification(account, it)
)
put(it.presentation, filterNotification(account, it))
}
}
private fun buildAlertSubscriptionData(account: AccountEntity): Map<String, Boolean> =
buildAlertsMap(account).mapKeys { "data[alerts][${it.key}]" }
// Called by UnifiedPush callback in UnifiedPushBroadcastReceiver
suspend fun registerUnifiedPushEndpoint(
suspend fun registerPushEndpoint(
account: AccountEntity,
endpoint: String
) = withContext(Dispatchers.IO) {
@ -817,7 +926,6 @@ class NotificationService @Inject constructor(
// 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
// TODO that is still correct?
val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1)
val auth = CryptoUtil.secureRandomBytesEncoded(16)
@ -827,10 +935,10 @@ class NotificationService @Inject constructor(
endpoint,
keyPair.pubkey,
auth,
buildSubscriptionData(account)
buildAlertSubscriptionData(account)
).onFailure { throwable ->
Log.w(TAG, "Error setting push endpoint for account ${account.id}", throwable)
disableUnifiedPushNotificationsForAccount(account)
disablePushNotificationsForAccount(account)
}.onSuccess {
Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}")
@ -843,26 +951,38 @@ class NotificationService @Inject constructor(
unifiedPushUrl = endpoint
)
}
UnifiedPush.getAckDistributor(context)?.let {
Log.d(TAG, "Saving distributor to preferences: $it")
val editor = preferences.edit()
editor.putString(PrefKeys.LAST_USED_PUSH_PROVDER, it)
editor.apply()
// TODO once this is selected it cannot be changed (except by wiping the application or uninstalling the provider)
}
}
}
// Synchronize the enabled / disabled state of notifications with server-side subscription
suspend fun updateUnifiedPushSubscription(account: AccountEntity) {
// Synchronize the enabled / disabled state of notifications with server-side subscription (also NotificationBlockStateBroadcastReceiver).
suspend fun updatePushSubscription(account: AccountEntity) {
withContext(Dispatchers.IO) {
api.updatePushNotificationSubscription(
"Bearer ${account.accessToken}",
account.domain,
buildSubscriptionData(account)
buildAlertSubscriptionData(account)
).onSuccess {
Log.d(TAG, "UnifiedPush subscription updated for account ${account.id}")
accountManager.updateAccount(account) {
copy(pushServerKey = it.serverKey)
}
}.onFailure { throwable ->
Log.e(TAG, "Could not update subscription ${throwable.message}")
}
}
}
suspend fun unregisterUnifiedPushEndpoint(account: AccountEntity) {
suspend fun unregisterPushEndpoint(account: AccountEntity) {
withContext(Dispatchers.IO) {
api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain)
.onFailure { throwable ->
@ -870,20 +990,23 @@ class NotificationService @Inject constructor(
}
.onSuccess {
Log.d(TAG, "UnifiedPush unregistration succeeded for account " + account.id)
// Clear the URL in database
accountManager.updateAccount(account) {
copy(
pushPubKey = "",
pushPrivKey = "",
pushAuth = "",
pushServerKey = "",
unifiedPushUrl = ""
)
}
resetPushSettingsInAccount(account)
}
}
}
private suspend fun resetPushSettingsInAccount(account: AccountEntity) {
accountManager.updateAccount(account) {
copy(
pushPubKey = "",
pushPrivKey = "",
pushAuth = "",
pushServerKey = "",
unifiedPushUrl = ""
)
}
}
companion object {
const val TAG = "NotificationService"
@ -905,6 +1028,6 @@ class NotificationService @Inject constructor(
private const val EXTRA_ACCOUNT_NAME = BuildConfig.APPLICATION_ID + ".notification.extra.account_name"
private const val EXTRA_NOTIFICATION_TYPE = BuildConfig.APPLICATION_ID + ".notification.extra.notification_type"
private const val GROUP_SUMMARY_TAG = BuildConfig.APPLICATION_ID + ".notification.group_summary"
private const val NOTIFICATION_PULL_TAG = "pullNotifications"
private const val NOTIFICATION_PULL_NAME = "pullNotifications"
}
}

View file

@ -130,4 +130,8 @@ data class AccountEntity(
fun isPushNotificationsEnabled(): Boolean {
return unifiedPushUrl.isNotEmpty()
}
fun matchesPushSubscription(endpoint: String): Boolean {
return unifiedPushUrl == endpoint
}
}

View file

@ -22,5 +22,6 @@ import com.squareup.moshi.JsonClass
data class NotificationSubscribeResult(
val id: Int,
val endpoint: String,
val alerts: Map<String, Boolean>,
@Json(name = "server_key") val serverKey: String
)

View file

@ -666,6 +666,12 @@ interface MastodonApi {
@Field("comment") note: String
): NetworkResult<Relationship>
@GET("api/v1/push/subscription")
suspend fun pushNotificationSubscription(
@Header("Authorization") auth: String,
@Header(DOMAIN_HEADER) domain: String
): NetworkResult<NotificationSubscribeResult>
@FormUrlEncoded
@POST("api/v1/push/subscription")
suspend fun subscribePushNotifications(

View file

@ -46,11 +46,11 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Build.VERSION.SDK_INT < 28) return
if (!notificationService.isUnifiedPushAvailable()) return
if (!notificationService.arePushNotificationsAvailable()) return
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val gid = when (intent.action) {
val accountIdentifier = when (intent.action) {
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED -> {
val channelId = intent.getStringExtra(NotificationManager.EXTRA_NOTIFICATION_CHANNEL_ID)
nm.getNotificationChannel(channelId).group
@ -61,10 +61,10 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
else -> null
} ?: return
accountManager.getAccountByIdentifier(gid)?.let { account ->
accountManager.getAccountByIdentifier(accountIdentifier)?.let { account ->
if (account.isPushNotificationsEnabled()) {
externalScope.launch {
notificationService.updateUnifiedPushSubscription(account)
notificationService.updatePushSubscription(account)
}
}
}

View file

@ -17,13 +17,10 @@ package com.keylesspalace.tusky.receiver
import android.content.Context
import android.util.Log
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.worker.NotificationWorker
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@ -43,19 +40,20 @@ class UnifiedPushBroadcastReceiver : MessagingReceiver() {
@Inject
@ApplicationScope
lateinit var externalScope: CoroutineScope
lateinit var applicationScope: CoroutineScope
override fun onMessage(context: Context, message: ByteArray, instance: String) {
Log.d(TAG, "New message received for account $instance")
val workManager = WorkManager.getInstance(context)
val request = OneTimeWorkRequest.from(NotificationWorker::class.java)
workManager.enqueue(request)
Log.d(TAG, "New message received for account $instance: #${message.size}")
val account = accountManager.getAccountById(instance.toLong())
account?.let {
notificationService.fetchNotificationsOnPushMessage(it)
}
}
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
Log.d(TAG, "Endpoint available for account $instance: $endpoint")
accountManager.getAccountById(instance.toLong())?.let {
externalScope.launch { notificationService.registerUnifiedPushEndpoint(it, endpoint) }
applicationScope.launch { notificationService.registerPushEndpoint(it, endpoint) }
}
}
@ -65,11 +63,12 @@ class UnifiedPushBroadcastReceiver : MessagingReceiver() {
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
externalScope.launch { notificationService.unregisterUnifiedPushEndpoint(it) }
// TODO its not: this is the Mastodon side and should be done (unregistered)
applicationScope.launch { notificationService.unregisterPushEndpoint(it) }
}
}
companion object {
const val TAG = "UnifiedPush"
const val TAG = "UnifiedPushBroadcastReceiver"
}
}

View file

@ -45,7 +45,7 @@ enum class AppTheme(val value: String) {
*
* - Adding a new preference that does not change the interpretation of an existing preference
*/
const val SCHEMA_VERSION = 2023112001
const val SCHEMA_VERSION = 2025021701
/** The schema version for fresh installs */
const val NEW_INSTALL_SCHEMA_VERSION = 0
@ -55,6 +55,8 @@ 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 APP_THEME = "appTheme"
const val LANGUAGE = "language"
const val STATUS_TEXT_SIZE = "statusTextSize"

View file

@ -1,6 +1,5 @@
package com.keylesspalace.tusky.usecase
import android.content.Context
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.db.AccountManager
@ -8,11 +7,9 @@ import com.keylesspalace.tusky.db.DatabaseCleaner
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ShareShortcutHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class LogoutUsecase @Inject constructor(
@ApplicationContext private val context: Context,
private val api: MastodonApi,
private val databaseCleaner: DatabaseCleaner,
private val accountManager: AccountManager,
@ -38,17 +35,7 @@ class LogoutUsecase @Inject constructor(
)
}
// disable push notifications
notificationService.disableUnifiedPushNotificationsForAccount(account)
// disable pull notifications
if (!notificationService.areNotificationsEnabled()) {
// TODO this is working very wrong
notificationService.disablePullNotifications()
}
// clear notification channels
notificationService.deleteNotificationChannelsForAccount(account)
notificationService.disableNotificationsForAccount(account)
// remove account from local AccountManager
val otherAccountAvailable = accountManager.remove(account) != null

View file

@ -29,7 +29,6 @@ import com.keylesspalace.tusky.components.systemnotifications.NotificationServic
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
/** Fetch and show new notifications. */
@HiltWorker
class NotificationWorker @AssistedInject constructor(
@Assisted appContext: Context,
@ -42,7 +41,8 @@ class NotificationWorker @AssistedInject constructor(
)
override suspend fun doWork(): Result {
notificationsFetcher.fetchAndShow()
val accountId = inputData.getLong(KEY_ACCOUNT_ID, 0).takeIf { it != 0L }
notificationsFetcher.fetchAndShow(accountId)
return Result.success()
}
@ -50,4 +50,8 @@ class NotificationWorker @AssistedInject constructor(
NotificationService.NOTIFICATION_ID_FETCH_NOTIFICATION,
notification
)
companion object {
const val KEY_ACCOUNT_ID = "accountId"
}
}