Updated policy (apps) layout to be more compact

Added pinch in to increase list span count / out to decrease
  The setting will be remembered across the whole app (every list that uses Staggered Grid)
Updated indication of whether the policy has root access enabled permitted or not
  Displays crossed out app logo if not permitted
This commit is contained in:
Viktor De Pasquale 2020-01-04 16:07:53 +01:00
parent eb929160b3
commit 8737be2623
15 changed files with 304 additions and 135 deletions

View File

@ -55,6 +55,7 @@ object Config : PreferenceModel, DBConfig {
const val SAFETY = "safety_notice"
const val THEME_ORDINAL = "theme_ordinal"
const val BOOT_ID = "boot_id"
const val LIST_SPAN_COUNT = "list_span_count"
// system state
const val MAGISKHIDE = "magiskhide"
@ -109,8 +110,7 @@ object Config : PreferenceModel, DBConfig {
Value.CANARY_DEBUG_CHANNEL
else
Value.CANARY_CHANNEL
}
else Value.DEFAULT_CHANNEL
} else Value.DEFAULT_CHANNEL
var bootId by preference(Key.BOOT_ID, "")
@ -137,6 +137,7 @@ object Config : PreferenceModel, DBConfig {
@JvmStatic
var coreOnly by preference(Key.COREONLY, false)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
var listSpanCount by preference(Key.LIST_SPAN_COUNT, 2)
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
var locale by preference(Key.LOCALE, "")
@ -149,7 +150,8 @@ object Config : PreferenceModel, DBConfig {
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
// Always return a path in external storage where we can write
val downloadDirectory get() =
val downloadDirectory
get() =
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
private const val SU_FINGERPRINT = "su_fingerprint"

View File

@ -1,8 +1,6 @@
package com.topjohnwu.magisk.model.entity.recycler
import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
@ -14,8 +12,6 @@ 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
import com.topjohnwu.magisk.utils.rotationTo
import com.topjohnwu.magisk.utils.setRevealed
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
@ -73,18 +69,15 @@ class PolicyItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<
fun toggle(viewModel: SuperuserViewModel) {
if (isExpanded.value) {
toggle()
return
}
isEnabled.toggle()
viewModel.togglePolicy(this, isEnabled.value)
}
fun toggle(view: View) {
fun toggle() {
isExpanded.toggle()
view.rotationTo(if (isExpanded.value) 225 else 180)
(view.parent as ViewGroup)
.findViewById<View>(R.id.policy_expand_container)
.setRevealed(isExpanded.value)
}
fun toggleNotify(viewModel: SuperuserViewModel) {

View File

@ -19,6 +19,7 @@ import com.topjohnwu.magisk.redesign.compat.CompatFragment
import com.topjohnwu.magisk.redesign.compat.hideKeyboard
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.MotionRevealHelper
import com.topjohnwu.magisk.utils.PinchZoomTouchListener
import org.koin.androidx.viewmodel.ext.android.viewModel
class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>(),
@ -67,13 +68,18 @@ class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
}
})
PinchZoomTouchListener.attachTo(binding.moduleFilterInclude.moduleFilterList)
PinchZoomTouchListener.attachTo(binding.moduleList)
}
override fun onDestroyView() {
listeners.forEach {
binding.moduleRemote.removeOnScrollListener(it)
binding.moduleList.removeOnScrollListener(it)
binding.moduleFilterInclude.moduleFilterList.removeOnScrollListener(it)
}
PinchZoomTouchListener.clear(binding.moduleList)
PinchZoomTouchListener.clear(binding.moduleFilterInclude.moduleFilterList)
super.onDestroyView()
}
@ -101,14 +107,14 @@ class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>
// ---
override fun onReselected() {
binding.moduleRemote
binding.moduleList
.takeIf {
(it.layoutManager as? StaggeredGridLayoutManager)?.let {
it.findFirstVisibleItemPositions(IntArray(it.spanCount)).min()
} ?: 0 > 10
}
?.also { it.scrollToPosition(10) }
.let { binding.moduleRemote }
.let { binding.moduleList }
.also { it.post { it.smoothScrollToPosition(0) } }
}
@ -117,11 +123,11 @@ class ModuleFragment : CompatFragment<ModuleViewModel, FragmentModuleMd2Binding>
override fun onPreBind(binding: FragmentModuleMd2Binding) = Unit
private fun setEndlessScroller() {
val lama = binding.moduleRemote.layoutManager ?: return
val lama = binding.moduleList.layoutManager ?: return
lama.isAutoMeasureEnabled = false
val listener = EndlessRecyclerScrollListener(lama, viewModel::loadRemote)
binding.moduleRemote.addOnScrollListener(listener)
binding.moduleList.addOnScrollListener(listener)
listeners.add(listener)
}

