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