diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index 02badc0d..0a17d961 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -58,7 +58,7 @@ fun findNonRuntimeException(ex: Throwable?): Throwable? { fun warnIfMainThread(context: String) { if(BuildConfig.DEBUG && Looper.myLooper() == Looper.getMainLooper()) - Logger.w(V8Plugin.TAG, "JAVASCRIPT ON MAIN THREAD\nAt: ${context}\n" + Thread.currentThread().stackTrace); + Logger.w(V8Plugin.TAG, "JAVASCRIPT ON MAIN THREAD\nAt: ${context}\n" + Thread.currentThread().stackTrace.joinToString { it.toString() }); } fun ensureNotMainThread() { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/PlatformContentPlaceholder.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/PlatformContentPlaceholder.kt index e8633aa8..d47c57a0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/PlatformContentPlaceholder.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/PlatformContentPlaceholder.kt @@ -4,7 +4,7 @@ import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.models.PlatformAuthorLink import java.time.OffsetDateTime -class PlatformContentPlaceholder(pluginId: String): IPlatformContent { +class PlatformContentPlaceholder(pluginId: String, exception: Throwable? = null): IPlatformContent { override val contentType: ContentType = ContentType.PLACEHOLDER; override val id: PlatformID = PlatformID("", null, pluginId); override val name: String = ""; @@ -12,4 +12,5 @@ class PlatformContentPlaceholder(pluginId: String): IPlatformContent { override val shareUrl: String = ""; override val datetime: OffsetDateTime? = null; override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("", pluginId), "", "", null, null); + val error: Throwable? = exception } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 926ff063..17399513 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -26,6 +26,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.* import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.engine.V8Plugin +import com.futo.platformplayer.engine.exceptions.PluginEngineException +import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptValidationException import com.futo.platformplayer.logging.Logger @@ -289,7 +291,8 @@ open class JSClient : IPlatformClient { .value; } catch(ex: Throwable) { - announcePluginUnhandledException("isChannelUrl", ex); + if(ex !is PluginEngineException) + announcePluginUnhandledException("isChannelUrl", ex); return false; } } @@ -565,7 +568,7 @@ open class JSClient : IPlatformClient { StateAnnouncement.instance.registerAnnouncement("PluginUnhandled_${config.id}_${method}", "Plugin ${config.name} encountered an error in [${method}]", "${ex.message}\nPlease contact the plugin developer", - AnnouncementType.RECURRING, + AnnouncementType.SESSION_RECURRING, OffsetDateTime.now()); } catch(_: Throwable) {} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiRefreshPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiRefreshPager.kt index af18e817..833b26f2 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiRefreshPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiRefreshPager.kt @@ -1,5 +1,6 @@ package com.futo.platformplayer.api.media.structures +import com.futo.platformplayer.api.media.models.contents.PlatformContentPlaceholder import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.logging.Logger @@ -37,8 +38,12 @@ abstract class MultiRefreshPager: IRefreshPager, IPager { synchronized(_pending) { _pending.remove(pendingPager); } - if(error != null) + if(error != null) { onPagerError.emit(error); + val replacing = _placeHolderPagersPaired[pendingPager]; + if(replacing != null) + updatePager(null, replacing, error); + } else updatePager(pendingPager.getCompleted()); } @@ -60,9 +65,17 @@ abstract class MultiRefreshPager: IRefreshPager, IPager { override fun nextPage() = synchronized(_pagersReusable){ _currentPager.nextPage() }; override fun getResults(): List = synchronized(_pagersReusable){ _currentPager.getResults() }; - private fun updatePager(pagerToAdd: IPager?) { - if(pagerToAdd == null) + private fun updatePager(pagerToAdd: IPager?, toReplacePager: IPager? = null, error: Throwable? = null) { + if(pagerToAdd == null) { + if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) { + val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: ""; + _currentPager = PlaceholderPager(5) { + return@PlaceholderPager PlatformContentPlaceholder(pluginId, error) + } as IPager; + onPagerChanged.emit(_currentPager); + } return; + } synchronized(_pagersReusable) { Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager") _pagersReusable.add(pagerToAdd.asReusable()); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/structures/PlaceholderPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/structures/PlaceholderPager.kt index c0bd8d6e..9a5dc2f0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/structures/PlaceholderPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/structures/PlaceholderPager.kt @@ -6,11 +6,11 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent * A placeholder pager simply generates PlatformContent by some creator function. */ class PlaceholderPager : IPager { - private val _creator: ()->IPlatformContent; + val placeholderFactory: ()->IPlatformContent; private val _pageSize: Int; constructor(pageSize: Int, placeholderCreator: ()->IPlatformContent) { - _creator = placeholderCreator; + placeholderFactory = placeholderCreator; _pageSize = pageSize; } @@ -18,7 +18,7 @@ class PlaceholderPager : IPager { override fun getResults(): List { val pages = ArrayList(); for(item in 1.._pageSize) - pages.add(_creator()); + pages.add(placeholderFactory()); return pages; } override fun hasMorePages(): Boolean = true; diff --git a/app/src/main/java/com/futo/platformplayer/constructs/Event.kt b/app/src/main/java/com/futo/platformplayer/constructs/Event.kt index 84802a6d..d3c1d001 100644 --- a/app/src/main/java/com/futo/platformplayer/constructs/Event.kt +++ b/app/src/main/java/com/futo/platformplayer/constructs/Event.kt @@ -8,6 +8,10 @@ abstract class EventBase: IEvent { protected val _conditionalListeners = mutableListOf>(); protected val _listeners = mutableListOf>(); + fun hasListeners(): Boolean = + synchronized(_listeners){_listeners.isNotEmpty()} || + synchronized(_conditionalListeners){_conditionalListeners.isNotEmpty()}; + fun subscribeConditional(listener: ConditionalHandler) { synchronized(_conditionalListeners) { _conditionalListeners.add(TaggedHandler(listener)); @@ -65,10 +69,7 @@ abstract class EventBase: IEvent { class Event0() : EventBase<(()->Unit), (()->Boolean)>() { fun emit() : Boolean { - var handled: Boolean; - synchronized(_listeners) { - handled = _listeners.isNotEmpty(); - } + var handled = false; synchronized(_conditionalListeners) { for (conditional in _conditionalListeners) @@ -76,6 +77,7 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() { } synchronized(_listeners) { + handled = handled || _listeners.isNotEmpty(); for (handler in _listeners) handler.handler.invoke(); } @@ -85,17 +87,14 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() { } class Event1() : EventBase<((T1)->Unit), ((T1)->Boolean)>() { fun emit(value : T1): Boolean { - var handled: Boolean; - synchronized(_listeners) { - handled = _listeners.isNotEmpty(); - } - + var handled = false; synchronized(_conditionalListeners) { for (conditional in _conditionalListeners) handled = handled || conditional.handler.invoke(value); } synchronized(_listeners) { + handled = handled || _listeners.isNotEmpty(); for (handler in _listeners) handler.handler.invoke(value); } @@ -105,10 +104,7 @@ class Event1() : EventBase<((T1)->Unit), ((T1)->Boolean)>() { } class Event2() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() { fun emit(value1 : T1, value2 : T2): Boolean { - var handled: Boolean; - synchronized(_listeners) { - handled = _listeners.isNotEmpty(); - } + var handled = false; synchronized(_conditionalListeners) { for (conditional in _conditionalListeners) @@ -116,6 +112,7 @@ class Event2() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() { } synchronized(_listeners) { + handled = handled || _listeners.isNotEmpty(); for (handler in _listeners) handler.handler.invoke(value1, value2); } @@ -126,10 +123,7 @@ class Event2() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() { class Event3() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Boolean)>() { fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean { - var handled: Boolean; - synchronized(_listeners) { - handled = _listeners.isNotEmpty(); - } + var handled = false; synchronized(_conditionalListeners) { for (conditional in _conditionalListeners) @@ -137,6 +131,7 @@ class Event3() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Bool } synchronized(_listeners) { + handled = handled || _listeners.isNotEmpty(); for (handler in _listeners) handler.handler.invoke(value1, value2, value3); } diff --git a/app/src/main/java/com/futo/platformplayer/constructs/TaskHandler.kt b/app/src/main/java/com/futo/platformplayer/constructs/TaskHandler.kt index 52798c4f..3036f667 100644 --- a/app/src/main/java/com/futo/platformplayer/constructs/TaskHandler.kt +++ b/app/src/main/java/com/futo/platformplayer/constructs/TaskHandler.kt @@ -57,6 +57,7 @@ class TaskHandler { fun run(parameter: TParameter) { val id = ++_idGenerator; + var handled = false; _scope().launch(_dispatcher) { if (id != _idGenerator) return@launch; @@ -67,24 +68,31 @@ class TaskHandler { return@launch; withContext(Dispatchers.Main) { - if (id != _idGenerator) + if (id != _idGenerator) { + handled = true; return@withContext; + } try { onSuccess.emit(result); + handled = true; } catch (e: Throwable) { Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e); onError.emit(e, parameter); + handled = true; } } } catch (e: Throwable) { Log.i("TaskHandler", "TaskHandler.run in exception: " + e.message); - if (id != _idGenerator) + if (id != _idGenerator) { + handled = true; return@launch; + } withContext(Dispatchers.Main) { + handled = true; if (id != _idGenerator) return@withContext; @@ -95,7 +103,18 @@ class TaskHandler { } } } - } + }/*.invokeOnCompletion { //Commented for now, because it doesn't fix the bug it was intended to fix, but might want it later anyway + if(!handled) { + if(it is CancellationException) { + Logger.w(TAG, "Detected unhandled TaskHandler due to cancellation, forwarding cancellation"); + onError.emit(it, parameter); + } + else { + //TODO: Forward exception? + Logger.w(TAG, "Detected unhandled TaskHandler due to [${it}]", it); + } + } + }*/ } @Synchronized diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index 694f69ee..a3508dfd 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -1,7 +1,6 @@ package com.futo.platformplayer.engine import android.content.Context -import android.os.Looper import com.caoccao.javet.exceptions.JavetCompilationException import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.interop.V8Host @@ -18,9 +17,7 @@ import com.futo.platformplayer.engine.exceptions.* import com.futo.platformplayer.engine.internal.V8Converter import com.futo.platformplayer.engine.packages.* import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateAssets -import kotlinx.coroutines.* class V8Plugin { val config: IV8PluginConfig; @@ -31,14 +28,29 @@ class V8Plugin { val httpClient: ManagedHttpClient get() = _client; val httpClientAuth: ManagedHttpClient get() = _clientAuth; + private val _runtimeLock = Object(); var _runtime : V8Runtime? = null; private val _deps : LinkedHashMap = LinkedHashMap(); private val _depsPackages : MutableList = mutableListOf(); private var _script : String? = null; + var isStopped = true; val onStopped = Event1(); + //TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial + private val _busyCounterLock = Object(); + private var _busyCounter = 0; + val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 }; + + /** + * Called before a busy counter is about to be removed. + * Is primarily used to prevent additional calls to dead runtimes. + * + * Parameter is the busy count after this execution + */ + val afterBusy = Event1(); + constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) { this._client = client; this._clientAuth = clientAuth; @@ -81,7 +93,7 @@ class V8Plugin { fun start() { val script = _script ?: throw IllegalStateException("Attempted to start V8 without script"); - synchronized(this) { + synchronized(_runtimeLock) { if (_runtime != null) return; @@ -121,19 +133,25 @@ class V8Plugin { catchScriptErrors("Plugin[${config.name}]") { it.getExecutor(script).executeVoid() }; + isStopped = false; } } } fun stop(){ Logger.i(TAG, "Stopping plugin [${config.name}]"); - synchronized(this) { - _runtime?.let { - _runtime = null; - if(!it.isClosed && !it.isDead) - it.close(); - }; + isStopped = true; + whenNotBusy { + synchronized(_runtimeLock) { + isStopped = true; + _runtime?.let { + _runtime = null; + if(!it.isClosed && !it.isDead) + it.close(); + Logger.i(TAG, "Stopped plugin [${config.name}]"); + }; + } + onStopped.emit(this); } - onStopped.emit(this); } fun execute(js: String) : V8Value { @@ -141,14 +159,53 @@ class V8Plugin { } fun executeTyped(js: String) : T { warnIfMainThread("V8Plugin.executeTyped"); + if(isStopped) + throw PluginEngineStoppedException(config, "Instance is stopped", js); + + synchronized(_busyCounterLock) { + _busyCounter++; + } val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); - return catchScriptErrors("Plugin[${config.name}]", js) { runtime.getExecutor(js).execute() }; + try { + return catchScriptErrors("Plugin[${config.name}]", js) { + runtime.getExecutor(js).execute() + }; + } + finally { + synchronized(_busyCounterLock) { + //Free busy *after* afterBusy calls are done to prevent calls on dead runtimes + try { + afterBusy.emit(_busyCounter - 1); + } + catch(ex: Throwable) { + Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex); + } + _busyCounter--; + } + } } fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value }; fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value }; fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value }; + fun whenNotBusy(handler: (V8Plugin)->Unit) { + synchronized(_busyCounterLock) { + if(_busyCounter == 0) + handler(this); + else { + val tag = Object(); + afterBusy.subscribe(tag) { + if(it == 0) { + Logger.w(TAG, "V8Plugin afterBusy handled"); + afterBusy.remove(tag); + handler(this); + } + } + } + } + } + private fun getPackage(context: Context, packageName: String): V8Package { //TODO: Auto get all package types? return when(packageName) { diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/PluginEngineException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/PluginEngineException.kt new file mode 100644 index 00000000..3ca2ec13 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/PluginEngineException.kt @@ -0,0 +1,11 @@ +package com.futo.platformplayer.engine.exceptions + +import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.getOrThrow +import java.lang.Exception + + +open class PluginEngineException(config: IV8PluginConfig, error: String, code: String? = null) : PluginException(config, error, null, code) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/PluginEngineStoppedException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/PluginEngineStoppedException.kt new file mode 100644 index 00000000..b3f6044e --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/PluginEngineStoppedException.kt @@ -0,0 +1,11 @@ +package com.futo.platformplayer.engine.exceptions + +import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.getOrThrow +import java.lang.Exception + + +class PluginEngineStoppedException(config: IV8PluginConfig, error: String, code: String? = null) : PluginEngineException(config, error, code) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index bf2e238b..1cac2735 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -110,7 +110,10 @@ class HomeFragment : MainFragment() { Logger.w(ChannelFragment.TAG, "Failed to load channel.", it); UIDialogs.showGeneralRetryErrorDialog(context, "Failed to get Home", it, { loadResults() - }); + }) { + finishRefreshLayoutLoader(); + setLoading(false); + }; }; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index 7f663f6b..8709cc2c 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -28,6 +28,7 @@ import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewHolder import com.futo.platformplayer.views.subscriptions.SubscriptionBar +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -168,7 +169,12 @@ class SubscriptionsFeedFragment : MainFragment() { .success { loadedResult(it); } .exception { Logger.w(ChannelFragment.TAG, "Failed to load channel.", it); - UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }); + if(it !is CancellationException) + UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }); + else { + finishRefreshLayoutLoader(); + setLoading(false); + } }; private fun initializeToolbarContent() { @@ -251,7 +257,11 @@ class SubscriptionsFeedFragment : MainFragment() { } catch (e: Throwable) { Logger.e(TAG, "Failed to finish loading", e) } - } + }/*.invokeOnCompletion { //Commented for now, because it doesn't fix the bug it was intended to fix, but might want it later anyway + if(it is CancellationException) { + setLoading(false); + } + }*/ } private fun handleExceptions(exs: List) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index 928b82b4..f416fc25 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -399,13 +399,15 @@ class StatePlatform { return@async searchResult; } catch(ex: Throwable) { Logger.e(TAG, "getHomeRefresh", ex); - return@async null; + throw ex; + //return@async null; } }); }.toList(); val finishedPager = deferred.map { it.second }.awaitFirstNotNullDeferred() ?: return EmptyPager(); val toAwait = deferred.filter { it.second != finishedPager.first }; + return RefreshDistributionContentPager( listOf(finishedPager.second), toAwait.map { it.second }, diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/PreviewPlaceholderViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/PreviewPlaceholderViewHolder.kt index 692aa8f4..8ca83a5b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/PreviewPlaceholderViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/PreviewPlaceholderViewHolder.kt @@ -3,8 +3,10 @@ package com.futo.platformplayer.views.adapters import android.content.Context import android.graphics.drawable.Animatable import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.TextView import com.futo.platformplayer.* import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails @@ -18,6 +20,7 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder { private val _loader: ImageView; private val _platformIndicator: PlatformIndicator; + private val _error: TextView; val context: Context; @@ -30,15 +33,28 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder { context = itemView.context; _loader = itemView.findViewById(R.id.loader); _platformIndicator = itemView.findViewById(R.id.thumbnail_platform); + _error = itemView.findViewById(R.id.text_error); - (_loader.drawable as Animatable?)?.start(); //TODO: stop? + (_loader.drawable as Animatable?)?.start(); } override fun bind(content: IPlatformContent) { - if(content is PlatformContentPlaceholder) + if(content is PlatformContentPlaceholder) { _platformIndicator.setPlatformFromClientID(content.id.pluginId); - else + _error.text = content.error?.message ?: ""; + if(content.error != null) { + _loader.visibility = View.GONE; + (_loader.drawable as Animatable?)?.stop(); + } + else { + _loader.visibility = View.VISIBLE; + (_loader.drawable as Animatable?)?.start(); + } + } + else { _platformIndicator.clearPlatform(); + (_loader.drawable as Animatable?)?.stop(); + } } override fun preview(video: IPlatformContentDetails?, paused: Boolean) { } diff --git a/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt b/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt index e6dda325..93c27e9a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt @@ -100,7 +100,8 @@ class AnnouncementView : LinearLayout { } private fun setAnnouncement(announcement: Announcement?, count: Int) { - Logger.i(TAG, "setAnnouncement announcement=$announcement count=$count"); + if(count == 0 && announcement == null) + Logger.i(TAG, "setAnnouncement announcement=$announcement count=$count"); _currentAnnouncement = announcement; diff --git a/app/src/main/res/layout/list_placeholder_preview.xml b/app/src/main/res/layout/list_placeholder_preview.xml index 95a3944c..005c9b8f 100644 --- a/app/src/main/res/layout/list_placeholder_preview.xml +++ b/app/src/main/res/layout/list_placeholder_preview.xml @@ -33,6 +33,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_placeholder_thumbnail.xml b/app/src/main/res/layout/list_placeholder_thumbnail.xml index 0f482b04..090aa2cf 100644 --- a/app/src/main/res/layout/list_placeholder_thumbnail.xml +++ b/app/src/main/res/layout/list_placeholder_thumbnail.xml @@ -16,6 +16,7 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" android:gravity="center"> + + + \ No newline at end of file