From 610c37698ab93b8d51efcaec9f721292cacfd854 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 14 Jun 2017 23:39:05 -0700 Subject: Create notification service framework --- .../main/kotlin/com/pitchedapps/frost/FrostApp.kt | 3 +- .../kotlin/com/pitchedapps/frost/LoginActivity.kt | 4 +- .../kotlin/com/pitchedapps/frost/MainActivity.kt | 6 +- .../com/pitchedapps/frost/dbflow/NotificationDb.kt | 31 ++++++ .../frost/services/NotificationExtensions.kt | 13 +++ .../frost/services/NotificationService.kt | 121 +++++++++++++++++++++ .../pitchedapps/frost/web/FrostWebViewClient.kt | 4 +- .../com/pitchedapps/frost/web/LoginWebView.kt | 2 +- 8 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/NotificationExtensions.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt (limited to 'app/src/main/kotlin') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index 85d1c8b5..09a54444 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -13,6 +13,7 @@ import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerUIUtils import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.services.requestNotifications import com.pitchedapps.frost.utils.CrashReportingTree import com.pitchedapps.frost.utils.Prefs import com.raizlabs.android.dbflow.config.FlowConfig @@ -47,7 +48,7 @@ class FrostApp : Application() { Prefs.initialize(this, "${com.pitchedapps.frost.BuildConfig.APPLICATION_ID}.prefs") FbCookie() super.onCreate() - + requestNotifications(Prefs.userId) //Drawer profile loading logic DrawerImageLoader.init(object : AbstractDrawerImageLoader() { override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt index 9ba83879..386bf4d3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/LoginActivity.kt @@ -87,7 +87,7 @@ class LoginActivity : BaseActivity() { (foundImage, name) -> refresh = false L.d("Zip done") - if (!foundImage) L.e("Could not get profile photo; Invalid id?\n\t$cookie") + if (!foundImage) L.e("Could not get profile photo; Invalid userId?\n\t$cookie") textview.setTextWithFade(String.format(getString(R.string.welcome), name), duration = 500) /* * The user may have logged into an account that is already in the database @@ -96,7 +96,7 @@ class LoginActivity : BaseActivity() { loadFbCookiesAsync { cookies -> Handler().postDelayed({ - launchNewTask(MainActivity::class.java, ArrayList(cookies)) + launchNewTask(MainActivity::class.java, ArrayList(cookies), clearStack = true) }, 1000) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt index fce3e670..17840a14 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/MainActivity.kt @@ -1,6 +1,5 @@ package com.pitchedapps.frost -import android.content.Intent import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.support.design.widget.FloatingActionButton @@ -30,6 +29,7 @@ import com.pitchedapps.frost.facebook.FbCookie.switchUser import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.fragments.WebFragment +import com.pitchedapps.frost.services.requestNotifications import com.pitchedapps.frost.utils.* import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject @@ -178,7 +178,9 @@ class MainActivity : BaseActivity() { }) } R.id.action_changelog -> showChangelog(R.xml.changelog) - R.id.action_call -> launchNewTask(LoginActivity::class.java) + R.id.action_call -> { + requestNotifications(Prefs.userId) + } R.id.action_db -> adapter.pages.saveAsync(this) R.id.action_restart -> restart() else -> return super.onOptionsItemSelected(item) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt new file mode 100644 index 00000000..e71500fc --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt @@ -0,0 +1,31 @@ +package com.pitchedapps.frost.dbflow + +import com.pitchedapps.frost.utils.L +import com.raizlabs.android.dbflow.annotation.ConflictAction +import com.raizlabs.android.dbflow.annotation.Database +import com.raizlabs.android.dbflow.annotation.PrimaryKey +import com.raizlabs.android.dbflow.annotation.Table +import com.raizlabs.android.dbflow.kotlinextensions.* +import com.raizlabs.android.dbflow.structure.BaseModel + +/** + * Created by Allan Wang on 2017-05-30. + */ + +@Database(name = NotificationDb.NAME, version = NotificationDb.VERSION) +object NotificationDb { + const val NAME = "Notifications" + const val VERSION = 1 +} + +@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE) +data class NotificationModel(@PrimaryKey var id: Long = -1L, var epoch: Long = -1L) : BaseModel() + +fun lastNotificationTime(id: Long): Long = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle()?.epoch ?: -1L + +fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) { + notificationModel.async save { + L.d("Fb notification $notificationModel saved") + callback?.invoke() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationExtensions.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationExtensions.kt new file mode 100644 index 00000000..ac94b527 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationExtensions.kt @@ -0,0 +1,13 @@ +package com.pitchedapps.frost.services + +import android.content.Context +import android.content.Intent + +/** + * Created by Allan Wang on 2017-06-14. + */ +fun Context.requestNotifications(id: Long) { + val intent = Intent(this, NotificationService::class.java) + intent.putExtra(NotificationService.ARG_ID, id) + startService(intent) +} \ 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 new file mode 100644 index 00000000..3bbecb4f --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -0,0 +1,121 @@ +package com.pitchedapps.frost.services + +import android.app.IntentService +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.support.v4.app.ActivityOptionsCompat +import android.support.v4.app.NotificationCompat +import android.support.v4.app.NotificationManagerCompat +import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.R +import com.pitchedapps.frost.WebOverlayActivity +import com.pitchedapps.frost.dbflow.NotificationModel +import com.pitchedapps.frost.dbflow.lastNotificationTime +import com.pitchedapps.frost.dbflow.loadFbCookie +import com.pitchedapps.frost.dbflow.saveNotificationTime +import com.pitchedapps.frost.facebook.FACEBOOK_COM +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.jsoup.Jsoup +import org.jsoup.nodes.Element + +/** + * Created by Allan Wang on 2017-06-14. + */ +class NotificationService : IntentService(NotificationService::class.java.simpleName) { + + 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]*),") } + } + + 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.i("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.i("Latest Epoch $latestEpoch") + unreadNotifications.forEach { + elem -> + val notif = parseNotification(data.id, elem) + if (notif != null) { + if (notif.timestamp <= latestEpoch) return@forEach + notif.createNotification(this) + latestEpoch = notif.timestamp + notifCount++ + } + } + saveNotificationTime(NotificationModel(data.id, latestEpoch)) + summaryNotification(data.id, notifCount) + } + + fun parseNotification(userId: Long, element: Element): NotificationContent? { + val a = element.getElementsByTag("a").first() ?: return null + val dataStore = a.attr("data-store") + val notifId = if (dataStore == null) System.currentTimeMillis() + else notifIdMatcher.find(dataStore)?.groups?.get(1)?.value?.toLong() ?: System.currentTimeMillis() + val abbr = element.getElementsByTag("abbr") + val timeString = abbr?.text() + var text = a.text().replace("\u00a0", " ") //remove   + if (timeString != null) text = text.removeSuffix(timeString) + text = text.trim() + val abbrData = abbr?.attr("data-store") + val epoch = if (abbrData == null) -1L else epochMatcher.find(abbrData)?.groups?.get(1)?.value?.toLong() ?: -1L + return NotificationContent(userId, notifId.toInt(), a.attr("href"), text, epoch) + } + + data class NotificationContent(val userId: Long, val notifId: Int, val href: String, val text: String, val timestamp: Long) { + fun createNotification(context: Context) { + val intent = Intent(context, WebOverlayActivity::class.java) +// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or PendingIntent.FLAG_ONE_SHOT) + intent.putExtra(ARG_URL, "$FB_URL_BASE$href") + intent.action = System.currentTimeMillis().toString() //dummy action + val bundle = ActivityOptionsCompat.makeCustomAnimation(context, R.anim.slide_in_right, R.anim.slide_out_right).toBundle() + val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT, bundle) + val notifBuilder = NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.frost_f_24) + .setContentTitle(context.string(R.string.app_name)) + .setContentText(text) + .setContentIntent(pendingIntent) + .setGroup("frost_$userId") + .setAutoCancel(true) + + if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000) + + NotificationManagerCompat.from(context).notify("frost_$userId", notifId, notifBuilder.build()) + } + } + + fun summaryNotification(userId: Long, count: Int) { + if (count <= 1) return + val notifBuilder = NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.frost_f_24) + .setContentTitle(string(R.string.app_name)) + .setContentText("$count notifications") + .setGroup("frost_$userId") + .setGroupSummary(true) + .setAutoCancel(true) + + NotificationManagerCompat.from(this).notify("frost_$userId", userId.toInt(), notifBuilder.build()) + } + + private fun log(element: Element) { + with(element) { + L.i("\n\nElement ${text()}") + attributes().forEach { + L.i("attr ${it.html()}") + } + L.i("Content ${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 4845f553..1ae17cde 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClient.kt @@ -61,7 +61,9 @@ open class FrostWebViewClient(val refreshObservable: Subject) : WebView internal fun onPageFinishedReveal(view: FrostWebViewCore, animate: Boolean) { L.d("Page finished reveal") - view.jsInject(CssHider.HEADER, CssAssets.MATERIAL_DARK, callback = { + view.jsInject(CssHider.HEADER, +// CssAssets.MATERIAL_DARK, + callback = { L.d("Finished ${it.contentToString()}") refreshObservable.onNext(false) if (animate) view.circularReveal(offset = 150L) 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 191d9350..721a75de 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -52,7 +52,7 @@ class LoginWebView @JvmOverloads constructor( cookieObservable.onComplete() loginObservable.onSuccess(CookieModel(id.toLong(), "", cookie)) } catch (e: NumberFormatException) { - //todo send report that id has changed + //todo send report that userId has changed } } } -- cgit v1.2.3