package com.pitchedapps.frost.services import android.app.job.JobParameters import android.app.job.JobService import android.content.Context 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.FACEBOOK_COM import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT_BASIC 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 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. * * 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 */ class NotificationService : JobService() { var future: Future? = null 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") frostAnswersCustom("NotificationTime", "Type" to "Service force stop", "IM Included" to Prefs.notificationsInstantMessages, "Duration" to time) future?.cancel(true) future = null return false } fun finish(params: JobParameters?) { val time = System.currentTimeMillis() - startTime L.i("Notification service has finished in $time ms") frostAnswersCustom("NotificationTime", "Type" to "Service", "IM Included" to Prefs.notificationsInstantMessages, "Duration" to time) jobFinished(params, false) future?.cancel(true) future = null } override fun onStartJob(params: JobParameters?): Boolean { L.i("Fetching notifications") future = doAsync { 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) } finish(params) } return true } 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 = Jsoup.connect(FbItem.NOTIFICATIONS.url).cookie(FACEBOOK_COM, data.cookie).userAgent(USER_AGENT_BASIC).get() //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 = Jsoup.connect(FbItem.MESSAGES.url).cookie(FACEBOOK_COM, data.cookie).userAgent(USER_AGENT_BASIC).get() 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() .setContentTitle(string(R.string.frost_name)) .setContentText(text) NotificationManagerCompat.from(this).notify(999, notifBuilder.build()) } }