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.activity:activity-ktx:1.3.1"
implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation "androidx.work:work-runtime-ktx:2.5.0" implementation "androidx.work:work-runtime-ktx:2.5.0"
implementation 'androidx.work:work-testing:2.5.0'
// Use the most recent version of CameraX // Use the most recent version of CameraX
def cameraX_version = '1.0.1' 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.PublicFeedStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.hasInternet import org.pixeldroid.app.utils.hasInternet
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
@ -97,6 +98,13 @@ class MainActivity : BaseActivity() {
} }
) )
setupTabs(tabs) setupTabs(tabs)
val showNotification: Boolean = intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false)
if(showNotification){
binding.viewPager.currentItem = 3
}
enablePullNotifications(this) enablePullNotifications(this)
} }
} }

View File

@ -15,6 +15,10 @@ interface NotificationDao: FeedContentDao<Notification> {
ORDER BY datetime(created_at) DESC""") ORDER BY datetime(created_at) DESC""")
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, Notification> 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") @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) 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.db.AppDatabase
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import dagger.Component import dagger.Component
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton import javax.inject.Singleton
@ -16,6 +17,7 @@ interface ApplicationComponent {
fun inject(application: PixelDroidApplication?) fun inject(application: PixelDroidApplication?)
fun inject(activity: BaseActivity?) fun inject(activity: BaseActivity?)
fun inject(feedFragment: BaseFragment) fun inject(feedFragment: BaseFragment)
fun inject(notificationsWorker: NotificationsWorker)
val context: Context? val context: Context?
val application: Application? val application: Application?

View File

@ -5,16 +5,17 @@ import androidx.work.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
fun enablePullNotifications(context: Context) { fun enablePullNotifications(context: Context) {
val workManager = WorkManager.getInstance(context) val workManager = WorkManager.getInstance(context)
workManager.cancelAllWorkByTag("NOTIFICATION_PULL_TAG") val tag = "NOTIFICATION_PULL_TAG"
val workRequest: WorkRequest = PeriodicWorkRequestBuilder<NotificationWorker>( workManager.cancelAllWorkByTag(tag)
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, val workRequest: WorkRequest = PeriodicWorkRequestBuilder<NotificationsWorker>(
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS 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") .build()
.setConstraints( workManager.enqueue(workRequest)
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() }
)
.build()
workManager.enqueue(workRequest)
}

View File

@ -1,71 +1,202 @@
package org.pixeldroid.app.utils.notificationsWorker package org.pixeldroid.app.utils.notificationsWorker
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.work.Worker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R 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, context: Context,
params: WorkerParameters params: WorkerParameters
) : Worker(context, params) { ) : CoroutineWorker(context, params) {
override fun doWork(): Result { @Inject
Log.e("worker", "is working") lateinit var db: AppDatabase
//TODO fetch notifications and create it @Inject
lateinit var apiHolder: PixelfedAPIHolder
createNotificationChannel() override suspend fun doWork(): Result {
// Create an explicit intent for an Activity in your app (applicationContext as PixelDroidApplication).getAppComponent().inject(this)
val intent = Intent(applicationContext, AboutActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 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() return Result.success()
} }
private fun createNotificationChannel() { private fun showNotification(
// Create the NotificationChannel, but only on API 26+ because notification: Notification,
// the NotificationChannel class is new and not in the support library user: UserDatabaseEntity,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { channelIdPrefix: String
val name = "test name" ) {
val descriptionText = "test description" val channelId = channelIdPrefix + (notification.type ?: "other").toString()
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel("TestNotification", name, importance).apply { val builder = NotificationCompat.Builder(applicationContext, channelId)
description = descriptionText .setSmallIcon(
} when (notification.type) {
// Register the channel with the system follow -> R.drawable.ic_follow
val notificationManager: NotificationManager = mention -> R.drawable.mention_at_24dp
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager reblog -> R.drawable.ic_reblog
notificationManager.createNotificationChannel(channel) 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"
}
} }