mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-31 07:08:01 +00:00
Merge remote-tracking branch 'john/master' into feature/redesign
# Conflicts: # app/build.gradle # app/src/main/java/com/topjohnwu/magisk/Hacks.kt # app/src/main/java/com/topjohnwu/magisk/data/database/RepoDatabase.kt # app/src/main/java/com/topjohnwu/magisk/data/repository/LogRepository.kt # app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt # app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt # app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt # app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt # app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt # app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt # app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt # app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt # app/src/main/res/xml/app_settings.xml
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
package a;
|
||||
|
||||
import androidx.core.app.AppComponentFactory;
|
||||
|
||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||
import com.topjohnwu.signing.BootSigner;
|
||||
|
||||
public class a extends AppComponentFactory {
|
||||
public class a {
|
||||
|
||||
@Deprecated
|
||||
public static boolean patchAPK(String in, String out, String pkg) {
|
||||
|
||||
@@ -11,11 +11,15 @@ import androidx.work.impl.WorkDatabase
|
||||
import androidx.work.impl.WorkDatabase_Impl
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase_Impl
|
||||
import com.topjohnwu.magisk.di.ActivityTracker
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.unwrap
|
||||
import com.topjohnwu.magisk.utils.RootInit
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
@@ -33,10 +37,12 @@ open class App() : Application() {
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootInit::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
FileProvider.callHandler = SuHandler
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
RepoDatabase::class.java -> RepoDatabase_Impl()
|
||||
SuLogDatabase::class.java -> SuLogDatabase_Impl()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -58,15 +64,15 @@ open class App() : Application() {
|
||||
app = this
|
||||
impl = base
|
||||
}
|
||||
ResourceMgr.init(impl)
|
||||
super.attachBaseContext(impl.wrap())
|
||||
val wrapped = impl.wrap()
|
||||
super.attachBaseContext(wrapped)
|
||||
|
||||
// Normal startup
|
||||
startKoin {
|
||||
androidContext(baseContext)
|
||||
androidContext(wrapped)
|
||||
modules(koinModules)
|
||||
}
|
||||
ResourceMgr.reload()
|
||||
ResourceMgr.init(impl)
|
||||
app.registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
||||
}
|
||||
@@ -77,7 +83,7 @@ open class App() : Application() {
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
ResourceMgr.reload(newConfig)
|
||||
resources.updateConfig(newConfig)
|
||||
if (!isRunningAsStub)
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
@@ -33,7 +32,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_FINGERPRINT = "su_fingerprint"
|
||||
const val SU_BIOMETRIC = "su_biometric"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val KEYSTORE = "keystore"
|
||||
|
||||
@@ -139,7 +138,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
@@ -147,9 +146,18 @@ object Config : PreferenceModel, DBConfig {
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
fun initialize() = prefs.edit {
|
||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
fun initialize() = prefs.also {
|
||||
if (it.getBoolean(SU_FINGERPRINT, false)) {
|
||||
suBiometric = true
|
||||
}
|
||||
}.edit {
|
||||
parsePrefs(this)
|
||||
|
||||
// Legacy stuff
|
||||
remove(SU_FINGERPRINT)
|
||||
|
||||
// Get actual state
|
||||
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
|
||||
@@ -157,7 +165,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||
putBoolean(Key.SU_BIOMETRIC, BiometricHelper.isEnabled)
|
||||
}.also {
|
||||
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
||||
prefs.edit().putString(Key.UPDATE_CHANNEL, defaultChannel.toString()).apply()
|
||||
@@ -166,7 +174,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config).buffered()
|
||||
val input = SuFileInputStream(config)
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
@@ -217,9 +225,10 @@ object Config : PreferenceModel, DBConfig {
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().commit()
|
||||
val context = get<Context>(Protected)
|
||||
val xml = File(
|
||||
"${get<Context>(Protected).filesDir.parent}/shared_prefs",
|
||||
"${packageName}_preferences.xml"
|
||||
"${context.filesDir.parent}/shared_prefs",
|
||||
"${context.packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ object Const {
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object Version {
|
||||
const val MIN_SUPPORT = 18000
|
||||
const val CONNECT_MODE = 20002
|
||||
const val MIN_VERSION = "v18.0"
|
||||
const val MIN_VERCODE = 18000
|
||||
const val CONNECT_MODE = 20100
|
||||
const val PROVIDER_CONNECT = 20102
|
||||
}
|
||||
|
||||
object ID {
|
||||
|
||||
@@ -14,8 +14,7 @@ import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.extensions.forceGetDeclaredField
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
@@ -23,6 +22,8 @@ import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.defaultLocale
|
||||
import java.util.*
|
||||
@@ -52,49 +53,22 @@ fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
||||
}
|
||||
}
|
||||
|
||||
// Override locale and inject resources from dynamic APK
|
||||
private fun Resources.patch(config: Configuration = Configuration(configuration)): Resources {
|
||||
config.setLocale(currentLocale)
|
||||
updateConfiguration(config, displayMetrics)
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(ResourceMgr.resApk)
|
||||
return this
|
||||
}
|
||||
|
||||
fun Class<*>.cmp(pkg: String = BuildConfig.APPLICATION_ID): ComponentName {
|
||||
fun Class<*>.cmp(pkg: String): ComponentName {
|
||||
val name = ClassMap[this].name
|
||||
return ComponentName(pkg, Info.stub?.componentMap?.get(name) ?: name)
|
||||
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
}
|
||||
|
||||
fun Context.intent(c: Class<*>): Intent {
|
||||
val cls = ClassMap[c]
|
||||
return Info.stub?.let {
|
||||
val className = it.componentMap.getOrElse(cls.name) { cls.name }
|
||||
Intent().setComponent(ComponentName(this, className))
|
||||
} ?: Intent(this, cls)
|
||||
}
|
||||
|
||||
fun resolveRes(idx: Int): Int {
|
||||
return Info.stub?.resourceMap?.get(idx) ?: when (idx) {
|
||||
DynAPK.NOTIFICATION -> R.drawable.ic_magisk_outline
|
||||
DynAPK.DOWNLOAD -> R.drawable.sc_cloud_download
|
||||
DynAPK.SUPERUSER -> R.drawable.sc_superuser
|
||||
DynAPK.MODULES -> R.drawable.sc_extension
|
||||
DynAPK.MAGISKHIDE -> R.drawable.sc_magiskhide
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||
|
||||
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
||||
open val mRes: Resources get() = ResourceMgr.resource
|
||||
private val loader by lazy { javaClass.classLoader!! }
|
||||
|
||||
override fun getResources(): Resources {
|
||||
return mRes
|
||||
}
|
||||
|
||||
override fun getClassLoader(): ClassLoader {
|
||||
return loader
|
||||
return javaClass.classLoader!!
|
||||
}
|
||||
|
||||
override fun createConfigurationContext(config: Configuration): Context {
|
||||
@@ -104,38 +78,31 @@ private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
||||
|
||||
private class ResContext(base: Context) : GlobalResContext(base) {
|
||||
override val mRes by lazy { base.resources.patch() }
|
||||
|
||||
private fun Resources.patch(): Resources {
|
||||
updateConfig()
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(ResourceMgr.resApk)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
object ResourceMgr {
|
||||
|
||||
internal lateinit var resource: Resources
|
||||
internal lateinit var resApk: String
|
||||
lateinit var resource: Resources
|
||||
lateinit var resApk: String
|
||||
|
||||
fun init(context: Context) {
|
||||
resource = context.resources
|
||||
if (isRunningAsStub)
|
||||
refreshLocale()
|
||||
if (isRunningAsStub) {
|
||||
resApk = DynAPK.current(context).path
|
||||
}
|
||||
|
||||
fun reload(config: Configuration = Configuration(resource.configuration)) {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
localeConfig.isEmpty() -> defaultLocale
|
||||
else -> localeConfig.langTagToLocale()
|
||||
resource.assets.addAssetPath(resApk)
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
resource.patch(config)
|
||||
}
|
||||
|
||||
fun getString(locale: Locale, @StringRes id: Int): String {
|
||||
val config = Configuration()
|
||||
config.setLocale(locale)
|
||||
return Resources(resource.assets, resource.displayMetrics, config).getString(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(api = 28)
|
||||
@RequiresApi(28)
|
||||
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
||||
|
||||
override fun schedule(job: JobInfo): Int {
|
||||
@@ -163,49 +130,15 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler
|
||||
}
|
||||
|
||||
private fun JobInfo.patch(): JobInfo {
|
||||
// We need to patch the component of JobInfo to access WorkManager SystemJobService
|
||||
|
||||
// We need to swap out the service of JobInfo
|
||||
val name = service.className
|
||||
val component = ComponentName(
|
||||
service.packageName,
|
||||
Info.stub!!.componentMap[name] ?: name
|
||||
Info.stub!!.classToComponent[name] ?: name
|
||||
)
|
||||
|
||||
// Clone the JobInfo except component
|
||||
val builder = JobInfo.Builder(id, component)
|
||||
.setExtras(extras)
|
||||
.setTransientExtras(transientExtras)
|
||||
.setClipData(clipData, clipGrantFlags)
|
||||
.setRequiredNetwork(requiredNetwork)
|
||||
.setEstimatedNetworkBytes(estimatedNetworkDownloadBytes, estimatedNetworkUploadBytes)
|
||||
.setRequiresCharging(isRequireCharging)
|
||||
.setRequiresDeviceIdle(isRequireDeviceIdle)
|
||||
.setRequiresBatteryNotLow(isRequireBatteryNotLow)
|
||||
.setRequiresStorageNotLow(isRequireStorageNotLow)
|
||||
.also {
|
||||
triggerContentUris?.let { uris ->
|
||||
for (uri in uris)
|
||||
it.addTriggerContentUri(uri)
|
||||
}
|
||||
}
|
||||
.setTriggerContentUpdateDelay(triggerContentUpdateDelay)
|
||||
.setTriggerContentMaxDelay(triggerContentMaxDelay)
|
||||
.setImportantWhileForeground(isImportantWhileForeground)
|
||||
.setPrefetch(isPrefetch)
|
||||
.setPersisted(isPersisted)
|
||||
|
||||
if (isPeriodic) {
|
||||
builder.setPeriodic(intervalMillis, flexMillis)
|
||||
} else {
|
||||
if (minLatencyMillis > 0)
|
||||
builder.setMinimumLatency(minLatencyMillis)
|
||||
if (maxExecutionDelayMillis > 0)
|
||||
builder.setOverrideDeadline(maxExecutionDelayMillis)
|
||||
}
|
||||
if (!isRequireDeviceIdle)
|
||||
builder.setBackoffCriteria(initialBackoffMillis, backoffPolicy)
|
||||
|
||||
return builder.build()
|
||||
javaClass.forceGetDeclaredField("service")?.set(this, component)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +153,9 @@ object ClassMap {
|
||||
GeneralReceiver::class.java to a.h::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java,
|
||||
ProcessPhoenix::class.java to a.r::class.java,
|
||||
RedesignActivity::class.java to a.i::class.java
|
||||
)
|
||||
|
||||
operator fun get(c: Class<*>) = map.getOrElse(c) { throw IllegalArgumentException() }
|
||||
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
|
||||
}
|
||||
|
||||
@@ -40,24 +40,21 @@ object Info {
|
||||
val str = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
||||
val code = ShellUtils.fastCmd("magisk -V").toInt()
|
||||
val hide = Shell.su("magiskhide --status").exec().isSuccess
|
||||
var mode = -1
|
||||
if (code >= Const.Version.CONNECT_MODE) {
|
||||
mode = Shell.su("magisk --connect-mode").exec().code
|
||||
if (mode == 0) {
|
||||
// Manually trigger broadcast test
|
||||
Shell.su("magisk --broadcast-test").exec()
|
||||
}
|
||||
}
|
||||
Env(code, str, hide, mode)
|
||||
Env(str, code, hide)
|
||||
}.getOrElse { Env() }
|
||||
|
||||
class Env(
|
||||
val magiskVersionCode: Int = -1,
|
||||
val magiskVersionString: String = "",
|
||||
hide: Boolean = false,
|
||||
var connectionMode: Int = -1
|
||||
code: Int = -1,
|
||||
hide: Boolean = false
|
||||
) {
|
||||
val magiskHide get() = Config.magiskHide
|
||||
val magiskVersionCode = when (code) {
|
||||
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
|
||||
else -> if(Shell.rootAccess()) code else -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
val isActive = magiskVersionCode >= 0
|
||||
|
||||
init {
|
||||
Config.magiskHide = hide
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import com.topjohnwu.magisk.base.BaseActivity
|
||||
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
@@ -17,7 +17,7 @@ abstract class BaseViewModel(
|
||||
|
||||
val isConnected = Observer(gIsConnected) { gIsConnected.value }
|
||||
|
||||
fun withView(action: Activity.() -> Unit) {
|
||||
fun withView(action: BaseActivity<*, *>.() -> Unit) {
|
||||
ViewActionEvent(action).publish()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LogDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.LOG
|
||||
|
||||
fun deleteOutdated(
|
||||
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
lessThan("time", suTimeout.toString())
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun deleteAll() = query<Delete> {}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
orderBy("time", Order.DESC)
|
||||
}.flattenAsFlowable { it }
|
||||
.map { it.toLog() }
|
||||
.toList()
|
||||
|
||||
fun put(log: MagiskLog) = query<Insert> {
|
||||
values(log.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
}
|
||||
@@ -3,7 +3,10 @@ package com.topjohnwu.magisk.data.database
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Delete
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Replace
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Select
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
@@ -16,7 +19,7 @@ class PolicyDao(
|
||||
private val context: Context
|
||||
) : BaseDao() {
|
||||
|
||||
override val table: String = DatabaseDefinition.Table.POLICY
|
||||
override val table: String = Table.POLICY
|
||||
|
||||
fun deleteOutdated(
|
||||
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
|
||||
@@ -72,4 +75,4 @@ class PolicyDao(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@ import androidx.room.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
abstract class RepoDao {
|
||||
abstract class RepoDao(private val db: RepoDatabase) {
|
||||
|
||||
val repoIDList get() = getRepoID().map { it.id }
|
||||
|
||||
@@ -15,13 +21,10 @@ abstract class RepoDao {
|
||||
}
|
||||
|
||||
var etagKey: String
|
||||
set(etag) = addEtagRaw(RepoEtag(0, etag))
|
||||
set(value) = addEtagRaw(RepoEtag(0, value))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
fun clear() {
|
||||
clearRepos()
|
||||
clearEtag()
|
||||
}
|
||||
fun clear() = db.clearAllTables()
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
@@ -52,12 +55,6 @@ abstract class RepoDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||
|
||||
@Query("DELETE FROM repos")
|
||||
protected abstract fun clearRepos()
|
||||
|
||||
@Query("DELETE FROM etag")
|
||||
protected abstract fun clearEtag()
|
||||
}
|
||||
|
||||
data class RepoID(
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Delete
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Replace
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Select
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.SETTINGS
|
||||
override val table = Table.SETTINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
@@ -19,4 +22,4 @@ class SettingsDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Delete
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Replace
|
||||
import com.topjohnwu.magisk.data.database.magiskdb.Select
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.STRINGS
|
||||
override val table = Table.STRINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
@@ -19,4 +22,4 @@ class StringDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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 java.util.*
|
||||
|
||||
@Database(version = 1, entities = [MagiskLog::class])
|
||||
abstract class SuLogDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun suLogDao(): SuLogDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
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() }
|
||||
|
||||
fun fetchAll() = deleteOutdated().andThen(fetch())
|
||||
|
||||
@Query("SELECT * FROM logs ORDER BY time DESC")
|
||||
protected abstract fun fetch(): Single<MutableList<MagiskLog>>
|
||||
|
||||
@Insert
|
||||
abstract fun insert(log: MagiskLog): Completable
|
||||
|
||||
@Query("DELETE FROM logs WHERE time < :timeout")
|
||||
protected abstract fun deleteOutdated(
|
||||
timeout: Long = twoWeeksAgo
|
||||
): Completable
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : MagiskQueryBuilder> query(builder: Builder.() -> Unit) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { MagiskQuery(it) }
|
||||
.query()
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
object DatabaseDefinition {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun MagiskQuery.query() = query.su()
|
||||
|
||||
fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
|
||||
fun String.su() = suRaw().map { it.toMap() }
|
||||
|
||||
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()
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
inline class MagiskQuery(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.data.database.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class TableStrict
|
||||
|
||||
@TableStrict
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : Query.Builder> query(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()
|
||||
@@ -1,27 +1,17 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
package com.topjohnwu.magisk.data.database.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.ASC
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.DESC
|
||||
|
||||
interface MagiskQueryBuilder {
|
||||
class Query(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
|
||||
val requestType: String
|
||||
var table: String
|
||||
|
||||
companion object {
|
||||
inline operator fun <reified Builder : MagiskQueryBuilder> invoke(builder: Builder.() -> Unit): MagiskQuery =
|
||||
Builder::class.java.newInstance()
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let {
|
||||
MagiskQuery(it)
|
||||
}
|
||||
interface Builder {
|
||||
val requestType: String
|
||||
var table: String
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Delete : MagiskQueryBuilder {
|
||||
class Delete : Query.Builder {
|
||||
override val requestType: String = "DELETE FROM"
|
||||
override var table = ""
|
||||
|
||||
@@ -36,7 +26,7 @@ class Delete : MagiskQueryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
class Select : MagiskQueryBuilder {
|
||||
class Select : Query.Builder {
|
||||
override val requestType: String get() = "SELECT $fields FROM"
|
||||
override lateinit var table: String
|
||||
|
||||
@@ -69,7 +59,7 @@ class Replace : Insert() {
|
||||
override val requestType: String = "REPLACE INTO"
|
||||
}
|
||||
|
||||
open class Insert : MagiskQueryBuilder {
|
||||
open class Insert : Query.Builder {
|
||||
override val requestType: String = "INSERT INTO"
|
||||
override lateinit var table: String
|
||||
|
||||
@@ -137,19 +127,11 @@ class Condition {
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
|
||||
@set:OrderStrict
|
||||
var order = DESC
|
||||
var field = ""
|
||||
|
||||
companion object {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
object Order {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
@StringDef(ASC, DESC)
|
||||
@StringDef(Order.ASC, Order.DESC)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class OrderStrict
|
||||
annotation class OrderStrict
|
||||
@@ -29,8 +29,8 @@ interface DBConfig {
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
||||
private var value: Int? = null
|
||||
@@ -47,29 +47,29 @@ class DBSettingsValue(
|
||||
this.value = value
|
||||
}
|
||||
thisRef.settingsDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
class DBBoolSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||
|
||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
||||
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean
|
||||
= base.getValue(thisRef, property) != 0
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||
base.getValue(thisRef, property) != 0
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
) : ReadWriteProperty<DBConfig, String> {
|
||||
|
||||
private var value: String? = null
|
||||
@@ -90,16 +90,16 @@ class DBStringsValue(
|
||||
thisRef.stringDao.delete(name).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.delete(name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
} else {
|
||||
if (sync) {
|
||||
thisRef.stringDao.put(name, value).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,40 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.LogDao
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class LogRepository(
|
||||
private val logDao: LogDao
|
||||
private val logDao: SuLogDao
|
||||
) {
|
||||
|
||||
fun fetchLogsNowrap() = logDao.fetchAll()
|
||||
.map { it.sortedByDescending { it.date.time } }
|
||||
|
||||
fun fetchLogs() = fetchLogsNowrap()
|
||||
.map { it.wrap() }
|
||||
fun fetchLogs() = fetchLogsNowrap().map { it.wrap() }
|
||||
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
|
||||
.filter { it.isNotEmpty() }
|
||||
fun fetchMagiskLogs() = Single.fromCallable {
|
||||
Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out
|
||||
}.flattenAsFlowable { it }.filter { it.isNotEmpty() }
|
||||
|
||||
fun clearLogs() = logDao.deleteAll()
|
||||
fun clearOutdated() = logDao.deleteOutdated()
|
||||
|
||||
fun clearMagiskLogs() = Shell.su("echo -n > " + Const.MAGISK_LOG)
|
||||
.toSingle()
|
||||
.map { it.exec() }
|
||||
fun clearMagiskLogs() = Completable.fromAction {
|
||||
Shell.su("echo -n > ${Const.MAGISK_LOG}").exec()
|
||||
}
|
||||
|
||||
fun put(log: MagiskLog) = logDao.put(log)
|
||||
fun insert(log: MagiskLog) = logDao.insert(log)
|
||||
|
||||
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
|
||||
val day = TimeUnit.DAYS.toMillis(1)
|
||||
return groupBy { it.date.time / day }
|
||||
return groupBy { it.time / day }
|
||||
.map { WrappedMagiskLog(it.key * day, it.value) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.data.repository
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
@@ -57,7 +56,7 @@ class MagiskRepository(
|
||||
.toList()
|
||||
|
||||
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
|
||||
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
|
||||
Shell.su("magiskhide --${isEnabled.state} $packageName $process").submit()
|
||||
|
||||
private val Boolean.state get() = if (this) "add" else "rm"
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.koin.dsl.module
|
||||
|
||||
|
||||
val databaseModule = module {
|
||||
single { LogDao() }
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
@@ -16,10 +15,16 @@ val databaseModule = module {
|
||||
single { get<RepoDatabase>().repoDao() }
|
||||
single { get<RepoDatabase>().repoByNameDao() }
|
||||
single { get<RepoDatabase>().repoByUpdatedDao() }
|
||||
single { createSuLogDatabase(get(Protected)).suLogDao() }
|
||||
single { RepoUpdater(get(), get()) }
|
||||
}
|
||||
|
||||
fun createRepoDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
fun createSuLogDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
fun ui(body: () -> Unit) = Handler(Looper.getMainLooper()).post(body)
|
||||
@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.ObservableField
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import io.reactivex.*
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposables
|
||||
@@ -53,14 +54,14 @@ fun <T> Single<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError)
|
||||
.subscribe(onSuccess, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
onSuccess: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
.subscribe(onSuccess, onError, onComplete)
|
||||
|
||||
fun <T> Flowable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
@@ -104,54 +105,54 @@ fun Completable.updateBy(
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
doOnSubscribe { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
doOnError { UiThreadHandler.run { body(it) } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
doOnNext { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
doOnSuccess { UiThreadHandler.run { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
fun Completable.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
doOnComplete { UiThreadHandler.run { body() } }
|
||||
|
||||
|
||||
fun <T, R> Observable<List<T>>.mapList(
|
||||
|
||||
@@ -12,13 +12,21 @@ import android.content.pm.PackageManager.*
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
@@ -100,6 +108,23 @@ fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
fun Context.readUri(uri: Uri) =
|
||||
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
fun Context.getBitmap(id: Int): Bitmap {
|
||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||
if (drawable is BitmapDrawable)
|
||||
return drawable.bitmap
|
||||
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
|
||||
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun Intent.startActivityWithRoot() {
|
||||
@@ -286,6 +311,8 @@ fun Context.unwrap(): Context {
|
||||
return context
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
||||
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toFile
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
@@ -21,8 +22,6 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun InputStream.writeTo(file: File) =
|
||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
||||
|
||||
@@ -105,4 +104,33 @@ fun Locale.toLangTag(): String {
|
||||
}
|
||||
|
||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||
|
||||
// Reflection hacks
|
||||
|
||||
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
|
||||
private val getDeclaredMethod = Class::class.java.getMethod("getDeclaredMethod",
|
||||
String::class.java, arrayOf<Class<*>>()::class.java)
|
||||
private val getDeclaredField = Class::class.java.getMethod("getDeclaredField", String::class.java)
|
||||
|
||||
fun ClassLoader.forceLoadClass(name: String) =
|
||||
runCatching { loadClass.invoke(this, name) }.getOrNull() as Class<*>?
|
||||
|
||||
fun Class<*>.forceGetDeclaredMethod(name: String, vararg types: Class<*>) =
|
||||
(runCatching { getDeclaredMethod.invoke(this, name, types) }.getOrNull() as Method?)?.also {
|
||||
it.isAccessible = true
|
||||
}
|
||||
|
||||
fun Class<*>.forceGetDeclaredField(name: String) =
|
||||
(runCatching { getDeclaredField.invoke(this, name) }.getOrNull() as Field?)?.also {
|
||||
it.isAccessible = true
|
||||
}
|
||||
|
||||
inline fun <reified T> T.forceGetClass(name: String) =
|
||||
T::class.java.classLoader?.forceLoadClass(name)
|
||||
|
||||
fun Class<*>.forceGetField(name: String): Field? =
|
||||
forceGetDeclaredField(name) ?: superclass?.forceGetField(name)
|
||||
|
||||
fun Class<*>.forceGetMethod(name: String, vararg types: Class<*>): Method? =
|
||||
forceGetDeclaredMethod(name, *types) ?: superclass?.forceGetMethod(name, *types)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.exists
|
||||
@@ -69,14 +69,14 @@ open class DownloadService : RemoteFileService() {
|
||||
|
||||
// ---
|
||||
|
||||
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
override fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActionsInternal(subject)
|
||||
is Module -> addActionsInternal(subject)
|
||||
is Manager -> addActionsInternal(subject)
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
|
||||
private fun Notification.Builder.addActionsInternal(subject: Magisk)
|
||||
= when (val conf = subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
@@ -92,7 +92,7 @@ open class DownloadService : RemoteFileService() {
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
|
||||
private fun Notification.Builder.addActionsInternal(subject: Module)
|
||||
= when (subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
@@ -106,19 +106,19 @@ open class DownloadService : RemoteFileService() {
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
|
||||
private fun Notification.Builder.addActionsInternal(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.setContentIntent(intent: Intent) =
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { setContentIntent(it) }
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { addAction(icon, getString(title), it) }
|
||||
|
||||
@@ -140,7 +140,7 @@ open class DownloadService : RemoteFileService() {
|
||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||
val app = context.applicationContext
|
||||
val builder = Builder().apply(argBuilder)
|
||||
val intent = app.intent(DownloadService::class.java).putExtra(ARG_URL, builder.subject)
|
||||
val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent)
|
||||
|
||||
@@ -38,7 +38,7 @@ private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||
patch(apk, id)
|
||||
} else {
|
||||
// Simply relaunch the app
|
||||
ProcessPhoenix.triggerRebirth(this)
|
||||
ProcessPhoenix.triggerRebirth(this, intent<ProcessPhoenix>())
|
||||
}
|
||||
} else {
|
||||
patch(apk, id)
|
||||
|
||||
@@ -3,22 +3,20 @@ package com.topjohnwu.magisk.model.download
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.topjohnwu.magisk.base.BaseService
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import org.koin.core.KoinComponent
|
||||
import java.util.*
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class NotificationService : BaseService(), KoinComponent {
|
||||
|
||||
abstract val defaultNotification: NotificationCompat.Builder
|
||||
abstract val defaultNotification: Notification.Builder
|
||||
|
||||
private val manager by lazy { NotificationManagerCompat.from(this) }
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
private val notifications =
|
||||
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
|
||||
Collections.synchronizedMap(mutableMapOf<Int, Notification.Builder>())
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
@@ -30,7 +28,7 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
body: (NotificationCompat.Builder) -> Unit = {}
|
||||
body: (Notification.Builder) -> Unit = {}
|
||||
) {
|
||||
val notification = notifications.getOrPut(id) { defaultNotification }
|
||||
|
||||
@@ -43,7 +41,7 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
|
||||
protected fun finishNotify(
|
||||
id: Int,
|
||||
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
|
||||
editBody: (Notification.Builder) -> Notification.Builder? = { null }
|
||||
) : Int {
|
||||
val currentNotification = remove(id)?.run(editBody)
|
||||
|
||||
@@ -62,11 +60,11 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
// ---
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
manager.notify(id, notification)
|
||||
Notifications.mgr.notify(id, notification)
|
||||
}
|
||||
|
||||
private fun cancel(id: Int) {
|
||||
manager.cancel(id)
|
||||
Notifications.mgr.cancel(id)
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
@@ -84,4 +82,4 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
||||
// --
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
@@ -27,7 +28,7 @@ abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
val service: GithubRawServices by inject()
|
||||
|
||||
override val defaultNotification: NotificationCompat.Builder
|
||||
override val defaultNotification
|
||||
get() = Notifications.progress(this, "")
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -117,8 +118,8 @@ abstract class RemoteFileService : NotificationService() {
|
||||
@Throws(Throwable::class)
|
||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||
|
||||
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
: NotificationCompat.Builder
|
||||
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||
: Notification.Builder
|
||||
|
||||
companion object : KoinComponent {
|
||||
const val ARG_URL = "arg_url"
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.extensions.timeFormatTime
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
data class MagiskLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
@@ -13,9 +17,10 @@ data class MagiskLog(
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val date: Date
|
||||
val time: Long = -1
|
||||
) {
|
||||
val timeString = date.time.toTime(timeFormatTime)
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
@Ignore val timeString = time.toTime(timeFormatTime)
|
||||
}
|
||||
|
||||
data class WrappedMagiskLog(
|
||||
@@ -23,35 +28,8 @@ data class WrappedMagiskLog(
|
||||
val items: List<MagiskLog>
|
||||
)
|
||||
|
||||
fun Map<String, String>.toLog(): MagiskLog {
|
||||
return MagiskLog(
|
||||
fromUid = get("from_uid")?.toIntOrNull() ?: -1,
|
||||
toUid = get("to_uid")?.toIntOrNull() ?: -1,
|
||||
fromPid = get("from_pid")?.toIntOrNull() ?: -1,
|
||||
packageName = get("package_name").orEmpty(),
|
||||
appName = get("app_name").orEmpty(),
|
||||
command = get("command").orEmpty(),
|
||||
action = get("action")?.toIntOrNull() != 0,
|
||||
date = get("time")?.toLongOrNull()?.toDate() ?: Date()
|
||||
)
|
||||
}
|
||||
|
||||
fun Long.toDate() = Date(this)
|
||||
|
||||
fun MagiskLog.toMap() = mapOf(
|
||||
"from_uid" to fromUid,
|
||||
"to_uid" to toUid,
|
||||
"from_pid" to fromPid,
|
||||
"package_name" to packageName,
|
||||
"app_name" to appName,
|
||||
"command" to command,
|
||||
"action" to action,
|
||||
"time" to date.time
|
||||
)
|
||||
|
||||
fun MagiskPolicy.toLog(
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
date: Date
|
||||
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, date)
|
||||
command: String
|
||||
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)
|
||||
|
||||
@@ -7,11 +7,11 @@ import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
|
||||
|
||||
|
||||
data class MagiskPolicy(
|
||||
val uid: Int,
|
||||
var uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val policy: Int = INTERACTIVE,
|
||||
val until: Long = -1L,
|
||||
var policy: Int = INTERACTIVE,
|
||||
var until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true,
|
||||
val applicationInfo: ApplicationInfo
|
||||
@@ -38,7 +38,7 @@ fun MagiskPolicy.toMap() = mapOf(
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, 0)
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
@@ -56,14 +56,15 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): MagiskPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, 0)
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
||||
return MagiskPolicy(
|
||||
uid = this,
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
policy = policy,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class Module(path: String) : BaseModule() {
|
||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||
moduleList.add(module)
|
||||
}
|
||||
return moduleList.sortedBy { it.name }
|
||||
return moduleList.sortedBy { it.name.toLowerCase() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,10 @@ class LogItemRvItem(
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemRvItem): Boolean = items
|
||||
.any { !other.items.contains(it) }
|
||||
override fun contentSameAs(other: LogItemRvItem): Boolean {
|
||||
if (items.size != other.items.size) return false
|
||||
return items.all { it in other.items }
|
||||
}
|
||||
|
||||
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
|
||||
}
|
||||
@@ -53,13 +55,7 @@ class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvI
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemEntryRvItem) = item.fromUid == other.item.fromUid &&
|
||||
item.toUid == other.item.toUid &&
|
||||
item.fromPid == other.item.fromPid &&
|
||||
item.packageName == other.item.packageName &&
|
||||
item.command == other.item.command &&
|
||||
item.action == other.item.action &&
|
||||
item.date == other.item.date
|
||||
override fun contentSameAs(other: LogItemEntryRvItem) = item == other.item
|
||||
|
||||
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.model.events
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -15,6 +14,7 @@ import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.extensions.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.base.BaseActivity
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
@@ -135,7 +135,7 @@ class UpdateSafetyNetEvent : ViewEvent(), ContextExecutor, KoinComponent, Safety
|
||||
}
|
||||
}
|
||||
|
||||
class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent(), ActivityExecutor {
|
||||
class ViewActionEvent(val action: BaseActivity<*, *>.() -> Unit) : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: AppCompatActivity) = activity.run(action)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,37 +2,25 @@ package com.topjohnwu.magisk.model.receiver
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.extensions.reboot
|
||||
import com.topjohnwu.magisk.extensions.startActivity
|
||||
import com.topjohnwu.magisk.extensions.startActivityWithRoot
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.SuLogger
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.core.inject
|
||||
import timber.log.Timber
|
||||
|
||||
open class GeneralReceiver : BaseReceiver() {
|
||||
|
||||
private val policyDB: PolicyDao by inject()
|
||||
|
||||
companion object {
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
}
|
||||
|
||||
private fun getPkg(intent: Intent): String {
|
||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||
}
|
||||
@@ -40,59 +28,19 @@ open class GeneralReceiver : BaseReceiver() {
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
// Debug messages
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(intent.action)
|
||||
intent.extras?.let { bundle ->
|
||||
bundle.keySet().forEach {
|
||||
Timber.d("[%s]=[%s]", it, bundle[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
|
||||
val action = intent.getStringExtra("action")
|
||||
if (action == null) {
|
||||
// Actual boot completed event
|
||||
Shell.su("mm_patch_dtbo").submit {
|
||||
if (it.isSuccess)
|
||||
Notifications.dtboPatched(context)
|
||||
}
|
||||
return
|
||||
}
|
||||
when (action) {
|
||||
REQUEST -> {
|
||||
val i = context.intent(SuRequestActivity::class.java)
|
||||
.setAction(action)
|
||||
.putExtra("socket", intent.getStringExtra("socket"))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
if (SDK_INT >= 29) {
|
||||
// Android Q does not allow starting activity from background
|
||||
i.startActivityWithRoot()
|
||||
} else {
|
||||
i.startActivity(context)
|
||||
}
|
||||
}
|
||||
LOG -> SuLogger.handleLogs(context, intent)
|
||||
NOTIFY -> SuLogger.handleNotify(context, intent)
|
||||
TEST -> {
|
||||
val mode = intent.getIntExtra("mode", 1 shl 1)
|
||||
if (mode > Info.env.connectionMode)
|
||||
Info.env.connectionMode = mode
|
||||
Shell.su("magisk --connect-mode $mode").submit()
|
||||
}
|
||||
}
|
||||
Intent.ACTION_REBOOT -> {
|
||||
SuHandler(context, intent.getStringExtra("action"), intent.extras)
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED ->
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
policyDB.delete(getPkg(intent)).blockingGet()
|
||||
}
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
val pkg = getPkg(intent)
|
||||
policyDB.delete(pkg).blockingGet()
|
||||
"magiskhide --rm $pkg".su().blockingGet()
|
||||
Shell.su("magiskhide --rm $pkg").submit()
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
|
||||
Const.Key.BROADCAST_MANAGER_UPDATE -> {
|
||||
|
||||
@@ -266,7 +266,7 @@ abstract class MagiskInstaller {
|
||||
|
||||
val patched = File(installDir, "new-boot.img")
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with test keys")
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File(installDir, "signed.img")
|
||||
try {
|
||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import com.ncapdevi.fragnav.FragNavController
|
||||
import com.ncapdevi.fragnav.FragNavTransactionOptions
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -29,7 +31,6 @@ import com.topjohnwu.magisk.ui.module.ReposFragment
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsFragment
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import timber.log.Timber
|
||||
import kotlin.reflect.KClass
|
||||
@@ -60,12 +61,21 @@ open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Na
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (!SplashActivity.DONE) {
|
||||
startActivity(intent(SplashActivity::class.java))
|
||||
startActivity(intent<SplashActivity>())
|
||||
finish()
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (Info.env.isUnsupported && !viewModel.shownUnsupportedDialog) {
|
||||
viewModel.shownUnsupportedDialog = true
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.unsupport_magisk_title)
|
||||
.setMessage(getString(R.string.unsupport_magisk_msg, Const.Version.MIN_VERSION))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
navigationController.apply {
|
||||
rootFragmentListener = this@MainActivity
|
||||
transactionListener = this@MainActivity
|
||||
@@ -153,16 +163,11 @@ open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Na
|
||||
|
||||
private fun checkHideSection() {
|
||||
val menu = binding.navView.menu
|
||||
menu.findItem(R.id.magiskHideFragment).isVisible =
|
||||
Shell.rootAccess() && Info.env.magiskHide
|
||||
menu.findItem(R.id.modulesFragment).isVisible =
|
||||
Shell.rootAccess() && Info.env.magiskVersionCode >= 0
|
||||
menu.findItem(R.id.reposFragment).isVisible =
|
||||
(viewModel.isConnected.value && Shell.rootAccess() && Info.env.magiskVersionCode >= 0)
|
||||
menu.findItem(R.id.logFragment).isVisible =
|
||||
Shell.rootAccess()
|
||||
menu.findItem(R.id.superuserFragment).isVisible =
|
||||
Utils.showSuperUser()
|
||||
menu.findItem(R.id.magiskHideFragment).isVisible = Info.env.isActive && Info.env.magiskHide
|
||||
menu.findItem(R.id.modulesFragment).isVisible = Info.env.isActive
|
||||
menu.findItem(R.id.reposFragment).isVisible = Info.isConnected.value && Info.env.isActive
|
||||
menu.findItem(R.id.logFragment).isVisible = Info.env.isActive
|
||||
menu.findItem(R.id.superuserFragment).isVisible = Utils.showSuperUser()
|
||||
}
|
||||
|
||||
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.topjohnwu.magisk.model.navigation.Navigation
|
||||
|
||||
class MainViewModel : BaseViewModel() {
|
||||
|
||||
var shownUnsupportedDialog = false
|
||||
|
||||
fun navPressed() = Navigation.Main.OPEN_NAV.publish()
|
||||
|
||||
fun navigationItemPressed(item: MenuItem): Boolean {
|
||||
|
||||
@@ -7,9 +7,13 @@ import android.text.TextUtils
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.intent
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
open class SplashActivity : Activity() {
|
||||
@@ -20,19 +24,7 @@ open class SplashActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Shell.getShell {
|
||||
if (Info.env.magiskVersionCode > 0 && Info.env.magiskVersionCode < Const.Version.MIN_SUPPORT) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.unsupport_magisk_title)
|
||||
.setMessage(R.string.unsupport_magisk_message)
|
||||
.setNegativeButton(android.R.string.ok, null)
|
||||
.setOnDismissListener { finish() }
|
||||
.show()
|
||||
} else {
|
||||
initAndStart()
|
||||
}
|
||||
}
|
||||
Shell.getShell { initAndStart() }
|
||||
}
|
||||
|
||||
private fun initAndStart() {
|
||||
@@ -41,7 +33,7 @@ open class SplashActivity : Activity() {
|
||||
Config.suManager = ""
|
||||
Shell.su("pm uninstall $pkg").submit()
|
||||
}
|
||||
if (TextUtils.equals(pkg, packageName)) {
|
||||
if (pkg == packageName) {
|
||||
runCatching {
|
||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
||||
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
|
||||
@@ -61,6 +53,11 @@ open class SplashActivity : Activity() {
|
||||
// Setup shortcuts
|
||||
Shortcuts.setup(this)
|
||||
|
||||
Shell.su("mm_patch_dtbo").submit {
|
||||
if (it.isSuccess)
|
||||
Notifications.dtboPatched(this)
|
||||
}
|
||||
|
||||
DONE = true
|
||||
Navigation.start(intent, this)
|
||||
finish()
|
||||
|
||||
@@ -2,9 +2,9 @@ package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -16,6 +16,7 @@ import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import java.io.File
|
||||
@@ -32,10 +33,11 @@ open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
super.onCreate(savedInstanceState)
|
||||
val id = intent.getIntExtra(Const.Key.DISMISS_ID, -1)
|
||||
if (id != -1)
|
||||
NotificationManagerCompat.from(this).cancel(id)
|
||||
Notifications.mgr.cancel(id)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -60,7 +62,7 @@ open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>()
|
||||
|
||||
companion object {
|
||||
|
||||
private fun intent(context: Context) = context.intent(FlashActivity::class.java)
|
||||
private fun intent(context: Context) = context.intent<FlashActivity>()
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
private fun intent(context: Context, file: File) = intent(context).setData(file.toUri())
|
||||
|
||||
|
||||
@@ -90,9 +90,7 @@ class HideViewModel(
|
||||
.toList()
|
||||
.map { it to items.calculateDiff(it) }
|
||||
|
||||
private fun toggleItem(item: HideProcessRvItem) = magiskRepo
|
||||
.toggleHide(item.isHidden.value, item.packageName, item.process)
|
||||
.subscribeK()
|
||||
.add()
|
||||
private fun toggleItem(item: HideProcessRvItem) =
|
||||
magiskRepo.toggleHide(item.isHidden.value, item.packageName, item.process)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class HomeViewModel(
|
||||
""
|
||||
}
|
||||
|
||||
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text)
|
||||
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text.res())
|
||||
val ctsState = KObservableField(SafetyNetState.IDLE)
|
||||
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
|
||||
val safetyNetState = Observer(ctsState, basicIntegrityState) {
|
||||
@@ -92,7 +92,7 @@ class HomeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val hasRoot = KObservableField(false)
|
||||
val isActive = KObservableField(false)
|
||||
|
||||
private var shownDialog = false
|
||||
|
||||
@@ -135,7 +135,7 @@ class HomeViewModel(
|
||||
fun safetyNetPressed() {
|
||||
ctsState.value = SafetyNetState.LOADING
|
||||
basicIntegrityState.value = SafetyNetState.LOADING
|
||||
safetyNetTitle.value = R.string.checking_safetyNet_status
|
||||
safetyNetTitle.value = R.string.checking_safetyNet_status.res()
|
||||
|
||||
UpdateSafetyNetEvent().publish()
|
||||
}
|
||||
@@ -144,7 +144,7 @@ class HomeViewModel(
|
||||
response and 0x0F == 0 -> {
|
||||
val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0
|
||||
val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0
|
||||
safetyNetTitle.value = R.string.safetyNet_check_success
|
||||
safetyNetTitle.value = R.string.safetyNet_check_success.res()
|
||||
ctsState.value = if (hasCtsPassed) {
|
||||
SafetyNetState.PASS
|
||||
} else {
|
||||
@@ -164,8 +164,8 @@ class HomeViewModel(
|
||||
ctsState.value = SafetyNetState.IDLE
|
||||
basicIntegrityState.value = SafetyNetState.IDLE
|
||||
safetyNetTitle.value = when (response) {
|
||||
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid
|
||||
else -> R.string.safetyNet_api_error
|
||||
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid.res()
|
||||
else -> R.string.safetyNet_api_error.res()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ class HomeViewModel(
|
||||
if (invalidate)
|
||||
Info.envRef.invalidate()
|
||||
|
||||
hasRoot.value = Shell.rootAccess()
|
||||
isActive.value = Info.env.isActive
|
||||
|
||||
val fetchUpdate = if (isConnected.value)
|
||||
magiskRepo.fetchUpdate().ignoreElement()
|
||||
@@ -192,7 +192,7 @@ class HomeViewModel(
|
||||
_managerState.value = MagiskState.LOADING
|
||||
ctsState.value = SafetyNetState.IDLE
|
||||
basicIntegrityState.value = SafetyNetState.IDLE
|
||||
safetyNetTitle.value = R.string.safetyNet_check_text
|
||||
safetyNetTitle.value = R.string.safetyNet_check_text.res()
|
||||
}.subscribeK {
|
||||
updateSelf()
|
||||
ensureEnv()
|
||||
@@ -215,8 +215,8 @@ class HomeViewModel(
|
||||
|
||||
private fun updateSelf() {
|
||||
magiskState.value = when (Info.env.magiskVersionCode) {
|
||||
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
|
||||
in 1 until (Info.remote.magisk.versionCode - 1) -> MagiskState.OBSOLETE
|
||||
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED
|
||||
in 1 until Info.remote.magisk.versionCode -> MagiskState.OBSOLETE
|
||||
else -> MagiskState.UP_TO_DATE
|
||||
}
|
||||
|
||||
@@ -224,10 +224,10 @@ class HomeViewModel(
|
||||
VERSION_FMT.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
|
||||
|
||||
_managerState.value = when (Info.remote.app.versionCode) {
|
||||
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
|
||||
in (BuildConfig.VERSION_CODE + 1) until Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED //wrong update channel
|
||||
in (BuildConfig.VERSION_CODE + 1) .. Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||
else -> {
|
||||
if (isRunningAsStub && Info.stub!!.version < Info.remote.stub.versionCode)
|
||||
if (Info.stub?.version ?: Int.MAX_VALUE < Info.remote.stub.versionCode)
|
||||
MagiskState.OBSOLETE
|
||||
else
|
||||
MagiskState.UP_TO_DATE
|
||||
|
||||
@@ -104,7 +104,6 @@ class LogViewModel(
|
||||
.add()
|
||||
|
||||
private fun clearMagiskLogs(callback: () -> Unit) = logRepo.clearMagiskLogs()
|
||||
.ignoreElement()
|
||||
.doOnComplete(callback)
|
||||
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
|
||||
.add()
|
||||
@@ -115,8 +114,7 @@ class LogViewModel(
|
||||
.toList()
|
||||
|
||||
private fun fetchMagiskLog() = logRepo.fetchMagiskLogs()
|
||||
.flattenAsFlowable { it }
|
||||
.map { ConsoleRvItem(it) }
|
||||
.toList()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class ModulesFragment : BaseFragment<ModuleViewModel, FragmentModulesBinding>()
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
|
||||
// Get the URI of the selected file
|
||||
val intent = activity.intent(FlashActivity::class.java)
|
||||
val intent = activity.intent<FlashActivity>()
|
||||
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@@ -19,13 +19,11 @@ import com.topjohnwu.magisk.data.database.RepoDao
|
||||
import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogCustomNameBinding
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.toLangTag
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.observer.Observer
|
||||
import com.topjohnwu.magisk.utils.*
|
||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Completable
|
||||
import org.koin.android.ext.android.inject
|
||||
@@ -64,8 +62,8 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
multiuserConfig = findPreference(Config.Key.SU_MULTIUSER_MODE)!!
|
||||
nsConfig = findPreference(Config.Key.SU_MNT_NS)!!
|
||||
val reauth = findPreference<SwitchPreferenceCompat>(Config.Key.SU_REAUTH)!!
|
||||
val fingerprint = findPreference<SwitchPreferenceCompat>(Config.Key.SU_FINGERPRINT)!!
|
||||
val generalCatagory = findPreference<PreferenceCategory>("general")!!
|
||||
val biometric = findPreference<SwitchPreferenceCompat>(Config.Key.SU_BIOMETRIC)!!
|
||||
val generalCategory = findPreference<PreferenceCategory>("general")!!
|
||||
val magiskCategory = findPreference<PreferenceCategory>("magisk")!!
|
||||
val suCategory = findPreference<PreferenceCategory>("superuser")!!
|
||||
val hideManager = findPreference<Preference>("hide")!!
|
||||
@@ -91,16 +89,16 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
suCategory.removePreference(reauth)
|
||||
}
|
||||
|
||||
// Disable fingerprint option if not possible
|
||||
if (!FingerprintHelper.canUseFingerprint()) {
|
||||
fingerprint.isEnabled = false
|
||||
fingerprint.isChecked = false
|
||||
fingerprint.setSummary(R.string.disable_fingerprint)
|
||||
// Disable biometric option if not possible
|
||||
if (!BiometricHelper.isSupported) {
|
||||
biometric.isEnabled = false
|
||||
biometric.isChecked = false
|
||||
biometric.setSummary(R.string.no_biometric)
|
||||
}
|
||||
|
||||
if (Const.USER_ID == 0 && Info.isConnected.value && Shell.rootAccess()) {
|
||||
if (Const.USER_ID == 0 && Info.isConnected.value && Info.env.isActive) {
|
||||
if (activity.packageName == BuildConfig.APPLICATION_ID) {
|
||||
generalCatagory.removePreference(restoreManager)
|
||||
generalCategory.removePreference(restoreManager)
|
||||
hideManager.setOnPreferenceClickListener {
|
||||
showManagerNameDialog {
|
||||
PatchAPK.hideManager(requireContext(), it)
|
||||
@@ -108,7 +106,7 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
generalCatagory.removePreference(hideManager)
|
||||
generalCategory.removePreference(hideManager)
|
||||
restoreManager.setOnPreferenceClickListener {
|
||||
DownloadService(requireContext()) {
|
||||
subject = DownloadSubject.Manager(Configuration.APK.Restore)
|
||||
@@ -118,25 +116,32 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
}
|
||||
} else {
|
||||
// Remove if not primary user, no connection, or no root
|
||||
generalCatagory.removePreference(restoreManager)
|
||||
generalCatagory.removePreference(hideManager)
|
||||
generalCategory.removePreference(restoreManager)
|
||||
generalCategory.removePreference(hideManager)
|
||||
}
|
||||
|
||||
if (!Utils.showSuperUser()) {
|
||||
preferenceScreen.removePreference(suCategory)
|
||||
}
|
||||
|
||||
if (!Shell.rootAccess()) {
|
||||
if (!Info.env.isActive) {
|
||||
preferenceScreen.removePreference(magiskCategory)
|
||||
generalCatagory.removePreference(hideManager)
|
||||
generalCategory.removePreference(hideManager)
|
||||
}
|
||||
|
||||
findPreference<Preference>("clear")?.setOnPreferenceClickListener {
|
||||
Completable.fromAction { repoDB.clear() }.subscribeK {
|
||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
||||
findPreference<Preference>("clear")?.also {
|
||||
if (Info.env.isActive) {
|
||||
it.setOnPreferenceClickListener {
|
||||
Completable.fromAction { repoDB.clear() }.subscribeK {
|
||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
||||
}
|
||||
true
|
||||
}
|
||||
} else {
|
||||
generalCategory.removePreference(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>("hosts")?.setOnPreferenceClickListener {
|
||||
Shell.su("add_hosts_module").submit {
|
||||
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
@@ -146,20 +151,21 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
|
||||
findPreference<Preference>(Config.Key.DOWNLOAD_PATH)?.apply {
|
||||
summary = Config.downloadPath
|
||||
}?.setOnPreferenceClickListener { preference ->
|
||||
activity.withExternalRW {
|
||||
onSuccess {
|
||||
showDownloadDialog {
|
||||
Config.downloadPath = it
|
||||
preference.summary = it
|
||||
setOnPreferenceClickListener { pref ->
|
||||
activity.withExternalRW {
|
||||
onSuccess {
|
||||
showDownloadDialog {
|
||||
Config.downloadPath = it
|
||||
pref.summary = it
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
updateChannel.setOnPreferenceChangeListener { _, value ->
|
||||
val channel = Integer.parseInt(value as String)
|
||||
val channel = value.toString().toInt()
|
||||
val previous = Config.updateChannel
|
||||
|
||||
if (channel == Config.Value.CUSTOM_CHANNEL) {
|
||||
@@ -201,7 +207,7 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
Shell.su("magiskhide --disable").submit()
|
||||
}
|
||||
Config.Key.LOCALE -> {
|
||||
ResourceMgr.reload()
|
||||
refreshLocale()
|
||||
activity.recreate()
|
||||
}
|
||||
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity)
|
||||
@@ -211,13 +217,13 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
when (preference.key) {
|
||||
Config.Key.SU_FINGERPRINT -> {
|
||||
Config.Key.SU_BIOMETRIC -> {
|
||||
val checked = (preference as SwitchPreferenceCompat).isChecked
|
||||
preference.isChecked = !checked
|
||||
FingerprintAuthDialog(requireActivity()) {
|
||||
BiometricHelper.authenticate(requireActivity()) {
|
||||
preference.isChecked = checked
|
||||
Config.suFingerprint = checked
|
||||
}.show()
|
||||
Config.suBiometric = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -225,22 +231,7 @@ class SettingsFragment : BasePreferenceFragment() {
|
||||
|
||||
private fun setLocalePreference(lp: ListPreference) {
|
||||
lp.isEnabled = false
|
||||
availableLocales.map {
|
||||
val names = mutableListOf<String>()
|
||||
val values = mutableListOf<String>()
|
||||
|
||||
names.add(
|
||||
ResourceMgr.getString(defaultLocale, R.string.system_default)
|
||||
)
|
||||
values.add("")
|
||||
|
||||
it.forEach { locale ->
|
||||
names.add(locale.getDisplayName(locale))
|
||||
values.add(locale.toLangTag())
|
||||
}
|
||||
|
||||
Pair(names.toTypedArray(), values.toTypedArray())
|
||||
}.subscribeK { (names, values) ->
|
||||
availableLocales.subscribeK { (names, values) ->
|
||||
lp.isEnabled = true
|
||||
lp.entries = names
|
||||
lp.entryValues = values
|
||||
|
||||
@@ -15,11 +15,10 @@ import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
@@ -42,6 +41,11 @@ class SuperuserViewModel(
|
||||
|
||||
init {
|
||||
rxBus.register<PolicyEnableEvent>()
|
||||
.filter {
|
||||
val isIgnored = it.item == ignoreNext
|
||||
if (isIgnored) ignoreNext = null
|
||||
!isIgnored
|
||||
}
|
||||
.subscribeK { togglePolicy(it.item, it.enable) }
|
||||
.add()
|
||||
rxBus.register<PolicyUpdateEvent>()
|
||||
@@ -78,8 +82,8 @@ class SuperuserViewModel(
|
||||
.add()
|
||||
|
||||
withView {
|
||||
if (FingerprintHelper.useFingerprint()) {
|
||||
FingerprintAuthDialog(this) { updateState() }.show()
|
||||
if (BiometricHelper.isEnabled) {
|
||||
BiometricHelper.authenticate(this) { updateState() }
|
||||
} else {
|
||||
CustomAlertDialog(this)
|
||||
.setTitle(R.string.su_revoke_title)
|
||||
@@ -126,12 +130,12 @@ class SuperuserViewModel(
|
||||
.add()
|
||||
}
|
||||
|
||||
if (FingerprintHelper.useFingerprint()) {
|
||||
if (BiometricHelper.isEnabled) {
|
||||
withView {
|
||||
FingerprintAuthDialog(this, { updateState() }, {
|
||||
BiometricHelper.authenticate(this, onError = {
|
||||
ignoreNext = item
|
||||
item.isEnabled.toggle()
|
||||
}).show()
|
||||
}) { updateState() }
|
||||
}
|
||||
} else {
|
||||
updateState()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -7,11 +8,11 @@ import android.view.Window
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.base.BaseActivity
|
||||
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.events.DieEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.utils.SuLogger
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.utils.SuHandler.REQUEST
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
||||
@@ -21,7 +22,7 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB
|
||||
override val viewModel: SuRequestViewModel by viewModel()
|
||||
|
||||
override fun onBackPressed() {
|
||||
viewModel.handler?.handleAction(MagiskPolicy.DENY, -1)
|
||||
viewModel.denyPressed()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -29,24 +30,34 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB
|
||||
lockOrientation()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intent = intent
|
||||
|
||||
when (intent?.action) {
|
||||
GeneralReceiver.REQUEST -> {
|
||||
if (!viewModel.handleRequest(intent))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
GeneralReceiver.LOG -> SuLogger.handleLogs(this, intent)
|
||||
GeneralReceiver.NOTIFY -> SuLogger.handleNotify(this, intent)
|
||||
fun showRequest() {
|
||||
if (!viewModel.handleRequest(intent))
|
||||
finish()
|
||||
}
|
||||
|
||||
finish()
|
||||
fun runHandler(action: String?) {
|
||||
SuHandler(this, action, intent.extras)
|
||||
finish()
|
||||
}
|
||||
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val action = intent.getStringExtra("action")
|
||||
if (action == REQUEST) {
|
||||
showRequest()
|
||||
} else {
|
||||
runHandler(action)
|
||||
}
|
||||
} else if (intent.action == REQUEST) {
|
||||
showRequest()
|
||||
} else {
|
||||
runHandler(intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) {
|
||||
super.onEventDispatched(event)
|
||||
when (event) {
|
||||
is ViewActionEvent -> event.action(this)
|
||||
is DieEvent -> finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.CountDownTimer
|
||||
import android.text.TextUtils
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.model.events.DieEvent
|
||||
import com.topjohnwu.magisk.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.magisk.utils.SuConnector
|
||||
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
|
||||
@@ -45,7 +42,6 @@ class SuRequestViewModel(
|
||||
val denyText = KObservableField(resources.getString(R.string.deny))
|
||||
val warningText = KObservableField<CharSequence>(resources.getString(R.string.su_warning))
|
||||
|
||||
val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint())
|
||||
val selectedItemPosition = KObservableField(0)
|
||||
|
||||
private val items = DiffObservableList(ComparableRvItem.callback)
|
||||
@@ -58,48 +54,33 @@ class SuRequestViewModel(
|
||||
setItems(items)
|
||||
}
|
||||
|
||||
private val cancelTasks = mutableListOf<() -> Unit>()
|
||||
|
||||
var handler: ActionHandler? = null
|
||||
private var timer: CountDownTimer? = null
|
||||
private var policy: MagiskPolicy? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updatePolicy(value)
|
||||
}
|
||||
|
||||
init {
|
||||
resources.getStringArray(R.array.allow_timeout)
|
||||
.map { SpinnerRvItem(it) }
|
||||
.let { items.update(it) }
|
||||
|
||||
selectedItemPosition.addOnPropertyChangedCallback {
|
||||
Timber.e("Changed position to $it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePolicy(policy: MagiskPolicy?) {
|
||||
policy ?: return
|
||||
|
||||
icon.value = policy.applicationInfo.loadIcon(packageManager)
|
||||
title.value = policy.appName
|
||||
packageName.value = policy.packageName
|
||||
|
||||
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0)
|
||||
}
|
||||
private lateinit var timer: CountDownTimer
|
||||
private lateinit var policy: MagiskPolicy
|
||||
private lateinit var connector: SuConnector
|
||||
|
||||
private fun cancelTimer() {
|
||||
timer?.cancel()
|
||||
timer.cancel()
|
||||
denyText.value = resources.getString(R.string.deny)
|
||||
}
|
||||
|
||||
fun grantPressed() {
|
||||
handler?.handleAction(MagiskPolicy.ALLOW)
|
||||
timer?.cancel()
|
||||
cancelTimer()
|
||||
if (BiometricHelper.isEnabled) {
|
||||
withView {
|
||||
BiometricHelper.authenticate(this) {
|
||||
handleAction(MagiskPolicy.ALLOW)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleAction(MagiskPolicy.ALLOW)
|
||||
}
|
||||
}
|
||||
|
||||
fun denyPressed() {
|
||||
handler?.handleAction(MagiskPolicy.DENY)
|
||||
timer?.cancel()
|
||||
handleAction(MagiskPolicy.DENY)
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
fun spinnerTouched(): Boolean {
|
||||
@@ -110,75 +91,27 @@ class SuRequestViewModel(
|
||||
fun handleRequest(intent: Intent): Boolean {
|
||||
val socketName = intent.getStringExtra("socket") ?: return false
|
||||
|
||||
val connector: SuConnector
|
||||
try {
|
||||
connector = object : SuConnector(socketName) {
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse() {
|
||||
out.writeInt(policy?.policy ?: return)
|
||||
}
|
||||
}
|
||||
val bundle = connector.readSocketInput()
|
||||
val uid = bundle.getString("uid")?.toIntOrNull() ?: return false
|
||||
policyDB.deleteOutdated().blockingGet() // wrong!
|
||||
policy = runCatching { policyDB.fetch(uid).blockingGet() }
|
||||
.getOrDefault(uid.toPolicy(packageManager))
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
connector = Connector(socketName)
|
||||
val map = connector.readRequest()
|
||||
val uid = map["uid"]?.toIntOrNull() ?: return false
|
||||
policy = uid.toPolicy(packageManager)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
handler = object : ActionHandler() {
|
||||
override fun handleAction() {
|
||||
connector.response()
|
||||
done()
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun handleAction(action: Int) {
|
||||
val pos = selectedItemPosition.value
|
||||
timeoutPrefs.edit().putInt(policy?.packageName, pos).commit()
|
||||
handleAction(action, Config.Value.TIMEOUT_LIST[pos])
|
||||
}
|
||||
|
||||
override fun handleAction(action: Int, time: Int) {
|
||||
val until = if (time >= 0) {
|
||||
if (time == 0) {
|
||||
0
|
||||
} else {
|
||||
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
|
||||
}
|
||||
} else {
|
||||
policy?.until ?: 0
|
||||
}
|
||||
policy = policy?.copy(policy = action, until = until)?.apply {
|
||||
policyDB.update(this).blockingGet()
|
||||
}
|
||||
|
||||
handleAction()
|
||||
}
|
||||
}
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (TextUtils.equals(policy?.packageName, BuildConfig.APPLICATION_ID))
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
||||
return false
|
||||
|
||||
// If not interactive, response directly
|
||||
if (policy?.policy != MagiskPolicy.INTERACTIVE) {
|
||||
handler?.handleAction()
|
||||
return true
|
||||
}
|
||||
|
||||
when (Config.suAutoReponse) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
handler?.handleAction(MagiskPolicy.DENY, 0)
|
||||
handleAction(MagiskPolicy.DENY, 0)
|
||||
return true
|
||||
}
|
||||
Config.Value.SU_AUTO_ALLOW -> {
|
||||
handler?.handleAction(MagiskPolicy.ALLOW, 0)
|
||||
handleAction(MagiskPolicy.ALLOW, 0)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -187,78 +120,65 @@ class SuRequestViewModel(
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun showUI() {
|
||||
resources.getStringArray(R.array.allow_timeout)
|
||||
.map { SpinnerRvItem(it) }
|
||||
.let { items.update(it) }
|
||||
|
||||
icon.value = policy.applicationInfo.loadIcon(packageManager)
|
||||
title.value = policy.appName
|
||||
packageName.value = policy.packageName
|
||||
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0)
|
||||
|
||||
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
|
||||
timer = object : CountDownTimer(millis, 1000) {
|
||||
override fun onTick(remains: Long) {
|
||||
denyText.value = "%s (%d)"
|
||||
.format(resources.getString(R.string.deny), remains / 1000)
|
||||
denyText.value = "${resources.getString(R.string.deny)} (${remains / 1000})"
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
denyText.value = resources.getString(R.string.deny)
|
||||
handler?.handleAction(MagiskPolicy.DENY)
|
||||
handleAction(MagiskPolicy.DENY)
|
||||
}
|
||||
}
|
||||
timer?.start()
|
||||
handler?.addCancel(Runnable { cancelTimer() })
|
||||
|
||||
val useFP = canUseFingerprint.value
|
||||
|
||||
if (useFP)
|
||||
try {
|
||||
val helper = SuFingerprint()
|
||||
helper.authenticate()
|
||||
handler?.addCancel(Runnable { helper.cancel() })
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
timer.start()
|
||||
cancelTasks.add { cancelTimer() }
|
||||
}
|
||||
|
||||
private inner class SuFingerprint @Throws(Exception::class)
|
||||
internal constructor() : FingerprintHelper() {
|
||||
private fun handleAction() {
|
||||
connector.response()
|
||||
cancelTasks.forEach { it() }
|
||||
DieEvent().publish()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
warningText.value = errString
|
||||
}
|
||||
private fun handleAction(action: Int) {
|
||||
val pos = selectedItemPosition.value
|
||||
timeoutPrefs.edit().putInt(policy.packageName, pos).apply()
|
||||
handleAction(action, Config.Value.TIMEOUT_LIST[pos])
|
||||
}
|
||||
|
||||
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
|
||||
warningText.value = helpString
|
||||
}
|
||||
private fun handleAction(action: Int, time: Int) {
|
||||
val until = if (time > 0)
|
||||
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
|
||||
else
|
||||
time.toLong()
|
||||
|
||||
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
|
||||
handler?.handleAction(MagiskPolicy.ALLOW)
|
||||
}
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
warningText.value = resources.getString(R.string.auth_fail)
|
||||
if (until >= 0)
|
||||
policyDB.update(policy).blockingAwait()
|
||||
|
||||
handleAction()
|
||||
}
|
||||
|
||||
private inner class Connector @Throws(Exception::class)
|
||||
internal constructor(name: String) : SuConnector(name) {
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse() {
|
||||
out.writeInt(policy.policy)
|
||||
}
|
||||
}
|
||||
|
||||
open inner class ActionHandler {
|
||||
private val cancelTasks = mutableListOf<Runnable>()
|
||||
|
||||
internal open fun handleAction() {
|
||||
done()
|
||||
}
|
||||
|
||||
internal open fun handleAction(action: Int) {
|
||||
done()
|
||||
}
|
||||
|
||||
internal open fun handleAction(action: Int, time: Int) {
|
||||
done()
|
||||
}
|
||||
|
||||
internal fun addCancel(r: Runnable) {
|
||||
cancelTasks.add(r)
|
||||
}
|
||||
|
||||
internal fun done() {
|
||||
cancelTasks.forEach { it.run() }
|
||||
DieEvent().publish()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
|
||||
object BiometricHelper: KoinComponent {
|
||||
|
||||
private val mgr by lazy { BiometricManager.from(get()) }
|
||||
|
||||
val isSupported get() = when (mgr.canAuthenticate()) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isEnabled: Boolean get() {
|
||||
val enabled = Config.suBiometric
|
||||
if (enabled && !isSupported) {
|
||||
Config.suBiometric = false
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
activity: FragmentActivity,
|
||||
onError: () -> Unit = {},
|
||||
onSuccess: () -> Unit): BiometricPrompt {
|
||||
val prompt = BiometricPrompt(activity,
|
||||
ContextCompat.getMainExecutor(activity),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
)
|
||||
val info = BiometricPrompt.PromptInfo.Builder()
|
||||
.setConfirmationRequired(true)
|
||||
.setDeviceCredentialAllowed(false)
|
||||
.setTitle(activity.getString(R.string.authenticate))
|
||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
||||
.build()
|
||||
prompt.authenticate(info)
|
||||
return prompt
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
abstract class FingerprintHelper @Throws(Exception::class)
|
||||
protected constructor() {
|
||||
|
||||
private val manager: FingerprintManager?
|
||||
private val cipher: Cipher
|
||||
private var cancel: CancellationSignal? = null
|
||||
private val context: Context by inject()
|
||||
|
||||
init {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
manager = context.getSystemService(FingerprintManager::class.java)
|
||||
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
|
||||
+ KeyProperties.BLOCK_MODE_CBC + "/"
|
||||
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
keyStore.load(null)
|
||||
var key = keyStore.getKey(SU_KEYSTORE_KEY, null) as SecretKey? ?: generateKey()
|
||||
runCatching {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||
}.onFailure {
|
||||
// Only happens on Marshmallow
|
||||
key = generateKey()
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
||||
|
||||
abstract fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence)
|
||||
|
||||
abstract fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult)
|
||||
|
||||
abstract fun onAuthenticationFailed()
|
||||
|
||||
fun authenticate() {
|
||||
cancel = CancellationSignal()
|
||||
val cryptoObject = FingerprintManager.CryptoObject(cipher)
|
||||
manager!!.authenticate(cryptoObject, cancel, 0, Callback(), null)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (cancel != null)
|
||||
cancel!!.cancel()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun generateKey(): SecretKey {
|
||||
val keygen = KeyGenerator
|
||||
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
|
||||
val builder = KeyGenParameterSpec.Builder(
|
||||
SU_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
builder.setInvalidatedByBiometricEnrollment(false)
|
||||
}
|
||||
keygen.init(builder.build())
|
||||
return keygen.generateKey()
|
||||
}
|
||||
|
||||
private inner class Callback : FingerprintManager.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
this@FingerprintHelper.onAuthenticationError(errorCode, errString)
|
||||
}
|
||||
|
||||
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
|
||||
this@FingerprintHelper.onAuthenticationHelp(helpCode, helpString)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
|
||||
this@FingerprintHelper.onAuthenticationSucceeded(result)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
this@FingerprintHelper.onAuthenticationFailed()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SU_KEYSTORE_KEY = "su_key"
|
||||
|
||||
fun useFingerprint(): Boolean {
|
||||
var fp = Config.suFingerprint
|
||||
if (fp && !canUseFingerprint()) {
|
||||
Config.suFingerprint = false
|
||||
fp = false
|
||||
}
|
||||
return fp
|
||||
}
|
||||
|
||||
fun canUseFingerprint(context: Context = get()): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||
return false
|
||||
val km = context.getSystemService(KeyguardManager::class.java)
|
||||
val fm = context.getSystemService(FingerprintManager::class.java)
|
||||
return km?.isKeyguardSecure ?: false &&
|
||||
fm != null && fm.isHardwareDetected && fm.hasEnrolledFingerprints()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.utils.PatchAPK.ALPHANUM
|
||||
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||
import com.topjohnwu.superuser.internal.InternalUtils
|
||||
@@ -38,7 +39,6 @@ class Keygen: CertKeyProvider {
|
||||
private const val ALIAS = "magisk"
|
||||
private val PASSWORD get() = "magisk".toCharArray()
|
||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
}
|
||||
|
||||
@@ -88,6 +88,17 @@ class Keygen: CertKeyProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun randomString(): String {
|
||||
val rand = kotlin.random.Random.Default
|
||||
val len = rand.nextInt(5, 10)
|
||||
val sb = StringBuilder(len)
|
||||
for (i in 0..len) {
|
||||
val idx = rand.nextInt(ALPHANUM.length)
|
||||
sb.append(ALPHANUM[idx])
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun init(): KeyStore {
|
||||
GlobalContext.getOrNull() ?: {
|
||||
// Invoked externally, do some basic initialization
|
||||
@@ -113,7 +124,7 @@ class Keygen: CertKeyProvider {
|
||||
|
||||
// Generate new private key and certificate
|
||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||
val dname = X500Name(DNAME)
|
||||
val dname = X500Name("CN=${randomString()}")
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val signer = JcaContentSignerBuilder("SHA256WithRSA").build(kp.private)
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.ResourceMgr
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.extensions.toLangTag
|
||||
import io.reactivex.Single
|
||||
import java.util.*
|
||||
import kotlin.Comparator
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
var currentLocale: Locale = Locale.getDefault()
|
||||
|
||||
@SuppressLint("ConstantLocale")
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val availableLocales = Single.fromCallable {
|
||||
val compareId = R.string.app_changelog
|
||||
mutableListOf<Locale>().apply {
|
||||
val config = ResourceMgr.resource.configuration
|
||||
val metrics = ResourceMgr.resource.displayMetrics
|
||||
val res = Resources(ResourceMgr.resource.assets, metrics, config)
|
||||
|
||||
val locales = mutableListOf<Locale>().apply {
|
||||
// Add default locale
|
||||
add(Locale.ENGLISH)
|
||||
|
||||
@@ -26,24 +34,56 @@ val availableLocales = Single.fromCallable {
|
||||
add(Locale.TAIWAN)
|
||||
add(Locale("pt", "BR"))
|
||||
|
||||
val config = Configuration()
|
||||
val metrics = ResourceMgr.resource.displayMetrics
|
||||
val res = Resources(ResourceMgr.resource.assets, metrics, config)
|
||||
|
||||
// Other locales
|
||||
val otherLocales = ResourceMgr.resource.assets.locales
|
||||
.map { it.langTagToLocale() }
|
||||
.distinctBy {
|
||||
config.setLocale(it)
|
||||
res.updateConfiguration(config, metrics)
|
||||
res.getString(compareId)
|
||||
}
|
||||
|
||||
listOf("", "").toTypedArray()
|
||||
.map { it.langTagToLocale() }
|
||||
.distinctBy {
|
||||
config.setLocale(it)
|
||||
res.updateConfiguration(config, metrics)
|
||||
res.getString(compareId)
|
||||
}
|
||||
|
||||
addAll(otherLocales)
|
||||
}.sortedWith(Comparator { a, b ->
|
||||
a.getDisplayName(a).toLowerCase(a)
|
||||
.compareTo(b.getDisplayName(b).toLowerCase(b))
|
||||
.compareTo(b.getDisplayName(b).toLowerCase(b))
|
||||
})
|
||||
|
||||
config.setLocale(defaultLocale)
|
||||
res.updateConfiguration(config, metrics)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
// Restore back to current locale
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, metrics)
|
||||
|
||||
Pair(locales, defName)
|
||||
}.map { (locales, defName) ->
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
val values = ArrayList<String>(locales.size + 1)
|
||||
|
||||
names.add(defName)
|
||||
values.add("")
|
||||
|
||||
locales.forEach { locale ->
|
||||
names.add(locale.getDisplayName(locale))
|
||||
values.add(locale.toLangTag())
|
||||
}
|
||||
|
||||
Pair(names.toTypedArray(), values.toTypedArray())
|
||||
}.cache()!!
|
||||
|
||||
fun Resources.updateConfig(config: Configuration = configuration) {
|
||||
config.setLocale(currentLocale)
|
||||
updateConfiguration(config, displayMetrics)
|
||||
}
|
||||
|
||||
fun refreshLocale() {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
localeConfig.isEmpty() -> defaultLocale
|
||||
else -> localeConfig.langTagToLocale()
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
ResourceMgr.resource.updateConfig()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ object PatchAPK {
|
||||
private val UPPERALPHA = LOWERALPHA.toUpperCase()
|
||||
private val ALPHA = LOWERALPHA + UPPERALPHA
|
||||
private const val DIGITS = "0123456789"
|
||||
private val ALPHANUM = ALPHA + DIGITS
|
||||
val ALPHANUM = ALPHA + DIGITS
|
||||
private val ALPHANUMDOTS = "$ALPHANUM............"
|
||||
|
||||
private fun genPackageName(prefix: String, length: Int): String {
|
||||
@@ -77,8 +77,9 @@ object PatchAPK {
|
||||
}
|
||||
|
||||
private fun patchAndHide(context: Context, label: String): Boolean {
|
||||
// If not running as stub, and we are compatible with stub, use stub
|
||||
val src = if (!isRunningAsStub && SDK_INT >= 28 && Info.env.connectionMode == 3) {
|
||||
val src = if (!isRunningAsStub && SDK_INT >= 28 &&
|
||||
Info.env.magiskVersionCode >= Const.Version.PROVIDER_CONNECT) {
|
||||
// If not running as stub, and we are compatible with stub, use stub
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
val svc = get<GithubRawServices>()
|
||||
runCatching {
|
||||
@@ -118,7 +119,7 @@ object PatchAPK {
|
||||
@JvmOverloads
|
||||
fun patch(apk: String, out: String, pkg: String, label: String = "Manager"): Boolean {
|
||||
try {
|
||||
val jar = JarMap(apk)
|
||||
val jar = JarMap.open(apk)
|
||||
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
||||
val xml = jar.getRawData(je)
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import androidx.collection.ArrayMap
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
|
||||
abstract class SuConnector @Throws(IOException::class)
|
||||
protected constructor(name: String) {
|
||||
@@ -21,24 +19,23 @@ protected constructor(name: String) {
|
||||
input = DataInputStream(BufferedInputStream(socket.inputStream))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readString(): String {
|
||||
val len = input.readInt()
|
||||
val buf = ByteArray(len)
|
||||
input.readFully(buf)
|
||||
return String(buf, Charset.forName("UTF-8"))
|
||||
return String(buf, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readSocketInput(): Bundle {
|
||||
val bundle = Bundle()
|
||||
fun readRequest(): Map<String, String> {
|
||||
val ret = ArrayMap<String, String>()
|
||||
while (true) {
|
||||
val name = readString()
|
||||
if (TextUtils.equals(name, "eof"))
|
||||
if (name == "eof")
|
||||
break
|
||||
bundle.putString(name, readString())
|
||||
ret[name] = readString()
|
||||
}
|
||||
return bundle
|
||||
return ret
|
||||
}
|
||||
|
||||
fun response() {
|
||||
|
||||
135
app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt
Normal file
135
app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.*
|
||||
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.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
|
||||
object SuHandler : ProviderCallHandler {
|
||||
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
|
||||
override fun call(context: Context, method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
invoke(context.wrap(), method, extras)
|
||||
return null
|
||||
}
|
||||
|
||||
operator fun invoke(context: Context, action: String?, data: Bundle?) {
|
||||
data ?: return
|
||||
|
||||
// Debug messages
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(action)
|
||||
data.let { bundle ->
|
||||
bundle.keySet().forEach {
|
||||
Timber.d("[%s]=[%s]", it, bundle[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (action) {
|
||||
REQUEST -> {
|
||||
val intent = context.intent<SuRequestActivity>()
|
||||
.setAction(action)
|
||||
.putExtras(data)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
// Android Q does not allow starting activity from background
|
||||
intent.startActivityWithRoot()
|
||||
} else {
|
||||
intent.startActivity(context)
|
||||
}
|
||||
}
|
||||
LOG -> handleLogs(context, data)
|
||||
NOTIFY -> handleNotify(context, data)
|
||||
TEST -> {
|
||||
val mode = data.getInt("mode", 2)
|
||||
Shell.su(
|
||||
"magisk --connect-mode $mode",
|
||||
"magisk --use-broadcast"
|
||||
).submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any?.toInt(): Int? {
|
||||
return when (this) {
|
||||
is Int -> this
|
||||
is Long -> this.toInt()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLogs(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val notify = data.getBoolean("notify", true)
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return }
|
||||
|
||||
if (notify)
|
||||
notify(context, policy)
|
||||
|
||||
val toUid = data["to.uid"].toInt() ?: return
|
||||
val pid = data["pid"].toInt() ?: return
|
||||
|
||||
val command = data.getString("command") ?: return
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command
|
||||
)
|
||||
|
||||
val logRepo = get<LogRepository>()
|
||||
logRepo.insert(log).subscribeK(onError = { Timber.e(it) })
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
|
||||
val pm = context.packageManager
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
runCatching {
|
||||
val policy = fromUid.toPolicy(pm, allow)
|
||||
if (policy.policy >= 0)
|
||||
notify(context, policy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(context: Context, policy: MagiskPolicy) {
|
||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == MagiskPolicy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import java.util.*
|
||||
|
||||
object SuLogger {
|
||||
|
||||
fun handleLogs(context: Context, intent: Intent) {
|
||||
|
||||
val fromUid = intent.getIntExtra("from.uid", -1)
|
||||
if (fromUid < 0) return
|
||||
if (fromUid == Process.myUid()) return
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val notify: Boolean
|
||||
val data = intent.extras
|
||||
val policy: MagiskPolicy = if (data!!.containsKey("notify")) {
|
||||
notify = data.getBoolean("notify")
|
||||
runCatching {
|
||||
fromUid.toPolicy(pm)
|
||||
}.getOrElse { return }
|
||||
} else {
|
||||
// Doesn't report whether notify or not, check database ourselves
|
||||
val policyDB = get<PolicyDao>()
|
||||
val policy = policyDB.fetch(fromUid).blockingGet() ?: return
|
||||
notify = policy.notification
|
||||
policy
|
||||
}.copy(policy = data.getInt("policy", -1))
|
||||
|
||||
if (policy.policy < 0)
|
||||
return
|
||||
|
||||
if (notify)
|
||||
handleNotify(context, policy)
|
||||
|
||||
val toUid = intent.getIntExtra("to.uid", -1)
|
||||
if (toUid < 0) return
|
||||
|
||||
val pid = intent.getIntExtra("pid", -1)
|
||||
if (pid < 0) return
|
||||
|
||||
val command = intent.getStringExtra("command") ?: return
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command,
|
||||
date = Date()
|
||||
)
|
||||
|
||||
val logRepo = get<LogRepository>()
|
||||
logRepo.put(log).blockingGet()?.printStackTrace()
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, policy: MagiskPolicy) {
|
||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
Utils.toast(
|
||||
context.getString(
|
||||
if (policy.policy == MagiskPolicy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast, policy.appName
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleNotify(context: Context, intent: Intent) {
|
||||
val fromUid = intent.getIntExtra("from.uid", -1)
|
||||
if (fromUid < 0) return
|
||||
if (fromUid == Process.myUid()) return
|
||||
runCatching {
|
||||
val pm = context.packageManager
|
||||
val policy = fromUid.toPolicy(pm)
|
||||
.copy(policy = intent.getIntExtra("policy", -1))
|
||||
if (policy.policy >= 0)
|
||||
handleNotify(context, policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -34,7 +33,7 @@ object Utils {
|
||||
}
|
||||
|
||||
fun showSuperUser(): Boolean {
|
||||
return Shell.rootAccess() && (Const.USER_ID == 0
|
||||
return Info.env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,53 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
|
||||
object Notifications {
|
||||
|
||||
val mgr by lazy { NotificationManagerCompat.from(get()) }
|
||||
private val icon by lazy { resolveRes(DynAPK.NOTIFICATION) }
|
||||
val mgr by lazy { get<Context>().getSystemService<NotificationManager>()!! }
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mgr.deleteNotificationChannel("magisk_notification")
|
||||
var channel = NotificationChannel(Const.ID.UPDATE_NOTIFICATION_CHANNEL,
|
||||
if (SDK_INT >= 26) {
|
||||
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
mgr.createNotificationChannel(channel)
|
||||
channel = NotificationChannel(Const.ID.PROGRESS_NOTIFICATION_CHANNEL,
|
||||
channel = NotificationChannel(PROGRESS_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
|
||||
mgr.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBuilder(context: Context): Notification.Builder {
|
||||
return Notification.Builder(context).apply {
|
||||
val bitmap = context.getBitmap(R.drawable.ic_magisk_outline)
|
||||
setLargeIcon(bitmap)
|
||||
if (SDK_INT >= 26) {
|
||||
setSmallIcon(bitmap.toIcon())
|
||||
setChannelId(UPDATE_NOTIFICATION_CHANNEL)
|
||||
} else {
|
||||
setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
setVibrate(longArrayOf(0, 100, 100, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun magiskUpdate(context: Context) {
|
||||
val intent = context.intent(SplashActivity::class.java)
|
||||
val intent = context.intent<SplashActivity>()
|
||||
.putExtra(Const.Key.OPEN_SECTION, "magisk")
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
|
||||
@@ -39,59 +55,66 @@ object Notifications {
|
||||
val pendingIntent = stackBuilder.getPendingIntent(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
|
||||
builder.setSmallIcon(icon)
|
||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setVibrate(longArrayOf(0, 100, 100, 100))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
val builder = updateBuilder(context)
|
||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
mgr.notify(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun managerUpdate(context: Context) {
|
||||
val intent = context.intent(GeneralReceiver::class.java)
|
||||
val intent = context.intent<GeneralReceiver>()
|
||||
.setAction(Const.Key.BROADCAST_MANAGER_UPDATE)
|
||||
.putExtra(Const.Key.INTENT_SET_APP, Info.remote.app)
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
|
||||
builder.setSmallIcon(icon)
|
||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setVibrate(longArrayOf(0, 100, 100, 100))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
val builder = updateBuilder(context)
|
||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun dtboPatched(context: Context) {
|
||||
val intent = context.intent(GeneralReceiver::class.java)
|
||||
val intent = context.intent<GeneralReceiver>()
|
||||
.setAction(Const.Key.BROADCAST_REBOOT)
|
||||
val pendingIntent = PendingIntent.getBroadcast(context,
|
||||
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
|
||||
builder.setSmallIcon(icon)
|
||||
.setContentTitle(context.getString(R.string.dtbo_patched_title))
|
||||
.setContentText(context.getString(R.string.dtbo_patched_reboot))
|
||||
.setVibrate(longArrayOf(0, 100, 100, 100))
|
||||
.addAction(R.drawable.ic_refresh, context.getString(R.string.reboot), pendingIntent)
|
||||
val builder = updateBuilder(context)
|
||||
.setContentTitle(context.getString(R.string.dtbo_patched_title))
|
||||
.setContentText(context.getString(R.string.dtbo_patched_reboot))
|
||||
|
||||
if (SDK_INT >= 23) {
|
||||
val action = Notification.Action.Builder(
|
||||
context.getBitmap(R.drawable.ic_refresh).toIcon(),
|
||||
context.getString(R.string.reboot), pendingIntent).build()
|
||||
builder.addAction(action)
|
||||
} else {
|
||||
builder.addAction(
|
||||
R.drawable.ic_refresh,
|
||||
context.getString(R.string.reboot), pendingIntent)
|
||||
}
|
||||
|
||||
mgr.notify(Const.ID.DTBO_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun progress(context: Context, title: CharSequence): NotificationCompat.Builder {
|
||||
val builder = NotificationCompat.Builder(context, Const.ID.PROGRESS_NOTIFICATION_CHANNEL)
|
||||
builder.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(title)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
fun progress(context: Context, title: CharSequence): Notification.Builder {
|
||||
val builder = if (SDK_INT >= 26) {
|
||||
Notification.Builder(context, PROGRESS_NOTIFICATION_CHANNEL)
|
||||
} else {
|
||||
Notification.Builder(context).setPriority(Notification.PRIORITY_LOW)
|
||||
}
|
||||
builder.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(title)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
return builder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,19 @@ import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toAdaptiveIcon
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.extensions.getBitmap
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
object Shortcuts {
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
val manager = context.getSystemService(ShortcutManager::class.java)
|
||||
val manager = context.getSystemService<ShortcutManager>()
|
||||
manager?.dynamicShortcuts = getShortCuts(context)
|
||||
}
|
||||
}
|
||||
@@ -24,49 +27,72 @@ object Shortcuts {
|
||||
@RequiresApi(api = 25)
|
||||
private fun getShortCuts(context: Context): List<ShortcutInfo> {
|
||||
val shortCuts = mutableListOf<ShortcutInfo>()
|
||||
val root = Shell.rootAccess()
|
||||
val intent = context.intent(SplashActivity::class.java)
|
||||
val intent = context.intent<SplashActivity>()
|
||||
|
||||
fun getIcon(id: Int): Icon {
|
||||
return if (Build.VERSION.SDK_INT >= 26)
|
||||
context.getBitmap(id).toAdaptiveIcon()
|
||||
else
|
||||
context.getBitmap(id).toIcon()
|
||||
}
|
||||
|
||||
if (Utils.showSuperUser()) {
|
||||
shortCuts.add(ShortcutInfo.Builder(context, "superuser")
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "superuser")
|
||||
.setShortLabel(context.getString(R.string.superuser))
|
||||
.setIntent(Intent(intent)
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "superuser")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
|
||||
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.SUPERUSER)))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_superuser))
|
||||
.setRank(0)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (root && Info.env.magiskHide) {
|
||||
shortCuts.add(ShortcutInfo.Builder(context, "magiskhide")
|
||||
if (Info.env.magiskHide) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "magiskhide")
|
||||
.setShortLabel(context.getString(R.string.magiskhide))
|
||||
.setIntent(Intent(intent)
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "magiskhide")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
|
||||
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.MAGISKHIDE)))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_magiskhide))
|
||||
.setRank(1)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (!Config.coreOnly && root && Info.env.magiskVersionCode >= 0) {
|
||||
shortCuts.add(ShortcutInfo.Builder(context, "modules")
|
||||
if (!Config.coreOnly && Info.env.isActive) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "modules")
|
||||
.setShortLabel(context.getString(R.string.modules))
|
||||
.setIntent(Intent(intent)
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "modules")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
|
||||
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.MODULES)))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_extension))
|
||||
.setRank(3)
|
||||
.build())
|
||||
shortCuts.add(ShortcutInfo.Builder(context, "downloads")
|
||||
.build()
|
||||
)
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, "downloads")
|
||||
.setShortLabel(context.getString(R.string.downloads))
|
||||
.setIntent(Intent(intent)
|
||||
.setIntent(
|
||||
Intent(intent)
|
||||
.putExtra(Const.Key.OPEN_SECTION, "downloads")
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
|
||||
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.DOWNLOAD)))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
.setIcon(getIcon(R.drawable.sc_cloud_download))
|
||||
.setRank(2)
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
return shortCuts
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package com.topjohnwu.magisk.view.dialogs
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
class FingerprintAuthDialog(activity: Activity, private val callback: () -> Unit)
|
||||
: CustomAlertDialog(activity) {
|
||||
|
||||
private var failureCallback: (() -> Unit)? = null
|
||||
private var helper: DialogFingerprintHelper? = null
|
||||
|
||||
init {
|
||||
val fingerprint = ContextCompat.getDrawable(activity, R.drawable.ic_fingerprint)
|
||||
fingerprint?.setBounds(0, 0, Utils.dpInPx(50), Utils.dpInPx(50))
|
||||
val theme = activity.theme
|
||||
val ta = theme.obtainStyledAttributes(intArrayOf(R.attr.imageColorTint))
|
||||
fingerprint?.setTint(ta.getColor(0, Color.GRAY))
|
||||
ta.recycle()
|
||||
binding.message.setCompoundDrawables(null, null, null, fingerprint)
|
||||
binding.message.compoundDrawablePadding = Utils.dpInPx(20)
|
||||
binding.message.gravity = Gravity.CENTER
|
||||
setMessage(R.string.auth_fingerprint)
|
||||
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
helper?.cancel()
|
||||
failureCallback?.invoke()
|
||||
}
|
||||
setOnCancelListener {
|
||||
helper?.cancel()
|
||||
failureCallback?.invoke()
|
||||
}
|
||||
runCatching {
|
||||
helper = DialogFingerprintHelper()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
constructor(activity: Activity, onSuccess: () -> Unit, onFailure: () -> Unit)
|
||||
: this(activity, onSuccess) {
|
||||
failureCallback = onFailure
|
||||
}
|
||||
|
||||
override fun show(): AlertDialog {
|
||||
return create().apply {
|
||||
if (helper == null) {
|
||||
dismiss()
|
||||
Utils.toast(R.string.auth_fail, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
helper?.authenticate()
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class DialogFingerprintHelper @Throws(Exception::class)
|
||||
constructor() : FingerprintHelper() {
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
binding.message.setTextColor(Color.RED)
|
||||
binding.message.text = errString
|
||||
}
|
||||
|
||||
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
|
||||
binding.message.setTextColor(Color.RED)
|
||||
binding.message.text = helpString
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
binding.message.setTextColor(Color.RED)
|
||||
binding.message.setText(R.string.auth_fail)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
|
||||
dismiss()
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user