feat: AlarmManager option for notifications

This commit is contained in:
rebelonion 2024-03-18 23:51:00 -05:00
parent deeefb8e35
commit 9471683501
17 changed files with 822 additions and 432 deletions

View file

@ -16,6 +16,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
@ -314,6 +315,22 @@
<action android:name="Aani.dantotsu.ACTION_ALARM" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.AlarmPermissionStateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.BootCompletedReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/>
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/>
<meta-data
android:name="preloaded_fonts"

View file

@ -6,15 +6,14 @@ import android.content.Context
import android.os.Bundle
import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication
import androidx.work.Constraints
import androidx.work.OneTimeWorkRequest
import androidx.work.PeriodicWorkRequest
import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
@ -127,49 +126,16 @@ class App : MultiDexApplication() {
}
private fun startWorkers() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(androidx.work.NetworkType.CONNECTED)
.build()
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
// CommentNotificationWorker
val commentInterval = CommentNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.CommentNotificationInterval)]
if (commentInterval.toInt() != 0) {
val recurringWork = PeriodicWorkRequest.Builder(
CommentNotificationWorker::class.java,
commentInterval, java.util.concurrent.TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
androidx.work.WorkManager.getInstance(this).enqueueUniquePeriodicWork(
CommentNotificationWorker.WORK_NAME,
androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
recurringWork
)
} else {
androidx.work.WorkManager.getInstance(this).cancelUniqueWork(CommentNotificationWorker.WORK_NAME)
//run once
androidx.work.WorkManager.getInstance(this).enqueue(OneTimeWorkRequest.Companion.from(
CommentNotificationWorker::class.java))
}
TaskScheduler.create(this, useAlarmManager).scheduleAllTasks(this)
androidx.work.WorkManager.getInstance(this)
.enqueue(OneTimeWorkRequest.Companion.from(CommentNotificationWorker::class.java))
androidx.work.WorkManager.getInstance(this)
.enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
// AnilistNotificationWorker
val anilistInterval = AnilistNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
if (anilistInterval.toInt() != 0) {
val recurringWork = PeriodicWorkRequest.Builder(
AnilistNotificationWorker::class.java,
anilistInterval, java.util.concurrent.TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
androidx.work.WorkManager.getInstance(this).enqueueUniquePeriodicWork(
AnilistNotificationWorker.WORK_NAME,
androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
recurringWork
)
} else {
androidx.work.WorkManager.getInstance(this).cancelUniqueWork(AnilistNotificationWorker.WORK_NAME)
//run once
androidx.work.WorkManager.getInstance(this).enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
}
androidx.work.WorkManager.getInstance(this).cancelUniqueWork("ani.dantotsu.notifications.NotificationWorker") //legacy worker
}

View file

@ -0,0 +1,77 @@
package ani.dantotsu.notifications
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import ani.dantotsu.notifications.anilist.AnilistNotificationReceiver
import ani.dantotsu.notifications.comment.CommentNotificationReceiver
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import java.util.concurrent.TimeUnit
class AlarmManagerScheduler(private val context: Context) : TaskScheduler {
override fun scheduleRepeatingTask(taskType: TaskType, interval: Long) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> Intent(
context,
CommentNotificationReceiver::class.java
)
TaskType.ANILIST_NOTIFICATION -> Intent(
context,
AnilistNotificationReceiver::class.java
)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
taskType.ordinal,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val triggerAtMillis = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(interval)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
pendingIntent
)
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)
}
} catch (e: SecurityException) {
PrefManager.setVal(PrefName.UseAlarmManager, false)
TaskScheduler.create(context, true).cancelAllTasks()
TaskScheduler.create(context, false).scheduleAllTasks(context)
}
}
override fun cancelTask(taskType: TaskType) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> Intent(
context,
CommentNotificationReceiver::class.java
)
TaskType.ANILIST_NOTIFICATION -> Intent(
context,
AnilistNotificationReceiver::class.java
)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
taskType.ordinal,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
}
}

View file

