From f1e1aec8487fd148eb8e75fe016a8438958989ad Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 21 Dec 2017 20:55:51 -0500 Subject: misc (#566) * Fix click validator * Update tests * Feature/fb requests (#567) * Add initial requesting interface * Update unit tests and dependencies * Resolve lint * Fix lint 2 * Fix toolbar location, closes #439 * Add prev version code, closes #551 * Clear test val * Update changelog --- .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 2 + .../frost/activities/BaseMainActivity.kt | 10 ++- .../frost/contracts/ActivityContract.kt | 1 + .../frost/contracts/FrostContentContract.kt | 5 ++ .../com/pitchedapps/frost/facebook/FbConst.kt | 3 +- .../com/pitchedapps/frost/facebook/FbRegex.kt | 20 +++++ .../com/pitchedapps/frost/facebook/FbRequest.kt | 98 ++++++++++++++++++++++ .../com/pitchedapps/frost/utils/Downloader.kt | 2 +- .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 2 + .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 21 ++++- .../pitchedapps/frost/views/FrostContentView.kt | 11 +++ .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 3 +- .../frost/web/FrostUrlOverlayValidator.kt | 38 ++------- .../com/pitchedapps/frost/web/LoginWebView.kt | 12 +-- 14 files changed, 179 insertions(+), 49 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt (limited to 'app/src/main/kotlin') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index 2c313ffe..fa2bdf8a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -27,6 +27,7 @@ import com.pitchedapps.frost.utils.Showcase import com.raizlabs.android.dbflow.config.DatabaseConfig import com.raizlabs.android.dbflow.config.FlowConfig import com.raizlabs.android.dbflow.config.FlowManager +import com.raizlabs.android.dbflow.runtime.ContentResolverNotifier import io.fabric.sdk.android.Fabric import java.util.* import kotlin.reflect.KClass @@ -46,6 +47,7 @@ class FrostApp : Application() { private fun FlowConfig.Builder.withDatabase(name: String, klass: KClass<*>) = addDatabaseConfig(DatabaseConfig.builder(klass.java) .databaseName(name) + .modelNotifier(ContentResolverNotifier("${BuildConfig.APPLICATION_ID}.dbflow.provider")) .build()) override fun onCreate() { 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 389ff88e..4a9cbb55 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -87,8 +87,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, val appBar: AppBarLayout by bindView(R.id.appbar) val coordinator: CoordinatorLayout by bindView(R.id.main_content) override var videoViewer: FrostVideoViewer? = null - lateinit var drawer: Drawer - lateinit var drawerHeader: AccountHeader + private lateinit var drawer: Drawer + private lateinit var drawerHeader: AccountHeader override var searchView: SearchView? = null private val searchViewCache = mutableMapOf>() @@ -96,11 +96,13 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (BuildConfig.VERSION_CODE > Prefs.versionCode) { + Prefs.prevVersionCode = Prefs.versionCode Prefs.versionCode = BuildConfig.VERSION_CODE if (!BuildConfig.DEBUG) { frostChangelog() frostAnswersCustom("Version", "Version code" to BuildConfig.VERSION_CODE, + "Prev version code" to Prefs.prevVersionCode, "Version name" to BuildConfig.VERSION_NAME, "Build type" to BuildConfig.BUILD_TYPE, "Frost id" to Prefs.frostId) @@ -346,6 +348,10 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, super.onDestroy() } + override fun collapseAppBar() { + appBar.setExpanded(false) + } + override fun backConsumer(): Boolean { if (currentFragment.onBackPressed()) return true if (Prefs.exitConfirmation) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt index f51c4e53..e46a4bfb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -11,4 +11,5 @@ interface MainActivityContract : ActivityContract { val fragmentSubject: PublishSubject fun setTitle(res: Int) fun setTitle(text: CharSequence) + fun collapseAppBar() } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt index 681636c4..117a1d36 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt @@ -53,6 +53,11 @@ interface FrostContentParent : DynamicUiContract { var baseEnum: FbItem? + /** + * Toggle state for allowing swipes + */ + var swipeEnabled: Boolean + /** * Binds the container to self * this will also handle all future bindings diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt index 43bc5724..d98241f1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt @@ -6,8 +6,9 @@ package com.pitchedapps.frost.facebook const val FACEBOOK_COM = "facebook.com" const val HTTPS_FACEBOOK_COM = "https://$FACEBOOK_COM" -const val FB_URL_BASE = "https://m.$FACEBOOK_COM/" +const val FB_URL_BASE = "https://touch.$FACEBOOK_COM/" fun PROFILE_PICTURE_URL(id: Long) = "https://graph.facebook.com/$id/picture?type=large" +const val FB_LOGIN_URL = "${FB_URL_BASE}login" const val USER_AGENT_FULL = "Mozilla/5.0 (Linux; Android 4.4.2; en-us; SAMSUNG SM-G900T Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.6 Chrome/28.0.1500.94 Mobile Safari/537.36" const val USER_AGENT_BASIC = "Mozilla/5.0 (BB10; Kbd) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.1.0.4633 Mobile Safari/537.10+" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt new file mode 100644 index 00000000..39e8c467 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt @@ -0,0 +1,20 @@ +package com.pitchedapps.frost.facebook + +/** + * Created by Allan Wang on 21/12/17. + * + * Collection of regex matchers + * Input text must be properly unescaped + * + * See [StringEscapeUtils] + */ + +/** + * Matches the fb_dtsg component of a page containing it as a hidden value + */ +val FB_DTSG_MATCHER: Regex by lazy { Regex("name=\"fb_dtsg\" value=\"(.*?)\"") } + +/** + * Matches user id from cookie + */ +val FB_USER_MATCHER: Regex by lazy { Regex("c_user=([0-9]*);") } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt new file mode 100644 index 00000000..428043a0 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt @@ -0,0 +1,98 @@ +package com.pitchedapps.frost.facebook + +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.utils.L +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import okhttp3.* +import okhttp3.logging.HttpLoggingInterceptor +import org.apache.commons.text.StringEscapeUtils + +/** + * Created by Allan Wang on 21/12/17. + */ +data class RequestAuth(val userId: Long = -1, val cookie: String = "", val fb_dtsg: String = "") + +private val client: OkHttpClient by lazy { + val builder = OkHttpClient.Builder() + if (BuildConfig.DEBUG) + builder.addInterceptor(HttpLoggingInterceptor() + .setLevel(HttpLoggingInterceptor.Level.BASIC)) + builder.build() +} + +private fun List>.toForm(): RequestBody { + val builder = FormBody.Builder() + forEach { (key, value) -> + val v = value?.toString() ?: "" + builder.add(key, v) + } + return builder.build() +} + +private fun String.requestBuilder() = Request.Builder() + .header("Cookie", this) + .header("User-Agent", USER_AGENT_BASIC) + .cacheControl(CacheControl.FORCE_NETWORK) + +private fun Request.Builder.call() = client.newCall(build()) + + +fun Pair.getAuth(): RequestAuth? { + val (userId, cookie) = this + val call = cookie.requestBuilder() + .url(FB_URL_BASE) + .get() + .call() + call.execute().body()?.charStream()?.useLines { + it.forEach { + val text = StringEscapeUtils.unescapeEcmaScript(it) + val result = FB_DTSG_MATCHER.find(text) + val fb_dtsg = result?.groupValues?.get(1) + if (fb_dtsg != null) { + L.d(null, "fb_dtsg for $userId: $fb_dtsg") + return RequestAuth(userId, cookie, fb_dtsg) + } + } + } + + return null +} + +fun RequestAuth.markNotificationRead(notifId: Long): Call { + + val body = listOf( + "click_type" to "notification_click", + "id" to notifId, + "target_id" to "null", + "m_sess" to null, + "fb_dtsg" to fb_dtsg, + "__dyn" to null, + "__req" to null, + "__ajax__" to null, + "__user" to userId + ) + + return cookie.requestBuilder() + .url("${FB_URL_BASE}a/jewel_notifications_log.php") + .post(body.toForm()) + .call() +} + +private inline fun zip(data: Array, + crossinline mapper: (List) -> O, + crossinline caller: (T) -> R): Single { + val singles = data.map { Single.fromCallable { caller(it) }.subscribeOn(Schedulers.io()) } + return Single.zip(singles) { + val results = it.mapNotNull { it as? R } + mapper(results) + } +} + +fun RequestAuth.markNotificationsRead(vararg notifId: Long) = zip(notifId.toTypedArray(), + { it.count { it } }) { + val response = markNotificationRead(it).execute() + val buffer = CharArray(20) + response.body()?.charStream()?.read(buffer) ?: return@zip false + !buffer.toString().contains("error") +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt index 2c638dfd..aab79e00 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -58,7 +58,7 @@ fun Context.frostDownload(uri: Uri?, request.setMimeType(mimeType) val cookie = loadFbCookie(Prefs.userId) ?: return@kauRequestPermissions val title = URLUtil.guessFileName(uri.toString(), contentDisposition, mimeType) - request.addRequestHeader("cookie", cookie.cookie) + request.addRequestHeader("Cookie", cookie.cookie) request.addRequestHeader("User-Agent", userAgent) request.setDescription(string(R.string.downloading)) request.setTitle(title) 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 94bf0016..cc5ee733 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -43,6 +43,8 @@ object Prefs : KPref() { var versionCode: Int by kpref("version_code", -1) + var prevVersionCode: Int by kpref("prev_version_code", -1) + var installDate: Long by kpref("install_date", -1L) var identifier: Int by kpref("identifier", -1) 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 5a83c3f3..0ca068b5 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -229,12 +229,23 @@ inline val String?.isVideoUrl /** * [true] if url can be displayed in a different webview */ -inline val String?.isIndependent - get() = this == null || (startsWith("http") && !isFacebookUrl) - || dependentSet.all { !contains(it) } +inline val String?.isIndependent: Boolean + get() { + if (this == null || length < 5) return false // ignore short queries + if (this[0] == '#' && !contains('/')) return false // ignore element values + if (startsWith("http") && !isFacebookUrl) return true // ignore non facebook urls + if (dependentSet.any { contains(it) }) return false // ignore known dependent segments + return true + } val dependentSet = setOf( - "photoset_token", "direct_action_execute" + "photoset_token", "direct_action_execute", "messages/?pageNum", "sharer.php", + /* + * 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 + */ + "messages/read/?tid=id", "messages/read/?tid=mid" ) inline val String?.isExplicitIntent @@ -254,6 +265,8 @@ inline fun Context.sendFrostEmail(@StringRes subjectId: Int, crossinline builder inline fun Context.sendFrostEmail(subjectId: String, crossinline builder: EmailBuilder.() -> Unit) = sendEmail(string(R.string.dev_email), subjectId) { builder() + + addItem("Prev version", Prefs.prevVersionCode.toString()) val proTag = if (IS_FROST_PRO) "TY" else "FP" addItem("Random Frost ID", "${Prefs.frostId}-$proTag") } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt index 58449de3..809b6090 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -12,6 +12,7 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.web.WEB_LOAD_DELAY @@ -57,6 +58,16 @@ abstract class FrostContentView @JvmOverloads constructor( protected abstract val layoutRes: Int + override var swipeEnabled: Boolean + get() = refresh.isEnabled + set(value) { + refresh.isEnabled = value + if (!value) { + // locked onto an input field; ensure content is visible + (context as? MainActivityContract)?.collapseAppBar() + } + } + /** * Sets up everything * Called by [bind] 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 e8135f5b..b567801b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -1,6 +1,5 @@ package com.pitchedapps.frost.web -import android.support.v4.widget.SwipeRefreshLayout import android.webkit.JavascriptInterface import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.contracts.VideoViewHolder @@ -68,7 +67,7 @@ class FrostJSI(val web: FrostWebView) { */ @JavascriptInterface fun disableSwipeRefresh(disable: Boolean) { - web.post { (web.parent as? SwipeRefreshLayout)?.isEnabled = !disable } + web.post { web.parent.swipeEnabled = !disable } } @JavascriptInterface 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 9255b5bb..253d4801 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -29,15 +29,16 @@ import org.jetbrains.anko.runOnUiThread * as we have no need of sending a new intent to the same activity */ fun FrostWebView.requestWebOverlay(url: String): Boolean { - if (url == "#" || !url.isIndependent) { - L.i("Forbid overlay switch", url) - return false - } + val context = context // finalize reference if (url.isVideoUrl && context is VideoViewHolder) { L.i("Found video", url) - context.runOnUiThread { (context as VideoViewHolder).showVideo(url) } + context.runOnUiThread { context.showVideo(url) } return true } + if (!url.isIndependent) { + L.i("Forbid overlay switch", url) + return false + } if (!Prefs.overlayEnabled) return false if (context is WebOverlayActivityBase) { L.v("Check web request from overlay", url) @@ -55,26 +56,6 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean { L.i("return false switch") return false } - /* - * Non facebook urls can be loaded - */ - if (!url.formattedFbUrl.isFacebookUrl) { - context.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) context.launchWebOverlay(url) return true @@ -87,9 +68,4 @@ val messageWhitelist = setOf(FbItem.MESSAGES, FbItem.CHAT, FbItem.FEED_MOST_RECE val String.shouldUseBasicAgent get() = !contains("story.php") //we will use basic agent for anything that isn't a comment section -// get() = (messageWhitelist.any { contains(it) }) || this == FB_URL_BASE - -/** - * The following components should never be launched in a new overlay - */ -val overlayBlacklist = setOf("messages/?pageNum", "photoset_token", "sharer.php") \ No newline at end of file +// get() = (messageWhitelist.any { contains(it) }) || this == FB_URL_BASE \ No newline at end of file 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 73d2476c..3a10ed32 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -9,8 +9,9 @@ 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.FB_URL_BASE +import com.pitchedapps.frost.facebook.FB_USER_MATCHER import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.FB_LOGIN_URL import com.pitchedapps.frost.injectors.CssHider import com.pitchedapps.frost.injectors.jsInject import com.pitchedapps.frost.utils.L @@ -26,11 +27,6 @@ class LoginWebView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : WebView(context, attrs, defStyleAttr) { - companion object { - const val LOGIN_URL = "${FB_URL_BASE}login" - private val userMatcher: Regex = Regex("c_user=([0-9]*);") - } - private lateinit var loginCallback: (CookieModel) -> Unit private lateinit var progressCallback: (Int) -> Unit @@ -50,7 +46,7 @@ class LoginWebView @JvmOverloads constructor( this.progressCallback = progressCallback this.loginCallback = loginCallback L.d("Begin loading login") - loadUrl(LOGIN_URL) + loadUrl(FB_LOGIN_URL) } private inner class LoginClient : BaseWebViewClient() { @@ -66,7 +62,7 @@ class LoginWebView @JvmOverloads constructor( 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 + val id = FB_USER_MATCHER.find(cookie)?.groupValues?.get(1)?.toLong() ?: return@doAsync uiThread { onFound(id, cookie) } } } -- cgit v1.2.3