Replace old design with redesign (p2)

This commit is contained in:
topjohnwu 2020-01-12 16:07:30 +08:00
parent df0a5b59f8
commit 9094cf7ce3
51 changed files with 22 additions and 4354 deletions

View File

@ -2,11 +2,11 @@ package a;
import android.content.Context;
import com.topjohnwu.magisk.model.update.UpdateCheckService;
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
import com.topjohnwu.magisk.model.update.UpdateCheckService;
public class g extends w<UpdateCheckService> {
/* Stub */
public g(@NonNull Context context, @NonNull WorkerParameters workerParams) {

View File

@ -18,13 +18,12 @@ 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
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.redesign.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.redesign.MainActivity as RedesignActivity
fun AssetManager.addAssetPath(path: String) {
DynAPK.addAssetPath(this, path)
@ -150,8 +149,7 @@ 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.b::class.java
ProcessPhoenix::class.java to a.r::class.java
)
operator fun get(c: Class<*>) = map.getOrElse(c) { c }

View File

@ -1,25 +1,13 @@
package com.topjohnwu.magisk.di
import android.net.Uri
import com.topjohnwu.magisk.ui.MainViewModel
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.hide.HideViewModel
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModules = module {
viewModel { MainViewModel() }
viewModel { HomeViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
viewModel { HideViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), get(), get()) }
viewModel { LogViewModel(get(), get()) }
viewModel { (action: String, file: Uri, additional: Uri) ->
FlashViewModel(action, file, additional, get())
}

View File

@ -5,20 +5,13 @@ import android.view.ViewGroup
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.startAnimations
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.ProcessHideApp
import com.topjohnwu.magisk.model.entity.StatefulProcess
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.redesign.hide.HideViewModel
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import kotlin.math.roundToInt
class HideItem(val item: ProcessHideApp) : ComparableRvItem<HideItem>() {
@ -86,81 +79,3 @@ class HideProcessItem(val item: StatefulProcess) : ComparableRvItem<HideProcessI
override fun itemSameAs(other: HideProcessItem) = item.name == other.item.name
}
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
ComparableRvItem<HideRvItem>() {
override val layoutRes: Int = R.layout.item_hide_app
val packageName = item.info.packageName.orEmpty()
val items = DiffObservableList(callback).also {
val items = item.processes.map {
val isHidden = targets.any { target ->
packageName == target.packageName && it == target.process
}
HideProcessRvItem(packageName, it, isHidden)
}
it.update(items)
}
val isHiddenState = KObservableField(currentState)
val isExpanded = KObservableField(false)
private val itemsProcess get() = items.filterIsInstance<HideProcessRvItem>()
private val currentState
get() = when (itemsProcess.count { it.isHidden.value }) {
items.size -> IndeterminateState.CHECKED
in 1 until items.size -> IndeterminateState.INDETERMINATE
else -> IndeterminateState.UNCHECKED
}
init {
itemsProcess.forEach {
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState }
}
}
fun toggle() {
val desiredState = when (isHiddenState.value) {
IndeterminateState.INDETERMINATE,
IndeterminateState.UNCHECKED -> true
IndeterminateState.CHECKED -> false
}
itemsProcess.forEach { it.isHidden.value = desiredState }
isHiddenState.value = currentState
}
fun toggleExpansion() {
if (items.size <= 1) return
isExpanded.toggle()
}
override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) }
override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info
}
class HideProcessRvItem(
val packageName: String,
val process: String,
isHidden: Boolean
) : ComparableRvItem<HideProcessRvItem>() {
override val layoutRes: Int = R.layout.item_hide_process
val isHidden = KObservableField(isHidden)
private val rxBus: RxBus by inject()
init {
this.isHidden.addOnPropertyChangedCallback {
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
}
}
fun toggle() = isHidden.toggle()
override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other)
override fun itemSameAs(other: HideProcessRvItem): Boolean =
packageName == other.packageName && process == other.process
}

View File

@ -3,79 +3,9 @@ package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.timeDateFormat
import com.topjohnwu.magisk.extensions.timeFormatMedium
import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
class LogRvItem : ComparableRvItem<LogRvItem>() {
override val layoutRes: Int = R.layout.item_page_log
val items = DiffObservableList(callback)
fun update(list: List<LogItemRvItem>) {
list.firstOrNull()?.isExpanded?.value = true
items.update(list)
}
//two of these will never be present, safe to assume it's unique
override fun contentSameAs(other: LogRvItem): Boolean = false
override fun itemSameAs(other: LogRvItem): Boolean = false
}
class LogItemRvItem(
item: WrappedMagiskLog
) : ComparableRvItem<LogItemRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log
val date = item.time.toTime(timeFormatMedium)
val items: List<ComparableRvItem<*>> = item.items.map { LogItemEntryRvItem(it) }
val isExpanded = KObservableField(false)
fun toggle() = isExpanded.toggle()
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
}
class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log_entry
val isExpanded = KObservableField(false)
fun toggle() = isExpanded.toggle()
override fun contentSameAs(other: LogItemEntryRvItem) = item == other.item
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
}
class MagiskLogRvItem : ComparableRvItem<MagiskLogRvItem>() {
override val layoutRes: Int = R.layout.item_page_magisk_log
val items = DiffObservableList(callback)
fun update(list: List<ConsoleRvItem>) {
items.update(list)
}
//two of these will never be present, safe to assume it's unique
override fun contentSameAs(other: MagiskLogRvItem): Boolean = false
override fun itemSameAs(other: MagiskLogRvItem): Boolean = false
}
// ---
class LogItem(val item: MagiskLog) : ObservableItem<LogItem>() {
@ -106,4 +36,4 @@ class LogItem(val item: MagiskLog) : ObservableItem<LogItem>() {
item.time == other.item.time &&
isTop == other.isTop &&
isBottom == other.isBottom
}
}

View File

@ -1,7 +1,5 @@
package com.topjohnwu.magisk.model.entity.recycler
import android.content.res.Resources
import androidx.annotation.StringRes
import androidx.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
@ -10,77 +8,11 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.module.Module
import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.redesign.module.ModuleViewModel
import com.topjohnwu.magisk.utils.KObservableField
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
override val layoutRes: Int = R.layout.item_module
val lastActionNotice = KObservableField("")
val isChecked = KObservableField(item.enable)
val isDeletable = KObservableField(item.remove)
init {
isChecked.addOnPropertyChangedCallback {
when (it) {
true -> {
item.enable = true
notice(R.string.disable_file_removed)
}
false -> {
item.enable = false
notice(R.string.disable_file_created)
}
}
}
isDeletable.addOnPropertyChangedCallback {
when (it) {
true -> {
item.remove = true
notice(R.string.remove_file_created)
}
false -> {
item.remove = false
notice(R.string.remove_file_deleted)
}
}
}
when {
item.updated -> notice(R.string.update_file_created)
item.remove -> notice(R.string.remove_file_created)
}
}
fun toggle() = isChecked.toggle()
fun toggleDelete() = isDeletable.toggle()
private fun notice(@StringRes info: Int) {
lastActionNotice.value = get<Resources>().getString(info)
}
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
&& item.name == other.item.name
override fun itemSameAs(other: ModuleRvItem): Boolean = item.id == other.item.id
}
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
override val layoutRes: Int = R.layout.item_repo
override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
}
object SafeModeNotice : ComparableRvItem<SafeModeNotice>() {
override val layoutRes = R.layout.item_safe_mode_notice
@ -227,4 +159,4 @@ abstract class ObservableItem<T> : ComparableRvItem<T>(), Observable {
fun notifyChange(id: Int) = list.notifyChange(this, id)
}
}

View File

@ -3,54 +3,11 @@ package com.topjohnwu.magisk.model.entity.recycler
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.redesign.superuser.SuperuserViewModel
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
override val layoutRes = R.layout.item_policy
val isExpanded = KObservableField(false)
val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW)
val shouldNotify = KObservableField(item.notification)
val shouldLog = KObservableField(item.logging)
fun toggle() = isExpanded.toggle()
private val rxBus: RxBus by inject()
private val currentStateItem
get() = item.copy(
policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
notification = shouldNotify.value,
logging = shouldLog.value
)
init {
isEnabled.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it))
}
shouldNotify.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem))
}
shouldLog.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
rxBus.post(PolicyUpdateEvent.Log(currentStateItem))
}
}
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other)
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid
}
class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyItem>() {
override val layoutRes = R.layout.item_policy_md2
@ -93,4 +50,4 @@ class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<
override fun contentSameAs(other: PolicyItem) = itemSameAs(other)
override fun itemSameAs(other: PolicyItem) = item.uid == other.item.uid
}
}

View File

@ -1,19 +1,11 @@
package com.topjohnwu.magisk.model.events
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
import com.topjohnwu.magisk.utils.RxBus
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event
sealed class PolicyUpdateEvent(val item: MagiskPolicy) : RxBus.Event {
class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item)
class Log(item: MagiskPolicy) : PolicyUpdateEvent(item)
}
class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event
data class SafetyNetResult(val responseCode: Int) : RxBus.Event
data class SafetyNetResult(val responseCode: Int) : RxBus.Event

View File