@ -0,0 +1,64 @@
package ani.dantotsu.notifications
import android.app.AlarmManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
if (context != null) {
val scheduler = AlarmManagerScheduler(context)
PrefManager.init(context)
Logger.init(context)
Logger.log("Starting Dantotsu Subscription Service on Boot")
if (PrefManager.getVal(PrefName.UseAlarmManager)) {
val commentInterval =
CommentNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.CommentNotificationInterval)]
val anilistInterval =
AnilistNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
scheduler.scheduleRepeatingTask(
TaskType.COMMENT_NOTIFICATION,
commentInterval
)
scheduler.scheduleRepeatingTask(
TaskType.ANILIST_NOTIFICATION,
anilistInterval
)
}
}
}
}
}
class AlarmPermissionStateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED) {
if (context != null) {
PrefManager.init(context)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val canScheduleExactAlarms = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
alarmManager.canScheduleExactAlarms()
} else {
true
}
if(canScheduleExactAlarms) {
TaskScheduler.create(context, false).cancelAllTasks()
TaskScheduler.create(context, true).scheduleAllTasks(context)
} else {
TaskScheduler.create(context, true).cancelAllTasks()
TaskScheduler.create(context, false).scheduleAllTasks(context)
}
PrefManager.setVal(PrefName.UseAlarmManager, canScheduleExactAlarms)
}
}
}
}

View file

@ -0,0 +1,48 @@
package ani.dantotsu.notifications
import android.content.Context
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
interface TaskScheduler {
fun scheduleRepeatingTask(taskType: TaskType, interval: Long)
fun cancelTask(taskType: TaskType)
fun cancelAllTasks() {
for (taskType in TaskType.entries) {
cancelTask(taskType)
}
}
fun scheduleAllTasks(context: Context) {
for (taskType in TaskType.entries) {
val interval = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> CommentNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.CommentNotificationInterval)]
TaskType.ANILIST_NOTIFICATION -> AnilistNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.AnilistNotificationInterval)]
}
scheduleRepeatingTask(taskType, interval)
}
}
companion object {
fun create(context: Context, useAlarmManager: Boolean): TaskScheduler {
return if (useAlarmManager) {
AlarmManagerScheduler(context)
} else {
WorkManagerScheduler(context)
}
}
}
enum class TaskType {
COMMENT_NOTIFICATION,
ANILIST_NOTIFICATION
}
}
interface Task {
suspend fun execute(context: Context): Boolean
}

View file

@ -0,0 +1,62 @@
package ani.dantotsu.notifications
import android.content.Context
import androidx.work.Constraints
import androidx.work.PeriodicWorkRequest
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.TaskScheduler.TaskType
class WorkManagerScheduler(private val context: Context) : TaskScheduler {
override fun scheduleRepeatingTask(taskType: TaskType, interval: Long) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(androidx.work.NetworkType.CONNECTED)
.build()
when (taskType) {
TaskType.COMMENT_NOTIFICATION -> {
val recurringWork = PeriodicWorkRequest.Builder(
CommentNotificationWorker::class.java,
interval, java.util.concurrent.TimeUnit.MINUTES,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, java.util.concurrent.TimeUnit.MINUTES
)
.setConstraints(constraints)
.build()
androidx.work.WorkManager.getInstance(context).enqueueUniquePeriodicWork(
CommentNotificationWorker.WORK_NAME,
androidx.work.ExistingPeriodicWorkPolicy.UPDATE,
recurringWork
)
}
TaskType.ANILIST_NOTIFICATION -> {
val recurringWork = PeriodicWorkRequest.Builder(
AnilistNotificationWorker::class.java,
interval, java.util.concurrent.TimeUnit.MINUTES,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, java.util.concurrent.TimeUnit.MINUTES
)
.setConstraints(constraints)
.build()
androidx.work.WorkManager.getInstance(context).enqueueUniquePeriodicWork(
AnilistNotificationWorker.WORK_NAME,
androidx.work.ExistingPeriodicWorkPolicy.UPDATE,
recurringWork
)
}
}
}
override fun cancelTask(taskType: TaskType) {
when (taskType) {
TaskType.COMMENT_NOTIFICATION -> {
androidx.work.WorkManager.getInstance(context)
.cancelUniqueWork(CommentNotificationWorker.WORK_NAME)
}
TaskType.ANILIST_NOTIFICATION -> {
androidx.work.WorkManager.getInstance(context)
.cancelUniqueWork(AnilistNotificationWorker.WORK_NAME)
}
}
}
}

