From e6910c3ce0ead4a193c8dccba1fff557a726c4d1 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 19 Aug 2021 14:39:11 +0200 Subject: [PATCH] Reliable (?) notifications --- app/build.gradle | 1 + .../pixeldroid/app/NotificationWorkerTest.kt | 33 +++ .../java/org/pixeldroid/app/MainActivity.kt | 8 + .../db/dao/feedContent/NotificationDao.kt | 4 + .../app/utils/di/ApplicationComponent.kt | 2 + .../NotificationsFetcher.kt | 25 +- .../NotificationsWorker.kt | 219 ++++++++++++++---- 7 files changed, 236 insertions(+), 56 deletions(-) create mode 100644 app/src/androidTest/java/org/pixeldroid/app/NotificationWorkerTest.kt diff --git a/app/build.gradle b/app/build.gradle index dc1b9b04..347c5041 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -127,6 +127,7 @@ dependencies { implementation "androidx.activity:activity-ktx:1.3.1" implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation "androidx.work:work-runtime-ktx:2.5.0" + implementation 'androidx.work:work-testing:2.5.0' // Use the most recent version of CameraX def cameraX_version = '1.0.1' diff --git a/app/src/androidTest/java/org/pixeldroid/app/NotificationWorkerTest.kt b/app/src/androidTest/java/org/pixeldroid/app/NotificationWorkerTest.kt new file mode 100644 index 00000000..ebaa0af0 --- /dev/null +++ b/app/src/androidTest/java/org/pixeldroid/app/NotificationWorkerTest.kt @@ -0,0 +1,33 @@ +package org.pixeldroid.app + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.work.ListenableWorker +import androidx.work.testing.TestListenableWorkerBuilder +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker + +//TODO actual test here +@RunWith(JUnit4::class) +class NotificationWorkerTest { + private lateinit var context: Context + + @Before + fun setup() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun testNotificationWorker() { + // Get the ListenableWorker + val worker = + TestListenableWorkerBuilder(context).build() // Run the worker synchronously + val result = worker.startWork().get() + MatcherAssert.assertThat(result, CoreMatchers.`is`(ListenableWorker.Result.success())) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/MainActivity.kt b/app/src/main/java/org/pixeldroid/app/MainActivity.kt index c9b144cb..cce50ec2 100644 --- a/app/src/main/java/org/pixeldroid/app/MainActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/MainActivity.kt @@ -46,6 +46,7 @@ import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.hasInternet +import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG import retrofit2.HttpException import java.io.IOException @@ -97,6 +98,13 @@ class MainActivity : BaseActivity() { } ) setupTabs(tabs) + + val showNotification: Boolean = intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false) + + if(showNotification){ + binding.viewPager.currentItem = 3 + } + enablePullNotifications(this) } } diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt index d391af8b..0c498190 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt @@ -15,6 +15,10 @@ interface NotificationDao: FeedContentDao { ORDER BY datetime(created_at) DESC""") override fun feedContent(userId: String, instanceUri: String): PagingSource + @Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri + ORDER BY datetime(created_at) DESC LIMIT 1""") + fun latestNotification(userId: String, instanceUri: String): Notification? + @Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") override suspend fun delete(id: String, userId: String, instanceUri: String) } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt index 3fdff9b7..3a59eec0 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt @@ -7,6 +7,7 @@ import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.BaseFragment import dagger.Component +import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import javax.inject.Singleton @@ -16,6 +17,7 @@ interface ApplicationComponent { fun inject(application: PixelDroidApplication?) fun inject(activity: BaseActivity?) fun inject(feedFragment: BaseFragment) + fun inject(notificationsWorker: NotificationsWorker) val context: Context? val application: Application? diff --git a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsFetcher.kt b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsFetcher.kt index 5520bb4e..53c8f88a 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsFetcher.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsFetcher.kt @@ -5,16 +5,17 @@ import androidx.work.* import java.util.concurrent.TimeUnit fun enablePullNotifications(context: Context) { - val workManager = WorkManager.getInstance(context) - workManager.cancelAllWorkByTag("NOTIFICATION_PULL_TAG") - val workRequest: WorkRequest = PeriodicWorkRequestBuilder( - PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, - PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS + val workManager = WorkManager.getInstance(context) + val tag = "NOTIFICATION_PULL_TAG" + workManager.cancelAllWorkByTag(tag) + val workRequest: WorkRequest = PeriodicWorkRequestBuilder( + PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, + PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS + ) + .addTag(tag) + .setConstraints( + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() ) - .addTag("NOTIFICATION_PULL_TAG") - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() - ) - .build() - workManager.enqueue(workRequest) - } + .build() + workManager.enqueue(workRequest) +} diff --git a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt index 5bcbc361..a9782b7a 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt @@ -1,71 +1,202 @@ package org.pixeldroid.app.utils.notificationsWorker import android.app.NotificationChannel +import android.app.NotificationChannelGroup import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build -import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.work.Worker +import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import org.pixeldroid.app.MainActivity import org.pixeldroid.app.R -import org.pixeldroid.app.settings.AboutActivity +import org.pixeldroid.app.posts.PostActivity +import org.pixeldroid.app.utils.PixelDroidApplication +import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser +import org.pixeldroid.app.utils.api.objects.Notification +import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.* +import org.pixeldroid.app.utils.api.objects.Status +import org.pixeldroid.app.utils.db.AppDatabase +import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import retrofit2.HttpException +import java.io.IOException +import java.time.OffsetDateTime +import java.util.* +import javax.inject.Inject -class NotificationWorker( +class NotificationsWorker( context: Context, params: WorkerParameters -) : Worker(context, params) { +) : CoroutineWorker(context, params) { - override fun doWork(): Result { - Log.e("worker", "is working") - //TODO fetch notifications and create it + @Inject + lateinit var db: AppDatabase + @Inject + lateinit var apiHolder: PixelfedAPIHolder - createNotificationChannel() + override suspend fun doWork(): Result { - // Create an explicit intent for an Activity in your app - val intent = Intent(applicationContext, AboutActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + (applicationContext as PixelDroidApplication).getAppComponent().inject(this) + + val users: List = db.userDao().getAll() + + for (user in users){ + val channelId = user.instance_uri + user.user_id + + createNotificationChannels( + "@${user.username}@${user.instance_uri.removePrefix("https://")}", + channelId + ) + + // Get newest notification from database + var previouslyLatestNotification: Notification? = db.notificationDao().latestNotification(user.user_id, user.instance_uri) + + val api = apiForUser(user, db, apiHolder) + + try { + // Request notifications from server + var newNotifications: List? = api.notifications( + since_id = previouslyLatestNotification?.id + ) + + while (!newNotifications.isNullOrEmpty() + && newNotifications.map { it.created_at ?: OffsetDateTime.MIN } + .maxOrNull()!! > previouslyLatestNotification?.created_at ?: OffsetDateTime.MIN + ) { + // Add to db + val filteredNewNotifications: List = newNotifications.filter { + it.created_at ?: OffsetDateTime.MIN > previouslyLatestNotification?.created_at ?: OffsetDateTime.MIN + }.map { + it.copy(user_id = user.user_id, instance_uri = user.instance_uri) + }.sortedBy { it.created_at } + + db.notificationDao().insertAll(filteredNewNotifications) + + // Launch new notifications + filteredNewNotifications.forEach { + showNotification(it, user, channelId) + } + + previouslyLatestNotification = + filteredNewNotifications.maxByOrNull { it.created_at ?: OffsetDateTime.MIN } + + // Request again + newNotifications = api.notifications( + since_id = previouslyLatestNotification?.id + ) + } + } catch (exception: IOException) { + return Result.failure() + } catch (exception: HttpException) { + return Result.failure() + } } - val pendingIntent: PendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0) - - val builder = NotificationCompat.Builder(applicationContext, "TestNotification") - .setSmallIcon(R.drawable.explore_24dp) - .setContentTitle("My notification") - .setContentText("Much longer text that cannot fit one line...") - .setStyle(NotificationCompat.BigTextStyle() - .bigText("Much longer text that cannot fit one line...")) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - // Set the intent that will fire when the user taps the notification - .setContentIntent(pendingIntent) - .setAutoCancel(true) - - with(NotificationManagerCompat.from(applicationContext)) { - // notificationId is a unique int for each notification that you must define - notify(420, builder.build()) - } - return Result.success() } - private fun createNotificationChannel() { - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "test name" - val descriptionText = "test description" - val importance = NotificationManager.IMPORTANCE_DEFAULT - val channel = NotificationChannel("TestNotification", name, importance).apply { - description = descriptionText - } - // Register the channel with the system - val notificationManager: NotificationManager = - applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) + private fun showNotification( + notification: Notification, + user: UserDatabaseEntity, + channelIdPrefix: String + ) { + val channelId = channelIdPrefix + (notification.type ?: "other").toString() + + val builder = NotificationCompat.Builder(applicationContext, channelId) + .setSmallIcon( + when (notification.type) { + follow -> R.drawable.ic_follow + mention -> R.drawable.mention_at_24dp + reblog -> R.drawable.ic_reblog + favourite -> R.drawable.ic_like_full + comment -> R.drawable.ic_comment_empty + poll -> R.drawable.poll + null -> R.drawable.ic_comment_empty + } + ) + .setContentTitle( + notification.account?.username?.let { username -> + applicationContext.getString( + when (notification.type) { + follow -> R.string.followed_notification + comment -> R.string.comment_notification + mention -> R.string.mention_notification + reblog -> R.string.shared_notification + favourite -> R.string.liked_notification + poll -> R.string.poll_notification + null -> R.string.other_notification + } + ).format(username) + } + ) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + // Set the intent that will fire when the user taps the notification + .setContentIntent( + PendingIntent.getActivity(applicationContext, 0, when (notification.type) { + mention -> notification.status?.let { + Intent(applicationContext, PostActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(Status.POST_TAG, notification.status) + putExtra(Status.VIEW_COMMENTS_TAG, true) + } + } ?: Intent(applicationContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(SHOW_NOTIFICATION_TAG, true) + } + else -> Intent(applicationContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(SHOW_NOTIFICATION_TAG, true) + } + }, PendingIntent.FLAG_IMMUTABLE)) + .setAutoCancel(true) + + if (notification.type == mention || notification.type == comment){ + builder.setContentText(notification.status?.content) + } + //TODO poll -> TODO() + + with(NotificationManagerCompat.from(applicationContext)) { + // notificationId is a unique int for each notification + notify((user.instance_uri + user.user_id + notification.id).hashCode(), builder.build()) } } + private fun createNotificationChannels(handle: String, channelId: String) { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // The id of the group. + val notificationManager: NotificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannelGroup(NotificationChannelGroup(channelId, handle)) + + val importance = NotificationManager.IMPORTANCE_DEFAULT + + val followsChannel = NotificationChannel(channelId + follow.toString(), "New followers", importance).apply { group = channelId } + val mentionChannel = NotificationChannel(channelId + mention.toString(), "Mentions", importance).apply { group = channelId } + val sharesChannel = NotificationChannel(channelId + reblog.toString(), "Shares", importance).apply { group = channelId } + val likesChannel = NotificationChannel(channelId + favourite.toString(), "Likes", importance).apply { group = channelId } + val commentsChannel = NotificationChannel(channelId + comment.toString(), "Comments", importance).apply { group = channelId } + val pollsChannel = NotificationChannel(channelId + poll.toString(), "Polls", importance).apply { group = channelId } + val othersChannel = NotificationChannel(channelId + "other", "Other", importance).apply { group = channelId } + + // Register the channels with the system + notificationManager.createNotificationChannel(followsChannel) + notificationManager.createNotificationChannel(mentionChannel) + notificationManager.createNotificationChannel(sharesChannel) + notificationManager.createNotificationChannel(likesChannel) + notificationManager.createNotificationChannel(commentsChannel) + notificationManager.createNotificationChannel(pollsChannel) + notificationManager.createNotificationChannel(othersChannel) + } + } + + companion object { + const val SHOW_NOTIFICATION_TAG = "SHOW_NOTIFICATION" + } + } \ No newline at end of file