Remove more code using RxJava
This commit is contained in:
parent
f7a650b9a4
commit
6348d0a6fb
@ -13,16 +13,12 @@ import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.exists
|
||||
import com.topjohnwu.magisk.extensions.provide
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
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.*
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import io.reactivex.Completable
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
@ -37,20 +33,20 @@ open class DownloadService : RemoteFileService() {
|
||||
.getMimeTypeFromExtension(extension)
|
||||
?: "resource/folder"
|
||||
|
||||
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> onFinishedInternal(subject, id)
|
||||
is Module -> onFinishedInternal(subject, id)
|
||||
is Manager -> onFinishedInternal(subject, id)
|
||||
override suspend fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> onFinished(subject, id)
|
||||
is Module -> onFinished(subject, id)
|
||||
is Manager -> onFinished(subject, id)
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
private suspend fun onFinished(
|
||||
subject: Magisk,
|
||||
id: Int
|
||||
) = when (val conf = subject.configuration) {
|
||||
Uninstall -> FlashFragment.uninstall(subject.file, id)
|
||||
EnvFix -> {
|
||||
remove(id)
|
||||
GlobalScope.launch { EnvFixTask(subject.file).exec() }
|
||||
EnvFixTask(subject.file).exec()
|
||||
Unit
|
||||
}
|
||||
is Patch -> FlashFragment.patch(subject.file, conf.fileUri, id)
|
||||
@ -58,7 +54,7 @@ open class DownloadService : RemoteFileService() {
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
private fun onFinished(
|
||||
subject: Module,
|
||||
id: Int
|
||||
) = when (subject.configuration) {
|
||||
@ -66,33 +62,30 @@ open class DownloadService : RemoteFileService() {
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
private suspend fun onFinished(
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
Completable.fromAction {
|
||||
handleAPK(subject)
|
||||
}.subscribeK {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
is APK.Restore -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActionsInternal(subject)
|
||||
is Module -> addActionsInternal(subject)
|
||||
is Manager -> addActionsInternal(subject)
|
||||
is Magisk -> addActions(subject)
|
||||
is Module -> addActions(subject)
|
||||
is Manager -> addActions(subject)
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Magisk)
|
||||
private fun Notification.Builder.addActions(subject: Magisk)
|
||||
= when (val conf = subject.configuration) {
|
||||
Download -> this.apply {
|
||||
Download -> apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
@ -101,18 +94,12 @@ open class DownloadService : RemoteFileService() {
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
|
||||
is Flash -> setContentIntent(
|
||||
FlashFragment.flashIntent(
|
||||
context,
|
||||
subject.file,
|
||||
conf is Secondary
|
||||
)
|
||||
)
|
||||
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, conf is Secondary))
|
||||
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, conf.fileUri))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Module)
|
||||
private fun Notification.Builder.addActions(subject: Module)
|
||||
= when (subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
@ -126,7 +113,7 @@ open class DownloadService : RemoteFileService() {
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun Notification.Builder.addActionsInternal(subject: Manager)
|
||||
private fun Notification.Builder.addActions(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
|
@ -32,14 +32,14 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
|
||||
private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
private suspend fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
if (isRunningAsStub) {
|
||||
// Move to upgrade location
|
||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
||||
apk.delete()
|
||||
if (Info.stub!!.version < Info.remote.stub.versionCode) {
|
||||
// We also want to upgrade stub
|
||||
service.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||
service.fetchFile(Info.remote.stub.link).byteStream().use {
|
||||
it.writeTo(apk)
|
||||
}
|
||||
patch(apk, id)
|
||||
@ -65,7 +65,7 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||
}
|
||||
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) =
|
||||
suspend fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) =
|
||||
when (subject.configuration) {
|
||||
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
|
@ -5,6 +5,9 @@ import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.koin.core.KoinComponent
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
@ -16,12 +19,19 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
|
||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
||||
|
||||
val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
abstract fun createNotification(): Notification.Builder
|
||||
|
||||
// --
|
||||
|
@ -9,17 +9,17 @@ import com.topjohnwu.magisk.core.ForegroundTracker
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
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.superuser.ShellUtils
|
||||
import io.reactivex.Completable
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.KoinComponent
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class RemoteFileService : NotificationService() {
|
||||
@ -29,7 +29,14 @@ abstract class RemoteFileService : NotificationService() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let {
|
||||
update(it.hashCode())
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
start(it)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
failNotify(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
@ -38,37 +45,24 @@ abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
// ---
|
||||
|
||||
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
||||
.onErrorResumeNext { download(subject) }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
failNotify(subject)
|
||||
}) {
|
||||
private suspend fun start(subject: DownloadSubject) {
|
||||
if (subject !is Magisk ||
|
||||
!subject.file.exists() ||
|
||||
!ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) {
|
||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
is Module ->
|
||||
stream.toModule(subject.file, service.fetchInstaller().byteStream())
|
||||
else ->
|
||||
stream.writeTo(subject.file)
|
||||
}
|
||||
}
|
||||
val newId = finishNotify(subject)
|
||||
if (ForegroundTracker.hasForeground) {
|
||||
onFinished(subject, newId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||
check(subject is Magisk) { "Download cache is disabled" }
|
||||
check(subject.file.exists() &&
|
||||
ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) {
|
||||
"The given file does not match checksum"
|
||||
}
|
||||
}
|
||||
|
||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||
.map { it.toProgressStream(subject) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
|
||||
val maxRaw = contentLength()
|
||||
val max = maxRaw / 1_000_000f
|
||||
@ -112,8 +106,7 @@ abstract class RemoteFileService : NotificationService() {
|
||||
// ---
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||
protected abstract suspend fun onFinished(subject: DownloadSubject, id: Int)
|
||||
|
||||
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
: Notification.Builder
|
||||
|
@ -31,7 +31,7 @@ data class Repo(
|
||||
|
||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
val readme get() = stringRepo.getReadme(this)
|
||||
suspend fun readme() = stringRepo.getReadme(this)
|
||||
|
||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||
|
||||
|
@ -15,7 +15,9 @@ import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.*
|
||||
import com.topjohnwu.magisk.extensions.readUri
|
||||
import com.topjohnwu.magisk.extensions.reboot
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import com.topjohnwu.magisk.model.events.dialog.EnvFixDialog
|
||||
import com.topjohnwu.signing.SignBoot
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@ -322,7 +324,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
tarOut = null
|
||||
it
|
||||
} ?: destFile.outputStream()
|
||||
patched.suInputStream().use { it.copyTo(os); os.close() }
|
||||
SuFileInputStream(patched).use { it.copyTo(os); os.close() }
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $destFile")
|
||||
Timber.e(e)
|
||||
@ -338,10 +340,10 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun postOTA(): Boolean {
|
||||
private suspend fun postOTA(): Boolean {
|
||||
val bootctl = SuFile("/data/adb/bootctl")
|
||||
try {
|
||||
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
|
||||
withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
|
||||
input, out -> input.copyTo(out)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
@ -368,7 +370,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
|
||||
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
||||
|
||||
protected fun secondSlot() =
|
||||
protected suspend fun secondSlot() =
|
||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv(zip: File): Boolean {
|
||||
@ -379,7 +381,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract fun operations(): Boolean
|
||||
protected abstract suspend fun operations(): Boolean
|
||||
|
||||
open suspend fun exec() = withContext(Dispatchers.IO) { operations() }
|
||||
}
|
||||
@ -407,7 +409,7 @@ sealed class MagiskInstaller(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(file, console, logs) {
|
||||
override fun operations() = doPatchFile(uri)
|
||||
override suspend fun operations() = doPatchFile(uri)
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
@ -415,7 +417,7 @@ sealed class MagiskInstaller(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(file, console, logs) {
|
||||
override fun operations() = secondSlot()
|
||||
override suspend fun operations() = secondSlot()
|
||||
}
|
||||
|
||||
class Direct(
|
||||
@ -423,7 +425,7 @@ sealed class MagiskInstaller(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(file, console, logs) {
|
||||
override fun operations() = direct()
|
||||
override suspend fun operations() = direct()
|
||||
}
|
||||
|
||||
}
|
||||
@ -431,7 +433,7 @@ sealed class MagiskInstaller(
|
||||
class EnvFixTask(
|
||||
private val zip: File
|
||||
) : MagiskInstallImpl() {
|
||||
override fun operations() = fixEnv(zip)
|
||||
override suspend fun operations() = fixEnv(zip)
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
|
@ -13,7 +13,8 @@ import com.topjohnwu.magisk.core.ResMgr
|
||||
import com.topjohnwu.magisk.core.addAssetPath
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.extensions.toLangTag
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import kotlin.Comparator
|
||||
import kotlin.collections.ArrayList
|
||||
@ -23,7 +24,10 @@ var currentLocale: Locale = Locale.getDefault()
|
||||
@SuppressLint("ConstantLocale")
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
val availableLocales = Single.fromCallable {
|
||||
private var cachedLocales: Pair<Array<String>, Array<String>>? = null
|
||||
|
||||
suspend fun availableLocales() = cachedLocales ?:
|
||||
withContext(Dispatchers.Default) {
|
||||
val compareId = R.string.app_changelog
|
||||
|
||||
// Create a completely new resource to prevent cross talk over app's configs
|
||||
@ -56,8 +60,6 @@ val availableLocales = Single.fromCallable {
|
||||
res.updateConfiguration(config, metrics)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
Pair(locales, defName)
|
||||
}.map { (locales, defName) ->
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
val values = ArrayList<String>(locales.size + 1)
|
||||
|
||||
@ -69,8 +71,8 @@ val availableLocales = Single.fromCallable {
|
||||
values.add(locale.toLangTag())
|
||||
}
|
||||
|
||||
Pair(names.toTypedArray(), values.toTypedArray())
|
||||
}.cache()!!
|
||||
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
|
||||
}
|
||||
|
||||
fun Resources.updateConfig(config: Configuration = configuration) {
|
||||
config.setLocale(currentLocale)
|
||||
|
@ -11,15 +11,18 @@ import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.view.Notifications
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignAPK
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.SecureRandom
|
||||
@ -102,16 +105,16 @@ object PatchAPK {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun patchAndHide(context: Context, label: String): Boolean {
|
||||
private suspend fun patchAndHide(context: Context, label: String): Boolean {
|
||||
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
|
||||
val src = if (dlStub) {
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
val svc = get<GithubRawServices>()
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||
svc.fetchFile(Info.remote.stub.link).byteStream().use {
|
||||
it.writeTo(stub)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
@ -143,10 +146,11 @@ object PatchAPK {
|
||||
fun hideManager(context: Context, label: String) {
|
||||
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
|
||||
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
|
||||
Single.fromCallable {
|
||||
GlobalScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
patchAndHide(context, label)
|
||||
}.subscribeK {
|
||||
if (!it)
|
||||
}
|
||||
if (!result)
|
||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
|
||||
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID)
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package com.topjohnwu.magisk.data.database
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class], exportSchema = false)
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
@ -26,7 +28,7 @@ abstract class RepoDao(private val db: RepoDatabase) {
|
||||
set(value) = addEtagRaw(RepoEtag(0, value))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
fun clear() = db.clearAllTables()
|
||||
suspend fun clear() = withContext(Dispatchers.IO) { db.clearAllTables() }
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
|
@ -3,7 +3,6 @@ package com.topjohnwu.magisk.data.network
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.*
|
||||
@ -26,18 +25,15 @@ interface GithubRawServices {
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
|
||||
suspend fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): ResponseBody
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
|
||||
suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody
|
||||
|
||||
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
|
||||
@Streaming
|
||||
fun fetchInstaller(): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<String>
|
||||
suspend fun fetchInstaller(): ResponseBody
|
||||
|
||||
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||
suspend fun fetchModuleFile(@Path(MODULE) id: String, @Path(FILE) file: String): String
|
||||
@ -50,10 +46,10 @@ interface GithubRawServices {
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
||||
|
||||
@GET
|
||||
fun fetchString(@Url url: String): Single<String>
|
||||
suspend fun fetchString(@Url url: String): String
|
||||
|
||||
|
||||
companion object {
|
||||
|
@ -20,8 +20,6 @@ class MagiskRepository(
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun fetchSafetynet() = apiRaw.fetchSafetynet()
|
||||
|
||||
suspend fun fetchUpdate() = try {
|
||||
var info = when (Config.updateChannel) {
|
||||
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
|
||||
|
@ -7,9 +7,10 @@ class StringRepository(
|
||||
private val api: GithubRawServices
|
||||
) {
|
||||
|
||||
fun getString(url: String) = api.fetchString(url)
|
||||
suspend fun getString(url: String) = api.fetchString(url)
|
||||
|
||||
suspend fun getMetadata(repo: Repo) = api.fetchModuleFile(repo.id, "module.prop")
|
||||
|
||||
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||
suspend fun getReadme(repo: Repo) = api.fetchModuleFile(repo.id, "README.md")
|
||||
|
||||
}
|
||||
|
@ -7,8 +7,11 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.databinding.BindingAdapter
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import io.reactivex.Single
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import io.noties.markwon.Markwon
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@BindingAdapter("gone")
|
||||
fun setGone(view: View, gone: Boolean) {
|
||||
@ -32,9 +35,16 @@ fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
|
||||
|
||||
@BindingAdapter("precomputedText")
|
||||
fun setPrecomputedText(tv: TextView, text: CharSequence) {
|
||||
Single.fromCallable {
|
||||
PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
|
||||
}.subscribeK {
|
||||
TextViewCompat.setPrecomputedText(tv, it);
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
|
||||
tv.post {
|
||||
TextViewCompat.setPrecomputedText(tv, pre);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("markdownText")
|
||||
fun setMarkdownText(tv: TextView, text: CharSequence) {
|
||||
val markwon = get<Markwon>()
|
||||
markwon.setMarkdown(tv, text.toString())
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
||||
import com.topjohnwu.magisk.view.PrecomputedTextSetter
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.image.ImagesPlugin
|
||||
@ -93,6 +94,7 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
|
||||
|
||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.textSetter(PrecomputedTextSetter())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create {
|
||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
||||
|
@ -25,7 +25,7 @@ val viewModelModules = module {
|
||||
viewModel { SettingsViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get()) }
|
||||
viewModel { ThemeViewModel() }
|
||||
viewModel { InstallViewModel(get(), get()) }
|
||||
viewModel { InstallViewModel(get()) }
|
||||
viewModel { MainViewModel() }
|
||||
|
||||
// Legacy
|
||||
|
@ -1,39 +1,15 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.ObservableField
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import io.reactivex.*
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposables
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import androidx.databinding.Observable as BindingObservable
|
||||
|
||||
fun <T> Observable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Flowable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Single<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Maybe<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Maybe<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun Completable.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
typealias OnCompleteListener = () -> Unit
|
||||
@ -49,128 +25,3 @@ fun <T> Observable<T>.subscribeK(
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Single<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onSuccess: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onSuccess, onError, onComplete)
|
||||
|
||||
fun <T> Flowable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun Completable.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onComplete, onError)
|
||||
|
||||
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T, R> Observable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Single<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Maybe<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Flowable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T> ObservableField<T>.toObservable(): Observable<T> {
|
||||
val observableField = this
|
||||
return Observable.create { emitter ->
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
|
||||
val callback = object : BindingObservable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) {
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
}
|
||||
}
|
||||
observableField.addOnPropertyChangedCallback(callback)
|
||||
emitter.setDisposable(Disposables.fromAction {
|
||||
observableField.removeOnPropertyChangedCallback(callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> T.toSingle() = Single.just(this)
|
||||
|
||||
inline fun <T1, T2, R> zip(
|
||||
t1: Single<T1>,
|
||||
t2: Single<T2>,
|
||||
crossinline zipper: (T1, T2) -> R
|
||||
) = Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
||||
|
@ -3,7 +3,7 @@ package com.topjohnwu.magisk.extensions
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.databinding.ObservableList
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun <T> MutableList<T>.update(newList: List<T>) {
|
||||
clear()
|
||||
@ -26,6 +26,7 @@ fun List<String>.toShellCmd(): String {
|
||||
|
||||
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||
target: DiffObservableList<T2>,
|
||||
scope: CoroutineScope,
|
||||
mapper: (List<T1>) -> List<T2>
|
||||
) = addOnListChangedCallback(object :
|
||||
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||
@ -49,14 +50,17 @@ fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||
updateAsync(sender ?: return)
|
||||
}
|
||||
|
||||
private var updater: Disposable? = null
|
||||
private var updater: Job? = null
|
||||
|
||||
private fun updateAsync(sender: List<T1>) {
|
||||
updater?.dispose()
|
||||
updater = sender.toSingle()
|
||||
.map { mapper(it) }
|
||||
.map { it to target.calculateDiff(it) }
|
||||
.subscribeK { target.update(it.first, it.second) }
|
||||
updater?.cancel()
|
||||
updater = scope.launch {
|
||||
val (list, diff) = withContext(Dispatchers.Default) {
|
||||
val list = mapper(sender)
|
||||
list to target.calculateDiff(list)
|
||||
}
|
||||
target.update(list, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -2,19 +2,11 @@ package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
|
||||
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||
}
|
||||
|
||||
fun File.suOutputStream() = SuFileOutputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
||||
|
||||
val hasRoot get() = Shell.rootAccess()
|
||||
|
||||
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }
|
||||
|
@ -8,23 +8,22 @@ import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.core.utils.SafetyNetHelper
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.extensions.OnErrorListener
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.ui.safetynet.SafetyNetResult
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import dalvik.system.DexFile
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONObject
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.InvocationHandler
|
||||
|
||||
/**
|
||||
@ -38,11 +37,15 @@ abstract class ViewEvent {
|
||||
var handled = false
|
||||
}
|
||||
|
||||
abstract class ViewEventsWithScope: ViewEvent() {
|
||||
lateinit var scope: CoroutineScope
|
||||
}
|
||||
|
||||
class CheckSafetyNetEvent(
|
||||
private val callback: (SafetyNetResult) -> Unit = {}
|
||||
) : ViewEvent(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
|
||||
) : ViewEventsWithScope(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
|
||||
|
||||
private val magiskRepo by inject<MagiskRepository>()
|
||||
private val svc by inject<GithubRawServices>()
|
||||
|
||||
private lateinit var apk: File
|
||||
private lateinit var dex: File
|
||||
@ -51,16 +54,21 @@ class CheckSafetyNetEvent(
|
||||
apk = File("${context.filesDir.parent}/snet", "snet.jar")
|
||||
dex = File(apk.parent, "snet.dex")
|
||||
|
||||
scope.launch {
|
||||
attest(context) {
|
||||
// Download and retry
|
||||
withContext(Dispatchers.IO) {
|
||||
Shell.sh("rm -rf " + apk.parent).exec()
|
||||
apk.parentFile?.mkdir()
|
||||
}
|
||||
download(context, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attest(context: Context, onError: OnErrorListener) {
|
||||
Completable.fromAction {
|
||||
private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) {
|
||||
try {
|
||||
val helper = withContext(Dispatchers.IO) {
|
||||
val loader = DynamicClassLoader(apk)
|
||||
val dex = DexFile.loadDex(apk.path, dex.path, 0)
|
||||
|
||||
@ -79,23 +87,39 @@ class CheckSafetyNetEvent(
|
||||
|
||||
val helper = helperClass
|
||||
.getMethod("get", Class::class.java, Context::class.java, Any::class.java)
|
||||
.invoke(null, SafetyNetHelper::class.java, context, this) as SafetyNetHelper
|
||||
.invoke(null, SafetyNetHelper::class.java,
|
||||
context, this@CheckSafetyNetEvent) as SafetyNetHelper
|
||||
|
||||
if (helper.version < Const.SNET_EXT_VER)
|
||||
throw Exception()
|
||||
|
||||
helper
|
||||
}
|
||||
helper.attest()
|
||||
}.subscribeK(onError = onError)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException)
|
||||
throw e
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun download(context: Context, askUser: Boolean) {
|
||||
fun downloadInternal() = magiskRepo.fetchSafetynet()
|
||||
.map { it.byteStream().writeTo(apk) }
|
||||
.subscribeK { attest(context) {
|
||||
fun downloadInternal() = scope.launch {
|
||||
val abort: suspend (Exception) -> Unit = {
|
||||
Timber.e(it)
|
||||
callback(SafetyNetResult())
|
||||
} }
|
||||
}
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
svc.fetchSafetynet().byteStream().writeTo(apk)
|
||||
}
|
||||
attest(context, abort)
|
||||
} catch (e: IOException) {
|
||||
if (e is CancellationException)
|
||||
throw e
|
||||
abort(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!askUser) {
|
||||
downloadInternal()
|
||||
@ -126,25 +150,10 @@ class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), Activi
|
||||
override fun invoke(activity: BaseActivity) = activity.run(action)
|
||||
}
|
||||
|
||||
class OpenChangelogEvent(val item: Repo) : ViewEvent(), ContextExecutor {
|
||||
class OpenChangelogEvent(val item: Repo) : ViewEventsWithScope(), ContextExecutor {
|
||||
override fun invoke(context: Context) {
|
||||
MarkDownWindow.show(context, null, item.readme)
|
||||
}
|
||||
}
|
||||
|
||||
class RxPermissionEvent(
|
||||
val permissions: List<String>,
|
||||
val callback: PublishSubject<Boolean>
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
|
||||
override fun invoke(activity: BaseActivity) =
|
||||
activity.withPermissions(*permissions.toTypedArray()) {
|
||||
onSuccess {
|
||||
callback.onNext(true)
|
||||
}
|
||||
onFailure {
|
||||
callback.onNext(false)
|
||||
callback.onError(SecurityException("User refused permissions"))
|
||||
scope.launch {
|
||||
MarkDownWindow.show(context, null, item::readme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,9 @@ import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ManagerInstallDialog : DialogEvent() {
|
||||
|
||||
@ -28,7 +31,11 @@ class ManagerInstallDialog : DialogEvent() {
|
||||
if (Info.remote.app.note.isEmpty()) return
|
||||
applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
titleRes = R.string.app_changelog
|
||||
onClick { MarkDownWindow.show(context, null, Info.remote.app.note) }
|
||||
onClick {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
MarkDownWindow.show(context, null, Info.remote.app.note)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
@ -96,6 +97,11 @@ abstract class BaseViewModel(
|
||||
_viewEvents.postValue(this)
|
||||
}
|
||||
|
||||
fun <Event : ViewEventsWithScope> Event.publish() {
|
||||
scope = viewModelScope
|
||||
_viewEvents.postValue(this)
|
||||
}
|
||||
|
||||
fun Int.publish() {
|
||||
_viewEvents.postValue(SimpleViewEvent(this))
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class FlashViewModel(
|
||||
private val logItems = Collections.synchronizedList(mutableListOf<String>())
|
||||
|
||||
init {
|
||||
outItems.sendUpdatesTo(items) { it.map { ConsoleItem(it) } }
|
||||
outItems.sendUpdatesTo(items, viewModelScope) { it.map { ConsoleItem(it) } }
|
||||
outItems.copyNewInputInto(logItems)
|
||||
|
||||
args.dismissId.takeIf { it != -1 }?.also {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.topjohnwu.magisk.ui.install
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
|
||||
import com.topjohnwu.magisk.extensions.value
|
||||
@ -21,6 +22,9 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
requireActivity().setTitle(R.string.install)
|
||||
|
||||
// Allow markwon to run in viewmodel scope
|
||||
binding.releaseNotes.tag = viewModel.viewModelScope
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
package com.topjohnwu.magisk.ui.install
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.ObservableField
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
@ -12,7 +11,6 @@ import com.topjohnwu.magisk.core.download.RemoteFileService
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.value
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
@ -20,13 +18,12 @@ import com.topjohnwu.magisk.model.events.RequestFileEvent
|
||||
import com.topjohnwu.magisk.model.events.dialog.SecondSlotWarningDialog
|
||||
import com.topjohnwu.magisk.ui.base.BaseViewModel
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.noties.markwon.Markwon
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.get
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class InstallViewModel(
|
||||
stringRepo: StringRepository,
|
||||
markwon: Markwon
|
||||
stringRepo: StringRepository
|
||||
) : BaseViewModel(State.LOADED) {
|
||||
|
||||
val isRooted get() = Shell.rootAccess()
|
||||
@ -35,8 +32,8 @@ class InstallViewModel(
|
||||
val step = ObservableField(0)
|
||||
val method = ObservableField(-1)
|
||||
val progress = ObservableField(0)
|
||||
val data = ObservableField<Uri?>(null)
|
||||
val notes = ObservableField<Spanned>(SpannableString(""))
|
||||
val data = ObservableField(null as Uri?)
|
||||
val notes = ObservableField("")
|
||||
|
||||
init {
|
||||
RemoteFileService.reset()
|
||||
@ -50,10 +47,8 @@ class InstallViewModel(
|
||||
state = State.LOADED
|
||||
}
|
||||
}
|
||||
stringRepo.getString(Info.remote.magisk.note).map {
|
||||
markwon.toMarkdown(it)
|
||||
}.subscribeK {
|
||||
notes.value = it
|
||||
viewModelScope.launch {
|
||||
notes.value = stringRepo.getString(Info.remote.magisk.note)
|
||||
}
|
||||
method.addOnPropertyChangedCallback {
|
||||
when (it!!) {
|
||||
|
@ -50,7 +50,7 @@ class SafetynetViewModel : BaseViewModel() {
|
||||
|
||||
private fun attest() {
|
||||
currentState = LOADING
|
||||
CheckSafetyNetEvent {
|
||||
CheckSafetyNetEvent() {
|
||||
resolveResponse(it)
|
||||
}.publish()
|
||||
}
|
||||
|
@ -16,10 +16,11 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.model.entity.recycler.SettingsItem
|
||||
import com.topjohnwu.magisk.utils.asTransitive
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
// --- Customization
|
||||
@ -38,8 +39,9 @@ object Language : SettingsItem.Selector() {
|
||||
override var entries = emptyArray<String>()
|
||||
override var entryValues = emptyArray<String>()
|
||||
|
||||
init {
|
||||
availableLocales.subscribeK { (names, values) ->
|
||||
suspend fun loadLanguages(scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
availableLocales().let { (names, values) ->
|
||||
entries = names
|
||||
entryValues = values
|
||||
val selectedLocale = currentLocale.getDisplayName(currentLocale)
|
||||
@ -47,6 +49,7 @@ object Language : SettingsItem.Selector() {
|
||||
notifyChange(BR.selectedEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Theme : SettingsItem.Blank() {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
@ -12,21 +12,18 @@ import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.core.utils.Utils
|
||||
import com.topjohnwu.magisk.data.database.RepoDao
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.value
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.recycler.SettingsItem
|
||||
import com.topjohnwu.magisk.model.events.RecreateEvent
|
||||
import com.topjohnwu.magisk.model.events.RxPermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.dialog.BiometricDialog
|
||||
import com.topjohnwu.magisk.ui.base.BaseViewModel
|
||||
import com.topjohnwu.magisk.ui.base.adapterOf
|
||||
import com.topjohnwu.magisk.ui.base.diffListOf
|
||||
import com.topjohnwu.magisk.ui.base.itemBindingOf
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.get
|
||||
|
||||
class SettingsViewModel(
|
||||
@ -48,6 +45,9 @@ class SettingsViewModel(
|
||||
// making theming a pain in the ass. Just forget about it
|
||||
list.remove(Theme)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
Language.loadLanguages(this)
|
||||
}
|
||||
|
||||
// Manager
|
||||
list.addAll(listOf(
|
||||
@ -125,8 +125,10 @@ class SettingsViewModel(
|
||||
}
|
||||
|
||||
private fun clearRepoCache() {
|
||||
Completable.fromAction { repositoryDao.clear() }
|
||||
.subscribeK { Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT) }
|
||||
viewModelScope.launch {
|
||||
repositoryDao.clear()
|
||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHosts() {
|
||||
@ -136,14 +138,7 @@ class SettingsViewModel(
|
||||
}
|
||||
|
||||
private fun requireRWPermission() {
|
||||
val callback = PublishSubject.create<Boolean>()
|
||||
callback.subscribeK { if (!it) requireRWPermission() }
|
||||
RxPermissionEvent(
|
||||
listOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
), callback
|
||||
).publish()
|
||||
withExternalRW { if (!it) requireRWPermission() }
|
||||
}
|
||||
|
||||
private fun updateManager(hide: Boolean) {
|
||||
|
@ -22,11 +22,8 @@ import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@ -42,14 +39,15 @@ fun setImageResource(view: AppCompatImageView, @DrawableRes resId: Int) {
|
||||
|
||||
@BindingAdapter("movieBehavior", "movieBehaviorText")
|
||||
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
|
||||
(view.tag as? Disposable)?.dispose()
|
||||
(view.tag as? Job)?.cancel()
|
||||
view.tag = null
|
||||
if (isMovieBehavior) {
|
||||
val observer = Observable
|
||||
.interval(150, TimeUnit.MILLISECONDS)
|
||||
.subscribeK {
|
||||
view.tag = GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
while (true) {
|
||||
delay(150)
|
||||
view.text = text.replaceRandomWithSpecial()
|
||||
}
|
||||
view.tag = observer
|
||||
}
|
||||
} else {
|
||||
view.text = text
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import com.topjohnwu.magisk.ui.base.itemBindingOf
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapters
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
class MagiskDialog @JvmOverloads constructor(
|
||||
class MagiskDialog(
|
||||
context: Context, theme: Int = 0
|
||||
) : AppCompatDialog(context, theme) {
|
||||
|
||||
|
@ -1,50 +1,60 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Spanned
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.PrecomputedTextCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import io.noties.markwon.Markwon
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.*
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class PrecomputedTextSetter : Markwon.TextSetter {
|
||||
|
||||
override fun setText(tv: TextView, text: Spanned, bufferType: TextView.BufferType, onComplete: Runnable) {
|
||||
val scope = tv.tag as? CoroutineScope ?: GlobalScope
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
|
||||
tv.post {
|
||||
TextViewCompat.setPrecomputedText(tv, pre)
|
||||
onComplete.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MarkDownWindow : KoinComponent {
|
||||
|
||||
private val stringRepo: StringRepository by inject()
|
||||
private val repo: StringRepository by inject()
|
||||
private val markwon: Markwon by inject()
|
||||
|
||||
fun show(activity: Context, title: String?, url: String) {
|
||||
show(activity, title, stringRepo.getString(url))
|
||||
}
|
||||
|
||||
fun show(activity: Context, title: String?, input: InputStream) {
|
||||
Single.just(Scanner(input, "UTF-8").apply { useDelimiter("\\A") })
|
||||
.map { it.next() }
|
||||
.also {
|
||||
show(activity, title, it)
|
||||
suspend fun show(activity: Context, title: String?, url: String) {
|
||||
show(activity, title) {
|
||||
repo.getString(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(activity: Context, title: String?, content: Single<String>) {
|
||||
suspend fun show(activity: Context, title: String?, input: suspend () -> String) {
|
||||
val mdRes = R.layout.markdown_window_md2
|
||||
val mv = LayoutInflater.from(activity).inflate(mdRes, null)
|
||||
val tv = mv.findViewById<TextView>(R.id.md_txt)
|
||||
tv.tag = CoroutineScope(coroutineContext)
|
||||
|
||||
content.map {
|
||||
markwon.setMarkdown(tv, it)
|
||||
}.ignoreElement().onErrorResumeNext {
|
||||
// Nothing we can actually do other than show error message
|
||||
Timber.e(it)
|
||||
try {
|
||||
markwon.setMarkdown(tv, input())
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException)
|
||||
throw e
|
||||
Timber.e(e)
|
||||
tv.setText(R.string.download_file_error)
|
||||
Completable.complete()
|
||||
}.subscribeK {
|
||||
}
|
||||
|
||||
MagiskDialog(activity)
|
||||
.applyTitle(title ?: "")
|
||||
.applyView(mv)
|
||||
@ -52,7 +62,5 @@ object MarkDownWindow : KoinComponent {
|
||||
titleRes = android.R.string.cancel
|
||||
}
|
||||
.reveal()
|
||||
return@subscribeK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -228,11 +228,12 @@
|
||||
app:strokeWidth="@{viewModel.step != 0 ? 0f : @dimen/l_125}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/release_notes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="15dp"
|
||||
android:textAppearance="@style/AppearanceFoundation.Caption"
|
||||
android:text="@{viewModel.notes}"/>
|
||||
markdownText="@{viewModel.notes}"/>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user