@ -10,12 +10,11 @@ import com.topjohnwu.magisk.redesign.hide.HideFragment
import com.topjohnwu.magisk.redesign.home.HomeFragment
import com.topjohnwu.magisk.redesign.install.InstallFragment
import com.topjohnwu.magisk.redesign.log.LogFragment
import com.topjohnwu.magisk.redesign.module.ModuleFragment
import com.topjohnwu.magisk.redesign.safetynet.SafetynetFragment
import com.topjohnwu.magisk.redesign.settings.SettingsFragment
import com.topjohnwu.magisk.redesign.superuser.SuperuserFragment
import com.topjohnwu.magisk.redesign.theme.ThemeFragment
import com.topjohnwu.magisk.ui.module.ModulesFragment
import com.topjohnwu.magisk.ui.module.ReposFragment
object Navigation {
@ -36,14 +35,10 @@ object Navigation {
fun modules() = MagiskNavigationEvent {
navDirections {
destination = ModulesFragment::class
destination = ModuleFragment::class
}
}
fun repos() = MagiskNavigationEvent {
navDirections { destination = ReposFragment::class }
}
fun hide() = MagiskNavigationEvent {
navDirections {
destination = HideFragment::class
@ -77,7 +72,6 @@ object Navigation {
fun fromSection(section: String) = when (section) {
"superuser" -> superuser()
"modules" -> modules()
"downloads" -> repos()
"magiskhide" -> hide()
"log" -> log()
"settings" -> settings()

View File

@ -24,12 +24,15 @@ import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.redesign.compat.CompatViewModel
import com.topjohnwu.magisk.redesign.compat.itemBindingOf
import com.topjohnwu.magisk.ui.home.MagiskState
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.BR
import kotlin.math.roundToInt
enum class MagiskState {
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
}
class HomeViewModel(
private val repoMagisk: MagiskRepository
) : CompatViewModel() {

View File

@ -7,11 +7,15 @@ import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.events.SafetyNetResult
import com.topjohnwu.magisk.model.events.UpdateSafetyNetEvent
import com.topjohnwu.magisk.redesign.compat.CompatViewModel
import com.topjohnwu.magisk.ui.home.SafetyNetState.*
import com.topjohnwu.magisk.redesign.safetynet.SafetyNetState.*
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.utils.SafetyNetHelper
enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE
}
class SafetynetViewModel(
rxBus: RxBus
) : CompatViewModel() {
@ -89,4 +93,4 @@ class SafetynetViewModel(
private var safetyNetResult = -1
}
}
}

View File

@ -1,252 +0,0 @@
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
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.ActivityMainBinding
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.log.LogFragment
import com.topjohnwu.magisk.ui.module.ModulesFragment
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 org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import kotlin.reflect.KClass
open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Navigator,
FragNavController.RootFragmentListener, FragNavController.TransactionListener {
override val layoutRes: Int = R.layout.activity_main
override val viewModel: MainViewModel by viewModel()
private val navHostId: Int = R.id.main_nav_host
private val defaultPosition: Int = 0
private val navigationController by lazy {
FragNavController(supportFragmentManager, navHostId)
}
private val isRootFragment get() =
navigationController.currentStackIndex != defaultPosition
override val baseFragments: List<KClass<out Fragment>> = listOf(
HomeFragment::class,
SuperuserFragment::class,
MagiskHideFragment::class,
ModulesFragment::class,
ReposFragment::class,
LogFragment::class,
SettingsFragment::class
)
override fun onCreate(savedInstanceState: Bundle?) {
if (!SplashActivity.DONE) {
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
initialize(defaultPosition, savedInstanceState)
}
checkHideSection()
setSupportActionBar(binding.mainInclude.mainToolbar)
viewModel.isConnected.addOnPropertyChangedCallback {
checkHideSection()
}
if (savedInstanceState == null) {
intent.getStringExtra(OPEN_SECTION)?.let {
onEventDispatched(Navigation.fromSection(it))
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigationController.onSaveInstanceState(outState)
}
override fun setTitle(title: CharSequence?) {
supportActionBar?.title = title
}
override fun setTitle(titleId: Int) {
supportActionBar?.setTitle(titleId)
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
binding.drawerLayout.closeDrawer(binding.navView)
} else {
val fragment = navigationController.currentFrag as? BaseFragment<*, *>
if (fragment?.onBackPressed() == true) {
return
}
try {
navigationController.popFragment()
} catch (e: UnsupportedOperationException) {
when {
isRootFragment -> {
val options = FragNavTransactionOptions.newBuilder()
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.build()
navigationController.switchTab(defaultPosition, options)
}
else -> super.onBackPressed()
}
}
}
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f)
is BackPressEvent -> onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(this)
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
}
}
override fun onSimpleEventDispatched(event: Int) {
super.onSimpleEventDispatched(event)
when (event) {
Navigation.Main.OPEN_NAV -> openNav()
}
}
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START)
private fun checkHideSection() {
val menu = binding.navView.menu
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) =
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
if (!options.anySet) {
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
}
}
override val numberOfRootFragments: Int get() = baseFragments.size
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
override fun onTabTransaction(fragment: Fragment?, index: Int) {
val fragmentId = when (fragment) {
is HomeFragment -> R.id.magiskFragment
is SuperuserFragment -> R.id.superuserFragment
is MagiskHideFragment -> R.id.magiskHideFragment
is ModulesFragment -> R.id.modulesFragment
is ReposFragment -> R.id.reposFragment
is LogFragment -> R.id.logFragment
is SettingsFragment -> R.id.settings
else -> return
}
binding.navView.setCheckedItem(fragmentId)
}
override fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
navigationController.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.let { if (it == -1) null else it } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { navigationController.popFragments(it) }
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
}
private fun navigateToActivity(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(this, destination)
.putExtras(event.navDirections.args)
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { startActivity(it) }
}
private fun navigateToFragment(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
-1 -> destination.newInstance()
.apply { arguments = event.navDirections.args }
.let { navigationController.pushFragment(it) }
// When it's desired that fragments of same class are put on top of one another edit this
else -> navigationController.switchTab(index)
}
}
override fun onFragmentTransaction(
fragment: Fragment?,
transactionType: FragNavController.TransactionType
) = Unit
}

View File

@ -1,29 +0,0 @@
package com.topjohnwu.magisk.ui
import android.view.MenuItem
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
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 {
when (item.itemId) {
R.id.magiskFragment -> Navigation.home()
R.id.superuserFragment -> Navigation.superuser()
R.id.magiskHideFragment -> Navigation.hide()
R.id.modulesFragment -> Navigation.modules()
R.id.reposFragment -> Navigation.repos()
R.id.logFragment -> Navigation.log()
R.id.settings -> Navigation.settings()
else -> null
}?.publish()?.let { return@navigationItemPressed true }
return false
}
}

View File

@ -1,96 +0,0 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.extensions.update
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber
class HideViewModel(
private val magiskRepo: MagiskRepository,
rxBus: RxBus
) : BaseViewModel() {
val query = KObservableField("")
val isShowSystem = KObservableField(false)
private val allItems = mutableListOf<ComparableRvItem<*>>()
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@HideViewModel)
}
init {
rxBus.register<HideProcessEvent>()
.subscribeK { toggleItem(it.item) }
.add()
isShowSystem.addOnPropertyChangedCallback { query() }
query.addOnPropertyChangedCallback { query() }
refresh()
}
fun refresh() {
// fetching this for every item is nonsensical, so we add .cache() so the response is all
// the same for every single mapped item, it only actually executes the whole thing the
// first time around.
val hideTargets = magiskRepo.fetchHideTargets().cache()
magiskRepo.fetchApps()
.flattenAsFlowable { it }
.map { HideRvItem(it, hideTargets.blockingGet()) }
.toList()
.map {
it.sortedWith(compareBy(
{ it.isHiddenState.value },
{ it.item.name.toLowerCase() },
{ it.packageName }
))
}
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this)
.subscribeK(onError = Timber::e) { items.update(it.first, it.second) }
.add()
}
private fun query() = queryRaw()
.subscribeK { items.update(it.first, it.second) }
.add()
private fun queryRaw(
showSystem: Boolean = isShowSystem.value,
query: String = this.query.value
) = allItems.toSingle()
.map { it.filterIsInstance<HideRvItem>() }
.flattenAsFlowable { it }
.filter {
it.item.name.contains(query, ignoreCase = true) ||
it.item.processes.any { it.contains(query, ignoreCase = true) }
}
.filter {
showSystem || (it.isHiddenState.value != IndeterminateState.UNCHECKED) ||
(it.item.info.flags and ApplicationInfo.FLAG_SYSTEM == 0)
}
.toList()
.map { it to items.calculateDiff(it) }
private fun toggleItem(item: HideProcessRvItem) =
magiskRepo.toggleHide(item.isHidden.value, item.packageName, item.process)
}

View File

@ -1,71 +0,0 @@
package com.topjohnwu.magisk.ui.hide
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.SearchView
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
class MagiskHideFragment : BaseFragment<HideViewModel, FragmentMagiskHideBinding>(),
SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_magisk_hide
override val viewModel: HideViewModel by viewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.magiskhide)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_magiskhide, menu)
menu.apply {
val query = viewModel.query.value
val searchItem = menu.findItem(R.id.app_search)
val searchView = searchItem.actionView as? SearchView
searchView?.run {
setOnQueryTextListener(this@MagiskHideFragment)
setQuery(query, false)
}
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView?.isIconified = false
} else {
searchItem.collapseActionView()
searchView?.isIconified = true
}
val showSystem = Config.showSystemApp
findItem(R.id.show_system).isChecked = showSystem
viewModel.isShowSystem.value = showSystem
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.show_system) {
val showSystem = !item.isChecked
item.isChecked = showSystem
Config.showSystemApp = showSystem
viewModel.isShowSystem.value = showSystem
}
return true
}
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.query.value = query.orEmpty()
return false
}
override fun onQueryTextChange(query: String?): Boolean {
viewModel.query.value = query.orEmpty()
return false
}
}

View File

