Reliable (?) notifications
This commit is contained in:
parent
330b009d14
commit
e6910c3ce0
@ -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'
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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?
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user