From 88ca90c13a7eac903882a08ae10c3e33a18ce67c Mon Sep 17 00:00:00 2001
From: Kelvin <kelvin@futo.org>
Date: Thu, 2 Nov 2023 22:23:24 +0100
Subject: [PATCH] Notification improvements, Polycentric subscription
 parallelization, Cache load parallelization

---
 .../com/futo/platformplayer/SettingsDev.kt    | 28 +++++++-
 .../background/BackgroundWorker.kt            | 66 +++++++++++++++----
 .../cache/ChannelContentCache.kt              |  7 +-
 .../fragment/mainactivity/main/FeedView.kt    | 12 +++-
 .../states/StateSubscriptions.kt              |  9 +--
 app/src/main/res/values/strings.xml           |  2 +
 6 files changed, 99 insertions(+), 25 deletions(-)

diff --git a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt
index 15ec7117..a720e3cd 100644
--- a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt
+++ b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt
@@ -2,14 +2,24 @@ package com.futo.platformplayer
 
 import android.content.Context
 import android.webkit.CookieManager
+import androidx.work.Constraints
+import androidx.work.Data
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
 import com.caoccao.javet.values.primitive.V8ValueInteger
 import com.caoccao.javet.values.primitive.V8ValueString
+import com.futo.platformplayer.activities.SettingsActivity
 import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.api.media.models.contents.IPlatformContent
 import com.futo.platformplayer.api.media.platforms.js.JSClient
 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.engine.V8Plugin
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
@@ -28,6 +38,8 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.serialization.*
 import kotlinx.serialization.json.*
+import java.util.UUID
+import java.util.concurrent.TimeUnit
 import java.util.stream.IntStream.range
 import kotlin.system.measureTimeMillis
 
@@ -87,11 +99,23 @@ class SettingsDev : FragmentedStorageFileJson() {
         val cookieManager: CookieManager = CookieManager.getInstance()
         cookieManager.removeAllCookies(null);
     }
