Merge "Add controls rearrange activity" into rvc-dev am: 0935694b5a
Change-Id: Ibf291eda825193cb6a062702c8fb3e381548ee33
This commit is contained in:
@@ -679,6 +679,15 @@
|
||||
android:visibleToInstantApps="true">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".controls.management.ControlsEditingActivity"
|
||||
android:theme="@style/Theme.ControlsManagement"
|
||||
android:excludeFromRecents="true"
|
||||
android:showForAllUsers="true"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
|
||||
android:visibleToInstantApps="true">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".controls.management.ControlsFavoritingActivity"
|
||||
android:theme="@style/Theme.ControlsManagement"
|
||||
android:excludeFromRecents="true"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/controls_management_list_margin"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/control_height"
|
||||
android:visibility="gone"
|
||||
>
|
||||
</FrameLayout>
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginStart="40dp"
|
||||
android:layout_marginEnd="40dp"
|
||||
android:background="#4dffffff" />
|
||||
</LinearLayout>
|
||||
@@ -14,18 +14,11 @@
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="@dimen/controls_management_list_margin">
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="@dimen/controls_management_list_margin"
|
||||
/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
27
packages/SystemUI/res/layout/controls_management_editing.xml
Normal file
27
packages/SystemUI/res/layout/controls_management_editing.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="@dimen/controls_management_list_margin"
|
||||
/>
|
||||
|
||||
@@ -2670,8 +2670,11 @@
|
||||
<string name="controls_favorite_default_title">Controls</string>
|
||||
<!-- Controls management controls screen subtitle [CHAR LIMIT=NONE] -->
|
||||
<string name="controls_favorite_subtitle">Choose controls to access from the power menu</string>
|
||||
<!-- Controls management controls screen, user direction for rearranging controls [CHAR LIMIT=NONE] -->
|
||||
<string name="controls_favorite_rearrange">Hold and drag a control to move it</string>
|
||||
<!-- Controls management editing screen, user direction for rearranging controls [CHAR LIMIT=NONE] -->
|
||||
<string name="controls_favorite_rearrange">Hold & drag to rearrange controls</string>
|
||||
|
||||
<!-- Controls management editing screen, text to indicate that all the favorites have been removed [CHAR LIMIT=NONE] -->
|
||||
<string name="controls_favorite_removed">All controls removed</string>
|
||||
|
||||
<!-- Controls management controls screen error on load message [CHAR LIMIT=60] -->
|
||||
<string name="controls_favorite_load_error">The list of all controls could not be loaded.</string>
|
||||
|
||||
@@ -18,10 +18,34 @@ package com.android.systemui.controls
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.service.controls.Control
|
||||
import android.service.controls.DeviceTypes
|
||||
|
||||
interface ControlInterface {
|
||||
val favorite: Boolean
|
||||
val component: ComponentName
|
||||
val controlId: String
|
||||
val title: CharSequence
|
||||
val subtitle: CharSequence
|
||||
val removed: Boolean
|
||||
get() = false
|
||||
@DeviceTypes.DeviceType val deviceType: Int
|
||||
}
|
||||
|
||||
data class ControlStatus(
|
||||
val control: Control,
|
||||
val component: ComponentName,
|
||||
var favorite: Boolean,
|
||||
val removed: Boolean = false
|
||||
)
|
||||
override val component: ComponentName,
|
||||
override var favorite: Boolean,
|
||||
override val removed: Boolean = false
|
||||
) : ControlInterface {
|
||||
override val controlId: String
|
||||
get() = control.controlId
|
||||
|
||||
override val title: CharSequence
|
||||
get() = control.title
|
||||
|
||||
override val subtitle: CharSequence
|
||||
get() = control.subtitle
|
||||
|
||||
@DeviceTypes.DeviceType override val deviceType: Int
|
||||
get() = control.deviceType
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.systemui.controls.controller
|
||||
|
||||
import android.service.controls.Control
|
||||
import android.service.controls.DeviceTypes
|
||||
|
||||
/**
|
||||
@@ -39,6 +40,14 @@ data class ControlInfo(
|
||||
|
||||
companion object {
|
||||
private const val SEPARATOR = ":"
|
||||
fun fromControl(control: Control): ControlInfo {
|
||||
return ControlInfo(
|
||||
control.controlId,
|
||||
control.title,
|
||||
control.subtitle,
|
||||
control.deviceType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,13 +58,4 @@ data class ControlInfo(
|
||||
override fun toString(): String {
|
||||
return "$SEPARATOR$controlId$SEPARATOR$controlTitle$SEPARATOR$deviceType"
|
||||
}
|
||||
|
||||
class Builder {
|
||||
lateinit var controlId: String
|
||||
lateinit var controlTitle: CharSequence
|
||||
lateinit var controlSubtitle: CharSequence
|
||||
var deviceType: Int = DeviceTypes.TYPE_UNKNOWN
|
||||
|
||||
fun build() = ControlInfo(controlId, controlTitle, controlSubtitle, deviceType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,18 @@ interface ControlsController : UserAwareController {
|
||||
*/
|
||||
fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo>
|
||||
|
||||
/**
|
||||
* Get all the favorites for a given structure.
|
||||
*
|
||||
* @param componentName the name of the service that provides the [Control]
|
||||
* @param structureName the name of the structure
|
||||
* @return a list of the current favorites in that structure
|
||||
*/
|
||||
fun getFavoritesForStructure(
|
||||
componentName: ComponentName,
|
||||
structureName: CharSequence
|
||||
): List<ControlInfo>
|
||||
|
||||
/**
|
||||
* Adds a single favorite to a given component and structure
|
||||
* @param componentName the name of the service that provides the [Control]
|
||||
|
||||
@@ -544,6 +544,15 @@ class ControlsControllerImpl @Inject constructor (
|
||||
override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
|
||||
Favorites.getStructuresForComponent(componentName)
|
||||
|
||||
override fun getFavoritesForStructure(
|
||||
componentName: ComponentName,
|
||||
structureName: CharSequence
|
||||
): List<ControlInfo> {
|
||||
return Favorites.getControlsForStructure(
|
||||
StructureInfo(componentName, structureName, emptyList())
|
||||
)
|
||||
}
|
||||
|
||||
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
|
||||
pw.println("ControlsController state:")
|
||||
pw.println(" Available: $available")
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.android.systemui.controls.controller.ControlsBindingControllerImpl
|
||||
import com.android.systemui.controls.controller.ControlsController
|
||||
import com.android.systemui.controls.controller.ControlsControllerImpl
|
||||
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
|
||||
import com.android.systemui.controls.management.ControlsEditingActivity
|
||||
import com.android.systemui.controls.management.ControlsFavoritingActivity
|
||||
import com.android.systemui.controls.management.ControlsListingController
|
||||
import com.android.systemui.controls.management.ControlsListingControllerImpl
|
||||
@@ -71,6 +72,13 @@ abstract class ControlsModule {
|
||||
activity: ControlsFavoritingActivity
|
||||
): Activity
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ControlsEditingActivity::class)
|
||||
abstract fun provideControlsEditingActivity(
|
||||
activity: ControlsEditingActivity
|
||||
): Activity
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ControlsRequestDialog::class)
|
||||
|
||||
@@ -37,23 +37,22 @@ import com.android.systemui.controls.controller.ControlInfo
|
||||
* @property controls List of controls as returned by loading
|
||||
* @property initialFavoriteIds sorted ids of favorite controls.
|
||||
* @property noZoneString text to use as header for all controls that have blank or `null` zone.
|
||||
* @property controlsModelCallback callback to notify that favorites have changed for the first time
|
||||
*/
|
||||
class AllModel(
|
||||
private val controls: List<ControlStatus>,
|
||||
initialFavoriteIds: List<String>,
|
||||
private val emptyZoneString: CharSequence
|
||||
private val emptyZoneString: CharSequence,
|
||||
private val controlsModelCallback: ControlsModel.ControlsModelCallback
|
||||
) : ControlsModel {
|
||||
|
||||
override val favorites: List<ControlInfo.Builder>
|
||||
private var modified = false
|
||||
|
||||
override val favorites: List<ControlInfo>
|
||||
get() = favoriteIds.mapNotNull { id ->
|
||||
val control = controls.firstOrNull { it.control.controlId == id }?.control
|
||||
control?.let {
|
||||
ControlInfo.Builder().apply {
|
||||
controlId = it.controlId
|
||||
controlTitle = it.title
|
||||
controlSubtitle = it.subtitle
|
||||
deviceType = it.deviceType
|
||||
}
|
||||
ControlInfo.fromControl(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,14 +65,18 @@ class AllModel(
|
||||
|
||||
override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
|
||||
val toChange = elements.firstOrNull {
|
||||
it is ControlWrapper && it.controlStatus.control.controlId == controlId
|
||||
} as ControlWrapper?
|
||||
it is ControlStatusWrapper && it.controlStatus.control.controlId == controlId
|
||||
} as ControlStatusWrapper?
|
||||
if (favorite == toChange?.controlStatus?.favorite) return
|
||||
if (favorite) {
|
||||
val changed: Boolean = if (favorite) {
|
||||
favoriteIds.add(controlId)
|
||||
} else {
|
||||
favoriteIds.remove(controlId)
|
||||
}
|
||||
if (changed && !modified) {
|
||||
modified = true
|
||||
controlsModelCallback.onFirstChange()
|
||||
}
|
||||
toChange?.let {
|
||||
it.controlStatus.favorite = favorite
|
||||
}
|
||||
@@ -84,9 +87,9 @@ class AllModel(
|
||||
it.control.zone ?: ""
|
||||
}
|
||||
val output = mutableListOf<ElementWrapper>()
|
||||
var emptyZoneValues: Sequence<ControlWrapper>? = null
|
||||
var emptyZoneValues: Sequence<ControlStatusWrapper>? = null
|
||||
for (zoneName in map.orderedKeys) {
|
||||
val values = map.getValue(zoneName).asSequence().map { ControlWrapper(it) }
|
||||
val values = map.getValue(zoneName).asSequence().map { ControlStatusWrapper(it) }
|
||||
if (TextUtils.isEmpty(zoneName)) {
|
||||
emptyZoneValues = values
|
||||
} else {
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.widget.TextView
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.controls.ControlInterface
|
||||
import com.android.systemui.controls.ui.RenderInfo
|
||||
|
||||
private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
|
||||
@@ -35,11 +36,10 @@ private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
|
||||
/**
|
||||
* Adapter for binding [Control] information to views.
|
||||
*
|
||||
* The model for this adapter is provided by a [FavoriteModel] that is set using
|
||||
* The model for this adapter is provided by a [ControlModel] that is set using
|
||||
* [changeFavoritesModel]. This allows for updating the model if there's a reload.
|
||||
*
|
||||
* @param layoutInflater an inflater for the views in the containing [RecyclerView]
|
||||
* @param onlyFavorites set to true to only display favorites instead of all controls
|
||||
* @property elevation elevation of each control view
|
||||
*/
|
||||
class ControlAdapter(
|
||||
private val elevation: Float
|
||||
@@ -48,11 +48,12 @@ class ControlAdapter(
|
||||
companion object {
|
||||
private const val TYPE_ZONE = 0
|
||||
private const val TYPE_CONTROL = 1
|
||||
private const val TYPE_DIVIDER = 2
|
||||
}
|
||||
|
||||
val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return if (getItemViewType(position) == TYPE_ZONE) 2 else 1
|
||||
return if (getItemViewType(position) != TYPE_CONTROL) 2 else 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +79,10 @@ class ControlAdapter(
|
||||
TYPE_ZONE -> {
|
||||
ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
|
||||
}
|
||||
TYPE_DIVIDER -> {
|
||||
DividerHolder(layoutInflater.inflate(
|
||||
R.layout.controls_horizontal_divider_withEmpty, parent, false))
|
||||
}
|
||||
else -> throw IllegalStateException("Wrong viewType: $viewType")
|
||||
}
|
||||
}
|
||||
@@ -95,11 +100,26 @@ class ControlAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
} else {
|
||||
model?.let {
|
||||
val el = it.elements[position]
|
||||
if (el is ControlInterface) {
|
||||
holder.updateFavorite(el.favorite)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
model?.let {
|
||||
return when (it.elements.get(position)) {
|
||||
is ZoneNameWrapper -> TYPE_ZONE
|
||||
is ControlWrapper -> TYPE_CONTROL
|
||||
is ControlStatusWrapper -> TYPE_CONTROL
|
||||
is ControlInfoWrapper -> TYPE_CONTROL
|
||||
is DividerWrapper -> TYPE_DIVIDER
|
||||
}
|
||||
} ?: throw IllegalStateException("Getting item type for null model")
|
||||
}
|
||||
@@ -115,6 +135,24 @@ sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
* Bind the data from the model into the view
|
||||
*/
|
||||
abstract fun bindData(wrapper: ElementWrapper)
|
||||
|
||||
open fun updateFavorite(favorite: Boolean) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder for using with [DividerWrapper] to display a divider between zones.
|
||||
*
|
||||
* The divider can be shown or hidden. It also has a frame view the height of a control, that can
|
||||
* be toggled visible or gone.
|
||||
*/
|
||||
private class DividerHolder(view: View) : Holder(view) {
|
||||
private val frame: View = itemView.requireViewById(R.id.frame)
|
||||
private val divider: View = itemView.requireViewById(R.id.divider)
|
||||
override fun bindData(wrapper: ElementWrapper) {
|
||||
wrapper as DividerWrapper
|
||||
frame.visibility = if (wrapper.showNone) View.VISIBLE else View.GONE
|
||||
divider.visibility = if (wrapper.showDivider) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,11 +168,14 @@ private class ZoneHolder(view: View) : Holder(view) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder for using with [ControlWrapper] to display names of zones.
|
||||
* Holder for using with [ControlStatusWrapper] to display names of zones.
|
||||
* @param favoriteCallback this callback will be called whenever the favorite state of the
|
||||
* [Control] this view represents changes.
|
||||
*/
|
||||
private class ControlHolder(view: View, val favoriteCallback: ModelFavoriteChanger) : Holder(view) {
|
||||
internal class ControlHolder(
|
||||
view: View,
|
||||
val favoriteCallback: ModelFavoriteChanger
|
||||
) : Holder(view) {
|
||||
private val icon: ImageView = itemView.requireViewById(R.id.icon)
|
||||
private val title: TextView = itemView.requireViewById(R.id.title)
|
||||
private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
|
||||
@@ -144,20 +185,23 @@ private class ControlHolder(view: View, val favoriteCallback: ModelFavoriteChang
|
||||
}
|
||||
|
||||
override fun bindData(wrapper: ElementWrapper) {
|
||||
wrapper as ControlWrapper
|
||||
val data = wrapper.controlStatus
|
||||
val renderInfo = getRenderInfo(data.component, data.control.deviceType)
|
||||
title.text = data.control.title
|
||||
subtitle.text = data.control.subtitle
|
||||
favorite.isChecked = data.favorite
|
||||
removed.text = if (data.removed) "Removed" else ""
|
||||
wrapper as ControlInterface
|
||||
val renderInfo = getRenderInfo(wrapper.component, wrapper.deviceType)
|
||||
title.text = wrapper.title
|
||||
subtitle.text = wrapper.subtitle
|
||||
favorite.isChecked = wrapper.favorite
|
||||
removed.text = if (wrapper.removed) "Removed" else ""
|
||||
itemView.setOnClickListener {
|
||||
favorite.isChecked = !favorite.isChecked
|
||||
favoriteCallback(data.control.controlId, favorite.isChecked)
|
||||
favoriteCallback(wrapper.controlId, favorite.isChecked)
|
||||
}
|
||||
applyRenderInfo(renderInfo)
|
||||
}
|
||||
|
||||
override fun updateFavorite(favorite: Boolean) {
|
||||
this.favorite.isChecked = favorite
|
||||
}
|
||||
|
||||
private fun getRenderInfo(
|
||||
component: ComponentName,
|
||||
@DeviceTypes.DeviceType deviceType: Int
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.controls.management
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewStub
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.controls.controller.ControlsControllerImpl
|
||||
import com.android.systemui.controls.controller.StructureInfo
|
||||
import com.android.systemui.settings.CurrentUserTracker
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Activity for rearranging and removing controls for a given structure
|
||||
*/
|
||||
class ControlsEditingActivity @Inject constructor(
|
||||
private val controller: ControlsControllerImpl,
|
||||
broadcastDispatcher: BroadcastDispatcher
|
||||
) : Activity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ControlsEditingActivity"
|
||||
private const val EXTRA_STRUCTURE = ControlsFavoritingActivity.EXTRA_STRUCTURE
|
||||
private val SUBTITLE_ID = R.string.controls_favorite_rearrange
|
||||
private val EMPTY_TEXT_ID = R.string.controls_favorite_removed
|
||||
}
|
||||
|
||||
private lateinit var component: ComponentName
|
||||
private lateinit var structure: CharSequence
|
||||
private lateinit var model: FavoritesModel
|
||||
private lateinit var subtitle: TextView
|
||||
private lateinit var saveButton: View
|
||||
|
||||
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
|
||||
private val startingUser = controller.currentUserId
|
||||
|
||||
override fun onUserSwitched(newUserId: Int) {
|
||||
if (newUserId != startingUser) {
|
||||
stopTracking()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)?.let {
|
||||
component = it
|
||||
} ?: run(this::finish)
|
||||
|
||||
intent.getCharSequenceExtra(EXTRA_STRUCTURE)?.let {
|
||||
structure = it
|
||||
} ?: run(this::finish)
|
||||
|
||||
bindViews()
|
||||
|
||||
bindButtons()
|
||||
|
||||
setUpList()
|
||||
|
||||
currentUserTracker.startTracking()
|
||||
}
|
||||
|
||||
private fun bindViews() {
|
||||
setContentView(R.layout.controls_management)
|
||||
requireViewById<ViewStub>(R.id.stub).apply {
|
||||
layoutResource = R.layout.controls_management_editing
|
||||
inflate()
|
||||
}
|
||||
requireViewById<TextView>(R.id.title).text = structure
|
||||
subtitle = requireViewById<TextView>(R.id.subtitle).apply {
|
||||
setText(SUBTITLE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindButtons() {
|
||||
requireViewById<Button>(R.id.other_apps).apply {
|
||||
visibility = View.VISIBLE
|
||||
setText(R.string.controls_menu_add)
|
||||
setOnClickListener {
|
||||
saveFavorites()
|
||||
val intent = Intent(this@ControlsEditingActivity,
|
||||
ControlsFavoritingActivity::class.java).apply {
|
||||
putExtras(this@ControlsEditingActivity.intent)
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_SINGLE_STRUCTURE, true)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
saveButton = requireViewById<Button>(R.id.done).apply {
|
||||
isEnabled = false
|
||||
setText(R.string.save)
|
||||
setOnClickListener {
|
||||
saveFavorites()
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFavorites() {
|
||||
controller.replaceFavoritesForStructure(
|
||||
StructureInfo(component, structure, model.favorites))
|
||||
}
|
||||
|
||||
private val favoritesModelCallback = object : FavoritesModel.FavoritesModelCallback {
|
||||
override fun onNoneChanged(showNoFavorites: Boolean) {
|
||||
if (showNoFavorites) {
|
||||
subtitle.setText(EMPTY_TEXT_ID)
|
||||
} else {
|
||||
subtitle.setText(SUBTITLE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFirstChange() {
|
||||
saveButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpList() {
|
||||
val controls = controller.getFavoritesForStructure(component, structure)
|
||||
model = FavoritesModel(component, controls, favoritesModelCallback)
|
||||
val elevation = resources.getFloat(R.dimen.control_card_elevation)
|
||||
val adapter = ControlAdapter(elevation)
|
||||
val recycler = requireViewById<RecyclerView>(R.id.list)
|
||||
val margin = resources
|
||||
.getDimensionPixelSize(R.dimen.controls_card_margin)
|
||||
val itemDecorator = MarginItemDecorator(margin, margin)
|
||||
|
||||
recycler.apply {
|
||||
this.adapter = adapter
|
||||
layoutManager = GridLayoutManager(recycler.context, 2).apply {
|
||||
spanSizeLookup = adapter.spanSizeLookup
|
||||
}
|
||||
addItemDecoration(itemDecorator)
|
||||
}
|
||||
adapter.changeModel(model)
|
||||
model.attachAdapter(adapter)
|
||||
ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recycler)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
currentUserTracker.stopTracking()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ class ControlsFavoritingActivity @Inject constructor(
|
||||
|
||||
// If provided, show this structure page first
|
||||
const val EXTRA_STRUCTURE = "extra_structure"
|
||||
const val EXTRA_SINGLE_STRUCTURE = "extra_single_structure"
|
||||
private const val TOOLTIP_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT
|
||||
private const val TOOLTIP_MAX_SHOWN = 2
|
||||
}
|
||||
@@ -131,6 +132,12 @@ class ControlsFavoritingActivity @Inject constructor(
|
||||
currentUserTracker.startTracking()
|
||||
}
|
||||
|
||||
private val controlsModelCallback = object : ControlsModel.ControlsModelCallback {
|
||||
override fun onFirstChange() {
|
||||
doneButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadControls() {
|
||||
component?.let {
|
||||
statusText.text = resources.getText(com.android.internal.R.string.loading)
|
||||
@@ -142,15 +149,20 @@ class ControlsFavoritingActivity @Inject constructor(
|
||||
val error = data.errorOnLoad
|
||||
val controlsByStructure = allControls.groupBy { it.control.structure ?: "" }
|
||||
listOfStructures = controlsByStructure.map {
|
||||
StructureContainer(it.key, AllModel(it.value, favoriteKeys, emptyZoneString))
|
||||
StructureContainer(it.key, AllModel(
|
||||
it.value, favoriteKeys, emptyZoneString, controlsModelCallback))
|
||||
}.sortedWith(comparator)
|
||||
|
||||
val structureIndex = listOfStructures.indexOfFirst {
|
||||
sc -> sc.structureName == structureExtra
|
||||
}.let { if (it == -1) 0 else it }
|
||||
|
||||
// If we were requested to show a single structure, set the list to just that one
|
||||
if (intent.getBooleanExtra(EXTRA_SINGLE_STRUCTURE, false)) {
|
||||
listOfStructures = listOf(listOfStructures[structureIndex])
|
||||
}
|
||||
|
||||
executor.execute {
|
||||
doneButton.isEnabled = true
|
||||
structurePager.adapter = StructureAdapter(listOfStructures)
|
||||
structurePager.setCurrentItem(structureIndex)
|
||||
if (error) {
|
||||
@@ -275,7 +287,7 @@ class ControlsFavoritingActivity @Inject constructor(
|
||||
setOnClickListener {
|
||||
if (component == null) return@setOnClickListener
|
||||
listOfStructures.forEach {
|
||||
val favoritesForStorage = it.model.favorites.map { it.build() }
|
||||
val favoritesForStorage = it.model.favorites
|
||||
controller.replaceFavoritesForStructure(
|
||||
StructureInfo(component!!, it.structureName, favoritesForStorage)
|
||||
)
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.systemui.controls.management
|
||||
|
||||
import android.content.ComponentName
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.systemui.controls.ControlInterface
|
||||
import com.android.systemui.controls.ControlStatus
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
|
||||
@@ -27,12 +30,12 @@ import com.android.systemui.controls.controller.ControlInfo
|
||||
interface ControlsModel {
|
||||
|
||||
/**
|
||||
* List of favorites (builders) in order.
|
||||
* List of favorites in order.
|
||||
*
|
||||
* This should be obtained prior to storing the favorites using
|
||||
* [ControlsController.replaceFavoritesForComponent].
|
||||
*/
|
||||
val favorites: List<ControlInfo.Builder>
|
||||
val favorites: List<ControlInfo>
|
||||
|
||||
/**
|
||||
* List of all the elements to display by the corresponding [RecyclerView].
|
||||
@@ -48,6 +51,24 @@ interface ControlsModel {
|
||||
* Move an item (in elements) from one position to another.
|
||||
*/
|
||||
fun onMoveItem(from: Int, to: Int) {}
|
||||
|
||||
/**
|
||||
* Attach an adapter to the model.
|
||||
*
|
||||
* This can be used to notify the adapter of changes in the model.
|
||||
*/
|
||||
fun attachAdapter(adapter: RecyclerView.Adapter<*>) {}
|
||||
|
||||
/**
|
||||
* Callback to notify elements (other than the adapter) of relevant changes in the model.
|
||||
*/
|
||||
interface ControlsModelCallback {
|
||||
|
||||
/**
|
||||
* Use to notify that the model has changed for the first time
|
||||
*/
|
||||
fun onFirstChange()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,5 +76,29 @@ interface ControlsModel {
|
||||
* [ControlAdapter].
|
||||
*/
|
||||
sealed class ElementWrapper
|
||||
|
||||
data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
|
||||
data class ControlWrapper(val controlStatus: ControlStatus) : ElementWrapper()
|
||||
|
||||
data class ControlStatusWrapper(
|
||||
val controlStatus: ControlStatus
|
||||
) : ElementWrapper(), ControlInterface by controlStatus
|
||||
|
||||
data class ControlInfoWrapper(
|
||||
override val component: ComponentName,
|
||||
val controlInfo: ControlInfo,
|
||||
override var favorite: Boolean
|
||||
) : ElementWrapper(), ControlInterface {
|
||||
override val controlId: String
|
||||
get() = controlInfo.controlId
|
||||
override val title: CharSequence
|
||||
get() = controlInfo.controlTitle
|
||||
override val subtitle: CharSequence
|
||||
get() = controlInfo.controlSubtitle
|
||||
override val deviceType: Int
|
||||
get() = controlInfo.deviceType
|
||||
}
|
||||
|
||||
data class DividerWrapper(
|
||||
var showNone: Boolean = false,
|
||||
var showDivider: Boolean = false
|
||||
) : ElementWrapper()
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.controls.management
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.android.systemui.controls.ControlStatus
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
|
||||
/**
|
||||
* Model for keeping track of current favorites and their order.
|
||||
*
|
||||
* This model is to be used with two [ControlAdapter] one that shows only favorites in the current
|
||||
* order and another that shows all controls, separated by zone. When the favorite state of any
|
||||
* control is modified or when the favorites are reordered, the adapters are notified of the change.
|
||||
*
|
||||
* @param listControls list of all the [ControlStatus] to display. This includes controls currently
|
||||
* marked as favorites as well as those that have been removed (not returned
|
||||
* from load)
|
||||
* @param listFavoritesIds list of the [Control.controlId] for all the favorites, including those
|
||||
* that have been removed.
|
||||
* @param favoritesAdapter [ControlAdapter] used by the [RecyclerView] that shows only favorites
|
||||
* @param allAdapter [ControlAdapter] used by the [RecyclerView] that shows all controls
|
||||
*/
|
||||
class FavoriteModel(
|
||||
private val listControls: List<ControlStatus>,
|
||||
listFavoritesIds: List<String>,
|
||||
private val favoritesAdapter: ControlAdapter,
|
||||
private val allAdapter: ControlAdapter
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FavoriteModel"
|
||||
}
|
||||
|
||||
/**
|
||||
* List of favorite controls ([ControlWrapper]) in order.
|
||||
*
|
||||
* Initially, this list will give a list of wrappers in the order specified by the constructor
|
||||
* variable `listFavoriteIds`.
|
||||
*
|
||||
* As the favorites are added, removed or moved, this list will keep track of those changes.
|
||||
*/
|
||||
val favorites: List<ControlWrapper> = listFavoritesIds.map { id ->
|
||||
ControlWrapper(listControls.first { it.control.controlId == id })
|
||||
}.toMutableList()
|
||||
|
||||
/**
|
||||
* List of all controls by zones.
|
||||
*
|
||||
* Lists all the controls with the zone names interleaved as a flat list. After each zone name,
|
||||
* the controls in that zone are listed. Zones are listed in alphabetical order
|
||||
*/
|
||||
val all: List<ElementWrapper> = listControls.groupBy { it.control.zone }
|
||||
.mapKeys { it.key ?: "" } // map null to empty
|
||||
.toSortedMap(CharSequenceComparator())
|
||||
.flatMap {
|
||||
val controls = it.value.map { ControlWrapper(it) }
|
||||
if (!TextUtils.isEmpty(it.key)) {
|
||||
listOf(ZoneNameWrapper(it.key)) + controls
|
||||
} else {
|
||||
controls
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the favorite status of a [Control].
|
||||
*
|
||||
* This can be invoked from any of the [ControlAdapter]. It will change the status of that
|
||||
* control and either add it to the list of favorites (at the end) or remove it from it.
|
||||
*
|
||||
* Removing the favorite status from a Removed control will make it disappear completely if
|
||||
* changes are saved.
|
||||
*
|
||||
* @param controlId the id of the [Control] to change the status
|
||||
* @param favorite `true` if and only if it's set to be a favorite.
|
||||
*/
|
||||
fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
|
||||
favorites as MutableList
|
||||
val index = all.indexOfFirst {
|
||||
it is ControlWrapper && it.controlStatus.control.controlId == controlId
|
||||
}
|
||||
val control = (all[index] as ControlWrapper).controlStatus
|
||||
if (control.favorite == favorite) {
|
||||
Log.d(TAG, "Changing favorite to same state for ${control.control.controlId} ")
|
||||
return
|
||||
} else {
|
||||
control.favorite = favorite
|
||||
}
|
||||
allAdapter.notifyItemChanged(index)
|
||||
if (favorite) {
|
||||
favorites.add(all[index] as ControlWrapper)
|
||||
favoritesAdapter.notifyItemInserted(favorites.size - 1)
|
||||
} else {
|
||||
val i = favorites.indexOfFirst { it.controlStatus.control.controlId == controlId }
|
||||
favorites.removeAt(i)
|
||||
favoritesAdapter.notifyItemRemoved(i)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move items in the model and notify the [favoritesAdapter].
|
||||
*/
|
||||
fun onMoveItem(from: Int, to: Int) {
|
||||
if (from < to) {
|
||||
for (i in from until to) {
|
||||
Collections.swap(favorites, i, i + 1)
|
||||
}
|
||||
} else {
|
||||
for (i in from downTo to + 1) {
|
||||
Collections.swap(favorites, i, i - 1)
|
||||
}
|
||||
}
|
||||
favoritesAdapter.notifyItemMoved(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares [CharSequence] as [String].
|
||||
*
|
||||
* It will have empty strings as the first element
|
||||
*/
|
||||
class CharSequenceComparator : Comparator<CharSequence> {
|
||||
override fun compare(p0: CharSequence?, p1: CharSequence?): Int {
|
||||
if (p0 == null && p1 == null) return 0
|
||||
else if (p0 == null && p1 != null) return -1
|
||||
else if (p0 != null && p1 == null) return 1
|
||||
return p0.toString().compareTo(p1.toString())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.controls.management
|
||||
|
||||
import android.content.ComponentName
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.systemui.controls.ControlInterface
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
import java.util.Collections
|
||||
|
||||
/**
|
||||
* Model used to show and rearrange favorites.
|
||||
*
|
||||
* The model will show all the favorite controls and a divider that can be toggled visible/gone.
|
||||
* It will place the items selected as favorites before the divider and the ones unselected after.
|
||||
*
|
||||
* @property componentName used by the [ControlAdapter] to retrieve resources.
|
||||
* @property favorites list of current favorites
|
||||
* @property favoritesModelCallback callback to notify on first change and empty favorites
|
||||
*/
|
||||
class FavoritesModel(
|
||||
private val componentName: ComponentName,
|
||||
favorites: List<ControlInfo>,
|
||||
private val favoritesModelCallback: FavoritesModelCallback
|
||||
) : ControlsModel {
|
||||
|
||||
private var adapter: RecyclerView.Adapter<*>? = null
|
||||
private var modified = false
|
||||
|
||||
override fun attachAdapter(adapter: RecyclerView.Adapter<*>) {
|
||||
this.adapter = adapter
|
||||
}
|
||||
|
||||
override val favorites: List<ControlInfo>
|
||||
get() = elements.take(dividerPosition).map {
|
||||
(it as ControlInfoWrapper).controlInfo
|
||||
}
|
||||
|
||||
override val elements: List<ElementWrapper> = favorites.map {
|
||||
ControlInfoWrapper(componentName, it, true)
|
||||
} + DividerWrapper()
|
||||
|
||||
/**
|
||||
* Indicates the position of the divider to determine
|
||||
*/
|
||||
private var dividerPosition = elements.size - 1
|
||||
|
||||
override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
|
||||
val position = elements.indexOfFirst { it is ControlInterface && it.controlId == controlId }
|
||||
if (position == -1) {
|
||||
return // controlId not found
|
||||
}
|
||||
if (position < dividerPosition && favorite || position > dividerPosition && !favorite) {
|
||||
return // Does not change favorite status
|
||||
}
|
||||
if (favorite) {
|
||||
onMoveItemInternal(position, dividerPosition)
|
||||
} else {
|
||||
onMoveItemInternal(position, elements.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMoveItem(from: Int, to: Int) {
|
||||
onMoveItemInternal(from, to)
|
||||
}
|
||||
|
||||
private fun updateDividerNone(oldDividerPosition: Int, show: Boolean) {
|
||||
(elements[oldDividerPosition] as DividerWrapper).showNone = show
|
||||
favoritesModelCallback.onNoneChanged(show)
|
||||
}
|
||||
|
||||
private fun updateDividerShow(oldDividerPosition: Int, show: Boolean) {
|
||||
(elements[oldDividerPosition] as DividerWrapper).showDivider = show
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the update in the model.
|
||||
*
|
||||
* * update the favorite field of the [ControlInterface]
|
||||
* * update the fields of the [DividerWrapper]
|
||||
* * move the corresponding element in [elements]
|
||||
*
|
||||
* It may emit the following signals:
|
||||
* * [RecyclerView.Adapter.notifyItemChanged] if a [ControlInterface.favorite] has changed
|
||||
* (in the new position) or if the information in [DividerWrapper] has changed (in the
|
||||
* old position).
|
||||
* * [RecyclerView.Adapter.notifyItemMoved]
|
||||
* * [FavoritesModelCallback.onNoneChanged] whenever we go from 1 to 0 favorites and back
|
||||
* * [ControlsModel.ControlsModelCallback.onFirstChange] upon the first change in the model
|
||||
*/
|
||||
private fun onMoveItemInternal(from: Int, to: Int) {
|
||||
if (from == dividerPosition) return // divider does not move
|
||||
var changed = false
|
||||
if (from < dividerPosition && to >= dividerPosition ||
|
||||
from > dividerPosition && to <= dividerPosition) {
|
||||
if (from < dividerPosition && to >= dividerPosition) {
|
||||
// favorite to not favorite
|
||||
(elements[from] as ControlInfoWrapper).favorite = false
|
||||
} else if (from > dividerPosition && to <= dividerPosition) {
|
||||
// not favorite to favorite
|
||||
(elements[from] as ControlInfoWrapper).favorite = true
|
||||
}
|
||||
changed = true
|
||||
updateDivider(from, to)
|
||||
}
|
||||
moveElement(from, to)
|
||||
adapter?.notifyItemMoved(from, to)
|
||||
if (changed) {
|
||||
adapter?.notifyItemChanged(to, Any())
|
||||
}
|
||||
if (!modified) {
|
||||
modified = true
|
||||
favoritesModelCallback.onFirstChange()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDivider(from: Int, to: Int) {
|
||||
var dividerChanged = false
|
||||
val oldDividerPosition = dividerPosition
|
||||
if (from < dividerPosition && to >= dividerPosition) { // favorite to not favorite
|
||||
dividerPosition--
|
||||
if (dividerPosition == 0) {
|
||||
updateDividerNone(oldDividerPosition, true)
|
||||
dividerChanged = true
|
||||
}
|
||||
if (dividerPosition == elements.size - 2) {
|
||||
updateDividerShow(oldDividerPosition, true)
|
||||
dividerChanged = true
|
||||
}
|
||||
} else if (from > dividerPosition && to <= dividerPosition) { // not favorite to favorite
|
||||
dividerPosition++
|
||||
if (dividerPosition == 1) {
|
||||
updateDividerNone(oldDividerPosition, false)
|
||||
dividerChanged = true
|
||||
}
|
||||
if (dividerPosition == elements.size - 1) {
|
||||
updateDividerShow(oldDividerPosition, false)
|
||||
dividerChanged = true
|
||||
}
|
||||
}
|
||||
if (dividerChanged) {
|
||||
adapter?.notifyItemChanged(oldDividerPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveElement(from: Int, to: Int) {
|
||||
if (from < to) {
|
||||
for (i in from until to) {
|
||||
Collections.swap(elements, i, i + 1)
|
||||
}
|
||||
} else {
|
||||
for (i in from downTo to + 1) {
|
||||
Collections.swap(elements, i, i - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch helper to facilitate dragging in the [RecyclerView].
|
||||
*
|
||||
* Only views above the divider line (favorites) can be dragged or accept drops.
|
||||
*/
|
||||
val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(0, 0) {
|
||||
|
||||
private val MOVEMENT = ItemTouchHelper.UP or
|
||||
ItemTouchHelper.DOWN or
|
||||
ItemTouchHelper.LEFT or
|
||||
ItemTouchHelper.RIGHT
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
onMoveItem(viewHolder.adapterPosition, target.adapterPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
if (viewHolder.adapterPosition < dividerPosition) {
|
||||
return ItemTouchHelper.Callback.makeMovementFlags(MOVEMENT, 0)
|
||||
} else {
|
||||
return ItemTouchHelper.Callback.makeMovementFlags(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun canDropOver(
|
||||
recyclerView: RecyclerView,
|
||||
current: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return target.adapterPosition < dividerPosition
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||
|
||||
override fun isItemViewSwipeEnabled() = false
|
||||
}
|
||||
|
||||
interface FavoritesModelCallback : ControlsModel.ControlsModelCallback {
|
||||
fun onNoneChanged(showNoFavorites: Boolean)
|
||||
}
|
||||
}
|
||||
@@ -33,16 +33,16 @@ import android.os.Process
|
||||
import android.provider.Settings
|
||||
import android.service.controls.Control
|
||||
import android.service.controls.actions.ControlAction
|
||||
import android.util.TypedValue
|
||||
import android.util.Log
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.util.TypedValue
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
@@ -50,23 +50,21 @@ import android.widget.LinearLayout
|
||||
import android.widget.ListPopupWindow
|
||||
import android.widget.Space
|
||||
import android.widget.TextView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.controls.ControlsServiceInfo
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
import com.android.systemui.controls.controller.ControlsController
|
||||
import com.android.systemui.controls.controller.StructureInfo
|
||||
import com.android.systemui.controls.management.ControlsEditingActivity
|
||||
import com.android.systemui.controls.management.ControlsFavoritingActivity
|
||||
import com.android.systemui.controls.management.ControlsListingController
|
||||
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||
import com.android.systemui.R
|
||||
|
||||
import dagger.Lazy
|
||||
|
||||
import java.text.Collator
|
||||
import java.util.function.Consumer
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -212,16 +210,30 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
}
|
||||
|
||||
private fun startFavoritingActivity(context: Context, si: StructureInfo) {
|
||||
val i = Intent(context, ControlsFavoritingActivity::class.java).apply {
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_APP,
|
||||
controlsListingController.get().getAppLabel(si.componentName))
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
|
||||
putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
|
||||
startTargetedActivity(context, si, ControlsFavoritingActivity::class.java)
|
||||
}
|
||||
|
||||
private fun startEditingActivity(context: Context, si: StructureInfo) {
|
||||
startTargetedActivity(context, si, ControlsEditingActivity::class.java)
|
||||
}
|
||||
|
||||
private fun startTargetedActivity(context: Context, si: StructureInfo, klazz: Class<*>) {
|
||||
val i = Intent(context, klazz).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
putIntentExtras(i, si)
|
||||
startActivity(context, i)
|
||||
}
|
||||
|
||||
private fun putIntentExtras(intent: Intent, si: StructureInfo) {
|
||||
intent.apply {
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_APP,
|
||||
controlsListingController.get().getAppLabel(si.componentName))
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, si.structure)
|
||||
putExtra(Intent.EXTRA_COMPONENT_NAME, si.componentName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startProviderSelectorActivity(context: Context) {
|
||||
val i = Intent(context, ControlsProviderSelectorActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@@ -255,6 +267,7 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
private fun createMenu() {
|
||||
val items = arrayOf(
|
||||
context.resources.getString(R.string.controls_menu_add),
|
||||
context.resources.getString(R.string.controls_menu_edit),
|
||||
"Reset"
|
||||
)
|
||||
var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
|
||||
@@ -275,8 +288,10 @@ class ControlsUiControllerImpl @Inject constructor (
|
||||
when (pos) {
|
||||
// 0: Add Control
|
||||
0 -> startFavoritingActivity(view.context, selectedStructure)
|
||||
// 1: TEMPORARY for reset controls
|
||||
1 -> showResetConfirmation()
|
||||
// 1: Edit controls
|
||||
1 -> startEditingActivity(view.context, selectedStructure)
|
||||
// 2: TEMPORARY for reset controls
|
||||
2 -> showResetConfirmation()
|
||||
else -> Log.w(ControlsUiController.TAG,
|
||||
"Unsupported index ($pos) on 'more' menu selection")
|
||||
}
|
||||
|
||||
@@ -86,9 +86,6 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
@Captor
|
||||
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
|
||||
|
||||
@Captor
|
||||
private lateinit var booleanConsumer: ArgumentCaptor<Consumer<Boolean>>
|
||||
|
||||
@Captor
|
||||
private lateinit var controlLoadCallbackCaptor:
|
||||
ArgumentCaptor<ControlsBindingController.LoadCallback>
|
||||
@@ -936,4 +933,33 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
verifyNoMoreInteractions(persistenceWrapper)
|
||||
verifyNoMoreInteractions(auxiliaryPersistenceWrapper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetFavoritesForStructure() {
|
||||
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
|
||||
controller.replaceFavoritesForStructure(
|
||||
TEST_STRUCTURE_INFO_2.copy(componentName = TEST_COMPONENT))
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
assertEquals(TEST_STRUCTURE_INFO.controls,
|
||||
controller.getFavoritesForStructure(TEST_COMPONENT, TEST_STRUCTURE))
|
||||
assertEquals(TEST_STRUCTURE_INFO_2.controls,
|
||||
controller.getFavoritesForStructure(TEST_COMPONENT, TEST_STRUCTURE_2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetFavoritesForStructure_wrongStructure() {
|
||||
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
assertTrue(controller.getFavoritesForStructure(TEST_COMPONENT, TEST_STRUCTURE_2).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetFavoritesForStructure_wrongComponent() {
|
||||
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
assertTrue(controller.getFavoritesForStructure(TEST_COMPONENT_2, TEST_STRUCTURE).isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@@ -43,6 +45,8 @@ class AllModelTest : SysuiTestCase() {
|
||||
|
||||
@Mock
|
||||
lateinit var pendingIntent: PendingIntent
|
||||
@Mock
|
||||
lateinit var controlsModelCallback: ControlsModel.ControlsModelCallback
|
||||
|
||||
val idPrefix = "controlId"
|
||||
val favoritesIndices = listOf(7, 3, 1, 9)
|
||||
@@ -84,7 +88,7 @@ class AllModelTest : SysuiTestCase() {
|
||||
it in favoritesIndices
|
||||
)
|
||||
}
|
||||
model = AllModel(controls, favoritesList, EMPTY_STRING)
|
||||
model = AllModel(controls, favoritesList, EMPTY_STRING, controlsModelCallback)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,28 +97,28 @@ class AllModelTest : SysuiTestCase() {
|
||||
// Zones are sorted by order of appearance, with empty at the end with special header.
|
||||
val expected = listOf(
|
||||
ZoneNameWrapper("1"),
|
||||
ControlWrapper(controls[0]),
|
||||
ControlWrapper(controls[3]),
|
||||
ControlWrapper(controls[6]),
|
||||
ControlWrapper(controls[9]),
|
||||
ControlStatusWrapper(controls[0]),
|
||||
ControlStatusWrapper(controls[3]),
|
||||
ControlStatusWrapper(controls[6]),
|
||||
ControlStatusWrapper(controls[9]),
|
||||
ZoneNameWrapper("2"),
|
||||
ControlWrapper(controls[1]),
|
||||
ControlWrapper(controls[4]),
|
||||
ControlWrapper(controls[7]),
|
||||
ControlStatusWrapper(controls[1]),
|
||||
ControlStatusWrapper(controls[4]),
|
||||
ControlStatusWrapper(controls[7]),
|
||||
ZoneNameWrapper("0"),
|
||||
ControlWrapper(controls[2]),
|
||||
ControlWrapper(controls[5]),
|
||||
ControlWrapper(controls[8]),
|
||||
ControlStatusWrapper(controls[2]),
|
||||
ControlStatusWrapper(controls[5]),
|
||||
ControlStatusWrapper(controls[8]),
|
||||
ZoneNameWrapper(EMPTY_STRING),
|
||||
ControlWrapper(controls[10]),
|
||||
ControlWrapper(controls[11])
|
||||
ControlStatusWrapper(controls[10]),
|
||||
ControlStatusWrapper(controls[11])
|
||||
)
|
||||
expected.zip(model.elements).forEachIndexed { index, it ->
|
||||
assertEquals("Error in item at index $index", it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sameControl(controlInfo: ControlInfo.Builder, control: Control): Boolean {
|
||||
private fun sameControl(controlInfo: ControlInfo, control: Control): Boolean {
|
||||
return controlInfo.controlId == control.controlId &&
|
||||
controlInfo.controlTitle == control.title &&
|
||||
controlInfo.controlSubtitle == control.subtitle &&
|
||||
@@ -124,10 +128,11 @@ class AllModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testAllEmpty_noHeader() {
|
||||
val selected_controls = listOf(controls[10], controls[11])
|
||||
val new_model = AllModel(selected_controls, emptyList(), EMPTY_STRING)
|
||||
val new_model = AllModel(selected_controls, emptyList(), EMPTY_STRING,
|
||||
controlsModelCallback)
|
||||
val expected = listOf(
|
||||
ControlWrapper(controls[10]),
|
||||
ControlWrapper(controls[11])
|
||||
ControlStatusWrapper(controls[10]),
|
||||
ControlStatusWrapper(controls[11])
|
||||
)
|
||||
|
||||
expected.zip(new_model.elements).forEachIndexed { index, it ->
|
||||
@@ -154,6 +159,8 @@ class AllModelTest : SysuiTestCase() {
|
||||
model.favorites.zip(expectedFavorites).forEach {
|
||||
assertTrue(sameControl(it.first, it.second))
|
||||
}
|
||||
|
||||
verify(controlsModelCallback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -163,10 +170,12 @@ class AllModelTest : SysuiTestCase() {
|
||||
model.changeFavoriteStatus(id, true)
|
||||
assertTrue(
|
||||
(model.elements.first {
|
||||
it is ControlWrapper && it.controlStatus.control.controlId == id
|
||||
} as ControlWrapper)
|
||||
it is ControlStatusWrapper && it.controlStatus.control.controlId == id
|
||||
} as ControlStatusWrapper)
|
||||
.controlStatus.favorite
|
||||
)
|
||||
|
||||
verify(controlsModelCallback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -180,6 +189,8 @@ class AllModelTest : SysuiTestCase() {
|
||||
model.favorites.zip(expectedFavorites).forEach {
|
||||
assertTrue(sameControl(it.first, it.second))
|
||||
}
|
||||
|
||||
verify(controlsModelCallback, never()).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -194,6 +205,8 @@ class AllModelTest : SysuiTestCase() {
|
||||
model.favorites.zip(expectedFavorites).forEach {
|
||||
assertTrue(sameControl(it.first, it.second))
|
||||
}
|
||||
|
||||
verify(controlsModelCallback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -203,10 +216,12 @@ class AllModelTest : SysuiTestCase() {
|
||||
model.changeFavoriteStatus(id, false)
|
||||
assertFalse(
|
||||
(model.elements.first {
|
||||
it is ControlWrapper && it.controlStatus.control.controlId == id
|
||||
} as ControlWrapper)
|
||||
it is ControlStatusWrapper && it.controlStatus.control.controlId == id
|
||||
} as ControlStatusWrapper)
|
||||
.controlStatus.favorite
|
||||
)
|
||||
|
||||
verify(controlsModelCallback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -219,5 +234,7 @@ class AllModelTest : SysuiTestCase() {
|
||||
model.favorites.zip(expectedFavorites).forEach {
|
||||
assertTrue(sameControl(it.first, it.second))
|
||||
}
|
||||
|
||||
verify(controlsModelCallback, never()).onFirstChange()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.controls.management
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.ComponentName
|
||||
import android.service.controls.Control
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.controls.ControlStatus
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
open class FavoriteModelTest : SysuiTestCase() {
|
||||
|
||||
@Mock
|
||||
lateinit var pendingIntent: PendingIntent
|
||||
@Mock
|
||||
lateinit var allAdapter: ControlAdapter
|
||||
@Mock
|
||||
lateinit var favoritesAdapter: ControlAdapter
|
||||
|
||||
val idPrefix = "controlId"
|
||||
val favoritesIndices = listOf(7, 3, 1, 9)
|
||||
val favoritesList = favoritesIndices.map { "controlId$it" }
|
||||
lateinit var controls: List<ControlStatus>
|
||||
|
||||
lateinit var model: FavoriteModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
// controlId0 --> zone = 0
|
||||
// controlId1 --> zone = 1, favorite
|
||||
// controlId2 --> zone = 2
|
||||
// controlId3 --> zone = 0, favorite
|
||||
// controlId4 --> zone = 1
|
||||
// controlId5 --> zone = 2
|
||||
// controlId6 --> zone = 0
|
||||
// controlId7 --> zone = 1, favorite
|
||||
// controlId8 --> zone = 2
|
||||
// controlId9 --> zone = 0, favorite
|
||||
controls = (0..9).map {
|
||||
ControlStatus(
|
||||
Control.StatelessBuilder("$idPrefix$it", pendingIntent)
|
||||
.setZone((it % 3).toString())
|
||||
.build(),
|
||||
ComponentName("", ""),
|
||||
it in favoritesIndices
|
||||
)
|
||||
}
|
||||
|
||||
model = FavoriteModel(controls, favoritesList, favoritesAdapter, allAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class FavoriteModelNonParametrizedTests : FavoriteModelTest() {
|
||||
@Test
|
||||
fun testAll() {
|
||||
// Zones are sorted alphabetically
|
||||
val expected = listOf(
|
||||
ZoneNameWrapper("0"),
|
||||
ControlWrapper(controls[0]),
|
||||
ControlWrapper(controls[3]),
|
||||
ControlWrapper(controls[6]),
|
||||
ControlWrapper(controls[9]),
|
||||
ZoneNameWrapper("1"),
|
||||
ControlWrapper(controls[1]),
|
||||
ControlWrapper(controls[4]),
|
||||
ControlWrapper(controls[7]),
|
||||
ZoneNameWrapper("2"),
|
||||
ControlWrapper(controls[2]),
|
||||
ControlWrapper(controls[5]),
|
||||
ControlWrapper(controls[8])
|
||||
)
|
||||
assertEquals(expected, model.all)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFavoritesInOrder() {
|
||||
val expected = favoritesIndices.map { ControlWrapper(controls[it]) }
|
||||
assertEquals(expected, model.favorites)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeFavoriteStatus_addFavorite() {
|
||||
val controlToAdd = 6
|
||||
model.changeFavoriteStatus("$idPrefix$controlToAdd", true)
|
||||
|
||||
val pair = model.all.findControl(controlToAdd)
|
||||
pair?.let {
|
||||
assertTrue(it.second.favorite)
|
||||
assertEquals(it.second, model.favorites.last().controlStatus)
|
||||
verify(favoritesAdapter).notifyItemInserted(model.favorites.size - 1)
|
||||
verify(allAdapter).notifyItemChanged(it.first)
|
||||
verifyNoMoreInteractions(favoritesAdapter, allAdapter)
|
||||
} ?: run {
|
||||
fail("control not found")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeFavoriteStatus_removeFavorite() {
|
||||
val controlToRemove = 3
|
||||
model.changeFavoriteStatus("$idPrefix$controlToRemove", false)
|
||||
|
||||
val pair = model.all.findControl(controlToRemove)
|
||||
pair?.let {
|
||||
assertFalse(it.second.favorite)
|
||||
assertTrue(model.favorites.none {
|
||||
it.controlStatus.control.controlId == "$idPrefix$controlToRemove"
|
||||
})
|
||||
verify(favoritesAdapter).notifyItemRemoved(favoritesIndices.indexOf(controlToRemove))
|
||||
verify(allAdapter).notifyItemChanged(it.first)
|
||||
verifyNoMoreInteractions(favoritesAdapter, allAdapter)
|
||||
} ?: run {
|
||||
fail("control not found")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeFavoriteStatus_sameStatus() {
|
||||
model.changeFavoriteStatus("${idPrefix}7", true)
|
||||
model.changeFavoriteStatus("${idPrefix}6", false)
|
||||
|
||||
val expected = favoritesIndices.map { ControlWrapper(controls[it]) }
|
||||
assertEquals(expected, model.favorites)
|
||||
|
||||
verifyNoMoreInteractions(favoritesAdapter, allAdapter)
|
||||
}
|
||||
|
||||
private fun List<ElementWrapper>.findControl(controlIndex: Int): Pair<Int, ControlStatus>? {
|
||||
val index = indexOfFirst {
|
||||
it is ControlWrapper &&
|
||||
it.controlStatus.control.controlId == "$idPrefix$controlIndex"
|
||||
}
|
||||
return if (index == -1) null else index to (get(index) as ControlWrapper).controlStatus
|
||||
}
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
@RunWith(Parameterized::class)
|
||||
class FavoriteModelParameterizedTest(val from: Int, val to: Int) : FavoriteModelTest() {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0} -> {1}")
|
||||
fun data(): Collection<Array<Int>> {
|
||||
return (0..3).flatMap { from ->
|
||||
(0..3).map { to ->
|
||||
arrayOf(from, to)
|
||||
}
|
||||
}.filterNot { it[0] == it[1] }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMoveItem() {
|
||||
val originalFavorites = model.favorites.toList()
|
||||
val originalFavoritesIds =
|
||||
model.favorites.map { it.controlStatus.control.controlId }.toSet()
|
||||
model.onMoveItem(from, to)
|
||||
assertEquals(originalFavorites[from], model.favorites[to])
|
||||
// Check that we still have the same favorites
|
||||
assertEquals(originalFavoritesIds,
|
||||
model.favorites.map { it.controlStatus.control.controlId }.toSet())
|
||||
|
||||
verify(favoritesAdapter).notifyItemMoved(from, to)
|
||||
|
||||
verifyNoMoreInteractions(allAdapter, favoritesAdapter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.controls.management
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.controls.ControlInterface
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class FavoritesModelTest : SysuiTestCase() {
|
||||
|
||||
companion object {
|
||||
private val TEST_COMPONENT = ComponentName.unflattenFromString("test_pkg/.test_cls")!!
|
||||
private val ID_PREFIX = "control"
|
||||
private val INITIAL_FAVORITES = (0..5).map {
|
||||
ControlInfo("$ID_PREFIX$it", "title$it", "subtitle$it", it)
|
||||
}
|
||||
}
|
||||
|
||||
@Mock
|
||||
private lateinit var callback: FavoritesModel.FavoritesModelCallback
|
||||
@Mock
|
||||
private lateinit var adapter: RecyclerView.Adapter<*>
|
||||
private lateinit var model: FavoritesModel
|
||||
private lateinit var dividerWrapper: DividerWrapper
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
model = FavoritesModel(TEST_COMPONENT, INITIAL_FAVORITES, callback)
|
||||
model.attachAdapter(adapter)
|
||||
dividerWrapper = model.elements.first { it is DividerWrapper } as DividerWrapper
|
||||
}
|
||||
|
||||
@After
|
||||
fun testListConsistency() {
|
||||
assertEquals(INITIAL_FAVORITES.size + 1, model.elements.toSet().size)
|
||||
val dividerIndex = getDividerPosition()
|
||||
model.elements.forEachIndexed { index, element ->
|
||||
if (index == dividerIndex) {
|
||||
assertEquals(dividerWrapper, element)
|
||||
} else {
|
||||
element as ControlInterface
|
||||
assertEquals(index < dividerIndex, element.favorite)
|
||||
}
|
||||
}
|
||||
assertEquals(model.favorites, model.elements.take(dividerIndex).map {
|
||||
(it as ControlInfoWrapper).controlInfo
|
||||
})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInitialElements() {
|
||||
val expected = INITIAL_FAVORITES.map {
|
||||
ControlInfoWrapper(TEST_COMPONENT, it, true)
|
||||
} + DividerWrapper()
|
||||
assertEquals(expected, model.elements)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFavorites() {
|
||||
assertEquals(INITIAL_FAVORITES, model.favorites)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveFavorite_notInFavorites() {
|
||||
val removed = 4
|
||||
val id = "$ID_PREFIX$removed"
|
||||
|
||||
model.changeFavoriteStatus(id, false)
|
||||
|
||||
assertTrue(model.favorites.none { it.controlId == id })
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveFavorite_endOfElements() {
|
||||
val removed = 4
|
||||
val id = "$ID_PREFIX$removed"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
|
||||
assertEquals(ControlInfoWrapper(
|
||||
TEST_COMPONENT, INITIAL_FAVORITES[4], false), model.elements.last())
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveFavorite_adapterNotified() {
|
||||
val removed = 4
|
||||
val id = "$ID_PREFIX$removed"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
|
||||
val lastPos = model.elements.size - 1
|
||||
verify(adapter).notifyItemChanged(eq(lastPos), any(Any::class.java))
|
||||
verify(adapter).notifyItemMoved(removed, lastPos)
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveFavorite_dividerMovedBack() {
|
||||
val oldDividerPosition = getDividerPosition()
|
||||
val removed = 4
|
||||
val id = "$ID_PREFIX$removed"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
|
||||
assertEquals(oldDividerPosition - 1, getDividerPosition())
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveFavorite_ShowDivider() {
|
||||
val oldDividerPosition = getDividerPosition()
|
||||
val removed = 4
|
||||
val id = "$ID_PREFIX$removed"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
|
||||
assertTrue(dividerWrapper.showDivider)
|
||||
verify(adapter).notifyItemChanged(oldDividerPosition)
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoubleRemove_onlyOnce() {
|
||||
val removed = 4
|
||||
val id = "$ID_PREFIX$removed"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
model.changeFavoriteStatus(id, false)
|
||||
|
||||
verify(adapter /* only once */).notifyItemChanged(anyInt(), any(Any::class.java))
|
||||
verify(adapter /* only once */).notifyItemMoved(anyInt(), anyInt())
|
||||
verify(adapter /* only once (divider) */).notifyItemChanged(anyInt())
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveTwo_InSameOrder() {
|
||||
val removedFirst = 3
|
||||
val removedSecond = 0
|
||||
model.changeFavoriteStatus("$ID_PREFIX$removedFirst", false)
|
||||
model.changeFavoriteStatus("$ID_PREFIX$removedSecond", false)
|
||||
|
||||
assertEquals(listOf(
|
||||
ControlInfoWrapper(TEST_COMPONENT, INITIAL_FAVORITES[removedFirst], false),
|
||||
ControlInfoWrapper(TEST_COMPONENT, INITIAL_FAVORITES[removedSecond], false)
|
||||
), model.elements.takeLast(2))
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveAll_showNone() {
|
||||
INITIAL_FAVORITES.forEach {
|
||||
model.changeFavoriteStatus(it.controlId, false)
|
||||
}
|
||||
assertEquals(dividerWrapper, model.elements.first())
|
||||
assertTrue(dividerWrapper.showNone)
|
||||
verify(adapter, times(2)).notifyItemChanged(anyInt()) // divider
|
||||
verify(callback).onNoneChanged(true)
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddFavorite_movedToEnd() {
|
||||
val added = 2
|
||||
val id = "$ID_PREFIX$added"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
model.changeFavoriteStatus(id, true)
|
||||
|
||||
assertEquals(id, model.favorites.last().controlId)
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddFavorite_onlyOnce() {
|
||||
val added = 2
|
||||
val id = "$ID_PREFIX$added"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
model.changeFavoriteStatus(id, true)
|
||||
model.changeFavoriteStatus(id, true)
|
||||
|
||||
// Once for remove and once for add
|
||||
verify(adapter, times(2)).notifyItemChanged(anyInt(), any(Any::class.java))
|
||||
verify(adapter, times(2)).notifyItemMoved(anyInt(), anyInt())
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddFavorite_notRemoved() {
|
||||
val added = 2
|
||||
val id = "$ID_PREFIX$added"
|
||||
model.changeFavoriteStatus(id, true)
|
||||
|
||||
verifyNoMoreInteractions(adapter)
|
||||
|
||||
verify(callback, never()).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddOnlyRemovedFavorite_dividerStopsShowing() {
|
||||
val added = 2
|
||||
val id = "$ID_PREFIX$added"
|
||||
model.changeFavoriteStatus(id, false)
|
||||
model.changeFavoriteStatus(id, true)
|
||||
|
||||
assertFalse(dividerWrapper.showDivider)
|
||||
val inOrder = inOrder(adapter)
|
||||
inOrder.verify(adapter).notifyItemChanged(model.elements.size - 1)
|
||||
inOrder.verify(adapter).notifyItemChanged(model.elements.size - 2)
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddFirstFavorite_dividerNotShowsNone() {
|
||||
INITIAL_FAVORITES.forEach {
|
||||
model.changeFavoriteStatus(it.controlId, false)
|
||||
}
|
||||
|
||||
verify(callback).onNoneChanged(true)
|
||||
|
||||
model.changeFavoriteStatus("${ID_PREFIX}3", true)
|
||||
assertEquals(1, getDividerPosition())
|
||||
|
||||
verify(callback).onNoneChanged(false)
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMoveBetweenFavorites() {
|
||||
val from = 2
|
||||
val to = 4
|
||||
|
||||
model.onMoveItem(from, to)
|
||||
assertEquals(
|
||||
listOf(0, 1, 3, 4, 2, 5).map { "$ID_PREFIX$it" },
|
||||
model.favorites.map(ControlInfo::controlId)
|
||||
)
|
||||
verify(adapter).notifyItemMoved(from, to)
|
||||
verify(adapter, never()).notifyItemChanged(anyInt(), any(Any::class.java))
|
||||
|
||||
verify(callback).onFirstChange()
|
||||
}
|
||||
|
||||
private fun getDividerPosition(): Int = model.elements.indexOf(dividerWrapper)
|
||||
}
|
||||
Reference in New Issue
Block a user