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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ data class Repo(
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
val readme get() = stringRepo.getReadme(this)
suspend fun readme() = stringRepo.getReadme(this)
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,9 +7,10 @@ class StringRepository(
private val api: GithubRawServices
) {
fun getString(url: String) = api.fetchString(url)
suspend fun getString(url: String) = api.fetchString(url)
suspend fun getMetadata(repo: Repo) = api.fetchModuleFile(repo.id, "module.prop")
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
suspend fun getReadme(repo: Repo) = api.fetchModuleFile(repo.id, "README.md")
}

View File

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

View File

@ -7,6 +7,7 @@ import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
import com.topjohnwu.magisk.view.PrecomputedTextSetter
import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin
@ -93,6 +94,7 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
return Markwon.builder(context)
.textSetter(PrecomputedTextSetter())
.usePlugin(HtmlPlugin.create())
.usePlugin(ImagesPlugin.create {
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))

View File

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

View File

@ -1,39 +1,15 @@
package com.topjohnwu.magisk.extensions
import androidx.databinding.ObservableField
import com.topjohnwu.superuser.internal.UiThreadHandler
import io.reactivex.*
import io.reactivex.Observable
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposables
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers
import androidx.databinding.Observable as BindingObservable
fun <T> Observable<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun <T> Flowable<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun <T> Single<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun <T> Maybe<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Maybe<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun Completable.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn)
/*=== ALIASES FOR OBSERVABLES ===*/
typealias OnCompleteListener = () -> Unit
@ -49,128 +25,3 @@ fun <T> Observable<T>.subscribeK(
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun <T> Single<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError)
fun <T> Maybe<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onSuccess: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onSuccess, onError, onComplete)
fun <T> Flowable<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun Completable.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {}
) = applySchedulers()
.subscribe(onComplete, onError)
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { UiThreadHandler.run { body() } }
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { UiThreadHandler.run { body() } }
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { UiThreadHandler.run { body() } }
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { UiThreadHandler.run { body() } }
fun Completable.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { UiThreadHandler.run { body() } }
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { UiThreadHandler.run { body(it) } }
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { UiThreadHandler.run { body(it) } }
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { UiThreadHandler.run { body(it) } }
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { UiThreadHandler.run { body(it) } }
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { UiThreadHandler.run { body(it) } }
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
doOnNext { UiThreadHandler.run { body(it) } }
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
doOnNext { UiThreadHandler.run { body(it) } }
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
doOnSuccess { UiThreadHandler.run { body(it) } }
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
doOnSuccess { UiThreadHandler.run { body(it) } }
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
doOnComplete { UiThreadHandler.run { body() } }
fun Completable.doOnCompleteUi(body: () -> Unit) =
doOnComplete { UiThreadHandler.run { body() } }
fun <T, R> Observable<List<T>>.mapList(
transformer: (T) -> R
) = flatMapIterable { it }
.map(transformer)
.toList()
fun <T, R> Single<List<T>>.mapList(
transformer: (T) -> R
) = flattenAsFlowable { it }
.map(transformer)
.toList()
fun <T, R> Maybe<List<T>>.mapList(
transformer: (T) -> R
) = flattenAsFlowable { it }
.map(transformer)
.toList()
fun <T, R> Flowable<List<T>>.mapList(
transformer: (T) -> R
) = flatMapIterable { it }
.map(transformer)
.toList()
fun <T> ObservableField<T>.toObservable(): Observable<T> {
val observableField = this
return Observable.create { emitter ->
observableField.get()?.let { emitter.onNext(it) }
val callback = object : BindingObservable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) {
observableField.get()?.let { emitter.onNext(it) }
}
}
observableField.addOnPropertyChangedCallback(callback)
emitter.setDisposable(Disposables.fromAction {
observableField.removeOnPropertyChangedCallback(callback)
})
}
}
fun <T : Any> T.toSingle() = Single.just(this)
inline fun <T1, T2, R> zip(
t1: Single<T1>,
t2: Single<T2>,
crossinline zipper: (T1, T2) -> R
) = Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })

View File

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

View File

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

View File

