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.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'
|
||||||
|
@ -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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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?
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user