View file

@ -0,0 +1,25 @@
package ani.dantotsu.notifications.anilist
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ani.dantotsu.notifications.AlarmManagerScheduler
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import kotlinx.coroutines.runBlocking
class AnilistNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Logger.log("AnilistNotificationReceiver: onReceive")
if (context != null) {
runBlocking {
AnilistNotificationTask().execute(context)
}
val anilistInterval =
AnilistNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
AlarmManagerScheduler(context).scheduleRepeatingTask(TaskScheduler.TaskType.ANILIST_NOTIFICATION, anilistInterval)
}
}
}

View file

@ -0,0 +1,109 @@
package ani.dantotsu.notifications.anilist
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.notifications.Task
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AnilistNotificationTask : Task {
override suspend fun execute(context: Context): Boolean {
try {
withContext(Dispatchers.IO) {
PrefManager.init(context) //make sure prefs are initialized
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
if (userId.isNotEmpty()) {
Anilist.getSavedToken()
val res = Anilist.query.getNotifications(
userId.toInt(),
resetNotification = false
)
val unreadNotificationCount = res?.data?.user?.unreadNotificationCount ?: 0
if (unreadNotificationCount > 0) {
val unreadNotifications =
res?.data?.page?.notifications?.sortedBy { it.id }
?.takeLast(unreadNotificationCount)
val lastId = PrefManager.getVal<Int>(PrefName.LastAnilistNotificationId)
val newNotifications = unreadNotifications?.filter { it.id > lastId }
val filteredTypes =
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes)
newNotifications?.forEach {
if (!filteredTypes.contains(it.notificationType)) {
val content = ActivityItemBuilder.getContent(it)
val notification = createNotification(context, content, it.id)
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
NotificationManagerCompat.from(context)
.notify(
Notifications.CHANNEL_ANILIST,
System.currentTimeMillis().toInt(),
notification
)
}
}
}
if (newNotifications?.isNotEmpty() == true) {
PrefManager.setVal(
PrefName.LastAnilistNotificationId,
newNotifications.last().id
)
}
}
}
}
return true
} catch (e: Exception) {
Logger.log("AnilistNotificationTask: ${e.message}")
Logger.log(e)
return false
}
}
private fun createNotification(
context: Context,
content: String,
notificationId: Int? = null
): android.app.Notification {
val title = "New Anilist Notification"
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
if (notificationId != null) {
Logger.log("notificationId: $notificationId")
putExtra("activityId", notificationId)
}
}
val pendingIntent = PendingIntent.getActivity(
context,
notificationId ?: 0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(context, Notifications.CHANNEL_ANILIST)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
}
}

View file