View File

@ -1,9 +1,12 @@
package com.topjohnwu.magisk.redesign.settings
import android.graphics.Insets
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import com.topjohnwu.magisk.redesign.compat.CompatFragment
import com.topjohnwu.magisk.utils.PinchZoomTouchListener
import org.koin.androidx.viewmodel.ext.android.viewModel
class SettingsFragment : CompatFragment<SettingsViewModel, FragmentSettingsMd2Binding>() {
@ -19,6 +22,16 @@ class SettingsFragment : CompatFragment<SettingsViewModel, FragmentSettingsMd2Bi
activity.title = resources.getString(R.string.section_settings)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
PinchZoomTouchListener.attachTo(binding.settingsList)
}
override fun onDestroyView() {
PinchZoomTouchListener.clear(binding.settingsList)
super.onDestroyView()
}
override fun onResume() {
super.onResume()
viewModel.items.forEach { it.refresh() }

View File

@ -1,13 +1,16 @@
package com.topjohnwu.magisk.redesign.superuser
import android.graphics.Insets
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.databinding.FragmentSuperuserMd2Binding
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.redesign.compat.CompatFragment
import com.topjohnwu.magisk.utils.PinchZoomTouchListener
import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment : CompatFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
@ -23,6 +26,16 @@ class SuperuserFragment : CompatFragment<SuperuserViewModel, FragmentSuperuserMd
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
PinchZoomTouchListener.attachTo(binding.superuserList)
}
override fun onDestroyView() {
PinchZoomTouchListener.clear(binding.superuserList)
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_superuser_md2, menu)
}

View File

@ -449,3 +449,11 @@ fun View.setRotationNotAnimated(rotation: Int) {
fun TextView.setTextSafe(text: Int) {
if (text == 0) this.text = null else setText(text)
}
@BindingAdapter("android:onLongClick")
fun View.setOnLongClickListenerBinding(listener: () -> Unit) {
setOnLongClickListener {
listener()
true
}
}

View File

@ -0,0 +1,24 @@
package com.topjohnwu.magisk.utils
import android.view.ScaleGestureDetector
abstract class PinchGestureCallback : ScaleGestureDetector.SimpleOnScaleGestureListener() {
private var startFactor: Float = 1f
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
startFactor = detector?.scaleFactor ?: 1f
return super.onScaleBegin(detector)
}
override fun onScaleEnd(detector: ScaleGestureDetector?) {
val endFactor = detector?.scaleFactor ?: 1f
if (endFactor > startFactor) onZoom()
else if (endFactor < startFactor) onPinch()
}
abstract fun onPinch()
abstract fun onZoom()
}

View File

