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 --- .../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 +++++++++++++++++ 14 files changed, 519 insertions(+), 409 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 (limited to 'app/src/main/kotlin/com') 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 + } +} + + + -- cgit v1.2.3