Improved notifications, experimental scheduled notifications

This commit is contained in:
Kelvin 2023-11-21 23:31:26 +01:00
commit e221b508d3
10 changed files with 313 additions and 104 deletions

View file

@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -39,6 +41,7 @@
<receiver android:name=".receivers.MediaControlReceiver" /> <receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" /> <receiver android:name=".receivers.AudioNoisyReceiver" />
<receiver android:name=".receivers.PlannedNotificationReceiver" />
<activity <activity
android:name=".activities.MainActivity" android:name=".activities.MainActivity"

View file

@ -471,8 +471,6 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.announcement, FieldForm.GROUP, -1, 10) @FormField(R.string.announcement, FieldForm.GROUP, -1, 10)
var announcementSettings = AnnouncementSettings(); var announcementSettings = AnnouncementSettings();
@Serializable @Serializable
@ -484,7 +482,15 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.plugins, FieldForm.GROUP, -1, 11) @FormField(R.string.notifications, FieldForm.GROUP, -1, 11)
var notifications = NotificationSettings();
@Serializable
class NotificationSettings {
@FormField(R.string.planned_content_notifications, FieldForm.TOGGLE, R.string.planned_content_notifications_description, 1)
var plannedContentNotification: Boolean = true;
}
@FormField(R.string.plugins, FieldForm.GROUP, -1, 12)
@Transient @Transient
var plugins = Plugins(); var plugins = Plugins();
@Serializable @Serializable
@ -521,7 +527,7 @@ class Settings : FragmentedStorageFileJson() {
} }
@FormField(R.string.external_storage, FieldForm.GROUP, -1, 12) @FormField(R.string.external_storage, FieldForm.GROUP, -1, 13)
var storage = Storage(); var storage = Storage();
@Serializable @Serializable
class Storage { class Storage {
@ -555,7 +561,7 @@ class Settings : FragmentedStorageFileJson() {
} }
@FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 12) @FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 14)
var autoUpdate = AutoUpdate(); var autoUpdate = AutoUpdate();
@Serializable @Serializable
class AutoUpdate { class AutoUpdate {
@ -637,7 +643,7 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.backup, FieldForm.GROUP, -1, 13) @FormField(R.string.backup, FieldForm.GROUP, -1, 15)
var backup = Backup(); var backup = Backup();
@Serializable @Serializable
class Backup { class Backup {
@ -690,7 +696,7 @@ class Settings : FragmentedStorageFileJson() {
}*/ }*/
} }
@FormField(R.string.payment, FieldForm.GROUP, -1, 14) @FormField(R.string.payment, FieldForm.GROUP, -1, 16)
var payment = Payment(); var payment = Payment();
@Serializable @Serializable
class Payment { class Payment {
@ -707,7 +713,7 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.other, FieldForm.GROUP, -1, 15) @FormField(R.string.other, FieldForm.GROUP, -1, 17)
var other = Other(); var other = Other();
@Serializable @Serializable
class Other { class Other {
@ -716,7 +722,7 @@ class Settings : FragmentedStorageFileJson() {
var bypassRotationPrevention: Boolean = false; var bypassRotationPrevention: Boolean = false;
} }
@FormField(R.string.info, FieldForm.GROUP, -1, 16) @FormField(R.string.info, FieldForm.GROUP, -1, 18)
var info = Info(); var info = Info();
@Serializable @Serializable
class Info { class Info {

View file

@ -20,6 +20,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.background.BackgroundWorker import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
@ -111,6 +112,14 @@ class SettingsDev : FragmentedStorageFileJson() {
.build(); .build();
wm.enqueue(req); wm.enqueue(req);
} }
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
R.string.test_background_worker_description, 3)
fun clearChannelContentCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
ChannelContentCache.instance.clearToday();
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
}
@Contextual @Contextual
@Transient @Transient

View file

@ -10,6 +10,7 @@ import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
@ -54,7 +55,6 @@ class UISlideOverlays {
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) { fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
var menu: SlideUpMenuOverlay? = null;
val originalNotif = subscription.doNotifications; val originalNotif = subscription.doNotifications;
val originalLive = subscription.doFetchLive; val originalLive = subscription.doFetchLive;
@ -62,6 +62,15 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos; val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts; val originalPosts = subscription.doFetchPosts;
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
withContext(Dispatchers.Main) {
var menu: SlideUpMenuOverlay? = null;
items.addAll(listOf( items.addAll(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", { SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications; subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
@ -69,18 +78,22 @@ class UISlideOverlays {
SlideUpMenuGroup(container.context, "Fetch Settings", SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.", "Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()), -1, listOf()),
SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", { if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive; subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
}, false), }, false) else null,
SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for finished streams", "fetchStreams", { if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchLive; subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
}, false), }, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", { SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchLive; subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
}, false), }, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", { SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchLive; subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
}, false))); }, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
}, false) else null).filterNotNull());
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items); menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
@ -111,6 +124,8 @@ class UISlideOverlays {
menu.show(); menu.show();
} }
}
}
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? { fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
val items = arrayListOf<View>(); val items = arrayListOf<View>();

View file

@ -6,33 +6,25 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.media.MediaSession2Service.MediaNotification
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.concurrent.futures.ResolvableFuture
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.Settings
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StateNotifications
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.adapters.viewholders.TabViewHolder import com.futo.platformplayer.toHumanNowDiffString
import com.google.common.util.concurrent.ListenableFuture import com.futo.platformplayer.toHumanNowDiffStringMinDay
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -54,8 +46,10 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
this.setSound(null, null); this.setSound(null, null);
}; };
notificationManager.createNotificationChannel(notificationChannel); notificationManager.createNotificationChannel(notificationChannel);
val contentChannel = StateNotifications.instance.contentNotifChannel
notificationManager.createNotificationChannel(contentChannel);
try { try {
doSubscriptionUpdating(notificationManager, notificationChannel); doSubscriptionUpdating(notificationManager, notificationChannel, contentChannel);
} }
catch(ex: Throwable) { catch(ex: Throwable) {
exception = ex; exception = ex;
@ -77,13 +71,13 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
} }
suspend fun doSubscriptionUpdating(manager: NotificationManager, notificationChannel: NotificationChannel) { suspend fun doSubscriptionUpdating(manager: NotificationManager, backgroundChannel: NotificationChannel, contentChannel: NotificationChannel) {
val notif = NotificationCompat.Builder(appContext, notificationChannel.id) val notif = NotificationCompat.Builder(appContext, backgroundChannel.id)
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground) .setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
.setContentTitle("Grayjay") .setContentTitle("Grayjay")
.setContentText("Updating subscriptions...") .setContentText("Updating subscriptions...")
.setSilent(true) .setSilent(true)
.setChannelId(notificationChannel.id) .setChannelId(backgroundChannel.id)
.setProgress(1, 0, true); .setProgress(1, 0, true);
manager.notify(12, notif.build()); manager.notify(12, notif.build());
@ -94,6 +88,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
val newItems = mutableListOf<IPlatformContent>(); val newItems = mutableListOf<IPlatformContent>();
val now = OffsetDateTime.now(); val now = OffsetDateTime.now();
val threeDays = now.minusDays(4);
val contentNotifs = mutableListOf<Pair<Subscription, IPlatformContent>>(); val contentNotifs = mutableListOf<Pair<Subscription, IPlatformContent>>();
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val results = StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(true, false,this, { progress, total -> val results = StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(true, false,this, { progress, total ->
@ -111,8 +106,14 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
synchronized(newSubChanges) { synchronized(newSubChanges) {
if(!newSubChanges.contains(sub)) { if(!newSubChanges.contains(sub)) {
newSubChanges.add(sub); newSubChanges.add(sub);
if(sub.doNotifications && content.datetime?.let { it < now } == true) if(sub.doNotifications) {
if(content.datetime != null) {
if(content.datetime!! <= now.plusMinutes(StateNotifications.instance.plannedWarningMinutesEarly) && content.datetime!! > threeDays)
contentNotifs.add(Pair(sub, content)); contentNotifs.add(Pair(sub, content));
else if(content.datetime!! > now.plusMinutes(StateNotifications.instance.plannedWarningMinutesEarly) && Settings.instance.notifications.plannedContentNotification)
StateNotifications.instance.scheduleContentNotification(applicationContext, content);
}
}
} }
newItems.add(content); newItems.add(content);
} }
@ -135,22 +136,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
val items = contentNotifs.take(5).toList() val items = contentNotifs.take(5).toList()
for(i in items.indices) { for(i in items.indices) {
val contentNotif = items.get(i); val contentNotif = items.get(i);
val thumbnail = if(contentNotif.second is IPlatformVideo) (contentNotif.second as IPlatformVideo).thumbnails.getHQThumbnail() StateNotifications.instance.notifyNewContentWithThumbnail(appContext, manager, contentChannel, 13 + i, contentNotif.second);
else null;
if(thumbnail != null)
Glide.with(appContext).asBitmap()
.load(thumbnail)
.into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, resource);
}
override fun onLoadCleared(placeholder: Drawable?) {}
override fun onLoadFailed(errorDrawable: Drawable?) {
notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, null);
}
})
else
notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, null);
} }
} }
catch(ex: Throwable) { catch(ex: Throwable) {
@ -165,20 +151,4 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
.setSilent(true) .setSilent(true)
.setChannelId(notificationChannel.id).build());*/ .setChannelId(notificationChannel.id).build());*/
} }
fun notifyNewContent(manager: NotificationManager, notificationChannel: NotificationChannel, id: Int, sub: Subscription, content: IPlatformContent, thumbnail: Bitmap? = null) {
val notifBuilder = NotificationCompat.Builder(appContext, notificationChannel.id)
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
.setContentTitle("New by [${sub.channel.name}]")
.setContentText("${content.name}")
.setSilent(true)
.setContentIntent(PendingIntent.getActivity(this.appContext, 0, MainActivity.getVideoIntent(this.appContext, content.url),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setChannelId(notificationChannel.id);
if(thumbnail != null) {
//notifBuilder.setLargeIcon(thumbnail);
notifBuilder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(thumbnail).bigLargeIcon(null as Bitmap?));
}
manager.notify(id, notifBuilder.build());
}
} }