@ -1,132 +0,0 @@
package com.topjohnwu.magisk.ui.home
import android.content.Context
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
import com.topjohnwu.magisk.extensions.DynamicClassLoader
import com.topjohnwu.magisk.extensions.openUrl
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.utils.SafetyNetHelper
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.*
import com.topjohnwu.superuser.Shell
import dalvik.system.DexFile
import io.reactivex.Completable
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
import java.lang.reflect.InvocationHandler
class HomeFragment : BaseFragment<HomeViewModel, FragmentMagiskBinding>(),
SafetyNetHelper.Callback {
override val layoutRes: Int = R.layout.fragment_magisk
override val viewModel: HomeViewModel by viewModel()
private val magiskRepo: MagiskRepository by inject()
private val EXT_APK by lazy { File("${activity.filesDir.parent}/snet", "snet.jar") }
private val EXT_DEX by lazy { File(EXT_APK.parent, "snet.dex") }
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenLinkEvent -> activity.openUrl(event.url)
is ManagerInstallEvent -> installManager()
is MagiskInstallEvent -> installMagisk()
is UninstallEvent -> uninstall()
is ManagerChangelogEvent -> changelogManager()
is EnvFixEvent -> fixEnv()
is UpdateSafetyNetEvent -> updateSafetyNet(false)
}
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.magisk)
}
private fun installMagisk() {
// Show Manager update first
if (Info.remote.app.versionCode > BuildConfig.VERSION_CODE) {
installManager()
return
}
MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show()
}
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
private fun uninstall() = UninstallDialog(requireActivity()).show()
private fun fixEnv() = EnvFixDialog(requireActivity()).show()
private fun changelogManager() = MarkDownWindow
.show(requireActivity(), null, resources.openRawResource(R.raw.changelog))
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
fun download() = magiskRepo.fetchSafetynet()
.map { it.byteStream().writeTo(EXT_APK) }
.subscribeK { updateSafetyNet(true) }
if (!requiresUserInput) {
download()
return
}
CustomAlertDialog(requireActivity())
.setTitle(R.string.proprietary_title)
.setMessage(R.string.proprietary_notice)
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ -> download() }
.setNegativeButton(android.R.string.no) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
.show()
}
private fun updateSafetyNet(dieOnError: Boolean) {
Completable.fromAction {
val loader = DynamicClassLoader(EXT_APK)
val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0)
// Scan through the dex and find our helper class
var helperClass: Class<*>? = null
for (className in dex.entries()) {
if (className.startsWith("x.")) {
val cls = loader.loadClass(className)
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
helperClass = cls
break
}
}
}
helperClass ?: throw Exception()
val helper = helperClass.getMethod("get",
Class::class.java, Context::class.java, Any::class.java)
.invoke(null, SafetyNetHelper::class.java, activity, this) as SafetyNetHelper
if (helper.version < Const.SNET_EXT_VER)
throw Exception()
helper.attest()
}.subscribeK(onError = {
if (dieOnError) {
viewModel.finishSafetyNetCheck(-1)
} else {
Shell.sh("rm -rf " + EXT_APK.parent).exec()
EXT_APK.parentFile?.mkdir()
downloadSafetyNet(!dieOnError)
}
})
}
}

View File

@ -1,259 +0,0 @@
package com.topjohnwu.magisk.ui.home
import android.content.pm.PackageManager
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.SafetyNetHelper
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE
}
enum class MagiskState {
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
}
enum class MagiskItem {
MANAGER, MAGISK
}
class HomeViewModel(
private val magiskRepo: MagiskRepository
) : BaseViewModel(State.LOADED) {
val hasGMS = runCatching {
get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true
}.getOrElse { false }
val isAdvancedExpanded = KObservableField(false)
val isForceEncryption = KObservableField(Info.keepEnc)
val isKeepVerity = KObservableField(Info.keepVerity)
val isRecovery = KObservableField(Info.recovery)
val magiskState = KObservableField(MagiskState.LOADING)
val magiskStateText = Observer(magiskState) {
when (magiskState.value) {
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res()
MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
MagiskState.OBSOLETE -> R.string.magisk_update_title.res()
}
}
val magiskCurrentVersion = KObservableField("")
val magiskLatestVersion = KObservableField("")
val magiskAdditionalInfo = Observer(magiskState) {
if (Config.coreOnly)
R.string.core_only_enabled.res()
else
""
}
private val _managerState = KObservableField(MagiskState.LOADING)
val managerState = Observer(_managerState, isConnected) {
if (isConnected.value) _managerState.value else MagiskState.UP_TO_DATE
}
val managerStateText = Observer(managerState) {
when (managerState.value) {
MagiskState.NOT_INSTALLED -> R.string.invalid_update_channel.res()
MagiskState.UP_TO_DATE -> R.string.manager_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
MagiskState.OBSOLETE -> R.string.manager_update_title.res()
}
}
val managerCurrentVersion = KObservableField("")
val managerLatestVersion = KObservableField("")
val managerAdditionalInfo = Observer(managerState) {
if (packageName != BuildConfig.APPLICATION_ID)
"($packageName)"
else
""
}
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text.res())
val ctsState = KObservableField(SafetyNetState.IDLE)
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
val safetyNetState = Observer(ctsState, basicIntegrityState) {
val cts = ctsState.value
val basic = basicIntegrityState.value
val states = listOf(cts, basic)
when {
states.any { it == SafetyNetState.LOADING } -> State.LOADING
states.any { it == SafetyNetState.IDLE } -> State.LOADING
else -> State.LOADED
}
}
val isActive = KObservableField(false)
private var shownDialog = false
init {
isForceEncryption.addOnPropertyChangedCallback {
Info.keepEnc = it ?: return@addOnPropertyChangedCallback
}
isKeepVerity.addOnPropertyChangedCallback {
Info.keepVerity = it ?: return@addOnPropertyChangedCallback
}
isRecovery.addOnPropertyChangedCallback {
Info.recovery = it ?: return@addOnPropertyChangedCallback
}
isConnected.addOnPropertyChangedCallback {
if (it == true) refresh(false)
}
refresh(false)
}
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish()
fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish()
fun githubPressed() = OpenLinkEvent(Const.Url.SOURCE_CODE_URL).publish()
fun xdaPressed() = OpenLinkEvent(Const.Url.XDA_THREAD).publish()
fun uninstallPressed() = UninstallEvent().publish()
fun advancedPressed() = isAdvancedExpanded.toggle()
fun installPressed(item: MagiskItem) = when (item) {
MagiskItem.MANAGER -> ManagerInstallEvent().publish()
MagiskItem.MAGISK -> MagiskInstallEvent().publish()
}
fun cardPressed(item: MagiskItem) = when (item) {
MagiskItem.MANAGER -> ManagerChangelogEvent().publish()
MagiskItem.MAGISK -> MagiskChangelogEvent().publish()
}
fun safetyNetPressed() {
ctsState.value = SafetyNetState.LOADING
basicIntegrityState.value = SafetyNetState.LOADING
safetyNetTitle.value = R.string.checking_safetyNet_status.res()
UpdateSafetyNetEvent().publish()
}
fun finishSafetyNetCheck(response: Int) = when {
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.res()
ctsState.value = if (hasCtsPassed) {
SafetyNetState.PASS
} else {
SafetyNetState.FAILED
}
basicIntegrityState.value = if (hasBasicIntegrityPassed) {
SafetyNetState.PASS
} else {
SafetyNetState.FAILED
}
}
response == -2 -> {
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
}
else -> {
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = when (response) {
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid.res()
else -> R.string.safetyNet_api_error.res()
}
}
}
@JvmOverloads
fun refresh(invalidate: Boolean = true) {
if (invalidate)
Info.envRef.invalidate()
isActive.value = Info.env.isActive
val fetchUpdate = if (isConnected.value)
magiskRepo.fetchUpdate().ignoreElement()
else
Completable.complete()
Completable.fromAction {
// Ensure value is ready
Info.env
}.andThen(fetchUpdate)
.applyViewModel(this)
.doOnSubscribeUi {
magiskState.value = MagiskState.LOADING
_managerState.value = MagiskState.LOADING
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = R.string.safetyNet_check_text.res()
}.subscribeK {
updateSelf()
ensureEnv()
refreshVersions()
}
}
private fun refreshVersions() {
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
VERSION_FMT.format(Info.env.magiskVersionString, Info.env.magiskVersionCode)
} else {
""
}
managerCurrentVersion.value = if (isRunningAsStub) MGR_VER_FMT
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, Info.stub!!.version)
else
VERSION_FMT.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
}
private fun updateSelf() {
magiskState.value = when (Info.env.magiskVersionCode) {
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED
in 1 until Info.remote.magisk.versionCode -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
magiskLatestVersion.value =
VERSION_FMT.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
_managerState.value = when (Info.remote.app.versionCode) {
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED //wrong update channel
in (BuildConfig.VERSION_CODE + 1) .. Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> {
if (Info.stub?.version ?: Int.MAX_VALUE < Info.remote.stub.versionCode)
MagiskState.OBSOLETE
else
MagiskState.UP_TO_DATE
}
}
managerLatestVersion.value = MGR_VER_FMT
.format(Info.remote.app.version, Info.remote.app.versionCode, Info.remote.stub.versionCode)
}
private fun ensureEnv() {
val invalidStates =
listOf(MagiskState.NOT_INSTALLED, MagiskState.LOADING)
// Don't bother checking env when magisk is not installed, loading or already has been shown
if (invalidStates.any { it == magiskState.value } || shownDialog) return
if (!Shell.su("env_check").exec().isSuccess) {
shownDialog = true
EnvFixEvent().publish()
}
}
companion object {
private const val VERSION_FMT = "%s (%d)"
private const val MGR_VER_FMT = "%s (%d) (%d)"
}
}

