From fd5f2a82eb968b5d50f586925ebb705249062446 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 10 Jan 2018 22:13:28 -0500 Subject: Misc (#614) * Add locale log * Add flyweight design for authenticator * Add option to have instant messages only * Update interceptor * Add hd image model loader (#613) * Launch image view for view full image * Update changelog * Greatly improve ImageActivity loading * Update hashes * Add back keyword filter * Clean up --- app/build.gradle | 1 + .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 7 +- .../frost/activities/BaseMainActivity.kt | 6 +- .../pitchedapps/frost/activities/ImageActivity.kt | 153 ++++++++++----------- .../pitchedapps/frost/activities/LoginActivity.kt | 6 +- .../com/pitchedapps/frost/facebook/FbRegex.kt | 1 + .../frost/facebook/requests/FbRequest.kt | 31 +++-- .../pitchedapps/frost/facebook/requests/Images.kt | 67 ++++++++- .../frost/facebook/requests/Messages.kt | 32 +++++ .../com/pitchedapps/frost/glide/GlideUtils.kt | 13 ++ .../com/pitchedapps/frost/iitems/MenuIItem.kt | 6 +- .../pitchedapps/frost/iitems/NotificationIItem.kt | 5 +- .../com/pitchedapps/frost/parsers/FrostParser.kt | 6 + .../com/pitchedapps/frost/parsers/MessageParser.kt | 2 + .../com/pitchedapps/frost/parsers/NotifParser.kt | 2 + .../com/pitchedapps/frost/parsers/SearchParser.kt | 2 + .../kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt | 86 ++++++++++++ .../frost/services/FrostNotifications.kt | 10 +- .../frost/services/NotificationService.kt | 3 +- .../kotlin/com/pitchedapps/frost/settings/Debug.kt | 21 +++ .../pitchedapps/frost/settings/Notifications.kt | 51 +++++-- .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 2 + .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 12 +- .../com/pitchedapps/frost/views/AccountItem.kt | 5 +- .../frost/web/FrostRequestInterceptor.kt | 30 +--- .../frost/web/FrostUrlOverlayValidator.kt | 11 +- .../pitchedapps/frost/web/FrostWebViewClients.kt | 4 - .../values-de-rDE/strings_pref_notifications.xml | 4 +- .../values-es-rES/strings_pref_notifications.xml | 4 +- .../values-fr-rFR/strings_pref_notifications.xml | 4 +- .../values-gl-rES/strings_pref_notifications.xml | 4 +- .../values-it-rIT/strings_pref_notifications.xml | 4 +- .../values-ko-rKR/strings_pref_notifications.xml | 4 +- .../values-pl-rPL/strings_pref_notifications.xml | 4 +- .../values-pt-rBR/strings_pref_notifications.xml | 4 +- .../values-vi-rVN/strings_pref_notifications.xml | 4 +- .../values-zh-rCN/strings_pref_notifications.xml | 4 +- app/src/main/res/values/strings_pref_debug.xml | 3 + .../main/res/values/strings_pref_notifications.xml | 8 +- app/src/main/res/xml/frost_changelog.xml | 4 +- .../com/pitchedapps/frost/facebook/FbRegexTest.kt | 8 ++ .../com/pitchedapps/frost/facebook/FbUrlTest.kt | 27 ++++ .../frost/rx/ResettableFlyweightTest.kt | 61 ++++++++ 43 files changed, 530 insertions(+), 196 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Messages.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt create mode 100644 app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt (limited to 'app') diff --git a/app/build.gradle b/app/build.gradle index 54284b09..9bd6c298 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,6 +177,7 @@ dependencies { implementation"com.mikepenz:fastadapter-extensions:${FAST_ADAPTER_EXTENSIONS}@aar" implementation "com.github.bumptech.glide:okhttp3-integration:${GLIDE}" + kapt "com.github.bumptech.glide:compiler:${GLIDE}" implementation "com.fasterxml.jackson.core:jackson-databind:2.9.3" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index 8897e804..67785a60 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -7,7 +7,6 @@ import android.net.Uri import android.os.Bundle import android.widget.ImageView import ca.allanwang.kau.logging.KL -import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ApplicationVersionSignature import com.crashlytics.android.Crashlytics @@ -18,6 +17,7 @@ import com.pitchedapps.frost.dbflow.CookiesDb import com.pitchedapps.frost.dbflow.FbTabsDb import com.pitchedapps.frost.dbflow.NotificationDb import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.services.scheduleNotifications import com.pitchedapps.frost.services.setupNotificationChannels import com.pitchedapps.frost.utils.FrostPglAdBlock @@ -86,8 +86,9 @@ class FrostApp : Application() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() { override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String) { val c = imageView.context - val old = Glide.with(c).load(uri).apply(RequestOptions().placeholder(placeholder)) - Glide.with(c).load(uri).apply(RequestOptions() + val request = GlideApp.with(c) + val old = request.load(uri).apply(RequestOptions().placeholder(placeholder)) + request.load(uri).apply(RequestOptions() .signature(ApplicationVersionSignature.obtain(c))) .thumbnail(old).into(imageView) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index ffcbadab..3a01e05b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -119,10 +119,6 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, viewPager.offscreenPageLimit = TAB_COUNT setupDrawer(savedInstanceState) -// fab.setOnClickListener { view -> -// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) -// .setAction("Action", null).show() -// } setFrostColors { toolbar(toolbar) themeWindow = false @@ -198,7 +194,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, } } -3L -> launchNewTask(clearStack = false) - -4L -> launchNewTask( cookies(), false) + -4L -> launchNewTask(cookies(), false) else -> { FbCookie.switchUser(profile.identifier, this@BaseMainActivity::refreshAll) tabsForEachView { _, view -> view.badgeText = null } 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 99fa6eee..cdde8311 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -1,12 +1,8 @@ package com.pitchedapps.frost.activities -import android.content.Context import android.content.Intent import android.content.res.ColorStateList -import android.graphics.Bitmap import android.graphics.Color -import android.graphics.drawable.Drawable -import android.net.Uri import android.os.Bundle import android.os.Environment import android.support.design.widget.FloatingActionButton @@ -20,24 +16,22 @@ 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.* -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager -import com.bumptech.glide.request.target.BaseTarget -import com.bumptech.glide.request.target.SizeReadyCallback -import com.bumptech.glide.request.target.Target -import com.bumptech.glide.request.transition.Transition import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.iconics.typeface.IIcon import com.pitchedapps.frost.R +import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER +import com.pitchedapps.frost.facebook.get import com.pitchedapps.frost.facebook.requests.call import com.pitchedapps.frost.utils.* import com.sothree.slidinguppanel.SlidingUpPanelLayout import okhttp3.Request import org.jetbrains.anko.activityUiThreadWithContext import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread import java.io.File +import java.io.FileFilter import java.io.IOException import java.text.SimpleDateFormat import java.util.* @@ -70,12 +64,13 @@ class ImageActivity : KauBaseActivity() { internal var savedFile: File? = null /** * Indicator for fab's click result + * Can be called from any thread */ internal var fabAction: FabStates = FabStates.NOTHING set(value) { if (field == value) return field = value - value.update(fab) + runOnUiThread { value.update(fab) } } companion object { @@ -87,25 +82,27 @@ class ImageActivity : KauBaseActivity() { private const val TIME_FORMAT = "yyyyMMdd_HHmmss" private const val IMG_TAG = "Frost" private const val IMG_EXTENSION = ".png" + private const val PURGE_TIME: Long = 10 * 60 * 1000 // 10 min block private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L) } - val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') } + val IMAGE_URL: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') } - val text: String? by lazy { intent.getStringExtra(ARG_TEXT) } + val TEXT: String? by lazy { intent.getStringExtra(ARG_TEXT) } - private val glide: RequestManager by lazy { Glide.with(this) } + // a unique image identifier based on the id (if it exists), and its hash + val IMAGE_HASH: String by lazy { "${Math.abs(FB_IMAGE_ID_MATCHER.find(IMAGE_URL)[1]?.hashCode() ?: 0)}_${Math.abs(IMAGE_URL.hashCode())}" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) intent?.extras ?: return finish() L.i { "Displaying image" } - val layout = if (!text.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless + val layout = if (!TEXT.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless setContentView(layout) container.setBackgroundColor(Prefs.bgColor.withMinAlpha(222)) caption?.setTextColor(Prefs.textColor) caption?.setBackgroundColor(Prefs.bgColor.colorToForeground(0.2f).withAlpha(255)) - caption?.text = text + caption?.text = TEXT progress.tint(Prefs.accentColor) panel?.addPanelSlideListener(object : SlidingUpPanelLayout.SimplePanelSlideListener() { override fun onPanelSlide(panel: View, slideOffset: Float) { @@ -119,90 +116,76 @@ class ImageActivity : KauBaseActivity() { override fun onImageLoadError(e: Exception?) { errorRef = e e.logFrostAnswers("Image load error") - imageCallback(null, false) + fabAction = FabStates.ERROR } }) - glide.asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback)) setFrostColors { themeWindow = false } - } - - /** - * Callback to add image to view - * [resource] is guaranteed to be nonnull when [success] is true - * and null when it is false - */ - private fun imageCallback(resource: Bitmap?, success: Boolean) { - if (progress.isVisible) progress.fadeOut() - if (success) { - saveTempImage(resource!!, { - if (it == null) { - imageCallback(null, false) - } else { - photo.setImage(ImageSource.uri(it)) + doAsync({ + L.e(it) { "Failed to load image $IMAGE_HASH" } + errorRef = it + fabAction = FabStates.ERROR + }) { + loadImage { file -> + if (file == null) { + fabAction = FabStates.ERROR + return@loadImage + } + tempFile = file + L.d { "Temp image path ${file.absolutePath}" } + uiThread { + photo.setImage(ImageSource.uri(frostUriFromFile(file))) fabAction = FabStates.DOWNLOAD - photo.animate().alpha(1f).scaleXY(1f).withEndAction(fab::show).start() + photo.animate().alpha(1f).scaleXY(1f).start() } - }) - } else { - fabAction = FabStates.ERROR - fab.show() + } } } /** - * Bitmap load handler + * Returns a file pointing to the image, or null if something goes wrong */ - class PhotoTarget(val callback: (resource: Bitmap?, success: Boolean) -> Unit) : BaseTarget() { - - override fun removeCallback(cb: SizeReadyCallback?) {} - - override fun onResourceReady(resource: Bitmap, transition: Transition?) = - callback(resource, true) + private inline fun loadImage(callback: (file: File?) -> Unit) { + val local = File(tempDir, IMAGE_HASH) + if (local.exists() && local.length() > 1) { + local.setLastModified(System.currentTimeMillis()) + L.d { "Loading from local cache ${local.absolutePath}" } + return callback(local) + } + val response = Request.Builder() + .url(IMAGE_URL) + .get() + .call() + .execute() - override fun onLoadFailed(errorDrawable: Drawable?) = - callback(null, false) + if (!response.isSuccessful) { + L.e { "Unsuccessful response for image" } + errorRef = Throwable("Unsuccessful response for image") + return callback(null) + } - override fun getSize(cb: SizeReadyCallback) = - cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + if (!local.createFreshFile()) { + L.e { "Could not create temp file" } + return callback(null) + } - } + var valid = false - private fun saveTempImage(resource: Bitmap, callback: (uri: Uri?) -> Unit) { - var photoFile: File? = null - try { - photoFile = createPrivateMediaFile() - } catch (e: IOException) { - errorRef = e - logImage(e) - } finally { - if (photoFile == null) { - callback(null) - } else { - tempFile = photoFile - L.d { "Temp image path ${tempFile?.absolutePath}" } - // File created; proceed with request - val photoURI = frostUriFromFile(photoFile) - photoFile.outputStream().use { resource.compress(Bitmap.CompressFormat.PNG, 100, it) } - callback(photoURI) + response.body()?.byteStream()?.use { input -> + local.outputStream().use { output -> + input.copyTo(output) + valid = true } } - } - private fun logImage(e: Exception?) { - if (!Prefs.analytics) return - val error = e ?: IOException("$imageUrl failed to load") - L.e(error) { "$imageUrl failed to load" } - } + if (!valid) { + L.e { "Failed to copy file" } + local.delete() + return callback(null) + } - @Throws(IOException::class) - private fun createPrivateMediaFile(): File { - val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date()) - val imageFileName = "${IMG_TAG}_${timeStamp}_" - if (!tempDir.exists()) - tempDir.mkdirs() - return File.createTempFile(imageFileName, IMG_EXTENSION, tempDir) + callback(local) } @Throws(IOException::class) @@ -218,7 +201,7 @@ class ImageActivity : KauBaseActivity() { @Throws(IOException::class) private fun downloadImageTo(file: File) { val body = Request.Builder() - .url(imageUrl) + .url(IMAGE_URL) .get() .call() .execute() @@ -270,8 +253,10 @@ class ImageActivity : KauBaseActivity() { override fun onDestroy() { tempFile = null - tempDir.deleteRecursively() - L.d { "Closing $localClassName" } + val purge = System.currentTimeMillis() - PURGE_TIME + tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge }).forEach { + it.delete() + } super.onDestroy() } } @@ -287,7 +272,7 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC if (activity.errorRef != null) L.e(activity.errorRef) { "ImageActivity error report" } activity.sendFrostEmail(R.string.debug_image_link_subject) { - addItem("Url", activity.imageUrl) + addItem("Url", activity.IMAGE_URL) addItem("Message", activity.errorRef?.message ?: "Null") } } 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 2434c8c2..3b320cce 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -10,7 +10,6 @@ import android.widget.ImageView import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.fadeIn import ca.allanwang.kau.utils.fadeOut -import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException @@ -24,6 +23,7 @@ import com.pitchedapps.frost.dbflow.loadFbCookiesAsync import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.glide.FrostGlide +import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.glide.transform import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.web.LoginWebView @@ -62,7 +62,7 @@ class LoginActivity : BaseActivity() { setContentView(R.layout.activity_login) setSupportActionBar(toolbar) setTitle(R.string.kau_login) - setFrostColors{ + setFrostColors { toolbar(toolbar) } web.loadLogin({ refresh = it != 100 }) { cookie -> @@ -73,7 +73,7 @@ class LoginActivity : BaseActivity() { loadInfo(cookie) }) } - profileLoader = Glide.with(profile) + profileLoader = GlideApp.with(profile) } private fun loadInfo(cookie: CookieModel) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt index acc23cad..4ba3c80d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt @@ -25,6 +25,7 @@ val FB_NOTIF_ID_MATCHER: Regex by lazy { Regex("notif_([0-9]+)") } val FB_MESSAGE_NOTIF_ID_MATCHER: Regex by lazy { Regex("[thread|user]_fbid_([0-9]+)") } val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|']?(.*?)[\"|']?\\)") } val FB_JSON_URL_MATCHER: Regex by lazy { Regex("\"(http.*?)\"") } +val FB_IMAGE_ID_MATCHER: Regex by lazy { Regex("fbcdn.*?/[0-9]+_([0-9]+)_") } operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt index ae8652e6..3ca37bb4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt @@ -2,6 +2,7 @@ package com.pitchedapps.frost.facebook.requests import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.rx.RxFlyweight import com.pitchedapps.frost.utils.L import io.reactivex.Single import io.reactivex.schedulers.Schedulers @@ -12,7 +13,17 @@ import org.apache.commons.text.StringEscapeUtils /** * Created by Allan Wang on 21/12/17. */ -private val authMap: MutableMap = mutableMapOf() +private class RxAuth : RxFlyweight() { + + override fun call(input: String) = input.getAuth() + + override fun validate(input: String, cond: Long) = + System.currentTimeMillis() - cond < 3600000 // valid for an hour + + override fun cache(input: String) = System.currentTimeMillis() +} + +private val auth = RxAuth() /** * Synchronously fetch [RequestAuth] from cookie @@ -21,18 +32,13 @@ private val authMap: MutableMap = mutableMapOf() */ fun String?.fbRequest(fail: () -> Unit = {}, action: RequestAuth.() -> Unit) { if (this == null) return fail() - val savedAuth = authMap[this] - if (savedAuth != null) { - savedAuth.action() - } else { - val auth = getAuth() - if (!auth.isValid) { - L.e { "Attempted fbrequest with invalid auth" } - return fail() + auth(this).subscribe { a: RequestAuth?, _ -> + if (a?.isValid == true) + a.action() + else { + L.e { "Failed auth for ${hashCode()}" } + fail() } - authMap.put(this, auth) - L._i { "Found auth $auth" } - auth.action() } } @@ -94,6 +100,7 @@ private fun String.requestBuilder() = Request.Builder() fun Request.Builder.call() = client.newCall(build())!! fun String.getAuth(): RequestAuth { + L.v { "Getting auth for ${hashCode()}" } var auth = RequestAuth(cookie = this) val id = FB_USER_MATCHER.find(this)[1]?.toLong() ?: return auth auth = auth.copy(userId = id) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt index 61a94ac5..fa78bbfa 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt @@ -1,9 +1,19 @@ package com.pitchedapps.frost.facebook.requests import com.bumptech.glide.Priority +import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import com.bumptech.glide.signature.ObjectKey +import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER import com.pitchedapps.frost.facebook.FB_URL_BASE +import com.pitchedapps.frost.facebook.get import okhttp3.Call import okhttp3.Request import java.io.IOException @@ -17,8 +27,54 @@ fun RequestAuth.getFullSizedImage(fbid: Long) = frostRequest(::getJsonUrl) { get() } -class ImageFbidFetcher(private val fbid: Long, - private val cookie: String) : DataFetcher { +/** + * Request loader for a potentially hd version of a url + * In this case, each url may potentially return an id, + * which may potentially be used to fetch a higher res image url + * The following aims to allow such loading while adhering to Glide's lifecycle + */ +data class HdImageMaybe(val url: String, val cookie: String) { + + val id: Long by lazy { FB_IMAGE_ID_MATCHER.find(url)[1]?.toLongOrNull() ?: -1 } + + val isValid: Boolean by lazy { + id != -1L && cookie.isNotBlank() + } + +} + +/* + * The following was a test to see if hd image loading would work + * + * It's working and tested, though the improvements aren't really worth the extra data use + * and reload + */ + +class HdImageLoadingFactory : ModelLoaderFactory { + + override fun build(multiFactory: MultiModelLoaderFactory) = HdImageLoading() + + override fun teardown() = Unit +} + +fun RequestBuilder.loadWithPotentialHd(model: HdImageMaybe) = + thumbnail(clone().load(model.url)) + .load(model) + .apply(RequestOptions().override(Target.SIZE_ORIGINAL)) + +class HdImageLoading : ModelLoader { + + override fun buildLoadData(model: HdImageMaybe, + width: Int, + height: Int, + options: Options?): ModelLoader.LoadData? = + if (!model.isValid) null + else ModelLoader.LoadData(ObjectKey(model), HdImageFetcher(model)) + + override fun handles(model: HdImageMaybe) = model.isValid +} + +class HdImageFetcher(private val model: HdImageMaybe) : DataFetcher { @Volatile private var cancelled: Boolean = false private var urlCall: Call? = null @@ -33,10 +89,12 @@ class ImageFbidFetcher(private val fbid: Long, override fun getDataSource(): DataSource = DataSource.REMOTE override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - cookie.fbRequest(fail = { callback.fail("Invalid auth") }) { + if (!model.isValid) return callback.fail("Model is invalid") + model.cookie.fbRequest(fail = { callback.fail("Invalid auth") }) { if (cancelled) return@fbRequest callback.fail("Cancelled") - val url = getFullSizedImage(fbid).invoke() ?: return@fbRequest callback.fail("Null url") + val url = getFullSizedImage(model.id).invoke() ?: return@fbRequest callback.fail("Null url") if (cancelled) return@fbRequest callback.fail("Cancelled") + if (!url.contains("png") && !url.contains("jpg")) return@fbRequest callback.fail("Invalid format") urlCall = Request.Builder().url(url).get().call() inputStream = try { @@ -44,7 +102,6 @@ class ImageFbidFetcher(private val fbid: Long, } catch (e: IOException) { null } - callback.onDataReady(inputStream) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Messages.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Messages.kt new file mode 100644 index 00000000..0e37a61e --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Messages.kt @@ -0,0 +1,32 @@ +package com.pitchedapps.frost.facebook.requests + +import com.pitchedapps.frost.facebook.FB_URL_BASE +import okhttp3.Call + +/** + * Created by Allan Wang on 07/01/18. + */ +fun RequestAuth.sendMessage(group: String, content: String): FrostRequest { + + // todo test more; only tested against tids=cid... + val body = listOf( + "tids" to group, + "body" to content, + "fb_dtsg" to fb_dtsg, + "__user" to userId + ).withEmptyData("m_sess", "__dyn", "__req", "__ajax__") + + return frostRequest(::validateMessage) { + url("${FB_URL_BASE}messages/send") + post(body.toForm()) + } +} + +/** + * Messages are a bit weird with their responses + */ +private fun validateMessage(call: Call): Boolean { + val body = call.execute().body() ?: return false + // todo + return true +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt index 6d2b3cda..651e57d8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt @@ -1,9 +1,14 @@ package com.pitchedapps.frost.glide +import android.content.Context +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.load.MultiTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.CircleCrop +import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.request.RequestOptions /** @@ -25,3 +30,11 @@ fun RequestBuilder.transform(vararg transformation: BitmapTransformation) 1 -> apply(RequestOptions.bitmapTransform(transformation[0])) else -> apply(RequestOptions.bitmapTransform(MultiTransformation(*transformation))) } + +@GlideModule +class FrostGlideModule : AppGlideModule() { + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { +// registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory()) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt index 690d1be8..f5b1df17 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt @@ -9,14 +9,13 @@ import ca.allanwang.kau.ui.createSimpleRippleDrawable import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.gone import ca.allanwang.kau.utils.visible -import com.bumptech.glide.Glide import com.mikepenz.fastadapter.FastAdapter import com.pitchedapps.frost.R import com.pitchedapps.frost.facebook.requests.MenuFooterItem import com.pitchedapps.frost.facebook.requests.MenuHeader import com.pitchedapps.frost.facebook.requests.MenuItem import com.pitchedapps.frost.glide.FrostGlide -import com.pitchedapps.frost.glide.transform +import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.utils.Prefs /** @@ -42,7 +41,8 @@ class MenuContentIItem(val data: MenuItem) badge.setTextColor(Prefs.textColor) val iconUrl = item.data.pic if (iconUrl != null) - Glide.with(itemView).load(iconUrl) + GlideApp.with(itemView) + .load(iconUrl) .transform(FrostGlide.roundCorner) .into(icon.visible()) else diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt index 2e16f386..8bc2c1fe 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt @@ -10,12 +10,11 @@ import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.gone import ca.allanwang.kau.utils.visible import ca.allanwang.kau.utils.withAlpha -import com.bumptech.glide.Glide import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.adapters.ItemAdapter import com.pitchedapps.frost.R import com.pitchedapps.frost.glide.FrostGlide -import com.pitchedapps.frost.glide.transform +import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.parsers.FrostNotif import com.pitchedapps.frost.services.FrostRunnable import com.pitchedapps.frost.utils.Prefs @@ -52,7 +51,7 @@ class NotificationIItem(val notification: FrostNotif, val cookie: String) : KauI val thumbnail: ImageView by bindView(R.id.item_thumbnail) private val glide - get() = Glide.with(itemView) + get() = GlideApp.with(itemView) override fun bindView(item: NotificationIItem, payloads: MutableList) { val notif = item.notification diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt index f0938eca..d5730e16 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt @@ -24,6 +24,12 @@ import org.jsoup.select.Elements */ interface FrostParser { + /** + * Name associated to parser + * Purely for display + */ + var nameRes: Int + /** * Url to request from */ diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt index 24ddd601..697cbbe8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt @@ -69,6 +69,8 @@ data class FrostThread(val id: Long, private class MessageParserImpl : FrostParserBase(true) { + override var nameRes = FbItem.MESSAGES.titleId + override val url = FbItem.MESSAGES.url override fun textToDoc(text: String): Document? { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt index 23852852..812f12e3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt @@ -60,6 +60,8 @@ data class FrostNotif(val id: Long, private class NotifParserImpl : FrostParserBase(false) { + override var nameRes = FbItem.NOTIFICATIONS.titleId + override val url = FbItem.NOTIFICATIONS.url override fun parseImpl(doc: Document): FrostNotifs? { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt index 7fbc0f08..5300bf11 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt @@ -55,6 +55,8 @@ data class FrostSearch(val href: String, val title: String, val description: Str private class SearchParserImpl : FrostParserBase(false) { + override var nameRes = FbItem._SEARCH.titleId + override val url = "${FbItem._SEARCH.url}?q=a" override fun parseImpl(doc: Document): FrostSearches? { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt new file mode 100644 index 00000000..159a9bf2 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt @@ -0,0 +1,86 @@ +package com.pitchedapps.frost.rx + +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit + +/** + * Created by Allan Wang on 07/01/18. + * + * Reactive flyweight to help deal with prolonged executions + * Each call will output a [Single], which may be new if none exist or the old one is invalidated, + * or reused if an old one is still valid + * + * Types: + * T input argument for caller + * C condition condition to check against for validity + * R response response within reactive output + */ +abstract class RxFlyweight { + + /** + * Given an input emit the desired response + * This will be executed in a separate thread + */ + protected abstract fun call(input: T): R + + /** + * Given an input and condition, check if + * we may used cache data or if we need to make a new request + * Return [true] to use cache, [false] otherwise + */ + protected abstract fun validate(input: T, cond: C): Boolean + + /** + * Given an input, create a new condition to be used + * for future requests + */ + protected abstract fun cache(input: T): C + + private val conditionals = mutableMapOf() + private val sources = mutableMapOf>() + + private val lock = Any() + + /** + * Entry point to give an input a receive a [Single] + * Note that the observer is not bound to any particular thread, + * as it is dependent on [createNewSource] + */ + operator fun invoke(input: T): Single { + synchronized(lock) { + val source = sources[input] + + // update condition and retrieve old one + val condition = conditionals.put(input, cache(input)) + + // check to reuse observable + if (source != null && condition != null && validate(input, condition)) + return source + + val newSource = createNewSource(input).cache().doOnError { sources.remove(input) } + + sources.put(input, newSource) + return newSource + } + } + + /** + * Open source creator + * Result will then be created with [Single.cache] + * If you don't have a need for cache, + * you likely won't have a need for flyweights + */ + open protected fun createNewSource(input: T): Single = + Single.fromCallable { call(input) } + .timeout(20, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + + fun reset() { + synchronized(lock) { + sources.clear() + conditionals.clear() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 50392dea..e8c5e7c1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -19,7 +19,6 @@ import android.support.v4.app.NotificationManagerCompat import ca.allanwang.kau.utils.color import ca.allanwang.kau.utils.dpToPx import ca.allanwang.kau.utils.string -import com.bumptech.glide.Glide import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.pitchedapps.frost.BuildConfig @@ -31,7 +30,7 @@ import com.pitchedapps.frost.dbflow.lastNotificationTime import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.glide.FrostGlide -import com.pitchedapps.frost.glide.transform +import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.parsers.FrostParser import com.pitchedapps.frost.parsers.MessageParser import com.pitchedapps.frost.parsers.NotifParser @@ -146,7 +145,10 @@ enum class NotificationType( fun fetch(context: Context, data: CookieModel) { val response = parser.parse(data.cookie) ?: return L.v { "$name notification data not found" } - val notifs = response.data.getUnreadNotifications(data) + val notifs = response.data.getUnreadNotifications(data).filter { + val text = it.text + Prefs.notificationKeywords.any { text.contains(it, true) } + } if (notifs.isEmpty()) return var notifCount = 0 val userId = data.id @@ -201,7 +203,7 @@ enum class NotificationType( if (profileUrl != null) { context.runOnUiThread { //todo verify if context is valid? - Glide.with(context) + GlideApp.with(context) .asBitmap() .load(profileUrl) .transform(FrostGlide.circleCrop) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 3d19606b..fc946772 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -61,7 +61,8 @@ class NotificationService : JobService() { val cookies = loadFbCookiesSync() cookies.forEach { val current = it.id == currentId - if (current || Prefs.notificationAllAccounts) + if (Prefs.notificationsGeneral + && (current || Prefs.notificationAllAccounts)) NotificationType.GENERAL.fetch(context, it) if (Prefs.notificationsInstantMessages && (current || Prefs.notificationsImAllAccounts)) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt index 3e795e80..3b37d1c3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt @@ -3,6 +3,7 @@ package com.pitchedapps.frost.settings import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder import ca.allanwang.kau.utils.materialDialog import ca.allanwang.kau.utils.startActivityForResult +import ca.allanwang.kau.utils.string import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.DebugActivity import com.pitchedapps.frost.activities.SettingsActivity @@ -10,6 +11,9 @@ import com.pitchedapps.frost.activities.SettingsActivity.Companion.ACTIVITY_REQU import com.pitchedapps.frost.debugger.OfflineWebsite import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.parsers.MessageParser +import com.pitchedapps.frost.parsers.NotifParser +import com.pitchedapps.frost.parsers.SearchParser import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostUriFromFile import com.pitchedapps.frost.utils.sendFrostEmail @@ -34,6 +38,23 @@ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = { descRes = R.string.debug_web_desc onClick = { this@getDebugPrefs.startActivityForResult(ACTIVITY_REQUEST_DEBUG) } } + + plainText(R.string.debug_parsers) { + descRes = R.string.debug_parsers_desc + onClick = { + + val parsers = arrayOf(NotifParser, MessageParser, SearchParser) + + materialDialog { + items(parsers.map { string(it.nameRes) }) + itemsCallback { dialog, _, position, _ -> + dialog.dismiss() + // todo add debugging + } + } + + } + } } private const val ZIP_NAME = "debug" 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 1108f5d4..2ee086c0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -23,7 +23,7 @@ import com.pitchedapps.frost.views.Keywords fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { text(R.string.notification_frequency, Prefs::notificationFreq, { Prefs.notificationFreq = it }) { - val options = longArrayOf(-1, 15, 30, 60, 120, 180, 300, 1440, 2880) + val options = longArrayOf(15, 30, 60, 120, 180, 300, 1440, 2880) val texts = options.map { if (it <= 0) string(R.string.no_notifications) else minuteToText(it) } onClick = { materialDialogThemed { @@ -36,6 +36,12 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { }) } } + enabler = { + val enabled = Prefs.notificationsGeneral || Prefs.notificationsInstantMessages + if (!enabled) + scheduleNotifications(-1) + enabled + } textGetter = { minuteToText(it) } } @@ -52,15 +58,34 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { } } - checkbox(R.string.notification_all_accounts, Prefs::notificationAllAccounts, { Prefs.notificationAllAccounts = it }) { - descRes = R.string.notification_all_accounts_desc + checkbox(R.string.notification_general, Prefs::notificationsGeneral, + { + Prefs.notificationsGeneral = it + reloadByTitle(R.string.notification_general_all_accounts) + if (!Prefs.notificationsInstantMessages) + reloadByTitle(R.string.notification_frequency) + }) { + descRes = R.string.notification_general_desc + } + + checkbox(R.string.notification_general_all_accounts, Prefs::notificationAllAccounts, + { Prefs.notificationAllAccounts = it }) { + descRes = R.string.notification_general_all_accounts_desc + enabler = Prefs::notificationsGeneral } - checkbox(R.string.notification_messages, Prefs::notificationsInstantMessages, { Prefs.notificationsInstantMessages = it; reloadByTitle(R.string.notification_messages_all_accounts) }) { + checkbox(R.string.notification_messages, Prefs::notificationsInstantMessages, + { + Prefs.notificationsInstantMessages = it + reloadByTitle(R.string.notification_messages_all_accounts) + if (!Prefs.notificationsGeneral) + reloadByTitle(R.string.notification_frequency) + }) { descRes = R.string.notification_messages_desc } - checkbox(R.string.notification_messages_all_accounts, Prefs::notificationsImAllAccounts, { Prefs.notificationsImAllAccounts = it }) { + checkbox(R.string.notification_messages_all_accounts, Prefs::notificationsImAllAccounts, + { Prefs.notificationsImAllAccounts = it }) { descRes = R.string.notification_messages_all_accounts_desc enabler = Prefs::notificationsInstantMessages } @@ -91,22 +116,28 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { } } - text(R.string.notification_ringtone, Prefs::notificationRingtone, { Prefs.notificationRingtone = it }) { + text(R.string.notification_ringtone, Prefs::notificationRingtone, + { Prefs.notificationRingtone = it }) { ringtone(SettingsActivity.REQUEST_NOTIFICATION_RINGTONE) } - text(R.string.message_ringtone, Prefs::messageRingtone, { Prefs.messageRingtone = it }) { + text(R.string.message_ringtone, Prefs::messageRingtone, + { Prefs.messageRingtone = it }) { ringtone(SettingsActivity.REQUEST_MESSAGE_RINGTONE) } - checkbox(R.string.notification_vibrate, Prefs::notificationVibrate, { Prefs.notificationVibrate = it }) + checkbox(R.string.notification_vibrate, Prefs::notificationVibrate, + { Prefs.notificationVibrate = it }) - checkbox(R.string.notification_lights, Prefs::notificationLights, { Prefs.notificationLights = it }) + checkbox(R.string.notification_lights, Prefs::notificationLights, + { Prefs.notificationLights = it }) plainText(R.string.notification_fetch_now) { descRes = R.string.notification_fetch_now_desc onClick = { - val text = if (fetchNotifications()) R.string.notification_fetch_success else R.string.notification_fetch_fail + val text = + if (fetchNotifications()) R.string.notification_fetch_success + else R.string.notification_fetch_fail frostSnackbar(text) } } 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 7bec8ce0..7422fb36 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -113,6 +113,8 @@ object Prefs : KPref() { var notificationKeywords: StringSet by kpref("notification_keywords", mutableSetOf()) + var notificationsGeneral: Boolean by kpref("notification_general", true) + var notificationAllAccounts: Boolean by kpref("notification_all_accounts", true) var notificationsInstantMessages: Boolean by kpref("notification_im", true) 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 1fb41dca..486fbae1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -250,8 +250,15 @@ inline val String?.isFacebookUrl /** * [true] if url is a video and can be accepted by VideoViewer */ -inline val String?.isVideoUrl - get() = this != null && (startsWith(VIDEO_REDIRECT) || startsWith("https://video-")) +inline val String.isVideoUrl + get() = startsWith(VIDEO_REDIRECT) || startsWith("https://video-") + +/** + * [true] if url is or redirects to an explicit facebook image + */ +inline val String.isImageUrl + get() = (contains("fbcdn.net") && (contains(".png") || contains(".jpg"))) + || contains("/photo/view_full_size") /** * [true] if url can be displayed in a different webview @@ -308,6 +315,7 @@ fun EmailBuilder.addFrostDetails() { addItem("Prev version", Prefs.prevVersionCode.toString()) val proTag = if (IS_FROST_PRO) "TY" else "FP" addItem("Random Frost ID", "${Prefs.frostId}-$proTag") + addItem("Locale", Locale.getDefault().displayName) } fun frostJsoup(url: String) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt index 64cf34a1..78847df4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt @@ -7,7 +7,6 @@ import android.view.View import android.widget.ImageView import ca.allanwang.kau.iitems.KauIItem import ca.allanwang.kau.utils.* -import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener @@ -17,7 +16,7 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.glide.FrostGlide -import com.pitchedapps.frost.glide.transform +import com.pitchedapps.frost.glide.GlideApp import com.pitchedapps.frost.utils.Prefs /** @@ -33,7 +32,7 @@ class AccountItem(val cookie: CookieModel?) : KauIItem { override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { text.fadeIn() 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 454e2a4b..0501e2e6 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt @@ -17,35 +17,13 @@ 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 = - setOf( - // "edge-chat.facebook.com" //todo make more specific? This is required for message responses - ) - -//these hosts will return null and skip logging -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 = - setOf( - "scontent-sea1-1.xx.fbcdn.net" - ) - fun WebView.shouldFrostInterceptRequest(request: WebResourceRequest): WebResourceResponse? { - request.url ?: return null - val httpUrl = HttpUrl.parse(request.url.toString()) ?: return null + val requestUrl = request.url?.toString() ?: return null + val httpUrl = HttpUrl.parse(requestUrl) ?: 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) && FrostPglAdBlock.isAdHost(host)) return blankResource + if (host.contains("facebook") || host.contains("fbcdn")) return null + if (FrostPglAdBlock.isAdHost(host)) 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 index 9a3dc331..6c09de7c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -3,7 +3,6 @@ package com.pitchedapps.frost.web import com.pitchedapps.frost.activities.WebOverlayActivity import com.pitchedapps.frost.activities.WebOverlayActivityBase import com.pitchedapps.frost.activities.WebOverlayBasicActivity - import com.pitchedapps.frost.contracts.VideoViewHolder import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT_BASIC @@ -36,6 +35,11 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean { context.runOnUiThread { context.showVideo(url) } return true } + if (url.isImageUrl) { + L.d { "Found fb image" } + context.launchImageActivity(url.formattedFbUrl, null) + return true + } if (!url.isIndependent) { L.d { "Forbid overlay switch" } return false @@ -43,13 +47,14 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean { if (!Prefs.overlayEnabled) return false if (context is WebOverlayActivityBase) { L.v { "Check web request from overlay" } + val shouldUseBasic = url.formattedFbUrl.shouldUseBasicAgent //already overlay; manage user agent - if (userAgentString != USER_AGENT_BASIC && url.formattedFbUrl.shouldUseBasicAgent) { + if (userAgentString != USER_AGENT_BASIC && shouldUseBasic) { L.i { "Switch to basic agent overlay" } context.launchWebOverlayBasic(url) return true } - if (context is WebOverlayBasicActivity && !url.formattedFbUrl.shouldUseBasicAgent) { + if (context is WebOverlayBasicActivity && !shouldUseBasic) { L.i { "Switch from basic agent" } context.launchWebOverlay(url) return true 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 e23ec0f8..d0f7d490 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -1,15 +1,11 @@ package com.pitchedapps.frost.web -import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient -import com.pitchedapps.frost.activities.LoginActivity -import com.pitchedapps.frost.activities.MainActivity -import com.pitchedapps.frost.activities.SelectorActivity import com.pitchedapps.frost.facebook.FB_URL_BASE import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.injectors.* diff --git a/app/src/main/res/values-de-rDE/strings_pref_notifications.xml b/app/src/main/res/values-de-rDE/strings_pref_notifications.xml index d2280858..c2ab299a 100644 --- a/app/src/main/res/values-de-rDE/strings_pref_notifications.xml +++ b/app/src/main/res/values-de-rDE/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Schlagwort hinzufügen Gebe das Schlagwort ein und drücke + Leeres Schlagwort - Benachrichtigungen von allen Accounts - Bekomme Benachrichtigungen von allen Accounts die eingeloggt sind. Deaktiviere dies, um nur noch Benachrichtigungen von dem aktuellen Account zu bekommen. + Benachrichtigungen von allen Accounts + Bekomme Benachrichtigungen von allen Accounts die eingeloggt sind. Deaktiviere dies, um nur noch Benachrichtigungen von dem aktuellen Account zu bekommen. Aktiviere Nachrichten Benachrichtigungen Bekomme sofortige Benachrichtigungen für deine Nachrichten für den aktuellen Account. Benachrichtigungen für Nachrichten von allen Accounts diff --git a/app/src/main/res/values-es-rES/strings_pref_notifications.xml b/app/src/main/res/values-es-rES/strings_pref_notifications.xml index 63192d10..69aa9b60 100644 --- a/app/src/main/res/values-es-rES/strings_pref_notifications.xml +++ b/app/src/main/res/values-es-rES/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Añadir palabra clave Escriba la palabra clave y pulse + Palabra clave vacía - Notificar de todas las cuentas - Obten notificaciones de todas las cuentas en las que estes logueado. Desactivar esto hará que se muestren solo las notificaciones de la cuenta en la que estés. + Notificar de todas las cuentas + Obten notificaciones de todas las cuentas en las que estes logueado. Desactivar esto hará que se muestren solo las notificaciones de la cuenta en la que estés. Activar notificaciones de mensajes Obten notificaciones instantáneas de mensajes para tu cuenta actual. Notificar mensajes de todas las cuentas diff --git a/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml b/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml index 89687082..fb519054 100644 --- a/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml +++ b/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Ajouter un mot-clé Tapez le mot-clé et appuyez sur + Mot-clé vide - Notifier de tous les comptes - Recevoir des notifications pour chaque compte connecté. Désactiver cette option fera une récupération uniquement pour le compte sélectionné. + Notifier de tous les comptes + Recevoir des notifications pour chaque compte connecté. Désactiver cette option fera une récupération uniquement pour le compte sélectionné. Activer les notifications de message Recevoir des notifications de messagerie instantanée pour le compte actuel. Notifier les messages de tous les comptes diff --git a/app/src/main/res/values-gl-rES/strings_pref_notifications.xml b/app/src/main/res/values-gl-rES/strings_pref_notifications.xml index aea99fb3..f3ce204d 100644 --- a/app/src/main/res/values-gl-rES/strings_pref_notifications.xml +++ b/app/src/main/res/values-gl-rES/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Engadir palabra chave Escribe a palabra e toca en + Palabra chave baleira - Notificar de todas as contas - Obtén notificacións de todas as contas con que iniciaras sesión. Ao desactivar isto, só obterás notificacións da conta actualmente seleccionada. + Notificar de todas as contas + Obtén notificacións de todas as contas con que iniciaras sesión. Ao desactivar isto, só obterás notificacións da conta actualmente seleccionada. Activar notificacións de mensaxes Obtén notificacións instantáneas de mensaxes para a túa conta actual. Notificar mensaxes de todas as contas diff --git a/app/src/main/res/values-it-rIT/strings_pref_notifications.xml b/app/src/main/res/values-it-rIT/strings_pref_notifications.xml index 9e3fb335..5f0b530c 100644 --- a/app/src/main/res/values-it-rIT/strings_pref_notifications.xml +++ b/app/src/main/res/values-it-rIT/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Aggiungi parole chiave Scrivi la parola e premi + Parola Chiave Vuota - Notifica da tutti gli accounts - Ricevi notifiche per ogni account registrato. Disattivando questa opzione riceverai notifiche solo dall\'account selezionato. + Notifica da tutti gli accounts + Ricevi notifiche per ogni account registrato. Disattivando questa opzione riceverai notifiche solo dall\'account selezionato. Attiva notifiche messaggi Ricevi notifiche istantanee di messaggi per il tuo account. Notifica messaggi da tutti gli account diff --git a/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml b/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml index beba9ee0..c6777c41 100644 --- a/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml +++ b/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml @@ -8,8 +8,8 @@ 키워드 추가 키워드를 작성하고 + 를 누르세요. 키워드 비어있음 - 모든 계정에서 알림 - 로그인 되어 있는 모든 계정에서 알림을 받습니다. 이 옺션을 해제하면 현재 로그인된 계정의 알림만 받게 됩니다. + 모든 계정에서 알림 + 로그인 되어 있는 모든 계정에서 알림을 받습니다. 이 옺션을 해제하면 현재 로그인된 계정의 알림만 받게 됩니다. 메시지 알림 활성화 현재 계정의 메시지 알림을 받습니다. 모든 계정의 메시지 받기 diff --git a/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml b/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml index 0f2a4cf8..12de118d 100644 --- a/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml +++ b/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Dodaj słowo kluczowe Wpisz słowo kluczowe i naciśnij + Puste słowo kluczowe - Powiadamiaj ze wszystkich kont - Otrzymuj powiadomienia dla każdego zalogowanego konta. Wyłączenie spowoduje sprawdzanie powiadomień, tylko z obecnie wybranego konta. + Powiadamiaj ze wszystkich kont + Otrzymuj powiadomienia dla każdego zalogowanego konta. Wyłączenie spowoduje sprawdzanie powiadomień, tylko z obecnie wybranego konta. Włącz powiadomienia o wiadomościach Otrzymuj natychmiastowe powiadomienia dla obecnego konta. Powiadamiaj ze wszystkich kont diff --git a/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml b/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml index cd8a8a25..1e128f7d 100644 --- a/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml +++ b/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Adicionar Palavra-Chave Escreva a palavra-chave e pressione + Palavra-chave Vazia - Notificações de todas as contas - Receber notificações para qualquer conta que esteja logada. Desativar esta opção vai apenas buscar as notificações da conta atualmente selecionada. + Notificações de todas as contas + Receber notificações para qualquer conta que esteja logada. Desativar esta opção vai apenas buscar as notificações da conta atualmente selecionada. Ativar as notificações de mensagem Receber notificações de conversas de sua conta atual. Notificar mensagens de todas as contas diff --git a/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml b/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml index 92889571..d077173c 100644 --- a/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml +++ b/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml @@ -8,8 +8,8 @@ Thêm từ khoá Nhập từ khoá rồi bấm + Từ khoá trống - Thông báo từ tất cả các tài khoản - Hiện thông báo từ tất cả các tài khoản đăng đăng nhập. Khi tắt, sẽ chỉ hiện thông báo từ tài khoản hiện tại. + Thông báo từ tất cả các tài khoản + Hiện thông báo từ tất cả các tài khoản đăng đăng nhập. Khi tắt, sẽ chỉ hiện thông báo từ tài khoản hiện tại. Bật thông báo tin nhắn Nhận thông báo tin nhắn cho tài khoản hiện tại. Thông báo tin nhắn từ tất cả các tài khoản diff --git a/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml b/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml index 66ae53a0..38a2722d 100644 --- a/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml +++ b/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml @@ -8,8 +8,8 @@ 添加关键字 输入关键字并按 + 空关键字 - 所有帐户通知 - 从每个登录帐户中获取通知。如果禁用则只从当前选择帐户中获取通知。 + 所有帐户通知 + 从每个登录帐户中获取通知。如果禁用则只从当前选择帐户中获取通知。 启用通知 获得当前帐户即时消息通知。 所有帐户消息通知 diff --git a/app/src/main/res/values/strings_pref_debug.xml b/app/src/main/res/values/strings_pref_debug.xml index 8e8c051c..771e5130 100644 --- a/app/src/main/res/values/strings_pref_debug.xml +++ b/app/src/main/res/values/strings_pref_debug.xml @@ -16,4 +16,7 @@ Navigate to the page with an issue and send the resources for debugging. Parsing Data + + Debug Parsers + Launch one of the available parsers to debug its response data \ No newline at end of file diff --git a/app/src/main/res/values/strings_pref_notifications.xml b/app/src/main/res/values/strings_pref_notifications.xml index 49665f8e..8db493b8 100644 --- a/app/src/main/res/values/strings_pref_notifications.xml +++ b/app/src/main/res/values/strings_pref_notifications.xml @@ -8,14 +8,16 @@ Add Keyword Type keyword and press + Empty Keyword - Notify from all accounts - Get notifications for every account that is logged in. Disabling this will only fetch notifications form the currently selected account. + Enable general notifications + Get general notifications for your current account. + Notify from all accounts + Get general notifications for every account that is logged in. Enable message notifications Get instant message notifications for your current account. Notify messages from all accounts Get instant message notifications from all accounts Fetch Notifications Now - Trigger the notification fetcher once. Note that fetching instant messages takes time. + Trigger the notification fetcher once. Fetching Notifications… Couldn\'t fetch notifications Notification sound diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index 28978193..0ddc28f3 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -9,8 +9,8 @@ - - + + diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt index a79ccf3f..08853466 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt @@ -49,4 +49,12 @@ class FbRegexTest { val data = "\"uri\":\"$url\"}" assertEquals(url, FB_JSON_URL_MATCHER.find(data)[1]) } + + @Test + fun imageIdRegex() { + val id = 123456L + val img = "https://scontent-yyz1-1.xx.fbcdn.net/v/t31.0-8/fr/cp0/e15/q65/89056_${id}_98239_o.jpg" + assertEquals(id, FB_IMAGE_ID_MATCHER.find(img)[1]?.toLongOrNull()) + } + } \ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt index 62b7cac2..79d5ea64 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt @@ -1,7 +1,10 @@ package com.pitchedapps.frost.facebook +import com.pitchedapps.frost.utils.isImageUrl import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue /** @@ -57,4 +60,28 @@ class FbUrlTest { assertFbFormat(expected, url) } + + @Test + fun imageRegex() { + arrayOf( + "https://scontent-yyz1-1.xx.fbcdn.net/v/t1.0-9/fr/cp0/e15/q65/229_546131_836546862_n.jpg?efg=e343J9&oh=d4245b1&oe=5453", + "/photo/view_full_size/?fbid=1523&ref_component=mbasic_photo_permalink&ref_page=%2Fwap%2Fphoto.php&refid=153&_ft_=...", + "#!/photo/view_full_size/?fbid=1523&ref_component=mbasic_photo_permalink&ref_page=%2Fwap%2Fphoto.php&refid=153&_ft_=..." + ).forEach { + assertTrue(it.isImageUrl, "Failed to match image for $it") + } + } + + @Test + fun antiImageRegex() { + arrayOf( + "http...fbcdn.net...mp4", + "/photo/...png", + "https://www.google.ca" + ).forEach { + assertFalse(it.isImageUrl, "Should not have matched image for $it") + } + + } + } \ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt new file mode 100644 index 00000000..ec92b059 --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt @@ -0,0 +1,61 @@ +package com.pitchedapps.frost.rx + +import org.junit.Before +import org.junit.Test +import java.util.concurrent.CountDownLatch +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +/** + * Created by Allan Wang on 07/01/18. + */ +private inline val threadId + get() = Thread.currentThread().id + +class ResettableFlyweightTest { + + class IntFlyweight : RxFlyweight() { + override fun call(input: Int): Long { + println("Call for $input on thread $threadId") + Thread.sleep(20) + return System.currentTimeMillis() + } + + override fun validate(input: Int, cond: Long) = System.currentTimeMillis() - cond < 500 + + override fun cache(input: Int): Long = System.currentTimeMillis() + } + + private lateinit var flyweight: IntFlyweight + private lateinit var latch: CountDownLatch + + @Before + fun init() { + flyweight = IntFlyweight() + latch = CountDownLatch(1) + } + + @Test + fun testCache() { + flyweight(1).subscribe { i -> + flyweight(1).subscribe { j -> + assertEquals(i, j, "Did not use cache during calls") + latch.countDown() + } + } + latch.await() + } + + @Test + fun testNoCache() { + flyweight(1).subscribe { i -> + flyweight(2).subscribe { j -> + assertNotEquals(i, j, "Should not use cache for calls with different keys") + latch.countDown() + } + } + latch.await() + } + + +} \ No newline at end of file -- cgit v1.2.3