View file

@ -58,6 +58,14 @@ class ChannelContentCache {
uncacheContent(content); uncacheContent(content);
} }
} }
fun clearToday() {
val yesterday = OffsetDateTime.now().minusDays(1);
synchronized(_channelContents) {
for(channel in _channelContents)
for(content in channel.value.getItems().filter { it.datetime?.isAfter(yesterday) == true })
uncacheContent(content);
}
}
fun getChannelCachePager(channelUrl: String): PlatformContentPager { fun getChannelCachePager(channelUrl: String): PlatformContentPager {
val validID = channelUrl.toSafeFileName(); val validID = channelUrl.toSafeFileName();

View file

@ -0,0 +1,48 @@
package com.futo.platformplayer.receivers
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.futo.platformplayer.Settings
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateNotifications
class PlannedNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
try {
Logger.i(TAG, "Planned Notification received");
if(!Settings.instance.notifications.plannedContentNotification)
return;
if(StateApp.instance.contextOrNull == null)
StateApp.instance.initializeFiles();
val notifs = StateNotifications.instance.getScheduledNotifications(60 * 15, true);
if(!notifs.isEmpty() && context != null) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
val channel = StateNotifications.instance.contentNotifChannel;
notificationManager.createNotificationChannel(channel);
var i = 0;
for (notif in notifs) {
StateNotifications.instance.notifyNewContentWithThumbnail(context, notificationManager, channel, 110 + i, notif);
i++;
}
}
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed PlannedNotificationReceiver.onReceive", ex);
}
}
companion object {
private val TAG = "PlannedNotificationReceiver"
fun getIntent(context: Context): PendingIntent {
return PendingIntent.getBroadcast(context, 110, Intent(context, PlannedNotificationReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE);
}
}
}

