mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
544 lines
23 KiB
Kotlin
544 lines
23 KiB
Kotlin
package com.futo.platformplayer
|
|
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.webkit.CookieManager
|
|
import androidx.work.Data
|
|
import androidx.work.OneTimeWorkRequestBuilder
|
|
import androidx.work.WorkManager
|
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
|
import com.caoccao.javet.values.primitive.V8ValueString
|
|
import com.futo.platformplayer.activities.MainActivity
|
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
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.fragment.mainactivity.main.DeveloperFragment
|
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
|
import com.futo.platformplayer.logging.Logger
|
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
|
import com.futo.platformplayer.states.StateAnnouncement
|
|
import com.futo.platformplayer.states.StateApp
|
|
import com.futo.platformplayer.states.StateCache
|
|
import com.futo.platformplayer.states.StateDeveloper
|
|
import com.futo.platformplayer.states.StateDownloads
|
|
import com.futo.platformplayer.states.StateHistory
|
|
import com.futo.platformplayer.states.StatePlatform
|
|
import com.futo.platformplayer.states.StateSubscriptions
|
|
import com.futo.platformplayer.stores.FragmentedStorage
|
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
|
import com.futo.platformplayer.views.fields.ButtonField
|
|
import com.futo.platformplayer.views.fields.FieldForm
|
|
import com.futo.platformplayer.views.fields.FormField
|
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import kotlinx.serialization.Contextual
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.Transient
|
|
import kotlinx.serialization.encodeToString
|
|
import kotlinx.serialization.json.Json
|
|
import java.time.OffsetDateTime
|
|
import java.util.stream.IntStream.range
|
|
import kotlin.system.measureTimeMillis
|
|
|
|
@Serializable()
|
|
class SettingsDev : FragmentedStorageFileJson() {
|
|
|
|
@FormField(R.string.developer_mode, FieldForm.TOGGLE, -1, 0)
|
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
var developerMode: Boolean = false;
|
|
|
|
@FormField(R.string.development_server, FieldForm.GROUP,
|
|
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 1)
|
|
val devServerSettings: DeveloperServerFields = DeveloperServerFields();
|
|
@Serializable
|
|
class DeveloperServerFields {
|
|
|
|
@FormField(R.string.start_server_on_boot, FieldForm.TOGGLE, -1, 0)
|
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
var devServerOnBoot: Boolean = false;
|
|
|
|
@FormField(R.string.start_server, FieldForm.BUTTON,
|
|
R.string.starts_a_devServer_on_port_11337_may_expose_vulnerabilities, 1)
|
|
fun startServer() {
|
|
StateDeveloper.instance.runServer();
|
|
StateApp.instance.contextOrNull?.let {
|
|
UIDialogs.toast(it, "Dev Started", false);
|
|
};
|
|
}
|
|
}
|
|
|
|
@FormField(R.string.experimental, FieldForm.GROUP,
|
|
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 2)
|
|
val experimentalSettings: ExperimentalFields = ExperimentalFields();
|
|
@Serializable
|
|
class ExperimentalFields {
|
|
|
|
@FormField(R.string.background_subscription_testing, FieldForm.TOGGLE, -1, 0)
|
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
var backgroundSubscriptionFetching: Boolean = false;
|
|
}
|
|
|
|
|
|
@FormField(R.string.cache, FieldForm.GROUP, -1, 3)
|
|
val cache: Cache = Cache();
|
|
@Serializable
|
|
class Cache {
|
|
|
|
@FormField(R.string.subscriptions_cache_5000, FieldForm.BUTTON, -1, 1, "subscription_cache_button")
|
|
fun subscriptionsCache5000() {
|
|
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
|
UIDialogs.toast(
|
|
StateApp.instance.activity!!,
|
|
"Started caching 5000 sub items"
|
|
);
|
|
val button = DeveloperFragment.currentView?.getField("subscription_cache_button");
|
|
if(button is ButtonField)
|
|
button.setButtonEnabled(false);
|
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
|
try {
|
|
val subsCache =
|
|
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this).first;
|
|
|
|
var total = 0;
|
|
var page = 0;
|
|
var lastToast = System.currentTimeMillis();
|
|
while(subsCache.hasMorePages() && total < 5000) {
|
|
subsCache.nextPage();
|
|
total += subsCache.getResults().size;
|
|
page++;
|
|
|
|
if(page % 10 == 0)
|
|
withContext(Dispatchers.Main) {
|
|
val diff = System.currentTimeMillis() - lastToast;
|
|
lastToast = System.currentTimeMillis();
|
|
UIDialogs.toast(
|
|
StateApp.instance.activity!!,
|
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
|
);
|
|
}
|
|
Thread.sleep(250);
|
|
}
|
|
|
|
withContext(Dispatchers.Main) {
|
|
UIDialogs.toast(
|
|
StateApp.instance.activity!!,
|
|
"FINISHED Page: ${page}, Total: ${total}"
|
|
);
|
|
}
|
|
}
|
|
catch(ex: Throwable) {
|
|
Logger.e("SettingsDev", ex.message, ex);
|
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
|
}
|
|
finally {
|
|
withContext(Dispatchers.Main) {
|
|
if(button is ButtonField)
|
|
button.setButtonEnabled(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@FormField(R.string.history_cache_100, FieldForm.BUTTON, -1, 1, "history_cache_button")
|
|
fun historyCache100() {
|
|
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
|
UIDialogs.toast(
|
|
StateApp.instance.activity!!,
|
|
"Started caching 100 history items (from home)"
|
|
);
|
|
val button = DeveloperFragment.currentView?.getField("history_cache_button");
|
|
if(button is ButtonField)
|
|
button.setButtonEnabled(false);
|
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
|
try {
|
|
val subsCache = StatePlatform.instance.getHome();
|
|
|
|
var num = 0;
|
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
|
num++;
|
|
}
|
|
|
|
var total = 0;
|
|
var page = 0;
|
|
var lastToast = System.currentTimeMillis();
|
|
while(subsCache.hasMorePages() && total < 5000) {
|
|
subsCache.nextPage();
|
|
total += subsCache.getResults().size;
|
|
page++;
|
|
|
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
|
num++;
|
|
}
|
|
|
|
if(page % 4 == 0)
|
|
withContext(Dispatchers.Main) {
|
|
val diff = System.currentTimeMillis() - lastToast;
|
|
lastToast = System.currentTimeMillis();
|
|
UIDialogs.toast(
|
|
StateApp.instance.activity!!,
|
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
|
);
|
|
}
|
|
Thread.sleep(500);
|
|
}
|
|
|
|
withContext(Dispatchers.Main) {
|
|
UIDialogs.toast(
|
|
StateApp.instance.activity!!,
|
|
"FINISHED Page: ${page}, Total: ${total}"
|
|
);
|
|
}
|
|
}
|
|
catch(ex: Throwable) {
|
|
Logger.e("SettingsDev", ex.message, ex);
|
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
|
}
|
|
finally {
|
|
withContext(Dispatchers.Main) {
|
|
if(button is ButtonField)
|
|
button.setButtonEnabled(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@FormField(R.string.crash_me, FieldForm.BUTTON,
|
|
R.string.crashes_the_application_on_purpose, 3)
|
|
fun crashMe() {
|
|
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
|
}
|
|
|
|
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
|
R.string.delete_all_announcements, 3)
|
|
fun deleteAnnouncements() {
|
|
StateAnnouncement.instance.deleteAllAnnouncements();
|
|
}
|
|
|
|
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
|
|
R.string.clear_all_cookies_from_the_cookieManager, 3)
|
|
fun clearCookies() {
|
|
val cookieManager: CookieManager = CookieManager.getInstance()
|
|
cookieManager.removeAllCookies(null);
|
|
}
|
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
|
R.string.test_background_worker_description, 4)
|
|
fun triggerBackgroundUpdate() {
|
|
val act = StateApp.instance.activity!!;
|
|
try {
|
|
UIDialogs.toast(StateApp.instance.activity!!, "Starting test background worker");
|
|
|
|
val wm = WorkManager.getInstance(act);
|
|
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
|
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
|
.build();
|
|
wm.enqueue(req);
|
|
} catch (e: Throwable) {
|
|
UIDialogs.showGeneralErrorDialog(act, "Failed to trigger background update", e)
|
|
}
|
|
}
|
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
|
R.string.test_background_worker_description, 4)
|
|
fun clearChannelContentCache() {
|
|
UIDialogs.toast(StateApp.instance.activity!!, "Clearing cache");
|
|
StateCache.instance.clearToday();
|
|
UIDialogs.toast(StateApp.instance.activity!!, "Cleared");
|
|
}
|
|
|
|
|
|
@Contextual
|
|
@Transient
|
|
@FormField(R.string.v8_benchmarks, FieldForm.GROUP,
|
|
R.string.various_benchmarks_using_the_integrated_v8_engine, 4)
|
|
val v8Benchmarks: V8Benchmarks = V8Benchmarks();
|
|
class V8Benchmarks {
|
|
@FormField(
|
|
R.string.test_v8_creation_speed, FieldForm.BUTTON,
|
|
R.string.tests_v8_creation_times_and_running, 1
|
|
)
|
|
fun testV8Creation() {
|
|
var plugin: V8Plugin? = null;
|
|
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
|
|
try {
|
|
val count = 1000;
|
|
val timeStart = System.currentTimeMillis();
|
|
for (i in range(0, count)) {
|
|
val v8 = V8Plugin(
|
|
StateApp.instance.context,
|
|
SourcePluginConfig("Test", "", "", "", "", ""),
|
|
"var i = 0; function test() { i = i + 1; return i; }"
|
|
);
|
|
|
|
v8.start();
|
|
if (v8.executeTyped<V8ValueInteger>("test()").value != 1)
|
|
throw java.lang.IllegalStateException("Test didn't properly respond");
|
|
v8.stop();
|
|
}
|
|
val timeEnd = System.currentTimeMillis();
|
|
val resp = "Restarted V8 ${count} times in ${(timeEnd - timeStart)}ms, ${(timeEnd - timeStart) / count}ms per instance\n(initializing, calling function with value, destroying)"
|
|
Logger.i("SettingsDev", resp);
|
|
|
|
withContext(Dispatchers.Main) {
|
|
StateApp.instance.contextOrNull?.let {
|
|
UIDialogs.toast(it, resp);
|
|
};
|
|
}
|
|
} catch (ex: Exception) {
|
|
withContext(Dispatchers.Main) {
|
|
StateApp.withContext {
|
|
UIDialogs.toast(it, "Failed: " + ex.message);
|
|
};
|
|
}
|
|
} finally {
|
|
plugin?.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
@FormField(
|
|
R.string.test_v8_communication_speed, FieldForm.BUTTON,
|
|
R.string.tests_v8_communication_speeds, 4
|
|
)
|
|
fun testV8RunSpeeds() {
|
|
var plugin: V8Plugin? = null;
|
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
|
try {
|
|
val count = 10000;
|
|
var str = "012346789012346789012346789012346789012346789";
|
|
val v8 = V8Plugin(
|
|
StateApp.instance.context,
|
|
SourcePluginConfig("Test"),
|
|
"function test(str) { return str; }"
|
|
);
|
|
v8.start();
|
|
val timeStart = System.currentTimeMillis();
|
|
for (i in range(0, count)) {
|
|
if (v8.executeTyped<V8ValueString>("test(\"" + str + "\")").value != str)
|
|
throw java.lang.IllegalStateException("Test didn't properly respond");
|
|
}
|
|
val timeEnd = System.currentTimeMillis();
|
|
v8.stop();
|
|
|
|
val resp = "Ran V8 ${count} times in ${(timeEnd - timeStart)}ms, ${(timeEnd - timeStart) / count}ms per instance\n(passing a string[50] back and forth)";
|
|
Logger.i("SettingsDev", resp);
|
|
withContext(Dispatchers.Main) {
|
|
StateApp.withContext {
|
|
UIDialogs.toast(it, resp);
|
|
};
|
|
}
|
|
} catch (ex: Exception) {
|
|
withContext(Dispatchers.Main) {
|
|
StateApp.withContext {
|
|
UIDialogs.toast(it, "Failed: " + ex.message);
|
|
};
|
|
}
|
|
} finally {
|
|
plugin?.stop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Contextual
|
|
@Transient
|
|
@FormField(R.string.v8_script_testing, FieldForm.GROUP, R.string.various_tests_against_a_custom_source, 4)
|
|
val v8ScriptTests: V8ScriptTests = V8ScriptTests();
|
|
class V8ScriptTests {
|
|
@Contextual
|
|
private var _currentPlugin : JSClient? = null;
|
|
@FormField(R.string.inject, FieldForm.BUTTON, R.string.injects_a_test_source_config_local_into_v8, 1)
|
|
fun testV8Init() {
|
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
|
try {
|
|
_currentPlugin =
|
|
getTestPlugin("http://192.168.1.132/Public/FUTO/TestConfig.json");
|
|
|
|
withContext(Dispatchers.Main) {
|
|
UIDialogs.toast(StateApp.instance.context, "TestPlugin injected");
|
|
}
|
|
}
|
|
catch(ex: Exception) {
|
|
toast(ex.message ?: "");
|
|
}
|
|
}
|
|
}
|
|
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
|
|
fun testV8Home() {
|
|
runTestPlugin(_currentPlugin) {
|
|
var home: IPager<IPlatformContent>?;
|
|
val resultPage1: String;
|
|
val resultPage2: String;
|
|
val page1Time = measureTimeMillis {
|
|
home = it.getHome();
|
|
val results = home!!.getResults();
|
|
resultPage1 = "Page1 Results=[${results.size}] HasMore=${home!!.hasMorePages()}\nResult[0]=${results.firstOrNull()?.name}";
|
|
}
|
|
toast(resultPage1);
|
|
val page2Time = measureTimeMillis {
|
|
home!!.nextPage();
|
|
val results = home!!.getResults();
|
|
resultPage2 = "Page2 Results=[${results.size}] HasMore=${home!!.hasMorePages()}\nResult[0]=${results.firstOrNull()?.name}";
|
|
}
|
|
toast(resultPage2);
|
|
toast("Page1: ${page1Time}ms, Page2: ${page2Time}ms");
|
|
}
|
|
}
|
|
|
|
private fun toast(str: String, isLong: Boolean = false) {
|
|
StateApp.instance.scope.launch(Dispatchers.Main) {
|
|
try {
|
|
UIDialogs.toast(StateApp.instance.context, str, isLong);
|
|
} catch (e: Throwable) {
|
|
Logger.e("SettingsDev", "Failed to show toast", e)
|
|
}
|
|
}
|
|
}
|
|
private fun runTestPlugin(plugin: JSClient?, handler: (JSClient) -> Unit) {
|
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
|
try {
|
|
if (plugin == null)
|
|
throw IllegalStateException("Test plugin not loaded, inject first");
|
|
else
|
|
handler(plugin);
|
|
} catch (ex: Exception) {
|
|
Logger.e("ScriptTesting", ex.message ?: "", ex);
|
|
toast("Failed: " + ex.message, true);
|
|
}
|
|
}
|
|
}
|
|
private fun getTestPlugin(configUrl: String) : JSClient {
|
|
val configResp =
|
|
ManagedHttpClient().get(configUrl);
|
|
if (!configResp.isOk || configResp.body == null)
|
|
throw IllegalStateException("Failed to load config");
|
|
val config = Json.decodeFromString<SourcePluginConfig>(configResp.body.string());
|
|
|
|
val scriptResp = ManagedHttpClient().get(config.absoluteScriptUrl);
|
|
if (!scriptResp.isOk || scriptResp.body == null)
|
|
throw IllegalStateException("Failed to load script");
|
|
val script = scriptResp.body.string();
|
|
|
|
val client = JSClient(StateApp.instance.context, SourcePluginDescriptor(config), null, script);
|
|
client.initialize();
|
|
|
|
return client;
|
|
}
|
|
}
|
|
|
|
|
|
@Contextual
|
|
@Transient
|
|
@FormField(R.string.other, FieldForm.GROUP, R.string.others_ellipsis, 5)
|
|
val otherTests: OtherTests = OtherTests();
|
|
class OtherTests {
|
|
@FormField(R.string.unsubscribe_all, FieldForm.BUTTON, R.string.removes_all_subscriptions, -1)
|
|
fun unsubscribeAll() {
|
|
val toUnsub = StateSubscriptions.instance.getSubscriptions();
|
|
UIDialogs.toast("Started unsubbing.. (${toUnsub.size})")
|
|
toUnsub.forEach {
|
|
StateSubscriptions.instance.removeSubscription(it.channel.url);
|
|
};
|
|
UIDialogs.toast("Finished unsubbing.. (${toUnsub.size})")
|
|
}
|
|
@FormField(R.string.clear_downloads, FieldForm.BUTTON, R.string.deletes_all_ongoing_downloads, 1)
|
|
fun clearDownloads() {
|
|
StateDownloads.instance.getDownloading().forEach {
|
|
StateDownloads.instance.removeDownload(it);
|
|
};
|
|
}
|
|
@FormField(R.string.clear_all_downloaded, FieldForm.BUTTON, R.string.deletes_all_downloaded_videos_and_related_files, 2)
|
|
fun clearDownloaded() {
|
|
StateDownloads.instance.getDownloadedVideos().forEach {
|
|
StateDownloads.instance.deleteCachedVideo(it.id);
|
|
};
|
|
}
|
|
@FormField(R.string.delete_unresolved, FieldForm.BUTTON, R.string.deletes_all_unresolved_source_files, 3)
|
|
fun cleanupDownloads() {
|
|
StateDownloads.instance.cleanupDownloads();
|
|
}
|
|
|
|
@FormField(R.string.fill_storage_till_error, FieldForm.BUTTON, R.string.writes_to_disk_till_no_space_is_left, 4)
|
|
fun fillStorage(context: Context, scope: CoroutineScope?) {
|
|
val gigabuffer = ByteArray(1024 * 1024 * 128);
|
|
var count: Long = 0;
|
|
|
|
UIDialogs.toast("Starting filling up space..");
|
|
|
|
scope?.launch(Dispatchers.IO) {
|
|
try {
|
|
do {
|
|
Logger.i("Developer", "Total: ${count}, Storage: ${(count * gigabuffer.size).toHumanBytesSize()}")
|
|
val tempFile = StateApp.instance.getTempFile();
|
|
tempFile.writeBytes(gigabuffer);
|
|
count++;
|
|
|
|
if(count % 50 == 0L) {
|
|
StateApp.instance.scopeOrNull?.launch (Dispatchers.Main) {
|
|
UIDialogs.toast(context, "Filled up ${(count * gigabuffer.size).toHumanBytesSize()}");
|
|
}
|
|
}
|
|
} while (true);
|
|
} catch (ex: Throwable) {
|
|
withContext(Dispatchers.Main) {
|
|
UIDialogs.toast("Total: ${count}, Storage: ${(count * gigabuffer.size).toHumanBytesSize()}\nError: ${ex.message}");
|
|
UIDialogs.showGeneralErrorDialog(context, ex.message ?: "", ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@FormField(R.string.test_playback, FieldForm.BUTTON,
|
|
R.string.test_playback, 1)
|
|
fun testPlayback(context: Context) {
|
|
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@FormField(R.string.networking, FieldForm.GROUP, -1, 18)
|
|
var networking = Networking();
|
|
@Serializable
|
|
class Networking {
|
|
@FormField(R.string.allow_all_certificates, FieldForm.TOGGLE, -1, 0)
|
|
@FormFieldWarning(R.string.allow_all_certificates_warning)
|
|
var allowAllCertificates: Boolean = false;
|
|
}
|
|
|
|
|
|
@Contextual
|
|
@Transient
|
|
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
|
var info = Info();
|
|
@Serializable
|
|
class Info {
|
|
@FormField(R.string.dev_info_channel_cache_size, FieldForm.READONLYTEXT, -1, 1, "channelCacheSize")
|
|
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
|
}
|
|
|
|
|
|
|
|
//region BOILERPLATE
|
|
override fun encode(): String {
|
|
return Json.encodeToString(this);
|
|
}
|
|
|
|
companion object {
|
|
val instance: SettingsDev get() {
|
|
return FragmentedStorage.get<SettingsDev>();
|
|
}
|
|
}
|
|
//endregion
|
|
} |