@ -1,102 +1,20 @@
package ani.dantotsu.notifications.anilist
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Worker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AnilistNotificationWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
CoroutineWorker(appContext, workerParams) {
override fun doWork(): Result {
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
PrefManager.init(applicationContext) //make sure prefs are initialized
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
if (userId.isNotEmpty()) {
Anilist.getSavedToken()
val res = Anilist.query.getNotifications(userId.toInt(), resetNotification = false)
val unreadNotificationCount = res?.data?.user?.unreadNotificationCount ?: 0
if (unreadNotificationCount > 0) {
val unreadNotifications = res?.data?.page?.notifications?.sortedBy { it.id }
?.takeLast(unreadNotificationCount)
val lastId = PrefManager.getVal<Int>(PrefName.LastAnilistNotificationId)
val newNotifications = unreadNotifications?.filter { it.id > lastId }
val filteredTypes =
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes)
newNotifications?.forEach {
if (!filteredTypes.contains(it.notificationType)) {
val content = ActivityItemBuilder.getContent(it)
val notification = createNotification(applicationContext, content, it.id)
if (ActivityCompat.checkSelfPermission(
applicationContext,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
NotificationManagerCompat.from(applicationContext)
.notify(
Notifications.CHANNEL_ANILIST,
System.currentTimeMillis().toInt(),
notification
)
}
}
}
if (newNotifications?.isNotEmpty() == true) {
PrefManager.setVal(PrefName.LastAnilistNotificationId, newNotifications.last().id)
}
}
}
override suspend fun doWork(): Result {
Logger.log("AnilistNotificationWorker: doWork")
return if (AnilistNotificationTask().execute(applicationContext)) {
Result.success()
} else {
Result.retry()
}
return Result.success()
}
private fun createNotification(
context: Context,
content: String,
notificationId: Int? = null
): android.app.Notification {
val title = "New Anilist Notification"
val intent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
if (notificationId != null) {
Logger.log("notificationId: $notificationId")
putExtra("activityId", notificationId)
}
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
notificationId ?: 0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(context, Notifications.CHANNEL_ANILIST)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
}
companion object {

View file

@ -0,0 +1,18 @@
package ani.dantotsu.notifications.comment
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ani.dantotsu.util.Logger
import kotlinx.coroutines.runBlocking
class CommentNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Logger.log("CommentNotificationReceiver: onReceive")
if (context != null) {
runBlocking {
CommentNotificationTask().execute(context)
}
}
}
}

View file

@ -0,0 +1,316 @@
package ani.dantotsu.notifications.comment
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.notifications.Task
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
class CommentNotificationTask : Task {
override suspend fun execute(context: Context): Boolean {
try {
withContext(Dispatchers.IO) {
PrefManager.init(context) //make sure prefs are initialized
val client = OkHttpClient()
CommentsAPI.fetchAuthToken(client)
val notificationResponse = CommentsAPI.getNotifications(client)
var notifications = notificationResponse?.notifications?.toMutableList()
//if we have at least one reply notification, we need to fetch the media titles
var names = emptyMap<Int, MediaNameFetch.Companion.ReturnedData>()
if (notifications?.any { it.type == 1 || it.type == null } == true) {
val mediaIds =
notifications.filter { it.type == 1 || it.type == null }.map { it.mediaId }
names = MediaNameFetch.fetchMediaTitles(mediaIds)
}
val recentGlobal = PrefManager.getVal<Int>(
PrefName.RecentGlobalNotification
)
notifications =
notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
?.toMutableList()
val newRecentGlobal =
notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
if (newRecentGlobal != null) {
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
}
if (notifications.isNullOrEmpty()) return@withContext
PrefManager.setVal(
PrefName.UnreadCommentNotifications,
PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications) + (notifications.size
?: 0)
)
notifications.forEach {
val type: CommentNotificationWorker.NotificationType = when (it.type) {
1 -> CommentNotificationWorker.NotificationType.COMMENT_REPLY
2 -> CommentNotificationWorker.NotificationType.COMMENT_WARNING
3 -> CommentNotificationWorker.NotificationType.APP_GLOBAL
420 -> CommentNotificationWorker.NotificationType.NO_NOTIFICATION
else -> CommentNotificationWorker.NotificationType.UNKNOWN
}
val notification = when (type) {
CommentNotificationWorker.NotificationType.COMMENT_WARNING -> {
val title = "You received a warning"
val message = it.content ?: "Be more thoughtful with your comments"
val commentStore = CommentStore(
title,
message,
it.mediaId,
it.commentId
)
addNotificationToStore(commentStore)
createNotification(
context,
CommentNotificationWorker.NotificationType.COMMENT_WARNING,
message,
title,
it.mediaId,
it.commentId,
"",
""
)
}
CommentNotificationWorker.NotificationType.COMMENT_REPLY -> {
val title = "New Comment Reply"
val mediaName = names[it.mediaId]?.title ?: "Unknown"
val message = "${it.username} replied to your comment in $mediaName"
val commentStore = CommentStore(
title,
message,
it.mediaId,
it.commentId
)
addNotificationToStore(commentStore)
createNotification(
context,
CommentNotificationWorker.NotificationType.COMMENT_REPLY,
message,
title,
it.mediaId,
it.commentId,
names[it.mediaId]?.color ?: "#222222",
names[it.mediaId]?.coverImage ?: ""
)
}
CommentNotificationWorker.NotificationType.APP_GLOBAL -> {
val title = "Update from Dantotsu"
val message = it.content ?: "New feature available"
val commentStore = CommentStore(
title,
message,
null,
null
)
addNotificationToStore(commentStore)
createNotification(
context,
CommentNotificationWorker.NotificationType.APP_GLOBAL,
message,
title,
0,
0,
"",
""
)
}
CommentNotificationWorker.NotificationType.NO_NOTIFICATION -> {
PrefManager.removeCustomVal("genre_thumb")
PrefManager.removeCustomVal("banner_ANIME_time")
PrefManager.removeCustomVal("banner_MANGA_time")
PrefManager.setVal(PrefName.ImageUrl, it.content ?: "")
null
}
CommentNotificationWorker.NotificationType.UNKNOWN -> {
null
}
}
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
if (notification != null) {
NotificationManagerCompat.from(context)
.notify(
type.id,
System.currentTimeMillis().toInt(),
notification
)
}
}
}
}
return true
} catch (e: Exception) {
Logger.log("CommentNotificationTask: ${e.message}")
Logger.log(e)
return false
}
}
private fun addNotificationToStore(notification: CommentStore) {
val notificationStore = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
val newStore = notificationStore.toMutableList()
if (newStore.size > 10) {
newStore.remove(newStore.minByOrNull { it.time })
}
if (newStore.any { it.content == notification.content }) {
return
}
newStore.add(notification)
PrefManager.setVal(PrefName.CommentNotificationStore, newStore)
}
private fun createNotification(
context: Context,
notificationType: CommentNotificationWorker.NotificationType,
message: String,
title: String,
mediaId: Int,
commentId: Int,
color: String,
imageUrl: String
): android.app.Notification? {
Logger.log(
"Creating notification of type $notificationType" +
", message: $message, title: $title, mediaId: $mediaId, commentId: $commentId"
)
val notification = when (notificationType) {
CommentNotificationWorker.NotificationType.COMMENT_WARNING -> {
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
commentId,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
builder.build()
}
CommentNotificationWorker.NotificationType.COMMENT_REPLY -> {
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
commentId,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
if (imageUrl.isNotEmpty()) {
val bitmap = getBitmapFromUrl(imageUrl)
if (bitmap != null) {
builder.setLargeIcon(bitmap)
}
}
if (color.isNotEmpty()) {
builder.color = Color.parseColor(color)
}
builder.build()
}
CommentNotificationWorker.NotificationType.APP_GLOBAL -> {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(),
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
builder.build()
}
else -> {
null
}
}
return notification
}
private fun getBitmapFromVectorDrawable(context: Context, drawableId: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
private fun getBitmapFromUrl(url: String): Bitmap? {
return try {
val inputStream = java.net.URL(url).openStream()
BitmapFactory.decodeStream(inputStream)
} catch (e: Exception) {
null
}
}
}

