Compare commits

...

14 Commits

Author SHA1 Message Date
Koen J 1579deaec2 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2026-05-27 21:03:27 +02:00
Koen J b0020a3fbb Busy fixes. 2026-05-27 21:03:07 +02:00
Koen J 620d4f8fb7 Last 4 seconds cut off from UMP downloads fix #2243. 2026-05-27 16:08:06 +02:00
Koen J 9c0c8fe927 Implemented performance optimization for composite trust 2026-05-27 14:37:30 +02:00
Koen J 1263070fc9 Added setting to use downloaded CA bundle also for OkHttp 2026-05-27 14:13:37 +02:00
Koen J 336f30a631 Work for diagnosing #3279 2026-05-27 12:48:21 +02:00
Koen J 9ec4b76d5e Potentail fixes for #3129 2026-05-27 10:55:33 +02:00
koen-futo 91bea8faf2 Merge pull request #3301 from GRBaset/master
Persist subtitle setting across restarts with optional fallback to first found
2026-05-27 10:30:20 +02:00
GRBaset 1d6f2d2ff7 Persist subtitle language to storage 2026-05-23 18:32:35 +02:00
Koen 32686215c4 Merge branch 'new-plugin-nasa-plus' into 'master'
Add NASA+ plugin

See merge request videostreaming/grayjay!174
2026-05-20 10:31:51 +00:00
Stefan f226669b77 Add NASA+ plugin 2026-05-20 11:26:24 +01:00
Koen J a792dea4c5 Build fix. 2026-05-08 17:50:30 +02:00
Koen J a7fc549afb Automatic updates for plugins defaults to true and made the loading bar smaller. 2026-05-08 17:40:57 +02:00
Koen J b345ba5ca3 Updated submodules. 2026-05-08 16:12:50 +02:00
71 changed files with 351 additions and 227 deletions
+6
View File
@@ -124,3 +124,9 @@
[submodule "app/src/stable/assets/sources/fosdem"]
path = app/src/stable/assets/sources/fosdem
url = ../plugins/fosdem.git
[submodule "app/src/unstable/assets/sources/nasa-plus"]
path = app/src/unstable/assets/sources/nasa-plus
url = ../plugins/nasa-plus.git
[submodule "app/src/stable/assets/sources/nasa-plus"]
path = app/src/stable/assets/sources/nasa-plus
url = ../plugins/nasa-plus.git
+1 -1
View File
@@ -60,7 +60,7 @@
<activity
android:name=".activities.MainActivity"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|smallestScreenSize|screenLayout|uiMode|density|fontScale"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:windowSoftInputMode="adjustPan"
@@ -134,6 +134,11 @@ inline fun V8Value.ensureIsBusy() {
}
}
fun V8Value.requireSourcePlugin(context: String): V8Plugin {
return getSourcePlugin()
?: throw IllegalStateException("$context: V8 object's plugin runtime is no longer available");
}
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
ensureIsBusy();
return when(T::class) {
@@ -698,6 +698,11 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
@Serializable(with = FlexibleBooleanSerializer::class)
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
@AdvancedField
@FormField(R.string.use_downloaded_ca_bundle, FieldForm.TOGGLE, R.string.use_downloaded_ca_bundle_description, 1)
@Serializable(with = FlexibleBooleanSerializer::class)
var useDownloadedCABundle: Boolean = false;
}
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
@@ -290,6 +290,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
exIntent.putExtra(ExceptionActivity.EXTRA_STACK, message);
startActivity(exIntent);
Logger.flushBlocking();
Runtime.getRuntime().exit(0);
}
}
@@ -1,6 +1,7 @@
package com.futo.platformplayer.api.http
import androidx.collection.arrayMapOf
import com.futo.platformplayer.Settings
import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread
@@ -17,11 +18,17 @@ import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.io.File
import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.time.Duration
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
import kotlin.system.measureTimeMillis
@@ -64,10 +71,17 @@ open class ManagedHttpClient {
Logger.w(TAG, "Creating INSECURE client (TrustAll)");
}
private fun applyMergedTrustStore(builder: OkHttpClient.Builder) {
val pair = getOrBuildCompositeTrustStore() ?: return;
builder.sslSocketFactory(pair.first, pair.second);
}
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
trustAllCertificates(builder);
else if(FragmentedStorage.isInitialized && Settings.instance.browsing.useDownloadedCABundle)
applyMergedTrustStore(builder);
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
@@ -328,5 +342,76 @@ open class ManagedHttpClient {
companion object {
val TAG = "ManagedHttpClient";
@Volatile private var _cachedCompositePair: Pair<SSLSocketFactory, X509TrustManager>? = null;
@Volatile private var _cachedCompositeMtime: Long = -1L;
private val _compositeBuildLock = Any();
private fun getOrBuildCompositeTrustStore(): Pair<SSLSocketFactory, X509TrustManager>? {
val context = StateApp.instance.contextOrNull ?: return null;
val bundleFile = File(context.noBackupFilesDir, "curl-ca-bundle.pem");
if (!bundleFile.exists()) {
Logger.w(TAG, "useDownloadedCABundle requested but bundle file not present yet");
return null;
}
val modTime = bundleFile.lastModified();
val cached = _cachedCompositePair;
if (cached != null && _cachedCompositeMtime == modTime) {
return cached;
}
synchronized(_compositeBuildLock) {
val recheck = _cachedCompositePair;
if (recheck != null && _cachedCompositeMtime == modTime) {
return recheck;
}
try {
val defaultAlgo = TrustManagerFactory.getDefaultAlgorithm();
val platformTmf = TrustManagerFactory.getInstance(defaultAlgo);
platformTmf.init(null as KeyStore?);
val platformTm = platformTmf.trustManagers.firstOrNull { it is X509TrustManager } as? X509TrustManager
?: return null;
val bundleKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null, null) };
val cf = CertificateFactory.getInstance("X.509");
val certs = bundleFile.inputStream().use { cf.generateCertificates(it) };
certs.forEachIndexed { i, cert -> bundleKeyStore.setCertificateEntry("bundle-$i", cert) };
val bundleTmf = TrustManagerFactory.getInstance(defaultAlgo);
bundleTmf.init(bundleKeyStore);
val bundleTm = bundleTmf.trustManagers.firstOrNull { it is X509TrustManager } as? X509TrustManager
?: return null;
val composite = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
platformTm.checkClientTrusted(chain, authType);
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
try {
platformTm.checkServerTrusted(chain, authType);
} catch (primary: CertificateException) {
bundleTm.checkServerTrusted(chain, authType);
}
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return platformTm.acceptedIssuers + bundleTm.acceptedIssuers;
}
};
val sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, arrayOf<TrustManager>(composite), SecureRandom());
val pair = Pair(sslContext.socketFactory, composite as X509TrustManager);
_cachedCompositePair = pair;
_cachedCompositeMtime = modTime;
Logger.i(TAG, "Built platform+bundle composite trust manager (${certs.size} extra certs from cacert.pem); cached until bundle mtime changes");
return pair;
} catch (ex: Throwable) {
Logger.e(TAG, "Failed to build downloaded CA bundle; will use platform default", ex);
return null;
}
}
}
}
}
@@ -100,7 +100,7 @@ class SourcePluginDescriptor {
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
var checkForUpdates: Boolean = true;
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
var automaticUpdate: Boolean = false;
var automaticUpdate: Boolean = true;
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
var tabEnabled = TabEnabled();
@@ -22,6 +22,7 @@ import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails(
@@ -97,14 +98,14 @@ open class JSArticleDetails(
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return client.busy {
return _content.requireSourcePlugin("ArticleDetails.getContentRecommendations").busy {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return@busy JSContentPager(_pluginConfig, client, contentPager);
}
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
return client.busy {
return _content.requireSourcePlugin("ArticleDetails.getComments").busy {
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return@busy JSCommentPager(_pluginConfig, client, commentPager);
}
@@ -13,6 +13,7 @@ import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import java.time.LocalDateTime
import java.time.OffsetDateTime
@@ -82,7 +83,7 @@ class JSComment : IPlatformComment {
return null;
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return plugin.busy {
return _comment!!.requireSourcePlugin("Comment.getReplies").busy {
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
return@busy JSCommentPager(_config!!, plugin, obj);
}
@@ -6,12 +6,13 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.requireSourcePlugin
class JSCommentPager : JSPager<IPlatformComment>, IPager<IPlatformComment> {
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) { }
override fun convertResult(obj: V8ValueObject): IPlatformComment {
return JSComment(config, plugin.getUnderlyingPlugin(), obj);
return JSComment(config, obj.requireSourcePlugin("JSCommentPager.convertResult"), obj);
}
}
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.warnIfMainThread
@@ -23,14 +24,14 @@ abstract class JSPager<T> : IPager<T> {
protected var _hasMorePages: Boolean = false;
//private var _morePagesWasFalse: Boolean = false;
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
val isAvailable get() = !pager.v8Runtime.isClosed && !pager.v8Runtime.isDead;
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) {
this.plugin = plugin;
this.pager = pager;
this.config = config;
plugin.busy {
requirePagerPluginV8("init").busy {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
getResults();
@@ -40,8 +41,13 @@ abstract class JSPager<T> : IPager<T> {
return config;
}
protected fun requirePagerPluginV8(context: String): V8Plugin {
return pager.getSourcePlugin()
?: throw IllegalStateException("[${plugin.config.name}] JSPager.$context: pager runtime is no longer available");
}
override fun hasMorePages(): Boolean {
return plugin.getUnderlyingPlugin().busy {
return requirePagerPluginV8("hasMorePages").busy {
_hasMorePages && !pager.isClosed;
}
}
@@ -49,7 +55,7 @@ abstract class JSPager<T> : IPager<T> {
override fun nextPage() {
warnIfMainThread("JSPager.nextPage");
val pluginV8 = plugin.getUnderlyingPlugin();
val pluginV8 = requirePagerPluginV8("nextPage");
pluginV8.busy {
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invokeV8("nextPage", arrayOf<Any>());
@@ -79,7 +85,7 @@ abstract class JSPager<T> : IPager<T> {
warnIfMainThread("JSPager.getResults");
return plugin.getUnderlyingPlugin().busy {
return requirePagerPluginV8("getResults").busy {
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
throw IllegalStateException("Runtime closed");
@@ -8,6 +8,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread
@@ -55,7 +56,7 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasCalledInit)
return;
_client.busy {
_obj.requireSourcePlugin("PlaybackTracker.onInit").busy {
if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeV8Void("onInit", seconds);
@@ -72,7 +73,7 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit)
onInit(seconds);
else {
_client.busy {
_obj.requireSourcePlugin("PlaybackTracker.onProgress").busy {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
_obj.invokeV8Void("onProgress", Math.floor(seconds), isPlaying);
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
@@ -86,7 +87,7 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasOnConcluded) {
synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded");
_client.busy {
_obj.requireSourcePlugin("PlaybackTracker.onConcluded").busy {
_obj.invokeV8Void("onConcluded", -1);
}
}
@@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.states.StateDeveloper
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
@@ -93,14 +94,14 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
return getContentRecommendationsJS(jsClient);
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return client.busy {
return _content.requireSourcePlugin("PostDetails.getContentRecommendations").busy {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return@busy JSContentPager(_pluginConfig, client, contentPager);
}
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
return client.busy {
return _content.requireSourcePlugin("PostDetails.getComments").busy {
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return@busy JSCommentPager(_pluginConfig, client, commentPager);
}
@@ -14,9 +14,11 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.Dispatchers
@@ -58,7 +60,7 @@ class JSRequestExecutor: AutoCloseable {
//TODO: Executor properties?
@Throws(ScriptException::class)
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
return _plugin.getUnderlyingPlugin().busy {
return _executor.requireSourcePlugin("JSRequestExecutor.executeRequest").busy {
if (_executor.isClosed)
throw IllegalStateException("Executor object is closed");
@@ -114,7 +116,8 @@ class JSRequestExecutor: AutoCloseable {
open fun cleanup() {
_plugin.busy {
val pluginV8 = _executor.getSourcePlugin() ?: return;
pluginV8.busy {
synchronized(_cleanLock) {
if (!hasCleanup || _executor.isClosed || _cleaned)
return@busy;
@@ -122,7 +125,7 @@ class JSRequestExecutor: AutoCloseable {
}
}
Logger.i("JSRequestExecutor", "JSRequestExecutor cleanup requested");
_plugin.busy {
pluginV8.busy {
if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
@@ -13,6 +13,7 @@ import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.requireSourcePlugin
class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
@@ -36,7 +37,7 @@ class JSRequestModifier: IRequestModifier {
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _plugin.busy {
return _modifier.requireSourcePlugin("JSRequestModifier.modifyRequest").busy {
if (_modifier.isClosed) {
return@busy Request(url, headers);
}
@@ -23,7 +23,7 @@ class JSVODEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
fun nextPage(ms: Int) = plugin.isBusyWith("JSLiveEventPager.nextPage") {
warnIfMainThread("VODEventPager.nextPage");
val pluginV8 = plugin.getUnderlyingPlugin();
val pluginV8 = requirePagerPluginV8("nextPage");
pluginV8.busy {
val newPager: V8Value = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage(...)") {
pager.invokeV8<V8Value>("nextPage", ms);
@@ -32,8 +32,8 @@ class JSVODEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
pager = newPager;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}
override fun nextPage() = nextPage(0);
@@ -26,6 +26,7 @@ import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -118,7 +119,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getPlaybackTrackerJS();
}
private fun getPlaybackTrackerJS(): IPlaybackTracker? {
return _plugin.busy {
return _content.requireSourcePlugin("VideoDetails.getPlaybackTracker").busy {
V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
val tracker = _content.invokeV8<V8Value>("getPlaybackTracker", arrayOf<Any>())
?: return@catchScriptErrors null;
@@ -147,7 +148,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return _plugin.busy {
return _content.requireSourcePlugin("VideoDetails.getContentRecommendations").busy {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return@busy JSContentPager(_pluginConfig, client, contentPager);
}
@@ -171,7 +172,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
}
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
return _plugin.busy {
return _content.requireSourcePlugin("VideoDetails.getComments").busy {
val commentPager = _content.invokeV8<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return@busy null;
@@ -183,7 +184,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
fun hasVODEvents(): Boolean{
return _hasGetVODEvents;
}
fun getVODEvents(url: String): IPager<IPlatformLiveEvent>? = _plugin.busy {
fun getVODEvents(url: String): IPager<IPlatformLiveEvent>? = _content.requireSourcePlugin("VideoDetails.getVODEvents").busy {
if(!_hasGetVODEvents)
return@busy null;
@@ -8,6 +8,7 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.requireSourcePlugin
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val licenseUri: String
@@ -23,7 +24,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
return _plugin.busy {
return _obj.requireSourcePlugin("JSAudioUrlWidevineSource.getLicenseRequestExecutor").busy {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return@busy null
@@ -19,6 +19,7 @@ import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -67,7 +68,8 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_plugin.busy { _obj.isClosed })
val pluginV8 = _obj.requireSourcePlugin("DashManifestRawAudioSource.generateAsync");
if(pluginV8.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed");
val pregenerated = _pregenerate;
@@ -76,25 +78,23 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
return pregenerated;
}
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null;
if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
pluginV8.catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
pluginV8.busy {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
result = pluginV8.catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
pluginV8.busy {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
return plugin.busy {
return pluginV8.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
@@ -111,29 +111,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
override fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_plugin.busy { _obj.isClosed })
val pluginV8 = _obj.requireSourcePlugin("DashManifestRawAudioSource.generate");
if(pluginV8.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: String? = null;
if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
pluginV8.catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
pluginV8.busy {
_obj.invokeV8<V8ValueString>("generate").value;
}
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
result = pluginV8.catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
pluginV8.busy {
_obj.invokeV8<V8ValueString>("generate").value;
}
}
if(result != null){
plugin.busy {
pluginV8.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
@@ -19,6 +19,7 @@ import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -90,7 +91,8 @@ open class JSDashManifestRawSource(
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_plugin.busy { _obj.isClosed })
val pluginV8 = _obj.requireSourcePlugin("DashManifestRawSource.generateAsync");
if(pluginV8.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed");
val pregenerated = _pregenerate;
if(pregenerated != null) {
@@ -98,26 +100,24 @@ open class JSDashManifestRawSource(
return pregenerated;
}
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null;
if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
pluginV8.catchScriptErrors("DashManifestRaw.generate", "generate()", {
pluginV8.busy {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
result = pluginV8.catchScriptErrors("DashManifestRaw.generate", "generate()", {
pluginV8.busy {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
return plugin.busy {
return pluginV8.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
@@ -142,28 +142,29 @@ open class JSDashManifestRawSource(
override open fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_plugin.busy { _obj.isClosed })
val pluginV8 = _obj.requireSourcePlugin("DashManifestRawSource.generate");
if(pluginV8.busy { _obj.isClosed })
throw IllegalStateException("Source object already closed");
var result: String? = null;
if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
pluginV8.catchScriptErrors("DashManifestRaw.generate", "generate()", {
pluginV8.busy {
_obj.invokeV8<V8ValueString>("generate").value;
}
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
result = pluginV8.catchScriptErrors("DashManifestRaw.generate", "generate()", {
pluginV8.busy {
_obj.invokeV8<V8ValueString>("generate").value;
}
});
if(result != null){
_plugin.busy {
pluginV8.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
@@ -11,6 +11,7 @@ import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.requireSourcePlugin
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
IDashManifestWidevineSource, JSSource {
@@ -49,7 +50,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
return _plugin.busy {
return _obj.requireSourcePlugin("JSDashManifestWidevineSource.getLicenseRequestExecutor").busy {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return@busy null
@@ -19,6 +19,7 @@ import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull
import com.futo.platformplayer.requireSourcePlugin
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
abstract class JSSource {
@@ -66,27 +67,27 @@ abstract class JSSource {
hasRequestExecutor = parsedHasRequestExecutor;
}
fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") {
fun getRequestModifier(): IRequestModifier? = _obj.requireSourcePlugin("JSSource.getRequestModifier").busy {
if(_requestModifier != null)
return@isBusyWith AdhocRequestModifier { url, headers ->
return@busy AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
};
if (!hasRequestModifier || _obj.isClosed)
return@isBusyWith null;
return@busy null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
_obj.invokeV8("getRequestModifier", arrayOf<Any>());
};
if (result !is V8ValueObject)
return@isBusyWith null;
return@busy null;
return@isBusyWith JSRequestModifier(_plugin, result)
return@busy JSRequestModifier(_plugin, result)
}
open fun getRequestExecutor(): JSRequestExecutor? = _plugin.isBusyWith("getRequestExecutor") {
open fun getRequestExecutor(): JSRequestExecutor? = _obj.requireSourcePlugin("JSSource.getRequestExecutor").busy {
if (!hasRequestExecutor || _obj.isClosed)
return@isBusyWith null;
return@busy null;
Logger.v("JSSource", "Request executor for [${type}] requesting");
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
@@ -96,9 +97,9 @@ abstract class JSSource {
Logger.v("JSSource", "Request executor for [${type}] received");
if (result !is V8ValueObject)
return@isBusyWith null;
return@busy null;
return@isBusyWith JSRequestExecutor(_plugin, result)
return@busy JSRequestExecutor(_plugin, result)
}
fun getUnderlyingPlugin(): JSClient? {
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.requireSourcePlugin
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
override val licenseUri: String
@@ -22,7 +23,7 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
return _plugin.busy {
return _obj.requireSourcePlugin("JSVideoUrlWidevineSource.getLicenseRequestExecutor").busy {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return@busy null
@@ -1024,6 +1024,31 @@ class VideoDownload {
}
}
}
try {
val tailUrl = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
Logger.i(TAG, "Downloading tail segment (segIndex=$indexCounter)");
val tailData = executeOrGet(client, executor, modifier, tailUrl);
fileStream.write(tailData, 0, tailData.size);
speedTracker.addWork(tailData.size.toLong());
written += tailData.size;
} catch (ex: Throwable) {
Logger.w(TAG, "$name tail segment fetch (segIndex=$indexCounter) failed; continuing without it", ex);
}
if (foundCues2 != null && foundTemplateUrl2 != null && fileStream2 != null) {
try {
val tailUrl2 = foundTemplateUrl2!!.replace("\$Number\$", foundCues2.size.toString());
Logger.i(TAG, "Downloading audio tail segment (segIndex=${foundCues2.size})");
val tailData2 = executeOrGet(client, executor, modifier, tailUrl2);
fileStream2.write(tailData2, 0, tailData2.size);
speedTracker.addWork(tailData2.size.toLong());
written2 += tailData2.size;
} catch (ex: Throwable) {
Logger.w(TAG, "$name(audio) tail segment fetch (segIndex=${foundCues2.size}) failed; continuing without it", ex);
}
}
sourceLength = written;
sourceLengthAudio = written2;
@@ -126,6 +126,7 @@ import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SendToDevicePackage
@@ -364,7 +365,7 @@ class VideoDetailView : ConstraintLayout {
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
Pair(0, 10) //around live, try every 10 seconds
);
private var _subtitleLanguage: String? = null
private var _subtitleLanguage: String? = _subtitleLanguageStore.value;
@androidx.annotation.OptIn(UnstableApi::class)
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
@@ -1289,6 +1290,7 @@ class VideoDetailView : ConstraintLayout {
_taskLoadVideo.cancel();
_commentsList.cancel();
_player.clear();
_player.changePlayer(null);
_cast.cleanup();
_container_content_replies.cleanup();
_container_content_queue.cleanup();
@@ -2807,6 +2809,7 @@ class VideoDetailView : ConstraintLayout {
}
_lastSubtitleSource = toSet;
_subtitleLanguage = toSet?.language
_subtitleLanguageStore.setAndSave(_subtitleLanguage ?: "")
}
private fun handleUnavailableVideo(msg: String? = null) {
@@ -3641,6 +3644,7 @@ class VideoDetailView : ConstraintLayout {
const val TAG_MORE = "MORE";
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
private val _subtitleLanguageStore = FragmentedStorage.get<StringStorage>("subtitleLanguage");
private var _lastOfflinePlaybackToastTime: Long = 0
}
}
@@ -14,6 +14,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
private var _shouldSubmitLogs = false;
private val _linesToWrite = ConcurrentLinkedQueue<String>();
private var _writer: BufferedWriter? = null;
private val _writerLock = Any();
private var _running: Boolean = false;
private var _file: File;
private val _level: LogLevel;
@@ -42,12 +43,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
submitLogs();
}
while (_linesToWrite.isNotEmpty()) {
val todo = _linesToWrite.remove()
_writer?.appendLine(todo);
}
_writer?.flush();
drainAndFlush();
} catch (e: Throwable) {
Log.e(TAG, "Failed to process logs.", e);
}
@@ -68,6 +64,25 @@ class FileLogConsumer : ILogConsumer, Closeable {
override fun consume(level: LogLevel, tag: String, text: String?, e: Throwable?) {
_linesToWrite.add(Logging.buildLogString(level, tag, text, e));
if (level == LogLevel.ERROR) {
try { drainAndFlush() } catch (_: Throwable) { /* best-effort */ }
}
}
fun flushBlocking() {
try { drainAndFlush() } catch (e: Throwable) {
Log.e(TAG, "flushBlocking failed", e);
}
}
private fun drainAndFlush() {
synchronized(_writerLock) {
val w = _writer ?: return
while (_linesToWrite.isNotEmpty()) {
w.appendLine(_linesToWrite.remove());
}
w.flush();
}
}
fun submitLogs() {
@@ -84,8 +99,10 @@ class FileLogConsumer : ILogConsumer, Closeable {
Log.i(TAG, "Requesting log writer exit.");
_running = false;
_writer?.close();
_writer = null;
synchronized(_writerLock) {
_writer?.close();
_writer = null;
}
//_logThread?.join();
_logThread = null;
}
@@ -74,6 +74,14 @@ class Logger {
return loggingEnabled;
}
fun flushBlocking() {
for (logConsumer in _logConsumers) {
if (logConsumer is FileLogConsumer) {
logConsumer.flushBlocking();
}
}
}
private fun log(level: LogLevel, tag: String, e: Throwable? = null, textBuilder: () -> String?) {
if (!_logConsumers.any { c -> c.willConsume(level, tag) }) {
return;
@@ -31,47 +31,37 @@ class StateUpdate {
private set
@Volatile var uiError: String? = null
private set
@Volatile var uiDismissed: Boolean = false
private set
val onUiChanged = Event0()
fun setUiAvailable(version: Int) {
val transitioned = uiState != UpdateUiState.AVAILABLE
uiState = UpdateUiState.AVAILABLE
uiVersion = version
uiError = null
if (transitioned) uiDismissed = false
onUiChanged.emit()
}
fun setUiDownloading(version: Int, progress: Int, indeterminate: Boolean) {
val transitioned = uiState != UpdateUiState.DOWNLOADING
uiState = UpdateUiState.DOWNLOADING
uiVersion = version
uiProgress = progress
uiIndeterminate = indeterminate
uiError = null
if (transitioned) uiDismissed = false
onUiChanged.emit()
}
fun setUiReady(version: Int, apkFile: File) {
val transitioned = uiState != UpdateUiState.READY
uiState = UpdateUiState.READY
uiVersion = version
uiApkFile = apkFile
uiError = null
if (transitioned) uiDismissed = false
onUiChanged.emit()
}
fun setUiFailed(version: Int, error: String?) {
val transitioned = uiState != UpdateUiState.FAILED
uiState = UpdateUiState.FAILED
uiVersion = version
uiError = error
if (transitioned) uiDismissed = false
onUiChanged.emit()
}
@@ -82,12 +72,6 @@ class StateUpdate {
uiIndeterminate = true
uiApkFile = null
uiError = null
uiDismissed = false
onUiChanged.emit()
}
fun dismissUi() {
uiDismissed = true
onUiChanged.emit()
}
@@ -243,4 +227,4 @@ class StateUpdate {
}
}
}
}
}
@@ -55,9 +55,18 @@ class PlayerManager {
}
fun modifyState(name: String, cb: (PlayerState) -> Unit) {
val state = getState(name);
val previousListener = state.listener;
cb(state);
if(_currentState == state)
if(_currentState == state) {
applyState(state);
val newListener = state.listener;
if(previousListener !== newListener) {
if(previousListener != null)
player.removeListener(previousListener);
if(newListener != null)
player.addListener(newListener);
}
}
}
fun switchState(name: String) {
val newState = getState(name);
@@ -29,11 +29,9 @@ class UpdateBannerView : LinearLayout {
private val _root: FrameLayout
private val _iconUpdate: ImageView
private val _textTitle: TextView
private val _textBody: TextView
private val _progressBar: ProgressBar
private val _buttonAction: FrameLayout
private val _textAction: TextView
private val _buttonClose: ImageView
private val _scope: CoroutineScope?
@@ -45,15 +43,9 @@ class UpdateBannerView : LinearLayout {
_root = findViewById(R.id.root)
_iconUpdate = findViewById(R.id.icon_update)
_textTitle = findViewById(R.id.text_title)
_textBody = findViewById(R.id.text_body)
_progressBar = findViewById(R.id.update_banner_progress)
_buttonAction = findViewById(R.id.button_action)
_textAction = findViewById(R.id.text_action)
_buttonClose = findViewById(R.id.button_close)
_buttonClose.setOnClickListener {
StateUpdate.instance.dismissUi()
}
_buttonAction.setOnClickListener {
onActionClicked()
@@ -96,17 +88,6 @@ class UpdateBannerView : LinearLayout {
Logger.w(TAG, "Retry start service failed", t)
}
}
UpdateUiState.DOWNLOADING -> {
val intent = Intent(context, UpdateDownloadService::class.java).apply {
putExtra(UpdateDownloadService.EXTRA_VERSION, st.uiVersion)
putExtra(UpdateDownloadService.EXTRA_CANCEL, true)
}
try {
ContextCompat.startForegroundService(context, intent)
} catch (t: Throwable) {
Logger.w(TAG, "Cancel start service failed", t)
}
}
UpdateUiState.AVAILABLE -> {
if (st.uiVersion == 0) return
val intent = Intent(context, UpdateDownloadService::class.java).apply {
@@ -118,6 +99,7 @@ class UpdateBannerView : LinearLayout {
Logger.w(TAG, "Download start service failed", t)
}
}
UpdateUiState.DOWNLOADING -> {}
UpdateUiState.NONE -> {}
}
}
@@ -125,7 +107,7 @@ class UpdateBannerView : LinearLayout {
private fun refresh() {
val st = StateUpdate.instance
val gateOpen = Settings.instance.autoUpdate.shouldBackgroundDownload
val visible = gateOpen && !st.uiDismissed && st.uiState != UpdateUiState.NONE
val visible = gateOpen && st.uiState != UpdateUiState.NONE
if (!visible) {
_root.visibility = View.GONE
@@ -135,41 +117,31 @@ class UpdateBannerView : LinearLayout {
when (st.uiState) {
UpdateUiState.AVAILABLE -> {
_textTitle.text = "Update available (v${st.uiVersion})"
_textBody.text = "A new Grayjay version is available."
_textBody.visibility = View.VISIBLE
_textTitle.text = "Update v${st.uiVersion}"
_progressBar.visibility = View.GONE
_textAction.text = "Download"
_buttonAction.visibility = View.VISIBLE
}
UpdateUiState.DOWNLOADING -> {
_textTitle.text = "Downloading update (v${st.uiVersion})"
if (st.uiIndeterminate) {
_textBody.text = "Starting download…"
_textTitle.text = "Downloading v${st.uiVersion}"
_progressBar.isIndeterminate = true
} else {
_textBody.text = "${st.uiProgress}% downloaded"
_textTitle.text = "Downloading v${st.uiVersion} - ${st.uiProgress}%"
_progressBar.isIndeterminate = false
_progressBar.progress = st.uiProgress
}
_textBody.visibility = View.VISIBLE
_progressBar.visibility = View.VISIBLE
_textAction.text = "Cancel"
_buttonAction.visibility = View.VISIBLE
_buttonAction.visibility = View.GONE
}
UpdateUiState.READY -> {
_textTitle.text = "Update v${st.uiVersion} ready"
_textBody.text = "Tap install to apply the update."
_textBody.visibility = View.VISIBLE
_textTitle.text = "Ready v${st.uiVersion}"
_progressBar.visibility = View.GONE
_textAction.text = "Install"
_buttonAction.visibility = View.VISIBLE
}
UpdateUiState.FAILED -> {
_textTitle.text = "Update failed"
val err = st.uiError
_textBody.text = if (err.isNullOrBlank()) "Could not download v${st.uiVersion}." else err
_textBody.visibility = View.VISIBLE
_progressBar.visibility = View.GONE
_textAction.text = "Retry"
_buttonAction.visibility = View.VISIBLE
+2 -2
View File
@@ -7,7 +7,7 @@
android:paddingEnd="12dp"
android:background="@drawable/background_pill"
android:layout_marginEnd="6dp"
android:layout_marginTop="17dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:id="@+id/root">
<LinearLayout
@@ -36,4 +36,4 @@
tools:text="Tag text" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
+36 -59
View File
@@ -1,94 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/root">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_16_round_4dp"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:layout_margin="10dp">
android:layout_marginLeft="10dp"
android:layout_marginTop="6dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="0dp"
android:minHeight="40dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingLeft="12dp"
android:paddingTop="6dp"
android:paddingRight="8dp"
android:paddingBottom="6dp">
<ImageView android:id="@+id/icon_update"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@drawable/ic_update"
android:layout_marginEnd="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="@id/text_title"
app:layout_constraintBottom_toBottomOf="@id/text_title" />
android:layout_marginRight="10dp"
android:alpha="0.9"
android:importantForAccessibility="no" />
<TextView android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Downloading update v123"
android:layout_weight="1"
tools:text="Downloading v123 - 42%"
android:fontFamily="@font/inter_semibold"
android:textSize="15sp"
android:textColor="@color/white"
android:layout_marginStart="8dp"
app:layout_constraintLeft_toRightOf="@id/icon_update"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/button_close" />
<ImageView android:id="@+id/button_close"
android:layout_width="32dp"
android:layout_height="32dp"
android:padding="6dp"
android:src="@drawable/ic_close"
android:contentDescription="@string/dismiss"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView android:id="@+id/text_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="42% downloaded"
android:fontFamily="@font/inter_light"
android:textSize="14sp"
android:textColor="#9D9D9D"
android:layout_marginTop="2dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/button_close"
app:layout_constraintTop_toBottomOf="@id/text_title" />
android:textColor="@color/white"
android:ellipsize="end"
android:maxLines="1" />
<ProgressBar android:id="@+id/update_banner_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_width="78dp"
android:layout_height="4dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="4dp"
android:max="100"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_body" />
tools:visibility="visible" />
<FrameLayout android:id="@+id/button_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/background_button_primary_round_4dp"
app:layout_constraintTop_toBottomOf="@id/update_banner_progress"
app:layout_constraintRight_toRightOf="parent">
android:layout_height="28dp"
android:layout_marginLeft="10dp"
android:background="@drawable/background_button_primary_round_4dp">
<TextView android:id="@+id/text_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
tools:text="Install"
android:fontFamily="@font/inter_regular"
android:textSize="14sp"
android:textSize="12sp"
android:textColor="@color/white"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp" />
android:paddingLeft="13dp"
android:paddingRight="13dp" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</FrameLayout>
+2
View File
@@ -372,6 +372,8 @@
<string name="deletes_license_keys_from_app">Deletes license keys from app</string>
<string name="download_when">Download when</string>
<string name="enable_video_cache">Enable Video Cache</string>
<string name="use_downloaded_ca_bundle">Use downloaded CA bundle</string>
<string name="use_downloaded_ca_bundle_description">May help on devices with stale root certificates.</string>
<string name="enable_casting">Enable casting</string>
<string name="experimental_background_update_for_subscriptions_cache">Experimental background update for subscriptions cache</string>
<string name="export_data">Export Data</string>
+2 -1
View File
@@ -19,7 +19,8 @@
"84331338-b045-419c-88e4-c86036f4cbf5": "sources/mixcloud/MixcloudConfig.json",
"009775f8-9173-48a2-8df3-d730d08d198d": "sources/radiobrowser/RadioBrowserConfig.json",
"5f6658bb-96cc-4965-ba04-c81f8686ab67": "sources/redbull-tv/RedBullTvConfig.json",
"d890ff43-7d9f-4f0e-a52d-239014fd512d": "sources/fosdem/FOSDEMConfig.json"
"d890ff43-7d9f-4f0e-a52d-239014fd512d": "sources/fosdem/FOSDEMConfig.json",
"a1b2c3d4-5e6f-7890-abcd-ef1234567890": "sources/nasa-plus/NASA-PlusConfig.json"
},
"SOURCES_EMBEDDED_DEFAULT": [
"35ae969a-a7db-11ed-afa1-0242ac120002"
+2 -1
View File
@@ -19,7 +19,8 @@
"84331338-b045-419c-88e4-c86036f4cbf5": "sources/mixcloud/MixcloudConfig.json",
"009775f8-9173-48a2-8df3-d730d08d198d": "sources/radiobrowser/RadioBrowserConfig.json",
"5f6658bb-96cc-4965-ba04-c81f8686ab67": "sources/redbull-tv/RedBullTvConfig.json",
"d890ff43-7d9f-4f0e-a52d-239014fd512d": "sources/fosdem/FOSDEMConfig.json"
"d890ff43-7d9f-4f0e-a52d-239014fd512d": "sources/fosdem/FOSDEMConfig.json",
"a1b2c3d4-5e6f-7890-abcd-ef1234567890": "sources/nasa-plus/NASA-PlusConfig.json"
},
"SOURCES_EMBEDDED_DEFAULT": [
"35ae969a-a7db-11ed-afa1-0242ac120002"