Reliable (?) notifications

This commit is contained in:
Matthieu 2021-08-19 14:39:11 +02:00
parent 330b009d14
commit e6910c3ce0
7 changed files with 236 additions and 56 deletions

View File

@ -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'

View File

@ -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<NotificationsWorker>(context).build() // Run the worker synchronously
val result = worker.startWork().get()
MatcherAssert.assertThat(result, CoreMatchers.`is`(ListenableWorker.Result.success()))
}
}

View File

@ -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)
}
}

View File

@ -15,6 +15,10 @@ interface NotificationDao: FeedContentDao<Notification> {
ORDER BY datetime(created_at) DESC""")
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, Notification>
@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)
}

View File

@ -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?

View File

@ -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<NotificationWorker>(
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<NotificationsWorker>(
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)
}

View File

@ -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<UserDatabaseEntity> = 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<Notification>? = 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<Notification> = 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"
}
}