View file

@ -1,310 +1,20 @@
package ani.dantotsu.notifications.comment
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.Worker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
class CommentNotificationWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
PrefManager.init(applicationContext) //make sure prefs are initialized
val client = OkHttpClient()
CommentsAPI.fetchAuthToken(client)
val notificationResponse = CommentsAPI.getNotifications(client)
var notifications = notificationResponse?.notifications?.toMutableList()
//if we have at least one reply notification, we need to fetch the media titles
var names = emptyMap<Int, MediaNameFetch.Companion.ReturnedData>()
if (notifications?.any { it.type == 1 || it.type == null } == true) {
val mediaIds =
notifications.filter { it.type == 1 || it.type == null }.map { it.mediaId }
names = MediaNameFetch.fetchMediaTitles(mediaIds)
}
val recentGlobal = PrefManager.getVal<Int>(
PrefName.RecentGlobalNotification
)
notifications =
notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
?.toMutableList()
val newRecentGlobal =
notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
if (newRecentGlobal != null) {
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
}
if (notifications.isNullOrEmpty()) return@launch
PrefManager.setVal(PrefName.UnreadCommentNotifications,
PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications) + (notifications?.size ?: 0)
)
notifications.forEach {
val type: NotificationType = when (it.type) {
1 -> NotificationType.COMMENT_REPLY
2 -> NotificationType.COMMENT_WARNING
3 -> NotificationType.APP_GLOBAL
420 -> NotificationType.NO_NOTIFICATION
else -> NotificationType.UNKNOWN
}
val notification = when (type) {
NotificationType.COMMENT_WARNING -> {
val title = "You received a warning"
val message = it.content ?: "Be more thoughtful with your comments"
val commentStore = CommentStore(
title,
message,
it.mediaId,
it.commentId
)
addNotificationToStore(commentStore)
createNotification(
NotificationType.COMMENT_WARNING,
message,
title,
it.mediaId,
it.commentId,
"",
""
)
}
NotificationType.COMMENT_REPLY -> {
val title = "New Comment Reply"
val mediaName = names[it.mediaId]?.title ?: "Unknown"
val message = "${it.username} replied to your comment in $mediaName"
val commentStore = CommentStore(
title,
message,
it.mediaId,
it.commentId
)
addNotificationToStore(commentStore)
createNotification(
NotificationType.COMMENT_REPLY,
message,
title,
it.mediaId,
it.commentId,
names[it.mediaId]?.color ?: "#222222",
names[it.mediaId]?.coverImage ?: ""
)
}
NotificationType.APP_GLOBAL -> {
val title = "Update from Dantotsu"
val message = it.content ?: "New feature available"
val commentStore = CommentStore(
title,
message,
null,
null
)
addNotificationToStore(commentStore)
createNotification(
NotificationType.APP_GLOBAL,
message,
title,
0,
0,
"",
""
)
}
NotificationType.NO_NOTIFICATION -> {
PrefManager.removeCustomVal("genre_thumb")
PrefManager.removeCustomVal("banner_ANIME_time")
PrefManager.removeCustomVal("banner_MANGA_time")
PrefManager.setVal(PrefName.ImageUrl, it.content ?: "")
null
}
NotificationType.UNKNOWN -> {
null
}
}
if (ActivityCompat.checkSelfPermission(
applicationContext,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
if (notification != null) {
NotificationManagerCompat.from(applicationContext)
.notify(
type.id,
System.currentTimeMillis().toInt(),
notification
)
}
}
}
}
return Result.success()
}
private fun addNotificationToStore(notification: CommentStore) {
val notificationStore = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
val newStore = notificationStore.toMutableList()
if (newStore.size > 10) {
newStore.remove(newStore.minByOrNull { it.time })
}
if (newStore.any { it.content == notification.content }) {
return
}
newStore.add(notification)
PrefManager.setVal(PrefName.CommentNotificationStore, newStore)
}
private fun createNotification(
notificationType: NotificationType,
message: String,
title: String,
mediaId: Int,
commentId: Int,
color: String,
imageUrl: String
): android.app.Notification? {
Logger.log(
"Creating notification of type $notificationType" +
", message: $message, title: $title, mediaId: $mediaId, commentId: $commentId"
)
val notification = when (notificationType) {
NotificationType.COMMENT_WARNING -> {
val intent = Intent(applicationContext, MainActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
commentId,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(applicationContext, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
builder.build()
}
NotificationType.COMMENT_REPLY -> {
val intent = Intent(applicationContext, MainActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
commentId,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(applicationContext, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
if (imageUrl.isNotEmpty()) {
val bitmap = getBitmapFromUrl(imageUrl)
if (bitmap != null) {
builder.setLargeIcon(bitmap)
}
}
if (color.isNotEmpty()) {
builder.color = Color.parseColor(color)
}
builder.build()
}
NotificationType.APP_GLOBAL -> {
val intent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
System.currentTimeMillis().toInt(),
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(applicationContext, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
builder.build()
}
else -> {
null
}
}
return notification
}
private fun getBitmapFromVectorDrawable(context: Context, drawableId: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
private fun getBitmapFromUrl(url: String): Bitmap? {
return try {
val inputStream = java.net.URL(url).openStream()
BitmapFactory.decodeStream(inputStream)
} catch (e: Exception) {
null
CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
Logger.log("CommentNotificationWorker: doWork")
return if (CommentNotificationTask().execute(applicationContext)) {
Result.success()
} else {
Result.retry()
}
}

View file

@ -1,9 +1,13 @@
package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Build
import android.os.Build.BRAND
import android.os.Build.DEVICE
import android.os.Build.SUPPORTED_ABIS
@ -47,6 +51,7 @@ import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.util.Logger
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.openLinkInBrowser
@ -764,6 +769,40 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
openSettings(this, null)
}
binding.settingsNotificationsUseAlarmManager.isChecked =
PrefManager.getVal(PrefName.UseAlarmManager)
binding.settingsNotificationsUseAlarmManager.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Use Alarm Manager")
.setMessage("Using Alarm Manger can help fight against battery optimization, but may consume more battery. It also requires the Alarm Manager permission.")
.setPositiveButton("Use") { dialog, _ ->
PrefManager.setVal(PrefName.UseAlarmManager, true)
if (SDK_INT >= Build.VERSION_CODES.S) {
if (!(getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()) {
val intent = Intent("android.settings.REQUEST_SCHEDULE_EXACT_ALARM")
startActivity(intent)
binding.settingsNotificationsCheckingSubscriptions.isChecked = true
}
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
binding.settingsNotificationsCheckingSubscriptions.isChecked = false
PrefManager.setVal(PrefName.UseAlarmManager, false)
dialog.dismiss()
}
.create()
alertDialog.window?.setDimAmount(0.8f)
alertDialog.show()
} else {
PrefManager.setVal(PrefName.UseAlarmManager, false)
TaskScheduler.create(this, true).cancelAllTasks()
TaskScheduler.create(this, false).scheduleAllTasks(this)
}
}
if (!BuildConfig.FLAVOR.contains("fdroid")) {
binding.settingsLogo.setOnLongClickListener {
lifecycleScope.launch(Dispatchers.IO) {

View file

@ -24,6 +24,7 @@ object PrefManager {
private var protectedPreferences: SharedPreferences? = null
fun init(context: Context) { //must be called in Application class or will crash
if (generalPreferences != null) return
generalPreferences =
context.getSharedPreferences(Location.General.location, Context.MODE_PRIVATE)
uiPreferences =

View file

@ -38,6 +38,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
AnilistNotificationInterval(Pref(Location.General, Int::class, 3)),
LastAnilistNotificationId(Pref(Location.General, Int::class, 0)),
AnilistFilteredTypes(Pref(Location.General, Set::class, setOf<String>())),
UseAlarmManager(Pref(Location.General, Boolean::class, false)),
//User Interface
UseOLED(Pref(Location.UI, Boolean::class, false)),

View file

@ -1469,6 +1469,24 @@
app:showText="false"
app:thumbTint="@color/button_switch_track" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/settingsNotificationsUseAlarmManager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:drawableStart="@drawable/ic_round_new_releases_24"
android:drawablePadding="16dp"
android:elegantTextHeight="true"
android:fontFamily="@font/poppins_bold"
android:minHeight="64dp"
android:text="@string/use_alarm_manager"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnBackground"
app:cornerRadius="0dp"
app:drawableTint="?attr/colorPrimary"
app:showText="false"
app:thumbTint="@color/button_switch_track" />
</ani.dantotsu.others.Xpandable>
<ani.dantotsu.others.Xpandable

View file

@ -383,6 +383,7 @@
\n\n_It is not required to sync both MAL and Anilist accounts._
</string>
<string name="notification_for_checking_subscriptions">Show notification for Checking Subscriptions</string>
<string name="use_alarm_manager">Use Alarm Manager for reliable Notifications</string>
<string name="checking_subscriptions">Notification for Checking Subscriptions</string>
<string name="subscriptions_checking_time_s">Subscriptions Update Frequency : %1$s</string>
<string name="subscriptions_checking_time">Subscriptions Update Frequency</string>