mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-06-17 04:49:20 +00:00
Reformated all the code
This commit is contained in:
@ -6,36 +6,38 @@ import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
|
||||
class Connection<B: IBinder, S: ConnectionService<B>>(private val serviceClass: Class<S>,
|
||||
private val onBind: ((Connection<B, S>, B) -> Unit)? = null,
|
||||
private val onUnbind: ((Connection<B, S>, B) -> Unit)? = null): ServiceConnection {
|
||||
var binder: B? = null
|
||||
private set
|
||||
class Connection<B : IBinder, S : ConnectionService<B>>(
|
||||
private val serviceClass: Class<S>,
|
||||
private val onBind: ((Connection<B, S>, B) -> Unit)? = null,
|
||||
private val onUnbind: ((Connection<B, S>, B) -> Unit)? = null
|
||||
) : ServiceConnection {
|
||||
var binder: B? = null
|
||||
private set
|
||||
|
||||
private fun handleUnbind() {
|
||||
binder?.let {
|
||||
binder = null
|
||||
onUnbind?.invoke(this, it)
|
||||
private fun handleUnbind() {
|
||||
binder?.let {
|
||||
binder = null
|
||||
onUnbind?.invoke(this, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
binder as B
|
||||
this.binder = binder
|
||||
onBind?.invoke(this, binder)
|
||||
}
|
||||
override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
binder as B
|
||||
this.binder = binder
|
||||
onBind?.invoke(this, binder)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(componentName: ComponentName) {
|
||||
handleUnbind()
|
||||
}
|
||||
override fun onServiceDisconnected(componentName: ComponentName) {
|
||||
handleUnbind()
|
||||
}
|
||||
|
||||
fun bind(context: Context) {
|
||||
context.bindService(Intent(context, serviceClass), this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
fun bind(context: Context) {
|
||||
context.bindService(Intent(context, serviceClass), this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
fun unbind(context: Context) {
|
||||
context.unbindService(this)
|
||||
handleUnbind()
|
||||
}
|
||||
fun unbind(context: Context) {
|
||||
context.unbindService(this)
|
||||
handleUnbind()
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,17 @@ package com.looker.droidify.service
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
|
||||
abstract class ConnectionService<T: IBinder>: Service() {
|
||||
abstract override fun onBind(intent: Intent): T
|
||||
abstract class ConnectionService<T : IBinder> : Service() {
|
||||
abstract override fun onBind(intent: Intent): T
|
||||
|
||||
fun startSelf() {
|
||||
val intent = Intent(this, this::class.java)
|
||||
if (Android.sdk(26)) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
fun startSelf() {
|
||||
val intent = Intent(this, this::class.java)
|
||||
if (Android.sdk(26)) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,6 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.Common
|
||||
import com.looker.droidify.MainActivity
|
||||
@ -25,345 +21,449 @@ import com.looker.droidify.utility.Utils
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.*
|
||||
|
||||
class DownloadService: ConnectionService<DownloadService.Binder>() {
|
||||
companion object {
|
||||
private const val ACTION_OPEN = "${BuildConfig.APPLICATION_ID}.intent.action.OPEN"
|
||||
private const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||
private const val EXTRA_CACHE_FILE_NAME = "${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
companion object {
|
||||
private const val ACTION_OPEN = "${BuildConfig.APPLICATION_ID}.intent.action.OPEN"
|
||||
private const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||
private const val EXTRA_CACHE_FILE_NAME =
|
||||
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
|
||||
private val downloadingSubject = PublishSubject.create<State.Downloading>()
|
||||
}
|
||||
|
||||
class Receiver: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action.orEmpty()
|
||||
when {
|
||||
action.startsWith("$ACTION_OPEN.") -> {
|
||||
val packageName = action.substring(ACTION_OPEN.length + 1)
|
||||
context.startActivity(Intent(context, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW).setData(Uri.parse("package:$packageName"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
action.startsWith("$ACTION_INSTALL.") -> {
|
||||
val packageName = action.substring(ACTION_INSTALL.length + 1)
|
||||
val cacheFileName = intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
context.startActivity(Intent(context, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_INSTALL).setData(Uri.parse("package:$packageName"))
|
||||
.putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, cacheFileName)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class State(val packageName: String, val name: String) {
|
||||
class Pending(packageName: String, name: String): State(packageName, name)
|
||||
class Connecting(packageName: String, name: String): State(packageName, name)
|
||||
class Downloading(packageName: String, name: String, val read: Long, val total: Long?): State(packageName, name)
|
||||
class Success(packageName: String, name: String, val release: Release,
|
||||
val consume: () -> Unit): State(packageName, name)
|
||||
class Error(packageName: String, name: String): State(packageName, name)
|
||||
class Cancel(packageName: String, name: String): State(packageName, name)
|
||||
}
|
||||
|
||||
private val stateSubject = PublishSubject.create<State>()
|
||||
|
||||
private class Task(val packageName: String, val name: String, val release: Release,
|
||||
val url: String, val authentication: String) {
|
||||
val notificationTag: String
|
||||
get() = "download-$packageName"
|
||||
}
|
||||
|
||||
private data class CurrentTask(val task: Task, val disposable: Disposable, val lastState: State)
|
||||
|
||||
private var started = false
|
||||
private val tasks = mutableListOf<Task>()
|
||||
private var currentTask: CurrentTask? = null
|
||||
|
||||
inner class Binder: android.os.Binder() {
|
||||
fun events(packageName: String): Observable<State> {
|
||||
return stateSubject.filter { it.packageName == packageName }
|
||||
private val downloadingSubject = PublishSubject.create<State.Downloading>()
|
||||
}
|
||||
|
||||
fun enqueue(packageName: String, name: String, repository: Repository, release: Release) {
|
||||
val task = Task(packageName, name, release, release.getDownloadUrl(repository), repository.authentication)
|
||||
if (Cache.getReleaseFile(this@DownloadService, release.cacheFileName).exists()) {
|
||||
publishSuccess(task)
|
||||
} else {
|
||||
cancelTasks(packageName)
|
||||
cancelCurrentTask(packageName)
|
||||
notificationManager.cancel(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING)
|
||||
tasks += task
|
||||
if (currentTask == null) {
|
||||
handleDownload()
|
||||
} else {
|
||||
stateSubject.onNext(State.Pending(packageName, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(packageName: String) {
|
||||
cancelTasks(packageName)
|
||||
cancelCurrentTask(packageName)
|
||||
handleDownload()
|
||||
}
|
||||
|
||||
fun getState(packageName: String): State? = currentTask
|
||||
?.let { if (it.task.packageName == packageName) it.lastState else null }
|
||||
?: tasks.find { it.packageName == packageName }?.let { State.Pending(it.packageName, it.name) }
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
override fun onBind(intent: Intent): Binder = binder
|
||||
|
||||
private var downloadingDisposable: Disposable? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (Android.sdk(26)) {
|
||||
NotificationChannel(Common.NOTIFICATION_CHANNEL_DOWNLOADING,
|
||||
getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW)
|
||||
.apply { setShowBadge(false) }
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
|
||||
downloadingDisposable = downloadingSubject
|
||||
.sample(500L, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribe { publishForegroundState(false, it) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
downloadingDisposable?.dispose()
|
||||
downloadingDisposable = null
|
||||
cancelTasks(null)
|
||||
cancelCurrentTask(null)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_CANCEL) {
|
||||
currentTask?.let { binder.cancel(it.task.packageName) }
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun cancelTasks(packageName: String?) {
|
||||
tasks.removeAll {
|
||||
(packageName == null || it.packageName == packageName) && run {
|
||||
stateSubject.onNext(State.Cancel(it.packageName, it.name))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelCurrentTask(packageName: String?) {
|
||||
currentTask?.let {
|
||||
if (packageName == null || it.task.packageName == packageName) {
|
||||
currentTask = null
|
||||
stateSubject.onNext(State.Cancel(it.task.packageName, it.task.name))
|
||||
it.disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ValidationError { INTEGRITY, FORMAT, METADATA, SIGNATURE, PERMISSIONS }
|
||||
|
||||
private sealed class ErrorType {
|
||||
object Network: ErrorType()
|
||||
object Http: ErrorType()
|
||||
class Validation(val validateError: ValidationError): ErrorType()
|
||||
}
|
||||
|
||||
private fun showNotificationError(task: Task, errorType: ErrorType) {
|
||||
notificationManager.notify(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getBroadcast(this, 0, Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_OPEN.${task.packageName}"), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.apply {
|
||||
when (errorType) {
|
||||
is ErrorType.Network -> {
|
||||
setContentTitle(getString(R.string.could_not_download_FORMAT, task.name))
|
||||
setContentText(getString(R.string.network_error_DESC))
|
||||
}
|
||||
is ErrorType.Http -> {
|
||||
setContentTitle(getString(R.string.could_not_download_FORMAT, task.name))
|
||||
setContentText(getString(R.string.http_error_DESC))
|
||||
}
|
||||
is ErrorType.Validation -> {
|
||||
setContentTitle(getString(R.string.could_not_validate_FORMAT, task.name))
|
||||
setContentText(getString(when (errorType.validateError) {
|
||||
ValidationError.INTEGRITY -> R.string.integrity_check_error_DESC
|
||||
ValidationError.FORMAT -> R.string.file_format_error_DESC
|
||||
ValidationError.METADATA -> R.string.invalid_metadata_error_DESC
|
||||
ValidationError.SIGNATURE -> R.string.invalid_signature_error_DESC
|
||||
ValidationError.PERMISSIONS -> R.string.invalid_permissions_error_DESC
|
||||
}))
|
||||
}
|
||||
}::class
|
||||
}
|
||||
.build())
|
||||
}
|
||||
|
||||
private fun showNotificationInstall(task: Task) {
|
||||
notificationManager.notify(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getBroadcast(this, 0, Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_INSTALL.${task.packageName}")
|
||||
.putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentTitle(getString(R.string.downloaded_FORMAT, task.name))
|
||||
.setContentText(getString(R.string.tap_to_install_DESC))
|
||||
.build())
|
||||
}
|
||||
|
||||
private fun publishSuccess(task: Task) {
|
||||
var consumed = false
|
||||
stateSubject.onNext(State.Success(task.packageName, task.name, task.release) { consumed = true })
|
||||
if (!consumed) {
|
||||
showNotificationInstall(task)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatePackage(task: Task, file: File): ValidationError? {
|
||||
val hash = try {
|
||||
val hashType = task.release.hashType.nullIfEmpty() ?: "SHA256"
|
||||
val digest = MessageDigest.getInstance(hashType)
|
||||
file.inputStream().use {
|
||||
val bytes = ByteArray(8 * 1024)
|
||||
generateSequence { it.read(bytes) }.takeWhile { it >= 0 }.forEach { digest.update(bytes, 0, it) }
|
||||
digest.digest().hex()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
return if (hash.isEmpty() || hash != task.release.hash) {
|
||||
ValidationError.INTEGRITY
|
||||
} else {
|
||||
val packageInfo = try {
|
||||
packageManager.getPackageArchiveInfo(file.path, Android.PackageManager.signaturesFlag)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
if (packageInfo == null) {
|
||||
ValidationError.FORMAT
|
||||
} else if (packageInfo.packageName != task.packageName ||
|
||||
packageInfo.versionCodeCompat != task.release.versionCode) {
|
||||
ValidationError.METADATA
|
||||
} else {
|
||||
val signature = packageInfo.singleSignature?.let(Utils::calculateHash).orEmpty()
|
||||
if (signature.isEmpty() || signature != task.release.signature) {
|
||||
ValidationError.SIGNATURE
|
||||
} else {
|
||||
val permissions = packageInfo.permissions?.asSequence().orEmpty().map { it.name }.toSet()
|
||||
if (!task.release.permissions.containsAll(permissions)) {
|
||||
ValidationError.PERMISSIONS
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val stateNotificationBuilder by lazy { NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.addAction(0, getString(R.string.cancel), PendingIntent.getService(this, 0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)) }
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask != null) {
|
||||
currentTask = currentTask?.copy(lastState = state)
|
||||
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
|
||||
when (state) {
|
||||
is State.Connecting -> {
|
||||
setContentTitle(getString(R.string.downloading_FORMAT, state.name))
|
||||
setContentText(getString(R.string.connecting))
|
||||
setProgress(1, 0, true)
|
||||
}
|
||||
is State.Downloading -> {
|
||||
setContentTitle(getString(R.string.downloading_FORMAT, state.name))
|
||||
if (state.total != null) {
|
||||
setContentText("${state.read.formatSize()} / ${state.total.formatSize()}")
|
||||
setProgress(100, (100f * state.read / state.total).roundToInt(), false)
|
||||
} else {
|
||||
setContentText(state.read.formatSize())
|
||||
setProgress(0, 0, true)
|
||||
class Receiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action.orEmpty()
|
||||
when {
|
||||
action.startsWith("$ACTION_OPEN.") -> {
|
||||
val packageName = action.substring(ACTION_OPEN.length + 1)
|
||||
context.startActivity(
|
||||
Intent(context, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
action.startsWith("$ACTION_INSTALL.") -> {
|
||||
val packageName = action.substring(ACTION_INSTALL.length + 1)
|
||||
val cacheFileName = intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
context.startActivity(
|
||||
Intent(context, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_INSTALL)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
.putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, cacheFileName)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is State.Pending, is State.Success, is State.Error, is State.Cancel -> {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}::class
|
||||
}.build())
|
||||
stateSubject.onNext(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDownload() {
|
||||
if (currentTask == null) {
|
||||
if (tasks.isNotEmpty()) {
|
||||
val task = tasks.removeAt(0)
|
||||
if (!started) {
|
||||
started = true
|
||||
startSelf()
|
||||
}
|
||||
val initialState = State.Connecting(task.packageName, task.name)
|
||||
stateNotificationBuilder.setWhen(System.currentTimeMillis())
|
||||
publishForegroundState(true, initialState)
|
||||
val partialReleaseFile = Cache.getPartialReleaseFile(this, task.release.cacheFileName)
|
||||
lateinit var disposable: Disposable
|
||||
disposable = Downloader
|
||||
.download(task.url, partialReleaseFile, "", "", task.authentication) { read, total ->
|
||||
if (!disposable.isDisposed) {
|
||||
downloadingSubject.onNext(State.Downloading(task.packageName, task.name, read, total))
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
currentTask = null
|
||||
throwable?.printStackTrace()
|
||||
if (result == null || !result.success) {
|
||||
showNotificationError(task, if (result != null) ErrorType.Http else ErrorType.Network)
|
||||
stateSubject.onNext(State.Error(task.packageName, task.name))
|
||||
} else {
|
||||
val validationError = validatePackage(task, partialReleaseFile)
|
||||
if (validationError == null) {
|
||||
val releaseFile = Cache.getReleaseFile(this, task.release.cacheFileName)
|
||||
partialReleaseFile.renameTo(releaseFile)
|
||||
}
|
||||
|
||||
sealed class State(val packageName: String, val name: String) {
|
||||
class Pending(packageName: String, name: String) : State(packageName, name)
|
||||
class Connecting(packageName: String, name: String) : State(packageName, name)
|
||||
class Downloading(packageName: String, name: String, val read: Long, val total: Long?) :
|
||||
State(packageName, name)
|
||||
|
||||
class Success(
|
||||
packageName: String, name: String, val release: Release,
|
||||
val consume: () -> Unit
|
||||
) : State(packageName, name)
|
||||
|
||||
class Error(packageName: String, name: String) : State(packageName, name)
|
||||
class Cancel(packageName: String, name: String) : State(packageName, name)
|
||||
}
|
||||
|
||||
private val stateSubject = PublishSubject.create<State>()
|
||||
|
||||
private class Task(
|
||||
val packageName: String, val name: String, val release: Release,
|
||||
val url: String, val authentication: String
|
||||
) {
|
||||
val notificationTag: String
|
||||
get() = "download-$packageName"
|
||||
}
|
||||
|
||||
private data class CurrentTask(val task: Task, val disposable: Disposable, val lastState: State)
|
||||
|
||||
private var started = false
|
||||
private val tasks = mutableListOf<Task>()
|
||||
private var currentTask: CurrentTask? = null
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
fun events(packageName: String): Observable<State> {
|
||||
return stateSubject.filter { it.packageName == packageName }
|
||||
}
|
||||
|
||||
fun enqueue(packageName: String, name: String, repository: Repository, release: Release) {
|
||||
val task = Task(
|
||||
packageName,
|
||||
name,
|
||||
release,
|
||||
release.getDownloadUrl(repository),
|
||||
repository.authentication
|
||||
)
|
||||
if (Cache.getReleaseFile(this@DownloadService, release.cacheFileName).exists()) {
|
||||
publishSuccess(task)
|
||||
} else {
|
||||
partialReleaseFile.delete()
|
||||
showNotificationError(task, ErrorType.Validation(validationError))
|
||||
stateSubject.onNext(State.Error(task.packageName, task.name))
|
||||
}
|
||||
} else {
|
||||
cancelTasks(packageName)
|
||||
cancelCurrentTask(packageName)
|
||||
notificationManager.cancel(task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING)
|
||||
tasks += task
|
||||
if (currentTask == null) {
|
||||
handleDownload()
|
||||
} else {
|
||||
stateSubject.onNext(State.Pending(packageName, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(packageName: String) {
|
||||
cancelTasks(packageName)
|
||||
cancelCurrentTask(packageName)
|
||||
handleDownload()
|
||||
}
|
||||
currentTask = CurrentTask(task, disposable, initialState)
|
||||
} else if (started) {
|
||||
started = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
fun getState(packageName: String): State? = currentTask
|
||||
?.let { if (it.task.packageName == packageName) it.lastState else null }
|
||||
?: tasks.find { it.packageName == packageName }
|
||||
?.let { State.Pending(it.packageName, it.name) }
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
override fun onBind(intent: Intent): Binder = binder
|
||||
|
||||
private var downloadingDisposable: Disposable? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (Android.sdk(26)) {
|
||||
NotificationChannel(
|
||||
Common.NOTIFICATION_CHANNEL_DOWNLOADING,
|
||||
getString(R.string.downloading), NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
.apply { setShowBadge(false) }
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
|
||||
downloadingDisposable = downloadingSubject
|
||||
.sample(500L, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribe { publishForegroundState(false, it) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
downloadingDisposable?.dispose()
|
||||
downloadingDisposable = null
|
||||
cancelTasks(null)
|
||||
cancelCurrentTask(null)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_CANCEL) {
|
||||
currentTask?.let { binder.cancel(it.task.packageName) }
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun cancelTasks(packageName: String?) {
|
||||
tasks.removeAll {
|
||||
(packageName == null || it.packageName == packageName) && run {
|
||||
stateSubject.onNext(State.Cancel(it.packageName, it.name))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelCurrentTask(packageName: String?) {
|
||||
currentTask?.let {
|
||||
if (packageName == null || it.task.packageName == packageName) {
|
||||
currentTask = null
|
||||
stateSubject.onNext(State.Cancel(it.task.packageName, it.task.name))
|
||||
it.disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ValidationError { INTEGRITY, FORMAT, METADATA, SIGNATURE, PERMISSIONS }
|
||||
|
||||
private sealed class ErrorType {
|
||||
object Network : ErrorType()
|
||||
object Http : ErrorType()
|
||||
class Validation(val validateError: ValidationError) : ErrorType()
|
||||
}
|
||||
|
||||
private fun showNotificationError(task: Task, errorType: ErrorType) {
|
||||
notificationManager.notify(task.notificationTag,
|
||||
Common.NOTIFICATION_ID_DOWNLOADING,
|
||||
NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_OPEN.${task.packageName}"),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.apply {
|
||||
when (errorType) {
|
||||
is ErrorType.Network -> {
|
||||
setContentTitle(
|
||||
getString(
|
||||
R.string.could_not_download_FORMAT,
|
||||
task.name
|
||||
)
|
||||
)
|
||||
setContentText(getString(R.string.network_error_DESC))
|
||||
}
|
||||
is ErrorType.Http -> {
|
||||
setContentTitle(
|
||||
getString(
|
||||
R.string.could_not_download_FORMAT,
|
||||
task.name
|
||||
)
|
||||
)
|
||||
setContentText(getString(R.string.http_error_DESC))
|
||||
}
|
||||
is ErrorType.Validation -> {
|
||||
setContentTitle(
|
||||
getString(
|
||||
R.string.could_not_validate_FORMAT,
|
||||
task.name
|
||||
)
|
||||
)
|
||||
setContentText(
|
||||
getString(
|
||||
when (errorType.validateError) {
|
||||
ValidationError.INTEGRITY -> R.string.integrity_check_error_DESC
|
||||
ValidationError.FORMAT -> R.string.file_format_error_DESC
|
||||
ValidationError.METADATA -> R.string.invalid_metadata_error_DESC
|
||||
ValidationError.SIGNATURE -> R.string.invalid_signature_error_DESC
|
||||
ValidationError.PERMISSIONS -> R.string.invalid_permissions_error_DESC
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}::class
|
||||
}
|
||||
.build())
|
||||
}
|
||||
|
||||
private fun showNotificationInstall(task: Task) {
|
||||
notificationManager.notify(
|
||||
task.notificationTag, Common.NOTIFICATION_ID_DOWNLOADING, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_INSTALL.${task.packageName}")
|
||||
.putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.setContentTitle(getString(R.string.downloaded_FORMAT, task.name))
|
||||
.setContentText(getString(R.string.tap_to_install_DESC))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun publishSuccess(task: Task) {
|
||||
var consumed = false
|
||||
stateSubject.onNext(State.Success(task.packageName, task.name, task.release) {
|
||||
consumed = true
|
||||
})
|
||||
if (!consumed) {
|
||||
showNotificationInstall(task)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatePackage(task: Task, file: File): ValidationError? {
|
||||
val hash = try {
|
||||
val hashType = task.release.hashType.nullIfEmpty() ?: "SHA256"
|
||||
val digest = MessageDigest.getInstance(hashType)
|
||||
file.inputStream().use {
|
||||
val bytes = ByteArray(8 * 1024)
|
||||
generateSequence { it.read(bytes) }.takeWhile { it >= 0 }
|
||||
.forEach { digest.update(bytes, 0, it) }
|
||||
digest.digest().hex()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
return if (hash.isEmpty() || hash != task.release.hash) {
|
||||
ValidationError.INTEGRITY
|
||||
} else {
|
||||
val packageInfo = try {
|
||||
packageManager.getPackageArchiveInfo(
|
||||
file.path,
|
||||
Android.PackageManager.signaturesFlag
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
if (packageInfo == null) {
|
||||
ValidationError.FORMAT
|
||||
} else if (packageInfo.packageName != task.packageName ||
|
||||
packageInfo.versionCodeCompat != task.release.versionCode
|
||||
) {
|
||||
ValidationError.METADATA
|
||||
} else {
|
||||
val signature = packageInfo.singleSignature?.let(Utils::calculateHash).orEmpty()
|
||||
if (signature.isEmpty() || signature != task.release.signature) {
|
||||
ValidationError.SIGNATURE
|
||||
} else {
|
||||
val permissions =
|
||||
packageInfo.permissions?.asSequence().orEmpty().map { it.name }.toSet()
|
||||
if (!task.release.permissions.containsAll(permissions)) {
|
||||
ValidationError.PERMISSIONS
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val stateNotificationBuilder by lazy {
|
||||
NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.addAction(
|
||||
0, getString(R.string.cancel), PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask != null) {
|
||||
currentTask = currentTask?.copy(lastState = state)
|
||||
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
|
||||
when (state) {
|
||||
is State.Connecting -> {
|
||||
setContentTitle(getString(R.string.downloading_FORMAT, state.name))
|
||||
setContentText(getString(R.string.connecting))
|
||||
setProgress(1, 0, true)
|
||||
}
|
||||
is State.Downloading -> {
|
||||
setContentTitle(getString(R.string.downloading_FORMAT, state.name))
|
||||
if (state.total != null) {
|
||||
setContentText("${state.read.formatSize()} / ${state.total.formatSize()}")
|
||||
setProgress(100, (100f * state.read / state.total).roundToInt(), false)
|
||||
} else {
|
||||
setContentText(state.read.formatSize())
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}
|
||||
is State.Pending, is State.Success, is State.Error, is State.Cancel -> {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}::class
|
||||
}.build())
|
||||
stateSubject.onNext(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDownload() {
|
||||
if (currentTask == null) {
|
||||
if (tasks.isNotEmpty()) {
|
||||
val task = tasks.removeAt(0)
|
||||
if (!started) {
|
||||
started = true
|
||||
startSelf()
|
||||
}
|
||||
val initialState = State.Connecting(task.packageName, task.name)
|
||||
stateNotificationBuilder.setWhen(System.currentTimeMillis())
|
||||
publishForegroundState(true, initialState)
|
||||
val partialReleaseFile =
|
||||
Cache.getPartialReleaseFile(this, task.release.cacheFileName)
|
||||
lateinit var disposable: Disposable
|
||||
disposable = Downloader
|
||||
.download(
|
||||
task.url,
|
||||
partialReleaseFile,
|
||||
"",
|
||||
"",
|
||||
task.authentication
|
||||
) { read, total ->
|
||||
if (!disposable.isDisposed) {
|
||||
downloadingSubject.onNext(
|
||||
State.Downloading(
|
||||
task.packageName,
|
||||
task.name,
|
||||
read,
|
||||
total
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
currentTask = null
|
||||
throwable?.printStackTrace()
|
||||
if (result == null || !result.success) {
|
||||
showNotificationError(
|
||||
task,
|
||||
if (result != null) ErrorType.Http else ErrorType.Network
|
||||
)
|
||||
stateSubject.onNext(State.Error(task.packageName, task.name))
|
||||
} else {
|
||||
val validationError = validatePackage(task, partialReleaseFile)
|
||||
if (validationError == null) {
|
||||
val releaseFile =
|
||||
Cache.getReleaseFile(this, task.release.cacheFileName)
|
||||
partialReleaseFile.renameTo(releaseFile)
|
||||
publishSuccess(task)
|
||||
} else {
|
||||
partialReleaseFile.delete()
|
||||
showNotificationError(task, ErrorType.Validation(validationError))
|
||||
stateSubject.onNext(State.Error(task.packageName, task.name))
|
||||
}
|
||||
}
|
||||
handleDownload()
|
||||
}
|
||||
currentTask = CurrentTask(task, disposable, initialState)
|
||||
} else if (started) {
|
||||
started = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,6 @@ import android.text.style.ForegroundColorSpan
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import com.looker.droidify.BuildConfig
|
||||
import com.looker.droidify.Common
|
||||
import com.looker.droidify.MainActivity
|
||||
@ -27,394 +22,487 @@ import com.looker.droidify.entity.ProductItem
|
||||
import com.looker.droidify.entity.Repository
|
||||
import com.looker.droidify.index.RepositoryUpdater
|
||||
import com.looker.droidify.utility.RxUtils
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.android.asSequence
|
||||
import com.looker.droidify.utility.extension.android.notificationManager
|
||||
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
||||
import com.looker.droidify.utility.extension.text.formatSize
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SyncService: ConnectionService<SyncService.Binder>() {
|
||||
companion object {
|
||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||
class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
companion object {
|
||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||
|
||||
private val stateSubject = PublishSubject.create<State>()
|
||||
private val finishSubject = PublishSubject.create<Unit>()
|
||||
}
|
||||
|
||||
private sealed class State {
|
||||
data class Connecting(val name: String): State()
|
||||
data class Syncing(val name: String, val stage: RepositoryUpdater.Stage,
|
||||
val read: Long, val total: Long?): State()
|
||||
object Finishing: State()
|
||||
}
|
||||
|
||||
private class Task(val repositoryId: Long, val manual: Boolean)
|
||||
private data class CurrentTask(val task: Task?, val disposable: Disposable,
|
||||
val hasUpdates: Boolean, val lastState: State)
|
||||
private enum class Started { NO, AUTO, MANUAL }
|
||||
|
||||
private var started = Started.NO
|
||||
private val tasks = mutableListOf<Task>()
|
||||
private var currentTask: CurrentTask? = null
|
||||
|
||||
private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null
|
||||
|
||||
enum class SyncRequest { AUTO, MANUAL, FORCE }
|
||||
|
||||
inner class Binder: android.os.Binder() {
|
||||
val finish: Observable<Unit>
|
||||
get() = finishSubject
|
||||
|
||||
private fun sync(ids: List<Long>, request: SyncRequest) {
|
||||
val cancelledTask = cancelCurrentTask { request == SyncRequest.FORCE && it.task?.repositoryId in ids }
|
||||
cancelTasks { !it.manual && it.repositoryId in ids }
|
||||
val currentIds = tasks.asSequence().map { it.repositoryId }.toSet()
|
||||
val manual = request != SyncRequest.AUTO
|
||||
tasks += ids.asSequence().filter { it !in currentIds &&
|
||||
it != currentTask?.task?.repositoryId }.map { Task(it, manual) }
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
if (request != SyncRequest.AUTO && started == Started.AUTO) {
|
||||
started = Started.MANUAL
|
||||
startSelf()
|
||||
handleSetStarted()
|
||||
currentTask?.lastState?.let { publishForegroundState(true, it) }
|
||||
}
|
||||
private val stateSubject = PublishSubject.create<State>()
|
||||
private val finishSubject = PublishSubject.create<Unit>()
|
||||
}
|
||||
|
||||
fun sync(request: SyncRequest) {
|
||||
val ids = Database.RepositoryAdapter.getAll(null)
|
||||
.asSequence().filter { it.enabled }.map { it.id }.toList()
|
||||
sync(ids, request)
|
||||
private sealed class State {
|
||||
data class Connecting(val name: String) : State()
|
||||
data class Syncing(
|
||||
val name: String, val stage: RepositoryUpdater.Stage,
|
||||
val read: Long, val total: Long?
|
||||
) : State()
|
||||
|
||||
object Finishing : State()
|
||||
}
|
||||
|
||||
fun sync(repository: Repository) {
|
||||
if (repository.enabled) {
|
||||
sync(listOf(repository.id), SyncRequest.FORCE)
|
||||
}
|
||||
}
|
||||
private class Task(val repositoryId: Long, val manual: Boolean)
|
||||
private data class CurrentTask(
|
||||
val task: Task?, val disposable: Disposable,
|
||||
val hasUpdates: Boolean, val lastState: State
|
||||
)
|
||||
|
||||
fun cancelAuto(): Boolean {
|
||||
val removed = cancelTasks { !it.manual }
|
||||
val currentTask = cancelCurrentTask { it.task?.manual == false }
|
||||
handleNextTask(currentTask?.hasUpdates == true)
|
||||
return removed || currentTask != null
|
||||
}
|
||||
private enum class Started { NO, AUTO, MANUAL }
|
||||
|
||||
fun setUpdateNotificationBlocker(fragment: Fragment?) {
|
||||
updateNotificationBlockerFragment = fragment?.let(::WeakReference)
|
||||
if (fragment != null) {
|
||||
notificationManager.cancel(Common.NOTIFICATION_ID_UPDATES)
|
||||
}
|
||||
}
|
||||
private var started = Started.NO
|
||||
private val tasks = mutableListOf<Task>()
|
||||
private var currentTask: CurrentTask? = null
|
||||
|
||||
fun setEnabled(repository: Repository, enabled: Boolean): Boolean {
|
||||
Database.RepositoryAdapter.put(repository.enable(enabled))
|
||||
if (enabled) {
|
||||
if (repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id }) {
|
||||
tasks += Task(repository.id, true)
|
||||
handleNextTask(false)
|
||||
}
|
||||
} else {
|
||||
cancelTasks { it.repositoryId == repository.id }
|
||||
val cancelledTask = cancelCurrentTask { it.task?.repositoryId == repository.id }
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null
|
||||
|
||||
fun isCurrentlySyncing(repositoryId: Long): Boolean {
|
||||
return currentTask?.task?.repositoryId == repositoryId
|
||||
}
|
||||
enum class SyncRequest { AUTO, MANUAL, FORCE }
|
||||
|
||||
fun deleteRepository(repositoryId: Long): Boolean {
|
||||
val repository = Database.RepositoryAdapter.get(repositoryId)
|
||||
return repository != null && run {
|
||||
setEnabled(repository, false)
|
||||
Database.RepositoryAdapter.markAsDeleted(repository.id)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
inner class Binder : android.os.Binder() {
|
||||
val finish: Observable<Unit>
|
||||
get() = finishSubject
|
||||
|
||||
private val binder = Binder()
|
||||
override fun onBind(intent: Intent): Binder = binder
|
||||
|
||||
private var stateDisposable: Disposable? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (Android.sdk(26)) {
|
||||
NotificationChannel(Common.NOTIFICATION_CHANNEL_SYNCING,
|
||||
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW)
|
||||
.apply { setShowBadge(false) }
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
NotificationChannel(Common.NOTIFICATION_CHANNEL_UPDATES,
|
||||
getString(R.string.updates), NotificationManager.IMPORTANCE_LOW)
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
|
||||
stateDisposable = stateSubject
|
||||
.sample(500L, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribe { publishForegroundState(false, it) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
stateDisposable?.dispose()
|
||||
stateDisposable = null
|
||||
cancelTasks { true }
|
||||
cancelCurrentTask { true }
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_CANCEL) {
|
||||
tasks.clear()
|
||||
val cancelledTask = cancelCurrentTask { it.task != null }
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun cancelTasks(condition: (Task) -> Boolean): Boolean {
|
||||
return tasks.removeAll(condition)
|
||||
}
|
||||
|
||||
private fun cancelCurrentTask(condition: ((CurrentTask) -> Boolean)): CurrentTask? {
|
||||
return currentTask?.let {
|
||||
if (condition(it)) {
|
||||
currentTask = null
|
||||
it.disposable.dispose()
|
||||
RepositoryUpdater.await()
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotificationError(repository: Repository, exception: Exception) {
|
||||
notificationManager.notify("repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentTitle(getString(R.string.could_not_sync_FORMAT, repository.name))
|
||||
.setContentText(getString(when (exception) {
|
||||
is RepositoryUpdater.UpdateException -> when (exception.errorType) {
|
||||
RepositoryUpdater.ErrorType.NETWORK -> R.string.network_error_DESC
|
||||
RepositoryUpdater.ErrorType.HTTP -> R.string.http_error_DESC
|
||||
RepositoryUpdater.ErrorType.VALIDATION -> R.string.validation_index_error_DESC
|
||||
RepositoryUpdater.ErrorType.PARSING -> R.string.parsing_index_error_DESC
|
||||
}
|
||||
else -> R.string.unknown_error_DESC
|
||||
}))
|
||||
.build())
|
||||
}
|
||||
|
||||
private val stateNotificationBuilder by lazy { NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.addAction(0, getString(R.string.cancel), PendingIntent.getService(this, 0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)) }
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask?.lastState != state) {
|
||||
currentTask = currentTask?.copy(lastState = state)
|
||||
if (started == Started.MANUAL) {
|
||||
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
|
||||
when (state) {
|
||||
is State.Connecting -> {
|
||||
setContentTitle(getString(R.string.syncing_FORMAT, state.name))
|
||||
setContentText(getString(R.string.connecting))
|
||||
setProgress(0, 0, true)
|
||||
private fun sync(ids: List<Long>, request: SyncRequest) {
|
||||
val cancelledTask =
|
||||
cancelCurrentTask { request == SyncRequest.FORCE && it.task?.repositoryId in ids }
|
||||
cancelTasks { !it.manual && it.repositoryId in ids }
|
||||
val currentIds = tasks.asSequence().map { it.repositoryId }.toSet()
|
||||
val manual = request != SyncRequest.AUTO
|
||||
tasks += ids.asSequence().filter {
|
||||
it !in currentIds &&
|
||||
it != currentTask?.task?.repositoryId
|
||||
}.map { Task(it, manual) }
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
if (request != SyncRequest.AUTO && started == Started.AUTO) {
|
||||
started = Started.MANUAL
|
||||
startSelf()
|
||||
handleSetStarted()
|
||||
currentTask?.lastState?.let { publishForegroundState(true, it) }
|
||||
}
|
||||
is State.Syncing -> {
|
||||
setContentTitle(getString(R.string.syncing_FORMAT, state.name))
|
||||
when (state.stage) {
|
||||
RepositoryUpdater.Stage.DOWNLOAD -> {
|
||||
if (state.total != null) {
|
||||
setContentText("${state.read.formatSize()} / ${state.total.formatSize()}")
|
||||
setProgress(100, (100f * state.read / state.total).roundToInt(), false)
|
||||
} else {
|
||||
setContentText(state.read.formatSize())
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun sync(request: SyncRequest) {
|
||||
val ids = Database.RepositoryAdapter.getAll(null)
|
||||
.asSequence().filter { it.enabled }.map { it.id }.toList()
|
||||
sync(ids, request)
|
||||
}
|
||||
|
||||
fun sync(repository: Repository) {
|
||||
if (repository.enabled) {
|
||||
sync(listOf(repository.id), SyncRequest.FORCE)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAuto(): Boolean {
|
||||
val removed = cancelTasks { !it.manual }
|
||||
val currentTask = cancelCurrentTask { it.task?.manual == false }
|
||||
handleNextTask(currentTask?.hasUpdates == true)
|
||||
return removed || currentTask != null
|
||||
}
|
||||
|
||||
fun setUpdateNotificationBlocker(fragment: Fragment?) {
|
||||
updateNotificationBlockerFragment = fragment?.let(::WeakReference)
|
||||
if (fragment != null) {
|
||||
notificationManager.cancel(Common.NOTIFICATION_ID_UPDATES)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEnabled(repository: Repository, enabled: Boolean): Boolean {
|
||||
Database.RepositoryAdapter.put(repository.enable(enabled))
|
||||
if (enabled) {
|
||||
if (repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id }) {
|
||||
tasks += Task(repository.id, true)
|
||||
handleNextTask(false)
|
||||
}
|
||||
RepositoryUpdater.Stage.PROCESS -> {
|
||||
val progress = state.total?.let { 100f * state.read / it }?.roundToInt()
|
||||
setContentText(getString(R.string.processing_FORMAT, "${progress ?: 0}%"))
|
||||
setProgress(100, progress ?: 0, progress == null)
|
||||
} else {
|
||||
cancelTasks { it.repositoryId == repository.id }
|
||||
val cancelledTask = cancelCurrentTask { it.task?.repositoryId == repository.id }
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun isCurrentlySyncing(repositoryId: Long): Boolean {
|
||||
return currentTask?.task?.repositoryId == repositoryId
|
||||
}
|
||||
|
||||
fun deleteRepository(repositoryId: Long): Boolean {
|
||||
val repository = Database.RepositoryAdapter.get(repositoryId)
|
||||
return repository != null && run {
|
||||
setEnabled(repository, false)
|
||||
Database.RepositoryAdapter.markAsDeleted(repository.id)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
override fun onBind(intent: Intent): Binder = binder
|
||||
|
||||
private var stateDisposable: Disposable? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (Android.sdk(26)) {
|
||||
NotificationChannel(
|
||||
Common.NOTIFICATION_CHANNEL_SYNCING,
|
||||
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
.apply { setShowBadge(false) }
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
NotificationChannel(
|
||||
Common.NOTIFICATION_CHANNEL_UPDATES,
|
||||
getString(R.string.updates), NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
|
||||
stateDisposable = stateSubject
|
||||
.sample(500L, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribe { publishForegroundState(false, it) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
stateDisposable?.dispose()
|
||||
stateDisposable = null
|
||||
cancelTasks { true }
|
||||
cancelCurrentTask { true }
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_CANCEL) {
|
||||
tasks.clear()
|
||||
val cancelledTask = cancelCurrentTask { it.task != null }
|
||||
handleNextTask(cancelledTask?.hasUpdates == true)
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun cancelTasks(condition: (Task) -> Boolean): Boolean {
|
||||
return tasks.removeAll(condition)
|
||||
}
|
||||
|
||||
private fun cancelCurrentTask(condition: ((CurrentTask) -> Boolean)): CurrentTask? {
|
||||
return currentTask?.let {
|
||||
if (condition(it)) {
|
||||
currentTask = null
|
||||
it.disposable.dispose()
|
||||
RepositoryUpdater.await()
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotificationError(repository: Repository, exception: Exception) {
|
||||
notificationManager.notify(
|
||||
"repository-${repository.id}", Common.NOTIFICATION_ID_SYNCING, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentTitle(getString(R.string.could_not_sync_FORMAT, repository.name))
|
||||
.setContentText(
|
||||
getString(
|
||||
when (exception) {
|
||||
is RepositoryUpdater.UpdateException -> when (exception.errorType) {
|
||||
RepositoryUpdater.ErrorType.NETWORK -> R.string.network_error_DESC
|
||||
RepositoryUpdater.ErrorType.HTTP -> R.string.http_error_DESC
|
||||
RepositoryUpdater.ErrorType.VALIDATION -> R.string.validation_index_error_DESC
|
||||
RepositoryUpdater.ErrorType.PARSING -> R.string.parsing_index_error_DESC
|
||||
}
|
||||
else -> R.string.unknown_error_DESC
|
||||
}
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private val stateNotificationBuilder by lazy {
|
||||
NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_SYNCING)
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.addAction(
|
||||
0, getString(R.string.cancel), PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, this::class.java).setAction(ACTION_CANCEL),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun publishForegroundState(force: Boolean, state: State) {
|
||||
if (force || currentTask?.lastState != state) {
|
||||
currentTask = currentTask?.copy(lastState = state)
|
||||
if (started == Started.MANUAL) {
|
||||
startForeground(Common.NOTIFICATION_ID_SYNCING, stateNotificationBuilder.apply {
|
||||
when (state) {
|
||||
is State.Connecting -> {
|
||||
setContentTitle(getString(R.string.syncing_FORMAT, state.name))
|
||||
setContentText(getString(R.string.connecting))
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
is State.Syncing -> {
|
||||
setContentTitle(getString(R.string.syncing_FORMAT, state.name))
|
||||
when (state.stage) {
|
||||
RepositoryUpdater.Stage.DOWNLOAD -> {
|
||||
if (state.total != null) {
|
||||
setContentText("${state.read.formatSize()} / ${state.total.formatSize()}")
|
||||
setProgress(
|
||||
100,
|
||||
(100f * state.read / state.total).roundToInt(),
|
||||
false
|
||||
)
|
||||
} else {
|
||||
setContentText(state.read.formatSize())
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}
|
||||
RepositoryUpdater.Stage.PROCESS -> {
|
||||
val progress =
|
||||
state.total?.let { 100f * state.read / it }?.roundToInt()
|
||||
setContentText(
|
||||
getString(
|
||||
R.string.processing_FORMAT,
|
||||
"${progress ?: 0}%"
|
||||
)
|
||||
)
|
||||
setProgress(100, progress ?: 0, progress == null)
|
||||
}
|
||||
RepositoryUpdater.Stage.MERGE -> {
|
||||
val progress = (100f * state.read / (state.total
|
||||
?: state.read)).roundToInt()
|
||||
setContentText(
|
||||
getString(
|
||||
R.string.merging_FORMAT,
|
||||
"${state.read} / ${state.total ?: state.read}"
|
||||
)
|
||||
)
|
||||
setProgress(100, progress, false)
|
||||
}
|
||||
RepositoryUpdater.Stage.COMMIT -> {
|
||||
setContentText(getString(R.string.saving_details))
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
is State.Finishing -> {
|
||||
setContentTitle(getString(R.string.syncing))
|
||||
setContentText(null)
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}::class
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetStarted() {
|
||||
stateNotificationBuilder.setWhen(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
private fun handleNextTask(hasUpdates: Boolean) {
|
||||
if (currentTask == null) {
|
||||
if (tasks.isNotEmpty()) {
|
||||
val task = tasks.removeAt(0)
|
||||
val repository = Database.RepositoryAdapter.get(task.repositoryId)
|
||||
if (repository != null && repository.enabled) {
|
||||
val lastStarted = started
|
||||
val newStarted =
|
||||
if (task.manual || lastStarted == Started.MANUAL) Started.MANUAL else Started.AUTO
|
||||
started = newStarted
|
||||
if (newStarted == Started.MANUAL && lastStarted != Started.MANUAL) {
|
||||
startSelf()
|
||||
handleSetStarted()
|
||||
}
|
||||
val initialState = State.Connecting(repository.name)
|
||||
publishForegroundState(true, initialState)
|
||||
val unstable = Preferences[Preferences.Key.UpdateUnstable]
|
||||
lateinit var disposable: Disposable
|
||||
disposable = RepositoryUpdater
|
||||
.update(repository, unstable) { stage, progress, total ->
|
||||
if (!disposable.isDisposed) {
|
||||
stateSubject.onNext(
|
||||
State.Syncing(
|
||||
repository.name,
|
||||
stage,
|
||||
progress,
|
||||
total
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
currentTask = null
|
||||
throwable?.printStackTrace()
|
||||
if (throwable != null && task.manual) {
|
||||
showNotificationError(repository, throwable as Exception)
|
||||
}
|
||||
handleNextTask(result == true || hasUpdates)
|
||||
}
|
||||
currentTask = CurrentTask(task, disposable, hasUpdates, initialState)
|
||||
} else {
|
||||
handleNextTask(hasUpdates)
|
||||
}
|
||||
RepositoryUpdater.Stage.MERGE -> {
|
||||
val progress = (100f * state.read / (state.total ?: state.read)).roundToInt()
|
||||
setContentText(getString(R.string.merging_FORMAT, "${state.read} / ${state.total ?: state.read}"))
|
||||
setProgress(100, progress, false)
|
||||
} else if (started != Started.NO) {
|
||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
|
||||
val disposable = RxUtils
|
||||
.querySingle { it ->
|
||||
Database.ProductAdapter
|
||||
.query(
|
||||
installed = true,
|
||||
updates = true,
|
||||
searchQuery = "",
|
||||
section = ProductItem.Section.All,
|
||||
order = ProductItem.Order.NAME,
|
||||
signal = it
|
||||
)
|
||||
.use {
|
||||
it.asSequence().map(Database.ProductAdapter::transformItem)
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
throwable?.printStackTrace()
|
||||
currentTask = null
|
||||
handleNextTask(false)
|
||||
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
|
||||
if (!blocked && result != null && result.isNotEmpty()) {
|
||||
displayUpdatesNotification(result)
|
||||
}
|
||||
}
|
||||
currentTask = CurrentTask(null, disposable, true, State.Finishing)
|
||||
} else {
|
||||
finishSubject.onNext(Unit)
|
||||
val needStop = started == Started.MANUAL
|
||||
started = Started.NO
|
||||
if (needStop) {
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
RepositoryUpdater.Stage.COMMIT -> {
|
||||
setContentText(getString(R.string.saving_details))
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||
val maxUpdates = 5
|
||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||
notificationManager.notify(
|
||||
Common.NOTIFICATION_ID_UPDATES, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES)
|
||||
.setSmallIcon(R.drawable.ic_new_releases)
|
||||
.setContentTitle(getString(R.string.new_updates_available))
|
||||
.setContentText(
|
||||
resources.getQuantityString(
|
||||
R.plurals.new_updates_DESC_FORMAT,
|
||||
productItems.size, productItems.size
|
||||
)
|
||||
)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_UPDATES),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.setStyle(NotificationCompat.InboxStyle().applyHack {
|
||||
for (productItem in productItems.take(maxUpdates)) {
|
||||
val builder = SpannableStringBuilder(productItem.name)
|
||||
builder.setSpan(
|
||||
ForegroundColorSpan(Color.BLACK), 0, builder.length,
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
builder.append(' ').append(productItem.version)
|
||||
addLine(builder)
|
||||
}
|
||||
if (productItems.size > maxUpdates) {
|
||||
val summary =
|
||||
getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates)
|
||||
if (Android.sdk(24)) {
|
||||
addLine(summary)
|
||||
} else {
|
||||
setSummaryText(summary)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
class Job : JobService() {
|
||||
private var syncParams: JobParameters? = null
|
||||
private var syncDisposable: Disposable? = null
|
||||
private val syncConnection =
|
||||
Connection(SyncService::class.java, onBind = { connection, binder ->
|
||||
syncDisposable = binder.finish.subscribe {
|
||||
val params = syncParams
|
||||
if (params != null) {
|
||||
syncParams = null
|
||||
syncDisposable?.dispose()
|
||||
syncDisposable = null
|
||||
connection.unbind(this)
|
||||
jobFinished(params, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is State.Finishing -> {
|
||||
setContentTitle(getString(R.string.syncing))
|
||||
setContentText(null)
|
||||
setProgress(0, 0, true)
|
||||
}
|
||||
}::class
|
||||
}.build())
|
||||
}
|
||||
binder.sync(SyncRequest.AUTO)
|
||||
}, onUnbind = { _, binder ->
|
||||
syncDisposable?.dispose()
|
||||
syncDisposable = null
|
||||
binder.cancelAuto()
|
||||
val params = syncParams
|
||||
if (params != null) {
|
||||
syncParams = null
|
||||
jobFinished(params, true)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
syncParams = params
|
||||
syncConnection.bind(this)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
syncParams = null
|
||||
syncDisposable?.dispose()
|
||||
syncDisposable = null
|
||||
val reschedule = syncConnection.binder?.cancelAuto() == true
|
||||
syncConnection.unbind(this)
|
||||
return reschedule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetStarted() {
|
||||
stateNotificationBuilder.setWhen(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
private fun handleNextTask(hasUpdates: Boolean) {
|
||||
if (currentTask == null) {
|
||||
if (tasks.isNotEmpty()) {
|
||||
val task = tasks.removeAt(0)
|
||||
val repository = Database.RepositoryAdapter.get(task.repositoryId)
|
||||
if (repository != null && repository.enabled) {
|
||||
val lastStarted = started
|
||||
val newStarted = if (task.manual || lastStarted == Started.MANUAL) Started.MANUAL else Started.AUTO
|
||||
started = newStarted
|
||||
if (newStarted == Started.MANUAL && lastStarted != Started.MANUAL) {
|
||||
startSelf()
|
||||
handleSetStarted()
|
||||
}
|
||||
val initialState = State.Connecting(repository.name)
|
||||
publishForegroundState(true, initialState)
|
||||
val unstable = Preferences[Preferences.Key.UpdateUnstable]
|
||||
lateinit var disposable: Disposable
|
||||
disposable = RepositoryUpdater
|
||||
.update(repository, unstable) { stage, progress, total ->
|
||||
if (!disposable.isDisposed) {
|
||||
stateSubject.onNext(State.Syncing(repository.name, stage, progress, total))
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
currentTask = null
|
||||
throwable?.printStackTrace()
|
||||
if (throwable != null && task.manual) {
|
||||
showNotificationError(repository, throwable as Exception)
|
||||
}
|
||||
handleNextTask(result == true || hasUpdates)
|
||||
}
|
||||
currentTask = CurrentTask(task, disposable, hasUpdates, initialState)
|
||||
} else {
|
||||
handleNextTask(hasUpdates)
|
||||
}
|
||||
} else if (started != Started.NO) {
|
||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
|
||||
val disposable = RxUtils
|
||||
.querySingle { Database.ProductAdapter
|
||||
.query(true, true, "", ProductItem.Section.All, ProductItem.Order.NAME, it)
|
||||
.use { it.asSequence().map(Database.ProductAdapter::transformItem).toList() } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
throwable?.printStackTrace()
|
||||
currentTask = null
|
||||
handleNextTask(false)
|
||||
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
|
||||
if (!blocked && result != null && result.isNotEmpty()) {
|
||||
displayUpdatesNotification(result)
|
||||
}
|
||||
}
|
||||
currentTask = CurrentTask(null, disposable, true, State.Finishing)
|
||||
} else {
|
||||
finishSubject.onNext(Unit)
|
||||
val needStop = started == Started.MANUAL
|
||||
started = Started.NO
|
||||
if (needStop) {
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||
val maxUpdates = 5
|
||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||
notificationManager.notify(Common.NOTIFICATION_ID_UPDATES, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES)
|
||||
.setSmallIcon(R.drawable.ic_new_releases)
|
||||
.setContentTitle(getString(R.string.new_updates_available))
|
||||
.setContentText(resources.getQuantityString(R.plurals.new_updates_DESC_FORMAT,
|
||||
productItems.size, productItems.size))
|
||||
.setColor(ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorAccent).defaultColor)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_UPDATES), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setStyle(NotificationCompat.InboxStyle().applyHack {
|
||||
for (productItem in productItems.take(maxUpdates)) {
|
||||
val builder = SpannableStringBuilder(productItem.name)
|
||||
builder.setSpan(ForegroundColorSpan(Color.BLACK), 0, builder.length,
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
builder.append(' ').append(productItem.version)
|
||||
addLine(builder)
|
||||
}
|
||||
if (productItems.size > maxUpdates) {
|
||||
val summary = getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates)
|
||||
if (Android.sdk(24)) {
|
||||
addLine(summary)
|
||||
} else {
|
||||
setSummaryText(summary)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build())
|
||||
}
|
||||
|
||||
class Job: JobService() {
|
||||
private var syncParams: JobParameters? = null
|
||||
private var syncDisposable: Disposable? = null
|
||||
private val syncConnection = Connection(SyncService::class.java, onBind = { connection, binder ->
|
||||
syncDisposable = binder.finish.subscribe {
|
||||
val params = syncParams
|
||||
if (params != null) {
|
||||
syncParams = null
|
||||
syncDisposable?.dispose()
|
||||
syncDisposable = null
|
||||
connection.unbind(this)
|
||||
jobFinished(params, false)
|
||||
}
|
||||
}
|
||||
binder.sync(SyncRequest.AUTO)
|
||||
}, onUnbind = { _, binder ->
|
||||
syncDisposable?.dispose()
|
||||
syncDisposable = null
|
||||
binder.cancelAuto()
|
||||
val params = syncParams
|
||||
if (params != null) {
|
||||
syncParams = null
|
||||
jobFinished(params, true)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
syncParams = params
|
||||
syncConnection.bind(this)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
syncParams = null
|
||||
syncDisposable?.dispose()
|
||||
syncDisposable = null
|
||||
val reschedule = syncConnection.binder?.cancelAuto() == true
|
||||
syncConnection.unbind(this)
|
||||
return reschedule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user