Merge "Add Structure pages to management screens" into rvc-dev am: 7103ff4a7d

Change-Id: I4d2b09498e63f7ccfda3b3d7be19d02fd29aa62a
This commit is contained in:
Automerger Merge Worker
2020-03-09 20:14:25 +00:00
13 changed files with 356 additions and 104 deletions

View File

@@ -46,6 +46,7 @@ android_library {
"SystemUIPluginLib",
"SystemUISharedLib",
"SettingsLib",
"androidx.viewpager2_viewpager2",
"androidx.legacy_legacy-support-v4",
"androidx.recyclerview_recyclerview",
"androidx.preference_preference",
@@ -106,6 +107,7 @@ android_library {
"SystemUIPluginLib",
"SystemUISharedLib",
"SettingsLib",
"androidx.viewpager2_viewpager2",
"androidx.legacy_legacy-support-v4",
"androidx.recyclerview_recyclerview",
"androidx.preference_preference",

View File

@@ -25,13 +25,40 @@
android:paddingStart="@dimen/controls_management_side_padding"
android:paddingEnd="@dimen/controls_management_side_padding" >
<TextView
android:id="@+id/title"
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/controls_title_size"
android:textAlignment="center" />
android:gravity="center_vertical">
<FrameLayout
android:id="@+id/icon_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:minWidth="56dp"
android:visibility="gone"
android:paddingTop="@dimen/controls_app_icon_frame_top_padding"
android:paddingBottom="@dimen/controls_app_icon_frame_bottom_padding"
android:paddingEnd="@dimen/controls_app_icon_frame_side_padding"
android:paddingStart="@dimen/controls_app_icon_frame_side_padding" >
<ImageView
android:id="@android:id/icon"
android:layout_width="@dimen/controls_app_icon_size"
android:layout_height="@dimen/controls_app_icon_size" />
</FrameLayout>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/controls_title_size"
android:textAlignment="center" />
</LinearLayout>
<TextView
android:id="@+id/subtitle"
@@ -41,19 +68,11 @@
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="center" />
<androidx.core.widget.NestedScrollView
<ViewStub
android:id="@+id/stub"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginTop="@dimen/controls_management_list_margin">
<ViewStub
android:id="@+id/stub"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.core.widget.NestedScrollView>
android:layout_weight="1"/>
<FrameLayout
android:layout_width="match_parent"

View File

@@ -14,12 +14,18 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.recyclerview.widget.RecyclerView
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginTop="@dimen/controls_management_list_margin">
</androidx.recyclerview.widget.RecyclerView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.core.widget.NestedScrollView>

View File

@@ -17,7 +17,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:orientation="vertical">
<TextView
@@ -29,11 +29,17 @@
android:gravity="center_horizontal"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listAll"
android:layout_width="match_parent"
<com.android.systemui.controls.management.ManagementPageIndicator
android:id="@+id/structure_page_indicator"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginTop="@dimen/controls_management_list_margin"
android:nestedScrollingEnabled="false"/>
android:visibility="gone" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/structure_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

View File

@@ -0,0 +1,31 @@
<?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.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginTop="@dimen/controls_management_list_margin">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listAll"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.core.widget.NestedScrollView>

View File

@@ -1249,6 +1249,7 @@
<dimen name="controls_app_icon_size">32dp</dimen>
<dimen name="controls_app_icon_frame_side_padding">8dp</dimen>
<dimen name="controls_app_icon_frame_top_padding">4dp</dimen>
<dimen name="controls_app_icon_frame_bottom_padding">@dimen/controls_app_icon_frame_top_padding</dimen>
<dimen name="controls_app_bottom_margin">8dp</dimen>
<dimen name="controls_app_text_padding">8dp</dimen>
<dimen name="controls_app_divider_height">2dp</dimen>

View File

@@ -253,17 +253,21 @@ class ControlsControllerImpl @Inject constructor (
}
override fun error(message: String) {
val loadData = Favorites.getControlsForComponent(componentName).let {
controls ->
executor.execute {
val loadData = Favorites.getControlsForComponent(componentName)
.let { controls ->
val keys = controls.map { it.controlId }
createLoadDataObject(
controls.map { createRemovedStatus(componentName, it, false) },
keys,
true
controls.map {
createRemovedStatus(componentName, it, false)
},
keys,
true
)
}
}
dataCallback.accept(loadData)
dataCallback.accept(loadData)
}
}
}
)

View File

@@ -22,14 +22,20 @@ import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.controller.ControlInfo
/**
* This model is used to show all controls separated by zones.
* This model is used to show controls separated by zones.
*
* The model will sort the controls and zones in the following manner:
* * The zones will be sorted in a first seen basis
* * The controls in each zone will be sorted in a first seen basis.
*
* @property controls List of all controls as returned by loading
* @property initialFavoriteIds sorted ids of favorite controls
* The controls passed should belong to the same structure, as an instance of this model will be
* created for each structure.
*
* The list of favorite ids can contain ids for controls not passed to this model. Those will be
* filtered out.
*
* @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.
*/
class AllModel(
@@ -50,7 +56,10 @@ class AllModel(
}
}
private val favoriteIds = initialFavoriteIds.toMutableList()
private val favoriteIds = run {
val ids = controls.mapTo(HashSet()) { it.control.controlId }
initialFavoriteIds.filter { it in ids }.toMutableList()
}
override val elements: List<ElementWrapper> = createWrappers(controls)

