1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-09-19 23:21:34 +02:00

Parse header badges

This commit is contained in:
Allan Wang 2017-06-19 15:31:10 -07:00
parent 0720413ee8
commit 382433780c
22 changed files with 225 additions and 79 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@
/app/src/main/res/values/strings_facebook.xml
/app/src/main/kotlin/com/pitchedapps/frost/facebook/Private.kt
*.min.css
.sass-cache/

View File

@ -96,6 +96,8 @@ dependencies {
testCompile "org.robolectric:robolectric:${ROBOELECTRIC}"
compile "org.greenrobot:eventbus:${EVENTBUS}"
//Icons
compile "com.mikepenz:google-material-typeface:${IICON_GOOGLE}.original@aar"
compile "com.mikepenz:material-design-iconic-typeface:${IICON_MATERIAL}@aar"

View File

@ -10,8 +10,6 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:name=".FrostApp"
@ -111,15 +109,13 @@
<service
android:name=".services.NotificationService"
android:enabled="true"
android:label="@string/frost_notifications" />
android:label="@string/frost_notifications"
android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver
android:name=".services.NotificationReceiver"
android:enabled="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
android:name=".services.UpdateReceiver"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="com.pitchedapps.frost.NOTIFICATIONS" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<meta-data

View File

@ -0,0 +1,3 @@
//bases the header contents if it exists
var header = document.getElementById('mJewelNav');
if (header !== null) Frost.handleHeader(header.outerHTML);

View File

@ -0,0 +1,3 @@
var header=document.getElementById("mJewelNav")
;null!==header&&Frost.handleHeader(header.outerHTML);

View File

@ -30,8 +30,12 @@ import com.pitchedapps.frost.facebook.FbTab
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.utils.*
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import org.jetbrains.anko.childrenSequence
import org.jsoup.Jsoup
import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity() {
@ -45,6 +49,7 @@ class MainActivity : BaseActivity() {
lateinit var drawerHeader: AccountHeader
var webFragmentObservable = PublishSubject.create<Int>()!!
var lastPosition = -1
val headerBadgeObservable = PublishSubject.create<String>()
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))) }
}

View File

@ -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<Int>(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)
}

View File

@ -42,6 +42,9 @@ fun loadFbCookiesAsync(callback: (cookies: List<CookieModel>) -> Unit) {
(select from CookieModel::class).orderBy(CookieModel_Table.name, true).async().queryListResultCallback { _, tResult -> callback.invoke(tResult) }.execute()
}
fun loadFbCookiesSync(): List<CookieModel> = (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")

View File

@ -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"

View File

@ -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
}
}

View File

@ -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,13 +21,53 @@ 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() {
var future: Future<Unit>? = null
override fun onStopJob(params: JobParameters?): Boolean {
future?.cancel(true)
future = null
return false
}
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)
}
return true
}
companion object {
const val ARG_ID = "arg_id"
@ -31,31 +75,6 @@ class NotificationService : IntentService(NotificationService::class.java.simple
val notifIdMatcher: Regex by lazy { Regex("notif_id\":([0-9]*),") }
}
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++
}
}
if (notifCount > 0) saveNotificationTime(NotificationModel(data.id, latestEpoch))
summaryNotification(data.id, notifCount)
}
fun parseNotification(data: CookieModel, element: Element): NotificationContent? {
val a = element.getElementsByTag("a").first() ?: return null
val dataStore = a.attr("data-store")
@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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<String>? = (context as? MainActivity)?.headerBadgeObservable
val cookies: ArrayList<CookieModel>
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)
}
}

View File

@ -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)
})

View File

@ -41,6 +41,7 @@ class FrostWebViewCore @JvmOverloads constructor(
val refreshObservable: PublishSubject<Boolean> // Only emits on page loads
val titleObservable: BehaviorSubject<String> // Only emits on different non http titles
var baseUrl: String? = null
var baseEnum: FbTab? = null
internal var frostWebClient: FrostWebViewClient? = null

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.constraint.ConstraintLayout>

View File

@ -41,4 +41,11 @@
<string name="custom">Custom</string>
<string name="frost_notifications">Frost Notifications</string>
<string name="requires_custom_theme">Requires custom theme</string>
<string name="none">None</string>
<string name="x_minutes">%d minutes</string>
<string name="one_hour">1 hour</string>
<string name="x_hours">%d hours</string>
<string name="one_day">1 day</string>
<string name="x_days">%d days</string>
</resources>

View File

@ -6,18 +6,18 @@
<item text="" />
-->
<version title="v0.2"/>
<item text="Removed unnecessary permissions" />
<item text="" />
<item text="" />
<item text="" />
<item text="" />
<version title="v0.1" />
<item text="Initial Changelog" />
<item text="Created core databases" />
<item text="Implemented CSS/Js injectors" />
<item text="Implemented ripple preferences" />
<item text="" />
<item text="" />
<item text="" />
<item text="" />
<item text="" />
<item text="" />
<item text="" />
<item text="" />
<item text="" />
<item text="Created multiple account caching" />
<item text="Created web overlay" />
</resources>

View File

@ -1,7 +1,12 @@
# Changelog
## v0.2
* Removed unnecessary permissions
## v0.1
* Initial Changelog
* Created core databases
* Implemented CSS/Js injectors
* Implemented ripple preferences
* Created multiple account caching
* Created web overlay

View File

@ -16,11 +16,11 @@ APP_GROUP=com.pitchedapps
MIN_SDK=21
TARGET_SDK=26
BUILD_TOOLS=26.0.0
VERSION_CODE=1
VERSION_NAME=0.1
VERSION_CODE=2
VERSION_NAME=0.2
ANDROID_SUPPORT_LIBS=26.0.0-alpha1
KAU=9832aed0ea
KAU=94defc1ab5
MATERIAL_DRAWER=5.9.2
MATERIAL_DRAWER_KT=1.0.2
IICON_GOOGLE=3.0.1.0
@ -34,6 +34,7 @@ JSOUP=1.10.2
ANKO=0.10.1
GLIDE=4.0.0-RC0
RETROFIT=2.2.0
EVENTBUS=3.0.0
DBFLOW=4.0.2
SQL_CIPHER=3.5.7
OKHTTP_INTERCEPTOR=3.6.0