From e4679b1663fa78a99c6c8225e454595c6c6f4e38 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 17 Jul 2017 12:38:42 -0700 Subject: Fix notifications and long press for albums (#69) * Allow for album images to be viewed * Update listing info * Web refractoring * Test message notifications * Fix notifications and context press --- README.md | 3 +- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 +- app/src/main/assets/js/context_a.js | 5 +- app/src/main/assets/js/context_a.min.js | 5 +- .../pitchedapps/frost/activities/MainActivity.kt | 10 +- .../com/pitchedapps/frost/facebook/FbCookie.kt | 10 +- .../com/pitchedapps/frost/injectors/JsActions.kt | 1 + .../frost/services/NotificationService.kt | 43 ++++-- .../com/pitchedapps/frost/web/BaseWebViewClient.kt | 16 -- .../com/pitchedapps/frost/web/FrostChromeClient.kt | 52 ------- .../pitchedapps/frost/web/FrostChromeClients.kt | 65 ++++++++ .../frost/web/FrostRequestInterceptor.kt | 24 ++- .../pitchedapps/frost/web/FrostWebViewClient.kt | 104 ------------- .../frost/web/FrostWebViewClientMenu.kt | 35 ----- .../pitchedapps/frost/web/FrostWebViewClients.kt | 172 +++++++++++++++++++++ .../pitchedapps/frost/web/FrostWebViewSearch.kt | 170 -------------------- .../com/pitchedapps/frost/web/MessageWebView.kt | 81 ++++++++++ .../com/pitchedapps/frost/web/SearchWebView.kt | 145 +++++++++++++++++ app/src/main/play/en-CA/listing/fulldescription | 5 +- app/src/main/res/xml/changelog.xml | 12 +- 21 files changed, 540 insertions(+), 421 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClientMenu.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt diff --git a/README.md b/README.md index 7f6203da..0a347c2b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ It contains many features, including: * Full theming across all activities * Overlaying browser to read posts and get right back to your previous task * Extensive notification support, with bundling, filtering, battery friendly scheduling, icons, and multi user support -* Context menu from any link through long press +* Context menu from any link via long press +* Native image viewer and downloader via long press * Reactive based loading * The transparency of open sourced development diff --git a/app/build.gradle b/app/build.gradle index a354d2e9..8729ed69 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,6 +71,7 @@ android { versionNameSuffix "-debug" resValue "string", "app_name", "Frost Debug" resValue "string", "frost_web", "Frost Web Debug" + ext.enableCrashlytics = false } releaseTest { minifyEnabled true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4135ab59..5758f38c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ - + diff --git a/app/src/main/assets/js/context_a.js b/app/src/main/assets/js/context_a.js index c7601764..689c6f0d 100644 --- a/app/src/main/assets/js/context_a.js +++ b/app/src/main/assets/js/context_a.js @@ -25,8 +25,9 @@ if (!window.hasOwnProperty('frost_context_a')) { if (!url) return; var text = element.parentNode.innerText; - //check if image item exists - var image = element.parentNode.querySelector('[style*="background-image: url("]'); + //check if image item exists, first in children and then in parent + var image = element.querySelector('[style*="background-image: url("]'); + if (!image) image = element.parentNode.querySelector('[style*="background-image: url("]'); if (image) { var imageUrl = window.getComputedStyle(image, null).backgroundImage.slice(5, -2); console.log('Context image', imageUrl); diff --git a/app/src/main/assets/js/context_a.min.js b/app/src/main/assets/js/context_a.min.js index 5c5f033a..97799c33 100644 --- a/app/src/main/assets/js/context_a.min.js +++ b/app/src/main/assets/js/context_a.min.js @@ -9,8 +9,9 @@ longClick=!0 "A"!==t.tagName&&(t=t.parentNode),"A"===t.tagName&&"#"!==t.getAttribute("href"))){ var o=t.getAttribute("href") ;if(!o)return -;var n=t.parentNode.innerText,r=t.parentNode.querySelector('[style*="background-image: url("]') -;if(r){ +;var n=t.parentNode.innerText,r=t.querySelector('[style*="background-image: url("]') +;if(r||(r=t.parentNode.querySelector('[style*="background-image: url("]')), +r){ var a=window.getComputedStyle(r,null).backgroundImage.slice(5,-2) ;console.log("Context image",a), "undefined"!=typeof Frost&&Frost.loadImage(a,n) 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 ba76e594..1227fd6b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -55,7 +55,7 @@ import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.utils.iab.validatePro import com.pitchedapps.frost.views.BadgedIcon import com.pitchedapps.frost.views.FrostViewPager -import com.pitchedapps.frost.web.FrostWebViewSearch +import com.pitchedapps.frost.web.SearchWebView import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers @@ -63,7 +63,7 @@ import io.reactivex.subjects.PublishSubject import org.jsoup.Jsoup import java.util.concurrent.TimeUnit -class MainActivity : BaseActivity(), FrostWebViewSearch.SearchContract, +class MainActivity : BaseActivity(), SearchWebView.SearchContract, ActivityWebContract, FileChooserContract by FileChooserDelegate() { lateinit var adapter: SectionsPagerAdapter @@ -78,13 +78,13 @@ class MainActivity : BaseActivity(), FrostWebViewSearch.SearchContract, var webFragmentObservable = PublishSubject.create()!! var lastPosition = -1 val headerBadgeObservable = PublishSubject.create() - var hiddenSearchView: FrostWebViewSearch? = null + var hiddenSearchView: SearchWebView? = null var firstLoadFinished = false set(value) { L.d("First fragment load has finished") field = value if (value && hiddenSearchView == null) { - hiddenSearchView = FrostWebViewSearch(this, this) + hiddenSearchView = SearchWebView(this, this) } } var searchView: SearchView? = null @@ -354,7 +354,7 @@ class MainActivity : BaseActivity(), FrostWebViewSearch.SearchContract, R.id.action_settings to GoogleMaterial.Icon.gmd_settings, R.id.action_search to GoogleMaterial.Icon.gmd_search) if (Prefs.searchBar) { - if (firstLoadFinished && hiddenSearchView == null) hiddenSearchView = FrostWebViewSearch(this, this) + if (firstLoadFinished && hiddenSearchView == null) hiddenSearchView = SearchWebView(this, this) if (searchView == null) searchView = bindSearchView(menu, R.id.action_search, Prefs.iconColor) { textObserver = { observable, _ -> diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 875f1c49..3b0125be 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -94,12 +94,14 @@ object FbCookie { * When coming back to the main app, switch back to our original account before continuing */ fun switchBackUser(callback: () -> Unit) { - if (Prefs.prevId != -1L && Prefs.prevId != Prefs.userId) { - switchUser(Prefs.prevId) { - L.d("Switch back user", "${Prefs.userId} to ${Prefs.prevId}") + if (Prefs.prevId == -1L) return callback() + val prevId = Prefs.prevId + Prefs.prevId = -1L + if (prevId != Prefs.userId) { + switchUser(prevId) { + L.d("Switch back user", "${Prefs.userId} to ${prevId}") callback() } } else callback() - if (Prefs.prevId != -1L) Prefs.prevId = -1L } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt index 4c44c1bf..de270948 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt @@ -15,6 +15,7 @@ enum class JsActions(body: String) : InjectorContract { */ LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"), BASE_HREF("document.write(\"\");"), + GET_MESSAGES("setTimeout(function(){Frost.handleHtml(document.getElementById('threadlist_rows').outerHtml)},1000)"), EMPTY(""); val function = "!function(){$body}();" 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 38282bf7..ad977d1a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -17,7 +17,9 @@ import com.pitchedapps.frost.facebook.USER_AGENT_BASIC import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostAnswersCustom +import com.pitchedapps.frost.web.MessageWebView import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread import org.jsoup.Jsoup import org.jsoup.nodes.Element import java.util.concurrent.Future @@ -45,17 +47,29 @@ class NotificationService : JobService() { return false } + override fun onStartJob(params: JobParameters?): Boolean { future = doAsync { if (Prefs.notificationAllAccounts) { - loadFbCookiesSync().forEach { - data -> - fetchNotifications(data) - } + val cookies = loadFbCookiesSync() + cookies.forEach { fetchGeneralNotifications(it) } +// if (Prefs.notificationsInstantMessages) { +// Prefs.prevId = Prefs.userId +// uiThread { +// val messageWebView = MessageWebView(this@NotificationService, params) +// cookies.forEach { messageWebView.request(it) } +// } +// return@doAsync +// } } else { val currentCookie = loadFbCookie(Prefs.userId) - if (currentCookie != null) - fetchNotifications(currentCookie) + if (currentCookie != null) { + fetchGeneralNotifications(currentCookie) +// if (Prefs.notificationsInstantMessages) { +// uiThread { MessageWebView(this@NotificationService, params).request(currentCookie) } +// return@doAsync +// } + } } L.d("Finished notifications") jobFinished(params, false) @@ -69,12 +83,6 @@ class NotificationService : JobService() { return null } - fun fetchNotifications(data: CookieModel) { - fetchGeneralNotifications(data) -// fetchMessageNotifications(data) - debugNotification("Hello") - } - fun fetchGeneralNotifications(data: CookieModel) { L.i("Notif fetch for $data") val doc = Jsoup.connect(FbTab.NOTIFICATIONS.url).cookie(FACEBOOK_COM, data.cookie).userAgent(USER_AGENT_BASIC).get() @@ -96,7 +104,8 @@ class NotificationService : JobService() { newLatestEpoch = notif.timestamp notifCount++ } - if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epoch = newLatestEpoch).update() + if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epoch = newLatestEpoch).save() + L.d("Notif new latest epoch ${lastNotificationTime(data.id).epoch}") frostAnswersCustom("Notifications") { putCustomAttribute("Type", "General") putCustomAttribute("Count", notifCount) @@ -120,10 +129,9 @@ class NotificationService : JobService() { return NotificationContent(data, notifId.toInt(), a.attr("href"), null, text, epoch, pUrl) } - fun fetchMessageNotifications(data: CookieModel) { - if (!Prefs.notificationsInstantMessages) return + fun fetchMessageNotifications(data: CookieModel, content: String) { L.i("Notif IM fetch for $data") - val doc = Jsoup.connect(FbTab.MESSAGES.url).cookie(FACEBOOK_COM, data.cookie).userAgent(USER_AGENT_BASIC).get() + val doc = Jsoup.parseBodyFragment(content) val unreadNotifications = (doc.getElementById("threadlist_rows") ?: return L.eThrow("Notification messages not found")).getElementsByClass("aclb") var notifCount = 0 L.d("IM notif count ${unreadNotifications.size}") @@ -146,7 +154,8 @@ class NotificationService : JobService() { newLatestEpoch = notif.timestamp notifCount++ } -// if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epochIm = newLatestEpoch).update() + if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epochIm = newLatestEpoch).save() + L.d("Notif new latest im epoch ${lastNotificationTime(data.id).epochIm}") frostAnswersCustom("Notifications") { putCustomAttribute("Type", "Message") putCustomAttribute("Count", notifCount) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt deleted file mode 100644 index 09241254..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/BaseWebViewClient.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.pitchedapps.frost.web - -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import android.webkit.WebView -import android.webkit.WebViewClient - -/** - * Created by Allan Wang on 2017-07-13. - */ -open class BaseWebViewClient : WebViewClient() { - - override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? - = shouldFrostInterceptRequest(view, request) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt deleted file mode 100644 index 4df6d6a7..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClient.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.pitchedapps.frost.web - -import android.net.Uri -import android.webkit.ConsoleMessage -import android.webkit.ValueCallback -import android.webkit.WebChromeClient -import android.webkit.WebView -import ca.allanwang.kau.utils.snackbar -import com.pitchedapps.frost.contracts.ActivityWebContract -import com.pitchedapps.frost.utils.L -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.Subject - - -/** - * Created by Allan Wang on 2017-05-31. - */ -class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() { - - val progressObservable: Subject = webCore.progressObservable - val titleObservable: BehaviorSubject = webCore.titleObservable - val activityContract = (webCore.context as? ActivityWebContract) - - companion object { - val consoleBlacklist = setOf( - "edge-chat" - ) - } - - override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { - if (consoleBlacklist.any { consoleMessage.message().contains(it) }) return true - L.i("Chrome Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}") - return true - } - - override fun onReceivedTitle(view: WebView, title: String) { - super.onReceivedTitle(view, title) - if (title.contains("http") || titleObservable.value == title) return - titleObservable.onNext(title) - } - - override fun onProgressChanged(view: WebView, newProgress: Int) { - super.onProgressChanged(view, newProgress) - progressObservable.onNext(newProgress) - } - - override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback>, fileChooserParams: FileChooserParams): Boolean { - activityContract?.openFileChooser(filePathCallback, fileChooserParams) ?: webView.snackbar("File chooser not found") - return activityContract != null - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt new file mode 100644 index 00000000..b8ba0d1d --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt @@ -0,0 +1,65 @@ +package com.pitchedapps.frost.web + +import android.net.Uri +import android.webkit.ConsoleMessage +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebView +import ca.allanwang.kau.utils.snackbar +import com.pitchedapps.frost.contracts.ActivityWebContract +import com.pitchedapps.frost.utils.L +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject + + +/** + * Created by Allan Wang on 2017-05-31. + * + * Collection of chrome clients + */ + +/** + * Nothing more than a client without logging + */ +class QuietChromeClient : WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage) = true +} + +/** + * The default chrome client + */ +class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() { + + val progressObservable: Subject = webCore.progressObservable + val titleObservable: BehaviorSubject = webCore.titleObservable + val activityContract = (webCore.context as? ActivityWebContract) + + companion object { + val consoleBlacklist = setOf( + "edge-chat" + ) + } + + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + if (consoleBlacklist.any { consoleMessage.message().contains(it) }) return true + L.i("Chrome Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}") + return true + } + + override fun onReceivedTitle(view: WebView, title: String) { + super.onReceivedTitle(view, title) + if (title.contains("http") || titleObservable.value == title) return + titleObservable.onNext(title) + } + + override fun onProgressChanged(view: WebView, newProgress: Int) { + super.onProgressChanged(view, newProgress) + progressObservable.onNext(newProgress) + } + + override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback>, fileChooserParams: FileChooserParams): Boolean { + activityContract?.openFileChooser(filePathCallback, fileChooserParams) ?: webView.snackbar("File chooser not found") + return activityContract != null + } + +} \ No newline at end of file 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 45dc83aa..3f2891d0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt @@ -1,17 +1,12 @@ package com.pitchedapps.frost.web -import android.graphics.Bitmap.CompressFormat import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import ca.allanwang.kau.utils.use -import com.pitchedapps.frost.utils.GlideApp import com.pitchedapps.frost.utils.L -import com.pitchedapps.frost.utils.Prefs import okhttp3.HttpUrl import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.InputStream /** @@ -62,6 +57,23 @@ fun shouldFrostInterceptRequest(view: WebView, request: WebResourceRequest): Web return null } +/** + * Wrapper to ensure that null exceptions are not reached + */ +fun WebResourceRequest.query(action: (url: String) -> Boolean): Boolean { + return action(url?.path ?: return false) +} + +/** + * Generic filter passthrough + * If Resource is already nonnull, pass it, otherwise check if filter is met and override the response accordingly + */ +fun WebResourceResponse?.filter(request: WebResourceRequest, filter: (url: String) -> Boolean): WebResourceResponse? + = this ?: if (request.query { filter(it) }) blankResource else null + fun WebResourceResponse?.filterCss(request: WebResourceRequest): WebResourceResponse? - = this ?: if (request.url.path.endsWith(".css")) blankResource else null + = filter(request) { it.endsWith(".css") } + +fun WebResourceResponse?.filterImage(request: WebResourceRequest): WebResourceResponse? + = filter(request) { it.contains(".jpg") || it.contains(".png") } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt deleted file mode 100644 index 5b2b4bfd..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.pitchedapps.frost.web - -import android.content.Context -import android.graphics.Bitmap -import android.webkit.WebResourceRequest -import android.webkit.WebView -import com.pitchedapps.frost.activities.LoginActivity -import com.pitchedapps.frost.activities.MainActivity -import com.pitchedapps.frost.activities.SelectorActivity -import com.pitchedapps.frost.facebook.FACEBOOK_COM -import com.pitchedapps.frost.facebook.FbCookie -import com.pitchedapps.frost.injectors.* -import com.pitchedapps.frost.utils.* -import io.reactivex.subjects.Subject - -/** - * Created by Allan Wang on 2017-05-31. - */ -open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient() { - - val refreshObservable: Subject = webCore.refreshObservable - - override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - L.i("FWV Loading $url") -// L.v("Cookies ${CookieManager.getInstance().getCookie(url)}") - refreshObservable.onNext(true) - if (!url.contains(FACEBOOK_COM)) return - if (url.contains("logout.php")) FbCookie.logout(Prefs.userId, { launchLogin(view.context) }) - else if (url.contains("login.php")) FbCookie.reset({ launchLogin(view.context) }) - } - - fun launchLogin(c: Context) { - if (c is MainActivity && c.cookies().isNotEmpty()) - c.launchNewTask(SelectorActivity::class.java, c.cookies()) - else - c.launchNewTask(LoginActivity::class.java) - } - - override fun onPageFinished(view: WebView, url: String) { - super.onPageFinished(view, url) - L.i("Page finished $url") - if (!url.contains(FACEBOOK_COM)) { - refreshObservable.onNext(false) - return - } - view.jsInject( - CssAssets.ROUND_ICONS.maybe(Prefs.showRoundedIcons), - CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!Prefs.showSuggestedFriends && Prefs.pro), - CssHider.ADS.maybe(!Prefs.showFacebookAds && Prefs.pro) - ) - onPageFinishedActions(url) - } - - open internal fun onPageFinishedActions(url: String) { - injectAndFinish() - } - - internal fun injectAndFinish() { - L.d("Page finished reveal") - webCore.jsInject(CssHider.HEADER, - Prefs.themeInjector, - callback = { - refreshObservable.onNext(false) - webCore.jsInject( - JsActions.LOGIN_CHECK, - JsAssets.CLICK_A.maybe(webCore.baseEnum != null && Prefs.overlayEnabled), - JsAssets.CONTEXT_A, - JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null) - ) - }) - } - - open fun handleHtml(html: String) { - L.d("Handle Html") - } - - open fun emit(flag: Int) { - L.d("Emit $flag") - } - - /** - * Helper to format the request and launch it - * returns true to override the url - */ - private fun launchRequest(request: WebResourceRequest): Boolean { - L.d("Launching ${request.url}") - webCore.context.launchWebOverlay(request.url.toString()) - return true - } - - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { - L.i("Url Loading ${request.url}") - val path = request.url.path ?: return super.shouldOverrideUrlLoading(view, request) - if (path.startsWith("/composer/")) return launchRequest(request) - return super.shouldOverrideUrlLoading(view, request) - } - -// override fun onPageCommitVisible(view: WebView?, url: String?) { -// L.d("ASDF PCV") -// super.onPageCommitVisible(view, url) -// } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClientMenu.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClientMenu.kt deleted file mode 100644 index 10648e73..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClientMenu.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.pitchedapps.frost.web - -import android.webkit.WebView -import com.pitchedapps.frost.facebook.FB_URL_BASE -import com.pitchedapps.frost.injectors.JsAssets -import com.pitchedapps.frost.injectors.jsInject - -/** - * Created by Allan Wang on 2017-05-31. - */ -class FrostWebViewClientMenu(webCore: FrostWebViewCore) : FrostWebViewClient(webCore) { - - private val String.shouldInjectMenu - get() = when (removePrefix(FB_URL_BASE)) { - "settings", - "settings#", - "settings#!/settings?soft=bookmarks" -> true - else -> false - } - - override fun onPageFinished(view: WebView, url: String) { - super.onPageFinished(view, url) - if (url.shouldInjectMenu) jsInject(JsAssets.MENU) - } - - override fun emit(flag: Int) { - super.emit(flag) - super.injectAndFinish() - } - - override fun onPageFinishedActions(url: String) { - if (!url.shouldInjectMenu) injectAndFinish() - } - -} \ 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 new file mode 100644 index 00000000..3e6ddd06 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -0,0 +1,172 @@ +package com.pitchedapps.frost.web + +import android.content.Context +import android.graphics.Bitmap +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.FACEBOOK_COM +import com.pitchedapps.frost.facebook.FB_URL_BASE +import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.injectors.* +import com.pitchedapps.frost.utils.* +import io.reactivex.subjects.Subject + +/** + * Created by Allan Wang on 2017-05-31. + * + * Collection of webview clients + */ + +/** + * The base of all webview clients + * Used to ensure that resources are properly intercepted + */ +open class BaseWebViewClient : WebViewClient() { + + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? + = shouldFrostInterceptRequest(view, request) + +} + +/** + * The default webview client + */ +open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient() { + + val refreshObservable: Subject = webCore.refreshObservable + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + L.i("FWV Loading $url") +// L.v("Cookies ${CookieManager.getInstance().getCookie(url)}") + refreshObservable.onNext(true) + if (!url.contains(FACEBOOK_COM)) return + if (url.contains("logout.php")) FbCookie.logout(Prefs.userId, { launchLogin(view.context) }) + else if (url.contains("login.php")) FbCookie.reset({ launchLogin(view.context) }) + } + + fun launchLogin(c: Context) { + if (c is MainActivity && c.cookies().isNotEmpty()) + c.launchNewTask(SelectorActivity::class.java, c.cookies()) + else + c.launchNewTask(LoginActivity::class.java) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + L.i("Page finished $url") + if (!url.contains(FACEBOOK_COM)) { + refreshObservable.onNext(false) + return + } + view.jsInject( + CssAssets.ROUND_ICONS.maybe(Prefs.showRoundedIcons), + CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!Prefs.showSuggestedFriends && Prefs.pro), + CssHider.ADS.maybe(!Prefs.showFacebookAds && Prefs.pro) + ) + onPageFinishedActions(url) + } + + open internal fun onPageFinishedActions(url: String) { + injectAndFinish() + } + + internal fun injectAndFinish() { + L.d("Page finished reveal") + webCore.jsInject(CssHider.HEADER, + Prefs.themeInjector, + callback = { + refreshObservable.onNext(false) + webCore.jsInject( + JsActions.LOGIN_CHECK, + JsAssets.CLICK_A.maybe(webCore.baseEnum != null && Prefs.overlayEnabled), + JsAssets.CONTEXT_A, + JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null) + ) + }) + } + + open fun handleHtml(html: String) { + L.d("Handle Html") + } + + open fun emit(flag: Int) { + L.d("Emit $flag") + } + + /** + * Helper to format the request and launch it + * returns true to override the url + */ + private fun launchRequest(request: WebResourceRequest): Boolean { + L.d("Launching ${request.url}") + webCore.context.launchWebOverlay(request.url.toString()) + return true + } + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + L.i("Url Loading ${request.url}") + val path = request.url.path ?: return super.shouldOverrideUrlLoading(view, request) + if (path.startsWith("/composer/")) return launchRequest(request) + return super.shouldOverrideUrlLoading(view, request) + } + +} + +/** + * Client variant for the menu view + */ +class FrostWebViewClientMenu(webCore: FrostWebViewCore) : FrostWebViewClient(webCore) { + + private val String.shouldInjectMenu + get() = when (removePrefix(FB_URL_BASE)) { + "settings", + "settings#", + "settings#!/settings?soft=bookmarks" -> true + else -> false + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + if (url.shouldInjectMenu) jsInject(JsAssets.MENU) + } + + override fun emit(flag: Int) { + super.emit(flag) + super.injectAndFinish() + } + + override fun onPageFinishedActions(url: String) { + if (!url.shouldInjectMenu) injectAndFinish() + } +} + +/** + * Headless client that injects content after a page load + * The JSI is meant to handle everything else + */ +class HeadlessWebViewClient(val tag: String, val postInjection: InjectorContract) : BaseWebViewClient() { + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + L.d("Headless Page $tag Started", url) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + L.d("Headless Page $tag Finished", url) + postInjection.inject(view) + } + + /** + * In addition to general filtration, we will also strip away css and images + */ + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? + = super.shouldInterceptRequest(view, request).filterCss(request).filterImage(request) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt deleted file mode 100644 index bcadf32a..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewSearch.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.pitchedapps.frost.web - -import android.annotation.SuppressLint -import android.content.Context -import android.view.View -import android.webkit.* -import ca.allanwang.kau.searchview.SearchItem -import ca.allanwang.kau.utils.gone -import com.pitchedapps.frost.facebook.FbTab -import com.pitchedapps.frost.facebook.USER_AGENT_BASIC -import com.pitchedapps.frost.injectors.JsAssets -import com.pitchedapps.frost.injectors.JsBuilder -import com.pitchedapps.frost.injectors.jsInject -import com.pitchedapps.frost.utils.L -import com.pitchedapps.frost.utils.Prefs -import io.reactivex.schedulers.Schedulers -import io.reactivex.subjects.PublishSubject -import org.jetbrains.anko.runOnUiThread -import org.jsoup.Jsoup -import java.util.concurrent.TimeUnit - -@SuppressLint("ViewConstructor") -/** - * Created by Allan Wang on 2017-06-25. - * - * A bare bone search view meant solely to extract data from the web - * This should be hidden - * Having a single webview allows us to avoid loading the whole page with each query - */ -class FrostWebViewSearch(context: Context, val contract: SearchContract) : WebView(context) { - - val searchSubject = PublishSubject.create() - - init { - gone() - setupWebview() - } - - /** - * Basic info of last search results, so we can check if the list has actually changed - * Contains the last item's href (search more) as well as the number of items found - * This holder is synchronized - */ - var previousResult: Pair = Pair(null, 0) - - fun saveResultFrame(result: List, String>>) { - synchronized(previousResult) { - previousResult = Pair(result.lastOrNull()?.second, result.size) - } - } - - @SuppressLint("SetJavaScriptEnabled") - fun setupWebview() { - settings.javaScriptEnabled = true - settings.userAgentString = USER_AGENT_BASIC - setLayerType(View.LAYER_TYPE_HARDWARE, null) - webViewClient = SearchWebViewClient() - webChromeClient = SearchChromeClient() - addJavascriptInterface(SearchJSI(), "Frost") - searchSubject.debounce(300, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread()) - .map { - Jsoup.parse(it).select("a:not([rel*='keywords(']):not([href=#])[rel]").map { - element -> - //split text into separate items - L.v("Search element ${element.attr("href")}") - val texts = element.select("div").map { (it.text()) }.filter { it.isNotBlank() } - val pair = Pair(texts, element.attr("href")) - L.v("Search element potential $pair") - pair - }.filter { it.first.isNotEmpty() } - } - .filter { content -> Pair(content.lastOrNull()?.second, content.size) != previousResult } - .subscribe { - content: List, String>> -> - saveResultFrame(content) - L.d("Search element count ${content.size}") - contract.emitSearchResponse(content.map { - (texts, href) -> - SearchItem(href, texts[0], texts.getOrNull(1)) - }) - } - reload() - } - - /** - * Toggles web activity - * Should be done in conjunction with showing/hiding the search view - */ - var pauseLoad: Boolean - get() = settings.blockNetworkLoads - set(value) { - context.runOnUiThread { settings.blockNetworkLoads = value } - } - - override fun reload() { - super.loadUrl(FbTab.SEARCH.url) - } - - /** - * Sets the input to have our given text, then dispatches the input event so the webpage recognizes it - */ - fun query(input: String) { - pauseLoad = false - L.d("Searching attempt", input) - JsBuilder().js("var e=document.getElementById('main-search-input');if(e){e.value='$input';var n=new Event('input',{bubbles:!0,cancelable:!0});e.dispatchEvent(n),e.dispatchEvent(new Event('focus'))}else console.log('Input field not found');").build().inject(this) - } - - /** - * Created by Allan Wang on 2017-05-31. - * - * Barebones client that does what [FrostWebViewSearch] needs - */ - inner class SearchWebViewClient : BaseWebViewClient() { - - override fun onPageFinished(view: WebView, url: String) { - super.onPageFinished(view, url) - L.i("Search Page finished $url") - view.jsInject(JsAssets.SEARCH) - } - - override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? - = super.shouldInterceptRequest(view, request).filterCss(request) - } - - class SearchChromeClient : WebChromeClient() { - - //mute console - override fun onConsoleMessage(consoleMessage: ConsoleMessage) = true - } - - inner class SearchJSI { - @JavascriptInterface - fun handleHtml(html: String) { - L.d("Search received response ${contract.isSearchOpened}") - if (!contract.isSearchOpened) pauseLoad = true - searchSubject.onNext(html) - } - - @JavascriptInterface - fun emit(flag: Int) { - when (flag) { - 0 -> { - L.d("Search loaded successfully") - } - 1 -> { //something is not found in the search view; this is effectively useless - L.eThrow("Search subject error; reverting to full overlay") - Prefs.searchBar = false - searchSubject.onComplete() - contract.searchOverlayDispose() - } - } - } - } - - /** - * Clear up some components - */ - fun dispose() { - searchSubject.onComplete() - } - - interface SearchContract { - fun searchOverlayDispose() - fun emitSearchResponse(items: List) - val isSearchOpened: Boolean - } -} - - - diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt new file mode 100644 index 00000000..0f3a12b6 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt @@ -0,0 +1,81 @@ +package com.pitchedapps.frost.web + +import android.annotation.SuppressLint +import android.app.job.JobParameters +import android.webkit.JavascriptInterface +import android.webkit.WebView +import ca.allanwang.kau.utils.gone +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.FbTab +import com.pitchedapps.frost.facebook.USER_AGENT_BASIC +import com.pitchedapps.frost.injectors.JsActions +import com.pitchedapps.frost.services.NotificationService +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.frostAnswersCustom +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.runOnUiThread + +@SuppressLint("ViewConstructor") +/** + * Created by Allan Wang on 2017-07-17. + * + * Bare boned headless view made solely to extract conversation info + */ +class MessageWebView(val service: NotificationService, val params: JobParameters?) : WebView(service) { + + init { + gone() + setupWebview() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebview() { + settings.javaScriptEnabled = true + settings.userAgentString = USER_AGENT_BASIC + webViewClient = HeadlessWebViewClient("MessageNotifs", JsActions.GET_MESSAGES) + webChromeClient = QuietChromeClient() + addJavascriptInterface(MessageJSI(), "Frost") + } + + private val startTime = System.currentTimeMillis() + private val endTime: Long by lazy { System.currentTimeMillis() } + private var inProgress = false + private val pendingRequests: MutableList = mutableListOf() + private lateinit var data: CookieModel + + fun request(data: CookieModel) { + pendingRequests.add(data) + if (inProgress) return + inProgress = true + load(data) + } + + private fun load(data: CookieModel) { + L.d("Notif retrieving messages", data.toString()) + this.data = data + FbCookie.setWebCookie(data.cookie) { context.runOnUiThread { L.d("Notif messages load"); loadUrl(FbTab.MESSAGES.url) } } + } + + inner class MessageJSI { + @JavascriptInterface + fun handleHtml(html: String) { + L.d("Notif messages received", data.toString()) + doAsync { service.fetchMessageNotifications(data, html) } + pendingRequests.remove(data) + if (pendingRequests.isEmpty()) { + val time = endTime - startTime + L.d("Notif messages finished $time") + frostAnswersCustom("Notifications") { + putCustomAttribute("Message retrieval duration", time) + } + post { destroy() } + service.jobFinished(params, false) + service.future = null + } else { + load(pendingRequests.first()) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt new file mode 100644 index 00000000..325d0333 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt @@ -0,0 +1,145 @@ +package com.pitchedapps.frost.web + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import android.webkit.JavascriptInterface +import android.webkit.WebView +import ca.allanwang.kau.searchview.SearchItem +import ca.allanwang.kau.utils.gone +import com.pitchedapps.frost.facebook.FbTab +import com.pitchedapps.frost.facebook.USER_AGENT_BASIC +import com.pitchedapps.frost.injectors.JsAssets +import com.pitchedapps.frost.injectors.JsBuilder +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.Prefs +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import org.jetbrains.anko.runOnUiThread +import org.jsoup.Jsoup +import java.util.concurrent.TimeUnit + +@SuppressLint("ViewConstructor") +/** + * Created by Allan Wang on 2017-06-25. + * + * A bare bone headless search view meant solely to extract search results from the web + * Having a single webview allows us to avoid loading the whole page with each query + */ +class SearchWebView(context: Context, val contract: SearchContract) : WebView(context) { + + val searchSubject = PublishSubject.create() + + init { + gone() + setupWebview() + } + + /** + * Basic info of last search results, so we can check if the list has actually changed + * Contains the last item's href (search more) as well as the number of items found + * This holder is synchronized + */ + var previousResult: Pair = Pair(null, 0) + + fun saveResultFrame(result: List, String>>) { + synchronized(previousResult) { + previousResult = Pair(result.lastOrNull()?.second, result.size) + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebview() { + settings.javaScriptEnabled = true + settings.userAgentString = USER_AGENT_BASIC + webViewClient = HeadlessWebViewClient("Search", JsAssets.SEARCH) + webChromeClient = QuietChromeClient() + addJavascriptInterface(SearchJSI(), "Frost") + searchSubject.debounce(300, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread()) + .map { + Jsoup.parse(it).select("a:not([rel*='keywords(']):not([href=#])[rel]").map { + element -> + //split text into separate items + L.v("Search element ${element.attr("href")}") + val texts = element.select("div").map { (it.text()) }.filter { it.isNotBlank() } + val pair = Pair(texts, element.attr("href")) + L.v("Search element potential $pair") + pair + }.filter { it.first.isNotEmpty() } + } + .filter { content -> Pair(content.lastOrNull()?.second, content.size) != previousResult } + .subscribe { + content: List, String>> -> + saveResultFrame(content) + L.d("Search element count ${content.size}") + contract.emitSearchResponse(content.map { + (texts, href) -> + SearchItem(href, texts[0], texts.getOrNull(1)) + }) + } + reload() + } + + /** + * Toggles web activity + * Should be done in conjunction with showing/hiding the search view + */ + var pauseLoad: Boolean + get() = settings.blockNetworkLoads + set(value) { + context.runOnUiThread { settings.blockNetworkLoads = value } + } + + override fun reload() { + super.loadUrl(FbTab.SEARCH.url) + } + + /** + * Sets the input to have our given text, then dispatches the input event so the webpage recognizes it + */ + fun query(input: String) { + pauseLoad = false + L.d("Searching attempt", input) + JsBuilder().js("var e=document.getElementById('main-search-input');if(e){e.value='$input';var n=new Event('input',{bubbles:!0,cancelable:!0});e.dispatchEvent(n),e.dispatchEvent(new Event('focus'))}else console.log('Input field not found');").build().inject(this) + } + + inner class SearchJSI { + @JavascriptInterface + fun handleHtml(html: String) { + L.d("Search received response ${contract.isSearchOpened}") + if (!contract.isSearchOpened) pauseLoad = true + searchSubject.onNext(html) + } + + @JavascriptInterface + fun emit(flag: Int) { + when (flag) { + 0 -> { + L.d("Search loaded successfully") + } + 1 -> { //something is not found in the search view; this is effectively useless + L.eThrow("Search subject error; reverting to full overlay") + Prefs.searchBar = false + searchSubject.onComplete() + contract.searchOverlayDispose() + } + } + } + } + + /** + * Clear up some components + */ + fun dispose() { + searchSubject.onComplete() + } + + interface SearchContract { + fun searchOverlayDispose() + fun emitSearchResponse(items: List) + val isSearchOpened: Boolean + } +} + + + diff --git a/app/src/main/play/en-CA/listing/fulldescription b/app/src/main/play/en-CA/listing/fulldescription index 61c94833..32cf0503 100644 --- a/app/src/main/play/en-CA/listing/fulldescription +++ b/app/src/main/play/en-CA/listing/fulldescription @@ -3,6 +3,7 @@ While being a web wrapper, Frost contains many unique and native features such a • True multi user interactions - More than just an option in a settings menu, Frost's account switcher is right in the drawer. You are one tap away from switching accounts, and everything refreshes on the switch so that you can view other accounts instantaneously. Furthermore, the notification service will fetch notifications from all accounts, and will let you know which account has the new notification. • Better multitasking - Frost contains an overlaying web browser that can be drawn on top of your foreground task. Open links and notifications with a full screen view, then swipe away to get back to your previous task. +• Contextual awareness - Frost integrates additional features via long presses. Need to copy a block of text or share a link? Long press the text. Need to zoom into an image or download it? Long press the image! • Material Design - Built for lollipop and up, Frost focuses strongly on a good UI, and embraces material transitions and dimensions. • Complete theme engine - Frost contains very comprehensive themes that customize all components of the app. Frost is also the only app to support transparent themes. • Fully opened - Nothing speaks for privacy more than being open sourced. Frost is proud to be one of those apps, and can be found on github (Link in the app's about section) @@ -11,7 +12,9 @@ Permissions used and why: • Internet, Network State, Wifi State - Frost fetches the pages from Facebook's mobile website. It also needs the network state so as to limit internet usage when you are on a metered network. • Receive Boot Completed - Frost notifications persist on reboot, and need this permission to be added each time. -• Read external storage - Needed to upload photos in a new status +• Read/write external storage - Needed to upload photos in a new status and save photos when prompted +• Vibrate - Needed to vibrate phone for notifications; this can be toggled in the settings +• Billing - For purchasing pro and unlocking all of Frost's features • That's it! No privacy intrusion and no extra demands. Permissions NOT used and why: diff --git a/app/src/main/res/xml/changelog.xml b/app/src/main/res/xml/changelog.xml index cf56eaeb..cb1f5f64 100644 --- a/app/src/main/res/xml/changelog.xml +++ b/app/src/main/res/xml/changelog.xml @@ -9,16 +9,18 @@ --> - - - - - + + + + + + + -- cgit v1.2.3