@ -0,0 +1,66 @@
package com.topjohnwu.magisk.utils
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.transition.TransitionManager
import com.topjohnwu.magisk.Config
import kotlin.math.max
import kotlin.math.min
class PinchZoomTouchListener private constructor(
private val view: RecyclerView,
private val max: Int = 3,
private val min: Int = 1
) : View.OnTouchListener {
private val layoutManager
get() = view.layoutManager
private val pinchListener = object : PinchGestureCallback() {
override fun onPinch() = updateSpanCount(Config.listSpanCount + 1)
override fun onZoom() = updateSpanCount(Config.listSpanCount - 1)
}
private val gestureDetector by lazy { ScaleGestureDetector(view.context, pinchListener) }
init {
updateSpanCount(Config.listSpanCount, false)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
gestureDetector.onTouchEvent(event)
return false
}
private fun updateSpanCount(count: Int, animate: Boolean = true) {
if (animate) {
TransitionManager.beginDelayedTransition(view)
}
val boundCount = max(min, min(max, count))
when (val l = layoutManager) {
is StaggeredGridLayoutManager -> l.spanCount = boundCount
is GridLayoutManager -> l.spanCount = boundCount
else -> Unit
}
Config.listSpanCount = boundCount
}
companion object {
@SuppressLint("ClickableViewAccessibility")
fun attachTo(view: RecyclerView) = view.setOnTouchListener(PinchZoomTouchListener(view))
fun clear(view: View) = view.setOnTouchListener(null)
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M16,17H7V10.5C7,8 9,6 11.5,6C14,6 16,8 16,10.5M18,16V10.5C18,7.43 15.86,4.86 13,4.18V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V4.18C7.13,4.86 5,7.43 5,10.5V16L3,18V19H20V18M11.5,22A2,2 0 0,0 13.5,20H9.5A2,2 0 0,0 11.5,22Z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorError"
android:pathData="M22.11 21.46L2.39 1.73L1.11 3L4.06 5.95C2.78 7.63 2 9.72 2 12C2 17.5 6.5 22 12 22C14.28 22 16.37 21.23 18.05 19.94L20.84 22.73L22.11 21.46M12 20C7.58 20 4 16.42 4 12C4 10.27 4.56 8.68 5.5 7.38L16.62 18.5C15.32 19.45 13.73 20 12 20M8.17 4.97L6.72 3.5C8.25 2.56 10.06 2 12 2C17.5 2 22 6.5 22 12C22 13.94 21.44 15.75 20.5 17.28L19.03 15.83C19.65 14.69 20 13.39 20 12C20 7.58 16.42 4 12 4C10.61 4 9.31 4.35 8.17 4.97Z" />
</vector>

View File

@ -20,7 +20,7 @@
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_remote"
android:id="@+id/module_list"
adapter="@{viewModel.adapter}"
dividerHorizontal="@{R.drawable.divider_l1}"
dividerVertical="@{R.drawable.divider_l1}"

View File

@ -19,6 +19,7 @@
dividerVertical="@{R.drawable.divider_l_50}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:id="@+id/settings_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="false"

View File

@ -25,6 +25,7 @@
items="@{viewModel.items}"
nestedScrollingEnabled="@{false}"
android:layout_width="match_parent"
android:id="@+id/superuser_list"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"

View File

@ -22,50 +22,71 @@
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"
tools:layout_width="200dp">
<LinearLayout
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="match_parent"
android:paddingTop="@dimen/l2"
android:paddingBottom="@dimen/l2">
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/policy_app_icon"
style="@style/WidgetFoundation.Image.Big"
style="@style/WidgetFoundation.Image"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:src="@{item.icon}"
app:layout_constraintEnd_toEndOf="parent"
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.AppCompatImageView
style="@style/WidgetFoundation.Image.Big"
gone="@{item.isEnabled()}"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/policy_app_icon"
app:layout_constraintEnd_toEndOf="@+id/policy_app_icon"
app:layout_constraintStart_toStartOf="@+id/policy_app_icon"
app:layout_constraintTop_toTopOf="@+id/policy_app_icon"
app:srcCompat="@drawable/ic_off"
app:tint="?colorSurface"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/policy_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_generic"
android:layout_marginTop="@dimen/l1"
android:layout_marginRight="@dimen/margin_generic"
android:ellipsize="middle"
android:gravity="center"
android:singleLine="true"
android:gravity="start"
android:maxLines="2"
android:text="@{item.item.appName}"
android:textAppearance="@style/AppearanceFoundation.Title"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textIsSelectable="false"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/policy_app_icon"
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
android:id="@+id/policy_package_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/l1"
android:ellipsize="middle"
android:gravity="center"
android:singleLine="true"
android:gravity="start"
android:maxLines="2"
android:text="@{item.item.packageName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="@android:color/tertiary_text_dark"
@ -74,61 +95,70 @@
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>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/policy_expand_container"
revealFix="@{item.isExpanded()}"
gone="@{!item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@+id/policy_package_name"
android:background="?colorSurfaceVariant"
android:visibility="invisible"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:alpha="0.9"
app:layout_constraintTop_toBottomOf="@+id/policy_package_name"
tools:visibility="visible">
<androidx.appcompat.widget.AppCompatImageView
<com.google.android.material.button.MaterialButton
android:id="@+id/policy_notify"
style="@style/WidgetFoundation.Icon"
android:background="?selectableItemBackground"
style="@style/WidgetFoundation.Button.Text"
isSelected="@{item.shouldNotify}"
tooltipText="@{@string/superuser_toggle_notification}"
android:contentDescription="@string/superuser_toggle_notification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="24dp"
android:onClick="@{() -> item.toggleNotify(viewModel)}"
app:layout_constraintBottom_toTopOf="@+id/policy_delete"
app:layout_constraintEnd_toStartOf="@+id/policy_log"
app:layout_constraintHorizontal_chainStyle="packed"
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_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_notifications"
app:tint="@color/color_state_primary_transient" />
<androidx.appcompat.widget.AppCompatImageView
<com.google.android.material.button.MaterialButton
android:id="@+id/policy_log"
style="@style/WidgetFoundation.Icon"
android:background="?selectableItemBackground"
style="@style/WidgetFoundation.Button.Text"
isSelected="@{item.shouldLog}"
tooltipText="@{@string/superuser_toggle_log}"
android:contentDescription="@string/superuser_toggle_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="24dp"
android:onClick="@{() -> item.toggleLog(viewModel)}"
app:layout_constraintBottom_toBottomOf="@+id/policy_notify"
android:text="@string/superuser_toggle_log"
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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/policy_notify"
app:layout_constraintTop_toTopOf="@+id/policy_notify"
app:srcCompat="@drawable/ic_bug_report"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/policy_notify"
app:tint="@color/color_state_primary_transient" />
<com.google.android.material.button.MaterialButton
android:id="@+id/policy_delete"
style="@style/WidgetFoundation.Button.Text"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="24dp"
android:onClick="@{() -> viewModel.deletePressed(item)}"
android:text="@string/superuser_toggle_revoke"
android:textAllCaps="false"
@ -139,20 +169,12 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/policy_notify"
app:layout_constraintTop_toBottomOf="@+id/policy_log"
app:rippleColor="?colorError" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.AppCompatImageView
style="@style/WidgetFoundation.Icon"
isEnabled="@{item.isEnabled}"
isSelected="@{item.isExpanded}"
android:layout_gravity="top|right"
android:onClick="@{(view) -> item.toggle(view)}"
android:rotation="@{item.isExpanded ? 225 : 180}"
app:srcCompat="@drawable/ic_more_collapse"
tools:ignore="RtlHardcoded" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -86,8 +86,8 @@
<string name="module_state_restore">Restore</string>
<string name="module_action_install_external">Install from storage</string>
<string name="superuser_toggle_log">Toggles logging</string>
<string name="superuser_toggle_notification">Toggles “toast” notifications</string>
<string name="superuser_toggle_log">Logs</string>
<string name="superuser_toggle_notification">Notifications</string>
<string name="superuser_toggle_revoke">Revoke</string>
<string name="hide_filter_hint">Filter by name</string>