Remove more code using RxJava

This commit is contained in:
topjohnwu 2020-07-10 04:19:18 -07:00
parent f7a650b9a4
commit 6348d0a6fb
31 changed files with 309 additions and 429 deletions

View File

@ -13,16 +13,12 @@ import com.topjohnwu.magisk.core.tasks.EnvFixTask
import com.topjohnwu.magisk.extensions.chooser import com.topjohnwu.magisk.extensions.chooser
import com.topjohnwu.magisk.extensions.exists import com.topjohnwu.magisk.extensions.exists
import com.topjohnwu.magisk.extensions.provide 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.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary 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.model.entity.internal.DownloadSubject.* import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.ui.flash.FlashFragment import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.APKInstall
import io.reactivex.Completable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.core.get import org.koin.core.get
import java.io.File import java.io.File
import kotlin.random.Random.Default.nextInt import kotlin.random.Random.Default.nextInt
@ -37,20 +33,20 @@ open class DownloadService : RemoteFileService() {
.getMimeTypeFromExtension(extension) .getMimeTypeFromExtension(extension)
?: "resource/folder" ?: "resource/folder"
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) { override suspend fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
is Magisk -> onFinishedInternal(subject, id) is Magisk -> onFinished(subject, id)
is Module -> onFinishedInternal(subject, id) is Module -> onFinished(subject, id)
is Manager -> onFinishedInternal(subject, id) is Manager -> onFinished(subject, id)
} }
private fun onFinishedInternal( private suspend fun onFinished(
subject: Magisk, subject: Magisk,
id: Int id: Int
) = when (val conf = subject.configuration) { ) = when (val conf = subject.configuration) {
Uninstall -> FlashFragment.uninstall(subject.file, id) Uninstall -> FlashFragment.uninstall(subject.file, id)
EnvFix -> { EnvFix -> {
remove(id) remove(id)
GlobalScope.launch { EnvFixTask(subject.file).exec() } EnvFixTask(subject.file).exec()
Unit Unit
} }
is Patch -> FlashFragment.patch(subject.file, conf.fileUri, id) is Patch -> FlashFragment.patch(subject.file, conf.fileUri, id)
@ -58,7 +54,7 @@ open class DownloadService : RemoteFileService() {
else -> Unit else -> Unit
} }
private fun onFinishedInternal( private fun onFinished(
subject: Module, subject: Module,
id: Int id: Int
) = when (subject.configuration) { ) = when (subject.configuration) {
@ -66,18 +62,15 @@ open class DownloadService : RemoteFileService() {
else -> Unit else -> Unit
} }
private fun onFinishedInternal( private suspend fun onFinished(
subject: Manager, subject: Manager,
id: Int id: Int
) { ) {
Completable.fromAction { handleAPK(subject)
handleAPK(subject) remove(id)
}.subscribeK { when (subject.configuration) {
remove(id) is APK.Upgrade -> APKInstall.install(this, subject.file)
when (subject.configuration) { is APK.Restore -> Unit
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) override fun Notification.Builder.addActions(subject: DownloadSubject)
= when (subject) { = when (subject) {
is Magisk -> addActionsInternal(subject) is Magisk -> addActions(subject)
is Module -> addActionsInternal(subject) is Module -> addActions(subject)
is Manager -> addActionsInternal(subject) is Manager -> addActions(subject)
} }
private fun Notification.Builder.addActionsInternal(subject: Magisk) private fun Notification.Builder.addActions(subject: Magisk)
= when (val conf = subject.configuration) { = when (val conf = subject.configuration) {
Download -> this.apply { Download -> apply {
fileIntent(subject.file.parentFile!!) fileIntent(subject.file.parentFile!!)
.takeIf { it.exists(get()) } .takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_parent, it.chooser()) } ?.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()) } ?.let { addAction(0, R.string.download_open_self, it.chooser()) }
} }
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file)) Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
is Flash -> setContentIntent( is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, conf is Secondary))
FlashFragment.flashIntent(
context,
subject.file,
conf is Secondary
)
)
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, conf.fileUri)) is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, conf.fileUri))
else -> this else -> this
} }
private fun Notification.Builder.addActionsInternal(subject: Module) private fun Notification.Builder.addActions(subject: Module)
= when (subject.configuration) { = when (subject.configuration) {
Download -> this.apply { Download -> this.apply {
fileIntent(subject.file.parentFile!!) fileIntent(subject.file.parentFile!!)
@ -126,7 +113,7 @@ open class DownloadService : RemoteFileService() {
else -> this else -> this
} }
private fun Notification.Builder.addActionsInternal(subject: Manager) private fun Notification.Builder.addActions(subject: Manager)
= when (subject.configuration) { = when (subject.configuration) {
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file)) APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
else -> this else -> this

View File

@ -32,14 +32,14 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
patched.renameTo(apk) patched.renameTo(apk)
} }
private fun RemoteFileService.upgrade(apk: File, id: Int) { private suspend fun RemoteFileService.upgrade(apk: File, id: Int) {
if (isRunningAsStub) { if (isRunningAsStub) {
// Move to upgrade location // Move to upgrade location
apk.copyTo(DynAPK.update(this), overwrite = true) apk.copyTo(DynAPK.update(this), overwrite = true)
apk.delete() apk.delete()
if (Info.stub!!.version < Info.remote.stub.versionCode) { if (Info.stub!!.version < Info.remote.stub.versionCode) {
// We also want to upgrade stub // 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) it.writeTo(apk)
} }
patch(apk, id) patch(apk, id)
@ -65,7 +65,7 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
Shell.su("pm install $apk && pm uninstall $packageName").exec() 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) { when (subject.configuration) {
is Upgrade -> upgrade(subject.file, subject.hashCode()) is Upgrade -> upgrade(subject.file, subject.hashCode())
is Restore -> restore(subject.file, subject.hashCode()) is Restore -> restore(subject.file, subject.hashCode())

View File

@ -5,6 +5,9 @@ import android.content.Intent
import android.os.IBinder import android.os.IBinder
import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.view.Notifications 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 org.koin.core.KoinComponent
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
@ -16,12 +19,19 @@ abstract class NotificationService : BaseService(), KoinComponent {
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>()) private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
val coroutineScope = CoroutineScope(Dispatchers.IO)
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
notifications.forEach { cancel(it.key) } notifications.forEach { cancel(it.key) }
notifications.clear() notifications.clear()
} }
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
abstract fun createNotification(): Notification.Builder abstract fun createNotification(): Notification.Builder
// -- // --

View File

@ -9,17 +9,17 @@ import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.utils.ProgressInputStream import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.core.view.Notifications import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject 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.Magisk
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import io.reactivex.Completable import kotlinx.coroutines.launch
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import java.io.InputStream import java.io.InputStream
abstract class RemoteFileService : NotificationService() { abstract class RemoteFileService : NotificationService() {
@ -29,7 +29,14 @@ abstract class RemoteFileService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let {
update(it.hashCode()) update(it.hashCode())
start(it) coroutineScope.launch {
try {
start(it)
} catch (e: IOException) {
Timber.e(e)
failNotify(it)
}
}
} }
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
@ -38,37 +45,24 @@ abstract class RemoteFileService : NotificationService() {
// --- // ---
private fun start(subject: DownloadSubject) = checkExisting(subject) private suspend fun start(subject: DownloadSubject) {
.onErrorResumeNext { download(subject) } if (subject !is Magisk ||
.subscribeK(onError = { !subject.file.exists() ||
Timber.e(it) !ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) {
failNotify(subject) val stream = service.fetchFile(subject.url).toProgressStream(subject)
}) { when (subject) {
val newId = finishNotify(subject) is Module ->
if (ForegroundTracker.hasForeground) { stream.toModule(subject.file, service.fetchInstaller().byteStream())
onFinished(subject, newId) else ->
stream.writeTo(subject.file)
} }
} }
val newId = finishNotify(subject)
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction { if (ForegroundTracker.hasForeground) {
check(subject is Magisk) { "Download cache is disabled" } onFinished(subject, newId)
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 { private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
val maxRaw = contentLength() val maxRaw = contentLength()
val max = maxRaw / 1_000_000f val max = maxRaw / 1_000_000f
@ -112,8 +106,7 @@ abstract class RemoteFileService : NotificationService() {
// --- // ---
@Throws(Throwable::class) protected abstract suspend fun onFinished(subject: DownloadSubject, id: Int)
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject) protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
: Notification.Builder : Notification.Builder

View File

@ -31,7 +31,7 @@ data class Repo(
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename() 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) val zipUrl: String get() = Const.Url.ZIP_URL.format(id)

View File

@ -15,7 +15,9 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.utils.Utils import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.Protected 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.magisk.model.events.dialog.EnvFixDialog
import com.topjohnwu.signing.SignBoot import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
@ -322,7 +324,7 @@ abstract class MagiskInstallImpl : KoinComponent {
tarOut = null tarOut = null
it it
} ?: destFile.outputStream() } ?: destFile.outputStream()
patched.suInputStream().use { it.copyTo(os); os.close() } SuFileInputStream(patched).use { it.copyTo(os); os.close() }
} catch (e: IOException) { } catch (e: IOException) {
console.add("! Failed to output to $destFile") console.add("! Failed to output to $destFile")
Timber.e(e) Timber.e(e)
@ -338,10 +340,10 @@ abstract class MagiskInstallImpl : KoinComponent {
return true return true
} }
private fun postOTA(): Boolean { private suspend fun postOTA(): Boolean {
val bootctl = SuFile("/data/adb/bootctl") val bootctl = SuFile("/data/adb/bootctl")
try { try {
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) { withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
input, out -> input.copyTo(out) input, out -> input.copyTo(out)
} }
} catch (e: IOException) { } catch (e: IOException) {
@ -368,7 +370,7 @@ abstract class MagiskInstallImpl : KoinComponent {
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot() protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
protected fun secondSlot() = protected suspend fun secondSlot() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA() findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
protected fun fixEnv(zip: File): Boolean { protected fun fixEnv(zip: File): Boolean {
@ -379,7 +381,7 @@ abstract class MagiskInstallImpl : KoinComponent {
} }
@WorkerThread @WorkerThread
protected abstract fun operations(): Boolean protected abstract suspend fun operations(): Boolean
open suspend fun exec() = withContext(Dispatchers.IO) { operations() } open suspend fun exec() = withContext(Dispatchers.IO) { operations() }
} }
@ -407,7 +409,7 @@ sealed class MagiskInstaller(
console: MutableList<String>, console: MutableList<String>,
logs: MutableList<String> logs: MutableList<String>
) : MagiskInstaller(file, console, logs) { ) : MagiskInstaller(file, console, logs) {
override fun operations() = doPatchFile(uri) override suspend fun operations() = doPatchFile(uri)
} }
class SecondSlot( class SecondSlot(
@ -415,7 +417,7 @@ sealed class MagiskInstaller(
console: MutableList<String>, console: MutableList<String>,
logs: MutableList<String> logs: MutableList<String>
) : MagiskInstaller(file, console, logs) { ) : MagiskInstaller(file, console, logs) {
override fun operations() = secondSlot() override suspend fun operations() = secondSlot()
} }
class Direct( class Direct(
@ -423,7 +425,7 @@ sealed class MagiskInstaller(
console: MutableList<String>, console: MutableList<String>,
logs: MutableList<String> logs: MutableList<String>
) : MagiskInstaller(file, console, logs) { ) : MagiskInstaller(file, console, logs) {
override fun operations() = direct() override suspend fun operations() = direct()
} }
} }
@ -431,7 +433,7 @@ sealed class MagiskInstaller(
class EnvFixTask( class EnvFixTask(
private val zip: File private val zip: File
) : MagiskInstallImpl() { ) : MagiskInstallImpl() {
override fun operations() = fixEnv(zip) override suspend fun operations() = fixEnv(zip)
override suspend fun exec(): Boolean { override suspend fun exec(): Boolean {
val success = super.exec() val success = super.exec()

View File

@ -13,7 +13,8 @@ import com.topjohnwu.magisk.core.ResMgr
import com.topjohnwu.magisk.core.addAssetPath import com.topjohnwu.magisk.core.addAssetPath
import com.topjohnwu.magisk.extensions.langTagToLocale import com.topjohnwu.magisk.extensions.langTagToLocale
import com.topjohnwu.magisk.extensions.toLangTag import com.topjohnwu.magisk.extensions.toLangTag
import io.reactivex.Single import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.* import java.util.*
import kotlin.Comparator import kotlin.Comparator
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -23,7 +24,10 @@ var currentLocale: Locale = Locale.getDefault()
@SuppressLint("ConstantLocale") @SuppressLint("ConstantLocale")
val defaultLocale: Locale = Locale.getDefault() 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 val compareId = R.string.app_changelog
// Create a completely new resource to prevent cross talk over app's configs // 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) res.updateConfiguration(config, metrics)
val defName = res.getString(R.string.system_default) val defName = res.getString(R.string.system_default)
Pair(locales, defName)
}.map { (locales, defName) ->
val names = ArrayList<String>(locales.size + 1) val names = ArrayList<String>(locales.size + 1)
val values = ArrayList<String>(locales.size + 1) val values = ArrayList<String>(locales.size + 1)
@ -69,8 +71,8 @@ val availableLocales = Single.fromCallable {
values.add(locale.toLangTag()) values.add(locale.toLangTag())
} }
Pair(names.toTypedArray(), values.toTypedArray()) (names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
}.cache()!! }
fun Resources.updateConfig(config: Configuration = configuration) { fun Resources.updateConfig(config: Configuration = configuration) {
config.setLocale(currentLocale) config.setLocale(currentLocale)

View File

@ -11,15 +11,18 @@ import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.view.Notifications import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.signing.JarMap import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignAPK import com.topjohnwu.signing.SignAPK
import com.topjohnwu.superuser.Shell 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 timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.security.SecureRandom import java.security.SecureRandom
@ -102,16 +105,16 @@ object PatchAPK {
return true 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 dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
val src = if (dlStub) { val src = if (dlStub) {
val stub = File(context.cacheDir, "stub.apk") val stub = File(context.cacheDir, "stub.apk")
val svc = get<GithubRawServices>() val svc = get<GithubRawServices>()
try { try {
svc.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use { svc.fetchFile(Info.remote.stub.link).byteStream().use {
it.writeTo(stub) it.writeTo(stub)
} }
} catch (e: Exception) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
return false return false
} }
@ -143,10 +146,11 @@ object PatchAPK {
fun hideManager(context: Context, label: String) { fun hideManager(context: Context, label: String) {
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title)) val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build()) Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
Single.fromCallable { GlobalScope.launch {
patchAndHide(context, label) val result = withContext(Dispatchers.IO) {
}.subscribeK { patchAndHide(context, label)
if (!it) }
if (!result)
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG) Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID) Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID)
} }

View File

@ -3,6 +3,8 @@ package com.topjohnwu.magisk.data.database
import androidx.room.* import androidx.room.*
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.module.Repo 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) @Database(version = 6, entities = [Repo::class, RepoEtag::class], exportSchema = false)
abstract class RepoDatabase : RoomDatabase() { abstract class RepoDatabase : RoomDatabase() {
@ -26,7 +28,7 @@ abstract class RepoDao(private val db: RepoDatabase) {
set(value) = addEtagRaw(RepoEtag(0, value)) set(value) = addEtagRaw(RepoEtag(0, value))
get() = etagRaw()?.key.orEmpty() 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") @Query("SELECT * FROM repos ORDER BY last_update DESC")
protected abstract fun getReposDateOrder(): List<Repo> protected abstract fun getReposDateOrder(): List<Repo>

View File

@ -3,7 +3,6 @@ package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
import io.reactivex.Single
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Response import retrofit2.Response
import retrofit2.http.* import retrofit2.http.*
@ -26,18 +25,15 @@ interface GithubRawServices {
@GET("$MAGISK_FILES/{$REVISION}/snet.jar") @GET("$MAGISK_FILES/{$REVISION}/snet.jar")
@Streaming @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") @GET("$MAGISK_FILES/{$REVISION}/bootctl")
@Streaming @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") @GET("$MAGISK_MASTER/scripts/module_installer.sh")
@Streaming @Streaming
fun fetchInstaller(): Single<ResponseBody> suspend fun fetchInstaller(): ResponseBody
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<String>
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}") @GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
suspend fun fetchModuleFile(@Path(MODULE) id: String, @Path(FILE) file: String): String suspend fun fetchModuleFile(@Path(MODULE) id: String, @Path(FILE) file: String): String
@ -50,10 +46,10 @@ interface GithubRawServices {
* */ * */
@GET @GET
@Streaming @Streaming
fun fetchFile(@Url url: String): Single<ResponseBody> suspend fun fetchFile(@Url url: String): ResponseBody
@GET @GET
fun fetchString(@Url url: String): Single<String> suspend fun fetchString(@Url url: String): String
companion object { companion object {

View File

@ -20,8 +20,6 @@ class MagiskRepository(
private val packageManager: PackageManager private val packageManager: PackageManager
) { ) {
fun fetchSafetynet() = apiRaw.fetchSafetynet()
suspend fun fetchUpdate() = try { suspend fun fetchUpdate() = try {
var info = when (Config.updateChannel) { var info = when (Config.updateChannel) {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate() Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()

View File

@ -7,9 +7,10 @@ class StringRepository(
private val api: GithubRawServices 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") 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")
} }

View File

@ -7,8 +7,11 @@ import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.extensions.get
import io.reactivex.Single import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@BindingAdapter("gone") @BindingAdapter("gone")
fun setGone(view: View, gone: Boolean) { fun setGone(view: View, gone: Boolean) {
@ -32,9 +35,16 @@ fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
@BindingAdapter("precomputedText") @BindingAdapter("precomputedText")
fun setPrecomputedText(tv: TextView, text: CharSequence) { fun setPrecomputedText(tv: TextView, text: CharSequence) {
Single.fromCallable { GlobalScope.launch(Dispatchers.Default) {
PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv)) val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
}.subscribeK { tv.post {
TextViewCompat.setPrecomputedText(tv, it); TextViewCompat.setPrecomputedText(tv, pre);
}
} }
} }
@BindingAdapter("markdownText")
fun setMarkdownText(tv: TextView, text: CharSequence) {
val markwon = get<Markwon>()
markwon.setMarkdown(tv, text.toString())
}

View File

@ -7,6 +7,7 @@ import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.net.Networking import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
import com.topjohnwu.magisk.view.PrecomputedTextSetter
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin 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 { fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
return Markwon.builder(context) return Markwon.builder(context)
.textSetter(PrecomputedTextSetter())
.usePlugin(HtmlPlugin.create()) .usePlugin(HtmlPlugin.create())
.usePlugin(ImagesPlugin.create { .usePlugin(ImagesPlugin.create {
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient)) it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))

View File

@ -25,7 +25,7 @@ val viewModelModules = module {
viewModel { SettingsViewModel(get()) } viewModel { SettingsViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get()) } viewModel { SuperuserViewModel(get(), get(), get()) }
viewModel { ThemeViewModel() } viewModel { ThemeViewModel() }
viewModel { InstallViewModel(get(), get()) } viewModel { InstallViewModel(get()) }
viewModel { MainViewModel() } viewModel { MainViewModel() }
// Legacy // Legacy

View File

@ -1,39 +1,15 @@
package com.topjohnwu.magisk.extensions package com.topjohnwu.magisk.extensions
import androidx.databinding.ObservableField import io.reactivex.Observable
import com.topjohnwu.superuser.internal.UiThreadHandler import io.reactivex.Scheduler
import io.reactivex.*
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposables
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import androidx.databinding.Observable as BindingObservable
fun <T> Observable<T>.applySchedulers( fun <T> Observable<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(), subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread() observeOn: Scheduler = AndroidSchedulers.mainThread()
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn) ): 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 ===*/ /*=== ALIASES FOR OBSERVABLES ===*/
typealias OnCompleteListener = () -> Unit typealias OnCompleteListener = () -> Unit
@ -49,128 +25,3 @@ fun <T> Observable<T>.subscribeK(
) = applySchedulers() ) = applySchedulers()
.subscribe(onNext, onError, onComplete) .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) })

View File

@ -3,7 +3,7 @@ package com.topjohnwu.magisk.extensions
import androidx.collection.SparseArrayCompat import androidx.collection.SparseArrayCompat
import androidx.databinding.ObservableList import androidx.databinding.ObservableList
import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.utils.DiffObservableList
import io.reactivex.disposables.Disposable import kotlinx.coroutines.*
fun <T> MutableList<T>.update(newList: List<T>) { fun <T> MutableList<T>.update(newList: List<T>) {
clear() clear()
@ -25,8 +25,9 @@ fun List<String>.toShellCmd(): String {
} }
fun <T1, T2> ObservableList<T1>.sendUpdatesTo( fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
target: DiffObservableList<T2>, target: DiffObservableList<T2>,
mapper: (List<T1>) -> List<T2> scope: CoroutineScope,
mapper: (List<T1>) -> List<T2>
) = addOnListChangedCallback(object : ) = addOnListChangedCallback(object :
ObservableList.OnListChangedCallback<ObservableList<T1>>() { ObservableList.OnListChangedCallback<ObservableList<T1>>() {
override fun onChanged(sender: ObservableList<T1>?) { override fun onChanged(sender: ObservableList<T1>?) {
@ -49,14 +50,17 @@ fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
updateAsync(sender ?: return) updateAsync(sender ?: return)
} }
private var updater: Disposable? = null private var updater: Job? = null
private fun updateAsync(sender: List<T1>) { private fun updateAsync(sender: List<T1>) {
updater?.dispose() updater?.cancel()
updater = sender.toSingle() updater = scope.launch {
.map { mapper(it) } val (list, diff) = withContext(Dispatchers.Default) {
.map { it to target.calculateDiff(it) } val list = mapper(sender)
.subscribeK { target.update(it.first, it.second) } list to target.calculateDiff(list)
}
target.update(list, diff)
}
} }
}) })

View File

@ -2,19 +2,11 @@ package com.topjohnwu.magisk.extensions
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
fun reboot(reason: String = if (Info.recovery) "recovery" else "") { fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit() 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() } suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }

View File

@ -8,23 +8,22 @@ import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.utils.SafetyNetHelper 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.DynamicClassLoader
import com.topjohnwu.magisk.extensions.OnErrorListener
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.ui.safetynet.SafetyNetResult import com.topjohnwu.magisk.ui.safetynet.SafetyNetResult
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import dalvik.system.DexFile import dalvik.system.DexFile
import io.reactivex.Completable
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.*
import org.json.JSONObject import org.json.JSONObject
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.inject import org.koin.core.inject
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
/** /**
@ -38,11 +37,15 @@ abstract class ViewEvent {
var handled = false var handled = false
} }
abstract class ViewEventsWithScope: ViewEvent() {
lateinit var scope: CoroutineScope
}
class CheckSafetyNetEvent( class CheckSafetyNetEvent(
private val callback: (SafetyNetResult) -> Unit = {} 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 apk: File
private lateinit var dex: File private lateinit var dex: File
@ -51,51 +54,72 @@ class CheckSafetyNetEvent(
apk = File("${context.filesDir.parent}/snet", "snet.jar") apk = File("${context.filesDir.parent}/snet", "snet.jar")
dex = File(apk.parent, "snet.dex") dex = File(apk.parent, "snet.dex")
attest(context) { scope.launch {
// Download and retry attest(context) {
Shell.sh("rm -rf " + apk.parent).exec() // Download and retry
apk.parentFile?.mkdir() withContext(Dispatchers.IO) {
download(context, true) Shell.sh("rm -rf " + apk.parent).exec()
apk.parentFile?.mkdir()
}
download(context, true)
}
} }
} }
private fun attest(context: Context, onError: OnErrorListener) { private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) {
Completable.fromAction { try {
val loader = DynamicClassLoader(apk) val helper = withContext(Dispatchers.IO) {
val dex = DexFile.loadDex(apk.path, dex.path, 0) val loader = DynamicClassLoader(apk)
val dex = DexFile.loadDex(apk.path, dex.path, 0)
// Scan through the dex and find our helper class // Scan through the dex and find our helper class
var helperClass: Class<*>? = null var helperClass: Class<*>? = null
for (className in dex.entries()) { for (className in dex.entries()) {
if (className.startsWith("x.")) { if (className.startsWith("x.")) {
val cls = loader.loadClass(className) val cls = loader.loadClass(className)
if (InvocationHandler::class.java.isAssignableFrom(cls)) { if (InvocationHandler::class.java.isAssignableFrom(cls)) {
helperClass = cls helperClass = cls
break 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() helper.attest()
}.subscribeK(onError = onError) } catch (e: Exception) {
if (e is CancellationException)
throw e
onError(e)
}
} }
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
private fun download(context: Context, askUser: Boolean) { private fun download(context: Context, askUser: Boolean) {
fun downloadInternal() = magiskRepo.fetchSafetynet() fun downloadInternal() = scope.launch {
.map { it.byteStream().writeTo(apk) } val abort: suspend (Exception) -> Unit = {
.subscribeK { attest(context) {
Timber.e(it) Timber.e(it)
callback(SafetyNetResult()) 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) { if (!askUser) {
downloadInternal() downloadInternal()
@ -126,27 +150,12 @@ class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), Activi
override fun invoke(activity: BaseActivity) = activity.run(action) 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) { override fun invoke(context: Context) {
MarkDownWindow.show(context, null, item.readme) scope.launch {
} 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"))
}
} }
}
} }
class PermissionEvent( class PermissionEvent(

View File

@ -8,6 +8,9 @@ import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.MarkDownWindow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class ManagerInstallDialog : DialogEvent() { class ManagerInstallDialog : DialogEvent() {
@ -28,7 +31,11 @@ class ManagerInstallDialog : DialogEvent() {
if (Info.remote.app.note.isEmpty()) return if (Info.remote.app.note.isEmpty()) return
applyButton(MagiskDialog.ButtonType.NEGATIVE) { applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.app_changelog 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)
}
}
} }
} }
} }

View File

@ -10,6 +10,7 @@ import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
@ -96,6 +97,11 @@ abstract class BaseViewModel(
_viewEvents.postValue(this) _viewEvents.postValue(this)
} }
fun <Event : ViewEventsWithScope> Event.publish() {
scope = viewModelScope
_viewEvents.postValue(this)
}
fun Int.publish() { fun Int.publish() {
_viewEvents.postValue(SimpleViewEvent(this)) _viewEvents.postValue(SimpleViewEvent(this))
} }

View File

@ -42,7 +42,7 @@ class FlashViewModel(
private val logItems = Collections.synchronizedList(mutableListOf<String>()) private val logItems = Collections.synchronizedList(mutableListOf<String>())
init { init {
outItems.sendUpdatesTo(items) { it.map { ConsoleItem(it) } } outItems.sendUpdatesTo(items, viewModelScope) { it.map { ConsoleItem(it) } }
outItems.copyNewInputInto(logItems) outItems.copyNewInputInto(logItems)
args.dismissId.takeIf { it != -1 }?.also { args.dismissId.takeIf { it != -1 }?.also {

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.ui.install package com.topjohnwu.magisk.ui.install
import android.content.Intent import android.content.Intent
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.extensions.value import com.topjohnwu.magisk.extensions.value
@ -21,6 +22,9 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
requireActivity().setTitle(R.string.install) requireActivity().setTitle(R.string.install)
// Allow markwon to run in viewmodel scope
binding.releaseNotes.tag = viewModel.viewModelScope
} }
} }

View File

@ -1,10 +1,9 @@
package com.topjohnwu.magisk.ui.install package com.topjohnwu.magisk.ui.install
import android.net.Uri import android.net.Uri
import android.text.SpannableString
import android.text.Spanned
import android.widget.Toast import android.widget.Toast
import androidx.databinding.ObservableField import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadService 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.core.utils.Utils
import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.value import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.entity.internal.Configuration import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject 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.model.events.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.noties.markwon.Markwon import kotlinx.coroutines.launch
import org.koin.core.get import org.koin.core.get
import kotlin.math.roundToInt import kotlin.math.roundToInt
class InstallViewModel( class InstallViewModel(
stringRepo: StringRepository, stringRepo: StringRepository
markwon: Markwon
) : BaseViewModel(State.LOADED) { ) : BaseViewModel(State.LOADED) {
val isRooted get() = Shell.rootAccess() val isRooted get() = Shell.rootAccess()
@ -35,8 +32,8 @@ class InstallViewModel(
val step = ObservableField(0) val step = ObservableField(0)
val method = ObservableField(-1) val method = ObservableField(-1)
val progress = ObservableField(0) val progress = ObservableField(0)
val data = ObservableField<Uri?>(null) val data = ObservableField(null as Uri?)
val notes = ObservableField<Spanned>(SpannableString("")) val notes = ObservableField("")
init { init {
RemoteFileService.reset() RemoteFileService.reset()
@ -50,10 +47,8 @@ class InstallViewModel(
state = State.LOADED state = State.LOADED
} }
} }
stringRepo.getString(Info.remote.magisk.note).map { viewModelScope.launch {
markwon.toMarkdown(it) notes.value = stringRepo.getString(Info.remote.magisk.note)
}.subscribeK {
notes.value = it
} }
method.addOnPropertyChangedCallback { method.addOnPropertyChangedCallback {
when (it!!) { when (it!!) {

View File

@ -50,7 +50,7 @@ class SafetynetViewModel : BaseViewModel() {
private fun attest() { private fun attest() {
currentState = LOADING currentState = LOADING
CheckSafetyNetEvent { CheckSafetyNetEvent() {
resolveResponse(it) resolveResponse(it)
}.publish() }.publish()
} }

View File

@ -16,10 +16,11 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.recycler.SettingsItem import com.topjohnwu.magisk.model.entity.recycler.SettingsItem
import com.topjohnwu.magisk.utils.asTransitive import com.topjohnwu.magisk.utils.asTransitive
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.io.File import java.io.File
// --- Customization // --- Customization
@ -38,13 +39,15 @@ object Language : SettingsItem.Selector() {
override var entries = emptyArray<String>() override var entries = emptyArray<String>()
override var entryValues = emptyArray<String>() override var entryValues = emptyArray<String>()
init { suspend fun loadLanguages(scope: CoroutineScope) {
availableLocales.subscribeK { (names, values) -> scope.launch {
entries = names availableLocales().let { (names, values) ->
entryValues = values entries = names
val selectedLocale = currentLocale.getDisplayName(currentLocale) entryValues = values
value = names.indexOfFirst { it == selectedLocale }.let { if (it == -1) 0 else it } val selectedLocale = currentLocale.getDisplayName(currentLocale)
notifyChange(BR.selectedEntry) value = names.indexOfFirst { it == selectedLocale }.let { if (it == -1) 0 else it }
notifyChange(BR.selectedEntry)
}
} }
} }
} }

View File

@ -1,9 +1,9 @@
package com.topjohnwu.magisk.ui.settings package com.topjohnwu.magisk.ui.settings
import android.Manifest
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const 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.PatchAPK
import com.topjohnwu.magisk.core.utils.Utils import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.data.database.RepoDao import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.value import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.entity.internal.Configuration import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.recycler.SettingsItem import com.topjohnwu.magisk.model.entity.recycler.SettingsItem
import com.topjohnwu.magisk.model.events.RecreateEvent 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.model.events.dialog.BiometricDialog
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.adapterOf import com.topjohnwu.magisk.ui.base.adapterOf
import com.topjohnwu.magisk.ui.base.diffListOf import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.reactivex.Completable import kotlinx.coroutines.launch
import io.reactivex.subjects.PublishSubject
import org.koin.core.get import org.koin.core.get
class SettingsViewModel( class SettingsViewModel(
@ -48,6 +45,9 @@ class SettingsViewModel(
// making theming a pain in the ass. Just forget about it // making theming a pain in the ass. Just forget about it
list.remove(Theme) list.remove(Theme)
} }
viewModelScope.launch {
Language.loadLanguages(this)
}
// Manager // Manager
list.addAll(listOf( list.addAll(listOf(
@ -125,8 +125,10 @@ class SettingsViewModel(
} }
private fun clearRepoCache() { private fun clearRepoCache() {
Completable.fromAction { repositoryDao.clear() } viewModelScope.launch {
.subscribeK { Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT) } repositoryDao.clear()
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
}
} }
private fun createHosts() { private fun createHosts() {
@ -136,14 +138,7 @@ class SettingsViewModel(
} }
private fun requireRWPermission() { private fun requireRWPermission() {
val callback = PublishSubject.create<Boolean>() withExternalRW { if (!it) requireRWPermission() }
callback.subscribeK { if (!it) requireRWPermission() }
RxPermissionEvent(
listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
), callback
).publish()
} }
private fun updateManager(hide: Boolean) { private fun updateManager(hide: Boolean) {

View File

@ -22,11 +22,8 @@ import com.google.android.material.chip.Chip
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import io.reactivex.Observable import kotlinx.coroutines.*
import io.reactivex.disposables.Disposable
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -42,14 +39,15 @@ fun setImageResource(view: AppCompatImageView, @DrawableRes resId: Int) {
@BindingAdapter("movieBehavior", "movieBehaviorText") @BindingAdapter("movieBehavior", "movieBehaviorText")
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) { fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
(view.tag as? Disposable)?.dispose() (view.tag as? Job)?.cancel()
view.tag = null
if (isMovieBehavior) { if (isMovieBehavior) {
val observer = Observable view.tag = GlobalScope.launch(Dispatchers.Main.immediate) {
.interval(150, TimeUnit.MILLISECONDS) while (true) {
.subscribeK { delay(150)
view.text = text.replaceRandomWithSpecial() view.text = text.replaceRandomWithSpecial()
} }
view.tag = observer }
} else { } else {
view.text = text view.text = text
} }

View File

@ -28,7 +28,7 @@ import com.topjohnwu.magisk.ui.base.itemBindingOf
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapters import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapters
import me.tatarka.bindingcollectionadapter2.ItemBinding import me.tatarka.bindingcollectionadapter2.ItemBinding
class MagiskDialog @JvmOverloads constructor( class MagiskDialog(
context: Context, theme: Int = 0 context: Context, theme: Int = 0
) : AppCompatDialog(context, theme) { ) : AppCompatDialog(context, theme) {

View File

@ -1,58 +1,66 @@
package com.topjohnwu.magisk.view package com.topjohnwu.magisk.view
import android.content.Context import android.content.Context
import android.text.Spanned
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import androidx.core.text.PrecomputedTextCompat
import androidx.core.widget.TextViewCompat
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.subscribeK
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.reactivex.Completable import kotlinx.coroutines.*
import io.reactivex.Single
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.inject import org.koin.core.inject
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import kotlin.coroutines.coroutineContext
import java.util.*
object MarkDownWindow : KoinComponent { class PrecomputedTextSetter : Markwon.TextSetter {
private val stringRepo: StringRepository by inject() override fun setText(tv: TextView, text: Spanned, bufferType: TextView.BufferType, onComplete: Runnable) {
private val markwon: Markwon by inject() val scope = tv.tag as? CoroutineScope ?: GlobalScope
scope.launch(Dispatchers.Default) {
fun show(activity: Context, title: String?, url: String) { val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
show(activity, title, stringRepo.getString(url)) tv.post {
} TextViewCompat.setPrecomputedText(tv, pre)
onComplete.run()
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)
} }
}
fun show(activity: Context, title: String?, content: Single<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)
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<TextView>(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()
}
}

View File

@ -228,11 +228,12 @@
app:strokeWidth="@{viewModel.step != 0 ? 0f : @dimen/l_125}"> app:strokeWidth="@{viewModel.step != 0 ? 0f : @dimen/l_125}">
<TextView <TextView
android:id="@+id/release_notes"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="15dp" android:layout_margin="15dp"
android:textAppearance="@style/AppearanceFoundation.Caption" android:textAppearance="@style/AppearanceFoundation.Caption"
android:text="@{viewModel.notes}"/> markdownText="@{viewModel.notes}"/>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>