From 8765d357798679198f8f49bf3655438080ec8fe6 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Mon, 6 Apr 2020 21:16:02 -0400 Subject: [PATCH] Add controls rearrange activity This activity is accesses from the overflow menu in the Controls UI. It does the following: * Shows all favorites for current structure * Allows for rearranging current favorites in that structure * Allows for removing current favorites in that structure * Links to ControlsFavoritingActivity with just that structure Test: manual Test: atest ControlsControllerImplTest Test: atest FavoritesModelTest Test: atest AllModelTest Fixes: 149138395 Change-Id: I8a57d4f835467247b7cc360fee4e382cd5553481 --- packages/SystemUI/AndroidManifest.xml | 9 + .../controls_horizontal_divider_withEmpty.xml | 44 +++ .../res/layout/controls_management_apps.xml | 17 +- .../layout/controls_management_editing.xml | 27 ++ packages/SystemUI/res/values/strings.xml | 7 +- .../systemui/controls/ControlStatus.kt | 32 +- .../controls/controller/ControlInfo.kt | 18 +- .../controls/controller/ControlsController.kt | 12 + .../controller/ControlsControllerImpl.kt | 9 + .../controls/dagger/ControlsModule.kt | 8 + .../systemui/controls/management/AllModel.kt | 29 +- .../controls/management/ControlAdapter.kt | 74 ++++- .../management/ControlsEditingActivity.kt | 176 +++++++++++ .../management/ControlsFavoritingActivity.kt | 18 +- .../controls/management/ControlsModel.kt | 51 ++- .../controls/management/FavoriteModel.kt | 145 --------- .../controls/management/FavoritesModel.kt | 221 +++++++++++++ .../controls/ui/ControlsUiControllerImpl.kt | 43 ++- .../controller/ControlsControllerImplTest.kt | 32 +- .../controls/management/AllModelTest.kt | 59 ++-- .../controls/management/FavoriteModelTest.kt | 200 ------------ .../controls/management/FavoritesModelTest.kt | 291 ++++++++++++++++++ 22 files changed, 1078 insertions(+), 444 deletions(-) create mode 100644 packages/SystemUI/res/layout/controls_horizontal_divider_withEmpty.xml create mode 100644 packages/SystemUI/res/layout/controls_management_editing.xml create mode 100644 packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/controls/management/FavoriteModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoriteModelTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index c6f03271f9316..5141f2f5a0a7c 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -679,6 +679,15 @@ android:visibleToInstantApps="true"> + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/controls_management_apps.xml b/packages/SystemUI/res/layout/controls_management_apps.xml index 42d73f3cc9cea..94df9d8f47753 100644 --- a/packages/SystemUI/res/layout/controls_management_apps.xml +++ b/packages/SystemUI/res/layout/controls_management_apps.xml @@ -14,18 +14,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + android:layout_height="match_parent" + android:layout_marginTop="@dimen/controls_management_list_margin" +/> - - - diff --git a/packages/SystemUI/res/layout/controls_management_editing.xml b/packages/SystemUI/res/layout/controls_management_editing.xml new file mode 100644 index 0000000000000..8a14ec3666b21 --- /dev/null +++ b/packages/SystemUI/res/layout/controls_management_editing.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 566d143208fc2..7c0b6054dddbd 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2670,8 +2670,11 @@ Controls Choose controls to access from the power menu - - Hold and drag a control to move it + + Hold & drag to rearrange controls + + + All controls removed The list of all controls could not be loaded. diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt index dec60073a55e7..5891a7f705c8b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt @@ -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 +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt index 6e59ac162657d..40606c2689e5d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt @@ -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) - } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 568fb289027d5..7cab847d52f7f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -147,6 +147,18 @@ interface ControlsController : UserAwareController { */ fun getFavoritesForComponent(componentName: ComponentName): List + /** + * 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 + /** * Adds a single favorite to a given component and structure * @param componentName the name of the service that provides the [Control] diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 8805694616a4b..6d34009169d50 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -544,6 +544,15 @@ class ControlsControllerImpl @Inject constructor ( override fun getFavoritesForComponent(componentName: ComponentName): List = Favorites.getStructuresForComponent(componentName) + override fun getFavoritesForStructure( + componentName: ComponentName, + structureName: CharSequence + ): List { + return Favorites.getControlsForStructure( + StructureInfo(componentName, structureName, emptyList()) + ) + } + override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { pw.println("ControlsController state:") pw.println(" Available: $available") diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index 946a2365585ae..3bed559123322 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt index 11181e56838ec..175ed061c7146 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt @@ -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, initialFavoriteIds: List, - private val emptyZoneString: CharSequence + private val emptyZoneString: CharSequence, + private val controlsModelCallback: ControlsModel.ControlsModelCallback ) : ControlsModel { - override val favorites: List + private var modified = false + + override val favorites: List 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() - var emptyZoneValues: Sequence? = null + var emptyZoneValues: Sequence? = 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 { diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt index 1291dd98932e5..607934c3bae73 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt @@ -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) { + 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 diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt new file mode 100644 index 0000000000000..ee1ce7ab3d83c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -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(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(R.id.stub).apply { + layoutResource = R.layout.controls_management_editing + inflate() + } + requireViewById(R.id.title).text = structure + subtitle = requireViewById(R.id.subtitle).apply { + setText(SUBTITLE_ID) + } + } + + private fun bindButtons() { + requireViewById