Skip to content

Commit

Permalink
Updated service architecture and extracted useful tools to separate c…
Browse files Browse the repository at this point in the history
…lass
  • Loading branch information
diareuse authored and John Wu committed Jul 20, 2019
1 parent 452db51 commit 967bdea
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 124 deletions.
4 changes: 2 additions & 2 deletions app/src/main/java/a/k.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package a

import com.topjohnwu.magisk.model.download.CompoundDownloadService
import com.topjohnwu.magisk.model.download.DownloadService

class k : CompoundDownloadService()
class k : DownloadService()
4 changes: 2 additions & 2 deletions app/src/main/java/com/topjohnwu/magisk/ClassMap.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.topjohnwu.magisk

import com.topjohnwu.magisk.model.download.CompoundDownloadService
import com.topjohnwu.magisk.model.download.DownloadModuleService
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.magisk.ui.MainActivity
Expand All @@ -18,7 +18,7 @@ object ClassMap {
UpdateCheckService::class.java to a.g::class.java,
GeneralReceiver::class.java to a.h::class.java,
DownloadModuleService::class.java to a.j::class.java,
CompoundDownloadService::class.java to a.k::class.java,
DownloadService::class.java to a.k::class.java,
SuRequestActivity::class.java to a.m::class.java
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,39 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.annotation.RequiresPermission
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.provide
import java.io.File
import kotlin.random.Random.Default.nextInt

/* More of a facade for [RemoteFileService], but whatever... */
@SuppressLint("Registered")
open class CompoundDownloadService : SubstrateDownloadService() {
open class DownloadService : RemoteFileService() {

private val context get() = this
private val String.downloadsFile get() = File(Const.EXTERNAL_PATH, this)
private val File.type
get() = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension)
.orEmpty()

override fun map(subject: DownloadSubject, file: File): File = when (subject) {
is Module -> file // todo tbd
else -> file
}

override fun onFinished(file: File, subject: DownloadSubject) = when (subject) {
is Magisk -> onFinishedInternal(file, subject)
Expand Down Expand Up @@ -83,19 +100,44 @@ open class CompoundDownloadService : SubstrateDownloadService() {
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { setContentIntent(it) }

// ---

private fun moveToDownloads(file: File) {
val destination = file.name.downloadsFile
if (file != destination) {
destination.deleteRecursively()
file.copyTo(destination)
}
Utils.toast(
getString(R.string.internal_storage, "/Download/${file.name}"),
Toast.LENGTH_LONG
)
}

private fun fileIntent(fileName: String): Intent {
val file = fileName.downloadsFile
return Intent(Intent.ACTION_VIEW)
.setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

class Builder {
lateinit var subject: DownloadSubject
}

companion object {

@RequiresPermission(allOf = [Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE])
fun download(context: Context, subject: DownloadSubject) {
Intent(context, ClassMap[CompoundDownloadService::class.java])
.putExtra(ARG_URL, subject)
.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(it)
} else {
context.startService(it)
}
}
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
val builder = Builder().apply(argBuilder)
val intent = Intent(context, ClassMap[DownloadService::class.java])
.putExtra(ARG_URL, builder.subject)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.topjohnwu.magisk.model.download

import android.app.Notification
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import java.util.*
import kotlin.random.Random.Default.nextInt

abstract class NotificationService : Service() {

abstract val defaultNotification: NotificationCompat.Builder

private val manager get() = getSystemService<NotificationManager>()
private val hasNotifications get() = notifications.isEmpty()

private val notifications =
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())

override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.values.forEach { cancel(it.hashCode()) }
notifications.clear()
}

// --

protected fun update(
id: Int,
body: (NotificationCompat.Builder) -> Unit = {}
) {
val notification = notifications.getOrPut(id) { defaultNotification }

notify(id, notification.also(body).build())

if (notifications.size == 1) {
updateForeground()
}
}

protected fun finishWork(
id: Int,
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
) {
val currentNotification = remove(id)?.run(editBody) ?: let {
cancel(id)
return
}

updateForeground()

cancel(id)
notify(nextInt(), currentNotification.build())

if (!hasNotifications) {
stopForeground(true)
stopSelf()
}
}

// ---

private fun notify(id: Int, notification: Notification) {
manager?.notify(id, notification)
}

private fun cancel(id: Int) {
manager?.cancel(id)
}

private fun remove(id: Int) = notifications.remove(id)
.also { updateForeground() }

private fun updateForeground() {
runCatching { notifications.keys.first() to notifications.values.first() }
.getOrNull()
?.let { startForeground(it.first, it.second.build()) }
}

// --

override fun onBind(p0: Intent?): IBinder? = null
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
package com.topjohnwu.magisk.model.download

import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.FileRepository
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.provide
import com.topjohnwu.magisk.utils.writeToCachedFile
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.ShellUtils
Expand All @@ -24,35 +16,26 @@ import okhttp3.ResponseBody
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.io.File
import java.util.*
import kotlin.random.Random.Default.nextInt

abstract class SubstrateDownloadService : Service() {
abstract class RemoteFileService : NotificationService() {

private val repo by inject<FileRepository>()
private val manager get() = getSystemService<NotificationManager>()

private val notifications =
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())

override fun onBind(p0: Intent?): IBinder? = null
override val defaultNotification: NotificationCompat.Builder
get() = Notifications
.progress(this, "")
.setContentText(getString(R.string.download_local))

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
return START_REDELIVER_INTENT
}

override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.values.forEach { manager?.cancel(it.hashCode()) }
notifications.clear()
}

// ---

private fun start(subject: DownloadSubject) = search(subject)
.onErrorResumeNext(download(subject))
.doOnSubscribe { updateNotification(subject.hashCode()) { it.setContentTitle(subject.fileName) } }
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.fileName) } }
.subscribeK {
runCatching { onFinished(it, subject) }.onFailure { Timber.e(it) }
finish(it, subject)
Expand Down Expand Up @@ -84,97 +67,32 @@ abstract class SubstrateDownloadService : Service() {

private fun download(subject: DownloadSubject) = repo.downloadFile(subject.url)
.map { it.toFile(subject.hashCode(), subject.fileName) }
.map { map(subject, it) }

// ---

protected fun fileIntent(fileName: String): Intent {
val file = downloadsFile(fileName)
return Intent(Intent.ACTION_VIEW)
.setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

protected fun moveToDownloads(file: File) {
val destination = downloadsFile(file.name)
if (file != destination) {
destination.deleteRecursively()
file.copyTo(destination)
}
Utils.toast(
getString(R.string.internal_storage, "/Download/${file.name}"),
Toast.LENGTH_LONG
)
}

// ---

private val File.type
get() = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension)
.orEmpty()

private fun downloadsFile(name: String) = File(Const.EXTERNAL_PATH, name)

private fun ResponseBody.toFile(id: Int, name: String): File {
val maxRaw = contentLength()
val max = maxRaw / 1_000_000f

return writeToCachedFile(this@SubstrateDownloadService, name) {
return writeToCachedFile(this@RemoteFileService, name) {
val progress = it / 1_000_000f

updateNotification(id) { notification ->
update(id) { notification ->
notification
.setProgress(maxRaw.toInt(), it.toInt(), false)
.setContentText(getString(R.string.download_progress, progress, max))
}
}
}

private fun finish(file: File, subject: DownloadSubject) {
val currentNotification = notifications.remove(subject.hashCode()) ?: let {
manager?.cancel(subject.hashCode())
return
}
val notification = currentNotification.addActions(file, subject)
private fun finish(file: File, subject: DownloadSubject) = finishWork(subject.hashCode()) {
it.addActions(file, subject)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
.build()

updateForeground()

manager?.cancel(subject.hashCode())
manager?.notify(nextInt(), notification)

if (notifications.isEmpty()) {
stopForeground(true)
stopSelf()
}
}

private inline fun updateNotification(
id: Int,
body: (NotificationCompat.Builder) -> Unit = {}
) {
val notification = notifications.getOrPut(id) {
Notifications
.progress(this, "")
.setContentText(getString(R.string.download_local))
}

manager?.notify(id, notification.also(body).build())

if (notifications.size == 1) {
updateForeground()
}
}

private fun updateForeground() {
runCatching { notifications.keys.first() to notifications.values.first() }
.getOrNull()
?.let { startForeground(it.first, it.second.build()) }
}

// ---
Expand All @@ -188,6 +106,7 @@ abstract class SubstrateDownloadService : Service() {
subject: DownloadSubject
): NotificationCompat.Builder

protected abstract fun map(subject: DownloadSubject, file: File): File

companion object {
const val ARG_URL = "arg_url"
Expand Down
Loading

0 comments on commit 967bdea

Please sign in to comment.