View file

@ -0,0 +1,147 @@
package com.futo.platformplayer.states
import android.app.AlarmManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Build
import androidx.core.app.NotificationCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.PlannedNotificationReceiver
import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNowDiffStringMinDay
import java.time.OffsetDateTime
class StateNotifications {
private val _alarmManagerLock = Object();
private var _alarmManager: AlarmManager? = null;
val plannedWarningMinutesEarly: Long = 10;
val contentNotifChannel = NotificationChannel("contentChannel", "Content Notifications",
NotificationManager.IMPORTANCE_HIGH).apply {
this.enableVibration(false);
this.setSound(null, null);
};
private val _plannedContent = FragmentedStorage.storeJson<SerializedPlatformContent>("planned_content_notifs", PlatformContentSerializer())
.load();
private fun getAlarmManager(context: Context): AlarmManager {
synchronized(_alarmManagerLock) {
if(_alarmManager == null)
_alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
return _alarmManager!!;
}
}
fun scheduleContentNotification(context: Context, content: IPlatformContent) {
try {
var existing = _plannedContent.findItem { it.url == content.url };
if(existing != null) {
_plannedContent.delete(existing);
existing = null;
}
if(existing == null && content.datetime != null) {
val item = SerializedPlatformContent.fromContent(content);
_plannedContent.saveAsync(item);
val manager = getAlarmManager(context);
val notifyDateTime = content.datetime!!.minusMinutes(plannedWarningMinutesEarly);
if(Build.VERSION.SDK_INT >= 31 && !manager.canScheduleExactAlarms()) {
Logger.i(TAG, "Scheduling in-exact notification for [${content.name}] at ${notifyDateTime.toHumanNowDiffString()}")
manager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, notifyDateTime.toEpochSecond().times(1000), PlannedNotificationReceiver.getIntent(context));
}
else {
Logger.i(TAG, "Scheduling exact notification for [${content.name}] at ${notifyDateTime.toHumanNowDiffString()}")
manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, notifyDateTime.toEpochSecond().times(1000), PlannedNotificationReceiver.getIntent(context))
}
}
}
catch(ex: Throwable) {
Logger.e(TAG, "scheduleContentNotification failed for [${content.name}]", ex);
}
}
fun removeChannelPlannedContent(channelUrl: String) {
val toDeletes = _plannedContent.findItems { it.author.url == channelUrl };
for(toDelete in toDeletes)
_plannedContent.delete(toDelete);
}
fun getScheduledNotifications(secondsFuture: Long, deleteReturned: Boolean = false): List<SerializedPlatformContent> {
val minDate = OffsetDateTime.now().plusSeconds(secondsFuture);
val toNotify = _plannedContent.findItems { it.datetime?.let { it.isBefore(minDate) } == true }
if(deleteReturned) {
for(toDelete in toNotify)
_plannedContent.delete(toDelete);
}
return toNotify;
}
fun notifyNewContentWithThumbnail(context: Context, manager: NotificationManager, notificationChannel: NotificationChannel, id: Int, content: IPlatformContent) {
val thumbnail = if(content is IPlatformVideo) (content as IPlatformVideo).thumbnails.getHQThumbnail()
else null;
if(thumbnail != null)
Glide.with(context).asBitmap()
.load(thumbnail)
.into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
notifyNewContent(context, manager, notificationChannel, id, content, resource);
}
override fun onLoadCleared(placeholder: Drawable?) {}
override fun onLoadFailed(errorDrawable: Drawable?) {
notifyNewContent(context, manager, notificationChannel, id, content, null);
}
})
else
notifyNewContent(context, manager, notificationChannel, id, content, null);
}
fun notifyNewContent(context: Context, manager: NotificationManager, notificationChannel: NotificationChannel, id: Int, content: IPlatformContent, thumbnail: Bitmap? = null) {
val notifBuilder = NotificationCompat.Builder(context, notificationChannel.id)
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
.setContentTitle("New by [${content.author.name}]")
.setContentText("${content.name}")
.setSubText(content.datetime?.toHumanNowDiffStringMinDay())
.setSilent(true)
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.getVideoIntent(context, content.url),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setChannelId(notificationChannel.id);
if(thumbnail != null) {
//notifBuilder.setLargeIcon(thumbnail);
notifBuilder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(thumbnail).bigLargeIcon(null as Bitmap?));
}
manager.notify(id, notifBuilder.build());
}
companion object {
val TAG = "StateNotifications";
private var _instance : StateNotifications? = null;
val instance : StateNotifications
get(){
if(_instance == null)
_instance = StateNotifications();
return _instance!!;
};
fun finish() {
_instance?.let {
_instance = null;
}
}
}
}

