From 84bf883a47b956865d31b1b618d5495fcd7d4876 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 19 Aug 2017 09:47:05 -0700 Subject: v1.4.7 (#195) * Add try catch (#179) * Add checks before injections (#180) * Enhancement/url redirect manager (#182) * Initial blacklist * Move js checks to java * Optimize imports and clean up request interceptor * Misc (#190) * Update play store description * Finalize description * Update kotlin and bg2 for custom themes * Update to Android Studio 3.0 beta 2 * Update test dependencies and add logging to image activity * Rename throwable to errorRef * Update searchview and media picker through kau * Update themes (#183) * Theme content now found view * Update verified bg and bg2 for transparent themes * Fix check in star text * Various fixes * Create base svg sass images * Feature/theme accent (#192) * Add lots of theming components * Optimize and add * Update accents * Misc 2 (#191) * Add further checks for iab and remove generic error dialog * Theme all snackbars * Add dynamic media action tile * Enhancement/media-camera-picker (#194) * Update kau * Update changelog --- .../pitchedapps/frost/activities/AboutActivity.kt | 2 +- .../pitchedapps/frost/activities/ImageActivity.kt | 38 ++++++------ .../pitchedapps/frost/activities/LoginActivity.kt | 1 - .../pitchedapps/frost/activities/MainActivity.kt | 1 + .../frost/activities/MediaPickerActivity.kt | 28 +++++++-- .../frost/activities/WebOverlayActivity.kt | 15 ++--- .../com/pitchedapps/frost/injectors/CssAssets.kt | 8 +-- .../com/pitchedapps/frost/injectors/JsInjector.kt | 3 +- .../com/pitchedapps/frost/settings/Appearance.kt | 13 +++- .../pitchedapps/frost/settings/Notifications.kt | 4 +- .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 24 ++++---- .../kotlin/com/pitchedapps/frost/utils/Theme.kt | 71 +++++++++++++++++++--- .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 37 +++++++---- .../com/pitchedapps/frost/utils/iab/IABBinder.kt | 4 +- .../com/pitchedapps/frost/views/FrostViewPager.kt | 7 ++- .../pitchedapps/frost/web/FrostChromeClients.kt | 5 +- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 10 ++- .../frost/web/FrostRequestInterceptor.kt | 44 +++++++------- .../frost/web/FrostUrlOverlayValidator.kt | 48 +++++++++++++++ .../pitchedapps/frost/web/FrostWebViewClients.kt | 24 ++++---- .../com/pitchedapps/frost/web/LoginWebView.kt | 15 ++--- 21 files changed, 272 insertions(+), 130 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt (limited to 'app/src/main/kotlin/com') 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 670e8669..b7bacbc2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt @@ -33,7 +33,7 @@ class AboutActivity : AboutActivityBase(null, { textColor = Prefs.textColor accentColor = Prefs.accentColor backgroundColor = Prefs.bgColor.withMinAlpha(200) - cutoutForeground = if (0xff3b5998.toInt().isColorVisibleOn(Prefs.bgColor)) 0xff3b5998.toInt() else Prefs.accentColor + cutoutForeground = Prefs.accentColor cutoutDrawableRes = R.drawable.frost_f_256 faqPageTitleRes = R.string.faq_title faqXmlRes = R.xml.frost_faq diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt index 31479d54..9a5f3c6e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -15,6 +15,7 @@ import android.widget.ProgressBar import android.widget.TextView import ca.allanwang.kau.email.sendEmail import ca.allanwang.kau.internal.KauBaseActivity +import ca.allanwang.kau.mediapicker.scanMedia import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE import ca.allanwang.kau.permissions.kauRequestPermissions import ca.allanwang.kau.utils.* @@ -47,6 +48,7 @@ class ImageActivity : KauBaseActivity() { val photo: SubsamplingScaleImageView by bindView(R.id.image_photo) val caption: TextView? by bindOptionalView(R.id.image_text) val fab: FloatingActionButton by bindView(R.id.image_fab) + var errorRef: Throwable? = null /** * Reference to the temporary file path @@ -85,22 +87,22 @@ class ImageActivity : KauBaseActivity() { caption?.text = text progress.tint(Prefs.accentColor) panel?.addPanelSlideListener(object : SlidingUpPanelLayout.SimplePanelSlideListener() { - override fun onPanelSlide(panel: View, slideOffset: Float) { if (slideOffset == 0f && !fab.isShown) fab.show() else if (slideOffset != 0f && fab.isShown) fab.hide() caption?.alpha = slideOffset / 2 + 0.5f } - }) fab.setOnClickListener { fabAction.onClick(this) } photo.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { override fun onImageLoadError(e: Exception?) { + errorRef = e e.logFrostAnswers("Image load error") imageCallback(null, false) } }) Glide.with(this).asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback)) + setFrostColors(themeWindow = false) } /** @@ -145,7 +147,8 @@ class ImageActivity : KauBaseActivity() { var photoFile: File? = null try { photoFile = createPrivateMediaFile(".png") - } catch (ignored: IOException) { + } catch (e: IOException) { + errorRef = e } finally { if (photoFile == null) { callback(null) @@ -173,14 +176,15 @@ class ImageActivity : KauBaseActivity() { var success = true try { File(tempFilePath).copyTo(destination, true) - scanFile(destination) + scanMedia(destination) } catch (e: Exception) { + errorRef = e success = false } finally { L.d("Download image async finished: $success") uiThread { val text = if (success) R.string.image_download_success else R.string.image_download_fail - snackbar(text) + frostSnackbar(text) if (success) fabAction = FabStates.SHARE } } @@ -189,17 +193,6 @@ class ImageActivity : KauBaseActivity() { } } - /** - * See Docs - */ - internal fun scanFile(file: File) { - if (!file.exists()) return - val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) - val contentUri = Uri.fromFile(file) - mediaScanIntent.data = contentUri - this.sendBroadcast(mediaScanIntent) - } - internal fun deleteTempFile() { if (tempFilePath != null) { File(tempFilePath!!).delete() @@ -213,7 +206,7 @@ class ImageActivity : KauBaseActivity() { } } -internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconColor, val backgroundTint: Int = Prefs.iconBackgroundColor.withAlpha(255)) { +internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconColor, val backgroundTint: Int = Int.MAX_VALUE) { ERROR(GoogleMaterial.Icon.gmd_error, Color.WHITE, Color.RED) { override fun onClick(activity: ImageActivity) { activity.materialDialogThemed { @@ -221,8 +214,11 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC content(R.string.bad_image_overlay) positiveText(R.string.kau_yes) onPositive { _, _ -> + if (activity.errorRef != null) + L.e(activity.errorRef, "ImageActivity error report") activity.sendEmail(R.string.dev_email, R.string.debug_image_link_subject) { addItem("Url", activity.imageUrl) + addItem("Message", activity.errorRef?.message ?: "Null") } } negativeText(R.string.kau_no) @@ -248,8 +244,9 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC } activity.startActivity(intent) } catch (e: Exception) { + activity.errorRef = e e.logFrostAnswers("Image share failed") - activity.snackbar(R.string.image_share_failed) + activity.frostSnackbar(R.string.image_share_failed) } } }; @@ -259,14 +256,15 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC * If it's in view, give it some animations */ fun update(fab: FloatingActionButton) { + val tint = if (backgroundTint != Int.MAX_VALUE) backgroundTint else Prefs.accentColor if (fab.isHidden) { fab.setIcon(iicon, color = iconColor) - fab.backgroundTintList = ColorStateList.valueOf(backgroundTint) + fab.backgroundTintList = ColorStateList.valueOf(tint) fab.show() } else { fab.fadeScaleTransition { setIcon(iicon, color = iconColor) - backgroundTintList = ColorStateList.valueOf(backgroundTint) + backgroundTintList = ColorStateList.valueOf(tint) } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 67f07635..6b1f2c5c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -1,6 +1,5 @@ package com.pitchedapps.frost.activities -import android.content.Intent import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Handler diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index 759be983..45488c94 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -368,6 +368,7 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract, if (firstLoadFinished && hiddenSearchView == null) hiddenSearchView = SearchWebView(this, this) if (searchView == null) searchView = bindSearchView(menu, R.id.action_search, Prefs.iconColor) { textCallback = { query, _ -> runOnUiThread { hiddenSearchView?.query(query) } } + searchCallback = { query, _ -> launchWebOverlay("${FbItem.SEARCH.url}/?q=$query"); true } foregroundColor = Prefs.textColor backgroundColor = Prefs.bgColor.withMinAlpha(200) openListener = { hiddenSearchView?.pauseLoad = false } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MediaPickerActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MediaPickerActivity.kt index 0d041e7a..162baf20 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MediaPickerActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MediaPickerActivity.kt @@ -1,10 +1,30 @@ package com.pitchedapps.frost.activities -import ca.allanwang.kau.mediapicker.MediaPickerActivityOverlayBase -import ca.allanwang.kau.mediapicker.MediaType +import android.content.Context +import android.net.Uri +import android.support.v4.content.FileProvider +import ca.allanwang.kau.mediapicker.* +import ca.allanwang.kau.utils.colorToBackground +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.utils.Prefs +import java.io.File /** * Created by Allan Wang on 2017-07-23. */ -class ImagePickerActivity : MediaPickerActivityOverlayBase(MediaType.IMAGE) -class VideoPickerActivity : MediaPickerActivityOverlayBase(MediaType.VIDEO) \ No newline at end of file +private fun actions(): List { + val color = Prefs.accentColorForWhite + return listOf(object : MediaActionCamera(color) { + + override fun createFile(context: Context): File + = createMediaFile("Frost", ".jpg") + + override fun createUri(context: Context, file: File): Uri + = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + + }, MediaActionGallery(color = color.colorToBackground(0.1f))) +} + +class ImagePickerActivity : MediaPickerActivityOverlayBase(MediaType.IMAGE, actions()) + +class VideoPickerActivity : MediaPickerActivityOverlayBase(MediaType.VIDEO, actions()) \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt index 7b612166..51aedae3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -60,10 +60,7 @@ open class WebOverlayActivity : KauBaseActivity(), supportActionBar?.setDisplayHomeAsUpEnabled(true) toolbar.navigationIcon = GoogleMaterial.Icon.gmd_close.toDrawable(this, 16, Prefs.iconColor) toolbar.setNavigationOnClickListener { finishSlideOut() } - kauSwipeOnCreate { - if (!Prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx - transitionSystemBars = false - } + setFrostColors(toolbar, themeWindow = false) coordinator.setBackgroundColor(Prefs.bgColor.withAlpha(255)) @@ -77,6 +74,11 @@ open class WebOverlayActivity : KauBaseActivity(), setAction(R.string.kau_got_it) { _ -> this.dismiss() } } } + + kauSwipeOnCreate { + if (!Prefs.overlayFullScreenSwipe) edgeSize = 20.dpToPx + transitionSystemBars = false + } } /** @@ -108,11 +110,6 @@ open class WebOverlayActivity : KauBaseActivity(), toolbar.overflowIcon?.setTint(Prefs.iconColor) } - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - kauSwipeOnPostCreate() - } - override fun onDestroy() { super.onDestroy() kauSwipeOnDestroy() 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 733bc151..cf935360 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt @@ -25,21 +25,19 @@ enum class CssAssets(val folder: String = "themes") : InjectorContract { try { var content = webView.context.assets.open("css/$folder/$file").bufferedReader().use { it.readText() } if (this == CUSTOM) { - var bbt = Prefs.bgColor val bt: String - if (Color.alpha(bbt) == 255) { - bbt = bbt.adjustAlpha(0.2f).colorToForeground(0.35f) + if (Color.alpha(Prefs.bgColor) == 255) { bt = Prefs.bgColor.toRgbaString() } else { - bbt = bbt.adjustAlpha(0.05f).colorToForeground(0.5f) bt = "transparent" } content = content .replace("\$T\$", Prefs.textColor.toRgbaString()) .replace("\$TT\$", Prefs.textColor.colorToBackground(0.05f).toRgbaString()) + .replace("\$A\$", Prefs.accentColor.toRgbaString()) .replace("\$B\$", Prefs.bgColor.toRgbaString()) .replace("\$BT\$", bt) - .replace("\$BBT\$", bbt.toRgbaString()) + .replace("\$BBT\$", Prefs.bgColor.withAlpha(51).colorToForeground(0.35f).toRgbaString()) .replace("\$O\$", Prefs.bgColor.withAlpha(255).toRgbaString()) .replace("\$OO\$", Prefs.bgColor.colorToForeground(0.35f).withAlpha(255).toRgbaString()) .replace("\$D\$", Prefs.textColor.adjustAlpha(0.3f).toRgbaString()) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt index 0387bb99..9fafbf88 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt @@ -5,13 +5,14 @@ import com.pitchedapps.frost.web.FrostWebViewClient import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.subjects.SingleSubject +import org.apache.commons.text.StringEscapeUtils class JsBuilder { private val css = StringBuilder() private val js = StringBuilder() fun css(css: String): JsBuilder { - this.css.append(css) + this.css.append(StringEscapeUtils.escapeEcmaScript(css)) return this } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt index e37afc33..7dff5b16 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Appearance.kt @@ -55,7 +55,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = { fun KPrefColorPicker.KPrefColorContract.dependsOnCustom() { enabler = { Prefs.isCustomTheme } - onDisabledClick = { itemView, _, _ -> itemView.frostSnackbar(R.string.requires_custom_theme); true } + onDisabledClick = { itemView, _, _ -> frostSnackbar(R.string.requires_custom_theme); true } allowCustom = true } @@ -73,6 +73,17 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = { allowCustomAlpha = false } + colorPicker(R.string.accent_color, { Prefs.customAccentColor }, { + Prefs.customAccentColor = it + reload() + invalidateCustomTheme() + shouldRestartMain() + }) { + dependsOnCustom() + allowCustomAlpha = false + } + + colorPicker(R.string.background_color, { Prefs.customBackgroundColor }, { Prefs.customBackgroundColor = it bgCanvas.ripple(it, duration = 500L) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index b1e5015f..8fcb3594 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -2,12 +2,12 @@ package com.pitchedapps.frost.settings import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder import ca.allanwang.kau.utils.minuteToText -import ca.allanwang.kau.utils.snackbar import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.SettingsActivity import com.pitchedapps.frost.services.fetchNotifications import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.frostSnackbar import com.pitchedapps.frost.utils.materialDialogThemed import com.pitchedapps.frost.views.Keywords @@ -70,7 +70,7 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { onClick = { _, _, _ -> val text = if (fetchNotifications()) R.string.notification_fetch_success else R.string.notification_fetch_fail - snackbar(text) + frostSnackbar(text) true } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt index 9b8064a4..34257da8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -1,10 +1,10 @@ package com.pitchedapps.frost.utils +import android.graphics.Color import ca.allanwang.kau.kotlin.lazyResettable import ca.allanwang.kau.kpref.KPref import ca.allanwang.kau.kpref.StringSet import ca.allanwang.kau.kpref.kpref -import ca.allanwang.kau.utils.colorToForeground import ca.allanwang.kau.utils.isColorVisibleOn import com.pitchedapps.frost.facebook.FeedSort import com.pitchedapps.frost.injectors.InjectorContract @@ -26,6 +26,8 @@ object Prefs : KPref() { var customTextColor: Int by kpref("color_text", 0xffeceff1.toInt()) + var customAccentColor: Int by kpref("color_accent", 0xff0288d1.toInt()) + var customBackgroundColor: Int by kpref("color_bg", 0xff212121.toInt()) var customHeaderColor: Int by kpref("color_header", 0xff01579b.toInt()) @@ -49,6 +51,14 @@ object Prefs : KPref() { val textColor: Int get() = t.textColor + val accentColor: Int + get() = t.accentColor + + val accentColorForWhite: Int + get() = if (accentColor.isColorVisibleOn(Color.WHITE)) accentColor + else if (textColor.isColorVisibleOn(Color.WHITE)) textColor + else FACEBOOK_BLUE + val bgColor: Int get() = t.bgColor @@ -58,18 +68,6 @@ object Prefs : KPref() { val iconColor: Int get() = t.iconColor - /** - * Ensures that the color is visible against the background - */ - val accentColor: Int - get() = if (headerColor.isColorVisibleOn(bgColor, 100)) headerColor else textColor - - /** - * Ensures that the color is visible against the background - */ - val iconBackgroundColor: Int - get() = if (headerColor.isColorVisibleOn(bgColor)) headerColor else headerColor.colorToForeground(0.2f) - val themeInjector: InjectorContract get() = t.injector diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Theme.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Theme.kt index cb265149..5cbb051d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Theme.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Theme.kt @@ -10,20 +10,71 @@ import com.pitchedapps.frost.injectors.JsActions /** * Created by Allan Wang on 2017-06-14. */ -enum class Theme(@StringRes val textRes: Int, val injector: InjectorContract, - private val textColorGetter: () -> Int, private val backgroundColorGetter: () -> Int, - private val headerColorGetter: () -> Int, private val iconColorGetter: () -> Int) { - DEFAULT(R.string.kau_default, JsActions.EMPTY, { 0xde000000.toInt() }, { 0xfffafafa.toInt() }, { 0xff3b5998.toInt() }, { Color.WHITE }), - LIGHT(R.string.kau_light, CssAssets.MATERIAL_LIGHT, { 0xde000000.toInt() }, { 0xfffafafa.toInt() }, { 0xff3b5998.toInt() }, { Color.WHITE }), - DARK(R.string.kau_dark, CssAssets.MATERIAL_DARK, { Color.WHITE }, { 0xff303030.toInt() }, { 0xff2e4b86.toInt() }, { Color.WHITE }), - AMOLED(R.string.kau_amoled, CssAssets.MATERIAL_AMOLED, { Color.WHITE }, { Color.BLACK }, { Color.BLACK }, { Color.WHITE }), - GLASS(R.string.kau_glass, CssAssets.MATERIAL_GLASS, { Color.WHITE }, { 0x80000000.toInt() }, { 0xb3000000.toInt() }, { Color.WHITE }), - CUSTOM(R.string.kau_custom, CssAssets.CUSTOM, { Prefs.customTextColor }, { Prefs.customBackgroundColor }, { Prefs.customHeaderColor }, { Prefs.customIconColor }) - ; +const val FACEBOOK_BLUE = 0xff3b5998.toInt() +const val BLUE_LIGHT = 0xff5d86dd.toInt() + +enum class Theme(@StringRes val textRes: Int, + val injector: InjectorContract, + private val textColorGetter: () -> Int, + private val accentColorGetter: () -> Int, + private val backgroundColorGetter: () -> Int, + private val headerColorGetter: () -> Int, + private val iconColorGetter: () -> Int) { + + DEFAULT(R.string.kau_default, + JsActions.EMPTY, + { 0xde000000.toInt() }, + { FACEBOOK_BLUE }, + { 0xfffafafa.toInt() }, + { FACEBOOK_BLUE }, + { Color.WHITE }), + + LIGHT(R.string.kau_light, + CssAssets.MATERIAL_LIGHT, + { 0xde000000.toInt() }, + { FACEBOOK_BLUE }, + { 0xfffafafa.toInt() }, + { FACEBOOK_BLUE }, + { Color.WHITE }), + + DARK(R.string.kau_dark, + CssAssets.MATERIAL_DARK, + { Color.WHITE }, + { BLUE_LIGHT }, + { 0xff303030.toInt() }, + { 0xff2e4b86.toInt() }, + { Color.WHITE }), + + AMOLED(R.string.kau_amoled, + CssAssets.MATERIAL_AMOLED, + { Color.WHITE }, + { BLUE_LIGHT }, + { Color.BLACK }, + { Color.BLACK }, + { Color.WHITE }), + + GLASS(R.string.kau_glass, + CssAssets.MATERIAL_GLASS, + { Color.WHITE }, + { BLUE_LIGHT }, + { 0x80000000.toInt() }, + { 0xb3000000.toInt() }, + { Color.WHITE }), + + CUSTOM(R.string.kau_custom, + CssAssets.CUSTOM, + { Prefs.customTextColor }, + { Prefs.customAccentColor }, + { Prefs.customBackgroundColor }, + { Prefs.customHeaderColor }, + { Prefs.customIconColor }); val textColor: Int get() = textColorGetter() + val accentColor: Int + get() = accentColorGetter() + val bgColor: Int get() = backgroundColorGetter() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index e79816f3..469a3951 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -13,6 +13,8 @@ import android.support.v7.widget.Toolbar import android.view.View import android.widget.FrameLayout import android.widget.TextView +import ca.allanwang.kau.mediapicker.createMediaFile +import ca.allanwang.kau.mediapicker.createPrivateMediaFile import ca.allanwang.kau.utils.* import com.afollestad.materialdialogs.MaterialDialog import com.bumptech.glide.RequestBuilder @@ -54,6 +56,11 @@ fun Activity.cookies(): ArrayList { return intent?.extras?.getParcelableArrayList(EXTRA_COOKIES) ?: arrayListOf() } +/** + * Launches the given url in a new overlay (if it already isn't in an overlay) + * Note that most requests may need to first check if the url can be launched as an overlay + * See [requestWebOverlay] to verify the launch + */ fun Context.launchWebOverlay(url: String) { val argUrl = url.formattedFbUrl L.v("Launch received", url) @@ -141,17 +148,20 @@ fun Throwable?.logFrostAnswers(text: String) { frostAnswersCustom("Errors", "text" to text, "message" to (this?.message ?: "NA")) } -fun View.frostSnackbar(@StringRes text: Int, builder: Snackbar.() -> Unit = {}) { - Snackbar.make(this, text, Snackbar.LENGTH_LONG).apply { - builder() - //hacky workaround, but it has proper checks and shouldn't crash - ((view as? FrameLayout)?.getChildAt(0) as? SnackbarContentLayout)?.apply { - messageView.setTextColor(Prefs.textColor) - actionView.setTextColor(Prefs.accentColor) - //only set if previous text colors are set - view.setBackgroundColor(Prefs.bgColor.withAlpha(255).colorToForeground(0.1f)) - } - show() +fun Activity.frostSnackbar(@StringRes text: Int, builder: Snackbar.() -> Unit = {}) + = snackbar(text, Snackbar.LENGTH_LONG, frostSnackbar(builder)) + +fun View.frostSnackbar(@StringRes text: Int, builder: Snackbar.() -> Unit = {}) + = snackbar(text, Snackbar.LENGTH_LONG, frostSnackbar(builder)) + +private inline fun frostSnackbar(crossinline builder: Snackbar.() -> Unit): Snackbar.() -> Unit = { + builder() + //hacky workaround, but it has proper checks and shouldn't crash + ((view as? FrameLayout)?.getChildAt(0) as? SnackbarContentLayout)?.apply { + messageView.setTextColor(Prefs.textColor) + actionView.setTextColor(Prefs.accentColor) + //only set if previous text colors are set + view.setBackgroundColor(Prefs.bgColor.withAlpha(255).colorToForeground(0.1f)) } } @@ -172,10 +182,13 @@ fun Context.createPrivateMediaFile(extension: String) = createPrivateMediaFile(" * @returns {@code true} if activity is resolved, {@code false} otherwise */ fun Context.resolveActivityForUri(uri: Uri): Boolean { - if (uri.toString().contains(FACEBOOK_COM) && !uri.toString().contains("intent:")) return false //ignore response as we will be triggering ourself + if (uri.toString().isFacebookUrl && !uri.toString().contains("intent:")) return false //ignore response as we will be triggering ourself val intent = Intent(Intent.ACTION_VIEW, uri) if (intent.resolveActivity(packageManager) == null) return false startActivity(intent) return true } +inline val String?.isFacebookUrl + get() = this != null && this.contains(FACEBOOK_COM) + diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt index 7f6e8a6d..8aa3bcde 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt @@ -110,7 +110,7 @@ abstract class IABBinder : FrostBilling { } val a = activity ?: return - if (!BillingProcessor.isIabServiceAvailable(a) || !bp.isOneTimePurchaseSupported) + if (!BillingProcessor.isIabServiceAvailable(a) || !bp.isInitialized || !bp.isOneTimePurchaseSupported) a.playStorePurchaseUnsupported() else bp.purchase(a, FROST_PRO) @@ -127,7 +127,7 @@ class IABSettings : IABBinder() { override fun onBillingError(errorCode: Int, error: Throwable?) { super.onBillingError(errorCode, error) - activity?.playStoreGenericError(null) + L.e("Billing error $errorCode ${error?.message}") } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostViewPager.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostViewPager.kt index d692c7aa..91673b15 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostViewPager.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostViewPager.kt @@ -15,7 +15,12 @@ import com.pitchedapps.frost.utils.Prefs class FrostViewPager @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) { var enableSwipe = true - override fun onInterceptTouchEvent(ev: MotionEvent?) = Prefs.viewpagerSwipe && enableSwipe && super.onInterceptTouchEvent(ev) + override fun onInterceptTouchEvent(ev: MotionEvent?) = + try { + Prefs.viewpagerSwipe && enableSwipe && super.onInterceptTouchEvent(ev) + } catch(e: IllegalArgumentException) { + true + } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(ev: MotionEvent?): Boolean = Prefs.viewpagerSwipe && enableSwipe && super.onTouchEvent(ev) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt index 61711092..386c5339 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt @@ -4,9 +4,10 @@ import android.net.Uri import android.webkit.* import ca.allanwang.kau.permissions.PERMISSION_ACCESS_FINE_LOCATION import ca.allanwang.kau.permissions.kauRequestPermissions -import ca.allanwang.kau.utils.snackbar +import com.pitchedapps.frost.R import com.pitchedapps.frost.contracts.ActivityWebContract import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.frostSnackbar import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject @@ -52,7 +53,7 @@ class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() { } override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback>, fileChooserParams: FileChooserParams): Boolean { - activityContract?.openFileChooser(filePathCallback, fileChooserParams) ?: webView.snackbar("File chooser not found") + activityContract?.openFileChooser(filePathCallback, fileChooserParams) ?: webView.frostSnackbar(R.string.file_chooser_not_found) return activityContract != null } 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 f24a7a51..2abc9b25 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -25,10 +25,14 @@ class FrostJSI(val webView: FrostWebViewCore) { val cookies: ArrayList get() = activity?.cookies() ?: arrayListOf() + /** + * Attempts to load the url in an overlay + * Returns {@code true} if successful, meaning the event is consumed, + * or {@code false} otherwise, meaning the event should be propagated + */ @JavascriptInterface - fun loadUrl(url: String) { - context.launchWebOverlay(url) - } + fun loadUrl(url: String?): Boolean + = if (url == null) false else context.requestWebOverlay(url) @JavascriptInterface fun reloadBaseUrl(animate: Boolean) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt index 1a907f7f..2dfdda89 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt @@ -3,6 +3,8 @@ package com.pitchedapps.frost.web import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView +import ca.allanwang.kau.kotlin.LazyContext +import ca.allanwang.kau.kotlin.lazyContext import ca.allanwang.kau.utils.use import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs @@ -19,41 +21,37 @@ import java.io.ByteArrayInputStream private val blankResource: WebResourceResponse by lazy { WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) } //these hosts will redirect to a blank resource -private val blacklistHost: Set by lazy { - setOf( - "edge-chat.facebook.com" - ) -} +private val blacklistHost: Set = + setOf( + "edge-chat.facebook.com" + ) //these hosts will return null and skip logging -private val whitelistHost: Set by lazy { - setOf( - "static.xx.fbcdn.net", - "m.facebook.com", - "touch.facebook.com" - ) -} +private val whitelistHost: Set = + setOf( + "static.xx.fbcdn.net", + "m.facebook.com", + "touch.facebook.com" + ) //these hosts will skip ad inspection //this list does not have to include anything from the two above -private val adWhitelistHost: Set by lazy { - setOf( - "scontent-sea1-1.xx.fbcdn.net" - ) -} +private val adWhitelistHost: Set = + setOf( + "scontent-sea1-1.xx.fbcdn.net" + ) -private var adblock: Set? = null +private val adblock: LazyContext> = lazyContext { + it.assets.open("adblock.txt").bufferedReader().use { it.readLines().toSet() } +} -fun shouldFrostInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { +fun WebView.shouldFrostInterceptRequest(request: WebResourceRequest): WebResourceResponse? { val httpUrl = HttpUrl.parse(request.url?.toString() ?: return null) ?: return null val host = httpUrl.host() val url = httpUrl.toString() if (blacklistHost.contains(host)) return blankResource if (whitelistHost.contains(host)) return null - if (!adWhitelistHost.contains(host)) { - if (adblock == null) adblock = view.context.assets.open("adblock.txt").bufferedReader().use { it.readLines().toSet() } - if (adblock?.any { url.contains(it) } ?: false) return blankResource - } + if (!adWhitelistHost.contains(host) && adblock(context).any { url.contains(it) }) return blankResource if (!shouldLoadImages && !Prefs.loadMediaOnMeteredNetwork && request.isMedia) return blankResource L.v("Intercept Request", "$host $url") return null diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt new file mode 100644 index 00000000..0a1878b3 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -0,0 +1,48 @@ +package com.pitchedapps.frost.web + +import android.content.Context +import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.isFacebookUrl +import com.pitchedapps.frost.utils.launchWebOverlay + +/** + * Created by Allan Wang on 2017-08-15. + * + * Due to the nature of facebook href's, many links + * cannot be resolved on a new window and must instead + * by loaded in the current page + * This helper method will collect all known cases and launch the overlay accordingly + * Returns {@code true} (default) if overlay is launcher, {@code false} otherwise + */ +fun Context.requestWebOverlay(url: String): Boolean { + if (url == "#") return false + /* + * Non facebook urls can be loaded + */ + if (!url.formattedFbUrl.isFacebookUrl) { + launchWebOverlay(url) + L.d("Request web overlay is not a facebook url", url) + return true + } + /* + * Check blacklist + */ + if (overlayBlacklist.any { url.contains(it) }) return false + /* + * Facebook messages have the following cases for the tid query + * mid* or id* for newer threads, which can be launched in new windows + * or a hash for old threads, which must be loaded on old threads + */ + if (url.contains("/messages/read/?tid=")) { + if (!url.contains("?tid=id") && !url.contains("?tid=mid")) return false + } + L.v("Request web overlay passed", url) + launchWebOverlay(url) + return true +} + +/** + * The following components should never be launched in a new overlay + */ +val overlayBlacklist = setOf("messages/?pageNum", "photoset_token") \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt index 5f679c65..b1466ee8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -11,7 +11,6 @@ import com.pitchedapps.frost.activities.LoginActivity import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.activities.SelectorActivity import com.pitchedapps.frost.activities.WebOverlayActivity -import com.pitchedapps.frost.facebook.FACEBOOK_COM import com.pitchedapps.frost.facebook.FB_URL_BASE import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem @@ -34,7 +33,7 @@ import org.jetbrains.anko.withAlpha open class BaseWebViewClient : WebViewClient() { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? - = shouldFrostInterceptRequest(view, request) + = view.shouldFrostInterceptRequest(request) } @@ -51,7 +50,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient if (url == null) return L.i("FWV Loading", url) refreshObservable.onNext(true) - if (!url.contains(FACEBOOK_COM)) return + if (!url.isFacebookUrl) return if (url.contains("logout.php")) FbCookie.logout(Prefs.userId, { launchLogin(view.context) }) else if (url.contains("login.php")) FbCookie.reset({ launchLogin(view.context) }) } @@ -71,18 +70,19 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient override fun onPageCommitVisible(view: WebView, url: String?) { super.onPageCommitVisible(view, url) injectBackgroundColor() - view.jsInject( - CssAssets.ROUND_ICONS.maybe(Prefs.showRoundedIcons), - CssHider.HEADER, - CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!Prefs.showSuggestedFriends && IS_FROST_PRO), - Prefs.themeInjector, - CssHider.NON_RECENT.maybe(webCore.url?.contains("?sk=h_chr") ?: false)) + if (url.isFacebookUrl) + view.jsInject( + CssAssets.ROUND_ICONS.maybe(Prefs.showRoundedIcons), + CssHider.HEADER, + CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!Prefs.showSuggestedFriends && IS_FROST_PRO), + Prefs.themeInjector, + CssHider.NON_RECENT.maybe(webCore.url?.contains("?sk=h_chr") ?: false)) } override fun onPageFinished(view: WebView, url: String?) { url ?: return L.i("Page finished", url) - if (!url.contains(FACEBOOK_COM)) { + if (!url.isFacebookUrl) { refreshObservable.onNext(false) return } @@ -124,9 +124,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient */ private fun launchRequest(request: WebResourceRequest): Boolean { L.d("Launching Url", request.url?.toString() ?: "null") - if (webCore.context is WebOverlayActivity) return false - webCore.context.launchWebOverlay(request.url.toString()) - return true + return webCore.context !is WebOverlayActivity && webCore.context.requestWebOverlay(request.url.toString()) } private fun launchImage(url: String, text: String? = null): Boolean { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt index aea25337..dd8e2390 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -8,13 +8,13 @@ import android.webkit.* import ca.allanwang.kau.utils.fadeIn import ca.allanwang.kau.utils.isVisible import com.pitchedapps.frost.dbflow.CookieModel -import com.pitchedapps.frost.facebook.FACEBOOK_COM import com.pitchedapps.frost.facebook.FB_URL_BASE import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.injectors.CssHider import com.pitchedapps.frost.injectors.jsInject import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.isFacebookUrl import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread @@ -55,17 +55,18 @@ class LoginWebView @JvmOverloads constructor( override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) - val containsFacebook = url?.contains(FACEBOOK_COM) ?: false checkForLogin(url) { id, cookie -> loginCallback(CookieModel(id, "", cookie)) } - view.jsInject(CssHider.HEADER.maybe(containsFacebook), - CssHider.CORE.maybe(containsFacebook), - Prefs.themeInjector.maybe(containsFacebook), - callback = { if (!view.isVisible) view.fadeIn(offset = WEB_LOAD_DELAY) }) + if (url.isFacebookUrl) + view.jsInject(CssHider.HEADER, + CssHider.CORE, + Prefs.themeInjector, + callback = { if (!view.isVisible) view.fadeIn(offset = WEB_LOAD_DELAY) }) + else if (!view.isVisible) view.fadeIn() } fun checkForLogin(url: String?, onFound: (id: Long, cookie: String) -> Unit) { doAsync { - if (url == null || !url.contains(FACEBOOK_COM)) return@doAsync + if (!url.isFacebookUrl) return@doAsync val cookie = CookieManager.getInstance().getCookie(url) ?: return@doAsync L.d("Checking cookie for login", cookie) val id = userMatcher.find(cookie)?.groups?.get(1)?.value?.toLong() ?: return@doAsync -- cgit v1.2.3