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" val vRoom = "2.2.5"
implementation("androidx.room:room-runtime:${vRoom}") implementation("androidx.room:room-runtime:${vRoom}")
implementation("androidx.room:room-rxjava2:${vRoom}") implementation("androidx.room:room-ktx:${vRoom}")
kapt("androidx.room:room-compiler:${vRoom}") kapt("androidx.room:room-compiler:${vRoom}")
implementation("androidx.navigation:navigation-fragment-ktx:${Deps.vNav}") implementation("androidx.navigation:navigation-fragment-ktx:${Deps.vNav}")
@ -139,9 +139,10 @@ dependencies {
implementation("androidx.preference:preference:1.1.1") implementation("androidx.preference:preference:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.1.0") implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.2.5") 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.transition:transition:1.3.1")
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.core:core-ktx:1.3.0") 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") 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.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.core.inject import org.koin.core.inject
open class GeneralReceiver : BaseReceiver() { open class GeneralReceiver : BaseReceiver() {
@ -25,6 +27,10 @@ open class GeneralReceiver : BaseReceiver() {
override fun onReceive(context: ContextWrapper, intent: Intent?) { override fun onReceive(context: ContextWrapper, intent: Intent?) {
intent ?: return intent ?: return
fun rmPolicy(pkg: String) = GlobalScope.launch {
policyDB.delete(pkg)
}
when (intent.action ?: return) { when (intent.action ?: return) {
Intent.ACTION_REBOOT -> { Intent.ACTION_REBOOT -> {
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras) SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
@ -32,11 +38,11 @@ open class GeneralReceiver : BaseReceiver() {
Intent.ACTION_PACKAGE_REPLACED -> { Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O // This will only work pre-O
if (Config.suReAuth) if (Config.suReAuth)
policyDB.delete(getPkg(intent)).blockingGet() rmPolicy(getPkg(intent))
} }
Intent.ACTION_PACKAGE_FULLY_REMOVED -> { Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent) val pkg = getPkg(intent)
policyDB.delete(pkg).blockingGet() rmPolicy(pkg)
Shell.su("magiskhide --rm $pkg").submit() Shell.su("magiskhide --rm $pkg").submit()
} }
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context) Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)

View File

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

View File

@ -1,8 +1,6 @@
package com.topjohnwu.magisk.core.magiskdb package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef import androidx.annotation.StringDef
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
abstract class BaseDao { abstract class BaseDao {
@ -20,25 +18,11 @@ abstract class BaseDao {
@TableStrict @TableStrict
abstract val table: String 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() Builder::class.java.newInstance()
.apply { table = this@BaseDao.table } .apply { table = this@BaseDao.table }
.apply(builder) .apply(builder)
.toString() .toString()
.let { Query(it) } .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.toMap
import com.topjohnwu.magisk.core.model.toPolicy import com.topjohnwu.magisk.core.model.toPolicy
import com.topjohnwu.magisk.extensions.now import com.topjohnwu.magisk.extensions.now
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -17,55 +19,56 @@ class PolicyDao(
override val table: String = Table.POLICY override val table: String = Table.POLICY
fun deleteOutdated( suspend fun deleteOutdated() = buildQuery<Delete> {
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
) = query<Delete> {
condition { condition {
greaterThan("until", "0") greaterThan("until", "0")
and { and {
lessThan("until", nowSeconds.toString()) lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
} }
or { or {
lessThan("until", "0") lessThan("until", "0")
} }
} }
}.ignoreElement() }.commit()
fun delete(packageName: String) = query<Delete> { suspend fun delete(packageName: String) = buildQuery<Delete> {
condition { condition {
equals("package_name", packageName) equals("package_name", packageName)
} }
}.ignoreElement() }.commit()
fun delete(uid: Int) = query<Delete> { suspend fun delete(uid: Int) = buildQuery<Delete> {
condition { condition {
equals("uid", uid) equals("uid", uid)
} }
}.ignoreElement() }.commit()
fun fetch(uid: Int) = query<Select> { suspend fun fetch(uid: Int) = buildQuery<Select> {
condition { condition {
equals("uid", uid) 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()) values(policy.toMap())
}.ignoreElement() }.commit()
fun fetchAll() = query<Select> { suspend fun <R: Any> fetchAll(mapper: (MagiskPolicy) -> R) = buildQuery<Select> {
condition { condition {
equals("uid/100000", Const.USER_ID) equals("uid/100000", Const.USER_ID)
} }
}.map { it.mapNotNull { it.toPolicySafe() } } }.query {
it.toPolicyOrNull()?.let(mapper)
}
private fun Map<String, String>.toPolicyOrNull(): MagiskPolicy? {
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
return runCatching { toPolicy(context.packageManager) }.getOrElse { return runCatching { toPolicy(context.packageManager) }.getOrElse {
Timber.e(it) Timber.e(it)
if (it is PackageManager.NameNotFoundException) { if (it is PackageManager.NameNotFoundException) {
val uid = getOrElse("uid") { null } ?: return null val uid = getOrElse("uid") { null } ?: return null
delete(uid).subscribe() GlobalScope.launch {
delete(uid.toInt())
}
} }
null null
} }

View File

@ -1,6 +1,12 @@
package com.topjohnwu.magisk.core.magiskdb package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef 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) { class Query(private val _query: String) {
val query get() = "magisk --sqlite '$_query'" val query get() = "magisk --sqlite '$_query'"
@ -9,6 +15,24 @@ class Query(private val _query: String) {
val requestType: String val requestType: String
var table: 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 { class Delete : Query.Builder {

View File

@ -4,17 +4,19 @@ class SettingsDao : BaseDao() {
override val table = Table.SETTINGS override val table = Table.SETTINGS
fun delete(key: String) = query<Delete> { suspend fun delete(key: String) = buildQuery<Delete> {
condition { equals("key", key) } 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) 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") fields("value")
condition { equals("key", key) } 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 override val table = Table.STRINGS
fun delete(key: String) = query<Delete> { suspend fun delete(key: String) = buildQuery<Delete> {
condition { equals("key", key) } 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) 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") fields("value")
condition { equals("key", key) } 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.get
import com.topjohnwu.magisk.extensions.startActivity import com.topjohnwu.magisk.extensions.startActivity
import com.topjohnwu.magisk.extensions.startActivityWithRoot import com.topjohnwu.magisk.extensions.startActivityWithRoot
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.toLog import com.topjohnwu.magisk.model.entity.toLog
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
object SuCallbackHandler : ProviderCallHandler { object SuCallbackHandler : ProviderCallHandler {
@ -111,7 +112,9 @@ object SuCallbackHandler : ProviderCallHandler {
) )
val logRepo = get<LogRepository>() val logRepo = get<LogRepository>()
logRepo.insert(log).subscribeK(onError = { Timber.e(it) }) GlobalScope.launch {
logRepo.insert(log)
}
} }
private fun handleNotify(context: Context, data: Bundle) { private fun handleNotify(context: Context, data: Bundle) {

View File

@ -111,13 +111,13 @@ abstract class SuRequestHandler(
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} finally { } finally {
if (until >= 0)
policyDB.update(policy).blockingAwait()
runCatching { runCatching {
input.close() input.close()
output.close() output.close()
socket.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 androidx.room.*
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
import io.reactivex.Completable import kotlinx.coroutines.Dispatchers
import io.reactivex.Single import kotlinx.coroutines.withContext
import java.util.* import java.util.*
@Database(version = 1, entities = [MagiskLog::class], exportSchema = false) @Database(version = 1, entities = [MagiskLog::class], exportSchema = false)
@ -18,19 +18,20 @@ abstract class SuLogDao(private val db: SuLogDatabase) {
private val twoWeeksAgo = private val twoWeeksAgo =
Calendar.getInstance().apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis 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") @Query("SELECT * FROM logs ORDER BY time DESC")
protected abstract fun fetch(): Single<MutableList<MagiskLog>> protected abstract suspend fun fetch(): MutableList<MagiskLog>
@Insert
abstract fun insert(log: MagiskLog): Completable
@Query("DELETE FROM logs WHERE time < :timeout") @Query("DELETE FROM logs WHERE time < :timeout")
protected abstract fun deleteOutdated( protected abstract suspend fun deleteOutdated(timeout: Long = twoWeeksAgo)
timeout: Long = twoWeeksAgo
): Completable @Insert
abstract suspend fun insert(log: MagiskLog)
} }

View File

@ -13,16 +13,16 @@ interface GithubRawServices {
//region topjohnwu/magisk_files //region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json") @GET("$MAGISK_FILES/master/stable.json")
fun fetchStableUpdate(): Single<UpdateInfo> suspend fun fetchStableUpdate(): UpdateInfo
@GET("$MAGISK_FILES/master/beta.json") @GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaUpdate(): Single<UpdateInfo> suspend fun fetchBetaUpdate(): UpdateInfo
@GET("$MAGISK_FILES/canary/debug.json") @GET("$MAGISK_FILES/canary/debug.json")
fun fetchCanaryUpdate(): Single<UpdateInfo> suspend fun fetchCanaryUpdate(): UpdateInfo
@GET @GET
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo> suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
@GET("$MAGISK_FILES/{$REVISION}/snet.jar") @GET("$MAGISK_FILES/{$REVISION}/snet.jar")
@Streaming @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.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao 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.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -38,17 +40,19 @@ class DBSettingsValue(
@Synchronized @Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int { override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
if (value == null) if (value == null)
value = thisRef.settingsDao.fetch(name, default).blockingGet() value = runBlocking {
return value!! thisRef.settingsDao.fetch(name, default)
}
return value as Int
} }
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) { override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
synchronized(this) { synchronized(this) {
this.value = value this.value = value
} }
thisRef.settingsDao.put(name, value) GlobalScope.launch {
.subscribeOn(Schedulers.io()) thisRef.settingsDao.put(name, value)
.subscribe() }
} }
} }
@ -77,7 +81,9 @@ class DBStringsValue(
@Synchronized @Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String { override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
if (value == null) if (value == null)
value = thisRef.stringDao.fetch(name, default).blockingGet() value = runBlocking {
thisRef.stringDao.fetch(name, default)
}
return value!! return value!!
} }
@ -87,19 +93,23 @@ class DBStringsValue(
} }
if (value.isEmpty()) { if (value.isEmpty()) {
if (sync) { if (sync) {
thisRef.stringDao.delete(name).blockingAwait() runBlocking {
thisRef.stringDao.delete(name)
}
} else { } else {
thisRef.stringDao.delete(name) GlobalScope.launch {
.subscribeOn(Schedulers.io()) thisRef.stringDao.delete(name)
.subscribe() }
} }
} else { } else {
if (sync) { if (sync) {
thisRef.stringDao.put(name, value).blockingAwait() runBlocking {
thisRef.stringDao.put(name, value)
}
} else { } else {
thisRef.stringDao.put(name, value) GlobalScope.launch {
.subscribeOn(Schedulers.io()) thisRef.stringDao.put(name, value)
.subscribe() }
} }
} }
} }

View File

@ -2,19 +2,18 @@ package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.database.SuLogDao import com.topjohnwu.magisk.data.database.SuLogDao
import com.topjohnwu.magisk.extensions.await
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
import io.reactivex.Single
class LogRepository( class LogRepository(
private val logDao: SuLogDao 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 list = object : AbstractMutableList<String>() {
val buf = StringBuilder() val buf = StringBuilder()
override val size get() = 0 override val size get() = 0
@ -28,16 +27,15 @@ class LogRepository(
} }
} }
} }
Shell.su("cat ${Const.MAGISK_LOG}").to(list).exec() Shell.su("cat ${Const.MAGISK_LOG}").to(list).await()
list.buf.toString() return list.buf.toString()
} }
fun clearLogs() = logDao.deleteAll() suspend fun clearLogs() = logDao.deleteAll()
fun clearMagiskLogs() = Completable.fromAction { fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
Shell.su("echo -n > ${Const.MAGISK_LOG}").exec() 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.Config
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.await
import com.topjohnwu.magisk.extensions.getLabel import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.extensions.packageName 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.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.superuser.Shell 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( class MagiskRepository(
private val apiRaw: GithubRawServices, private val apiRaw: GithubRawServices,
private val packageManager: PackageManager private val packageManager: PackageManager
) { ) {
fun fetchSafetynet() = apiRaw.fetchSafetynet() fun fetchSafetynet() = apiRaw.fetchSafetynet()
fun fetchUpdate() = when (Config.updateChannel) { suspend fun fetchUpdate() = try {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate() var info = when (Config.updateChannel) {
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate() Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate() Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl) Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
else -> throw IllegalArgumentException() Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
}.flatMap { else -> throw IllegalArgumentException()
// 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)
} }
}.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() = suspend fun fetchApps() = withContext(Dispatchers.Default) {
Single.fromCallable { packageManager.getInstalledApplications(0) } packageManager.getInstalledApplications(0).filter {
.flattenAsFlowable { it } it.enabled && !blacklist.contains(it.packageName)
.filter { it.enabled && !blacklist.contains(it.packageName) } }.map {
.map { val label = it.getLabel(packageManager)
val label = it.getLabel(packageManager) val icon = it.loadIcon(packageManager)
val icon = it.loadIcon(packageManager) HideAppInfo(it, label, icon)
HideAppInfo(it, label, icon) }.filter { it.processes.isNotEmpty() }
} }
.filter { it.processes.isNotEmpty() }
.toList()
fun fetchHideTargets() = Shell.su("magiskhide --ls").toSingle() suspend fun fetchHideTargets() =
.map { it.exec().out } Shell.su("\"magiskhide --ls\"").await().out.map {
.flattenAsFlowable { it } HideTarget(it)
.map { HideTarget(it) } }
.toList()
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) = fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
Shell.su("magiskhide --${isEnabled.state} $packageName $process").submit() 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.content.Context
import android.os.Build import android.os.Build
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.topjohnwu.magisk.core.ResMgr import com.topjohnwu.magisk.core.ResMgr
import com.topjohnwu.magisk.utils.RxBus import com.topjohnwu.magisk.utils.RxBus
@ -19,7 +18,6 @@ val applicationModule = module {
factory(Protected) { createDEContext(get()) } factory(Protected) { createDEContext(get()) }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) } single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) } single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
single { LocalBroadcastManager.getInstance(get()) }
} }
private fun createDEContext(context: Context): Context { 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.Shell
import com.topjohnwu.superuser.io.SuFileInputStream import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream import com.topjohnwu.superuser.io.SuFileOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
fun reboot(reason: String = if (Info.recovery) "recovery" else "") { fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
@ -14,3 +16,5 @@ fun File.suOutputStream() = SuFileOutputStream(this)
fun File.suInputStream() = SuFileInputStream(this) fun File.suInputStream() = SuFileInputStream(this)
val hasRoot get() = Shell.rootAccess() 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) { class HideTarget(line: String) {
private val split = line.split(Regex("\\|"), 2) val packageName: String
val process: String
val packageName = split[0] init {
val process = split.getOrElse(1) { packageName } 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.annotation.CallSuper
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.ObservableField import androidx.databinding.ObservableField
import androidx.databinding.PropertyChangeRegistry import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -13,23 +14,18 @@ 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
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.extensions.value import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.events.* import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.navigation.NavigationWrapper import com.topjohnwu.magisk.model.navigation.NavigationWrapper
import com.topjohnwu.magisk.model.observer.Observer import com.topjohnwu.magisk.model.observer.Observer
import io.reactivex.*
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import androidx.databinding.Observable as BindingObservable
abstract class BaseViewModel( abstract class BaseViewModel(
initialState: State = State.LOADING, initialState: State = State.LOADING
val useRx: Boolean = true ) : ViewModel(), Observable, KoinComponent {
) : ViewModel(), BindingObservable, KoinComponent {
enum class State { enum class State {
LOADED, LOADING, LOADING_FAILED LOADED, LOADING, LOADING_FAILED
@ -53,8 +49,8 @@ abstract class BaseViewModel(
private val _viewEvents = MutableLiveData<ViewEvent>() private val _viewEvents = MutableLiveData<ViewEvent>()
private var runningTask: Disposable? = null private var runningTask: Disposable? = null
private var runningJob: Job? = null private var runningJob: Job? = null
private val refreshCallback = object : androidx.databinding.Observable.OnPropertyChangedCallback() { private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: androidx.databinding.Observable?, propertyId: Int) { override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
requestRefresh() requestRefresh()
} }
} }
@ -66,13 +62,6 @@ abstract class BaseViewModel(
/** This should probably never be called manually, it's called manually via delegate. */ /** This should probably never be called manually, it's called manually via delegate. */
@Synchronized @Synchronized
fun requestRefresh() { fun requestRefresh() {
if (useRx) {
if (runningTask?.isDisposed?.not() == true) {
return
}
runningTask = rxRefresh()
return
}
if (runningJob?.isActive == true) { if (runningJob?.isActive == true) {
return return
} }
@ -100,11 +89,6 @@ abstract class BaseViewModel(
ViewActionEvent(action).publish() 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) { fun withPermissions(vararg permissions: String, callback: (Boolean) -> Unit) {
PermissionEvent(permissions.toList(), callback).publish() PermissionEvent(permissions.toList(), callback).publish()
} }
@ -137,7 +121,7 @@ abstract class BaseViewModel(
private var callbacks: PropertyChangeRegistry? = null private var callbacks: PropertyChangeRegistry? = null
@Synchronized @Synchronized
override fun addOnPropertyChangedCallback(callback: BindingObservable.OnPropertyChangedCallback) { override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
if (callbacks == null) { if (callbacks == null) {
callbacks = PropertyChangeRegistry() callbacks = PropertyChangeRegistry()
} }
@ -145,7 +129,7 @@ abstract class BaseViewModel(
} }
@Synchronized @Synchronized
override fun removeOnPropertyChangedCallback(callback: BindingObservable.OnPropertyChangedCallback) { override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks?.remove(callback) callbacks?.remove(callback)
} }
@ -168,63 +152,4 @@ abstract class BaseViewModel(
callbacks?.notifyCallbacks(this, fieldId, null) 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 package com.topjohnwu.magisk.ui.flash
import android.Manifest
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.view.MenuItem 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.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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -96,25 +97,23 @@ class FlashViewModel(
return true return true
} }
private fun savePressed() = withPermissions( private fun savePressed() = withExternalRW {
Manifest.permission.READ_EXTERNAL_STORAGE, if (!it)
Manifest.permission.WRITE_EXTERNAL_STORAGE return@withExternalRW
) viewModelScope.launch {
.map { now } val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
.map { it.toTime(timeFormatStandard) } val file = File(Config.downloadDirectory, name)
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) } withContext(Dispatchers.IO) {
.map { File(Config.downloadDirectory, it) } file.bufferedWriter().use { writer ->
.map { file -> logItems.forEach {
file.bufferedWriter().use { writer -> writer.write(it)
logItems.forEach { writer.newLine()
writer.write(it) }
writer.newLine()
} }
} }
file.path SnackbarEvent(file.path).publish()
} }
.subscribeK { SnackbarEvent(it).publish() } }
.add()
fun restartPressed() = reboot() fun restartPressed() = reboot()
} }

View File

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

View File

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

View File

@ -1,12 +1,12 @@
package com.topjohnwu.magisk.ui.log package com.topjohnwu.magisk.ui.log
import androidx.databinding.ObservableField import androidx.databinding.ObservableField
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.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.repository.LogRepository import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.value import com.topjohnwu.magisk.extensions.value
import com.topjohnwu.magisk.model.entity.recycler.LogItem import com.topjohnwu.magisk.model.entity.recycler.LogItem
import com.topjohnwu.magisk.model.entity.recycler.TextItem 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.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.*
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException
import java.util.* import java.util.*
class LogViewModel( class LogViewModel(
@ -43,28 +41,21 @@ class LogViewModel(
val consoleText = ObservableField(" ") val consoleText = ObservableField(" ")
override fun rxRefresh(): Disposable { override fun refresh() = viewModelScope.launch {
val logs = repo.fetchLogs() consoleText.value = repo.fetchMagiskLogs()
.map { it.map { LogItem(it) } } val deferred = withContext(Dispatchers.Default) {
.observeOn(Schedulers.computation()) async {
.map { it to items.calculateDiff(it) } val suLogs = repo.fetchSuLogs().map { LogItem(it) }
.observeOn(AndroidSchedulers.mainThread()) suLogs to items.calculateDiff(suLogs)
.doOnSuccess {
items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false
items.update(it.first, it.second)
items.firstOrNull()?.isTop = true
items.lastOrNull()?.isBottom = true
} }
.ignoreElement() }
delay(500)
val console = repo.fetchMagiskLogs() val (suLogs, diff) = deferred.await()
.doOnSuccess { consoleText.value = it } items.firstOrNull()?.isTop = false
.ignoreElement() items.lastOrNull()?.isBottom = false
items.update(suLogs, diff)
return Completable.merge(listOf(logs, console)).subscribeK() items.firstOrNull()?.isTop = true
items.lastOrNull()?.isBottom = true
} }
fun saveMagiskLog() { fun saveMagiskLog() {
@ -76,10 +67,10 @@ class LogViewModel(
) )
val logFile = File(Config.downloadDirectory, filename) val logFile = File(Config.downloadDirectory, filename)
runCatching { try {
logFile.createNewFile() logFile.createNewFile()
}.onFailure { } catch (e: IOException) {
Timber.e(it) Timber.e(e)
return return
} }
@ -88,18 +79,14 @@ class LogViewModel(
} }
} }
fun clearMagiskLog() = repo.clearMagiskLogs() fun clearMagiskLog() = repo.clearMagiskLogs {
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish()
SnackbarEvent(R.string.logs_cleared).publish() requestRefresh()
requestRefresh() }
}
.add()
fun clearLog() = repo.clearLogs()
.subscribeK {
SnackbarEvent(R.string.logs_cleared).publish()
requestRefresh()
}
.add()
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 repoName: RepoByNameDao,
private val repoUpdated: RepoByUpdatedDao, private val repoUpdated: RepoByUpdatedDao,
private val repoUpdater: RepoUpdater private val repoUpdater: RepoUpdater
) : BaseViewModel(useRx = false), Queryable { ) : BaseViewModel(), Queryable {
override val queryDelay = 1000L override val queryDelay = 1000L
private var queryJob: Job? = null 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.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import androidx.databinding.ObservableArrayList import androidx.databinding.ObservableArrayList
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.magiskdb.PolicyDao 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.BiometricHelper
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.databinding.ComparableRvItem 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.extensions.toggle
import com.topjohnwu.magisk.model.entity.recycler.PolicyItem import com.topjohnwu.magisk.model.entity.recycler.PolicyItem
import com.topjohnwu.magisk.model.entity.recycler.TappableHeadlineItem 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.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 io.reactivex.Single import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
class SuperuserViewModel( class SuperuserViewModel(
@ -53,27 +54,23 @@ class SuperuserViewModel(
// --- // ---
override fun rxRefresh() = db.fetchAll() override fun refresh() = viewModelScope.launch {
.flattenAsFlowable { it } state = State.LOADING
.parallel() val (policies, diff) = withContext(Dispatchers.Default) {
.map { PolicyItem(it, it.applicationInfo.loadIcon(packageManager)) } val policies = db.fetchAll {
.sequential() PolicyItem(it, it.applicationInfo.loadIcon(packageManager))
.sorted { o1, o2 -> }.sortedWith(compareBy(
compareBy<PolicyItem>(
{ it.item.appName.toLowerCase(currentLocale) }, { it.item.appName.toLowerCase(currentLocale) },
{ it.item.packageName } { it.item.packageName }
).compare(o1, o2) ))
policies to itemsPolicies.calculateDiff(policies)
} }
.toList() itemsPolicies.update(policies, diff)
.map { it to itemsPolicies.calculateDiff(it) } if (itemsPolicies.isNotEmpty()) {
.applySchedulers() itemsHelpers.remove(itemNoData)
.applyViewModel(this)
.subscribeK {
itemsPolicies.update(it.first, it.second)
if (itemsPolicies.isNotEmpty()) {
itemsHelpers.remove(itemNoData)
}
} }
state = State.LOADED
}
// --- // ---
@ -91,14 +88,13 @@ class SuperuserViewModel(
SuperuserFragmentDirections.actionSuperuserFragmentToHideFragment().publish() SuperuserFragmentDirections.actionSuperuserFragmentToHideFragment().publish()
fun deletePressed(item: PolicyItem) { fun deletePressed(item: PolicyItem) {
fun updateState() = deletePolicy(item.item) fun updateState() = viewModelScope.launch {
.subscribeK { db.delete(item.item.uid)
itemsPolicies.removeAll { it.genericItemSameAs(item) } itemsPolicies.removeAll { it.genericItemSameAs(item) }
if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) { if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) {
itemsHelpers.add(itemNoData) itemsHelpers.add(itemNoData)
}
} }
.add() }
if (BiometricHelper.isEnabled) { if (BiometricHelper.isEnabled) {
BiometricDialog { BiometricDialog {
@ -114,34 +110,37 @@ class SuperuserViewModel(
//--- //---
fun updatePolicy(it: PolicyUpdateEvent) = when (it) { fun updatePolicy(it: PolicyUpdateEvent) = viewModelScope.launch {
is PolicyUpdateEvent.Notification -> updatePolicy(it.item).map { val snackStr = when (it) {
when { is PolicyUpdateEvent.Notification -> {
it.notification -> R.string.su_snack_notif_on updatePolicy(it.item)
else -> R.string.su_snack_notif_off when {
} to it.appName 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 { SnackbarEvent(resources.getString(snackStr, it.item.appName)).publish()
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()
fun togglePolicy(item: PolicyItem, enable: Boolean) { fun togglePolicy(item: PolicyItem, enable: Boolean) {
fun updateState() { fun updateState() {
val policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY val policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY
val app = item.item.copy(policy = policy) val app = item.item.copy(policy = policy)
updatePolicy(app) viewModelScope.launch {
.map { it.policy == MagiskPolicy.ALLOW } updatePolicy(app)
.map { if (it) R.string.su_snack_grant else R.string.su_snack_deny } val res = if (app.policy == MagiskPolicy.ALLOW) R.string.su_snack_grant
.map { resources.getString(it).format(item.item.appName) } else R.string.su_snack_deny
.subscribeK { SnackbarEvent(it).publish() } SnackbarEvent(resources.getString(res).format(item.item.appName))
.add() }
} }
if (BiometricHelper.isEnabled) { if (BiometricHelper.isEnabled) {
@ -156,10 +155,6 @@ class SuperuserViewModel(
//--- //---
private fun updatePolicy(policy: MagiskPolicy) = private suspend fun updatePolicy(policy: MagiskPolicy) = db.update(policy)
db.update(policy).andThen(Single.just(policy))
private fun deletePolicy(policy: MagiskPolicy) =
db.delete(policy.uid).andThen(Single.just(policy))
} }

View File

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