@ -8,23 +8,22 @@ import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.utils.SafetyNetHelper
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.DynamicClassLoader
import com.topjohnwu.magisk.extensions.OnErrorListener
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.ui.safetynet.SafetyNetResult
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.superuser.Shell
import dalvik.system.DexFile
import io.reactivex.Completable
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.*
import org.json.JSONObject
import org.koin.core.KoinComponent
import org.koin.core.inject
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.lang.reflect.InvocationHandler
/**
@ -38,11 +37,15 @@ abstract class ViewEvent {
var handled = false
}
abstract class ViewEventsWithScope: ViewEvent() {
lateinit var scope: CoroutineScope
}
class CheckSafetyNetEvent(
private val callback: (SafetyNetResult) -> Unit = {}
) : ViewEvent(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
) : ViewEventsWithScope(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback {
private val magiskRepo by inject<MagiskRepository>()
private val svc by inject<GithubRawServices>()
private lateinit var apk: File
private lateinit var dex: File
@ -51,51 +54,72 @@ class CheckSafetyNetEvent(
apk = File("${context.filesDir.parent}/snet", "snet.jar")
dex = File(apk.parent, "snet.dex")
attest(context) {
// Download and retry
Shell.sh("rm -rf " + apk.parent).exec()
apk.parentFile?.mkdir()
download(context, true)
scope.launch {
attest(context) {
// Download and retry
withContext(Dispatchers.IO) {
Shell.sh("rm -rf " + apk.parent).exec()
apk.parentFile?.mkdir()
}
download(context, true)
}
}
}
private fun attest(context: Context, onError: OnErrorListener) {
Completable.fromAction {
val loader = DynamicClassLoader(apk)
val dex = DexFile.loadDex(apk.path, dex.path, 0)
private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) {
try {
val helper = withContext(Dispatchers.IO) {
val loader = DynamicClassLoader(apk)
val dex = DexFile.loadDex(apk.path, dex.path, 0)
// Scan through the dex and find our helper class
var helperClass: Class<*>? = null
for (className in dex.entries()) {
if (className.startsWith("x.")) {
val cls = loader.loadClass(className)
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
helperClass = cls
break
// Scan through the dex and find our helper class
var helperClass: Class<*>? = null
for (className in dex.entries()) {
if (className.startsWith("x.")) {
val cls = loader.loadClass(className)
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
helperClass = cls
break
}
}
}
helperClass ?: throw Exception()
val helper = helperClass
.getMethod("get", Class::class.java, Context::class.java, Any::class.java)
.invoke(null, SafetyNetHelper::class.java,
context, this@CheckSafetyNetEvent) as SafetyNetHelper
if (helper.version < Const.SNET_EXT_VER)
throw Exception()
helper
}
helperClass ?: throw Exception()
val helper = helperClass
.getMethod("get", Class::class.java, Context::class.java, Any::class.java)
.invoke(null, SafetyNetHelper::class.java, context, this) as SafetyNetHelper
if (helper.version < Const.SNET_EXT_VER)
throw Exception()
helper.attest()
}.subscribeK(onError = onError)
} catch (e: Exception) {
if (e is CancellationException)
throw e
onError(e)
}
}
@Suppress("SameParameterValue")
private fun download(context: Context, askUser: Boolean) {
fun downloadInternal() = magiskRepo.fetchSafetynet()
.map { it.byteStream().writeTo(apk) }
.subscribeK { attest(context) {
fun downloadInternal() = scope.launch {
val abort: suspend (Exception) -> Unit = {
Timber.e(it)
callback(SafetyNetResult())
} }
}
try {
withContext(Dispatchers.IO) {
svc.fetchSafetynet().byteStream().writeTo(apk)
}
attest(context, abort)
} catch (e: IOException) {
if (e is CancellationException)
throw e
abort(e)
}
}
if (!askUser) {
downloadInternal()
@ -126,27 +150,12 @@ class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), Activi
override fun invoke(activity: BaseActivity) = activity.run(action)
}
class OpenChangelogEvent(val item: Repo) : ViewEvent(), ContextExecutor {
class OpenChangelogEvent(val item: Repo) : ViewEventsWithScope(), ContextExecutor {
override fun invoke(context: Context) {
MarkDownWindow.show(context, null, item.readme)
}
}
class RxPermissionEvent(
val permissions: List<String>,
val callback: PublishSubject<Boolean>
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseActivity) =
activity.withPermissions(*permissions.toTypedArray()) {
onSuccess {
callback.onNext(true)
}
onFailure {
callback.onNext(false)
callback.onError(SecurityException("User refused permissions"))
}
scope.launch {
MarkDownWindow.show(context, null, item::readme)
}
}
}
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.view.MagiskDialog
import com.topjohnwu.magisk.view.MarkDownWindow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class ManagerInstallDialog : DialogEvent() {
@ -28,7 +31,11 @@ class ManagerInstallDialog : DialogEvent() {
if (Info.remote.app.note.isEmpty()) return
applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.app_changelog
onClick { MarkDownWindow.show(context, null, Info.remote.app.note) }
onClick {
GlobalScope.launch(Dispatchers.Main.immediate) {
MarkDownWindow.show(context, null, Info.remote.app.note)
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,58 +1,66 @@
package com.topjohnwu.magisk.view
import android.content.Context
import android.text.Spanned
import android.view.LayoutInflater
import android.widget.TextView
import androidx.core.text.PrecomputedTextCompat
import androidx.core.widget.TextViewCompat
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.subscribeK
import io.noties.markwon.Markwon
import io.reactivex.Completable
import io.reactivex.Single
import kotlinx.coroutines.*
import org.koin.core.KoinComponent
import org.koin.core.inject
import timber.log.Timber
import java.io.InputStream
import java.util.*
import kotlin.coroutines.coroutineContext
object MarkDownWindow : KoinComponent {
class PrecomputedTextSetter : Markwon.TextSetter {
private val stringRepo: StringRepository by inject()
private val markwon: Markwon by inject()
fun show(activity: Context, title: String?, url: String) {
show(activity, title, stringRepo.getString(url))
}
fun show(activity: Context, title: String?, input: InputStream) {
Single.just(Scanner(input, "UTF-8").apply { useDelimiter("\\A") })
.map { it.next() }
.also {
show(activity, title, it)
override fun setText(tv: TextView, text: Spanned, bufferType: TextView.BufferType, onComplete: Runnable) {
val scope = tv.tag as? CoroutineScope ?: GlobalScope
scope.launch(Dispatchers.Default) {
val pre = PrecomputedTextCompat.create(text, TextViewCompat.getTextMetricsParams(tv))
tv.post {
TextViewCompat.setPrecomputedText(tv, pre)
onComplete.run()
}
}
fun show(activity: Context, title: String?, content: Single<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}">
<TextView
android:id="@+id/release_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:text="@{viewModel.notes}"/>
markdownText="@{viewModel.notes}"/>
</com.google.android.material.card.MaterialCardView>