View File

@ -1,57 +0,0 @@
package com.topjohnwu.magisk.ui.log
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentLogBinding
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import org.koin.androidx.viewmodel.ext.android.viewModel
class LogFragment : BaseFragment<LogViewModel, FragmentLogBinding>() {
override val layoutRes: Int = R.layout.fragment_log
override val viewModel: LogViewModel by viewModel()
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is PageChangedEvent -> activity.invalidateOptionsMenu()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.logTabs.setupWithViewPager(binding.logContainer, true)
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
activity.setTitle(R.string.log)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_log, menu)
menu.findItem(R.id.menu_save).isVisible = viewModel.currentPage.value == 1
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> activity.withExternalRW {
onSuccess {
viewModel.saveLog()
}
}
R.id.menu_clear -> viewModel.clearLog()
R.id.menu_refresh -> viewModel.refresh()
}
return true
}
}

View File

@ -1,120 +0,0 @@
package com.topjohnwu.magisk.ui.log
import android.content.res.Resources
import com.topjohnwu.magisk.BR
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.repository.LogRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.binding.BindingAdapter
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber
import java.io.File
import java.util.*
class LogViewModel(
private val resources: Resources,
private val logRepo: LogRepository
) : BaseViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
val itemsAdapter = BindingAdapter()
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@LogViewModel)
}
val currentPage = KObservableField(0)
private val currentItem get() = items[currentPage.value]
private val logItem get() = items[0] as LogRvItem
private val magiskLogItem get() = items[1] as MagiskLogRvItem
val scrollPosition = KObservableField(0)
init {
currentPage.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
PageChangedEvent().publish()
}
items.addAll(listOf(LogRvItem(), MagiskLogRvItem()))
refresh()
}
override fun getPageTitle(position: Int, item: ComparableRvItem<*>?) = when (item) {
is LogRvItem -> resources.getString(R.string.superuser)
is MagiskLogRvItem -> resources.getString(R.string.magisk)
else -> ""
}
fun scrollDownPressed() {
scrollPosition.value = magiskLogItem.items.size - 1
}
fun refresh() {
fetchLogs().subscribeK { logItem.update(it) }
fetchMagiskLog().subscribeK { magiskLogItem.update(it) }
}
fun saveLog() {
val now = Calendar.getInstance()
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
)
val logFile = File(Config.downloadDirectory, filename)
runCatching {
logFile.createNewFile()
}.onFailure {
Timber.e(it)
return
}
Shell.su("cat ${Const.MAGISK_LOG} > $logFile").submit {
SnackbarEvent(logFile.path).publish()
}
}
fun clearLog() = when (currentItem) {
is LogRvItem -> clearLogs { refresh() }
is MagiskLogRvItem -> clearMagiskLogs { refresh() }
else -> Unit
}
private fun clearLogs(callback: () -> Unit) = logRepo.clearLogs()
.doOnSubscribeUi(callback)
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
.add()
private fun clearMagiskLogs(callback: () -> Unit) = logRepo.clearMagiskLogs()
.doOnComplete(callback)
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
.add()
private fun fetchLogs() = logRepo.fetchLogs()
.flattenAsFlowable { it }
.map { LogItemRvItem(it) }
.toList()
private fun fetchMagiskLog() = logRepo.fetchMagiskLogs()
.map { ConsoleRvItem(it) }
.toList()
}

View File

@ -1,111 +0,0 @@
package com.topjohnwu.magisk.ui.module
import android.content.res.Resources
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.module.Module
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem
import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.tasks.RepoUpdater
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.OnItemBind
class ModuleViewModel(
private val resources: Resources,
private val repoUpdater: RepoUpdater,
private val repoDB: RepoDao
) : BaseViewModel() {
val query = KObservableField("")
private val allItems = mutableListOf<ComparableRvItem<*>>()
val itemsInstalled = DiffObservableList(ComparableRvItem.callback)
val itemsRemote = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@ModuleViewModel)
}
private var queryDisposable: Disposable? = null
init {
query.addOnPropertyChangedCallback {
queryDisposable?.dispose()
queryDisposable = query()
}
refresh(false)
}
fun fabPressed() = OpenFilePickerEvent().publish()
fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish()
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
fun refresh(force: Boolean) {
Single.fromCallable { Module.loadModules() }
.flattenAsFlowable { it }
.map { ModuleRvItem(it) }
.toList()
.map { it to itemsInstalled.calculateDiff(it) }
.doOnSuccessUi { itemsInstalled.update(it.first, it.second) }
.flatMap { repoUpdater(force) }
.flattenAsFlowable { repoDB.repos }
.map { RepoRvItem(it) }
.toList()
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this)
.subscribeK { itemsRemote.update(it.first, it.second) }
}
private fun query() = queryRaw()
.subscribeK { itemsRemote.update(it.first, it.second) }
private fun queryRaw(query: String = this.query.value) = allItems.toSingle()
.map { it.filterIsInstance<RepoRvItem>() }
.flattenAsFlowable { it }
.filter {
it.item.name.contains(query, ignoreCase = true) ||
it.item.author.contains(query, ignoreCase = true) ||
it.item.description.contains(query, ignoreCase = true)
}
.toList()
.map { if (query.isEmpty()) it.divide() else it }
.map { it to itemsRemote.calculateDiff(it) }
private fun List<RepoRvItem>.divide(): List<ComparableRvItem<*>> {
val installed = itemsInstalled.filterIsInstance<ModuleRvItem>()
fun <T : ComparableRvItem<*>> List<T>.withTitle(text: Int) =
if (isEmpty()) this else listOf(SectionRvItem(resources.getString(text))) + this
val groupedItems = groupBy { repo ->
installed.firstOrNull { it.item.id == repo.item.id }?.let {
if (it.item.versionCode < repo.item.versionCode) MODULE_UPDATABLE
else MODULE_INSTALLED
} ?: MODULE_REMOTE
}
return groupedItems.getOrElse(MODULE_UPDATABLE) { listOf() }.withTitle(R.string.update_available) +
groupedItems.getOrElse(MODULE_INSTALLED) { listOf() }.withTitle(R.string.installed) +
groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed)
}
companion object {
protected const val MODULE_INSTALLED = 0
protected const val MODULE_REMOTE = 1
protected const val MODULE_UPDATABLE = 2
}
}

View File

@ -1,99 +0,0 @@
package com.topjohnwu.magisk.ui.module
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentModulesBinding
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ModulesFragment : BaseFragment<ModuleViewModel, FragmentModulesBinding>() {
override val layoutRes: Int = R.layout.fragment_modules
override val viewModel: ModuleViewModel by sharedViewModel()
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>()
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
startActivity(intent)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.modulesContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
binding.modulesRefreshLayout.isEnabled = recyclerView.getChildAt(0).top >= 0
}
})
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenFilePickerEvent -> selectFile()
}
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.modules)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_reboot, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.reboot -> {
reboot()
return true
}
R.id.reboot_recovery -> {
Shell.su("/system/bin/reboot recovery").submit()
return true
}
R.id.reboot_bootloader -> {
reboot("bootloader")
return true
}
R.id.reboot_download -> {
reboot("download")
return true
}
R.id.reboot_edl -> {
reboot("edl")
return true
}
else -> return false
}
}
private fun selectFile() {
activity.withExternalRW {
onSuccess {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
startActivityForResult(intent, Const.ID.FETCH_ZIP)
}
}
}
}

View File

@ -1,116 +0,0 @@
package com.topjohnwu.magisk.ui.module
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.SearchView
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentReposBinding
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.entity.module.Repo
import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ReposFragment : BaseFragment<ModuleViewModel, FragmentReposBinding>(),
SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_repos
override val viewModel: ModuleViewModel by sharedViewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.downloads)
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenChangelogEvent -> openChangelog(event.item)
is InstallModuleEvent -> installModule(event.item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_repo, menu)
val query = viewModel.query.value
val searchItem = menu.findItem(R.id.repo_search)
val searchView = searchItem.actionView as? SearchView
searchView?.run {
setOnQueryTextListener(this@ReposFragment)
setQuery(query, false)
}
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView?.isIconified = false
} else {
searchItem.collapseActionView()
searchView?.isIconified = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.repo_sort) {
AlertDialog.Builder(activity)
.setTitle(R.string.sorting_order)
.setSingleChoiceItems(
R.array.sorting_orders,
Config.repoOrder
) { d, which ->
Config.repoOrder = which
viewModel.refresh(false)
d.dismiss()
}.show()
}
return true
}
override fun onQueryTextSubmit(p0: String?): Boolean {
viewModel.query.value = p0.orEmpty()
return false
}
override fun onQueryTextChange(p0: String?): Boolean {
viewModel.query.value = p0.orEmpty()
return false
}
private fun openChangelog(item: Repo) {
MarkDownWindow.show(requireActivity(), null, item.readme)
}
@SuppressLint("MissingPermission")
private fun installModule(item: Repo) {
val context = activity
fun download(install: Boolean) = context.withExternalRW {
onSuccess {
DownloadService(context) {
val config = if (install) Configuration.Flash.Primary else Configuration.Download
subject = DownloadSubject.Module(item, config)
}
}
}
CustomAlertDialog(context)
.setTitle(context.getString(R.string.repo_install_title, item.name))
.setMessage(context.getString(R.string.repo_install_msg, item.downloadFilename))
.setCancelable(true)
.setPositiveButton(R.string.install) { _, _ -> download(true) }
.setNeutralButton(R.string.download) { _, _ -> download(false) }
.show()
}
}

View File

@ -1,347 +0,0 @@
package com.topjohnwu.magisk.ui.settings
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.databinding.DataBindingUtil
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreferenceCompat
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.base.BasePreferenceFragment
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.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.superuser.Shell
import io.reactivex.Completable
import org.koin.android.ext.android.inject
import java.io.File
class SettingsFragment : BasePreferenceFragment() {
private val repoDB: RepoDao by inject()
private lateinit var updateChannel: ListPreference
private lateinit var autoRes: ListPreference
private lateinit var suNotification: ListPreference
private lateinit var requestTimeout: ListPreference
private lateinit var rootConfig: ListPreference
private lateinit var multiuserConfig: ListPreference
private lateinit var nsConfig: ListPreference
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.settings)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.setStorageDeviceProtected()
setPreferencesFromResource(R.xml.app_settings, rootKey)
findPreference<PreferenceCategory>("redesign_cat")?.isVisible = BuildConfig.DEBUG
// Get preferences
updateChannel = findPreference(Config.Key.UPDATE_CHANNEL)!!
rootConfig = findPreference(Config.Key.ROOT_ACCESS)!!
autoRes = findPreference(Config.Key.SU_AUTO_RESPONSE)!!
requestTimeout = findPreference(Config.Key.SU_REQUEST_TIMEOUT)!!
suNotification = findPreference(Config.Key.SU_NOTIFICATION)!!
multiuserConfig = findPreference(Config.Key.SU_MULTIUSER_MODE)!!
nsConfig = findPreference(Config.Key.SU_MNT_NS)!!
val reauth = findPreference<SwitchPreferenceCompat>(Config.Key.SU_REAUTH)!!
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")!!
val restoreManager = findPreference<Preference>("restore")!!
// Remove/Disable entries
// Only show canary channels if user is already on canary channel
// or the user have already chosen canary channel
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) {
// Remove the last 2 entries
val entries = updateChannel.entries
updateChannel.entries = entries.copyOf(entries.size - 2)
}
// Remove dangerous settings in secondary user
if (Const.USER_ID > 0) {
suCategory.removePreference(multiuserConfig)
}
// Remove re-authentication option on Android O, it will not work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
suCategory.removePreference(reauth)
}
// 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 && Info.env.isActive) {
if (activity.packageName == BuildConfig.APPLICATION_ID) {
generalCategory.removePreference(restoreManager)
hideManager.setOnPreferenceClickListener {
showManagerNameDialog {
PatchAPK.hideManager(requireContext(), it)
}
true
}
} else {
generalCategory.removePreference(hideManager)
restoreManager.setOnPreferenceClickListener {
DownloadService(requireContext()) {
subject = DownloadSubject.Manager(Configuration.APK.Restore)
}
true
}
}
} else {
// Remove if not primary user, no connection, or no root
generalCategory.removePreference(restoreManager)
generalCategory.removePreference(hideManager)
}
if (!Utils.showSuperUser()) {
preferenceScreen.removePreference(suCategory)
}
if (!Info.env.isActive) {
preferenceScreen.removePreference(magiskCategory)
generalCategory.removePreference(hideManager)
}
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)
}
}
findPreference<Preference>("hosts")?.setOnPreferenceClickListener {
Shell.su("add_hosts_module").submit {
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
true
}
findPreference<Preference>(Config.Key.DOWNLOAD_PATH)?.apply {
summary = Config.downloadPath
setOnPreferenceClickListener { pref ->
activity.withExternalRW {
onSuccess {
showDownloadDialog {
Config.downloadPath = it
pref.summary = it
}
}
}
true
}
}
updateChannel.setOnPreferenceChangeListener { _, value ->
val channel = value.toString().toInt()
val previous = Config.updateChannel
if (channel == Config.Value.CUSTOM_CHANNEL) {
showUrlDialog(Config.customChannelUrl, {
Config.updateChannel = previous
}, {
Config.customChannelUrl = it
})
}
true
}
setLocalePreference(findPreference(Config.Key.LOCALE)!!)
setSummary()
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
fun getStrInt() = prefs.getString(key, null)?.toInt() ?: 0
when (key) {
Config.Key.ROOT_ACCESS -> Config.rootMode = getStrInt()
Config.Key.SU_MULTIUSER_MODE -> Config.suMultiuserMode = getStrInt()
Config.Key.SU_MNT_NS -> Config.suMntNamespaceMode = getStrInt()
Config.Key.DARK_THEME -> requireActivity().recreate()
Config.Key.COREONLY -> {
if (prefs.getBoolean(key, false)) {
runCatching {
Const.MAGISK_DISABLE_FILE.createNewFile()
}
} else {
Const.MAGISK_DISABLE_FILE.delete()
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG)
}
Config.Key.MAGISKHIDE -> if (prefs.getBoolean(key, false)) {
Shell.su("magiskhide --enable").submit()
} else {
Shell.su("magiskhide --disable").submit()
}
Config.Key.LOCALE -> {
refreshLocale()
activity.recreate()
}
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity)
}
setSummary(key)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
Config.Key.SU_BIOMETRIC -> {
val checked = (preference as SwitchPreferenceCompat).isChecked
preference.isChecked = !checked
BiometricHelper.authenticate(requireActivity()) {
preference.isChecked = checked
Config.suBiometric = checked
}
}
}
return true
}
private fun setLocalePreference(lp: ListPreference) {
lp.isEnabled = false
availableLocales.subscribeK { (names, values) ->
lp.isEnabled = true
lp.entries = names
lp.entryValues = values
lp.summary = currentLocale.getDisplayName(currentLocale)
}
}
private fun setSummary(key: String) {
when (key) {
Config.Key.ROOT_ACCESS -> rootConfig.summary = resources
.getStringArray(R.array.su_access)[Config.rootMode]
Config.Key.SU_MULTIUSER_MODE -> multiuserConfig.summary = resources
.getStringArray(R.array.multiuser_summary)[Config.suMultiuserMode]
Config.Key.SU_MNT_NS -> nsConfig.summary = resources
.getStringArray(R.array.namespace_summary)[Config.suMntNamespaceMode]
Config.Key.UPDATE_CHANNEL -> {
var ch = Config.updateChannel
ch = if (ch < 0) Config.Value.STABLE_CHANNEL else ch
updateChannel.summary = resources
.getStringArray(R.array.update_channel)[ch]
}
Config.Key.SU_AUTO_RESPONSE -> autoRes.summary = resources
.getStringArray(R.array.auto_response)[Config.suAutoReponse]
Config.Key.SU_NOTIFICATION -> suNotification.summary = resources
.getStringArray(R.array.su_notification)[Config.suNotification]
Config.Key.SU_REQUEST_TIMEOUT -> requestTimeout.summary =
getString(R.string.request_timeout_summary, Config.suDefaultTimeout)
}
}
private fun setSummary() {
setSummary(Config.Key.ROOT_ACCESS)
setSummary(Config.Key.SU_MULTIUSER_MODE)
setSummary(Config.Key.SU_MNT_NS)
setSummary(Config.Key.UPDATE_CHANNEL)
setSummary(Config.Key.SU_AUTO_RESPONSE)
setSummary(Config.Key.SU_NOTIFICATION)
setSummary(Config.Key.SU_REQUEST_TIMEOUT)
}
private inline fun showUrlDialog(
initialValue: String,
crossinline onCancel: () -> Unit = {},
crossinline onSuccess: (String) -> Unit
) {
val v = LayoutInflater
.from(requireActivity())
.inflate(R.layout.custom_channel_dialog, null)
val url = v.findViewById<EditText>(R.id.custom_url).apply {
setText(initialValue)
}
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
.setPositiveButton(android.R.string.ok) { _, _ -> onSuccess(url.text.toString()) }
.setNegativeButton(android.R.string.cancel) { _, _ -> onCancel() }
.setOnCancelListener { onCancel() }
.show()
}
inner class DownloadDialogData(initialValue: String) {
val text = KObservableField(initialValue)
val path = Observer(text) {
File(Environment.getExternalStorageDirectory(), text.value).absolutePath
}
}
private inline fun showDownloadDialog(
initialValue: String = Config.downloadPath,
crossinline onSuccess: (String) -> Unit
) {
val data = DownloadDialogData(initialValue)
val binding: CustomDownloadDialogBinding = DataBindingUtil
.inflate(layoutInflater, R.layout.custom_download_dialog, null, false)
binding.also { it.data = data }
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_download_path_title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
Utils.ensureDownloadPath(data.text.value)?.let { onSuccess(data.text.value) }
?: Utils.toast(R.string.settings_download_path_error, Toast.LENGTH_SHORT)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private inline fun showManagerNameDialog(
crossinline onSuccess: (String) -> Unit
) {
val data = ManagerNameData()
val view = DialogCustomNameBinding
.inflate(LayoutInflater.from(requireContext()))
.also { it.data = data }
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_app_name)
.setView(view.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (view.dialogNameInput.error.isNullOrBlank()) {
onSuccess(data.name.value)
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
inner class ManagerNameData {
val name = KObservableField(resources.getString(R.string.re_app_name))
}
}

View File

@ -1,25 +0,0 @@
package com.topjohnwu.magisk.ui.superuser
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment :
BaseFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
override val layoutRes: Int = R.layout.fragment_superuser
override val viewModel: SuperuserViewModel by viewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.superuser)
}
override fun onResume() {
super.onResume()
viewModel.updatePolicies()
}
}

View File

@ -1,151 +0,0 @@
package com.topjohnwu.magisk.ui.superuser
import android.content.pm.PackageManager
import android.content.res.Resources
import com.topjohnwu.magisk.BR
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.applySchedulers
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy
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.RxBus
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.ItemBinding
class SuperuserViewModel(
private val policyDB: PolicyDao,
private val packageManager: PackageManager,
private val resources: Resources,
rxBus: RxBus
) : BaseViewModel() {
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel)
}
private var ignoreNext: PolicyRvItem? = null
private var fetchTask: Disposable? = null
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>()
.subscribeK { updatePolicy(it) }
.add()
updatePolicies()
}
fun updatePolicies() {
if (fetchTask?.isDisposed?.not() == true) return
fetchTask = policyDB.fetchAll()
.flattenAsFlowable { it }
.map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) }
.toList()
.map {
it.sortedWith(compareBy(
{ it.item.appName.toLowerCase() },
{ it.item.packageName }
))
}
.map { it to items.calculateDiff(it) }
.applySchedulers()
.applyViewModel(this)
.subscribeK { items.update(it.first, it.second) }
}
fun deletePressed(item: PolicyRvItem) {
fun updateState() = deletePolicy(item.item)
.map { items.filterIsInstance<PolicyRvItem>().toMutableList() }
.map { it.removeAll { it.item.packageName == item.item.packageName }; it }
.map { it to items.calculateDiff(it) }
.subscribeK { items.update(it.first, it.second) }
.add()
withView {
if (BiometricHelper.isEnabled) {
BiometricHelper.authenticate(this) { updateState() }
} else {
CustomAlertDialog(this)
.setTitle(R.string.su_revoke_title)
.setMessage(getString(R.string.su_revoke_msg, item.item.appName))
.setPositiveButton(android.R.string.yes) { _, _ -> updateState() }
.setNegativeButton(android.R.string.no, null)
.setCancelable(true)
.show()
}
}
}
private fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
is PolicyUpdateEvent.Notification -> updatePolicy(it.item) {
val textId =
if (it.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
}
is PolicyUpdateEvent.Log -> updatePolicy(it.item) {
val textId =
if (it.logging) R.string.su_snack_log_on else R.string.su_snack_log_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
}
}
private fun updatePolicy(item: MagiskPolicy, onSuccess: (MagiskPolicy) -> Unit) =
updatePolicy(item)
.subscribeK { onSuccess(it) }
.add()
private fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun updateState() {
val app = item.item.copy(policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY)
updatePolicy(app)
.map { it.policy == MagiskPolicy.ALLOW }
.subscribeK {
val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny
val text = resources.getString(textId).format(item.item.appName)
SnackbarEvent(text).publish()
}
.add()
}
if (BiometricHelper.isEnabled) {
withView {
BiometricHelper.authenticate(this, onError = {
ignoreNext = item
item.isEnabled.toggle()
}) { updateState() }
}
} else {
updateState()
}
}
private fun updatePolicy(policy: MagiskPolicy) =
policyDB.update(policy).andThen(Single.just(policy))
private fun deletePolicy(policy: MagiskPolicy) =
policyDB.delete(policy.uid).andThen(Single.just(policy))
}

View File

@ -77,19 +77,6 @@ object Shortcuts {
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_extension))
.setRank(3)
.build()
)
shortCuts.add(
ShortcutInfo.Builder(context, "downloads")
.setShortLabel(context.getString(R.string.downloads))
.setIntent(
Intent(intent)
.putExtra(Const.Key.OPEN_SECTION, "downloads")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
.setIcon(getIcon(R.drawable.sc_cloud_download))
.setRank(2)
.build()
)

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.MainViewModel" />
</data>
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
android:id="@+id/main_include"
layout="@layout/activity_main_content"
viewModel="@{viewModel}" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
onNavigationClick="@{(item) -> viewModel.navigationItemPressed(item)}"
app:menu="@menu/drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
</layout>

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.MainViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
app:elevation="0dp"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/main_toolbar"
onNavigationClick="@{() -> viewModel.navPressed()}"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/primary"
app:navigationIcon="@drawable/ic_menu"
app:title="@string/magisk"
app:titleTextColor="@android:color/white" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings_update_custom_msg"
app:hintEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/custom_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.SettingsFragment.DownloadDialogData" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_custom_download_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{@string/settings_download_path_message(data.path)}"
tools:text="@string/settings_download_path_message" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_download_path_title"
app:hintEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:text="@={data.text}" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.extensions.XStringKt" />
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.SettingsFragment.ManagerNameData" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic"
android:paddingEnd="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/dialog_name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings_app_name_hint"
app:counterEnabled="true"
app:counterMaxLength="14"
app:counterOverflowTextColor="@color/colorError"
app:error="@{data.name.length() > 14 || data.name.empty || XStringKt.isEmptyInternal(data.name) ? @string/settings_app_name_error : @string/empty}"
app:errorEnabled="true"
app:helperText="@string/settings_app_name_helper">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={data.name}"
tools:text="@string/re_app_name" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>

View File

@ -17,7 +17,7 @@
<import type="com.topjohnwu.magisk.BuildConfig" />
<import type="com.topjohnwu.magisk.ui.home.MagiskState" />
<import type="com.topjohnwu.magisk.redesign.home.MagiskState" />
<import type="com.topjohnwu.magisk.extensions.XAndroidKt" />
@ -801,4 +801,4 @@
</androidx.core.widget.NestedScrollView>
</layout>
</layout>

View File

@ -1,37 +0,0 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/log_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:elevation="4dp"
app:tabSelectedTextColor="@android:color/white"
app:tabTextColor="@android:color/secondary_text_dark" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/log_container"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
pageTitles="@{viewModel}"
position="@={viewModel.currentPage}"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</layout>

View File

@ -1,503 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State" />
<import type="com.topjohnwu.magisk.ui.home.SafetyNetState" />
<import type="com.topjohnwu.magisk.ui.home.MagiskState" />
<import type="com.topjohnwu.magisk.ui.home.MagiskItem" />
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.home.HomeViewModel" />
</data>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
app:onRefreshListener="@{() -> viewModel.refresh()}"
app:refreshing="@{viewModel.loading}">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="@+id/app_name"
app:layout_constraintEnd_toStartOf="@+id/app_name"
app:layout_constraintHorizontal_bias="0.42"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/app_name"
app:srcCompat="@drawable/ic_logo" />
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
style="@style/Widget.Divider.Horizontal"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<include
android:id="@+id/home_magisk_version"
additionalInfo="@{viewModel.magiskAdditionalInfo}"
currentVersion="@{viewModel.magiskCurrentVersion}"
item="@{MagiskItem.MAGISK}"
latestVersion="@{viewModel.magiskLatestVersion}"
layout="@layout/include_update_card"
state="@{viewModel.magiskState}"
text="@{viewModel.magiskStateText}"
viewModel="@{viewModel}" />
<include
android:id="@+id/home_manager_version"
additionalInfo="@{viewModel.managerAdditionalInfo}"
currentVersion="@{viewModel.managerCurrentVersion}"
item="@{MagiskItem.MANAGER}"
latestVersion="@{viewModel.managerLatestVersion}"
layout="@layout/include_update_card"
state="@{viewModel.managerState}"
text="@{viewModel.managerStateText}"
viewModel="@{viewModel}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic" />
<View
style="@style/Widget.Divider.Horizontal"
gone="@{!viewModel.isConnected}"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
gone="@{!viewModel.isConnected}"
android:onClick="@{() -> viewModel.advancedPressed()}"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:minHeight="48dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:rotation="@{viewModel.isAdvancedExpanded ? 180 : 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/title"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow"
app:tint="?attr/imageColorTint" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/advanced_settings_title"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/install_option_expand"
gone="@{!viewModel.isAdvancedExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<CheckBox
android:id="@+id/keep_force_enc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="@={viewModel.isForceEncryption}"
android:text="@string/keep_force_encryption"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
app:layout_constraintWidth_min="300dp" />
<CheckBox
android:id="@+id/keep_verity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="@={viewModel.isKeepVerity}"
android:text="@string/keep_dm_verity"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keep_force_enc"
app:layout_constraintWidth_default="wrap"
app:layout_constraintWidth_min="300dp" />
<CheckBox
android:id="@+id/recovery_mode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="@={viewModel.isRecovery}"
android:text="@string/recovery_mode"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keep_verity"
app:layout_constraintWidth_default="wrap"
app:layout_constraintWidth_min="300dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<View
style="@style/Widget.Divider.Horizontal"
gone="@{!viewModel.isActive || !viewModel.isConnected}"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<LinearLayout
gone="@{!viewModel.isConnected || !viewModel.hasGMS}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/margin_generic_half"
android:paddingBottom="@dimen/margin_generic_half">
<ImageView
android:id="@+id/sn_logo"
android:layout_width="30dp"
android:layout_height="30dp"
android:tint="@color/green500"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/safetyNet_status"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_safetynet" />
<TextView
android:id="@+id/safetyNet_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_generic"
android:layout_marginRight="@dimen/margin_generic"
android:gravity="center"
android:maxLines="1"
android:text="@{viewModel.safetyNetTitle}"
android:textStyle="bold"
app:autoSizeMaxTextSize="14sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/safetyNet_refresh"
app:layout_constraintStart_toEndOf="@+id/sn_logo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="200dp" />
<ImageView
android:id="@+id/safetyNet_refresh"
invisible="@{viewModel.ctsState == SafetyNetState.LOADING || viewModel.basicIntegrityState == SafetyNetState.LOADING}"
android:layout_width="25dp"
android:layout_height="25dp"
android:onClick="@{() -> viewModel.safetyNetPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/safetyNet_status"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_refresh" />
<ProgressBar
android:id="@+id/safetyNet_check_progress"
style="@style/Widget.Progress"
goneUnless="@{viewModel.ctsState == SafetyNetState.LOADING || viewModel.basicIntegrityState == SafetyNetState.LOADING}"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/safetyNet_refresh"
app:layout_constraintEnd_toEndOf="@+id/safetyNet_refresh"
app:layout_constraintStart_toStartOf="@+id/safetyNet_refresh"
app:layout_constraintTop_toTopOf="@+id/safetyNet_refresh" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/safetyNet_expand"
gone="@{viewModel.safetyNetState == State.LOADING}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/cts_status_icon"
srcCompat="@{viewModel.ctsState == SafetyNetState.PASS ? R.drawable.ic_check_circle : R.drawable.ic_cancel}"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/cts_status"
app:layout_constraintEnd_toStartOf="@+id/cts_status"
app:layout_constraintTop_toTopOf="@+id/cts_status"
app:tint="@{viewModel.ctsState == SafetyNetState.PASS ? @color/colorCorrect : @color/colorError}"
tools:srcCompat="@drawable/ic_check_circle" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/basic_status_icon"
srcCompat="@{viewModel.basicIntegrityState == SafetyNetState.PASS ? R.drawable.ic_check_circle : R.drawable.ic_cancel}"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/basic_status"
app:layout_constraintEnd_toStartOf="@+id/basic_status"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/basic_status"
app:tint="@{viewModel.basicIntegrityState == SafetyNetState.PASS ? @color/colorCorrect : @color/colorError}"
tools:srcCompat="@drawable/ic_check_circle" />
<TextView
android:id="@+id/cts_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@{String.format(`ctsProfile: %b`, viewModel.ctsState == SafetyNetState.PASS)}"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/basic_status"
app:layout_constraintStart_toStartOf="@+id/basic_status"
app:layout_constraintTop_toTopOf="parent"
tools:text="ctsProfile: true" />
<TextView
android:id="@+id/basic_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:padding="6dp"
android:text="@{String.format(`basicIntegrity: %b`, viewModel.basicIntegrityState == SafetyNetState.PASS)}"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/basic_status_icon"
app:layout_constraintTop_toBottomOf="@+id/cts_status"
tools:text="basicIntegrity: true" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<View
style="@style/Widget.Divider.Horizontal"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/twitter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.twitterPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/github"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/paypal">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_twitter" />
</FrameLayout>
<FrameLayout
android:id="@+id/github"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.githubPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/xda"
app:layout_constraintStart_toEndOf="@+id/twitter">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_github" />
</FrameLayout>
<FrameLayout
android:id="@+id/xda"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.xdaPressed()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/github">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_xda" />
</FrameLayout>
<LinearLayout
android:id="@+id/patreon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_horizontal"
android:onClick="@{() -> viewModel.patreonPressed()}"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/paypal"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_patreon" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:text="Patreon"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/paypal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_horizontal"
android:onClick="@{() -> viewModel.paypalPressed()}"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@+id/patreon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:layout_margin="10dp"
android:tint="?attr/imageColorTint"
app:srcCompat="@drawable/ic_paypal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:text="PayPal"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
style="@style/Widget.Divider.Horizontal"
gone="@{!viewModel.isActive}"
android:layout_width="match_parent"
android:layout_margin="@dimen/margin_generic" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.Button.Text"
gone="@{!viewModel.isActive}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.uninstallPressed()}"
android:text="@string/uninstall" />
</LinearLayout>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</layout>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{() -> viewModel.refresh()}"
app:refreshing="@{viewModel.loading}">
<androidx.recyclerview.widget.RecyclerView
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_app" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</layout>

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/modules_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:onRefreshListener="@{() -> viewModel.refresh(false)}"
app:refreshing="@{viewModel.loading}">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/modules_content"
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.itemsInstalled}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_module" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/modules_status_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/no_modules_found"
android:textSize="20sp"
android:textStyle="italic"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_margin="@dimen/fab_padding"
android:onClick="@{() -> viewModel.fabPressed()}"
android:focusable="true"
android:clickable="true"
app:fabSize="normal"
app:layout_behavior="com.google.android.material.floatingactionbutton.FloatingActionButton$Behavior"
app:srcCompat="@drawable/ic_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/repos_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:onRefreshListener="@{() -> viewModel.refresh(true)}"
app:refreshing="@{viewModel.loading}">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/repos_content"
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.itemsRemote}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_repo" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/repos_status_text"
gone="@{!(viewModel.loaded &amp;&amp; viewModel.itemsRemote.size == 0)}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/no_modules_found"
android:textSize="20sp"
android:textStyle="italic" />
</FrameLayout>
</layout>

View File

@ -1,53 +0,0 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.superuser.SuperuserViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/superuser_content"
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
gone="@{!viewModel.loaded}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_policy" />
<ProgressBar
style="@style/Widget.Progress"
gone="@{!viewModel.loading}"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/superuser_empty"
gone="@{!viewModel.loaded || viewModel.items.size &gt; 0}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/no_apps_found"
android:textSize="20sp"
android:textStyle="italic" />
</FrameLayout>
</layout>

View File

@ -1,151 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.ui.home.MagiskState" />
<import type="com.topjohnwu.magisk.ui.home.MagiskItem" />
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.ui.home.MagiskItem" />
<variable
name="state"
type="com.topjohnwu.magisk.ui.home.MagiskState" />
<variable
name="text"
type="String" />
<variable
name="currentVersion"
type="String" />
<variable
name="latestVersion"
type="String" />
<variable
name="additionalInfo"
type="String" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.home.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:background="?android:attr/selectableItemBackground"
android:onClick="@{() -> viewModel.cardPressed(item)}">
<com.google.android.material.button.MaterialButton
android:id="@+id/install"
style="@style/Widget.Button.Text"
gone="@{state == MagiskState.LOADING || !viewModel.isConnected}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:onClick="@{() -> viewModel.installPressed(item)}"
android:text="@{state != MagiskState.OBSOLETE ? @string/install : @string/update}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/install" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/status_icon"
invisible="@{state == MagiskState.LOADING}"
srcCompat="@{state == MagiskState.UP_TO_DATE ? R.drawable.ic_check_circle : (state == MagiskState.OBSOLETE ? R.drawable.ic_update : R.drawable.ic_help)}"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/status"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@{state == MagiskState.UP_TO_DATE ? @color/colorCorrect : (state == MagiskState.OBSOLETE ? @color/colorUpdate : @color/colorError)}" />
<ProgressBar
android:id="@+id/progress"
style="@style/Widget.Progress"
gone="@{state != MagiskState.LOADING}"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/status_icon"
app:layout_constraintEnd_toEndOf="@+id/status_icon"
app:layout_constraintStart_toStartOf="@+id/status_icon"
app:layout_constraintTop_toTopOf="@+id/status_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
style="@style/Widget.Text.Emphasize"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:text="@{text}"
android:maxLines="2"
app:layout_constraintBottom_toTopOf="@+id/current_version"
app:layout_constraintEnd_toStartOf="@+id/install"
app:layout_constraintStart_toEndOf="@+id/status_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/current_version"
gone="@{state == MagiskState.LOADING || currentVersion.length == 0}"
android:layout_width="0dp"
style="@style/Widget.Text.Caption.Inactive"
android:layout_height="wrap_content"
android:maxLines="2"
android:text="@{state != MagiskState.LOADING ? @string/current_installed(currentVersion) : @string/checking_for_updates}"
app:layout_constraintBottom_toTopOf="@+id/latest_version"
app:layout_constraintEnd_toEndOf="@+id/status"
app:layout_constraintStart_toStartOf="@+id/status"
app:layout_constraintTop_toBottomOf="@+id/status"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/latest_version"
gone="@{currentVersion == latestVersion || !viewModel.isConnected}"
android:layout_width="0dp"
style="@style/Widget.Text.Caption.Inactive"
android:layout_height="wrap_content"
android:maxLines="2"
android:text="@{state != MagiskState.LOADING ? @string/latest_version(latestVersion) : @string/checking_for_updates}"
app:layout_constraintBottom_toTopOf="@+id/additional"
app:layout_constraintEnd_toEndOf="@+id/status"
app:layout_constraintStart_toStartOf="@+id/status"
app:layout_constraintTop_toBottomOf="@+id/current_version"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/additional"
gone="@{additionalInfo.length == 0}"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.Text.Caption.Inactive"
android:maxLines="2"
android:text="@{additionalInfo}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/status"
app:layout_constraintStart_toStartOf="@+id/status"
app:layout_constraintTop_toBottomOf="@+id/latest_version"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -1,126 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
android:onClick="@{() -> item.toggleExpansion()}"
app:cardElevation="@dimen/card_elevation">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="end"
android:src="@{item.item.icon}"
app:layout_constraintBottom_toTopOf="@+id/hide_app_processes"
app:layout_constraintEnd_toStartOf="@+id/hide_app_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_magisk" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_generic"
android:layout_marginEnd="@dimen/margin_generic"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.item.name}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/hide_app_package"
app:layout_constraintEnd_toStartOf="@+id/hide_app_checkbox"
app:layout_constraintStart_toEndOf="@+id/hide_app_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Magisk" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_app_package"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.packageName}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/hide_app_processes"
app:layout_constraintEnd_toStartOf="@id/hide_app_arrow"
app:layout_constraintStart_toStartOf="@+id/hide_app_name"
app:layout_constraintTop_toBottomOf="@+id/hide_app_name"
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_app_arrow"
gone="@{item.items.size == 1}"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:background="@android:color/transparent"
android:rotation="@{item.isExpanded() ? 180 : 0}"
app:layout_constraintBottom_toBottomOf="@+id/hide_app_package"
app:layout_constraintEnd_toEndOf="@+id/hide_app_name"
app:layout_constraintStart_toEndOf="@+id/hide_app_package"
app:layout_constraintTop_toTopOf="@+id/hide_app_package"
app:srcCompat="@drawable/ic_arrow"
app:tint="?attr/imageColorTint" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_app_checkbox"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
isChecked="@{item.isHiddenState}"
android:onClick="@{() -> item.toggle()}"
app:layout_constraintBottom_toTopOf="@+id/hide_app_processes"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/hide_app_name"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/imageColorTint"
tools:src="@drawable/ic_checked" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hide_app_processes"
gone="@{!item.isExpanded}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/margin_generic_half"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
tools:listitem="@layout/item_hide_process" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.hide.HideViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="false"
android:focusable="false"
android:onClick="@{() -> item.toggle()}">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_generic"
android:ellipsize="marquee"
android:singleLine="true"
android:text="@{item.process}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/hide_process_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="com.topjohnwu.magisk.process" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_process_icon"
style="@style/Widget.Icon"
isChecked="@{item.isHidden}"
android:background="@null"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/imageColorTint"
tools:src="@drawable/ic_checked" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -1,149 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
app:cardElevation="2dp"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_generic">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.name}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/version_name"
app:layout_constraintEnd_toStartOf="@+id/checkbox"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Magisk Module" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/version_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.version.length == 0 ? @string/no_info_provided : item.item.version}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/author"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="v1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.author.length == 0 ? @string/no_info_provided : @string/author(item.item.author)}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/description"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/version_name"
tools:text="topjohnwu" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{item.item.description.length == 0 ? @string/no_info_provided : item.item.description}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/notice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/author"
tools:lines="4"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/notice"
gone="@{item.lastActionNotice.length == 0}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{item.lastActionNotice}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/red500"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/description" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/checkbox"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
isChecked="@{item.isChecked}"
android:layout_marginEnd="@dimen/margin_generic"
android:onClick="@{() -> item.toggle()}"
app:layout_constraintBottom_toTopOf="@+id/description"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintStart_toEndOf="@+id/title"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/imageColorTint"
tools:src="@drawable/ic_checked" />
<View
style="@style/Widget.Divider.Vertical"
android:backgroundTint="?attr/imageColorTint"
app:layout_constraintBottom_toBottomOf="@+id/checkbox"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintStart_toEndOf="@+id/checkbox"
app:layout_constraintTop_toTopOf="@+id/checkbox" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/delete"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
srcCompat="@{item.isDeletable ? R.drawable.ic_undelete : R.drawable.ic_delete}"
android:onClick="@{() -> item.toggleDelete()}"
app:layout_constraintBottom_toBottomOf="@+id/checkbox"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/checkbox"
app:layout_constraintTop_toTopOf="@+id/checkbox"
app:tint="?attr/imageColorTint"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_delete" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.LogRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
dividerColor="@{@android:color/transparent}"
dividerSize="@{@dimen/margin_generic}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_generic"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_superuser_log" />
</FrameLayout>
</layout>

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
adapter="@{viewModel.itemsAdapter}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
scrollPosition="@={viewModel.scrollPosition}"
scrollPositionSmooth="@{true}"
android:focusable="true"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_console" />
</HorizontalScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
hide="@{viewModel.scrollPosition == item.items.size - 1 || item.items.size == 0}"
android:focusable="true"
android:clickable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/margin_generic"
android:onClick="@{() -> viewModel.scrollDownPressed()}"
app:srcCompat="@drawable/ic_arrow_down"
app:tint="@color/colorTextInverse" />
</FrameLayout>
</layout>