View File

@@ -42,7 +42,6 @@ private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
* @param onlyFavorites set to true to only display favorites instead of all controls
*/
class ControlAdapter(
private val layoutInflater: LayoutInflater,
private val elevation: Float
) : RecyclerView.Adapter<Holder>() {
@@ -60,6 +59,7 @@ class ControlAdapter(
private var model: ControlsModel? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val layoutInflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_CONTROL -> {
ControlHolder(

View File

@@ -19,20 +19,24 @@ package com.android.systemui.controls.management
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.text.TextUtils
import android.view.View
import android.view.ViewStub
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsControllerImpl
import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.PageIndicator
import com.android.systemui.settings.CurrentUserTracker
import java.text.Collator
import java.util.concurrent.Executor
import java.util.function.Consumer
import javax.inject.Inject
@@ -40,6 +44,7 @@ import javax.inject.Inject
class ControlsFavoritingActivity @Inject constructor(
@Main private val executor: Executor,
private val controller: ControlsControllerImpl,
private val listingController: ControlsListingController,
broadcastDispatcher: BroadcastDispatcher
) : Activity() {
@@ -48,12 +53,18 @@ class ControlsFavoritingActivity @Inject constructor(
const val EXTRA_APP = "extra_app_label"
}
private lateinit var recyclerViewAll: RecyclerView
private lateinit var adapterAll: ControlAdapter
private lateinit var statusText: TextView
private var model: ControlsModel? = null
private var component: ComponentName? = null
private var structureName: CharSequence = ""
private var appName: CharSequence? = null
private lateinit var structurePager: ViewPager2
private lateinit var statusText: TextView
private lateinit var titleView: TextView
private lateinit var iconView: ImageView
private lateinit var iconFrame: View
private lateinit var pageIndicator: PageIndicator
private var listOfStructures = emptyList<StructureContainer>()
private lateinit var comparator: Comparator<StructureContainer>
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
private val startingUser = controller.currentUserId
@@ -66,29 +77,117 @@ class ControlsFavoritingActivity @Inject constructor(
}
}
private val listingCallback = object : ControlsListingController.ControlsListingCallback {
private var icon: Drawable? = null
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
val newIcon = serviceInfos.firstOrNull { it.componentName == component }?.loadIcon()
if (icon == newIcon) return
icon = newIcon
executor.execute {
if (icon != null) {
iconView.setImageDrawable(icon)
}
iconFrame.visibility = if (icon != null) View.VISIBLE else View.GONE
}
}
}
override fun onBackPressed() {
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val collator = Collator.getInstance(resources.configuration.locales[0])
comparator = compareBy(collator) { it.structureName }
appName = intent.getCharSequenceExtra(EXTRA_APP)
component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
bindViews()
setUpPager()
loadControls()
listingController.addCallback(listingCallback)
currentUserTracker.startTracking()
}
private fun loadControls() {
component?.let {
statusText.text = resources.getText(com.android.internal.R.string.loading)
val emptyZoneString = resources.getText(
R.string.controls_favorite_other_zone_header)
controller.loadForComponent(it, Consumer { data ->
val allControls = data.allControls
val favoriteKeys = data.favoritesIds
val error = data.errorOnLoad
val controlsByStructure = allControls.groupBy { it.control.structure ?: "" }
listOfStructures = controlsByStructure.map {
StructureContainer(it.key, AllModel(it.value, favoriteKeys, emptyZoneString))
}.sortedWith(comparator)
executor.execute {
structurePager.adapter = StructureAdapter(listOfStructures)
if (error) {
statusText.text = resources.getText(R.string.controls_favorite_load_error)
} else {
statusText.visibility = View.GONE
}
pageIndicator.setNumPages(listOfStructures.size)
pageIndicator.setLocation(0f)
pageIndicator.visibility =
if (listOfStructures.size > 1) View.VISIBLE else View.GONE
}
})
}
}
private fun setUpPager() {
structurePager.apply {
adapter = StructureAdapter(emptyList())
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val name = listOfStructures[position].structureName
titleView.text = if (!TextUtils.isEmpty(name)) name else appName
}
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
pageIndicator.setLocation(position + positionOffset)
}
})
}
}
private fun bindViews() {
setContentView(R.layout.controls_management)
requireViewById<ViewStub>(R.id.stub).apply {
layoutResource = R.layout.controls_management_favorites
inflate()
}
val app = intent.getCharSequenceExtra(EXTRA_APP)
component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
statusText = requireViewById(R.id.status_message)
pageIndicator = requireViewById(R.id.structure_page_indicator)
setUpRecyclerView()
requireViewById<TextView>(R.id.title).text = app?.let { it }
?: resources.getText(R.string.controls_favorite_default_title)
titleView = requireViewById<TextView>(R.id.title).apply {
text = appName ?: resources.getText(R.string.controls_favorite_default_title)
}
requireViewById<TextView>(R.id.subtitle).text =
resources.getText(R.string.controls_favorite_subtitle)
iconView = requireViewById(com.android.internal.R.id.icon)
iconFrame = requireViewById(R.id.icon_frame)
structurePager = requireViewById<ViewPager2>(R.id.structure_pager)
bindButtons()
}
private fun bindButtons() {
requireViewById<Button>(R.id.other_apps).apply {
visibility = View.VISIBLE
setOnClickListener {
@@ -98,64 +197,21 @@ class ControlsFavoritingActivity @Inject constructor(
requireViewById<Button>(R.id.done).setOnClickListener {
if (component == null) return@setOnClickListener
val favoritesForStorage = model?.favorites?.map {
it.build()
}
if (favoritesForStorage != null) {
controller.replaceFavoritesForStructure(StructureInfo(component!!, structureName,
listOfStructures.forEach {
val favoritesForStorage = it.model.favorites.map { it.build() }
controller.replaceFavoritesForStructure(StructureInfo(component!!, it.structureName,
favoritesForStorage))
finishAffinity()
}
}
component?.let {
statusText.text = resources.getText(com.android.internal.R.string.loading)
controller.loadForComponent(it, Consumer { data ->
val allControls = data.allControls
val favoriteKeys = data.favoritesIds
val error = data.errorOnLoad
val structures = allControls.fold(hashSetOf<CharSequence>()) {
s, c ->
s.add(c.control.structure ?: "")
s
}
// TODO add multi structure switching support
executor.execute {
val emptyZoneString = resources.getText(
R.string.controls_favorite_other_zone_header)
val model = AllModel(allControls, favoriteKeys, emptyZoneString)
adapterAll.changeModel(model)
this.model = model
if (error) {
statusText.text = resources.getText(R.string.controls_favorite_load_error)
} else {
statusText.visibility = View.GONE
}
}
})
}
currentUserTracker.startTracking()
}
private fun setUpRecyclerView() {
val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
val itemDecorator = MarginItemDecorator(margin, margin)
val layoutInflater = LayoutInflater.from(applicationContext)
val elevation = resources.getFloat(R.dimen.control_card_elevation)
adapterAll = ControlAdapter(layoutInflater, elevation)
recyclerViewAll = requireViewById<RecyclerView>(R.id.listAll).apply {
adapter = adapterAll
layoutManager = GridLayoutManager(applicationContext, 2).apply {
spanSizeLookup = adapterAll.spanSizeLookup
}
addItemDecoration(itemDecorator)
finishAffinity()
}
}
override fun onDestroy() {
currentUserTracker.stopTracking()
listingController.removeCallback(listingCallback)
super.onDestroy()
}
}
data class StructureContainer(val structureName: CharSequence, val model: ControlsModel)

View File

@@ -0,0 +1,43 @@
/*
* 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.Context
import android.util.AttributeSet
import android.view.View
import com.android.systemui.qs.PageIndicator
/**
* Page indicator for management screens.
*
* Adds RTL support to [PageIndicator]. To be used with [ViewPager2].
*/
class ManagementPageIndicator(
context: Context,
attrs: AttributeSet
) : PageIndicator(context, attrs) {
override fun setLocation(location: Float) {
// Location doesn't know about RTL
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
val numPages = childCount
super.setLocation(numPages - 1 - location)
} else {
super.setLocation(location)
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
class StructureAdapter(
private val models: List<StructureContainer>
) : RecyclerView.Adapter<StructureAdapter.StructureHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, p1: Int): StructureHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return StructureHolder(
layoutInflater.inflate(R.layout.controls_structure_page, parent, false)
)
}
override fun getItemCount() = models.size
override fun onBindViewHolder(holder: StructureHolder, index: Int) {
holder.bind(models[index].model)
}
class StructureHolder(view: View) : RecyclerView.ViewHolder(view) {
private val recyclerView: RecyclerView
private val controlAdapter: ControlAdapter
init {
recyclerView = itemView.requireViewById<RecyclerView>(R.id.listAll)
val elevation = itemView.context.resources.getFloat(R.dimen.control_card_elevation)
controlAdapter = ControlAdapter(elevation)
setUpRecyclerView()
}
fun bind(model: ControlsModel) {
controlAdapter.changeModel(model)
}
private fun setUpRecyclerView() {
val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.controls_card_margin)
val itemDecorator = MarginItemDecorator(margin, margin)
recyclerView.apply {
this.adapter = controlAdapter
layoutManager = GridLayoutManager(recyclerView.context, 2).apply {
spanSizeLookup = controlAdapter.spanSizeLookup
}
addItemDecoration(itemDecorator)
}
}
}
}

View File

@@ -340,6 +340,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
controlLoadCallbackCaptor.value.error("")
delayableExecutor.runAllReady()
assertTrue(loaded)
}