diff options
author | Allan Wang <me@allanwang.ca> | 2019-02-05 23:16:29 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-05 23:16:29 -0500 |
commit | 8e3bfc168009c8682c4f6191d655f3ca10ae9f21 (patch) | |
tree | cf8419a06e193c08622ead5e6854b995a5eeba77 /app/src/main/kotlin | |
parent | 83fb36f666fbb934b74b5f763b8ffb2e56ca7761 (diff) | |
parent | ddfc310fde5f50ba52ef930287449c2e08faaca8 (diff) | |
download | frost-8e3bfc168009c8682c4f6191d655f3ca10ae9f21.tar.gz frost-8e3bfc168009c8682c4f6191d655f3ca10ae9f21.tar.bz2 frost-8e3bfc168009c8682c4f6191d655f3ca10ae9f21.zip |
Merge pull request #1334 from AllanWang/fix/offline-crash
Fix/offline crash
Diffstat (limited to 'app/src/main/kotlin')
9 files changed, 92 insertions, 74 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt index 8d849bff..f4c1244f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt @@ -27,6 +27,7 @@ import ca.allanwang.kau.about.LibraryIItem import ca.allanwang.kau.adapters.FastItemThemedAdapter import ca.allanwang.kau.adapters.ThemableIItem import ca.allanwang.kau.adapters.ThemableIItemDelegate +import ca.allanwang.kau.logging.KL import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.dimenPixelSize import ca.allanwang.kau.utils.resolveDrawable @@ -79,7 +80,8 @@ class AboutActivity : AboutActivityBase(null, { ) val l = libs.prepareLibraries(this, include, null, false, true, true) -// l.forEach { KL.d{"Lib ${it.definedName}"} } + if (BuildConfig.DEBUG) + l.forEach { KL.d { "Lib ${it.definedName}" } } return l } @@ -155,7 +157,7 @@ class AboutActivity : AboutActivityBase(null, { val c = itemView.context val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds) images = arrayOf<Pair<IIcon, () -> Unit>>( - GoogleMaterial.Icon.gmd_arrow_downward to { c.startLink(R.string.github_downloads_url) }, + GoogleMaterial.Icon.gmd_file_download to { c.startLink(R.string.github_downloads_url) }, CommunityMaterial.Icon2.cmd_reddit to { c.startLink(R.string.reddit_url) }, CommunityMaterial.Icon.cmd_github_circle to { c.startLink(R.string.github_url) }, CommunityMaterial.Icon2.cmd_slack to { c.startLink(R.string.slack_url) }, diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt index 6cf6f41b..9ee34ab7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt @@ -46,7 +46,7 @@ enum class FbItem( FEED_TOP_STORIES(R.string.top_stories, GoogleMaterial.Icon.gmd_star, "home.php?sk=h_nor"), FRIENDS(R.string.friends, GoogleMaterial.Icon.gmd_person_add, "friends/center/requests"), GROUPS(R.string.groups, GoogleMaterial.Icon.gmd_group, "groups"), - MARKETPLACE(R.string.marketplace, CommunityMaterial.Icon2.cmd_home_currency_usd, "marketplace"), + MARKETPLACE(R.string.marketplace, GoogleMaterial.Icon.gmd_store, "marketplace"), MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings", ::MenuFragment), MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"), NOTES(R.string.notes, CommunityMaterial.Icon2.cmd_note, "notes"), diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt index 9f26f3f7..37af690b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt @@ -30,6 +30,7 @@ import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostJsoup import com.pitchedapps.frost.views.FrostRecyclerView import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext /** @@ -53,7 +54,7 @@ abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), Recycle val data = try { reloadImpl(progress) } catch (e: Exception) { - L.e(e) { "Recycler reload fail" } + L.e(e) { "Recycler reload fail $baseUrl" } null } withMainContext { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt index 0caeda1a..a466feec 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt @@ -19,6 +19,7 @@ package com.pitchedapps.frost.injectors import android.content.Context import android.graphics.Color import android.webkit.WebView +import androidx.annotation.VisibleForTesting import ca.allanwang.kau.kotlin.lazyContext import ca.allanwang.kau.utils.adjustAlpha import ca.allanwang.kau.utils.colorToBackground @@ -43,7 +44,8 @@ enum class CssAssets(val folder: String = THEME_FOLDER) : InjectorContract { MATERIAL_LIGHT, MATERIAL_DARK, MATERIAL_AMOLED, MATERIAL_GLASS, CUSTOM, ROUND_ICONS("components") ; - private val file = "${name.toLowerCase(Locale.CANADA)}.css" + @VisibleForTesting + internal val file = "${name.toLowerCase(Locale.CANADA)}.css" /** * Note that while this can be loaded from any thread, it is typically done through [load] diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt index 4b1bde43..e0be7977 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt @@ -18,6 +18,7 @@ package com.pitchedapps.frost.injectors import android.content.Context import android.webkit.WebView +import androidx.annotation.VisibleForTesting import ca.allanwang.kau.kotlin.lazyContext import com.pitchedapps.frost.utils.L import kotlinx.coroutines.Dispatchers @@ -32,11 +33,12 @@ import java.util.Locale * The enum name must match the css file name */ enum class JsAssets : InjectorContract { - MENU, MENU_DEBUG, CLICK_A, CONTEXT_A, MEDIA, HEADER_BADGES, HEADER_HIDER, TEXTAREA_LISTENER, NOTIF_MSG, + MENU, CLICK_A, CONTEXT_A, MEDIA, HEADER_BADGES, HEADER_HIDER, TEXTAREA_LISTENER, NOTIF_MSG, DOCUMENT_WATCHER ; - private val file = "${name.toLowerCase(Locale.CANADA)}.js" + @VisibleForTesting + internal val file = "${name.toLowerCase(Locale.CANADA)}.js" private val injector = lazyContext { try { val content = it.assets.open("js/$file").bufferedReader().use(BufferedReader::readText) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt index 914ce151..56acfc11 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt @@ -16,11 +16,14 @@ */ package com.pitchedapps.frost.kotlin +import com.pitchedapps.frost.utils.L import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -64,57 +67,60 @@ class Flyweight<K, V>( completeExceptionally(result.exceptionOrNull()!!) } + private val errHandler = CoroutineExceptionHandler { _, throwable -> L.d { "FbAuth failed ${throwable.message}" } } + init { - job = scope.launch(Dispatchers.IO) { - launch { - while (isActive) { - select<Unit> { - /* - * New request received. Continuation should be fulfilled eventually - */ - actionChannel.onReceive { (key, completable) -> - val lastUpdate = conditionMap[key] - val lastResult = resultMap[key] - // Valid value, retrieved within acceptable time - if (lastResult != null && lastUpdate != null && System.currentTimeMillis() - lastUpdate < maxAge) { - completable.completeWith(lastResult) - } else { - val valueRequestPending = key in pendingMap - pendingMap.getOrPut(key) { mutableListOf() }.add(completable) - if (!valueRequestPending) - fulfill(key) + job = + scope.launch(Dispatchers.IO + SupervisorJob() + errHandler) { + launch { + while (isActive) { + select<Unit> { + /* + * New request received. Continuation should be fulfilled eventually + */ + actionChannel.onReceive { (key, completable) -> + val lastUpdate = conditionMap[key] + val lastResult = resultMap[key] + // Valid value, retrieved within acceptable time + if (lastResult != null && lastUpdate != null && System.currentTimeMillis() - lastUpdate < maxAge) { + completable.completeWith(lastResult) + } else { + val valueRequestPending = key in pendingMap + pendingMap.getOrPut(key) { mutableListOf() }.add(completable) + if (!valueRequestPending) + fulfill(key) + } } - } - /* - * Invalidator received. Existing result associated with key should not be used. - * Note that any unfulfilled request and future requests should still operate, but with a new value. - */ - invalidatorChannel.onReceive { key -> - if (key !in resultMap) { - // Nothing to invalidate. - // If pending requests exist, they are already in the process of being updated. - return@onReceive + /* + * Invalidator received. Existing result associated with key should not be used. + * Note that any unfulfilled request and future requests should still operate, but with a new value. + */ + invalidatorChannel.onReceive { key -> + if (key !in resultMap) { + // Nothing to invalidate. + // If pending requests exist, they are already in the process of being updated. + return@onReceive + } + conditionMap.remove(key) + resultMap.remove(key) + if (pendingMap[key]?.isNotEmpty() == true) + // Refetch value for pending requests + fulfill(key) } - conditionMap.remove(key) - resultMap.remove(key) - if (pendingMap[key]?.isNotEmpty() == true) - // Refetch value for pending requests - fulfill(key) - } - /* - * Value request fulfilled. Should now fulfill pending requests - */ - receiverChannel.onReceive { (key, result) -> - conditionMap[key] = System.currentTimeMillis() - resultMap[key] = result - pendingMap.remove(key)?.forEach { - it.completeWith(result) + /* + * Value request fulfilled. Should now fulfill pending requests + */ + receiverChannel.onReceive { (key, result) -> + conditionMap[key] = System.currentTimeMillis() + resultMap[key] = result + pendingMap.remove(key)?.forEach { + it.completeWith(result) + } } } } } } - } } /* diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt index 62330e4d..fbaa4574 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt @@ -20,7 +20,6 @@ import android.content.Context import ca.allanwang.kau.utils.copyToClipboard import ca.allanwang.kau.utils.shareText import ca.allanwang.kau.utils.string -import ca.allanwang.kau.utils.toast import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.facebook.formattedFbUrl @@ -29,19 +28,19 @@ import com.pitchedapps.frost.facebook.formattedFbUrl * Created by Allan Wang on 2017-07-07. */ fun Context.showWebContextMenu(wc: WebContext) { - - var title = wc.url + if (wc.isEmpty) return + var title = wc.url ?: string(R.string.menu) title = title.substring(title.indexOf("m/") + 1) //just so if defaults to 0 in case it's not .com/ if (title.length > 100) title = title.substring(0, 100) + '\u2026' + val menuItems = WebContextType.values + .filter { it.constraint(wc) } + materialDialogThemed { title(title) - items(WebContextType.values.map { - if (it == WebContextType.COPY_TEXT && wc.text == null) return@map null - this@showWebContextMenu.string(it.textId) - }.filterNotNull()) + items(menuItems.map { string(it.textId) }) itemsCallback { _, _, position, _ -> - WebContextType[position].onClick(this@showWebContextMenu, wc) + menuItems[position].onClick(this@showWebContextMenu, wc) } dismissListener { //showing the dialog interrupts the touch down event, so we must ensure that the viewpager's swipe is enabled @@ -50,18 +49,23 @@ fun Context.showWebContextMenu(wc: WebContext) { } } -class WebContext(val unformattedUrl: String, val text: String?) { - val url = unformattedUrl.formattedFbUrl +class WebContext(val unformattedUrl: String?, val text: String?) { + val url: String? = unformattedUrl?.formattedFbUrl + inline val hasUrl get() = unformattedUrl != null + inline val hasText get() = text != null + inline val isEmpty get() = !hasUrl && !hasText } -enum class WebContextType(val textId: Int, val onClick: (c: Context, wc: WebContext) -> Unit) { - OPEN_LINK(R.string.open_link, { c, wc -> c.launchWebOverlay(wc.unformattedUrl) }), - COPY_LINK(R.string.copy_link, { c, wc -> c.copyToClipboard(wc.url) }), - COPY_TEXT( - R.string.copy_text, - { c, wc -> if (wc.text != null) c.copyToClipboard(wc.text) else c.toast(R.string.no_text) }), - SHARE_LINK(R.string.share_link, { c, wc -> c.shareText(wc.url) }), - DEBUG_LINK(R.string.debug_link, { c, wc -> +enum class WebContextType( + val textId: Int, + val constraint: (wc: WebContext) -> Boolean, + val onClick: (c: Context, wc: WebContext) -> Unit +) { + OPEN_LINK(R.string.open_link, { it.hasUrl }, { c, wc -> c.launchWebOverlay(wc.unformattedUrl!!) }), + COPY_LINK(R.string.copy_link, { it.hasUrl }, { c, wc -> c.copyToClipboard(wc.url) }), + COPY_TEXT(R.string.copy_text, { it.hasText }, { c, wc -> c.copyToClipboard(wc.text) }), + SHARE_LINK(R.string.share_link, { it.hasUrl }, { c, wc -> c.shareText(wc.url) }), + DEBUG_LINK(R.string.debug_link, { it.hasUrl }, { c, wc -> c.materialDialogThemed { title(R.string.debug_link) content(R.string.debug_link_desc) @@ -69,8 +73,8 @@ enum class WebContextType(val textId: Int, val onClick: (c: Context, wc: WebCont onPositive { _, _ -> c.sendFrostEmail(R.string.debug_link_subject) { message = c.string(R.string.debug_link_content) - addItem("Unformatted url", wc.unformattedUrl) - addItem("Formatted url", wc.url) + addItem("Unformatted url", wc.unformattedUrl!!) + addItem("Formatted url", wc.url!!) } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt index 860bf36c..ce7437a7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -27,8 +27,10 @@ import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.fragments.RecyclerContentContract +import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch /** @@ -74,7 +76,7 @@ class FrostRecyclerView @JvmOverloads constructor( if (Prefs.animate) fadeOut(onFinish = onReloadClear) scope.launch { parent.refreshChannel.offer(true) - val loaded = recyclerContract.reload { parent.progressChannel.offer(it) } + recyclerContract.reload { parent.progressChannel.offer(it) } parent.progressChannel.offer(100) parent.refreshChannel.offer(false) if (Prefs.animate) circularReveal() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 19d16e87..50a5e2e1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -76,10 +76,9 @@ class FrostJSI(val web: FrostWebView) { } @JavascriptInterface - fun contextMenu(url: String, text: String?) { - if (!text.isIndependent) return + fun contextMenu(url: String?, text: String?) { //url will be formatted through webcontext - web.post { context.showWebContextMenu(WebContext(url, text)) } + web.post { context.showWebContextMenu(WebContext(url.takeIf { it.isIndependent }, text)) } } /** |