From 382433780c3f4403723a78e409cb161c9fad5034 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 19 Jun 2017 15:31:10 -0700 Subject: Parse header badges --- app/src/main/AndroidManifest.xml | 14 ++-- app/src/main/assets/js/header_badges.js | 3 + app/src/main/assets/js/header_badges.min.js | 3 + .../kotlin/com/pitchedapps/frost/MainActivity.kt | 21 ++++++ .../com/pitchedapps/frost/SettingsActivity.kt | 38 ++++++++-- .../com/pitchedapps/frost/dbflow/CookiesDb.kt | 3 + .../com/pitchedapps/frost/injectors/JsAssets.kt | 2 +- .../frost/services/NotificationReceiver.kt | 20 ----- .../frost/services/NotificationService.kt | 85 +++++++++++++++------- .../pitchedapps/frost/services/UpdateReceiver.kt | 20 +++++ .../kotlin/com/pitchedapps/frost/utils/Prefs.kt | 2 + .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 28 ++++++- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 9 +++ .../pitchedapps/frost/web/FrostWebViewClient.kt | 9 ++- .../com/pitchedapps/frost/web/FrostWebViewCore.kt | 1 + app/src/main/res/layout/view_badged_icon.xml | 6 ++ app/src/main/res/values/strings.xml | 7 ++ app/src/main/res/xml/changelog.xml | 18 ++--- 18 files changed, 213 insertions(+), 76 deletions(-) create mode 100644 app/src/main/assets/js/header_badges.js create mode 100644 app/src/main/assets/js/header_badges.min.js delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt create mode 100644 app/src/main/res/layout/view_badged_icon.xml (limited to 'app/src') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 64ec94df..b59e5041 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,8 +10,6 @@ - - + android:label="@string/frost_notifications" + android:permission="android.permission.BIND_JOB_SERVICE" /> + android:name=".services.UpdateReceiver" + android:enabled="true"> - - - + ()!! var lastPosition = -1 + val headerBadgeObservable = PublishSubject.create() companion object { const val FRAGMENT_REFRESH = 99 @@ -97,6 +102,22 @@ class MainActivity : BaseActivity() { currentFragment.web.scrollOrRefresh() } }) + headerBadgeObservable.throttleFirst(15, TimeUnit.SECONDS).subscribeOn(Schedulers.newThread()) + .map { Jsoup.parse(it) } + .filter { it.select("[data-sigil=\"count\"]").size >= 0 } //ensure headers exist + .map { + val feed = it.select("[data-sigil*=\"feed\"] [data-sigil=\"count\"]") + val requests = it.select("[data-sigil*=\"requests\"] [data-sigil=\"count\"]") + val messages = it.select("[data-sigil*=\"messages\"] [data-sigil=\"count\"]") + val notifications = it.select("[data-sigil*=\"notifications\"] [data-sigil=\"count\"]") + return@map arrayOf(feed, requests, messages, notifications).map { it?.getOrNull(0)?.ownText() } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + (feed, requests, messages, notifications) -> + L.d("Header subscription $feed $requests $messages $notifications") + L.d("contained nulls ${feed == null}") + } adapter.pages.forEach { tabs.addTab(tabs.newTab().setIcon(it.icon.toDrawable(this, sizeDp = 20, color = Prefs.iconColor))) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt index 0a9732b4..b55b2a10 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/SettingsActivity.kt @@ -6,16 +6,18 @@ import ca.allanwang.kau.kpref.KPrefAdapterBuilder import ca.allanwang.kau.utils.* import ca.allanwang.kau.views.RippleCanvas import com.pitchedapps.frost.utils.* +import org.jetbrains.anko.toast /** * Created by Allan Wang on 2017-06-06. */ class SettingsActivity : KPrefActivity() { + override fun onCreateKPrefs(savedInstanceState: android.os.Bundle?): KPrefAdapterBuilder.() -> Unit = { textColor = { Prefs.textColor } accentColor = { Prefs.textColor } header(R.string.settings) - text(R.string.theme, { Prefs.theme }, { Prefs.theme = it }) { + text(R.string.theme, { Prefs.theme }, { Prefs.theme = it }) { onClick = { _, _, item -> this@SettingsActivity.materialDialogThemed { @@ -65,16 +67,40 @@ class SettingsActivity : KPrefActivity() { allowCustom = true } - colorPicker(R.string.icon_color, { Prefs.customIconColor }, { Prefs.customIconColor = it; toolbar.setTitleTextColor(it) }) { - enabler = { Prefs.isCustomTheme } - onDisabledClick = { itemView, _, _ -> itemView.snackbar(R.string.requires_custom_theme); true } - allowCustomAlpha = false - allowCustom = true + fun Long.timeToText(): String = + if (this == -1L) string(R.string.none) + else if (this == 60L) string(R.string.one_hour) + else if (this == 1440L) string(R.string.one_day) + else if (this % 1440L == 0L) String.format(string(R.string.x_days), this / 1440L) + else if (this % 60L == 0L) String.format(string(R.string.x_hours), this / 60L) + else String.format(string(R.string.x_minutes), this) + + text(R.string.notifications, { Prefs.notificationFreq }, { Prefs.notificationFreq = it; reloadByTitle(R.string.notifications) }) { + val options = longArrayOf(-1, 15, 30, 60, 120, 180, 300, 1440, 2880) + val texts = options.map { it.timeToText() } + onClick = { + _, _, item -> + this@SettingsActivity.materialDialogThemed { + title(R.string.notifications) + items(texts) + itemsCallbackSingleChoice(options.indexOf(item.pref), { + _, _, which, text -> + item.pref = options[which] + this@SettingsActivity.scheduleNotifications(item.pref) + this@SettingsActivity.toast(text) + true + }) + } + true + } + textGetter = { it.timeToText() } } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setFrostTheme() themeExterior(false) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt index 46946ab9..1d1aa86e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt @@ -42,6 +42,9 @@ fun loadFbCookiesAsync(callback: (cookies: List) -> Unit) { (select from CookieModel::class).orderBy(CookieModel_Table.name, true).async().queryListResultCallback { _, tResult -> callback.invoke(tResult) }.execute() } +fun loadFbCookiesSync(): List = (select from CookieModel::class).orderBy(CookieModel_Table.name, true).queryList() + + fun saveFbCookie(cookie: CookieModel, callback: (() -> Unit)? = null) { cookie.async save { L.d("Fb cookie $cookie saved") diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt index 864f95ea..e3db5444 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt @@ -10,7 +10,7 @@ import com.pitchedapps.frost.utils.L * //TODO add folder mapping using Prefs */ enum class JsAssets : InjectorContract { - MENU, MENU_CLICK, CLICK_INTERCEPTOR + MENU, MENU_CLICK, CLICK_INTERCEPTOR, HEADER_BADGES ; var file = "${name.toLowerCase()}.min.js" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt deleted file mode 100644 index b37ca1f8..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationReceiver.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.pitchedapps.frost.services - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -/** - * Created by Allan Wang on 2017-05-31. - */ -class NotificationReceiver : BroadcastReceiver() { - - companion object { - const val ACTION = "com.pitchedapps.frost.NOTIFICATIONS" - } - - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != ACTION) return - } - -} \ No newline at end of file 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 85765541..f506bd8f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -1,14 +1,18 @@ package com.pitchedapps.frost.services -import android.app.IntentService import android.app.Notification import android.app.PendingIntent +import android.app.job.JobParameters +import android.app.job.JobService import android.content.Context import android.content.Intent +import android.os.Looper import android.support.v4.app.ActivityOptionsCompat import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationManagerCompat +import ca.allanwang.kau.utils.checkThread import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.WebOverlayActivity import com.pitchedapps.frost.dbflow.* @@ -17,43 +21,58 @@ import com.pitchedapps.frost.facebook.FB_URL_BASE import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.utils.ARG_URL import com.pitchedapps.frost.utils.L +import org.jetbrains.anko.doAsync import org.jsoup.Jsoup import org.jsoup.nodes.Element +import java.util.concurrent.Future /** * Created by Allan Wang on 2017-06-14. */ -class NotificationService : IntentService(NotificationService::class.java.simpleName) { +class NotificationService : JobService() { - companion object { - const val ARG_ID = "arg_id" - val epochMatcher: Regex by lazy { Regex(":([0-9]*),") } - val notifIdMatcher: Regex by lazy { Regex("notif_id\":([0-9]*),") } + var future: Future? = null + + override fun onStopJob(params: JobParameters?): Boolean { + future?.cancel(true) + future = null + return false } - override fun onHandleIntent(intent: Intent) { - val id = intent.getLongExtra(ARG_ID, -1L) - L.i("Handling notifications for $id") - if (id == -1L) return - val data = loadFbCookie(id) ?: return - L.v("Using data $data") - val doc = Jsoup.connect(FbTab.NOTIFICATIONS.url).cookie(FACEBOOK_COM, data.cookie).get() - val unreadNotifications = doc.getElementById("notifications_list").getElementsByClass("aclb") - var notifCount = 0 - var latestEpoch = lastNotificationTime(data.id) - L.v("Latest Epoch $latestEpoch") - unreadNotifications.forEach { - elem -> - val notif = parseNotification(data, elem) - if (notif != null) { - if (notif.timestamp <= latestEpoch) return@forEach - notif.createNotification(this) - latestEpoch = notif.timestamp - notifCount++ + override fun onStartJob(params: JobParameters?): Boolean { + future = doAsync { + loadFbCookiesSync().forEach { + data -> + L.i("Handling notifications for ${data.id}") + L.v("Using data $data") + val doc = Jsoup.connect(FbTab.NOTIFICATIONS.url).cookie(FACEBOOK_COM, data.cookie).get() + val unreadNotifications = doc.getElementById("notifications_list").getElementsByClass("aclb") + var notifCount = 0 + var latestEpoch = lastNotificationTime(data.id) + L.v("Latest Epoch $latestEpoch") + unreadNotifications.forEach unread@ { + elem -> + val notif = parseNotification(data, elem) + if (notif != null) { + if (notif.timestamp <= latestEpoch) return@unread + notif.createNotification(this@NotificationService) + latestEpoch = notif.timestamp + notifCount++ + } + } + if (notifCount > 0) saveNotificationTime(NotificationModel(data.id, latestEpoch)) + summaryNotification(data.id, notifCount) } + L.d("Finished notifications") + jobFinished(params, false) } - if (notifCount > 0) saveNotificationTime(NotificationModel(data.id, latestEpoch)) - summaryNotification(data.id, notifCount) + return true + } + + companion object { + const val ARG_ID = "arg_id" + val epochMatcher: Regex by lazy { Regex(":([0-9]*),") } + val notifIdMatcher: Regex by lazy { Regex("notif_id\":([0-9]*),") } } fun parseNotification(data: CookieModel, element: Element): NotificationContent? { @@ -71,6 +90,18 @@ class NotificationService : IntentService(NotificationService::class.java.simple return NotificationContent(data, notifId.toInt(), a.attr("href"), text, epoch) } + private fun Context.debugNotification(text: String) { + if (BuildConfig.DEBUG) { + val notifBuilder = NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.frost_f_24) + .setContentTitle(string(R.string.app_name)) + .setContentText(text) + .setAutoCancel(true) + + NotificationManagerCompat.from(this).notify(999, notifBuilder.build()) + } + } + data class NotificationContent(val data: CookieModel, val notifId: Int, val href: String, val text: String, val timestamp: Long) { fun createNotification(context: Context) { val intent = Intent(context, WebOverlayActivity::class.java) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt new file mode 100644 index 00000000..c40750ef --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt @@ -0,0 +1,20 @@ +package com.pitchedapps.frost.services + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.scheduleNotifications + +/** + * Created by Allan Wang on 2017-05-31. + */ +class UpdateReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + L.d("Frost has updated") + context.scheduleNotifications(Prefs.notificationFreq) //Update notifications + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt index e217da46..b319fcc3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -31,6 +31,8 @@ object Prefs : KPref() { var exitConfirmation: Boolean by kpref("exit_confirmation", true) + var notificationFreq: Long by kpref("notification_freq", -1L) + private val loader = lazyResettable { Theme.values[Prefs.theme] } private val t: Theme by loader diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index abbd8366..52922822 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -1,11 +1,13 @@ package com.pitchedapps.frost.utils import android.app.Activity +import android.app.job.JobInfo +import android.app.job.JobScheduler +import android.content.ComponentName import android.content.Context import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable -import android.support.v4.app.ActivityOptionsCompat import android.support.v4.content.ContextCompat import android.support.v7.widget.Toolbar import android.view.View @@ -18,6 +20,7 @@ import com.pitchedapps.frost.WebOverlayActivity import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FB_URL_BASE import com.pitchedapps.frost.facebook.FbTab +import com.pitchedapps.frost.services.NotificationService /** * Created by Allan Wang on 2017-06-03. @@ -99,4 +102,27 @@ fun Activity.setFrostColors(toolbar: Toolbar? = null, themeWindow: Boolean = tru texts.forEach { it.setTextColor(Prefs.textColor) } headers.forEach { it.setBackgroundColor(darkAccent) } backgrounds.forEach { it.setBackgroundColor(Prefs.bgColor) } +} + + +const val NOTIFICATION_JOB = 7 +/** + * [interval] is # of min, which must be at least 15 + * returns false if an error occurs; true otherwise + */ +fun Context.scheduleNotifications(minutes: Long): Boolean { + val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + scheduler.cancel(NOTIFICATION_JOB) + if (minutes < 0L) return true + val serviceComponent = ComponentName(this, NotificationService::class.java) + val builder = JobInfo.Builder(NOTIFICATION_JOB, serviceComponent) + .setPeriodic(minutes * 60000) + .setPersisted(true) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) //TODO add options + val result = scheduler.schedule(builder.build()) + if (result <= 0) { + L.e("Notification scheduler failed") + return false + } + return true } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 36193b8b..532b9f82 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -10,12 +10,16 @@ import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.cookies import com.pitchedapps.frost.utils.launchNewTask import com.pitchedapps.frost.utils.launchWebOverlay +import io.reactivex.subjects.Subject /** * Created by Allan Wang on 2017-06-01. */ class FrostJSI(val context: Context, val webView: FrostWebViewCore) { + + val headerObservable: Subject? = (context as? MainActivity)?.headerBadgeObservable + val cookies: ArrayList get() = (context as? MainActivity)?.cookies() ?: arrayListOf() @@ -51,4 +55,9 @@ class FrostJSI(val context: Context, val webView: FrostWebViewCore) { webView.post { webView.frostWebClient!!.handleHtml(html) } } + @JavascriptInterface + fun handleHeader(html: String) { + headerObservable?.onNext(html) + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt index 0fe3304a..d015e22e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt @@ -3,7 +3,10 @@ package com.pitchedapps.frost.web import android.content.Context import android.graphics.Bitmap import android.view.KeyEvent -import android.webkit.* +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient import com.pitchedapps.frost.LoginActivity import com.pitchedapps.frost.MainActivity import com.pitchedapps.frost.SelectorActivity @@ -50,7 +53,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : WebViewClient() { refreshObservable.onNext(false) return } - JsActions.LOGIN_CHECK.inject(view) + view.jsInject(JsActions.LOGIN_CHECK, JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null)) onPageFinishedActions(url) } @@ -62,7 +65,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : WebViewClient() { L.d("Page finished reveal") webCore.jsInject(CssHider.HEADER, Prefs.themeInjector, -// JsAssets.CLICK_INTERCEPTOR, + // JsAssets.CLICK_INTERCEPTOR, callback = { refreshObservable.onNext(false) }) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt index b6353252..6e9e956f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt @@ -41,6 +41,7 @@ class FrostWebViewCore @JvmOverloads constructor( val refreshObservable: PublishSubject // Only emits on page loads val titleObservable: BehaviorSubject // Only emits on different non http titles + var baseUrl: String? = null var baseEnum: FbTab? = null internal var frostWebClient: FrostWebViewClient? = null diff --git a/app/src/main/res/layout/view_badged_icon.xml b/app/src/main/res/layout/view_badged_icon.xml new file mode 100644 index 00000000..d8fb3247 --- /dev/null +++ b/app/src/main/res/layout/view_badged_icon.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7fa7b19..f05d2ce5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,4 +41,11 @@ Custom Frost Notifications Requires custom theme + + None + %d minutes + 1 hour + %d hours + 1 day + %d days diff --git a/app/src/main/res/xml/changelog.xml b/app/src/main/res/xml/changelog.xml index 8a46973a..964bc8e1 100644 --- a/app/src/main/res/xml/changelog.xml +++ b/app/src/main/res/xml/changelog.xml @@ -6,18 +6,18 @@ --> + + + + + + + - - - - - - - - - + + \ No newline at end of file -- cgit v1.2.3