Remove more code using RxJava
This commit is contained in:
parent
f7a650b9a4
commit
6348d0a6fb
@ -13,16 +13,12 @@ import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
|||||||
import com.topjohnwu.magisk.extensions.chooser
|
import com.topjohnwu.magisk.extensions.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,33 +62,30 @@ 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)
|
||||||
}.subscribeK {
|
|
||||||
remove(id)
|
remove(id)
|
||||||
when (subject.configuration) {
|
when (subject.configuration) {
|
||||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||||
is APK.Restore -> Unit
|
is APK.Restore -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
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
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
|
||||||
// --
|
// --
|
||||||
|
@ -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())
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
start(it)
|
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) {
|
||||||
|
is Module ->
|
||||||
|
stream.toModule(subject.file, service.fetchInstaller().byteStream())
|
||||||
|
else ->
|
||||||
|
stream.writeTo(subject.file)
|
||||||
|
}
|
||||||
|
}
|
||||||
val newId = finishNotify(subject)
|
val newId = finishNotify(subject)
|
||||||
if (ForegroundTracker.hasForeground) {
|
if (ForegroundTracker.hasForeground) {
|
||||||
onFinished(subject, newId)
|
onFinished(subject, newId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
|
||||||
check(subject is Magisk) { "Download cache is disabled" }
|
|
||||||
check(subject.file.exists() &&
|
|
||||||
ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) {
|
|
||||||
"The given file does not match checksum"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
|
||||||
.map { it.toProgressStream(subject) }
|
|
||||||
.flatMapCompletable { stream ->
|
|
||||||
when (subject) {
|
|
||||||
is Module -> service.fetchInstaller()
|
|
||||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
|
||||||
.ignoreElement()
|
|
||||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
|
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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
patchAndHide(context, label)
|
patchAndHide(context, label)
|
||||||
}.subscribeK {
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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) })
|
|
||||||
|
@ -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()
|
||||||
@ -26,6 +26,7 @@ fun List<String>.toShellCmd(): String {
|
|||||||
|
|
||||||
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||||
target: DiffObservableList<T2>,
|
target: DiffObservableList<T2>,
|
||||||
|
scope: CoroutineScope,
|
||||||
mapper: (List<T1>) -> List<T2>
|
mapper: (List<T1>) -> List<T2>
|
||||||
) = addOnListChangedCallback(object :
|
) = addOnListChangedCallback(object :
|
||||||
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
ObservableList.OnListChangedCallback<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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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() }
|
||||||
|
@ -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,16 +54,21 @@ 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")
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
attest(context) {
|
attest(context) {
|
||||||
// Download and retry
|
// Download and retry
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
Shell.sh("rm -rf " + apk.parent).exec()
|
Shell.sh("rm -rf " + apk.parent).exec()
|
||||||
apk.parentFile?.mkdir()
|
apk.parentFile?.mkdir()
|
||||||
|
}
|
||||||
download(context, true)
|
download(context, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun attest(context: Context, onError: OnErrorListener) {
|
private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) {
|
||||||
Completable.fromAction {
|
try {
|
||||||
|
val helper = withContext(Dispatchers.IO) {
|
||||||
val loader = DynamicClassLoader(apk)
|
val loader = DynamicClassLoader(apk)
|
||||||
val dex = DexFile.loadDex(apk.path, dex.path, 0)
|
val dex = DexFile.loadDex(apk.path, dex.path, 0)
|
||||||
|
|
||||||
@ -79,23 +87,39 @@ class CheckSafetyNetEvent(
|
|||||||
|
|
||||||
val helper = helperClass
|
val helper = helperClass
|
||||||
.getMethod("get", Class::class.java, Context::class.java, Any::class.java)
|
.getMethod("get", Class::class.java, Context::class.java, Any::class.java)
|
||||||
.invoke(null, SafetyNetHelper::class.java, context, this) as SafetyNetHelper
|
.invoke(null, SafetyNetHelper::class.java,
|
||||||
|
context, this@CheckSafetyNetEvent) as SafetyNetHelper
|
||||||
|
|
||||||
if (helper.version < Const.SNET_EXT_VER)
|
if (helper.version < Const.SNET_EXT_VER)
|
||||||
throw Exception()
|
throw Exception()
|
||||||
|
helper
|
||||||
|
}
|
||||||
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,25 +150,10 @@ 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"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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!!) {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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,8 +39,9 @@ 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 {
|
||||||
|
availableLocales().let { (names, values) ->
|
||||||
entries = names
|
entries = names
|
||||||
entryValues = values
|
entryValues = values
|
||||||
val selectedLocale = currentLocale.getDisplayName(currentLocale)
|
val selectedLocale = currentLocale.getDisplayName(currentLocale)
|
||||||
@ -47,6 +49,7 @@ object Language : SettingsItem.Selector() {
|
|||||||
notifyChange(BR.selectedEntry)
|
notifyChange(BR.selectedEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Theme : SettingsItem.Blank() {
|
object Theme : SettingsItem.Blank() {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
|
@ -1,50 +1,60 @@
|
|||||||
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.*
|
|
||||||
|
class PrecomputedTextSetter : Markwon.TextSetter {
|
||||||
|
|
||||||
|
override fun setText(tv: TextView, text: Spanned, bufferType: TextView.BufferType, onComplete: Runnable) {
|
||||||
|
val scope = tv.tag as? CoroutineScope ?: GlobalScope
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
|
||||||
|
tv.post {
|
||||||
|
TextViewCompat.setPrecomputedText(tv, pre)
|
||||||
|
onComplete.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object MarkDownWindow : KoinComponent {
|
object MarkDownWindow : KoinComponent {
|
||||||
|
|
||||||
private val stringRepo: StringRepository by inject()
|
private val repo: StringRepository by inject()
|
||||||
private val markwon: Markwon by inject()
|
private val markwon: Markwon by inject()
|
||||||
|
|
||||||
fun show(activity: Context, title: String?, url: String) {
|
suspend fun show(activity: Context, title: String?, url: String) {
|
||||||
show(activity, title, stringRepo.getString(url))
|
show(activity, title) {
|
||||||
}
|
repo.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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show(activity: Context, title: String?, content: Single<String>) {
|
suspend fun show(activity: Context, title: String?, input: suspend () -> String) {
|
||||||
val mdRes = R.layout.markdown_window_md2
|
val mdRes = R.layout.markdown_window_md2
|
||||||
val mv = LayoutInflater.from(activity).inflate(mdRes, null)
|
val mv = LayoutInflater.from(activity).inflate(mdRes, null)
|
||||||
val tv = mv.findViewById<TextView>(R.id.md_txt)
|
val tv = mv.findViewById<TextView>(R.id.md_txt)
|
||||||
|
tv.tag = CoroutineScope(coroutineContext)
|
||||||
|
|
||||||
content.map {
|
try {
|
||||||
markwon.setMarkdown(tv, it)
|
markwon.setMarkdown(tv, input())
|
||||||
}.ignoreElement().onErrorResumeNext {
|
} catch (e: Exception) {
|
||||||
// Nothing we can actually do other than show error message
|
if (e is CancellationException)
|
||||||
Timber.e(it)
|
throw e
|
||||||
|
Timber.e(e)
|
||||||
tv.setText(R.string.download_file_error)
|
tv.setText(R.string.download_file_error)
|
||||||
Completable.complete()
|
}
|
||||||
}.subscribeK {
|
|
||||||
MagiskDialog(activity)
|
MagiskDialog(activity)
|
||||||
.applyTitle(title ?: "")
|
.applyTitle(title ?: "")
|
||||||
.applyView(mv)
|
.applyView(mv)
|
||||||
@ -52,7 +62,5 @@ object MarkDownWindow : KoinComponent {
|
|||||||
titleRes = android.R.string.cancel
|
titleRes = android.R.string.cancel
|
||||||
}
|
}
|
||||||
.reveal()
|
.reveal()
|
||||||
return@subscribeK
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user