diff --git a/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt b/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt index 3147ce62..b0609604 100644 --- a/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt +++ b/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt @@ -7,6 +7,10 @@ import android.os.IBinder import android.os.SystemClock import com.futo.platformplayer.UIDialogs.ActionStyle import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.ImageVariable +import com.futo.platformplayer.states.AnnouncementType +import com.futo.platformplayer.states.SessionAnnouncement +import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateUpdate import kotlinx.coroutines.* @@ -14,6 +18,7 @@ import java.io.File import java.io.FileOutputStream import java.net.HttpURLConnection import java.net.URL +import java.time.OffsetDateTime class UpdateDownloadService : Service() { @@ -85,13 +90,16 @@ class UpdateDownloadService : Service() { job.cancel() } - private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean) { + private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean, onProgress: ((Int) -> Unit)? = null) { val now = SystemClock.elapsedRealtime() val force = progress == 100 && !indeterminate if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) { lastProgressUpdateElapsedMs = now - UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate) + UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate); + + if(onProgress != null) + onProgress.invoke(progress); } } @@ -99,6 +107,7 @@ class UpdateDownloadService : Service() { val apkFile = StateUpdate.getApkFile(this, version) val partialFile = StateUpdate.getPartialApkFile(this, version) + var announcement: SessionAnnouncement? = null; try { if (apkFile.exists() && apkFile.length() > 0L) { Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}") @@ -106,6 +115,14 @@ class UpdateDownloadService : Service() { return } + try { + announcement = StateAnnouncement.instance.registerLoading("Downloading new version [${version}]", "New version is being downloaded..", + ImageVariable.fromResource(R.drawable.foreground)); + } + catch(ex: Exception){ + Logger.e(TAG, "Failed to set progress announcement", ex); + } + var backoffMs = INITIAL_BACKOFF_MS for (attempt in 0 until MAX_RETRIES) { @@ -115,7 +132,13 @@ class UpdateDownloadService : Service() { } try { - performDownload(StateUpdate.APK_URL, partialFile, version) + performDownload(StateUpdate.APK_URL, partialFile, version, { + try { + if (announcement != null) + announcement?.setProgress(it); + } + catch(ex: Throwable) {} + }) if (!cancelRequested) { if (apkFile.exists()) { @@ -145,6 +168,12 @@ class UpdateDownloadService : Service() { } } } finally { + try { + if (announcement != null) { + StateAnnouncement.instance.closeAnnouncement(announcement.id); + } + } + catch(ex: Throwable){} isDownloading = false cancelRequested = false stopForeground(Service.STOP_FOREGROUND_REMOVE) @@ -152,7 +181,7 @@ class UpdateDownloadService : Service() { } } - private fun performDownload(url: String, partialFile: File, version: Int) { + private fun performDownload(url: String, partialFile: File, version: Int, onProgress: ((Int)->Unit)? = null) { var startOffset = if (partialFile.exists()) partialFile.length() else 0L Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset") @@ -204,7 +233,7 @@ class UpdateDownloadService : Service() { progress > 100 -> 100 else -> progress } - throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false) + throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false, onProgress) } } else { throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true) @@ -250,6 +279,18 @@ class UpdateDownloadService : Service() { UpdateNotificationManager.cancelAll(ctx) UpdateInstaller.startInstall(ctx, version, apkFile) }, ActionStyle.PRIMARY, true)); + + try { + StateAnnouncement.instance.registerAnnouncement("install-update-apk", "Grayjay v${version} is ready!", "You can now install the new Grayjay version.", + AnnouncementType.SESSION, + OffsetDateTime.now(), "update", "Install", { + UpdateNotificationManager.cancelAll(ctx) + UpdateInstaller.startInstall(ctx, version, apkFile) + }); + } + catch(ex: Throwable) { + + } } catch (t: Throwable) { Logger.w(TAG, "Failed to show in-app update downloaded dialog", t) updateDownloadedDialog = null diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 85e4c47d..891d4fd5 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -110,6 +110,7 @@ import com.futo.platformplayer.stores.StringStorage import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.views.ToastView +import com.futo.platformplayer.views.notification.NotificationOverlayView import com.futo.polycentric.core.ApiMethods import com.google.gson.JsonParser import com.google.zxing.integration.android.IntentIntegrator @@ -201,6 +202,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { lateinit var _fragLibraryVideos: LibraryVideosFragment; lateinit var _fragLibrarySearch: LibrarySearchFragment; lateinit var _fragLibraryFiles: LibraryFilesFragment; + lateinit var _fragNotifications: NotificationOverlayView.Frag; lateinit var _fragSettings: SettingsFragment; lateinit var _fragDeveloper: DeveloperFragment; lateinit var _fragLogin: LoginFragment; @@ -389,6 +391,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragLibraryVideos = LibraryVideosFragment.newInstance(); _fragLibraryFiles = LibraryFilesFragment.newInstance(); _fragLibrarySearch = LibrarySearchFragment.newInstance(); + _fragNotifications = NotificationOverlayView.Frag(); _fragSettings = SettingsFragment.newInstance(); _fragDeveloper = DeveloperFragment.newInstance(); _fragLogin = LoginFragment.newInstance(); @@ -538,6 +541,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragLibrarySearch.topBar = _fragTopBarSearch; _fragSettings.topBar = _fragTopBarNavigation; _fragDeveloper.topBar = _fragTopBarNavigation; + _fragNotifications.topBar = _fragTopBarGeneral; _fragBrowser.topBar = _fragTopBarNavigation; @@ -1368,6 +1372,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { LibraryVideosFragment::class -> _fragLibraryVideos as T; LibraryFilesFragment::class -> _fragLibraryFiles as T; LibrarySearchFragment::class -> _fragLibrarySearch as T; + NotificationOverlayView.Frag::class -> _fragNotifications as T; SettingsFragment:: class -> _fragSettings as T; DeveloperFragment::class -> _fragDeveloper as T; LoginFragment::class -> _fragLogin as T; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt index 3e92db3c..60390e05 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt @@ -61,6 +61,11 @@ class SourcePluginConfig( val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl); val absoluteScriptUrl: String get() = resolveAbsoluteUrl(scriptUrl, sourceUrl)!!; + fun isOfficialAuthor(): Boolean { + return scriptSignature != null && + scriptPublicKey == "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsoFJU4AReDyUnSQI9A99UjLCwkY8OH+1o8cdtf2EjSb+fO2qmP8MGMTAvfvgmq5d2QBJE2XHRkRO3JKcTlcc1j0WlOlU8P9W272DYCeX6oYaavpKNqGKoGEuodp9wtiyNwyH46++JfpU/uIUacZbZKkHv9gIGchmNvpKYZQjFd/8pUqXGpcXZP54tGSC9PLcY+5TozZThK7Oy1+3YEf1bZ44UinRYYATbLk/wNuAfsupvlt6nxZOcJhABhdo9V+gY0FE6Ayg5+1cd1noWhnRtLF+sPdEr3z8Nt15JEK5a/524t25FMhwz8yKxlGW5qW3QLJHSUgLQncL6a1zlZ1s8QIDAQAB" + } + private fun resolveAbsoluteUrl(url: String?, sourceUrl: String?): String? { if(url == null) return null; @@ -165,6 +170,12 @@ class SourcePluginConfig( "Unrestricted Http Header access", "Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests." )) + if(packagesOptional.contains("Browser") || packages.contains("Browser")) { + list.add(Pair( + "Browser Interop", + "This plugin requires webbrowser interop. May access urls outside of the restricted urls. This will only work for official plugins and during development builds." + )) + } return list; } 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 41834cb0..1c44aed1 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -15,7 +15,9 @@ import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.IV8ValuePromise import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValuePromise +import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.engine.exceptions.NoInternetException @@ -34,6 +36,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.internal.V8Converter import com.futo.platformplayer.engine.packages.PackageBridge +import com.futo.platformplayer.engine.packages.PackageBrowser import com.futo.platformplayer.engine.packages.PackageDOMParser import com.futo.platformplayer.engine.packages.PackageHttp import com.futo.platformplayer.engine.packages.PackageHttpImp @@ -44,6 +47,7 @@ import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateAssets +import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.toList import com.futo.platformplayer.toV8ValueBlocking import com.futo.platformplayer.toV8ValueAsync @@ -218,6 +222,9 @@ class V8Plugin { if(pack is PackageHttp) { pack.cleanup(); } + else if(pack is PackageBrowser) { + pack.deinitialize(); + } } _runtime?.let { @@ -387,6 +394,18 @@ class V8Plugin { "HttpImp" -> PackageHttpImp(this, config) "Utilities" -> PackageUtilities(this, config) "JSDOM" -> PackageJSDOM(this, config) + "Browser" -> { + val isOfficial = (config is SourcePluginConfig && config.isOfficialAuthor()); + + if(BuildConfig.DEBUG) + PackageBrowser(this) + else if(isOfficial) + PackageBrowser(this) + else if(config is SourcePluginConfig && config.id == StateDeveloper.DEV_ID) + PackageBrowser(this) + else + throw IllegalArgumentException("Browser is only allowed for debug and official plugins due to security"); + }; else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}"); }; } diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index 038c734d..6c7dab49 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt @@ -105,6 +105,11 @@ class PackageBridge : V8Package { ) } + @V8Function + fun hasPackage(str: String): Boolean { + return _plugin.getPackages().any { it.name == str }; + } + @V8Function fun dispose(value: V8Value) { Logger.e(TAG, "Manual dispose: " + value.javaClass.name); diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBrowser.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBrowser.kt new file mode 100644 index 00000000..4cf0ab09 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBrowser.kt @@ -0,0 +1,197 @@ +package com.futo.platformplayer.engine.packages + +import android.graphics.Bitmap +import android.webkit.ConsoleMessage +import android.webkit.JavascriptInterface +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.collection.emptyLongSet +import com.caoccao.javet.annotations.V8Function +import com.caoccao.javet.values.reference.V8ValueFunction +import com.futo.platformplayer.api.media.platforms.js.JSClient +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.engine.V8Plugin +import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StateDeveloper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.withTimeout + +class PackageBrowser: V8Package { + override val name: String get() = "Browser"; + override val variableName: String = "browser"; + + @Transient + private var _readySemaphore: Semaphore? = null; + @Transient + private val _callbacks = mutableMapOfUnit>(); + @Transient + private val _interop = JSInterop(this); + @Transient + private var _browser: WebView? = null; + private val browser: WebView get() { + if(_browser == null) + throw IllegalStateException("Browser not initialized"); + return _browser!!; + } + + constructor(v8Plugin: V8Plugin): super(v8Plugin) { + + } + @V8Function + fun initialize() { + if(_browser == null){ + StateApp.instance.scope.launch(Dispatchers.Main) { + _browser = WebView(StateApp.instance.contextOrNull ?: return@launch); + _browser?.settings?.javaScriptEnabled = true; + _browser?.settings?.blockNetworkImage = false; + _browser?.settings?.blockNetworkLoads = false; + _browser?.settings?.allowContentAccess = false; + _browser?.settings?.allowFileAccess = false; + //_browser?.settings?.useWideViewPort = true; + //_browser?.settings?.loadWithOverviewMode = true; + _browser?.webViewClient = object : WebViewClient() { + override fun onPageCommitVisible(view: WebView?, url: String?) { + super.onPageCommitVisible(view, url) + _readySemaphore?.release(); + _readySemaphore = null; + Logger.i("PackageBrowser", "Browser loaded"); + } + } + _browser?.webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { + if(consoleMessage?.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { + val msg = "Browser Error:${consoleMessage?.message()} [${consoleMessage?.lineNumber()}]" ?: "" + Logger.e("PackageBrowser", msg); + if(_plugin.config is SourcePluginConfig && _plugin.config.id == StateDeveloper.DEV_ID) + StateDeveloper.instance.logDevException(StateDeveloper.instance.currentDevID ?: "", msg) + } + else { + val msg = "Browser Log:" + consoleMessage?.message() ?: ""; + Logger.e("PackageBrowser", msg); + if(_plugin.config is SourcePluginConfig && _plugin.config.id == StateDeveloper.DEV_ID) + StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", msg); + } + return super.onConsoleMessage(consoleMessage) + } + } + _browser?.addJavascriptInterface(_interop, "__GJ"); + } + return; + } + } + @V8Function + fun deinitialize() { + _browser?.destroy(); + _browser = null; + } + + @V8Function + fun getCurrentUrl(): String? { + return browser.url; + } + @V8Function + fun waitTillLoaded(timeout: Int = 1000): Boolean { + val acquired = _readySemaphore?.let { + if(!it.tryAcquire()) { + Logger.i("PackageBrowser", "Waiting for browser to be ready"); + if(!runBlocking { + try { + return@runBlocking withTimeout(timeout.toLong(), { + it.acquire() + return@withTimeout true; + }); + } + catch(ex: TimeoutCancellationException) { + return@runBlocking false; + } + }) return@let false; + } + it.release(); + return@let true; + } ?: true; + if(acquired) + Logger.i("PackageBrowser", "Browser is ready"); + else + Logger.i("PackageBrowser", "Browser failed wait ready"); + return acquired; + } + + @V8Function + fun load(url: String) { + Logger.i("PackageBrowser", "Browser loading url [${url}]"); + _readySemaphore = Semaphore(1, 1); + StateApp.instance.scope.launch(Dispatchers.Main) { + browser.loadUrl(url); + } + } + + @V8Function + fun run(js: String, callbackId: String? = null, callback: V8ValueFunction? = null) { + waitTillLoaded(); + val funcClone = callback?.toClone() + if(callbackId != null && callback != null) { + synchronized(_callbacks) { + _callbacks.put(callbackId, { + funcClone?.callVoid(null, arrayOf(it)); + }); + } + } + StateApp.instance.scope.launch(Dispatchers.Main) { + try { + Logger.i("PackageBrowser", "Browser running JS with callback [${callbackId}]\n${(if(js.length > 200) (js.substring(0, 200) + "...") else js)})"); + browser.evaluateJavascript(js, object : ValueCallback { + override fun onReceiveValue(value: String?) { + Logger.i("PackageBrowser", "Browser run finished"); + } + }) + } + catch(ex: Throwable) { + Logger.e("PackageBrowser", "Failed to invoke browser", ex); + } + } + } + @V8Function + fun runWithReturn(js: String, callback: V8ValueFunction? = null) { + waitTillLoaded(); + val funcClone = callback?.toClone() + StateApp.instance.scope.launch(Dispatchers.Main) { + try { + Logger.i("PackageBrowser", "Browser running JS with callback [sync]\n${(if(js.length > 200) (js.substring(0, 200) + "...") else js)})"); + browser.evaluateJavascript(js, object : ValueCallback { + override fun onReceiveValue(value: String?) { + Logger.i("PackageBrowser", "Browser run returned: " + (value ?: "")); + funcClone?.callVoid(null, arrayOf(value)); + } + }) + } + catch(ex: Throwable) { + Logger.e("PackageBrowser", "Failed to invoke browser", ex); + } + } + } + + class JSInterop(private val pack: PackageBrowser) { + + @JavascriptInterface + fun callback(id: String, result: String) { + Logger.i("PackageBrowser", "Browser Callback [${id}]: ${result}"); + val callback = synchronized(pack._callbacks) { pack._callbacks.remove(id); }; + if(callback != null) + callback.invoke(result); + } + + @JavascriptInterface + fun log(msg: String) { + Logger.i("PackageBrowser", "Log: " + msg); + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt index d73f38c7..8f08e098 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt @@ -10,6 +10,7 @@ import android.webkit.WebView import android.webkit.WebViewClient import android.widget.AdapterView import android.widget.ArrayAdapter +import android.widget.LinearLayout import android.widget.Spinner import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -21,6 +22,7 @@ class BrowserFragment : MainFragment() { override val isTab: Boolean = false; override val hasBottomBar: Boolean get() = true; + private var _root: LinearLayout? = null; private var _webview: WebView? = null; private val _webviewWithoutHandling = object: WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { @@ -31,6 +33,7 @@ class BrowserFragment : MainFragment() { override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val view = inflater.inflate(R.layout.fragment_browser, container, false); + _root = view.findViewById(R.id.root); _webview = view.findViewById(R.id.webview).apply { this.webViewClient = _webviewWithoutHandling; this.settings.javaScriptEnabled = true; @@ -43,7 +46,12 @@ class BrowserFragment : MainFragment() { override fun onShownWithView(parameter: Any?, isBack: Boolean) { super.onShownWithView(parameter, isBack) - if(parameter is String) { + if(parameter is WebView) { + _root?.removeView(_webview); + _root?.addView(parameter); + _webview = parameter; + } + else if(parameter is String) { _webview?.webViewClient = _webviewWithoutHandling; _webview?.loadUrl(parameter); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt index d49ae1e0..d0df0d9e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt @@ -47,7 +47,7 @@ abstract class FeedView : L private val _progressBar: ProgressBar; private val _spinnerSortBy: Spinner; private val _containerSortBy: LinearLayout; - private val _announcementView: AnnouncementView; + //private val _announcementView: AnnouncementView; private val _tagsView: TagsView; private val _textCentered: TextView; private val _emptyPagerContainer: FrameLayout; @@ -87,7 +87,7 @@ abstract class FeedView : L _textCentered = findViewById(R.id.text_centered); _emptyPagerContainer = findViewById(R.id.empty_pager_container); _progressBar = findViewById(R.id.progress_bar); - _announcementView = findViewById(R.id.announcement_view) + //_announcementView = findViewById(R.id.announcement_view) _progressBar.inactiveColor = Color.TRANSPARENT; _swipeRefresh = findViewById(R.id.swipe_refresh); @@ -192,7 +192,7 @@ abstract class FeedView : L } protected fun showAnnouncementView() { - _announcementView.visibility = View.VISIBLE + //_announcementView.visibility = View.VISIBLE } private fun ensureEnoughContentVisible(filteredResults: List) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt index 16c8df37..40ae4536 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt @@ -266,7 +266,7 @@ class LibraryFragment : MainFragment() { }); if(this.allowMusic) { - val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount); + val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount) adapterArtists.setData(artists); if (artists.size == 0) sectionArtists.setEmpty( @@ -289,7 +289,7 @@ class LibraryFragment : MainFragment() { } if(this.allowMusic) { - val albums = StateLibrary.instance.getAlbums(); + val albums = StateLibrary.instance.getAlbums() adapterAlbums.setData(albums); if (albums.size == 0) sectionAlbums.setEmpty("No albums", "No albums were found on your device", -1); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt index a496c668..0938a94d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt @@ -7,6 +7,9 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment @@ -17,18 +20,54 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData import com.futo.platformplayer.models.SearchType +import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.views.casting.CastButton +import com.futo.platformplayer.views.notification.NotificationOverlayView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class GeneralTopBarFragment : TopFragment() { private var _buttonSearch: ImageButton? = null; private var _buttonCast: CastButton? = null; + private var _buttonNotifs: ConstraintLayout? = null; + private var _buttonNotifIcon: ImageView? = null; + private var _buttonNotifCount: TextView? = null; + + init { + StateAnnouncement.instance.onAnnouncementChanged.subscribe { + lifecycleScope?.launch(Dispatchers.Main) { + updateNotifCount(); + } + } + } + + fun updateNotifCount() { + val currentAnnouncements = StateAnnouncement.instance.getVisibleAnnouncements(); + if(currentAnnouncements.any()) + _buttonNotifCount?.let { + it.text = currentAnnouncements.size.toString(); + it.visibility = View.VISIBLE; + } + else + _buttonNotifCount?.let { + it.text = currentAnnouncements.size.toString(); + it.visibility = View.GONE; + } + } + override fun onShown(parameter: Any?) { if(currentMain is CreatorsFragment) { _buttonSearch?.setImageResource(R.drawable.ic_person_search_300w); } else { _buttonSearch?.setImageResource(R.drawable.ic_search_300w); } + if(currentMain is NotificationOverlayView.Frag) { + _buttonNotifIcon?.setImageResource(R.drawable.ic_notifications_filled) + } + else { + _buttonNotifIcon?.setImageResource(R.drawable.ic_notifications) + } } override fun onHide() { @@ -44,6 +83,16 @@ class GeneralTopBarFragment : TopFragment() { val buttonSearch: ImageButton = view.findViewById(R.id.button_search); _buttonCast = view.findViewById(R.id.button_cast); + _buttonNotifs = view.findViewById(R.id.button_notifs); + _buttonNotifIcon = view.findViewById(R.id.button_notifs_icon); + _buttonNotifCount = view.findViewById(R.id.button_notifs_count); + + updateNotifCount(); + + _buttonNotifs?.setOnClickListener { + navigate(); + } + buttonSearch.setOnClickListener { if(currentMain is CreatorsFragment) { navigate(SuggestionsFragmentData("", SearchType.CREATOR)); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt b/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt index 0c12d35a..3712b565 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt @@ -1,13 +1,22 @@ package com.futo.platformplayer.states +import android.view.View +import android.view.WindowManager +import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.constructs.Event2 +import com.futo.platformplayer.dialogs.PluginUpdateDialog import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringHashSetStorage import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -110,6 +119,48 @@ class StateAnnouncement { onAnnouncementChanged.emit(); } + //Special Announcements + fun registerPluginUpdate(oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): SessionAnnouncement { + val announcement = SessionAnnouncement( + "update-plugin-" + UUID.randomUUID().toString(), + "${newConfig.name} update v${newConfig.version} available!", + "An update is available to upgrade from ${oldConfig.version} to ${newConfig.version}.", + AnnouncementType.SESSION, + null, "updates", "Update", StateAnnouncement.ACTION_UPDATE_PLUGIN, + null, null,oldConfig.id, + newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) } + ).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, oldConfig.id); + registerAnnouncementSession(announcement); + return announcement; + } + fun registerPluginUpdated(newConfig: SourcePluginConfig) { + registerAnnouncementSession(SessionAnnouncement( + "updated-plugin-" + UUID.randomUUID().toString(), + "${newConfig.name} updated to v${newConfig.version}!", + "You have succesfully been updater to v${newConfig.version}.", + AnnouncementType.SESSION, + null, "updates", null, null, + null, null,null, + newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) } + ).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, newConfig.id)); + } + + fun registerLoading(title: String, description: String, icon: ImageVariable? = null, customId: String? = null): SessionAnnouncement { + val id = "loading-" + UUID.randomUUID().toString(); + val announcement = SessionAnnouncement( + customId ?: id, + title, + description, + AnnouncementType.ONGOING, + null, "loading", null, null, + null, null,null, icon + ); + registerAnnouncementSession(announcement); + return announcement; + } + + + fun getVisibleAnnouncements(category: String? = null): List { synchronized(_lock) { if (category != null) { @@ -122,7 +173,9 @@ class StateAnnouncement { } } - fun closeAnnouncement(id: String) { + fun closeAnnouncement(id: String?) { + if(id == null) + return; val item: Announcement?; synchronized(_lock) { item = _announcementsStore.findItem { it.id == id }; @@ -164,6 +217,7 @@ class StateAnnouncement { cancelAction?.invoke(item); } } + onAnnouncementChanged?.emit(); } fun deleteAllAnnouncements() { @@ -194,7 +248,9 @@ class StateAnnouncement { onAnnouncementChanged.emit(); } - fun neverAnnouncement(id: String) { + fun neverAnnouncement(id: String?) { + if(id == null) + return; synchronized(_lock) { val item = _announcementsStore.findItem { it.id == id }; if (item != null && !_announcementsNever.contains(id)) @@ -208,19 +264,26 @@ class StateAnnouncement { _announcementsNever.save(); onAnnouncementChanged.emit(); } - fun actionAnnouncement(id: String) { + fun actionAnnouncement(id: String?, extra: Boolean = false) { + if(id == null) + return; val item = _announcementsStore.findItem { it.id == id } ?: _sessionAnnouncements[id]; if(item != null) - actionAnnouncement(item); + actionAnnouncement(item, extra); } - fun actionAnnouncement(item: Announcement) { + fun actionAnnouncement(item: Announcement, extra: Boolean = false) { + val actionId = if(!extra) item.actionId else if(item is SessionAnnouncement) item.extraActionId else null; + val actionData = if(!extra) item.actionData else if(item is SessionAnnouncement) item.extraActionData else null; + val action = _sessionActions[item.id]; if (action != null) { action(item); } else { - when (item.actionId) { + when (actionId) { ACTION_NEVER -> neverAnnouncement(item.id); ACTION_SOMETHING -> actionSomething(); + ACTION_CHANGELOG -> actionChangelog(actionData); + ACTION_UPDATE_PLUGIN -> actionUpdatePlugin(item.id, actionData); } } } @@ -251,6 +314,84 @@ class StateAnnouncement { } + private fun actionChangelog(id: String?) { + if(id == null) + return; + + StateApp.instance.contextOrNull?.let { context -> + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + val plugin = StatePlugins.instance.getPlugin(id); + if (plugin == null) + return@launch + val update = StatePlugins.instance.checkForUpdates(plugin.config); + if(update == null) + return@launch; + + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + UIDialogs.showChangelogDialog(context, update.version, update.changelog!!.filterKeys { it.toIntOrNull() != null } + .mapKeys { it.key.toInt() } + .mapValues { update.getChangelogString(it.key.toString()) ?: "" }); + } + } + } + } + private fun actionUpdatePlugin(notifId: String?, id: String?) { + if(id == null) + return; + val plugin = StatePlugins.instance.getPlugin(id); + if (plugin == null) + return + + closeAnnouncement(notifId); + val loadingAnnouncement = registerLoading("Updating ${plugin.config.name}..", "An update is in progress for ${plugin.config.name}.", + if(plugin.config.absoluteIconUrl != null) ImageVariable.fromUrl(plugin.config.absoluteIconUrl!!) else null); + + val loadingId = loadingAnnouncement.id; + + StateApp.instance.contextOrNull?.let { context -> + + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + try { + val update = StatePlugins.instance.checkForUpdates(plugin.config); + if (update == null) + return@launch; + + val client = ManagedHttpClient(); + client.setTimeout(10000); + val script = StatePlugins.instance.getScript(plugin.config.id) ?: ""; + val newScript = client.get(update.absoluteScriptUrl)?.body?.string(); + if(newScript.isNullOrEmpty()) + throw IllegalStateException("No script found"); + + if(true || plugin.config.isLowRiskUpdate(script, update, newScript)) { + StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, update, newScript, + { text: String, progress: Double -> }, + { ex -> + if(ex == null) { + registerPluginUpdated(update); + } + else { + UIDialogs.appToast("Update for ${update.name} failed\n" + ex.message); + } + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + closeAnnouncement(loadingId); + } + }); + } + else { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + closeAnnouncement(loadingId); + UIDialogs.showPluginUpdateDialog(context, plugin.config, update); + } + } + } + catch(ex: Throwable) { + Logger.e(TAG, "Failed to trigger update from announcement", ex); + } + } + } + } + fun registerDefaultHandlerAnnouncement() { registerAnnouncement( "default-url-handler", @@ -279,6 +420,8 @@ class StateAnnouncement { const val ACTION_SOMETHING = "SOMETHING"; + const val ACTION_CHANGELOG = "CHANGELOG"; + const val ACTION_UPDATE_PLUGIN = "UPDATE_PLUGIN"; const val ACTION_NEVER = "NEVER"; private const val TAG = "StateAnnouncement"; } @@ -294,7 +437,8 @@ open class Announcement( val time: OffsetDateTime? = null, val category: String? = null, val actionName: String? = null, - val actionId: String? = null + val actionId: String? = null, + val actionData: String? = null ); class SessionAnnouncement( id: String, @@ -306,7 +450,9 @@ class SessionAnnouncement( actionName: String? = null, actionId: String? = null, val cancelName: String? = null, - val cancelActionId: String? = null + val cancelActionId: String? = null, + actionData: String? = null, + val icon: ImageVariable? = null ): Announcement( id= id, title = title, @@ -315,13 +461,40 @@ class SessionAnnouncement( time = time, category = category, actionName = actionName, - actionId = actionId -); + actionId = actionId, + actionData = actionData +) { + var extraActionName: String? = null; + var extraActionId: String? = null; + var extraActionData: String? = null; + + var extraObj: Any? = null; + + var progress: Double? = null; + val onProgressChanged = Event1(); + + fun withExtraAction(name: String, id: String, data: String? = null): SessionAnnouncement { + extraActionName = name; + extraActionId = id; + extraActionData = data; + return this; + } + + fun setProgress(progress: Double) { + this.progress = progress; + onProgressChanged?.emit(this); + } + fun setProgress(progress: Int) { + this.progress = progress.toDouble().div(100); + onProgressChanged?.emit(this); + } +} enum class AnnouncementType(val value : Int) { DELETABLE(0), //Close button deletes announcement (generally for actions) RECURRING(1), //Shows up till never is pressed (generally for patchnotes etc) PERMANENT(2), //Shows up until deleted through other means (action) SESSION(3), //Not persistent, only during this session - SESSION_RECURRING(4); //Not persistent, only during this session, recurring id + SESSION_RECURRING(4), //Not persistent, only during this session, recurring id + ONGOING(5); } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index ad58ffbb..b5fd97f0 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -43,6 +43,7 @@ import com.futo.platformplayer.logging.AndroidLogConsumer import com.futo.platformplayer.logging.FileLogConsumer import com.futo.platformplayer.logging.LogLevel import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.receivers.AudioNoisyReceiver import com.futo.platformplayer.services.DownloadService import com.futo.platformplayer.stores.FragmentedStorage @@ -732,8 +733,10 @@ class StateApp { )); for(update in updateAvailable) - if(StatePlatform.instance.isClientEnabled(update.first.id)) - UIDialogs.showPluginUpdateDialog(context, update.first, update.second); + if(StatePlatform.instance.isClientEnabled(update.first.id)) { + //UIDialogs.showPluginUpdateDialog(context, update.first, update.second); + StateAnnouncement.instance.registerPluginUpdate(update.first, update.second); + } } } } diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt index 38473023..b7c2be7a 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt @@ -116,7 +116,7 @@ class StatePlugins { _updatesAvailableMap = updatesAvailableFor return@withContext configs; } - private suspend fun checkForUpdates(c: SourcePluginConfig): SourcePluginConfig? = withContext(Dispatchers.IO) { + suspend fun checkForUpdates(c: SourcePluginConfig): SourcePluginConfig? = withContext(Dispatchers.IO) { val sourceUrl = c.sourceUrl ?: return@withContext null; Logger.i(TAG, "Check for source updates '${c.name}'."); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt b/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt index 2a39bb1b..1a000045 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt @@ -113,7 +113,10 @@ class StateUpdate { if (!dir.exists()) { dir.mkdirs(); } - return File(dir, "app-${DESIRED_ABI}-${version}.apk"); + val result = File(dir, "app-${DESIRED_ABI}-${version}.apk"); + //if(result.exists()) + // result.delete(); + return result; } fun getPartialApkFile(context: Context, version: Int): File { @@ -121,7 +124,10 @@ class StateUpdate { if (!dir.exists()) { dir.mkdirs(); } - return File(dir, "app-${DESIRED_ABI}-${version}.apk.part"); + val result = File(dir, "app-${DESIRED_ABI}-${version}.apk.part"); + //if(result.exists()) + // result.delete(); + return result; } fun finish() { 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 68d38c4d..2d7491b8 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 @@ -6,12 +6,10 @@ import android.view.View import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event0 -import com.futo.platformplayer.dp import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.Announcement import com.futo.platformplayer.states.AnnouncementType @@ -162,6 +160,10 @@ class AnnouncementView : LinearLayout { _textClose.visibility = View.VISIBLE; _textNever.visibility = View.VISIBLE; } + AnnouncementType.ONGOING -> { + _textClose.visibility = View.GONE; + _textNever.visibility = View.GONE; + } } if (announcement.time != null) { diff --git a/app/src/main/java/com/futo/platformplayer/views/notification/NotificationOverlayView.kt b/app/src/main/java/com/futo/platformplayer/views/notification/NotificationOverlayView.kt new file mode 100644 index 00000000..84ce2375 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/notification/NotificationOverlayView.kt @@ -0,0 +1,238 @@ +package com.futo.platformplayer.views.notification + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.recyclerview.widget.RecyclerView +import com.futo.platformplayer.R +import com.futo.platformplayer.fragment.mainactivity.main.MainFragment +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.Announcement +import com.futo.platformplayer.states.AnnouncementType +import com.futo.platformplayer.states.SessionAnnouncement +import com.futo.platformplayer.states.StateAnnouncement +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.views.AnyAdapterView +import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny +import com.futo.platformplayer.views.LoaderView +import com.futo.platformplayer.views.adapters.AnyAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class NotificationOverlayView: ConstraintLayout { + + lateinit var recycler: RecyclerView; + var adapterNotifications: AnyAdapterView; + + constructor(context: Context) : super(context) { + inflate(context, R.layout.overlay_notifications, this) + + recycler = findViewById(R.id.container_notifications); + adapterNotifications = recycler.asAny(RecyclerView.VERTICAL, false, { + + }); + + } + + fun onShown(parameter: Any?) { + val announcements = StateAnnouncement.instance.getVisibleAnnouncements(); + adapterNotifications.adapter.setData(announcements); + + StateAnnouncement.instance.onAnnouncementChanged.subscribe(this) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + Logger.i("NotificationOverlayView", "Announcements Changed"); + val adapter = adapterNotifications; + val announcements = StateAnnouncement.instance.getVisibleAnnouncements(); + adapter.adapter.setData(announcements); + } + } + } + + fun onResume() { + + } + + fun onPause() { + StateAnnouncement.instance.onAnnouncementChanged.remove(this); + } + + class ViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder( + LayoutInflater.from(_viewGroup.context).inflate( + R.layout.list_announcement, + _viewGroup, false)) { + + protected var _announcement: Announcement? = null; + protected val _textName: TextView + protected val _textMetadata: TextView; + protected val _icon: ImageView; + protected val _buttonIgnore: ImageView + protected val _buttonNever: LinearLayout + protected val _buttonAction: LinearLayout + protected val _buttonActionText: TextView + protected val _buttonExtra: LinearLayout + protected val _buttonExtraText: TextView + protected val _loader: LoaderView; + protected val _progress: ProgressBar; + + init { + _textName = _view.findViewById(R.id.text_name); + _textMetadata = _view.findViewById(R.id.text_metadata); + _buttonIgnore = _view.findViewById(R.id.button_ignore); + _buttonNever = _view.findViewById(R.id.button_never); + _buttonAction = _view.findViewById(R.id.button_action); + _buttonActionText = _view.findViewById(R.id.button_action_text); + _buttonExtra = _view.findViewById(R.id.button_extra); + _buttonExtraText = _view.findViewById(R.id.button_extra_text); + _icon = _view.findViewById(R.id.icon); + _loader = _view.findViewById(R.id.loader); + _progress = _view.findViewById(R.id.progress); + + _buttonIgnore.setOnClickListener { + _announcement.let { + StateAnnouncement.instance.closeAnnouncement(it?.id); + } + } + _buttonNever.setOnClickListener { + _announcement.let { + StateAnnouncement.instance.neverAnnouncement(it?.id); + } + } + _buttonExtra.setOnClickListener { + _announcement.let { + StateAnnouncement.instance.actionAnnouncement(it?.id, true) + } + } + _buttonAction.setOnClickListener { + _announcement.let { + StateAnnouncement.instance.actionAnnouncement(it?.id); + } + } + } + + + + override fun bind(value: Announcement) { + val oldAnnouncement = _announcement; + _announcement = value; + + if(oldAnnouncement is SessionAnnouncement) + oldAnnouncement.onProgressChanged.clear(); + + _textName.text = value.title; + _textMetadata.text = value.msg; + + if(value is SessionAnnouncement) { + if(value.icon != null) { + value.icon.setImageView(_icon); + _icon.visibility = View.VISIBLE; + } + else + _icon.visibility = View.GONE; + if(value.extraActionName != null && value.extraActionId != null) { + _buttonExtraText.text = value.extraActionName; + _buttonExtra.visibility = View.VISIBLE; + } + else + _buttonExtra.visibility = View.GONE; + + if(value.announceType == AnnouncementType.ONGOING) { + _buttonIgnore.visibility = View.GONE; + } + else { + _buttonIgnore.visibility = View.VISIBLE; + } + if(value.progress != null && value.announceType == AnnouncementType.ONGOING) { + _progress.isVisible = true; + _progress.min = 0; + _progress.max = 100; + value.onProgressChanged.subscribe { + val prog = it.progress; + if(prog == 0.toDouble() || prog == 100.toDouble()) { + _progress.isIndeterminate = true; + } + else { + _progress.isIndeterminate = false; + _progress.setProgress(it.progress?.times(100)?.toInt() ?: 0, false); + } + } + } + else + _progress.isVisible = false; + } + else { + _buttonExtra.visibility = View.GONE; + _icon.visibility = View.GONE; + _buttonIgnore.visibility = View.VISIBLE; + } + + if(value.announceType == AnnouncementType.ONGOING) { + _loader.visibility = View.VISIBLE; + _loader.start(); + } + else { + _loader.visibility = View.GONE; + _loader.stop(); + } + + _buttonNever.visibility = + if (value.announceType == AnnouncementType.RECURRING || value.announceType == AnnouncementType.SESSION_RECURRING) + View.VISIBLE + else + View.GONE; + + _buttonAction.visibility = + if(value.actionId != null && value.actionName != null) + View.VISIBLE; + else View.GONE; + + if(value.actionId != null && value.actionName != null) { + _buttonActionText.text = value.actionName; + } + } + + } + + + class Frag : MainFragment() { + override val isMainView : Boolean = true; + override val isTab: Boolean = true; + override val hasBottomBar: Boolean get() = true; + + private var _view: NotificationOverlayView? = null; + + override fun onShownWithView(parameter: Any?, isBack: Boolean) { + super.onShownWithView(parameter, isBack); + _view?.onShown(parameter); + } + + override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = NotificationOverlayView(requireContext()); + _view = view; + return view; + } + + override fun onDestroyMainView() { + super.onDestroyMainView(); + _view = null; + } + + override fun onResume() { + super.onResume() + _view?.onResume(); + } + + override fun onPause() { + super.onPause() + _view?.onPause(); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/background_primary_round_20dp.xml b/app/src/main/res/drawable/background_primary_round_20dp.xml new file mode 100644 index 00000000..7ef91cd2 --- /dev/null +++ b/app/src/main/res/drawable/background_primary_round_20dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_filled.xml b/app/src/main/res/drawable/ic_notifications_filled.xml new file mode 100644 index 00000000..cd47b10a --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index 9454a48e..ac8fe198 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -1,5 +1,6 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index 8ea93ae9..69aaeec9 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -38,11 +38,12 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/overlay_notifications.xml b/app/src/main/res/layout/overlay_notifications.xml new file mode 100644 index 00000000..bb4a0db3 --- /dev/null +++ b/app/src/main/res/layout/overlay_notifications.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4e6f199..b2487a27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -896,6 +896,7 @@ Creator thumbnail Clear search Search + Notifications Search icon Back button App icon