+    @FormField(R.string.test_background_worker, FieldForm.BUTTON,
+        R.string.test_background_worker_description, 3)
+    fun triggerBackgroundUpdate() {
+        val act = SettingsActivity.getActivity()!!;
+        UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
+
+        val wm = WorkManager.getInstance(act);
+        val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
+            .setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
+            .build();
+        wm.enqueue(req);
+    }
 
     @Contextual
     @Transient
     @FormField(R.string.v8_benchmarks, FieldForm.GROUP,
-        R.string.various_benchmarks_using_the_integrated_v8_engine, 3)
+        R.string.various_benchmarks_using_the_integrated_v8_engine, 4)
     val v8Benchmarks: V8Benchmarks = V8Benchmarks();
     class V8Benchmarks {
         @FormField(
@@ -139,7 +163,7 @@ class SettingsDev : FragmentedStorageFileJson() {
 
         @FormField(
             R.string.test_v8_communication_speed, FieldForm.BUTTON,
-            R.string.tests_v8_communication_speeds, 2
+            R.string.tests_v8_communication_speeds, 4
         )
         fun testV8RunSpeeds() {
             var plugin: V8Plugin? = null;
diff --git a/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt b/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt
index f0f026b0..d23d6d25 100644
--- a/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt
+++ b/app/src/main/java/com/futo/platformplayer/background/BackgroundWorker.kt
@@ -4,6 +4,8 @@ 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.media.MediaSession2Service.MediaNotification
 import androidx.concurrent.futures.CallbackToFutureAdapter
 import androidx.concurrent.futures.ResolvableFuture
@@ -11,8 +13,12 @@ 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.activities.MainActivity
 import com.futo.platformplayer.api.media.models.contents.IPlatformContent
+import com.futo.platformplayer.api.media.models.video.IPlatformVideo
 import com.futo.platformplayer.getNowDiffSeconds
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.models.Subscription
@@ -29,10 +35,10 @@ import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import java.time.OffsetDateTime
 
-class BackgroundWorker(private val appContext: Context, workerParams: WorkerParameters) :
+class BackgroundWorker(private val appContext: Context, private val workerParams: WorkerParameters) :
     CoroutineWorker(appContext, workerParams) {
     override suspend fun doWork(): Result {
-        if(StateApp.instance.isMainActive) {
+        if(StateApp.instance.isMainActive && !inputData.getBoolean("bypassMainCheck", false)) {
             Logger.i("BackgroundWorker", "CANCELLED");
             return Result.success();
         }
@@ -86,9 +92,10 @@ class BackgroundWorker(private val appContext: Context, workerParams: WorkerPara
         val newSubChanges = hashSetOf<Subscription>();
         val newItems = mutableListOf<IPlatformContent>();
 
+        val now = OffsetDateTime.now();
         val contentNotifs = mutableListOf<Pair<Subscription, IPlatformContent>>();
         withContext(Dispatchers.IO) {
-            StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(true, false,this, { progress, total ->
+            val results = StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(true, false,this, { progress, total ->
                 Logger.i("BackgroundWorker", "SUBSCRIPTION PROGRESS: ${progress}/${total}");
 
                 synchronized(manager) {
@@ -103,29 +110,46 @@ class BackgroundWorker(private val appContext: Context, workerParams: WorkerPara
                 synchronized(newSubChanges) {
                     if(!newSubChanges.contains(sub)) {
                         newSubChanges.add(sub);
-                        if(sub.doNotifications)
+                        if(sub.doNotifications && content.datetime?.let { it < now } == true)
                             contentNotifs.add(Pair(sub, content));
                     }
                     newItems.add(content);
                 }
             });
+
+            //Only for testing notifications
+            val testNotifs = 0;
+            if(contentNotifs.size == 0 && testNotifs > 0) {
+                results.first.getResults().filter { it is IPlatformVideo && it.datetime?.let { it < now } == true }
+                    .take(testNotifs).forEach {
+                        contentNotifs.add(Pair(StateSubscriptions.instance.getSubscriptions().first(), it));
+                    }
+            }
         }
 
         manager.cancel(12);
 
-        if(newItems.size > 0) {
+        if(contentNotifs.size > 0) {
             try {
                 val items = contentNotifs.take(5).toList()
                 for(i in items.indices) {
                     val contentNotif = items.get(i);
-                    manager.notify(13 + i, NotificationCompat.Builder(appContext, notificationChannel.id)
-                        .setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
-                        .setContentTitle("New video by [${contentNotif.first.channel.name}]")
-                        .setContentText("${contentNotif.second.name}")
-                        .setSilent(true)
-                        .setContentIntent(PendingIntent.getActivity(this.appContext, 0, MainActivity.getVideoIntent(this.appContext, contentNotif.second.url),
-                            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
-                        .setChannelId(notificationChannel.id).build());
+                    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);
                 }
             }
             catch(ex: Throwable) {
@@ -140,4 +164,20 @@ class BackgroundWorker(private val appContext: Context, workerParams: WorkerPara
                 .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());
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
index f4ef7d2d..34bd4935 100644
--- a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
+++ b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt
@@ -18,10 +18,11 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import java.time.OffsetDateTime
+import kotlin.streams.toList
 import kotlin.system.measureTimeMillis
 
 class ChannelContentCache {
-    private val _targetCacheSize = 2000;
+    private val _targetCacheSize = 3000;
     val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache");
     val _channelContents: HashMap<String, ManagedStore<SerializedPlatformContent>>;
     init {
@@ -29,11 +30,11 @@ class ChannelContentCache {
         val initializeTime = measureTimeMillis {
             _channelContents = HashMap(allFiles
                 .filter { it.isDirectory }
-                .associate {
+                .parallelStream().map {
                     Pair(it.name, FragmentedStorage.storeJson(_channelCacheDir, it.name, PlatformContentSerializer())
                             .withoutBackup()
                             .load())
-                });
+                }.toList().associate { it })
         }
         val minDays = OffsetDateTime.now().minusDays(10);
         val totalItems = _channelContents.map { it.value.count() }.sum();
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
index 0f70aef3..1dad57ef 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
@@ -122,6 +122,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
 
         _toolbarContentView = findViewById(R.id.container_toolbar_content);
 
+        var filteredNextPageCounter = 0;
         _nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
             if (it is IAsyncPager<*>)
                 it.nextPageAsync();
@@ -141,10 +142,15 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
             val filteredResults = filterResults(it);
             recyclerData.results.addAll(filteredResults);
             recyclerData.resultsUnfiltered.addAll(it);
-            if(filteredResults.isEmpty())
-                loadNextPage()
-            else
+            if(filteredResults.isEmpty()) {
+                filteredNextPageCounter++
+                if(filteredNextPageCounter <= 4)
+                    loadNextPage()
+            }
+            else {
+                filteredNextPageCounter = 0;
                 recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
+            }
         }.exception<Throwable> {
             Logger.w(TAG, "Failed to load next page.", it);
             UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
index 88a12ff5..8272dd24 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
@@ -39,6 +39,7 @@ import java.util.concurrent.ForkJoinTask
 import kotlin.collections.ArrayList
 import kotlin.coroutines.resumeWithException
 import kotlin.coroutines.suspendCoroutine
+import kotlin.streams.toList
 import kotlin.system.measureTimeMillis
 
 /***
@@ -250,12 +251,12 @@ class StateSubscriptions {
         }
 
         val usePolycentric = true;
-        val subUrls = getSubscriptions().associateWith {
+        val subUrls = getSubscriptions().parallelStream().map {
             if(usePolycentric)
-                StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id);
+                Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
             else
-                listOf(it.channel.url);
-        };
+                Pair(it, listOf(it.channel.url));
+        }.toList().associate { it };
 
         val result = algo.getSubscriptions(subUrls);
         return Pair(result.pager, result.exceptions);
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 39259336..2859f2ec 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -276,6 +276,8 @@
     <string name="change_the_external_storage_for_download_files">Change the external storage for download files</string>
     <string name="clear_cookies">Clear Cookies</string>
     <string name="clear_cookies_on_logout">Clear Cookies on Logout</string>
+    <string name="test_background_worker">Test Background Worker</string>
+    <string name="test_background_worker_description"></string>
     <string name="clear_payment">Clear Payment</string>
     <string name="clears_cookies_when_you_log_out">Clears cookies when you log out</string>
     <string name="clears_in_app_browser_cookies">Clears in-app browser cookies</string>
-- 
GitLab