View file

@ -31,7 +31,7 @@ class FragmentedStorage {
fun initialize(filesDir: File) { fun initialize(filesDir: File) {
_filesDir = filesDir; _filesDir = filesDir;
} }
inline fun <reified T> storeJson(name: String, serializer: KSerializer<T>? = null): ManagedStore<T> = store(name, JsonStoreSerializer.create(serializer), null, null);
inline fun <reified T> storeJson(parentDir: File, name: String, serializer: KSerializer<T>? = null): ManagedStore<T> = store(name, JsonStoreSerializer.create(serializer), null, parentDir); inline fun <reified T> storeJson(parentDir: File, name: String, serializer: KSerializer<T>? = null): ManagedStore<T> = store(name, JsonStoreSerializer.create(serializer), null, parentDir);
inline fun <reified T> storeJson(name: String, prettyName: String? = null, parentDir: File? = null): ManagedStore<T> = store(name, JsonStoreSerializer.create(), prettyName, parentDir); inline fun <reified T> storeJson(name: String, prettyName: String? = null, parentDir: File? = null): ManagedStore<T> = store(name, JsonStoreSerializer.create(), prettyName, parentDir);
inline fun <reified T> store(name: String, serializer: StoreSerializer<T>, prettyName: String? = null, parentDir: File? = null): ManagedStore<T> { inline fun <reified T> store(name: String, serializer: StoreSerializer<T>, prettyName: String? = null, parentDir: File? = null): ManagedStore<T> {

View file

@ -267,6 +267,9 @@
<string name="a_list_of_user_reported_and_self_reported_issues">A list of user-reported and self-reported issues</string> <string name="a_list_of_user_reported_and_self_reported_issues">A list of user-reported and self-reported issues</string>
<string name="also_removes_any_data_related_plugin_like_login_or_settings">Also removes any data related plugin like login or settings</string> <string name="also_removes_any_data_related_plugin_like_login_or_settings">Also removes any data related plugin like login or settings</string>
<string name="announcement">Announcement</string> <string name="announcement">Announcement</string>
<string name="notifications">Notifications</string>
<string name="planned_content_notifications">Planned Content Notifications</string>
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string> <string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
<string name="auto_update">Auto Update</string> <string name="auto_update">Auto Update</string>
<string name="auto_rotate">Auto-Rotate</string> <string name="auto_rotate">Auto-Rotate</string>