diff options
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/services')
3 files changed, 198 insertions, 69 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt new file mode 100644 index 00000000..6af6b1db --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -0,0 +1,149 @@ +package com.pitchedapps.frost.services + +import android.app.Notification +import android.app.PendingIntent +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.Bitmap +import android.net.Uri +import android.support.v4.app.NotificationCompat +import android.support.v4.app.NotificationManagerCompat +import ca.allanwang.kau.utils.color +import ca.allanwang.kau.utils.dpToPx +import ca.allanwang.kau.utils.string +import com.bumptech.glide.request.target.SimpleTarget +import com.bumptech.glide.request.transition.Transition +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.FrostWebActivity +import com.pitchedapps.frost.R +import com.pitchedapps.frost.WebOverlayActivity +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.dbflow.fetchUsername +import com.pitchedapps.frost.facebook.FB_URL_BASE +import com.pitchedapps.frost.utils.GlideApp +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.withRoundIcon +import org.jetbrains.anko.runOnUiThread + +/** + * Created by Allan Wang on 2017-07-08. + */ +val Context.frostNotification: NotificationCompat.Builder + get() = NotificationCompat.Builder(this, BuildConfig.APPLICATION_ID).apply { + setSmallIcon(R.drawable.frost_f_24) + setAutoCancel(true) + color = color(R.color.frost_notification_accent) + } + +val NotificationCompat.Builder.withBigText: NotificationCompat.BigTextStyle + get() = NotificationCompat.BigTextStyle(this) + +/** + * Created by Allan Wang on 2017-07-08. + * + * Custom target to set the content view and update a given notification + */ +class FrostNotificationTarget(val context: Context, + val notifId: Int, + val notifTag: String, + val builder: NotificationCompat.Builder +) : SimpleTarget<Bitmap>(40.dpToPx, 40.dpToPx) { + + override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>) { + builder.setLargeIcon(resource) + NotificationManagerCompat.from(context).notify(notifTag, notifId, builder.withBigText.build()) + } +} + +/** + * Notification data holder + */ +data class NotificationContent(val data: CookieModel, + val notifId: Int, + val href: String, + val text: String, + val timestamp: Long, + val profileUrl: String) { + fun createNotification(context: Context, verifiedUser: Boolean = false) { + //in case we haven't found the name, we will try one more time before passing the notification + if (!verifiedUser && data.name?.isBlank() ?: true) { + data.fetchUsername { + data.name = it + createNotification(context, true) + } + } else { + val intent = Intent(context, FrostWebActivity::class.java) + intent.data = Uri.parse("${FB_URL_BASE}$href") + intent.putExtra(WebOverlayActivity.ARG_USER_ID, data.id) + val group = "frost_${data.id}" + val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + val notifBuilder = context.frostNotification + .setContentTitle(context.string(R.string.app_name)) + .setContentText(text) + .setContentIntent(pendingIntent) + .setCategory(Notification.CATEGORY_SOCIAL) + .setSubText(data.name) + .setGroup(group) + + if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000) + L.v("Notif load $this") + NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.withBigText.build()) + + if (profileUrl.isNotBlank()) { + context.runOnUiThread { + GlideApp.with(context) + .asBitmap() + .load(profileUrl) + .withRoundIcon() + .into(FrostNotificationTarget(context, notifId, group, notifBuilder)) + } + } + } + } +} + +const val NOTIFICATION_PERIODIC_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_PERIODIC_JOB) + if (minutes < 0L) return true + val serviceComponent = ComponentName(this, NotificationService::class.java) + val builder = JobInfo.Builder(NOTIFICATION_PERIODIC_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.eThrow("Notification scheduler failed") + return false + } + return true +} + +const val NOTIFICATION_JOB_NOW = 6 + +/** + * Run notification job right now + */ +fun Context.fetchNotifications():Boolean { + val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + val serviceComponent = ComponentName(this, NotificationService::class.java) + val builder = JobInfo.Builder(NOTIFICATION_JOB_NOW, serviceComponent) + .setMinimumLatency(0L) + .setOverrideDeadline(2000L) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + val result = scheduler.schedule(builder.build()) + if (result <= 0) { + L.eThrow("Notification scheduler failed") + return false + } + return true +}
\ 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 d00ca8f0..4c03f056 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -1,26 +1,18 @@ package com.pitchedapps.frost.services -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.net.Uri import android.support.v4.app.NotificationManagerCompat import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig -import com.pitchedapps.frost.FrostWebActivity import com.pitchedapps.frost.R -import com.pitchedapps.frost.WebOverlayActivity import com.pitchedapps.frost.dbflow.* 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.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostAnswersCustom -import com.pitchedapps.frost.utils.frostNotification import org.jetbrains.anko.doAsync import org.jsoup.Jsoup import org.jsoup.nodes.Element @@ -36,6 +28,12 @@ class NotificationService : JobService() { var future: Future<Unit>? = null + companion object { + val epochMatcher: Regex by lazy { Regex(":([0-9]*?),") } + val notifIdMatcher: Regex by lazy { Regex("notif_id\":([0-9]*?),") } + val profMatcher: Regex by lazy { Regex("url\\(\"(.*?)\"\\)") } + } + override fun onStopJob(params: JobParameters?): Boolean { future?.cancel(true) future = null @@ -44,27 +42,15 @@ class NotificationService : JobService() { override fun onStartJob(params: JobParameters?): Boolean { future = doAsync { - loadFbCookiesSync().forEach { - data -> - L.i("Handle notifications for $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 (Prefs.notificationAllAccounts) { + loadFbCookiesSync().forEach { + data -> + fetchNotifications(data) } - if (notifCount > 0) saveNotificationTime(NotificationModel(data.id, latestEpoch)) - frostAnswersCustom("Notifications") { putCustomAttribute("Count", notifCount) } - summaryNotification(data.id, notifCount) + } else { + val currentCookie = loadFbCookie(Prefs.userId) + if (currentCookie != null) + fetchNotifications(currentCookie) } L.d("Finished notifications") jobFinished(params, false) @@ -73,13 +59,35 @@ class NotificationService : JobService() { return true } - companion object { - val epochMatcher: Regex by lazy { Regex(":([0-9]*),") } - val notifIdMatcher: Regex by lazy { Regex("notif_id\":([0-9]*),") } + fun fetchNotifications(data: CookieModel) { + L.i("Notif fetch for $data") + val doc = Jsoup.connect(FbTab.NOTIFICATIONS.url).cookie(FACEBOOK_COM, data.cookie).get() + //aclb for unread, acw for read + val unreadNotifications = doc.getElementById("notifications_list").getElementsByClass("aclb") + var notifCount = 0 +// val prevLatestEpoch = 1498931565L // for testing + val prevLatestEpoch = lastNotificationTime(data.id) + 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 + notif.createNotification(this@NotificationService) + if (notif.timestamp > newLatestEpoch) + newLatestEpoch = notif.timestamp + notifCount++ + } + if (newLatestEpoch != prevLatestEpoch) saveNotificationTime(NotificationModel(data.id, newLatestEpoch)) + frostAnswersCustom("Notifications") { putCustomAttribute("Count", notifCount) } + summaryNotification(data.id, notifCount) } + fun parseNotification(data: CookieModel, element: Element): NotificationContent? { val a = element.getElementsByTag("a").first() ?: return null + //fetch id val dataStore = a.attr("data-store") val notifId = if (dataStore == null) System.currentTimeMillis() else notifIdMatcher.find(dataStore)?.groups?.get(1)?.value?.toLong() ?: System.currentTimeMillis() @@ -89,48 +97,21 @@ class NotificationService : JobService() { if (Prefs.notificationKeywords.any { text.contains(it, ignoreCase = true) }) return null //notification filtered out if (timeString != null) text = text.removeSuffix(timeString) text = text.trim() + //fetch epoch val abbrData = abbr?.attr("data-store") val epoch = if (abbrData == null) -1L else epochMatcher.find(abbrData)?.groups?.get(1)?.value?.toLong() ?: -1L - return NotificationContent(data, notifId.toInt(), a.attr("href"), text, epoch) + //fetch profpic + val p = element.select("i.img[style*=url]") + val pUrl = profMatcher.find(p.getOrNull(0)?.attr("style") ?: "")?.groups?.get(1)?.value ?: "" + return NotificationContent(data, notifId.toInt(), a.attr("href"), text, epoch, pUrl) } private fun Context.debugNotification(text: String) { - if (BuildConfig.DEBUG) { - val notifBuilder = frostNotification - .setContentTitle(string(R.string.app_name)) - .setContentText(text) - - 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, verifiedUser: Boolean = false) { - //in case we haven't found the name, we will try one more time before passing the notification - if (!verifiedUser && data.name?.isBlank() ?: true) { - data.fetchUsername { - data.name = it - createNotification(context, true) - } - } else { - val intent = Intent(context, FrostWebActivity::class.java) - intent.data = Uri.parse("$FB_URL_BASE$href") - intent.putExtra(WebOverlayActivity.ARG_USER_ID, data.id) - val group = "frost_${data.id}" - val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) - val notifBuilder = context.frostNotification - .setContentTitle(context.string(R.string.app_name)) - .setContentText(text) - .setContentIntent(pendingIntent) - .setCategory(Notification.CATEGORY_SOCIAL) - .setSubText(data.name) - .setGroup(group) - - if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000) - - NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.build()) - } - } + if (!BuildConfig.DEBUG) return + val notifBuilder = frostNotification + .setContentTitle(string(R.string.app_name)) + .setContentText(text) + NotificationManagerCompat.from(this).notify(999, notifBuilder.build()) } fun summaryNotification(userId: Long, count: Int) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt index a239e58f..52f7412f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt @@ -5,7 +5,6 @@ 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. |