/* * Copyright 2018 Allan Wang * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.pitchedapps.frost.services import android.app.job.JobInfo import android.app.job.JobParameters import android.app.job.JobScheduler import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.BaseBundle import android.os.PersistableBundle import com.pitchedapps.frost.facebook.requests.RequestAuth import com.pitchedapps.frost.facebook.requests.fbAuth import com.pitchedapps.frost.facebook.requests.markNotificationRead import com.pitchedapps.frost.utils.EnumBundle import com.pitchedapps.frost.utils.EnumBundleCompanion import com.pitchedapps.frost.utils.EnumCompanion import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch /** * Created by Allan Wang on 28/12/17. */ /** * Private helper data */ private enum class FrostRequestCommands : EnumBundle { NOTIF_READ { override fun invoke(auth: RequestAuth, bundle: PersistableBundle) { val id = bundle.getLong(ARG_0, -1L) val success = auth.markNotificationRead(id).invoke() L.d { "Marked notif $id as read: $success" } } override fun propagate(bundle: BaseBundle) = FrostRunnable.prepareMarkNotificationRead( bundle.getLong(ARG_0), bundle.getCookie() ) }; override val bundleContract: EnumBundleCompanion get() = Companion /** * Call request with arguments inside bundle */ abstract fun invoke(auth: RequestAuth, bundle: PersistableBundle) /** * Return bundle builder given arguments in the old bundle * Must not write to old bundle! */ abstract fun propagate(bundle: BaseBundle): BaseBundle.() -> Unit companion object : EnumCompanion("frost_arg_commands", values()) } private const val ARG_COMMAND = "frost_request_command" private const val ARG_COOKIE = "frost_request_cookie" private const val ARG_0 = "frost_request_arg_0" private const val ARG_1 = "frost_request_arg_1" private const val ARG_2 = "frost_request_arg_2" private const val ARG_3 = "frost_request_arg_3" private fun BaseBundle.getCookie(): String = getString(ARG_COOKIE)!! private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie) /** * Singleton handler for running requests in [FrostRequestService] * Requests are typically completely decoupled from the UI, * and are optional enhancers. * * Nothing guarantees the completion time, or whether it even executes at all * * Design: * prepare function - creates a bundle binder * actor function - calls the service with the given arguments * * Global: * propagator - given a bundle with a command, extracts and executes the requests */ object FrostRunnable { fun prepareMarkNotificationRead(id: Long, cookie: String): BaseBundle.() -> Unit = { FrostRequestCommands.NOTIF_READ.put(this) putLong(ARG_0, id) putCookie(cookie) } fun markNotificationRead(context: Context, id: Long, cookie: String): Boolean { if (id <= 0) { L.d { "Invalid notification id $id for marking as read" } return false } return schedule( context, FrostRequestCommands.NOTIF_READ, prepareMarkNotificationRead(id, cookie) ) } fun propagate(context: Context, intent: Intent?) { val extras = intent?.extras ?: return val command = FrostRequestCommands[intent] ?: return intent.removeExtra(ARG_COMMAND) // reset L.d { "Propagating command ${command.name}" } val builder = command.propagate(extras) schedule(context, command, builder) } private fun schedule( context: Context, command: FrostRequestCommands, bundleBuilder: PersistableBundle.() -> Unit ): Boolean { val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler val serviceComponent = ComponentName(context, FrostRequestService::class.java) val bundle = PersistableBundle() bundle.bundleBuilder() bundle.putString(ARG_COMMAND, command.name) if (bundle.getCookie().isNullOrBlank()) { L.e { "Scheduled frost request with empty cookie" } return false } val builder = JobInfo.Builder(REQUEST_SERVICE_BASE + command.ordinal, serviceComponent) .setMinimumLatency(0L) .setExtras(bundle) .setOverrideDeadline(2000L) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) val result = scheduler.schedule(builder.build()) if (result <= 0) { L.eThrow("FrostRequestService scheduler failed for ${command.name}") return false } L.d { "Scheduled ${command.name}" } return true } } class FrostRequestService : BaseJobService() { override fun onStartJob(params: JobParameters?): Boolean { super.onStartJob(params) if (!Prefs.authRequests) { L.i { "Auth requests disabled; skipping request service" } return false } val bundle = params?.extras if (bundle == null) { L.eThrow("Launched ${this::class.java.simpleName} without param data") return false } val cookie = bundle.getCookie() if (cookie.isBlank()) { L.eThrow("Launched ${this::class.java.simpleName} without cookie") return false } val command = FrostRequestCommands[bundle] if (command == null) { L.eThrow("Launched ${this::class.java.simpleName} without command") return false } launch(Dispatchers.IO) { try { val auth = fbAuth.fetch(cookie).await() command.invoke(auth, bundle) L.d { "Finished frost service for ${command.name} in ${System.currentTimeMillis() - startTime} ms" } } catch (e: Exception) { L.e(e) { "Failed frost service for ${command.name} in ${System.currentTimeMillis() - startTime} ms" } } finally { jobFinished(params, false) } } return true } }