mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Improved notifications, experimental scheduled notifications
This commit is contained in:
parent
dfafac7d99
commit
e221b508d3
10 changed files with 313 additions and 104 deletions
|
@ -9,6 +9,8 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WRITE_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
|
||||
android:allowBackup="true"
|
||||
|
@ -39,6 +41,7 @@
|
|||
|
||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||
<receiver android:name=".receivers.PlannedNotificationReceiver" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
|
|
|
@ -471,8 +471,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.announcement, FieldForm.GROUP, -1, 10)
|
||||
var announcementSettings = AnnouncementSettings();
|
||||
@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
|
||||
var plugins = Plugins();
|
||||
@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();
|
||||
@Serializable
|
||||
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();
|
||||
@Serializable
|
||||
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();
|
||||
@Serializable
|
||||
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();
|
||||
@Serializable
|
||||
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();
|
||||
@Serializable
|
||||
class Other {
|
||||
|
@ -716,7 +722,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
var bypassRotationPrevention: Boolean = false;
|
||||
}
|
||||
|
||||
@FormField(R.string.info, FieldForm.GROUP, -1, 16)
|
||||
@FormField(R.string.info, FieldForm.GROUP, -1, 18)
|
||||
var info = Info();
|
||||
@Serializable
|
||||
class Info {
|
||||
|
|
|
@ -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.structures.IPager
|
||||
import com.futo.platformplayer.background.BackgroundWorker
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||
|
@ -111,6 +112,14 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||
.build();
|
||||
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
|
||||
@Transient
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.widget.ImageButton
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
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.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
|
@ -54,7 +55,6 @@ class UISlideOverlays {
|
|||
|
||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
||||
val items = arrayListOf<View>();
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
|
||||
val originalNotif = subscription.doNotifications;
|
||||
val originalLive = subscription.doFetchLive;
|
||||
|
@ -62,54 +62,69 @@ class UISlideOverlays {
|
|||
val originalVideo = subscription.doFetchVideos;
|
||||
val originalPosts = subscription.doFetchPosts;
|
||||
|
||||
items.addAll(listOf(
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
}, false),
|
||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for finished streams", "fetchStreams", {
|
||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchLive;
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchLive;
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchLive;
|
||||
}, false)));
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||
val capabilities = plugin.getChannelCapabilities();
|
||||
|
||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
if(subscription.doNotifications)
|
||||
menu.selectOption(null, "notifications", true, true);
|
||||
if(subscription.doFetchLive)
|
||||
menu.selectOption(null, "fetchLive", true, true);
|
||||
if(subscription.doFetchStreams)
|
||||
menu.selectOption(null, "fetchStreams", true, true);
|
||||
if(subscription.doFetchVideos)
|
||||
menu.selectOption(null, "fetchVideos", true, true);
|
||||
if(subscription.doFetchPosts)
|
||||
menu.selectOption(null, "fetchPosts", true, true);
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
|
||||
menu.onOK.subscribe {
|
||||
subscription.save();
|
||||
menu.hide(true);
|
||||
};
|
||||
menu.onCancel.subscribe {
|
||||
subscription.doNotifications = originalNotif;
|
||||
subscription.doFetchLive = originalLive;
|
||||
subscription.doFetchStreams = originalStream;
|
||||
subscription.doFetchVideos = originalVideo;
|
||||
subscription.doFetchPosts = originalPosts;
|
||||
};
|
||||
|
||||
menu.setOk("Save");
|
||||
items.addAll(listOf(
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
}, false),
|
||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()),
|
||||
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;
|
||||
}, false) else null,
|
||||
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.doFetchStreams;
|
||||
}, false) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
}, 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.show();
|
||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
||||
|
||||
if(subscription.doNotifications)
|
||||
menu.selectOption(null, "notifications", true, true);
|
||||
if(subscription.doFetchLive)
|
||||
menu.selectOption(null, "fetchLive", true, true);
|
||||
if(subscription.doFetchStreams)
|
||||
menu.selectOption(null, "fetchStreams", true, true);
|
||||
if(subscription.doFetchVideos)
|
||||
menu.selectOption(null, "fetchVideos", true, true);
|
||||
if(subscription.doFetchPosts)
|
||||
menu.selectOption(null, "fetchPosts", true, true);
|
||||
|
||||
menu.onOK.subscribe {
|
||||
subscription.save();
|
||||
menu.hide(true);
|
||||
};
|
||||
menu.onCancel.subscribe {
|
||||
subscription.doNotifications = originalNotif;
|
||||
subscription.doFetchLive = originalLive;
|
||||
subscription.doFetchStreams = originalStream;
|
||||
subscription.doFetchVideos = originalVideo;
|
||||
subscription.doFetchPosts = originalPosts;
|
||||
};
|
||||
|
||||
menu.setOk("Save");
|
||||
|
||||
menu.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
||||
|
|
|
@ -6,33 +6,25 @@ import android.app.PendingIntent
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
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.work.CoroutineWorker
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.futo.platformplayer.Settings
|
||||
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.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
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.views.adapters.viewholders.TabViewHolder
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNowDiffStringMinDay
|
||||
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 java.time.OffsetDateTime
|
||||
|
||||
|
@ -54,8 +46,10 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
|
|||
this.setSound(null, null);
|
||||
};
|
||||
notificationManager.createNotificationChannel(notificationChannel);
|
||||
val contentChannel = StateNotifications.instance.contentNotifChannel
|
||||
notificationManager.createNotificationChannel(contentChannel);
|
||||
try {
|
||||
doSubscriptionUpdating(notificationManager, notificationChannel);
|
||||
doSubscriptionUpdating(notificationManager, notificationChannel, contentChannel);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
exception = ex;
|
||||
|
@ -77,13 +71,13 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
|
|||
}
|
||||
|
||||
|
||||
suspend fun doSubscriptionUpdating(manager: NotificationManager, notificationChannel: NotificationChannel) {
|
||||
val notif = NotificationCompat.Builder(appContext, notificationChannel.id)
|
||||
suspend fun doSubscriptionUpdating(manager: NotificationManager, backgroundChannel: NotificationChannel, contentChannel: NotificationChannel) {
|
||||
val notif = NotificationCompat.Builder(appContext, backgroundChannel.id)
|
||||
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
|
||||
.setContentTitle("Grayjay")
|
||||
.setContentText("Updating subscriptions...")
|
||||
.setSilent(true)
|
||||
.setChannelId(notificationChannel.id)
|
||||
.setChannelId(backgroundChannel.id)
|
||||
.setProgress(1, 0, true);
|
||||
|
||||
manager.notify(12, notif.build());
|
||||
|
@ -94,6 +88,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
|
|||
val newItems = mutableListOf<IPlatformContent>();
|
||||
|
||||
val now = OffsetDateTime.now();
|
||||
val threeDays = now.minusDays(4);
|
||||
val contentNotifs = mutableListOf<Pair<Subscription, IPlatformContent>>();
|
||||
withContext(Dispatchers.IO) {
|
||||
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) {
|
||||
if(!newSubChanges.contains(sub)) {
|
||||
newSubChanges.add(sub);
|
||||
if(sub.doNotifications && content.datetime?.let { it < now } == true)
|
||||
contentNotifs.add(Pair(sub, content));
|
||||
if(sub.doNotifications) {
|
||||
if(content.datetime != null) {
|
||||
if(content.datetime!! <= now.plusMinutes(StateNotifications.instance.plannedWarningMinutesEarly) && content.datetime!! > threeDays)
|
||||
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);
|
||||
}
|
||||
|
@ -135,22 +136,7 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
|
|||
val items = contentNotifs.take(5).toList()
|
||||
for(i in items.indices) {
|
||||
val contentNotif = items.get(i);
|
||||
val thumbnail = if(contentNotif.second is IPlatformVideo) (contentNotif.second as IPlatformVideo).thumbnails.getHQThumbnail()
|
||||
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);
|
||||
StateNotifications.instance.notifyNewContentWithThumbnail(appContext, manager, contentChannel, 13 + i, contentNotif.second);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
|
@ -165,20 +151,4 @@ class BackgroundWorker(private val appContext: Context, private val workerParams
|
|||
.setSilent(true)
|
||||
.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());
|
||||
}
|
||||
}
|
|
@ -58,6 +58,14 @@ class ChannelContentCache {
|
|||
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 {
|
||||
val validID = channelUrl.toSafeFileName();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ class FragmentedStorage {
|
|||
fun initialize(filesDir: File) {
|
||||
_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(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> {
|
||||
|
|
|
@ -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="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="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="auto_update">Auto Update</string>
|
||||
<string name="auto_rotate">Auto-Rotate</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue