diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt index dd5e7bebc..f0545b75e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt @@ -10,4 +10,5 @@ val applicationModule = module { single { RxBus() } single { get().resources } single { get() as App } + single { get().packageManager } } diff --git a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt index b3c44dc0d..483f9e3ef 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt @@ -1,6 +1,9 @@ package com.topjohnwu.magisk.di +import com.topjohnwu.magisk.App import org.koin.dsl.module -val databaseModule = module {} +val databaseModule = module { + single { get().DB } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt index e4df34487..c4538d0f6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -2,6 +2,7 @@ package com.topjohnwu.magisk.di import com.topjohnwu.magisk.ui.MainViewModel import com.topjohnwu.magisk.ui.home.HomeViewModel +import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -9,4 +10,5 @@ import org.koin.dsl.module val viewModelModules = module { viewModel { MainViewModel() } viewModel { HomeViewModel(get(), get()) } + viewModel { SuperuserViewModel(get(), get(), get()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt new file mode 100644 index 000000000..e7093af63 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/PolicyRvItem.kt @@ -0,0 +1,23 @@ +package com.topjohnwu.magisk.model.entity.recycler + +import android.graphics.drawable.Drawable +import com.skoumal.teanity.databinding.ComparableRvItem +import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.model.entity.Policy +import com.topjohnwu.magisk.utils.toggle + +class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem() { + + override val layoutRes: Int = R.layout.item_policy + + val isExpanded = KObservableField(false) + val isEnabled = KObservableField(item.policy == Policy.ALLOW) + val shouldNotify = KObservableField(item.notification) + val shouldLog = KObservableField(item.logging) + + fun toggle() = isExpanded.toggle() + + override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other) + override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt index bc92e7479..f4b5fbf5b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -1,5 +1,6 @@ package com.topjohnwu.magisk.model.events +import android.app.Activity import com.skoumal.teanity.viewevents.ViewEvent @@ -15,3 +16,5 @@ class UninstallEvent : ViewEvent() class EnvFixEvent : ViewEvent() class UpdateSafetyNetEvent : ViewEvent() + +class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent() \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt index f145bde2a..b47364d17 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt @@ -11,6 +11,7 @@ import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavTransactionOptions import com.skoumal.teanity.viewevents.ViewEvent import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.model.events.ViewActionEvent import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent import com.topjohnwu.magisk.model.navigation.Navigator @@ -62,6 +63,7 @@ abstract class MagiskActivity navigateTo(event) + is ViewActionEvent -> event.action(this) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt index fe7f97922..da231691f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt @@ -5,6 +5,7 @@ import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment import com.skoumal.teanity.view.TeanityFragment import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.model.events.ViewActionEvent import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent import com.topjohnwu.magisk.model.navigation.Navigator import kotlin.reflect.KClass @@ -25,6 +26,7 @@ abstract class MagiskFragment navigateTo(event) + is ViewActionEvent -> event.action(requireActivity()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt index 2f7d29fcd..2df9d2c3d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt @@ -1,6 +1,8 @@ package com.topjohnwu.magisk.ui.base +import android.app.Activity import com.skoumal.teanity.viewmodel.LoadingViewModel +import com.topjohnwu.magisk.model.events.ViewActionEvent import com.topjohnwu.magisk.utils.Event import timber.log.Timber @@ -10,4 +12,8 @@ abstract class MagiskViewModel : LoadingViewModel(), Event.AutoListener { override fun onEvent(event: Int) = Timber.i("Event of $event was not handled") override fun getListeningEvents(): IntArray = intArrayOf() + fun withView(action: Activity.() -> Unit) { + ViewActionEvent(action).publish() + } + } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.java deleted file mode 100644 index a78ca91ed..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.topjohnwu.magisk.ui.superuser; - -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.model.adapters.PolicyAdapter; -import com.topjohnwu.magisk.model.entity.Policy; -import com.topjohnwu.magisk.ui.base.BaseFragment; - -import java.util.List; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; - -public class SuperuserFragment extends BaseFragment { - - @BindView(R.id.recyclerView) RecyclerView recyclerView; - @BindView(R.id.empty_rv) TextView emptyRv; - - private PackageManager pm; - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_superuser, container, false); - unbinder = new SuperuserFragment_ViewBinding(this, view); - - pm = requireActivity().getPackageManager(); - return view; - } - - @Override - public void onStart() { - super.onStart(); - requireActivity().setTitle(getString(R.string.superuser)); - } - - @Override - public void onResume() { - super.onResume(); - displayPolicyList(); - } - - private void displayPolicyList() { - List policyList = app.getDB().getPolicyList(); - - if (policyList.size() == 0) { - emptyRv.setVisibility(View.VISIBLE); - recyclerView.setVisibility(View.GONE); - } else { - recyclerView.setAdapter(new PolicyAdapter(policyList, app.getDB(), pm)); - emptyRv.setVisibility(View.GONE); - recyclerView.setVisibility(View.VISIBLE); - } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt new file mode 100644 index 000000000..db545db56 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt @@ -0,0 +1,24 @@ +package com.topjohnwu.magisk.ui.superuser + +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding +import com.topjohnwu.magisk.ui.base.MagiskFragment +import org.koin.androidx.viewmodel.ext.android.viewModel + +class SuperuserFragment : + MagiskFragment() { + + override val layoutRes: Int = R.layout.fragment_superuser + override val viewModel: SuperuserViewModel by viewModel() + + override fun onStart() { + super.onStart() + magiskActivity.supportActionBar?.title = getString(R.string.superuser) + } + + override fun onResume() { + super.onResume() + viewModel.updatePolicies() + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt new file mode 100644 index 000000000..bf5e0935c --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt @@ -0,0 +1,146 @@ +package com.topjohnwu.magisk.ui.superuser + +import android.content.pm.PackageManager +import android.content.res.Resources +import com.skoumal.teanity.databinding.ComparableRvItem +import com.skoumal.teanity.extensions.addOnPropertyChangedCallback +import com.skoumal.teanity.extensions.applySchedulers +import com.skoumal.teanity.util.DiffObservableList +import com.skoumal.teanity.viewevents.SnackbarEvent +import com.topjohnwu.magisk.BR +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.data.database.MagiskDB +import com.topjohnwu.magisk.model.entity.Policy +import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem +import com.topjohnwu.magisk.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.FingerprintHelper +import com.topjohnwu.magisk.utils.toggle +import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog +import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog +import io.reactivex.Single +import me.tatarka.bindingcollectionadapter2.ItemBinding +import timber.log.Timber + +class SuperuserViewModel( + private val database: MagiskDB, + private val packageManager: PackageManager, + private val resources: Resources +) : MagiskViewModel() { + + val items = DiffObservableList(ComparableRvItem.callback) + val itemBinding = ItemBinding.of> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel) + } + + private var ignoreNext: PolicyRvItem? = null + + init { + updatePolicies() + } + + fun updatePolicies() { + Single.fromCallable { database.policyList } + .flattenAsFlowable { it } + .map { PolicyRvItem(it, it.info.loadIcon(packageManager)).setListeners() } + .toList() + .applySchedulers() + .applyViewModel(this) + .subscribe({ + items.update(it) + }, Timber::e) + .add() + } + + fun deletePressed(item: PolicyRvItem) { + fun updateState() = deletePolicy(item.item) + .subscribe({ + items.remove(item) + }, Timber::e) + .add() + + withView { + if (FingerprintHelper.useFingerprint()) { + FingerprintAuthDialog(this) { updateState() }.show() + } else { + CustomAlertDialog(this) + .setTitle(R.string.su_revoke_title) + .setMessage(getString(R.string.su_revoke_msg, item.item.appName)) + .setPositiveButton(R.string.yes) { _, _ -> updateState() } + .setNegativeButton(R.string.no_thanks, null) + .setCancelable(true) + .show() + } + } + } + + private fun PolicyRvItem.setListeners() = apply { + isEnabled.addOnPropertyChangedCallback { + it ?: return@addOnPropertyChangedCallback + + if (ignoreNext == this) { + ignoreNext = null + return@addOnPropertyChangedCallback + } + + fun updateState() { + item.policy = if (it) Policy.ALLOW else Policy.DENY + + updatePolicy(item) + .map { it.policy == Policy.ALLOW } + .subscribe({ + val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny + val text = resources.getString(textId).format(item.appName) + SnackbarEvent(text).publish() + }, Timber::e) + .add() + } + + if (FingerprintHelper.useFingerprint()) { + withView { + FingerprintAuthDialog(this, { updateState() }, { + ignoreNext = this@setListeners + isEnabled.toggle() + }).show() + } + } else { + updateState() + } + } + shouldNotify.addOnPropertyChangedCallback { + it ?: return@addOnPropertyChangedCallback + item.notification = it + + updatePolicy(item) + .map { it.notification } + .subscribe({ + val textId = if (it) R.string.su_snack_notif_on else R.string.su_snack_notif_off + val text = resources.getString(textId).format(item.appName) + SnackbarEvent(text).publish() + }, Timber::e) + .add() + } + shouldLog.addOnPropertyChangedCallback { + it ?: return@addOnPropertyChangedCallback + item.logging = it + + updatePolicy(item) + .map { it.logging } + .subscribe({ + val textId = if (it) R.string.su_snack_log_on else R.string.su_snack_log_off + val text = resources.getString(textId).format(item.appName) + SnackbarEvent(text).publish() + }, Timber::e) + .add() + } + } + + private fun updatePolicy(policy: Policy) = + Single.fromCallable { database.updatePolicy(policy); policy } + .applySchedulers() + + private fun deletePolicy(policy: Policy) = + Single.fromCallable { database.deletePolicy(policy); policy } + .applySchedulers() + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_superuser.xml b/app/src/main/res/layout/fragment_superuser.xml index a09b31fdd..c67588746 100644 --- a/app/src/main/res/layout/fragment_superuser.xml +++ b/app/src/main/res/layout/fragment_superuser.xml @@ -1,26 +1,53 @@ - + xmlns:tools="http://schemas.android.com/tools"> - + + + + + + + android:layout_height="match_parent"> - + + + + + + + + + + - diff --git a/app/src/main/res/layout/item_policy.xml b/app/src/main/res/layout/item_policy.xml new file mode 100644 index 000000000..d5ebbed9d --- /dev/null +++ b/app/src/main/res/layout/item_policy.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles_new.xml b/app/src/main/res/values-night/styles_new.xml index 5b0ea2a22..350f77b95 100644 --- a/app/src/main/res/values-night/styles_new.xml +++ b/app/src/main/res/values-night/styles_new.xml @@ -9,6 +9,7 @@ @color/dark_secondary_text ?android:attr/textColorSecondary @color/accentFallback + @android:color/black \ No newline at end of file diff --git a/app/src/main/res/values/styles_new.xml b/app/src/main/res/values/styles_new.xml index cc4fa1c1e..03793ea6d 100644 --- a/app/src/main/res/values/styles_new.xml +++ b/app/src/main/res/values/styles_new.xml @@ -10,6 +10,7 @@ @color/colorSecondary @color/colorSecondary @color/colorSecondaryDark + @android:color/white