1
mirror of https://github.com/topjohnwu/Magisk synced 2024-11-13 20:54:12 +01:00

Remove more RxJava

This commit is contained in:
topjohnwu 2020-07-09 04:49:14 -07:00
parent 4631077c49
commit 8647ba4729
26 changed files with 351 additions and 397 deletions

View File

@ -126,7 +126,7 @@ dependencies {
val vRoom = "2.2.5"
implementation("androidx.room:room-runtime:${vRoom}")
implementation("androidx.room:room-rxjava2:${vRoom}")
implementation("androidx.room:room-ktx:${vRoom}")
kapt("androidx.room:room-compiler:${vRoom}")
implementation("androidx.navigation:navigation-fragment-ktx:${Deps.vNav}")
@ -139,9 +139,10 @@ dependencies {
implementation("androidx.preference:preference:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.2.5")
implementation("androidx.work:work-runtime:2.3.4")
implementation("androidx.work:work-runtime-ktx:2.3.4")
implementation("androidx.transition:transition:1.3.1")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.core:core-ktx:1.3.0")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
implementation("com.google.android.material:material:1.2.0-beta01")
}

View File

@ -12,6 +12,8 @@ import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.core.inject
open class GeneralReceiver : BaseReceiver() {
@ -25,6 +27,10 @@ open class GeneralReceiver : BaseReceiver() {
override fun onReceive(context: ContextWrapper, intent: Intent?) {
intent ?: return
fun rmPolicy(pkg: String) = GlobalScope.launch {
policyDB.delete(pkg)
}
when (intent.action ?: return) {
Intent.ACTION_REBOOT -> {
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
@ -32,11 +38,11 @@ open class GeneralReceiver : BaseReceiver() {
Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O
if (Config.suReAuth)
policyDB.delete(getPkg(intent)).blockingGet()
rmPolicy(getPkg(intent))
}
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent)
policyDB.delete(pkg).blockingGet()
rmPolicy(pkg)
Shell.su("magiskhide --rm $pkg").submit()
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)

View File

@ -1,31 +1,33 @@
package com.topjohnwu.magisk.core
import android.content.Context
import androidx.work.Worker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.view.Notifications
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
import org.koin.core.inject
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
: Worker(context, workerParams) {
: CoroutineWorker(context, workerParams), KoinComponent {
private val magiskRepo: MagiskRepository by inject()
override fun doWork(): Result {
override suspend fun doWork(): Result {
// Make sure shell initializer was ran
Shell.getShell()
return runCatching {
magiskRepo.fetchUpdate().blockingGet()
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
withContext(Dispatchers.IO) {
Shell.getShell()
}
return magiskRepo.fetchUpdate()?.let {
if (BuildConfig.VERSION_CODE < it.app.versionCode)
Notifications.managerUpdate(applicationContext)
else if (Info.env.isActive && Info.env.magiskVersionCode < Info.remote.magisk.versionCode)
else if (Info.env.isActive && Info.env.magiskVersionCode < it.magisk.versionCode)
Notifications.magiskUpdate(applicationContext)
Result.success()
}.getOrElse {
Result.failure()
}
} ?: Result.failure()
}
}

View File

@ -1,8 +1,6 @@
package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
abstract class BaseDao {
@ -20,25 +18,11 @@ abstract class BaseDao {
@TableStrict
abstract val table: String
inline fun <reified Builder : Query.Builder> query(builder: Builder.() -> Unit = {}) =
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
Builder::class.java.newInstance()
.apply { table = this@BaseDao.table }
.apply(builder)
.toString()
.let { Query(it) }
.query()
}
fun Query.query() = query.su()
private fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
private fun String.su() = suRaw().map { it.toMap() }
private fun List<String>.toMap() = map { it.split(Regex("\\|")) }
.map { it.toMapInternal() }
private fun List<String>.toMapInternal() = map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { Pair(it[0], it[1]) }
.toMap()

View File

@ -7,6 +7,8 @@ import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.core.model.toMap
import com.topjohnwu.magisk.core.model.toPolicy
import com.topjohnwu.magisk.extensions.now
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit
@ -17,55 +19,56 @@ class PolicyDao(
override val table: String = Table.POLICY
fun deleteOutdated(
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
) = query<Delete> {
suspend fun deleteOutdated() = buildQuery<Delete> {
condition {
greaterThan("until", "0")
and {
lessThan("until", nowSeconds.toString())
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
}
or {
lessThan("until", "0")
}
}
}.ignoreElement()
}.commit()
fun delete(packageName: String) = query<Delete> {
suspend fun delete(packageName: String) = buildQuery<Delete> {
condition {
equals("package_name", packageName)
}
}.ignoreElement()
}.commit()
fun delete(uid: Int) = query<Delete> {
suspend fun delete(uid: Int) = buildQuery<Delete> {
condition {
equals("uid", uid)
}
}.ignoreElement()
}.commit()
fun fetch(uid: Int) = query<Select> {
suspend fun fetch(uid: Int) = buildQuery<Select> {
condition {
equals("uid", uid)
}
}.map { it.first().toPolicySafe() }
}.query().first().toPolicyOrNull()
fun update(policy: MagiskPolicy) = query<Replace> {
suspend fun update(policy: MagiskPolicy) = buildQuery<Replace> {
values(policy.toMap())
}.ignoreElement()
}.commit()
fun fetchAll() = query<Select> {
suspend fun <R: Any> fetchAll(mapper: (MagiskPolicy) -> R) = buildQuery<Select> {
condition {
equals("uid/100000", Const.USER_ID)
}
}.map { it.mapNotNull { it.toPolicySafe() } }
}.query {
it.toPolicyOrNull()?.let(mapper)
}
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
private fun Map<String, String>.toPolicyOrNull(): MagiskPolicy? {
return runCatching { toPolicy(context.packageManager) }.getOrElse {
Timber.e(it)
if (it is PackageManager.NameNotFoundException) {
val uid = getOrElse("uid") { null } ?: return null
delete(uid).subscribe()
GlobalScope.launch {
delete(uid.toInt())
}
}
null
}

View File

@ -1,6 +1,12 @@
package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef
import com.topjohnwu.magisk.extensions.await
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
class Query(private val _query: String) {
val query get() = "magisk --sqlite '$_query'"
@ -9,6 +15,24 @@ class Query(private val _query: String) {
val requestType: String
var table: String
}
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
withContext(Dispatchers.Default) {
Shell.su(query).await().out.map { line ->
async {
line.split("\\|".toRegex())
.map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { it[0] to it[1] }
.toMap()
.let(mapper)
}
}.awaitAll().filterNotNull()
}
suspend inline fun query() = query { it }
suspend inline fun commit() = Shell.su(query).to(null).await()
}
class Delete : Query.Builder {

View File

@ -4,17 +4,19 @@ class SettingsDao : BaseDao() {
override val table = Table.SETTINGS
fun delete(key: String) = query<Delete> {
suspend fun delete(key: String) = buildQuery<Delete> {
condition { equals("key", key) }
}.ignoreElement()
}.commit()
fun put(key: String, value: Int) = query<Replace> {
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
values("key" to key, "value" to value)
}.ignoreElement()
}.commit()
fun fetch(key: String, default: Int = -1) = query<Select> {
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
fields("value")
condition { equals("key", key) }
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
}.query {
it["value"]?.toIntOrNull()
}.firstOrNull() ?: default
}

View File

@ -4,17 +4,19 @@ class StringDao : BaseDao() {
override val table = Table.STRINGS
fun delete(key: String) = query<Delete> {
suspend fun delete(key: String) = buildQuery<Delete> {
condition { equals("key", key) }
}.ignoreElement()
}.commit()
fun put(key: String, value: String) = query<Replace> {
suspend fun put(key: String, value: String) = buildQuery<Replace> {
values("key" to key, "value" to value)
}.ignoreElement()
}.commit()
fun fetch(key: String, default: String = "") = query<Select> {
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
fields("value")
condition { equals("key", key) }
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
}.query {
it["value"]
}.firstOrNull() ?: default
}

View File

@ -19,10 +19,11 @@ import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.startActivity
import com.topjohnwu.magisk.extensions.startActivityWithRoot
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.toLog
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
object SuCallbackHandler : ProviderCallHandler {
@ -111,7 +112,9 @@ object SuCallbackHandler : ProviderCallHandler {
)
val logRepo = get<LogRepository>()
logRepo.insert(log).subscribeK(onError = { Timber.e(it) })
GlobalScope.launch {
logRepo.insert(log)
}
}
private fun handleNotify(context: Context, data: Bundle) {

View File

@ -111,13 +111,13 @@ abstract class SuRequestHandler(
} catch (e: IOException) {
Timber.e(e)
} finally {
if (until >= 0)
policyDB.update(policy).blockingAwait()
runCatching {
input.close()
output.close()
socket.close()
}
if (until >= 0)
policyDB.update(policy)
}
}
}

View File

@ -2,8 +2,8 @@ package com.topjohnwu.magisk.data.database
import androidx.room.*
import com.topjohnwu.magisk.model.entity.MagiskLog
import io.reactivex.Completable
import io.reactivex.Single
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
@Database(version = 1, entities = [MagiskLog::class], exportSchema = false)
@ -18,19 +18,20 @@ abstract class SuLogDao(private val db: SuLogDatabase) {
private val twoWeeksAgo =
Calendar.getInstance().apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
fun deleteAll() = Completable.fromAction { db.clearAllTables() }
suspend fun deleteAll() = withContext(Dispatchers.IO) { db.clearAllTables() }
fun fetchAll() = deleteOutdated().andThen(fetch())
suspend fun fetchAll(): MutableList<MagiskLog> {
deleteOutdated()
return fetch()
}
@Query("SELECT * FROM logs ORDER BY time DESC")
protected abstract fun fetch(): Single<MutableList<MagiskLog>>
@Insert
abstract fun insert(log: MagiskLog): Completable
protected abstract suspend fun fetch(): MutableList<MagiskLog>
@Query("DELETE FROM logs WHERE time < :timeout")
protected abstract fun deleteOutdated(
timeout: Long = twoWeeksAgo
): Completable
protected abstract suspend fun deleteOutdated(timeout: Long = twoWeeksAgo)
@Insert
abstract suspend fun insert(log: MagiskLog)
}

View File

@ -13,16 +13,16 @@ interface GithubRawServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
fun fetchStableUpdate(): Single<UpdateInfo>
suspend fun fetchStableUpdate(): UpdateInfo
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaUpdate(): Single<UpdateInfo>
suspend fun fetchBetaUpdate(): UpdateInfo
@GET("$MAGISK_FILES/canary/debug.json")
fun fetchCanaryUpdate(): Single<UpdateInfo>
suspend fun fetchCanaryUpdate(): UpdateInfo
@GET
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
@Streaming

View File

@ -2,7 +2,9 @@ package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@ -38,17 +40,19 @@ class DBSettingsValue(
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
if (value == null)
value = thisRef.settingsDao.fetch(name, default).blockingGet()
return value!!
value = runBlocking {
thisRef.settingsDao.fetch(name, default)
}
return value as Int
}
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
synchronized(this) {
this.value = value
}
thisRef.settingsDao.put(name, value)
.subscribeOn(Schedulers.io())
.subscribe()
GlobalScope.launch {
thisRef.settingsDao.put(name, value)
}
}
}
@ -77,7 +81,9 @@ class DBStringsValue(
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
if (value == null)
value = thisRef.stringDao.fetch(name, default).blockingGet()
value = runBlocking {
thisRef.stringDao.fetch(name, default)
}
return value!!
}
@ -87,19 +93,23 @@ class DBStringsValue(
}
if (value.isEmpty()) {
if (sync) {
thisRef.stringDao.delete(name).blockingAwait()
runBlocking {
thisRef.stringDao.delete(name)
}
} else {
thisRef.stringDao.delete(name)
.subscribeOn(Schedulers.io())
.subscribe()
GlobalScope.launch {
thisRef.stringDao.delete(name)
}
}
} else {
if (sync) {
thisRef.stringDao.put(name, value).blockingAwait()
runBlocking {
thisRef.stringDao.put(name, value)
}
} else {
thisRef.stringDao.put(name, value)
.subscribeOn(Schedulers.io())
.subscribe()
GlobalScope.launch {
thisRef.stringDao.put(name, value)
}
}
}
}

View File

@ -2,19 +2,18 @@ package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.database.SuLogDao
import com.topjohnwu.magisk.extensions.await
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
import io.reactivex.Single
class LogRepository(
private val logDao: SuLogDao
) {
fun fetchLogs() = logDao.fetchAll()
suspend fun fetchSuLogs() = logDao.fetchAll()
fun fetchMagiskLogs() = Single.fromCallable {
suspend fun fetchMagiskLogs(): String {
val list = object : AbstractMutableList<String>() {
val buf = StringBuilder()
override val size get() = 0
@ -28,16 +27,15 @@ class LogRepository(
}
}
}
Shell.su("cat ${Const.MAGISK_LOG}").to(list).exec()
list.buf.toString()
Shell.su("cat ${Const.MAGISK_LOG}").to(list).await()
return list.buf.toString()
}
fun clearLogs() = logDao.deleteAll()
suspend fun clearLogs() = logDao.deleteAll()
fun clearMagiskLogs() = Completable.fromAction {
Shell.su("echo -n > ${Const.MAGISK_LOG}").exec()
}
fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
Shell.su("echo -n > ${Const.MAGISK_LOG}").submit(cb)
fun insert(log: MagiskLog) = logDao.insert(log)
suspend fun insert(log: MagiskLog) = logDao.insert(log)
}

View File

@ -4,55 +4,58 @@ import android.content.pm.PackageManager
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.await
import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.extensions.packageName
import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
class MagiskRepository(
private val apiRaw: GithubRawServices,
private val packageManager: PackageManager
private val apiRaw: GithubRawServices,
private val packageManager: PackageManager
) {
fun fetchSafetynet() = apiRaw.fetchSafetynet()
fun fetchUpdate() = when (Config.updateChannel) {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}.flatMap {
// If remote version is lower than current installed, try switching to beta
if (it.magisk.versionCode < Info.env.magiskVersionCode
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
apiRaw.fetchBetaUpdate()
} else {
Single.just(it)
suspend fun fetchUpdate() = try {
var info = when (Config.updateChannel) {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}
}.doOnSuccess { Info.remote = it }
if (info.magisk.versionCode < Info.env.magiskVersionCode &&
Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
info = apiRaw.fetchBetaUpdate()
}
Info.remote = info
info
} catch (e: IOException) {
Timber.e(e)
null
}
fun fetchApps() =
Single.fromCallable { packageManager.getInstalledApplications(0) }
.flattenAsFlowable { it }
.filter { it.enabled && !blacklist.contains(it.packageName) }
.map {
val label = it.getLabel(packageManager)
val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon)
}
.filter { it.processes.isNotEmpty() }
.toList()
suspend fun fetchApps() = withContext(Dispatchers.Default) {
packageManager.getInstalledApplications(0).filter {
it.enabled && !blacklist.contains(it.packageName)
}.map {
val label = it.getLabel(packageManager)
val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon)
}.filter { it.processes.isNotEmpty() }
}
fun fetchHideTargets() = Shell.su("magiskhide --ls").toSingle()
.map { it.exec().out }
.flattenAsFlowable { it }
.map { HideTarget(it) }
.toList()
suspend fun fetchHideTargets() =
Shell.su("\"magiskhide --ls\"").await().out.map {
HideTarget(it)
}
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
Shell.su("magiskhide --${isEnabled.state} $packageName $process").submit()

View File

@ -2,7 +2,6 @@ package com.topjohnwu.magisk.di
import android.content.Context
import android.os.Build
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import com.topjohnwu.magisk.core.ResMgr
import com.topjohnwu.magisk.utils.RxBus
@ -19,7 +18,6 @@ val applicationModule = module {
factory(Protected) { createDEContext(get()) }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
single { LocalBroadcastManager.getInstance(get()) }
}
private fun createDEContext(context: Context): Context {

View File

@ -4,6 +4,8 @@ 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 "") {
@ -14,3 +16,5 @@ 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

@ -2,9 +2,13 @@ package com.topjohnwu.magisk.model.entity
class HideTarget(line: String) {
private val split = line.split(Regex("\\|"), 2)
val packageName: String
val process: String
val packageName = split[0]
val process = split.getOrElse(1) { packageName }
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}
}

View File

@ -4,6 +4,7 @@ import android.Manifest
import androidx.annotation.CallSuper
import androidx.core.graphics.Insets
import androidx.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
@ -13,23 +14,18 @@ import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.navigation.NavigationWrapper
import com.topjohnwu.magisk.model.observer.Observer
import io.reactivex.*
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.Job
import org.koin.core.KoinComponent
import androidx.databinding.Observable as BindingObservable
abstract class BaseViewModel(
initialState: State = State.LOADING,
val useRx: Boolean = true
) : ViewModel(), BindingObservable, KoinComponent {
initialState: State = State.LOADING
) : ViewModel(), Observable, KoinComponent {
enum class State {
LOADED, LOADING, LOADING_FAILED
@ -53,8 +49,8 @@ abstract class BaseViewModel(
private val _viewEvents = MutableLiveData<ViewEvent>()
private var runningTask: Disposable? = null
private var runningJob: Job? = null
private val refreshCallback = object : androidx.databinding.Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: androidx.databinding.Observable?, propertyId: Int) {
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
requestRefresh()
}
}
@ -66,13 +62,6 @@ abstract class BaseViewModel(
/** This should probably never be called manually, it's called manually via delegate. */
@Synchronized
fun requestRefresh() {
if (useRx) {
if (runningTask?.isDisposed?.not() == true) {
return
}
runningTask = rxRefresh()
return
}
if (runningJob?.isActive == true) {
return
}
@ -100,11 +89,6 @@ abstract class BaseViewModel(
ViewActionEvent(action).publish()
}
fun withPermissions(vararg permissions: String): Observable<Boolean> {
val subject = PublishSubject.create<Boolean>()
return subject.doOnSubscribeUi { RxPermissionEvent(permissions.toList(), subject).publish() }
}
fun withPermissions(vararg permissions: String, callback: (Boolean) -> Unit) {
PermissionEvent(permissions.toList(), callback).publish()
}
@ -137,7 +121,7 @@ abstract class BaseViewModel(
private var callbacks: PropertyChangeRegistry? = null
@Synchronized
override fun addOnPropertyChangedCallback(callback: BindingObservable.OnPropertyChangedCallback) {
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
if (callbacks == null) {
callbacks = PropertyChangeRegistry()
}
@ -145,7 +129,7 @@ abstract class BaseViewModel(
}
@Synchronized
override fun removeOnPropertyChangedCallback(callback: BindingObservable.OnPropertyChangedCallback) {
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks?.remove(callback)
}
@ -168,63 +152,4 @@ abstract class BaseViewModel(
callbacks?.notifyCallbacks(this, fieldId, null)
}
//region Rx
protected fun <T> Observable<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnNext { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun <T> Single<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnSuccess { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun <T> Maybe<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnComplete { if (allowFinishing) viewModel.state =
State.LOADED
}
.doOnSuccess { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun <T> Flowable<T>.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnNext { if (allowFinishing) viewModel.state =
State.LOADED
}
protected fun Completable.applyViewModel(viewModel: BaseViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state =
State.LOADING
}
.doOnError { viewModel.state =
State.LOADING_FAILED
}
.doOnComplete { if (allowFinishing) viewModel.state =
State.LOADED
}
//endregion
}

View File

@ -1,6 +1,5 @@
package com.topjohnwu.magisk.ui.flash
import android.Manifest
import android.content.res.Resources
import android.net.Uri
import android.view.MenuItem
@ -21,7 +20,9 @@ import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
@ -96,25 +97,23 @@ class FlashViewModel(
return true
}
private fun savePressed() = withPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
.map { now }
.map { it.toTime(timeFormatStandard) }
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
.map { File(Config.downloadDirectory, it) }
.map { file ->
file.bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
private fun savePressed() = withExternalRW {
if (!it)
return@withExternalRW
viewModelScope.launch {
val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
val file = File(Config.downloadDirectory, name)
withContext(Dispatchers.IO) {
file.bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
}
file.path
SnackbarEvent(file.path).publish()
}
.subscribeK { SnackbarEvent(it).publish() }
.add()
}
fun restartPressed() = reboot()
}

View File

@ -3,10 +3,10 @@ package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import androidx.databinding.Bindable
import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
@ -18,6 +18,9 @@ import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.Queryable
import com.topjohnwu.magisk.ui.base.filterableListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HideViewModel(
private val magiskRepo: MagiskRepository
@ -50,19 +53,18 @@ class HideViewModel(
val isFilterExpanded = ObservableField(false)
override fun rxRefresh() = magiskRepo.fetchApps()
.map { it to magiskRepo.fetchHideTargets().blockingGet() }
.map { pair -> pair.first.map { mergeAppTargets(it, pair.second) } }
.flattenAsFlowable { it }
.map { HideItem(it) }
.toList()
.map { it.sort() }
.map { it to items.calculateDiff(it) }
.applyViewModel(this)
.subscribeK {
items.update(it.first, it.second)
submitQuery()
override fun refresh() = viewModelScope.launch {
state = State.LOADING
val apps = magiskRepo.fetchApps()
val hides = magiskRepo.fetchHideTargets()
val (hidden, diff) = withContext(Dispatchers.Default) {
val hidden = apps.map { mergeAppTargets(it, hides) }.map { HideItem(it) }.sort()
hidden to items.calculateDiff(hidden)
}
items.update(hidden, diff)
submitQuery()
state = State.LOADED
}
// ---

View File

@ -1,8 +1,8 @@
package com.topjohnwu.magisk.ui.home
import android.Manifest
import android.os.Build
import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
@ -11,9 +11,11 @@ import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.extensions.await
import com.topjohnwu.magisk.extensions.packageName
import com.topjohnwu.magisk.extensions.res
import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Manager
import com.topjohnwu.magisk.model.entity.recycler.DeveloperItem
import com.topjohnwu.magisk.model.entity.recycler.HomeItem
@ -26,6 +28,7 @@ import com.topjohnwu.magisk.model.events.dialog.UninstallDialog
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
import me.tatarka.bindingcollectionadapter2.BR
import kotlin.math.roundToInt
@ -72,27 +75,29 @@ class HomeViewModel(
}
}
override fun rxRefresh() = repoMagisk.fetchUpdate()
.onErrorReturn { null }
.subscribeK { it?.updateUI() }
override fun refresh() = viewModelScope.launch {
repoMagisk.fetchUpdate()?.apply {
stateMagisk.value = when {
!Info.env.isActive -> MagiskState.NOT_INSTALLED
magisk.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
private fun UpdateInfo.updateUI() {
stateMagisk.value = when {
!Info.env.isActive -> MagiskState.NOT_INSTALLED
magisk.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
stateManager.value = when {
!app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED
app.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
stateMagiskRemoteVersion.value =
"${magisk.version} (${magisk.versionCode})"
stateManagerRemoteVersion.value =
"${app.version} (${app.versionCode}) (${stub.versionCode})"
launch {
ensureEnv()
}
}
stateManager.value = when {
!app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED
app.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
stateMagiskRemoteVersion.value = "${magisk.version} (${magisk.versionCode})"
stateManagerRemoteVersion.value = "${app.version} (${app.versionCode}) (${stub.versionCode})"
ensureEnv()
}
val showTest = false
@ -109,19 +114,18 @@ class HomeViewModel(
fun onManagerPressed() = ManagerInstallDialog().publish()
fun onMagiskPressed() = withPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
).map { check(it);it }
.subscribeK { HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish() }
.add()
fun onMagiskPressed() = withExternalRW {
if (it) {
HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish()
}
}
fun hideNotice() {
Config.safetyNotice = false
isNoticeVisible.value = false
}
private fun ensureEnv() {
private suspend fun ensureEnv() {
val invalidStates = listOf(
MagiskState.NOT_INSTALLED,
MagiskState.LOADING
@ -138,21 +142,18 @@ class HomeViewModel(
return
}
Shell.su("env_check")
.toSingle()
.map { it.exec() }
.filter { !it.isSuccess }
.subscribeK {
shownDialog = true
EnvFixDialog().publish()
}
val result = Shell.su("env_check").await()
if (!result.isSuccess) {
shownDialog = true
EnvFixDialog().publish()
}
}
private val MagiskJson.isObsolete
get() = Info.env.isActive && Info.env.magiskVersionCode < versionCode
val ManagerJson.isUpdateChannelCorrect
private val ManagerJson.isUpdateChannelCorrect
get() = versionCode > 0
val ManagerJson.isObsolete
private val ManagerJson.isObsolete
get() = BuildConfig.VERSION_CODE < versionCode
}

View File

@ -1,12 +1,12 @@
package com.topjohnwu.magisk.ui.log
import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.entity.recycler.LogItem
import com.topjohnwu.magisk.model.entity.recycler.TextItem
@ -15,12 +15,10 @@ import com.topjohnwu.magisk.ui.base.BaseViewModel
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.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.*
class LogViewModel(
@ -43,28 +41,21 @@ class LogViewModel(
val consoleText = ObservableField(" ")
override fun rxRefresh(): Disposable {
val logs = repo.fetchLogs()
.map { it.map { LogItem(it) } }
.observeOn(Schedulers.computation())
.map { it to items.calculateDiff(it) }
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess {
items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false
items.update(it.first, it.second)
items.firstOrNull()?.isTop = true
items.lastOrNull()?.isBottom = true
override fun refresh() = viewModelScope.launch {
consoleText.value = repo.fetchMagiskLogs()
val deferred = withContext(Dispatchers.Default) {
async {
val suLogs = repo.fetchSuLogs().map { LogItem(it) }
suLogs to items.calculateDiff(suLogs)
}
.ignoreElement()
val console = repo.fetchMagiskLogs()
.doOnSuccess { consoleText.value = it }
.ignoreElement()
return Completable.merge(listOf(logs, console)).subscribeK()
}
delay(500)
val (suLogs, diff) = deferred.await()
items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false
items.update(suLogs, diff)
items.firstOrNull()?.isTop = true
items.lastOrNull()?.isBottom = true
}
fun saveMagiskLog() {
@ -76,10 +67,10 @@ class LogViewModel(
)
val logFile = File(Config.downloadDirectory, filename)
runCatching {
try {
logFile.createNewFile()
}.onFailure {
Timber.e(it)
} catch (e: IOException) {
Timber.e(e)
return
}
@ -88,18 +79,14 @@ class LogViewModel(
}
}
fun clearMagiskLog() = repo.clearMagiskLogs()
.subscribeK {
SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
.add()
fun clearLog() = repo.clearLogs()
.subscribeK {
SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
.add()
fun clearMagiskLog() = repo.clearMagiskLogs {
SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
fun clearLog() = viewModelScope.launch {
repo.clearLogs()
SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
}

View File

@ -47,7 +47,7 @@ class ModuleViewModel(
private val repoName: RepoByNameDao,
private val repoUpdated: RepoByUpdatedDao,
private val repoUpdater: RepoUpdater
) : BaseViewModel(useRx = false), Queryable {
) : BaseViewModel(), Queryable {
override val queryDelay = 1000L
private var queryJob: Job? = null

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk.ui.superuser
import android.content.pm.PackageManager
import android.content.res.Resources
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
@ -10,8 +11,6 @@ import com.topjohnwu.magisk.core.model.MagiskPolicy
import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.applySchedulers
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.recycler.PolicyItem
import com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem
@ -24,7 +23,9 @@ 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 io.reactivex.Single
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
class SuperuserViewModel(
@ -53,27 +54,23 @@ class SuperuserViewModel(
// ---
override fun rxRefresh() = db.fetchAll()
.flattenAsFlowable { it }
.parallel()
.map { PolicyItem(it, it.applicationInfo.loadIcon(packageManager)) }
.sequential()
.sorted { o1, o2 ->
compareBy<PolicyItem>(
override fun refresh() = viewModelScope.launch {
state = State.LOADING
val (policies, diff) = withContext(Dispatchers.Default) {
val policies = db.fetchAll {
PolicyItem(it, it.applicationInfo.loadIcon(packageManager))
}.sortedWith(compareBy(
{ it.item.appName.toLowerCase(currentLocale) },
{ it.item.packageName }
).compare(o1, o2)
))
policies to itemsPolicies.calculateDiff(policies)
}
.toList()
.map { it to itemsPolicies.calculateDiff(it) }
.applySchedulers()
.applyViewModel(this)
.subscribeK {
itemsPolicies.update(it.first, it.second)
if (itemsPolicies.isNotEmpty()) {
itemsHelpers.remove(itemNoData)
}
itemsPolicies.update(policies, diff)
if (itemsPolicies.isNotEmpty()) {
itemsHelpers.remove(itemNoData)
}
state = State.LOADED
}
// ---
@ -91,14 +88,13 @@ class SuperuserViewModel(
SuperuserFragmentDirections.actionSuperuserFragmentToHideFragment().publish()
fun deletePressed(item: PolicyItem) {
fun updateState() = deletePolicy(item.item)
.subscribeK {
itemsPolicies.removeAll { it.genericItemSameAs(item) }
if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) {
itemsHelpers.add(itemNoData)
}
fun updateState() = viewModelScope.launch {
db.delete(item.item.uid)
itemsPolicies.removeAll { it.genericItemSameAs(item) }
if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) {
itemsHelpers.add(itemNoData)
}
.add()
}
if (BiometricHelper.isEnabled) {
BiometricDialog {
@ -114,34 +110,37 @@ class SuperuserViewModel(
//---
fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
is PolicyUpdateEvent.Notification -> updatePolicy(it.item).map {
when {
it.notification -> R.string.su_snack_notif_on
else -> R.string.su_snack_notif_off
} to it.appName
fun updatePolicy(it: PolicyUpdateEvent) = viewModelScope.launch {
val snackStr = when (it) {
is PolicyUpdateEvent.Notification -> {
updatePolicy(it.item)
when {
it.item.notification -> R.string.su_snack_notif_on
else -> R.string.su_snack_notif_off
}
}
is PolicyUpdateEvent.Log -> {
updatePolicy(it.item)
when {
it.item.logging -> R.string.su_snack_log_on
else -> R.string.su_snack_log_off
}
}
}
is PolicyUpdateEvent.Log -> updatePolicy(it.item).map {
when {
it.logging -> R.string.su_snack_log_on
else -> R.string.su_snack_log_off
} to it.appName
}
}.map { resources.getString(it.first, it.second) }
.subscribeK { SnackbarEvent(it).publish() }
.add()
SnackbarEvent(resources.getString(snackStr, it.item.appName)).publish()
}
fun togglePolicy(item: PolicyItem, enable: Boolean) {
fun updateState() {
val policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY
val app = item.item.copy(policy = policy)
updatePolicy(app)
.map { it.policy == MagiskPolicy.ALLOW }
.map { if (it) R.string.su_snack_grant else R.string.su_snack_deny }
.map { resources.getString(it).format(item.item.appName) }
.subscribeK { SnackbarEvent(it).publish() }
.add()
viewModelScope.launch {
updatePolicy(app)
val res = if (app.policy == MagiskPolicy.ALLOW) R.string.su_snack_grant
else R.string.su_snack_deny
SnackbarEvent(resources.getString(res).format(item.item.appName))
}
}
if (BiometricHelper.isEnabled) {
@ -156,10 +155,6 @@ class SuperuserViewModel(
//---
private fun updatePolicy(policy: MagiskPolicy) =
db.update(policy).andThen(Single.just(policy))
private fun deletePolicy(policy: MagiskPolicy) =
db.delete(policy.uid).andThen(Single.just(policy))
private suspend fun updatePolicy(policy: MagiskPolicy) = db.update(policy)
}

View File

@ -60,7 +60,7 @@ subprojects {
plugins.hasPlugin("com.android.application")) {
android.apply {
compileSdkVersion(30)
buildToolsVersion = "30.0.0"
buildToolsVersion = "30.0.1"
defaultConfig {
if (minSdkVersion == null)