diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt index ce57ccc60..868057295 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt @@ -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,18 +62,15 @@ 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 - } + handleAPK(subject) + remove(id) + when (subject.configuration) { + is APK.Upgrade -> APKInstall.install(this, subject.file) + is APK.Restore -> Unit } } @@ -85,14 +78,14 @@ open class DownloadService : RemoteFileService() { 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt index f3c2ecff9..4431225f3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt @@ -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()) diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt index c6fb94432..03c02c479 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt @@ -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()) + 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 // -- diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt index 4cd8df26e..66b5d504b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt @@ -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(ARG_URL)?.let { update(it.hashCode()) - start(it) + 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) - }) { - val newId = finishNotify(subject) - if (ForegroundTracker.hasForeground) { - onFinished(subject, newId) + 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) } } - - 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" + val newId = finishNotify(subject) + if (ForegroundTracker.hasForeground) { + onFinished(subject, newId) } } - 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt index d42d3c513..04e6e097c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt @@ -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) diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt index 9290195c4..e1f2254e3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt @@ -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, logs: MutableList ) : 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, logs: MutableList ) : MagiskInstaller(file, console, logs) { - override fun operations() = secondSlot() + override suspend fun operations() = secondSlot() } class Direct( @@ -423,7 +425,7 @@ sealed class MagiskInstaller( console: MutableList, logs: MutableList ) : 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() diff --git a/app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt index b98813ccd..e34df967b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt @@ -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>? = 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(locales.size + 1) val values = ArrayList(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) diff --git a/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt index 9e6e033fd..a9658f5a1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt @@ -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() 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 { - patchAndHide(context, label) - }.subscribeK { - if (!it) + GlobalScope.launch { + val result = withContext(Dispatchers.IO) { + patchAndHide(context, label) + } + if (!result) Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG) Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID) } diff --git a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt index 2df1fdf88..23953ad9b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt @@ -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 diff --git a/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt b/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt index 2049bcaa6..38a84ff2e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt @@ -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 + 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 + suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody @GET("$MAGISK_MASTER/scripts/module_installer.sh") @Streaming - fun fetchInstaller(): Single - - @GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}") - fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single + 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 + suspend fun fetchFile(@Url url: String): ResponseBody @GET - fun fetchString(@Url url: String): Single + suspend fun fetchString(@Url url: String): String companion object { diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt index d9a300c6b..6936e38d5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt @@ -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() diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt index fb807fe5b..e7889ef59 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt @@ -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") + } diff --git a/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt b/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt index f845545b2..61fcb7e10 100644 --- a/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt +++ b/app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt @@ -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.setMarkdown(tv, text.toString()) +} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt index 62ab2dd24..791b35bbf 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt @@ -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 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)) diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt index 05d28d79a..59c3ed3b5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -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 diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt index 962ccb402..d2f0a333d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt @@ -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 Observable.applySchedulers( subscribeOn: Scheduler = Schedulers.io(), observeOn: Scheduler = AndroidSchedulers.mainThread() ): Observable = this.subscribeOn(subscribeOn).observeOn(observeOn) -fun Flowable.applySchedulers( - subscribeOn: Scheduler = Schedulers.io(), - observeOn: Scheduler = AndroidSchedulers.mainThread() -): Flowable = this.subscribeOn(subscribeOn).observeOn(observeOn) - -fun Single.applySchedulers( - subscribeOn: Scheduler = Schedulers.io(), - observeOn: Scheduler = AndroidSchedulers.mainThread() -): Single = this.subscribeOn(subscribeOn).observeOn(observeOn) - -fun Maybe.applySchedulers( - subscribeOn: Scheduler = Schedulers.io(), - observeOn: Scheduler = AndroidSchedulers.mainThread() -): Maybe = 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 Observable.subscribeK( ) = applySchedulers() .subscribe(onNext, onError, onComplete) -fun Single.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onNext: OnSuccessListener = {} -) = applySchedulers() - .subscribe(onNext, onError) - -fun Maybe.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onComplete: OnCompleteListener = {}, - onSuccess: OnSuccessListener = {} -) = applySchedulers() - .subscribe(onSuccess, onError, onComplete) - -fun Flowable.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onComplete: OnCompleteListener = {}, - onNext: OnSuccessListener = {} -) = applySchedulers() - .subscribe(onNext, onError, onComplete) - -fun Completable.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onComplete: OnCompleteListener = {} -) = applySchedulers() - .subscribe(onComplete, onError) - -fun Observable.doOnSubscribeUi(body: () -> Unit) = - doOnSubscribe { UiThreadHandler.run { body() } } - -fun Single.doOnSubscribeUi(body: () -> Unit) = - doOnSubscribe { UiThreadHandler.run { body() } } - -fun Maybe.doOnSubscribeUi(body: () -> Unit) = - doOnSubscribe { UiThreadHandler.run { body() } } - -fun Flowable.doOnSubscribeUi(body: () -> Unit) = - doOnSubscribe { UiThreadHandler.run { body() } } - -fun Completable.doOnSubscribeUi(body: () -> Unit) = - doOnSubscribe { UiThreadHandler.run { body() } } - - -fun Observable.doOnErrorUi(body: (Throwable) -> Unit) = - doOnError { UiThreadHandler.run { body(it) } } - -fun Single.doOnErrorUi(body: (Throwable) -> Unit) = - doOnError { UiThreadHandler.run { body(it) } } - -fun Maybe.doOnErrorUi(body: (Throwable) -> Unit) = - doOnError { UiThreadHandler.run { body(it) } } - -fun Flowable.doOnErrorUi(body: (Throwable) -> Unit) = - doOnError { UiThreadHandler.run { body(it) } } - -fun Completable.doOnErrorUi(body: (Throwable) -> Unit) = - doOnError { UiThreadHandler.run { body(it) } } - - -fun Observable.doOnNextUi(body: (T) -> Unit) = - doOnNext { UiThreadHandler.run { body(it) } } - -fun Flowable.doOnNextUi(body: (T) -> Unit) = - doOnNext { UiThreadHandler.run { body(it) } } - -fun Single.doOnSuccessUi(body: (T) -> Unit) = - doOnSuccess { UiThreadHandler.run { body(it) } } - -fun Maybe.doOnSuccessUi(body: (T) -> Unit) = - doOnSuccess { UiThreadHandler.run { body(it) } } - -fun Maybe.doOnCompleteUi(body: () -> Unit) = - doOnComplete { UiThreadHandler.run { body() } } - -fun Completable.doOnCompleteUi(body: () -> Unit) = - doOnComplete { UiThreadHandler.run { body() } } - - -fun Observable>.mapList( - transformer: (T) -> R -) = flatMapIterable { it } - .map(transformer) - .toList() - -fun Single>.mapList( - transformer: (T) -> R -) = flattenAsFlowable { it } - .map(transformer) - .toList() - -fun Maybe>.mapList( - transformer: (T) -> R -) = flattenAsFlowable { it } - .map(transformer) - .toList() - -fun Flowable>.mapList( - transformer: (T) -> R -) = flatMapIterable { it } - .map(transformer) - .toList() - -fun ObservableField.toObservable(): Observable { - 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.toSingle() = Single.just(this) - -inline fun zip( - t1: Single, - t2: Single, - crossinline zipper: (T1, T2) -> R -) = Single.zip(t1, t2, BiFunction { rt1, rt2 -> zipper(rt1, rt2) }) diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt index 88e5807c1..6dc6812d7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt @@ -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 MutableList.update(newList: List) { clear() @@ -25,8 +25,9 @@ fun List.toShellCmd(): String { } fun ObservableList.sendUpdatesTo( - target: DiffObservableList, - mapper: (List) -> List + target: DiffObservableList, + scope: CoroutineScope, + mapper: (List) -> List ) = addOnListChangedCallback(object : ObservableList.OnListChangedCallback>() { override fun onChanged(sender: ObservableList?) { @@ -49,14 +50,17 @@ fun ObservableList.sendUpdatesTo( updateAsync(sender ?: return) } - private var updater: Disposable? = null + private var updater: Job? = null private fun updateAsync(sender: List) { - 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) + } } }) diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt index b6cce5211..236cc2bc4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt @@ -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() } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt index b15f68eac..cb5825c2d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -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() + private val svc by inject() private lateinit var apk: File private lateinit var dex: File @@ -51,51 +54,72 @@ class CheckSafetyNetEvent( apk = File("${context.filesDir.parent}/snet", "snet.jar") dex = File(apk.parent, "snet.dex") - attest(context) { - // Download and retry - Shell.sh("rm -rf " + apk.parent).exec() - apk.parentFile?.mkdir() - download(context, true) + 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 { - val loader = DynamicClassLoader(apk) - val dex = DexFile.loadDex(apk.path, dex.path, 0) + 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) - // Scan through the dex and find our helper class - var helperClass: Class<*>? = null - for (className in dex.entries()) { - if (className.startsWith("x.")) { - val cls = loader.loadClass(className) - if (InvocationHandler::class.java.isAssignableFrom(cls)) { - helperClass = cls - break + // Scan through the dex and find our helper class + var helperClass: Class<*>? = null + for (className in dex.entries()) { + if (className.startsWith("x.")) { + val cls = loader.loadClass(className) + if (InvocationHandler::class.java.isAssignableFrom(cls)) { + helperClass = cls + break + } } } + helperClass ?: throw Exception() + + val helper = helperClass + .getMethod("get", Class::class.java, Context::class.java, Any::class.java) + .invoke(null, SafetyNetHelper::class.java, + context, this@CheckSafetyNetEvent) as SafetyNetHelper + + if (helper.version < Const.SNET_EXT_VER) + throw Exception() + helper } - helperClass ?: throw Exception() - - val helper = helperClass - .getMethod("get", Class::class.java, Context::class.java, Any::class.java) - .invoke(null, SafetyNetHelper::class.java, context, this) as SafetyNetHelper - - if (helper.version < Const.SNET_EXT_VER) - throw Exception() - 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,27 +150,12 @@ 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, - val callback: PublishSubject -) : 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) } + } } class PermissionEvent( diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt index 507bb243b..c10d572b1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt @@ -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) + } + } } } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt index 39545dbcb..f82629d74 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt @@ -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.publish() { + scope = viewModelScope + _viewEvents.postValue(this) + } + fun Int.publish() { _viewEvents.postValue(SimpleViewEvent(this)) } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt index 69a9e8c38..36f9e1c87 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -42,7 +42,7 @@ class FlashViewModel( private val logItems = Collections.synchronizedList(mutableListOf()) 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 { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt index 105f44d41..cf26b4886 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt @@ -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(null) - val notes = ObservableField(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!!) { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt index 2ba78357e..4756ca186 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt @@ -50,7 +50,7 @@ class SafetynetViewModel : BaseViewModel() { private fun attest() { currentState = LOADING - CheckSafetyNetEvent { + CheckSafetyNetEvent() { resolveResponse(it) }.publish() } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt index 2c17172b4..f6816a7b4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt @@ -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,13 +39,15 @@ object Language : SettingsItem.Selector() { override var entries = emptyArray() override var entryValues = emptyArray() - init { - availableLocales.subscribeK { (names, values) -> - entries = names - entryValues = values - val selectedLocale = currentLocale.getDisplayName(currentLocale) - value = names.indexOfFirst { it == selectedLocale }.let { if (it == -1) 0 else it } - notifyChange(BR.selectedEntry) + suspend fun loadLanguages(scope: CoroutineScope) { + scope.launch { + availableLocales().let { (names, values) -> + entries = names + entryValues = values + val selectedLocale = currentLocale.getDisplayName(currentLocale) + value = names.indexOfFirst { it == selectedLocale }.let { if (it == -1) 0 else it } + notifyChange(BR.selectedEntry) + } } } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt index f5fb47c51..81c09109a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt @@ -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() - 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) { diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt index 83ce0e6bb..bbad3a7a0 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -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 } diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt index 49ed8a911..b0eff1453 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt @@ -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) { diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt index c2304f863..9da1056a7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt +++ b/app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt @@ -1,58 +1,66 @@ 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 -object MarkDownWindow : KoinComponent { +class PrecomputedTextSetter : Markwon.TextSetter { - private val stringRepo: 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) + 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() } - } - - fun show(activity: Context, title: String?, content: Single) { - val mdRes = R.layout.markdown_window_md2 - val mv = LayoutInflater.from(activity).inflate(mdRes, null) - val tv = mv.findViewById(R.id.md_txt) - - content.map { - markwon.setMarkdown(tv, it) - }.ignoreElement().onErrorResumeNext { - // Nothing we can actually do other than show error message - Timber.e(it) - tv.setText(R.string.download_file_error) - Completable.complete() - }.subscribeK { - MagiskDialog(activity) - .applyTitle(title ?: "") - .applyView(mv) - .applyButton(MagiskDialog.ButtonType.NEGATIVE) { - titleRes = android.R.string.cancel - } - .reveal() - return@subscribeK } } } + +object MarkDownWindow : KoinComponent { + + private val repo: StringRepository by inject() + private val markwon: Markwon by inject() + + suspend fun show(activity: Context, title: String?, url: String) { + show(activity, title) { + repo.getString(url) + } + } + + 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(R.id.md_txt) + tv.tag = CoroutineScope(coroutineContext) + + try { + markwon.setMarkdown(tv, input()) + } catch (e: Exception) { + if (e is CancellationException) + throw e + Timber.e(e) + tv.setText(R.string.download_file_error) + } + + MagiskDialog(activity) + .applyTitle(title ?: "") + .applyView(mv) + .applyButton(MagiskDialog.ButtonType.NEGATIVE) { + titleRes = android.R.string.cancel + } + .reveal() + } +} diff --git a/app/src/main/res/layout/fragment_install_md2.xml b/app/src/main/res/layout/fragment_install_md2.xml index 01bf98bb1..066ae40a6 100644 --- a/app/src/main/res/layout/fragment_install_md2.xml +++ b/app/src/main/res/layout/fragment_install_md2.xml @@ -228,11 +228,12 @@ app:strokeWidth="@{viewModel.step != 0 ? 0f : @dimen/l_125}"> + markdownText="@{viewModel.notes}"/>