diff options
author | Allan Wang <me@allanwang.ca> | 2017-12-26 03:37:32 -0500 |
---|---|---|
committer | Allan Wang <me@allanwang.ca> | 2017-12-26 03:39:28 -0500 |
commit | 1769dbcef9786b847ffeaebdf6ecced45da9222c (patch) | |
tree | 50f543469607b608c0b62ca8a63c6a65325802b9 /app/src/main/kotlin | |
parent | 8080d43dbd9a6863fadb86594b179d0919d6215b (diff) | |
download | frost-1769dbcef9786b847ffeaebdf6ecced45da9222c.tar.gz frost-1769dbcef9786b847ffeaebdf6ecced45da9222c.tar.bz2 frost-1769dbcef9786b847ffeaebdf6ecced45da9222c.zip |
Enhancement/fb requests (#575)v1.7.2
* Update lambdas to references
* Simplify regex and parsers
* Fix some parsing and add more tests
* Improve message parser and tests
* Simplify parser
* Shorten interfaces
* Push rem
* Create notification parser
* Clean up notification service
* Clean up notification service
* Add safe cookie fallback
* Fix cookie reference
* Make parsers only hold cookie string
* Clean up cookie references
* Fix up login and event theme
* Update changelog
Remove workspace backup
Diffstat (limited to 'app/src/main/kotlin')
15 files changed, 457 insertions, 279 deletions
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 80d248bc..8f932a94 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -58,6 +58,7 @@ import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.fragments.BaseFragment +import com.pitchedapps.frost.parsers.FrostSearch import com.pitchedapps.frost.parsers.SearchParser import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.utils.iab.FrostBilling @@ -127,6 +128,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, onCreateBilling() } + fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) { (0 until tabs.tabCount).asSequence().forEach { i -> action(i, tabs.getTabAt(i)!!.customView as BadgedIcon) @@ -193,7 +195,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, -3L -> launchNewTask(LoginActivity::class.java, clearStack = false) -4L -> launchNewTask(SelectorActivity::class.java, cookies(), false) else -> { - FbCookie.switchUser(profile.identifier, { refreshAll() }) + FbCookie.switchUser(profile.identifier, this@BaseMainActivity::refreshAll) tabsForEachView { _, view -> view.badgeText = null } } } @@ -248,7 +250,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, onClick { _ -> onClick(); false } } - fun refreshAll() { + private fun refreshAll() { fragmentSubject.onNext(REQUEST_REFRESH) } @@ -266,8 +268,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, runOnUiThread { searchView?.results = results } else doAsync { - val data = SearchParser.query(query) ?: return@doAsync - val items = data.map { SearchItem(it.href, it.title, it.description) }.toMutableList() + val data = SearchParser.query(FbCookie.webCookie, query)?.data?.results ?: return@doAsync + val items = data.map(FrostSearch::toSearchItem).toMutableList() if (items.isNotEmpty()) items.add(SearchItem("${FbItem._SEARCH.url}?q=$query", string(R.string.show_all_results), iicon = null)) searchViewCache.put(query, items) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 0d6cce07..e2f7a3d2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -25,10 +25,9 @@ import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.web.LoginWebView -import io.reactivex.Observable +import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.BiFunction -import io.reactivex.internal.operators.single.SingleToObservable import io.reactivex.subjects.SingleSubject @@ -37,18 +36,18 @@ import io.reactivex.subjects.SingleSubject */ class LoginActivity : BaseActivity() { - val toolbar: Toolbar by bindView(R.id.toolbar) - val web: LoginWebView by bindView(R.id.login_webview) - val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) - val textview: AppCompatTextView by bindView(R.id.textview) - val profile: ImageView by bindView(R.id.profile) + private val toolbar: Toolbar by bindView(R.id.toolbar) + private val web: LoginWebView by bindView(R.id.login_webview) + private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) + private val textview: AppCompatTextView by bindView(R.id.textview) + private val profile: ImageView by bindView(R.id.profile) - val profileObservable = SingleSubject.create<Boolean>() - val usernameObservable = SingleSubject.create<String>() - lateinit var profileLoader: RequestManager + private val profileSubject = SingleSubject.create<Boolean>() + private val usernameSubject = SingleSubject.create<String>() + private lateinit var profileLoader: RequestManager // Helper to set and enable swipeRefresh - var refresh: Boolean + private var refresh: Boolean get() = swipeRefresh.isRefreshing set(value) { if (value) swipeRefresh.isEnabled = true @@ -73,10 +72,12 @@ class LoginActivity : BaseActivity() { profileLoader = Glide.with(profile) } - fun loadInfo(cookie: CookieModel) { + private fun loadInfo(cookie: CookieModel) { refresh = true - Observable.zip(SingleToObservable(profileObservable), SingleToObservable(usernameObservable), - BiFunction<Boolean, String, Pair<Boolean, String>> { foundImage, name -> Pair(foundImage, name) }) + Single.zip<Boolean, String, Pair<Boolean, String>>( + profileSubject, + usernameSubject, + BiFunction(::Pair)) .observeOn(AndroidSchedulers.mainThread()).subscribe { (foundImage, name) -> refresh = false if (!foundImage) { @@ -85,7 +86,11 @@ class LoginActivity : BaseActivity() { } textview.text = String.format(getString(R.string.welcome), name) textview.fadeIn() - frostAnswers { logLogin(LoginEvent().putMethod("frost_browser").putSuccess(true)) } + frostAnswers { + logLogin(LoginEvent() + .putMethod("frost_browser") + .putSuccess(true)) + } /* * The user may have logged into an account that is already in the database * We will let the db handle duplicates and load it now after the new account has been saved @@ -102,23 +107,23 @@ class LoginActivity : BaseActivity() { } - fun loadProfile(id: Long) { + private fun loadProfile(id: Long) { profileLoader.load(PROFILE_PICTURE_URL(id)).withRoundIcon().listener(object : RequestListener<Drawable> { override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { - profileObservable.onSuccess(true) + profileSubject.onSuccess(true) return false } override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean { e.logFrostAnswers("Profile loading exception") - profileObservable.onSuccess(false) + profileSubject.onSuccess(false) return false } }).into(profile) } - fun loadUsername(cookie: CookieModel) { - cookie.fetchUsername { usernameObservable.onSuccess(it) } + private fun loadUsername(cookie: CookieModel) { + cookie.fetchUsername(usernameSubject::onSuccess) } override fun backConsumer(): Boolean { @@ -129,4 +134,14 @@ class LoginActivity : BaseActivity() { return false } + override fun onResume() { + super.onResume() + web.resumeTimers() + } + + override fun onPause() { + web.pauseTimers() + super.onPause() + } + }
\ No newline at end of file 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 e46a4bfb..14d7ae09 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -1,5 +1,6 @@ package com.pitchedapps.frost.contracts +import com.pitchedapps.frost.dbflow.CookieModel import io.reactivex.subjects.PublishSubject /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt index 39e8c467..8d625582 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt @@ -13,8 +13,17 @@ package com.pitchedapps.frost.facebook * 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=\"(.*?)\"") } +val FB_REV_MATCHER: Regex by lazy{Regex("\"app_version\":\"(.*?)\"")} /** * Matches user id from cookie */ -val FB_USER_MATCHER: Regex by lazy { Regex("c_user=([0-9]*);") }
\ No newline at end of file +val FB_USER_MATCHER: Regex by lazy { Regex("c_user=([0-9]*);") } + +val FB_EPOCH_MATCHER: Regex by lazy { Regex(":([0-9]+)") } +val FB_NOTIF_ID_MATCHER: Regex by lazy { Regex("notif_id\":([0-9]+)") } +val FB_MESSAGE_NOTIF_ID_MATCHER: Regex by lazy { Regex("[thread|user]_fbid_([0-9]+)") } +val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|'](.*?)[\"|']\\)") } + +operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex) + diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt index 428043a0..2fa20917 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt @@ -11,7 +11,13 @@ 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 = "") +data class RequestAuth(val userId: Long = -1, + val cookie: String = "", + val fb_dtsg: String = "", + val rev: String = "") { + val isValid + get() = userId > 0 && cookie.isNotEmpty() && fb_dtsg.isNotEmpty() && rev.isNotEmpty() +} private val client: OkHttpClient by lazy { val builder = OkHttpClient.Builder() @@ -21,7 +27,7 @@ private val client: OkHttpClient by lazy { builder.build() } -private fun List<Pair<String, Any?>>.toForm(): RequestBody { +private fun List<Pair<String, Any?>>.toForm(): FormBody { val builder = FormBody.Builder() forEach { (key, value) -> val v = value?.toString() ?: "" @@ -30,6 +36,12 @@ private fun List<Pair<String, Any?>>.toForm(): RequestBody { return builder.build() } +private fun List<Pair<String, Any?>>.withEmptyData(vararg key: String): List<Pair<String, Any?>> { + val newList = toMutableList() + newList.addAll(key.map { it to null }) + return newList +} + private fun String.requestBuilder() = Request.Builder() .header("Cookie", this) .header("User-Agent", USER_AGENT_BASIC) @@ -38,25 +50,33 @@ private fun String.requestBuilder() = Request.Builder() private fun Request.Builder.call() = client.newCall(build()) -fun Pair<Long, String>.getAuth(): RequestAuth? { +fun Pair<Long, String>.getAuth(): RequestAuth { val (userId, cookie) = this + var auth = RequestAuth(userId, cookie) val call = cookie.requestBuilder() - .url(FB_URL_BASE) + .url("https://touch.facebook.com") .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) + val fb_dtsg = FB_DTSG_MATCHER.find(text)[1] if (fb_dtsg != null) { L.d(null, "fb_dtsg for $userId: $fb_dtsg") - return RequestAuth(userId, cookie, fb_dtsg) + auth = auth.copy(fb_dtsg = fb_dtsg) + if (auth.isValid) return auth + } + + val rev = FB_REV_MATCHER.find(text)[1] + if (rev != null) { + L.d(null, "rev for $userId: $rev") + auth = auth.copy(rev = rev) + if (auth.isValid) return auth } } } - return null + return auth } fun RequestAuth.markNotificationRead(notifId: Long): Call { @@ -65,13 +85,9 @@ fun RequestAuth.markNotificationRead(notifId: Long): Call { "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 - ) + ).withEmptyData("m_sess", "__dyn", "__req", "__ajax__") return cookie.requestBuilder() .url("${FB_URL_BASE}a/jewel_notifications_log.php") @@ -96,3 +112,18 @@ fun RequestAuth.markNotificationsRead(vararg notifId: Long) = zip<Long, Boolean, response.body()?.charStream()?.read(buffer) ?: return@zip false !buffer.toString().contains("error") } + +/** + * Execute the call and attempt to check validity + */ +fun Call.executeAndCheck(): Boolean { + val body = execute().body() ?: return false + var empty = true + body.charStream().useLines { + it.forEach { + if (empty && it.isNotEmpty()) empty = false + if (it.contains("error")) return true + } + } + return !empty +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt index 498164c0..58d9ebd4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt @@ -13,7 +13,9 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.contracts.DynamicUiContract import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.contracts.MainActivityContract +import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.enums.FeedSort +import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.parsers.FrostParser import com.pitchedapps.frost.utils.* @@ -28,6 +30,9 @@ import org.jetbrains.anko.toast /** * Created by Allan Wang on 2017-11-07. + * + * All fragments pertaining to the main view + * Must be attached to activities implementing [MainActivityContract] */ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract { @@ -59,6 +64,12 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract { protected abstract val layoutRes: Int + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (context !is MainActivityContract) + throw IllegalArgumentException("${this::class.java.simpleName} is not attached to a context implementing MainActivityContract") + } + override final fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layoutRes, container, false) val content = view as? FrostContentParent @@ -162,7 +173,7 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract { override fun onTabClick(): Unit = content?.core?.onTabClicked() ?: Unit } -abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), RecyclerContentContract { +abstract class RecyclerFragment<T : Any, Item : IItem<*, *>> : BaseFragment(), RecyclerContentContract { override val layoutRes: Int = R.layout.view_content_recycler @@ -199,7 +210,7 @@ abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), Recycle progress(10) val doc = frostJsoup(baseUrl) progress(60) - val data = parser.parse(doc) + val data = parser.parse(FbCookie.webCookie, doc) if (data == null) { context?.toast(R.string.error_generic) L.eThrow("RecyclerFragment failed for ${baseEnum.name}") @@ -207,7 +218,7 @@ abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), Recycle return@doAsync callback(false) } progress(80) - val items = toItems(data) + val items = toItems(data.data) progress(97) adapter.setNewList(items) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt index 62b1de33..00429730 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt @@ -5,6 +5,7 @@ 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.dbflow.CookieModel import com.pitchedapps.frost.views.FrostRecyclerView import io.reactivex.disposables.Disposable diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt index 186633e5..016f33e8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt @@ -1,6 +1,14 @@ package com.pitchedapps.frost.parsers +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER +import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.facebook.get +import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.utils.frostJsoup +import org.jsoup.Jsoup import org.jsoup.nodes.Document +import org.jsoup.nodes.Element /** * Created by Allan Wang on 2017-10-06. @@ -13,80 +21,88 @@ import org.jsoup.nodes.Document * The return type must be nonnull if no parsing errors occurred, as null signifies a parse error * If null really must be allowed, use Optionals */ -interface FrostParser<T> { +interface FrostParser<out T : Any> { + /** - * Extracts data from the JSoup document - * In some cases, the document can be created directly from a connection - * In other times, it needs to be created from scripts, which otherwise - * won't be parsed + * Url to request from */ - fun parse(doc: Document): T? + val url: String /** - * Parse a String input + * Call parsing with default implementation using cookie */ - fun parse(text: String?): T? + fun parse(cookie: String?): ParseResponse<T>? /** - * Take in doc and emit debug output + * Call parsing with given document */ - fun debug(doc: Document): String + fun parse(cookie: String?, document: Document): ParseResponse<T>? /** - * Attempts to parse input and emit a debugger + * Call parsing with given data */ - fun debug(text: String?): String + fun parseFromData(cookie: String?, text: String): ParseResponse<T>? + +} + +data class FrostLink(val text: String, val href: String) + +data class ParseResponse<out T>(val cookie: String, val data: T) { + override fun toString() = "ParseResponse\ncookie: $cookie\ndata:\n$data" } -internal abstract class FrostParserBase<T> : FrostParser<T> { +interface ParseNotification { + fun getUnreadNotifications(data: CookieModel): List<NotificationContent> +} + +internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder().apply { + val tabs = "\t".repeat(indent) + append("$tabs$tag: [\n\t$tabs") + append(this@toJsonString.joinToString("\n\t$tabs")) + append("\n$tabs]\n") +}.toString() + +/** + * T should have a readable toString() function + * [redirectToText] dictates whether all data should be converted to text then back to document before parsing + */ +internal abstract class FrostParserBase<out T : Any>(private val redirectToText: Boolean) : FrostParser<T> { + + override final fun parse(cookie: String?) = parse(cookie, frostJsoup(cookie, url)) - override final fun parse(text: String?): T? { - text ?: return null + override final fun parseFromData(cookie: String?, text: String): ParseResponse<T>? { + cookie ?: return null val doc = textToDoc(text) ?: return null - return parse(doc) + val data = parseImpl(doc) ?: return null + return ParseResponse(cookie, data) } - protected abstract fun textToDoc(text: String): Document? - - override fun debug(text: String?): String { - val result = mutableListOf<String>() - result.add("Testing parser for ${this::class.java.simpleName}") - if (text == null) { - result.add("Null text input") - return result.joinToString("\n") - } - val doc = textToDoc(text) - if (doc == null) { - result.add("Null document from text") - return result.joinToString("\n") - } - return debug(doc, result) + override fun parse(cookie: String?, document: Document): ParseResponse<T>? { + cookie ?: return null + if (redirectToText) + return parseFromData(cookie, document.toString()) + val data = parseImpl(document) ?: return null + return ParseResponse(cookie, data) } - override final fun debug(doc: Document): String { - val result = mutableListOf<String>() - result.add("Testing parser for ${this::class.java.simpleName}") - return debug(doc, result) - } + protected abstract fun parseImpl(doc: Document): T? - private fun debug(doc: Document, result: MutableList<String>): String { - val output = parse(doc) - if (output == null) { - result.add("Output is null") - return result.joinToString("\n") - } else { - result.add("Output is not null") - } - debugImpl(output, result) - return result.joinToString("\n") - } + // protected abstract fun parse(doc: Document): T? - protected abstract fun debugImpl(data: T, result: MutableList<String>) -} + /** + * Attempts to find inner <i> element with some style containing a url + * Returns the formatted url, or an empty string if nothing was found + */ + protected fun Element.getInnerImgStyle() = + FB_CSS_URL_MATCHER.find(select("i.img[style*=url]").attr("style"))[1]?.formattedFbUrl ?: "" -object FrostRegex { - val epoch = Regex(":([0-9]+)") - val notifId = Regex("notif_id\":([0-9]+)") - val messageNotifId = Regex("thread_fbid_([0-9]+)") - val profilePicture = Regex("url\\(\"(.*?)\"\\)") + protected open fun textToDoc(text: String) = if (!redirectToText) + Jsoup.parse(text) + else + throw RuntimeException("${this::class.java.simpleName} requires text redirect but did not implement textToDoc") + + protected fun parseLink(element: Element?): FrostLink? { + val a = element?.getElementsByTag("a")?.first() ?: return null + return FrostLink(a.text(), a.attr("href")) + } }
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt index 9430407d..9d4a2193 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt @@ -1,6 +1,8 @@ package com.pitchedapps.frost.parsers -import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.utils.L import org.apache.commons.text.StringEscapeUtils import org.jsoup.Jsoup @@ -14,13 +16,55 @@ import org.jsoup.nodes.Element * We can parse out the content we want directly and load it ourselves * */ -object MessageParser : FrostParser<Triple<List<FrostThread>, FrostLink?, List<FrostLink>>> by MessageParserImpl() +object MessageParser : FrostParser<FrostMessages> by MessageParserImpl() -data class FrostThread(val id: Int, val img: String, val title: String, val time: Long, val url: String, val unread: Boolean, val content: String?) +data class FrostMessages(val threads: List<FrostThread>, + val seeMore: FrostLink?, + val extraLinks: List<FrostLink> +) : ParseNotification { + override fun toString() = StringBuilder().apply { + append("FrostMessages {\n") + append(threads.toJsonString("threads", 1)) + append("\tsee more: $seeMore\n") + append(extraLinks.toJsonString("extra links", 1)) + append("}") + }.toString() -data class FrostLink(val text: String, val href: String) + override fun getUnreadNotifications(data: CookieModel) = + threads.filter(FrostThread::unread).map { + with(it) { + NotificationContent( + data = data, + notifId = Math.abs(id.toInt()), + href = url, + title = title, + text = content ?: "", + timestamp = time, + profileUrl = img + ) + } + } +} + +/** + * [id] user/thread id, or current time fallback + * [img] parsed url for profile img + * [time] time of message + * [url] link to thread + * [unread] true if image is unread, false otherwise + * [content] optional string for thread + */ +data class FrostThread(val id: Long, + val img: String, + val title: String, + val time: Long, + val url: String, + val unread: Boolean, + val content: String?) -private class MessageParserImpl : FrostParserBase<Triple<List<FrostThread>, FrostLink?, List<FrostLink>>>() { +private class MessageParserImpl : FrostParserBase<FrostMessages>(true) { + + override val url = FbItem.MESSAGES.url override fun textToDoc(text: String): Document? { var content = StringEscapeUtils.unescapeEcmaScript(text) @@ -39,32 +83,29 @@ private class MessageParserImpl : FrostParserBase<Triple<List<FrostThread>, Fros return Jsoup.parseBodyFragment("<div $content") } - override fun parse(doc: Document): Triple<List<FrostThread>, FrostLink?, List<FrostLink>>? { - val threadList = doc.getElementById("threadlist_rows") + override fun parseImpl(doc: Document): FrostMessages? { + val threadList = doc.getElementById("threadlist_rows") ?: return null val threads: List<FrostThread> = threadList.getElementsByAttributeValueContaining("id", "thread_fbid_") - .mapNotNull { parseMessage(it) } + .mapNotNull(this::parseMessage) val seeMore = parseLink(doc.getElementById("see_older_threads")) val extraLinks = threadList.nextElementSibling().select("a") - .mapNotNull { parseLink(it) } - return Triple(threads, seeMore, extraLinks) + .mapNotNull(this::parseLink) + return FrostMessages(threads, seeMore, extraLinks) } private fun parseMessage(element: Element): FrostThread? { val a = element.getElementsByTag("a").first() ?: return null val abbr = element.getElementsByTag("abbr") - val epoch = FrostRegex.epoch.find(abbr.attr("data-store")) - ?.groupValues?.getOrNull(1)?.toLongOrNull() ?: -1L + val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L //fetch id - val id = FrostRegex.messageNotifId.find(element.id()) - ?.groupValues?.getOrNull(1)?.toLongOrNull() ?: System.currentTimeMillis() + val id = FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull() + ?: System.currentTimeMillis() val content = element.select("span.snippet").firstOrNull()?.text()?.trim() - //fetch convo pic - val p = element.select("i.img[style*=url]") - val pUrl = FrostRegex.profilePicture.find(p.attr("style"))?.groups?.get(1)?.value?.formattedFbUrl ?: "" + val img = element.getInnerImgStyle() L.v("url", a.attr("href")) return FrostThread( - id = id.toInt(), - img = pUrl.formattedFbUrl, + id = id, + img = img, title = a.text(), time = epoch, url = a.attr("href").formattedFbUrl, @@ -73,15 +114,4 @@ private class MessageParserImpl : FrostParserBase<Triple<List<FrostThread>, Fros ) } - private fun parseLink(element: Element?): FrostLink? { - val a = element?.getElementsByTag("a")?.first() ?: return null - return FrostLink(a.text(), a.attr("href")) - } - - override fun debugImpl(data: Triple<List<FrostThread>, FrostLink?, List<FrostLink>>, result: MutableList<String>) { - result.addAll(data.first.map(FrostThread::toString)) - result.add("See more link:") - result.add("\t${data.second}") - result.addAll(data.third.map(FrostLink::toString)) - } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt new file mode 100644 index 00000000..f743a43a --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt @@ -0,0 +1,92 @@ +package com.pitchedapps.frost.parsers + +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.utils.L +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +/** + * Created by Allan Wang on 2017-12-25. + * + */ +object NotifParser : FrostParser<FrostNotifs> by NotifParserImpl() + +data class FrostNotifs( + val notifs: List<FrostNotif>, + val seeMore: FrostLink? +) : ParseNotification { + override fun toString() = StringBuilder().apply { + append("FrostNotifs {\n") + append(notifs.toJsonString("notifs", 1)) + append("\tsee more: $seeMore\n") + append("}") + }.toString() + + override fun getUnreadNotifications(data: CookieModel) = + notifs.filter(FrostNotif::unread).map { + with(it) { + NotificationContent( + data = data, + notifId = Math.abs(id.toInt()), + href = url, + title = null, + text = content ?: "", + timestamp = time, + profileUrl = img + ) + } + } +} + +/** + * [id] notif id, or current time fallback + * [img] parsed url for profile img + * [time] time of message + * [url] link to thread + * [unread] true if image is unread, false otherwise + * [content] optional string for thread + */ +data class FrostNotif(val id: Long, + val img: String, + val time: Long, + val url: String, + val unread: Boolean, + val content: String?) + +private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) { + + override val url = FbItem.NOTIFICATIONS.url + + override fun parseImpl(doc: Document): FrostNotifs? { + val notificationList = doc.getElementById("notifications_list") ?: return null + val notifications = notificationList.getElementsByAttributeValueContaining("id", "list_notif_") + .mapNotNull { parseNotif(it) } + val seeMore = parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first()) + return FrostNotifs(notifications, seeMore) + } + + private fun parseNotif(element: Element): FrostNotif? { + val a = element.getElementsByTag("a").first() ?: return null + val abbr = element.getElementsByTag("abbr") + val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L + //fetch id + val id = FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull() + ?: System.currentTimeMillis() + val img = element.getInnerImgStyle() + val timeString = abbr.text() + val content = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() //remove + L.v("url", a.attr("href")) + return FrostNotif( + id = id, + img = img, + time = epoch, + url = a.attr("href").formattedFbUrl, + unread = !element.hasClass("acw"), + content = content + ) + } + + +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt index 908bb153..bc09d4db 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt @@ -1,8 +1,10 @@ package com.pitchedapps.frost.parsers -import ca.allanwang.kau.utils.withMaxLength +import ca.allanwang.kau.searchview.SearchItem +import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.parsers.FrostSearch.Companion.create import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostJsoup import org.jsoup.Jsoup @@ -12,11 +14,11 @@ import org.jsoup.nodes.Element /** * Created by Allan Wang on 2017-10-09. */ -object SearchParser : FrostParser<List<FrostSearch>> by SearchParserImpl() { - fun query(input: String): List<FrostSearch>? { +object SearchParser : FrostParser<FrostSearches> by SearchParserImpl() { + fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? { val url = "${FbItem._SEARCH.url}?q=${if (input.isNotBlank()) input else "a"}" L.i(null, "Search Query $url") - return parse(frostJsoup(url)) + return parse(cookie, frostJsoup(url)) } } @@ -25,25 +27,40 @@ enum class SearchKeys(val key: String) { EVENTS("keywords_events") } +data class FrostSearches(val results: List<FrostSearch>) { + + override fun toString() = StringBuilder().apply { + append("FrostSearches {\n") + append(results.toJsonString("results", 1)) + append("}") + }.toString() +} + /** * As far as I'm aware, all links are independent, so the queries don't matter * A lot of it is tracking information, which I'll strip away * Other text items are formatted for safety + * + * Note that it's best to create search results from [create] */ -class FrostSearch(href: String, title: String, description: String?) { - val href = with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) } - val title = title.format() - val description = description?.format() +data class FrostSearch(val href: String, val title: String, val description: String?) { - private fun String.format() = replace("\n", " ").withMaxLength(50) - - override fun toString(): String - = "FrostSearch(href=$href, title=$title, description=$description)" + fun toSearchItem() = SearchItem(href, title, description) + companion object { + fun create(href: String, title: String, description: String?) = FrostSearch( + with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) }, + title.format(), + description?.format() + ) + } } -private class SearchParserImpl : FrostParserBase<List<FrostSearch>>() { - override fun parse(doc: Document): List<FrostSearch>? { +private class SearchParserImpl : FrostParserBase<FrostSearches>(false) { + + override val url = "${FbItem._SEARCH.url}?q=a" + + override fun parseImpl(doc: Document): FrostSearches? { val container: Element = doc.getElementById("BrowseResultsContainer") ?: doc.getElementById("root") ?: return null @@ -51,19 +68,11 @@ private class SearchParserImpl : FrostParserBase<List<FrostSearch>>() { * * Removed [data-store*=result_id] */ - return container.select("a.touchable[href]").filter(Element::hasText).map { - FrostSearch(it.attr("href").formattedFbUrl, + return FrostSearches(container.select("a.touchable[href]").filter(Element::hasText).map { + FrostSearch.create(it.attr("href").formattedFbUrl, it.select("._uoi").first()?.text() ?: "", it.select("._1tcc").first()?.text()) - }.filter { it.title.isNotBlank() } - } - - - override fun textToDoc(text: String): Document? = Jsoup.parse(text) - - override fun debugImpl(data: List<FrostSearch>, result: MutableList<String>) { - result.add("Has size ${data.size}") - result.addAll(data.map(FrostSearch::toString)) + }.filter { it.title.isNotBlank() }) } }
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 44b01bc3..afa30a91 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -24,12 +24,17 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.dbflow.NotificationModel +import com.pitchedapps.frost.dbflow.lastNotificationTime import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.formattedFbUrl -import com.pitchedapps.frost.parsers.FrostThread +import com.pitchedapps.frost.parsers.FrostParser +import com.pitchedapps.frost.parsers.MessageParser +import com.pitchedapps.frost.parsers.NotifParser +import com.pitchedapps.frost.parsers.ParseNotification import com.pitchedapps.frost.utils.* import org.jetbrains.anko.runOnUiThread +import java.util.* /** * Created by Allan Wang on 2017-07-08. @@ -88,23 +93,66 @@ class FrostNotificationTarget(val context: Context, * Enum to handle notification creations */ enum class NotificationType( - private val groupPrefix: String, private val overlayContext: OverlayContext, - private val contentRes: Int, - private val pendingUrl: String, + private val fbItem: FbItem, + private val parser: FrostParser<ParseNotification>, + private val getTime: (notif: NotificationModel) -> Long, + private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel, private val ringtone: () -> String) { - GENERAL("frost", OverlayContext.NOTIFICATION, R.string.notifications, FbItem.NOTIFICATIONS.url, { Prefs.notificationRingtone }), - MESSAGE("frost_im", OverlayContext.MESSAGE, R.string.messages, FbItem.MESSAGES.url, { Prefs.messageRingtone }); + GENERAL(OverlayContext.NOTIFICATION, + FbItem.NOTIFICATIONS, + NotifParser, + NotificationModel::epoch, + { notif, time -> notif.copy(epoch = time) }, + Prefs::notificationRingtone), + MESSAGE(OverlayContext.MESSAGE, + FbItem.MESSAGES, + MessageParser, + NotificationModel::epochIm, + { notif, time -> notif.copy(epochIm = time) }, + Prefs::messageRingtone); + + private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}" + + /** + * Get unread data from designated parser + * Display notifications for those after old epoch + * Save new epoch + */ + fun fetch(context: Context, data: CookieModel) { + val response = parser.parse(data.cookie) + ?: return L.eThrow("$name notification data not found") + val notifs = response.data.getUnreadNotifications(data) + if (notifs.isEmpty()) return + var notifCount = 0 + val userId = data.id + val prevNotifTime = lastNotificationTime(userId) + val prevLatestEpoch = getTime(prevNotifTime) + L.v("Notif $name prev epoch $prevLatestEpoch") + var newLatestEpoch = prevLatestEpoch + notifs.forEach { notif -> + L.v("Notif timestamp ${notif.timestamp}") + if (notif.timestamp <= prevLatestEpoch) return@forEach + createNotification(context, notif, notifCount == 0) + if (notif.timestamp > newLatestEpoch) + newLatestEpoch = notif.timestamp + notifCount++ + } + if (newLatestEpoch != prevLatestEpoch) + putTime(prevNotifTime, newLatestEpoch).save() + L.d("Notif $name new epoch ${getTime(lastNotificationTime(userId))}") + summaryNotification(context, userId, notifCount) + } /** * Create and submit a new notification with the given [content] * If [withDefaults] is set, it will also add the appropriate sound, vibration, and light * Note that when we have multiple notifications coming in at once, we don't want to have defaults for all of them */ - fun createNotification(context: Context, content: NotificationContent, withDefaults: Boolean) { + private fun createNotification(context: Context, content: NotificationContent, withDefaults: Boolean) { with(content) { val intent = Intent(context, FrostWebActivity::class.java) - intent.data = Uri.parse(href.formattedFbUrl) + intent.data = Uri.parse(href) intent.putExtra(ARG_USER_ID, data.id) intent.putExtra(ARG_OVERLAY_CONTEXT, overlayContext) val group = "${groupPrefix}_${data.id}" @@ -142,16 +190,16 @@ enum class NotificationType( * This will always produce sound, vibration, and lights based on preferences * and will only show if we have at least 2 notifications */ - fun summaryNotification(context: Context, userId: Long, count: Int) { + private fun summaryNotification(context: Context, userId: Long, count: Int) { frostAnswersCustom("Notifications", "Type" to name, "Count" to count) if (count <= 1) return val intent = Intent(context, FrostWebActivity::class.java) - intent.data = Uri.parse(pendingUrl) + intent.data = Uri.parse(fbItem.url) intent.putExtra(ARG_USER_ID, userId) val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) val notifBuilder = context.frostNotification.withDefaults(ringtone()) .setContentTitle(context.string(R.string.frost_name)) - .setContentText("$count ${context.string(contentRes)}") + .setContentText("$count ${context.string(fbItem.titleId)}") .setGroup("${groupPrefix}_$userId") .setGroupSummary(true) .setContentIntent(pendingIntent) @@ -167,13 +215,10 @@ enum class NotificationType( data class NotificationContent(val data: CookieModel, val notifId: Int, val href: String, - val title: String? = null, + val title: String? = null, // defaults to frost title val text: String, val timestamp: Long, - val profileUrl: String) { - constructor(data: CookieModel, thread: FrostThread) - : this(data, thread.id, thread.url, thread.title, thread.content ?: "", thread.time, thread.img) -} + val profileUrl: String) const val NOTIFICATION_PERIODIC_JOB = 7 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 c4ab6161..adeefec6 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -7,18 +7,11 @@ import android.support.v4.app.NotificationManagerCompat import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R -import com.pitchedapps.frost.dbflow.CookieModel -import com.pitchedapps.frost.dbflow.lastNotificationTime import com.pitchedapps.frost.dbflow.loadFbCookiesSync -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.formattedFbUrl -import com.pitchedapps.frost.parsers.MessageParser import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostAnswersCustom -import com.pitchedapps.frost.utils.frostJsoup import org.jetbrains.anko.doAsync -import org.jsoup.nodes.Element import java.util.concurrent.Future /** @@ -27,8 +20,7 @@ import java.util.concurrent.Future * Service to manage notifications * Will periodically check through all accounts in the db and send notifications when appropriate * - * Note that general notifications are parsed directly with Jsoup, - * but instant messages are done so with a headless webview as it is generated from JS + * All fetching is done through parsers */ class NotificationService : JobService() { @@ -36,13 +28,6 @@ class NotificationService : JobService() { val startTime = System.currentTimeMillis() - companion object { - val epochMatcher: Regex by lazy { Regex(":([0-9]*?),") } - val notifIdMatcher: Regex by lazy { Regex("notif_id\":([0-9]*?),") } - val messageNotifIdMatcher: Regex by lazy { Regex("thread_fbid_([0-9]+)") } - val profMatcher: Regex by lazy { Regex("url\\(\"(.*?)\"\\)") } - } - override fun onStopJob(params: JobParameters?): Boolean { val time = System.currentTimeMillis() - startTime L.d("Notification service has finished abruptly in $time ms") @@ -70,104 +55,28 @@ class NotificationService : JobService() { override fun onStartJob(params: JobParameters?): Boolean { L.i("Fetching notifications") future = doAsync { + val context = weakRef.get() + ?: return@doAsync L.eThrow("NotificationService had null weakRef to self") val currentId = Prefs.userId val cookies = loadFbCookiesSync() cookies.forEach { val current = it.id == currentId if (current || Prefs.notificationAllAccounts) - fetchGeneralNotifications(it) - if (Prefs.notificationsInstantMessages && (current || Prefs.notificationsImAllAccounts)) - fetchMessageNotifications(it) + NotificationType.GENERAL.fetch(context, it) + if (Prefs.notificationsInstantMessages + && (current || Prefs.notificationsImAllAccounts)) + NotificationType.MESSAGE.fetch(context, it) } finish(params) } return true } - fun logNotif(text: String): NotificationContent? { + private fun logNotif(text: String): NotificationContent? { L.eThrow("NotificationService: $text") return null } - /* - * ---------------------------------------------------------------- - * General notification logic. - * Fetch notifications -> Filter new ones -> Parse notifications -> - * Show notifications -> Show group notification - * ---------------------------------------------------------------- - */ - - fun fetchGeneralNotifications(data: CookieModel) { - L.d("Notif fetch", data.toString()) - val doc = frostJsoup(data.cookie, FbItem.NOTIFICATIONS.url) - //aclb for unread, acw for read - val unreadNotifications = (doc.getElementById("notifications_list") ?: return L.eThrow("Notification list not found")).getElementsByClass("aclb") - var notifCount = 0 - //val prevLatestEpoch = 1498931565L // for testing - val prevNotifTime = lastNotificationTime(data.id) - val prevLatestEpoch = prevNotifTime.epoch - L.v("Notif Prev Latest Epoch $prevLatestEpoch") - var newLatestEpoch = prevLatestEpoch - unreadNotifications.forEach unread@ { elem -> - val notif = parseNotification(data, elem) ?: return@unread - L.v("Notif timestamp ${notif.timestamp}") - if (notif.timestamp <= prevLatestEpoch) return@unread - NotificationType.GENERAL.createNotification(this, notif, notifCount == 0) - if (notif.timestamp > newLatestEpoch) - newLatestEpoch = notif.timestamp - notifCount++ - } - if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epoch = newLatestEpoch).save() - L.d("Notif new latest epoch ${lastNotificationTime(data.id).epoch}") - NotificationType.GENERAL.summaryNotification(this, data.id, notifCount) - } - - fun parseNotification(data: CookieModel, element: Element): NotificationContent? { - val a = element.getElementsByTag("a").first() ?: return logNotif("IM No a tag") - val abbr = element.getElementsByTag("abbr") - val epoch = epochMatcher.find(abbr.attr("data-store"))?.groups?.get(1)?.value?.toLong() ?: return logNotif("IM No epoch") - //fetch id - val notifId = notifIdMatcher.find(a.attr("data-store"))?.groups?.get(1)?.value?.toLong() ?: System.currentTimeMillis() - val timeString = abbr.text() - val text = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() //remove - if (Prefs.notificationKeywords.any { text.contains(it, ignoreCase = true) }) return null //notification filtered out - //fetch profpic - val p = element.select("i.img[style*=url]") - val pUrl = profMatcher.find(p.attr("style"))?.groups?.get(1)?.value?.formattedFbUrl ?: "" - return NotificationContent(data, notifId.toInt(), a.attr("href"), null, text, epoch, pUrl) - } - - /* - * ---------------------------------------------------------------- - * Instant message notification logic. - * Fetch notifications -> Filter new ones -> Parse notifications -> - * Show notifications -> Show group notification - * ---------------------------------------------------------------- - */ - - fun fetchMessageNotifications(data: CookieModel) { - L.d("Notif IM fetch", data.toString()) - val doc = frostJsoup(data.cookie, FbItem.MESSAGES.url) - val (threads, _, _) = MessageParser.parse(doc.toString()) ?: return L.e("Could not parse IM") - - var notifCount = 0 - val prevNotifTime = lastNotificationTime(data.id) - val prevLatestEpoch = prevNotifTime.epochIm - L.v("Notif Prev Latest Im Epoch $prevLatestEpoch") - var newLatestEpoch = prevLatestEpoch - threads.filter { it.unread }.forEach { notif -> - L.v("Notif Im timestamp ${notif.time}") - if (notif.time <= prevLatestEpoch) return@forEach - NotificationType.MESSAGE.createNotification(this, NotificationContent(data, notif), notifCount == 0) - if (notif.time > newLatestEpoch) - newLatestEpoch = notif.time - notifCount++ - } - if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epochIm = newLatestEpoch).save() - L.d("Notif new latest im epoch ${lastNotificationTime(data.id).epochIm}") - NotificationType.MESSAGE.summaryNotification(this, data.id, notifCount) - } - private fun Context.debugNotification(text: String) { if (!BuildConfig.DEBUG) return val notifBuilder = frostNotification.withDefaults() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index 4bd41802..1108f5d4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -22,7 +22,7 @@ import com.pitchedapps.frost.views.Keywords */ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { - text(R.string.notification_frequency, { Prefs.notificationFreq }, { Prefs.notificationFreq = it }) { + text(R.string.notification_frequency, Prefs::notificationFreq, { Prefs.notificationFreq = it }) { val options = longArrayOf(-1, 15, 30, 60, 120, 180, 300, 1440, 2880) val texts = options.map { if (it <= 0) string(R.string.no_notifications) else minuteToText(it) } onClick = { @@ -52,23 +52,27 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { } } - checkbox(R.string.notification_all_accounts, { Prefs.notificationAllAccounts }, { Prefs.notificationAllAccounts = it }) { + checkbox(R.string.notification_all_accounts, Prefs::notificationAllAccounts, { Prefs.notificationAllAccounts = it }) { descRes = R.string.notification_all_accounts_desc } - checkbox(R.string.notification_messages, { Prefs.notificationsInstantMessages }, { Prefs.notificationsInstantMessages = it; reloadByTitle(R.string.notification_messages_all_accounts) }) { + checkbox(R.string.notification_messages, Prefs::notificationsInstantMessages, { Prefs.notificationsInstantMessages = it; reloadByTitle(R.string.notification_messages_all_accounts) }) { descRes = R.string.notification_messages_desc } - checkbox(R.string.notification_messages_all_accounts, { Prefs.notificationsImAllAccounts }, { Prefs.notificationsImAllAccounts = it }) { + checkbox(R.string.notification_messages_all_accounts, Prefs::notificationsImAllAccounts, { Prefs.notificationsImAllAccounts = it }) { descRes = R.string.notification_messages_all_accounts_desc - enabler = { Prefs.notificationsInstantMessages } + enabler = Prefs::notificationsInstantMessages } - checkbox(R.string.notification_sound, { Prefs.notificationSound }, { Prefs.notificationSound = it; reloadByTitle(R.string.notification_ringtone, R.string.message_ringtone) }) + checkbox(R.string.notification_sound, Prefs::notificationSound, { + Prefs.notificationSound = it + reloadByTitle(R.string.notification_ringtone, + R.string.message_ringtone) + }) fun KPrefText.KPrefTextContract<String>.ringtone(code: Int) { - enabler = { Prefs.notificationSound } + enabler = Prefs::notificationSound textGetter = { if (it.isBlank()) string(R.string.kau_default) else RingtoneManager.getRingtone(this@getNotificationPrefs, Uri.parse(it)) @@ -87,17 +91,17 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { } } - text(R.string.notification_ringtone, { Prefs.notificationRingtone }, { Prefs.notificationRingtone = it }) { + text(R.string.notification_ringtone, Prefs::notificationRingtone, { Prefs.notificationRingtone = it }) { ringtone(SettingsActivity.REQUEST_NOTIFICATION_RINGTONE) } - text(R.string.message_ringtone, { Prefs.messageRingtone }, { Prefs.messageRingtone = it }) { + text(R.string.message_ringtone, Prefs::messageRingtone, { Prefs.messageRingtone = it }) { ringtone(SettingsActivity.REQUEST_MESSAGE_RINGTONE) } - checkbox(R.string.notification_vibrate, { Prefs.notificationVibrate }, { Prefs.notificationVibrate = it }) + checkbox(R.string.notification_vibrate, Prefs::notificationVibrate, { Prefs.notificationVibrate = it }) - checkbox(R.string.notification_lights, { Prefs.notificationLights }, { Prefs.notificationLights = it }) + checkbox(R.string.notification_lights, Prefs::notificationLights, { Prefs.notificationLights = it }) plainText(R.string.notification_fetch_now) { descRes = R.string.notification_fetch_now_desc 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 f8b487a2..9251e607 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -2,6 +2,7 @@ package com.pitchedapps.frost.web import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.graphics.Color import android.util.AttributeSet import android.view.View @@ -12,6 +13,7 @@ import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FB_LOGIN_URL import com.pitchedapps.frost.facebook.FB_USER_MATCHER import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.get import com.pitchedapps.frost.injectors.CssHider import com.pitchedapps.frost.injectors.jsInject import com.pitchedapps.frost.utils.L @@ -31,7 +33,7 @@ class LoginWebView @JvmOverloads constructor( private lateinit var progressCallback: (Int) -> Unit init { - FbCookie.reset { setupWebview() } + FbCookie.reset(this::setupWebview) } @SuppressLint("SetJavaScriptEnabled") @@ -62,13 +64,14 @@ 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 = FB_USER_MATCHER.find(cookie)?.groupValues?.get(1)?.toLong() ?: return@doAsync + val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return@doAsync uiThread { onFound(id, cookie) } } } override fun onPageCommitVisible(view: WebView, url: String?) { super.onPageCommitVisible(view, url) + L.d("Login page commit visible") view.setBackgroundColor(Color.TRANSPARENT) if (url.isFacebookUrl) view.jsInject(CssHider.HEADER, |