Update superuser list

This commit is contained in:
topjohnwu 2020-08-10 02:33:44 -07:00
parent f5e547944a
commit c7e30ac63e
5 changed files with 188 additions and 200 deletions

View File

@ -9,53 +9,60 @@ import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ObservableItem<PolicyItem>() { class PolicyItem(
val item: MagiskPolicy,
val icon: Drawable,
val viewModel: SuperuserViewModel
) : ObservableItem<PolicyItem>() {
override val layoutRes = R.layout.item_policy_md2 override val layoutRes = R.layout.item_policy_md2
@get:Bindable @get:Bindable
var isExpanded = false var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded) set(value) = set(value, field, { field = it }, BR.expanded)
@get:Bindable // This property hosts the policy state
var isEnabled = item.policy == MagiskPolicy.ALLOW var policyState = item.policy == MagiskPolicy.ALLOW
set(value) = set(value, field, { field = it }, BR.enabled) set(value) = set(value, field, { field = it }, BR.enabled)
// This property binds with the UI state
@get:Bindable
var isEnabled
get() = policyState
set(value) = set(value, policyState, { viewModel.togglePolicy(this, it) }, BR.enabled)
@get:Bindable @get:Bindable
var shouldNotify = item.notification var shouldNotify = item.notification
set(value) = set(value, field, { field = it }, BR.shouldNotify) set(value) = set(value, field, { field = it }, BR.shouldNotify) {
viewModel.updatePolicy(updatedPolicy, isLogging = false)
}
@get:Bindable @get:Bindable
var shouldLog = item.logging var shouldLog = item.logging
set(value) = set(value, field, { field = it }, BR.shouldLog) set(value) = set(value, field, { field = it }, BR.shouldLog) {
viewModel.updatePolicy(updatedPolicy, isLogging = true)
}
private val updatedPolicy private val updatedPolicy
get() = item.copy( get() = item.copy(
policy = if (isEnabled) MagiskPolicy.ALLOW else MagiskPolicy.DENY, policy = if (policyState) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
notification = shouldNotify, notification = shouldNotify,
logging = shouldLog logging = shouldLog
) )
fun toggle(viewModel: SuperuserViewModel) { fun toggleExpand() {
if (isExpanded) {
toggle()
return
}
isEnabled = !isEnabled
viewModel.togglePolicy(this, isEnabled)
}
fun toggle() {
isExpanded = !isExpanded isExpanded = !isExpanded
} }
fun toggleNotify(viewModel: SuperuserViewModel) { fun toggleNotify() {
shouldNotify = !shouldNotify shouldNotify = !shouldNotify
viewModel.updatePolicy(updatedPolicy, isLogging = false)
} }
fun toggleLog(viewModel: SuperuserViewModel) { fun toggleLog() {
shouldLog = !shouldLog shouldLog = !shouldLog
viewModel.updatePolicy(updatedPolicy, isLogging = true) }
fun revoke() {
viewModel.deletePressed(this)
} }
override fun contentSameAs(other: PolicyItem) = itemSameAs(other) override fun contentSameAs(other: PolicyItem) = itemSameAs(other)

View File

@ -35,9 +35,7 @@ class SuperuserViewModel(
private val itemNoData = TextItem(R.string.superuser_policy_none) private val itemNoData = TextItem(R.string.superuser_policy_none)
private val itemsPolicies = diffListOf<PolicyItem>() private val itemsPolicies = diffListOf<PolicyItem>()
private val itemsHelpers = ObservableArrayList<TextItem>().also { private val itemsHelpers = ObservableArrayList<TextItem>()
it.add(itemNoData)
}
val adapter = adapterOf<ComparableRvItem<*>>() val adapter = adapterOf<ComparableRvItem<*>>()
val items = MergeObservableList<ComparableRvItem<*>>() val items = MergeObservableList<ComparableRvItem<*>>()
@ -45,7 +43,6 @@ class SuperuserViewModel(
.insertList(itemsHelpers) .insertList(itemsHelpers)
.insertList(itemsPolicies) .insertList(itemsPolicies)
val itemBinding = itemBindingOf<ComparableRvItem<*>> { val itemBinding = itemBindingOf<ComparableRvItem<*>> {
it.bindExtra(BR.viewModel, this)
it.bindExtra(BR.listener, this) it.bindExtra(BR.listener, this)
} }
@ -55,7 +52,7 @@ class SuperuserViewModel(
state = State.LOADING state = State.LOADING
val (policies, diff) = withContext(Dispatchers.Default) { val (policies, diff) = withContext(Dispatchers.Default) {
val policies = db.fetchAll { val policies = db.fetchAll {
PolicyItem(it, it.applicationInfo.loadIcon(packageManager)) PolicyItem(it, it.applicationInfo.loadIcon(packageManager), this@SuperuserViewModel)
}.sortedWith(compareBy( }.sortedWith(compareBy(
{ it.item.appName.toLowerCase(currentLocale) }, { it.item.appName.toLowerCase(currentLocale) },
{ it.item.packageName } { it.item.packageName }
@ -63,15 +60,15 @@ class SuperuserViewModel(
policies to itemsPolicies.calculateDiff(policies) policies to itemsPolicies.calculateDiff(policies)
} }
itemsPolicies.update(policies, diff) itemsPolicies.update(policies, diff)
if (itemsPolicies.isNotEmpty()) { if (itemsPolicies.isNotEmpty())
itemsHelpers.remove(itemNoData) itemsHelpers.clear()
} else if (itemsHelpers.isEmpty())
itemsHelpers.add(itemNoData)
state = State.LOADED state = State.LOADED
} }
// --- // ---
@Suppress("REDUNDANT_ELSE_IN_WHEN")
override fun onItemPressed(item: TappableHeadlineItem) = when (item) { override fun onItemPressed(item: TappableHeadlineItem) = when (item) {
TappableHeadlineItem.Hide -> hidePressed() TappableHeadlineItem.Hide -> hidePressed()
else -> Unit else -> Unit
@ -120,6 +117,8 @@ class SuperuserViewModel(
fun togglePolicy(item: PolicyItem, enable: Boolean) { fun togglePolicy(item: PolicyItem, enable: Boolean) {
fun updateState() { fun updateState() {
item.policyState = enable
val policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY val policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY
val app = item.item.copy(policy = policy) val app = item.item.copy(policy = policy)
@ -127,14 +126,13 @@ class SuperuserViewModel(
db.update(app) db.update(app)
val res = if (app.policy == MagiskPolicy.ALLOW) R.string.su_snack_grant val res = if (app.policy == MagiskPolicy.ALLOW) R.string.su_snack_grant
else R.string.su_snack_deny else R.string.su_snack_deny
SnackbarEvent(resources.getString(res).format(item.item.appName)) SnackbarEvent(resources.getString(res).format(item.item.appName)).publish()
} }
} }
if (BiometricHelper.isEnabled) { if (BiometricHelper.isEnabled) {
BiometricDialog { BiometricDialog {
onSuccess { updateState() } onSuccess { updateState() }
onFailure { item.isEnabled = !item.isEnabled }
}.publish() }.publish()
} else { } else {
updateState() updateState()

View File

@ -37,6 +37,11 @@ fun setImageResource(view: ImageView, @DrawableRes resId: Int) {
view.setImageResource(resId) view.setImageResource(resId)
} }
@BindingAdapter("srcCompat")
fun setImageResource(view: ImageView, drawable: Drawable) {
view.setImageDrawable(drawable)
}
@BindingAdapter("movieBehavior", "movieBehaviorText") @BindingAdapter("movieBehavior", "movieBehaviorText")
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) { fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
(view.tag as? Job)?.cancel() (view.tag as? Job)?.cancel()

View File

@ -21,7 +21,7 @@
android:id="@+id/superuser_list" android:id="@+id/superuser_list"
adapter="@{viewModel.adapter}" adapter="@{viewModel.adapter}"
dividerHorizontal="@{@drawable/divider_l1}" dividerHorizontal="@{@drawable/divider_l1}"
dividerVertical="@{@drawable/divider_l1}" dividerVertical="@{@drawable/divider_l_50}"
goneUnless="@{viewModel.loaded || !viewModel.items.empty}" goneUnless="@{viewModel.loaded || !viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}" itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}" items="@{viewModel.items}"
@ -33,10 +33,7 @@
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}" android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}" android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:spanCount="2" tools:listitem="@layout/item_policy_md2" />
tools:layout_marginTop="24dp"
tools:listitem="@layout/item_policy_md2"
tools:paddingTop="@dimen/l1" />
<LinearLayout <LinearLayout
goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}" goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
@ -44,9 +41,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="vertical"
tools:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/safetynet_attest_loading" android:text="@string/safetynet_attest_loading"

View File

@ -9,190 +9,170 @@
name="item" name="item"
type="com.topjohnwu.magisk.model.entity.recycler.PolicyItem" /> type="com.topjohnwu.magisk.model.entity.recycler.PolicyItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.superuser.SuperuserViewModel" />
</data> </data>
<com.google.android.material.card.MaterialCardView <FrameLayout
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center">
android:alpha="@{item.isEnabled() ? 1f : .5f}"
android:onClick="@{() -> item.toggle(viewModel)}"
android:onLongClick="@{() -> item.toggle()}"
tools:layout_marginBottom="@dimen/l1"
tools:layout_marginEnd="@dimen/l1">
<LinearLayout <com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Variant"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:alpha="@{item.enabled ? 1f : .5f}"
android:onClick="@{() -> item.toggleExpand()}">
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/policy_app_icon" android:layout_width="match_parent"
style="@style/WidgetFoundation.Image" android:layout_height="wrap_content">
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:src="@{item.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:srcCompat="@drawable/ic_logo" />
<androidx.appcompat.widget.AppCompatTextView <ImageView
android:id="@+id/policy_app_name" android:id="@+id/policy_app_icon"
android:layout_width="0dp" style="@style/WidgetFoundation.Image"
android:layout_height="wrap_content" android:layout_marginStart="@dimen/l1"
android:layout_marginLeft="@dimen/margin_generic" android:layout_marginTop="@dimen/l1"
android:layout_marginRight="@dimen/margin_generic" android:layout_marginBottom="@dimen/l1"
android:ellipsize="middle" srcCompat="@{item.icon}"
android:gravity="start" app:layout_constraintBottom_toBottomOf="parent"
android:maxLines="2" app:layout_constraintStart_toStartOf="parent"
android:text="@{item.item.appName}" app:layout_constraintTop_toTopOf="parent"
android:textAppearance="@style/AppearanceFoundation.Body" app:layout_constraintVertical_bias="0"
android:textIsSelectable="false" tools:srcCompat="@drawable/ic_logo" />
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/policy_indicator"
app:layout_constraintStart_toEndOf="@+id/policy_app_icon"
app:layout_constraintTop_toTopOf="@+id/policy_app_icon"
tools:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView <TextView
android:id="@+id/policy_package_name" android:id="@+id/policy_app_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/l1" android:layout_marginLeft="@dimen/margin_generic"
android:ellipsize="middle" android:layout_marginRight="@dimen/margin_generic"
android:gravity="start" android:ellipsize="middle"
android:maxLines="2" android:gravity="start"
android:text="@{item.item.packageName}" android:maxLines="2"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant" android:text="@{item.item.appName}"
android:textColor="@android:color/tertiary_text_dark" android:textAppearance="@style/AppearanceFoundation.Body"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintBottom_toBottomOf="parent" android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@id/policy_app_name" app:layout_constraintEnd_toStartOf="@+id/policy_indicator"
app:layout_constraintStart_toStartOf="@id/policy_app_name" app:layout_constraintStart_toEndOf="@+id/policy_app_icon"
app:layout_constraintTop_toBottomOf="@id/policy_app_name" app:layout_constraintTop_toTopOf="@+id/policy_app_icon"
app:layout_constraintVertical_bias="0" tools:text="@string/app_name" />
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView <TextView
android:id="@+id/policy_indicator" android:id="@+id/policy_package_name"
style="@style/WidgetFoundation.Switch" android:layout_width="0dp"
isSelected="@{item.enabled}" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l1" android:layout_marginBottom="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent" android:ellipsize="middle"
app:layout_constraintEnd_toEndOf="parent" android:gravity="start"
app:layout_constraintTop_toTopOf="parent" /> android:maxLines="2"
android:text="@{item.item.packageName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/policy_app_name"
app:layout_constraintStart_toStartOf="@id/policy_app_name"
app:layout_constraintTop_toBottomOf="@id/policy_app_name"
app:layout_constraintVertical_bias="0"
tools:text="com.topjohnwu.magisk" />
</androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/policy_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={item.enabled}"
android:layout_marginEnd="@dimen/l1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/policy_expand_container"
gone="@{!item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?colorSurfaceVariant"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/policy_package_name"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton <LinearLayout
android:id="@+id/policy_notify" android:id="@+id/policy_expand_container"
style="@style/WidgetFoundation.Button.Text" android:orientation="horizontal"
isSelected="@{item.shouldNotify}" gone="@{!item.isExpanded}"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:minHeight="24dp" android:background="?colorSurfaceVariant"
android:onClick="@{() -> item.toggleNotify(viewModel)}" tools:visibility="visible">
android:text="@string/superuser_toggle_notification"
android:textAllCaps="false"
android:textAppearance="@style/AppearanceFoundation.Tiny"
android:textColor="@color/color_state_primary_transient"
app:icon="@drawable/ic_notifications_md2"
app:iconTint="@color/color_state_primary_transient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/policy_log"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/color_state_primary_transient" />
<View <com.google.android.material.button.MaterialButton
android:layout_width="1dp" android:id="@+id/policy_notify"
android:layout_height="0dp" style="@style/WidgetFoundation.Button.Text"
android:layout_marginTop="@dimen/l_50" isSelected="@{item.shouldNotify}"
android:layout_marginBottom="@dimen/l_50" android:layout_weight="1"
android:background="?colorSurface" android:layout_width="0dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@+id/policy_notify" android:minHeight="24dp"
app:layout_constraintTop_toTopOf="parent" /> android:onClick="@{() -> item.toggleNotify()}"
android:text="@string/superuser_toggle_notification"
android:textAllCaps="false"
android:textAppearance="@style/AppearanceFoundation.Tiny"
android:textColor="@color/color_state_primary_transient"
app:icon="@drawable/ic_notifications_md2"
app:iconTint="@color/color_state_primary_transient"
app:tint="@color/color_state_primary_transient" />
<com.google.android.material.button.MaterialButton <View
android:id="@+id/policy_log" android:layout_width="1dp"
style="@style/WidgetFoundation.Button.Text" android:layout_height="match_parent"
isSelected="@{item.shouldLog}" android:layout_marginTop="@dimen/l_50"
android:layout_width="0dp" android:layout_marginBottom="@dimen/l_50"
android:layout_height="wrap_content" android:background="?colorSurfaceSurfaceVariant" />
android:minHeight="24dp"
android:onClick="@{() -> item.toggleLog(viewModel)}"
android:text="@string/logs"
android:textAllCaps="false"
android:textAppearance="@style/AppearanceFoundation.Tiny"
android:textColor="@color/color_state_primary_transient"
app:icon="@drawable/ic_bug_md2"
app:iconTint="@color/color_state_primary_transient"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/policy_delete"
app:layout_constraintStart_toEndOf="@+id/policy_notify"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/color_state_primary_transient" />
<View <com.google.android.material.button.MaterialButton
android:layout_width="1dp" android:id="@+id/policy_log"
android:layout_height="0dp" style="@style/WidgetFoundation.Button.Text"
android:layout_marginTop="@dimen/l_50" isSelected="@{item.shouldLog}"
android:layout_marginBottom="@dimen/l_50" android:layout_weight="1"
android:background="?colorSurface" android:layout_width="0dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@+id/policy_log" android:minHeight="24dp"
app:layout_constraintTop_toTopOf="parent" /> android:onClick="@{() -> item.toggleLog()}"
android:text="@string/logs"
android:textAllCaps="false"
android:textAppearance="@style/AppearanceFoundation.Tiny"
android:textColor="@color/color_state_primary_transient"
app:icon="@drawable/ic_bug_md2"
app:iconTint="@color/color_state_primary_transient"
app:tint="@color/color_state_primary_transient" />
<com.google.android.material.button.MaterialButton <View
android:id="@+id/policy_delete" android:layout_width="1dp"
style="@style/WidgetFoundation.Button.Text" android:layout_height="match_parent"
android:layout_width="0dp" android:layout_marginTop="@dimen/l_50"
android:layout_height="wrap_content" android:layout_marginBottom="@dimen/l_50"
android:minHeight="24dp" android:background="?colorSurfaceSurfaceVariant" />
android:onClick="@{() -> viewModel.deletePressed(item)}"
android:text="@string/superuser_toggle_revoke"
android:textAllCaps="false"
android:textColor="?colorError"
android:textSize="12sp"
app:icon="@drawable/ic_delete_md2"
app:iconTint="?colorError"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/policy_log"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="?colorError" />
</androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.button.MaterialButton
android:id="@+id/policy_delete"
style="@style/WidgetFoundation.Button.Text"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="24dp"
android:onClick="@{() -> item.revoke()}"
android:text="@string/superuser_toggle_revoke"
android:textAllCaps="false"
android:textColor="?colorError"
android:textSize="12sp"
app:icon="@drawable/ic_delete_md2"
app:iconTint="?colorError"
app:rippleColor="?colorError" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </LinearLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
</layout> </layout>