View File

@ -1,211 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.superuser.SuperuserViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
android:onClick="@{() -> item.toggle()}"
app:cardCornerRadius="@dimen/radius_generic"
app:cardElevation="2dp"
app:cardPreventCornerOverlap="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/app_icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:gravity="end"
android:src="@{item.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/app_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_logo" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_generic"
android:layout_marginRight="@dimen/margin_generic"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.item.appName}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/package_name"
app:layout_constraintEnd_toStartOf="@+id/master_switch"
app:layout_constraintStart_toEndOf="@+id/app_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/package_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.item.packageName}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/app_name"
app:layout_constraintStart_toStartOf="@id/app_name"
app:layout_constraintTop_toBottomOf="@id/app_name"
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/master_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@={item.isEnabled}"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/app_name"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/expand_layout"
gone="@{!item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_generic_half"
android:paddingTop="@dimen/margin_generic_half"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/package_name">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bell"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/notification_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_notifications" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/notification_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@={item.shouldNotify}"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toEndOf="@+id/bell"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.4" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/logging_switch"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_bug_report" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/logging_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:checked="@={item.shouldLog}"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toEndOf="@+id/bug"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.8" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/more_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:tint="@color/icon_grey"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintStart_toStartOf="@id/guideline2"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_more" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/delete"
style="@style/Widget.Icon"
android:layout_gravity="center"
android:layout_weight="1"
android:onClick="@{() -> viewModel.deletePressed(item)}"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/more_info"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_delete" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@ -1,130 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.RepoRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="?android:attr/listPreferredItemHeight"
android:onClick="@{() -> viewModel.repoPressed(item)}"
app:cardElevation="2dp"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_generic">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.name}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/version_name"
app:layout_constraintEnd_toStartOf="@+id/download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Magisk Module" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/version_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.version.length == 0 ? @string/no_info_provided : item.item.version}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/author"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="v1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.author.length == 0 ? @string/no_info_provided : @string/author(item.item.author)}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
android:textStyle="bold|italic"
app:layout_constraintBottom_toTopOf="@+id/description"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/version_name"
tools:text="topjohnwu" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{item.item.description.length == 0 ? @string/no_info_provided : item.item.description}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textIsSelectable="false"
app:layout_constraintBottom_toTopOf="@+id/update_time"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/author"
tools:lines="4"
tools:text="@tools:sample/lorem/random" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/update_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@{@string/updated_on(item.item.lastUpdateString)}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/tertiary_text_dark"
android:textStyle="bold|italic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/description"
tools:text="@tools:sample/date/ddmmyy" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/download"
android:focusable="true"
android:clickable="true"
style="@style/Widget.Icon"
isEnabled="@{viewModel.isConnected}"
android:alpha="@{viewModel.isConnected ? 1f : .2f}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:onClick="@{() -> viewModel.downloadPressed(item)}"
android:tint="?attr/imageColorTint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/title"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_file_download_black" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/log_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:onClick="@{() -> item.toggle()}"
android:orientation="horizontal"
android:paddingLeft="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic_half"
android:paddingRight="@dimen/margin_generic"
android:paddingBottom="@dimen/margin_generic_half"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.Emphasize"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center_vertical"
android:text="@{item.date}"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/log_header_indicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/log_header_indicator"
style="@style/Widget.Icon"
android:background="@android:color/transparent"
android:rotation="@{item.isExpanded() ? 180 : 0}"
android:tint="?attr/imageColorTint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow"
tools:rotation="180" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
gone="@{!item.isExpanded}"
itemBinding="@{viewModel.itemBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/log_header"
tools:itemCount="3"
tools:listitem="@layout/item_superuser_log_entry" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@ -1,112 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.LogItemEntryRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.log.LogViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="?attr/selectableItemBackground"
android:onClick="@{() -> item.toggle()}"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic_half"
android:paddingRight="@dimen/margin_generic"
android:paddingBottom="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.item.appName}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:textIsSelectable="false"
tools:text="@tools:sample/lorem" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:text="@{item.item.action ? @string/grant : @string/deny}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
tools:text="@string/grant" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:text="@{item.item.timeString}"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
tools:text="@tools:sample/date/hhmm" />
</LinearLayout>
<LinearLayout
gone="@{!item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="@dimen/margin_generic"
android:paddingTop="@dimen/margin_generic_half"
android:paddingEnd="@dimen/margin_generic"
android:paddingBottom="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{@string/pid(item.item.fromPid)}"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:text="@string/pid" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{@string/target_uid(item.item.toUid)}"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:text="@string/target_uid" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="@{@string/command(item.item.command)}"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
tools:text="@string/command" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:checkableBehavior="single"
android:id="@+id/main_group">
<item
android:id="@+id/magiskFragment"
android:icon="@drawable/ic_magisk_outline"
android:title="@string/magisk"/>
<item
android:id="@+id/superuserFragment"
android:icon="@drawable/ic_superuser"
android:title="@string/superuser"
android:visible="false"/>
<item
android:id="@+id/magiskHideFragment"
android:icon="@drawable/ic_magiskhide"
android:title="@string/magiskhide"
android:visible="false"/>
</group>
<group
android:checkableBehavior="single"
android:id="@+id/second_group">
<item
android:id="@+id/modulesFragment"
android:icon="@drawable/ic_extension"
android:title="@string/modules"
android:visible="false"/>
<item
android:id="@+id/reposFragment"
android:icon="@drawable/ic_cloud_download"
android:title="@string/downloads"/>
</group>
<group
android:checkableBehavior="single"
android:id="@+id/third_group">
<item
android:id="@+id/logFragment"
android:icon="@drawable/ic_bug_report"
android:title="@string/log" />
<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings"/>
</group>
</menu>

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_save"
android:icon="@drawable/ic_save"
android:title="@string/menuSaveLog"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_clear"
android:icon="@drawable/ic_delete"
android:title="@string/menuClearLog"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_refresh"
android:icon="@drawable/ic_refresh"
android:title="@string/menuReload"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/app_search"
android:title=""
app:actionViewClass="android.widget.SearchView"
app:showAsAction="always"/>
<item
android:id="@+id/show_system"
android:title="@string/show_system_app"
android:checkable="true"/>
</menu>