Merge "Changes to favorite management screen"
This commit is contained in:
committed by
Android (Google) Code Review
commit
4cbfb4a408
@@ -646,16 +646,17 @@
|
||||
android:label="Controls Providers"
|
||||
android:theme="@style/Theme.ControlsManagement"
|
||||
android:showForAllUsers="true"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
|
||||
android:visibleToInstantApps="true">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".controls.management.ControlsFavoritingActivity"
|
||||
android:parentActivityName=".controls.management.ControlsProviderSelectorActivity"
|
||||
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>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="@dimen/controls_management_top_padding"
|
||||
android:paddingStart="@dimen/controls_management_side_padding"
|
||||
android:paddingEnd="@dimen/controls_management_side_padding" >
|
||||
|
||||
<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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/controls_management_titles_margin"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/controls_management_list_margin" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2019 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.
|
||||
-->
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Control.Title"
|
||||
android:textColor="?android:attr/colorPrimary"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
</TextView>
|
||||
@@ -72,8 +72,8 @@
|
||||
<CheckBox
|
||||
android:id="@+id/favorite"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
95
packages/SystemUI/res/layout/controls_management.xml
Normal file
95
packages/SystemUI/res/layout/controls_management.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2019 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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="@dimen/controls_management_top_padding"
|
||||
android:paddingStart="@dimen/controls_management_side_padding"
|
||||
android:paddingEnd="@dimen/controls_management_side_padding" >
|
||||
|
||||
<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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/controls_management_titles_margin"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
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>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/controls_app_divider_height"
|
||||
android:layout_gravity="center_horizontal|top"
|
||||
android:background="?android:attr/listDivider" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="4dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="See other apps"
|
||||
android:textAppearance="@style/TextAppearance.Control.Title"
|
||||
android:textColor="?android:attr/colorPrimary"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/done"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="Done"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
25
packages/SystemUI/res/layout/controls_management_apps.xml
Normal file
25
packages/SystemUI/res/layout/controls_management_apps.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2019 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"
|
||||
>
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2019 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.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_favorites"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="FAVORITES"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/divider1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/controls_app_divider_height"
|
||||
android:layout_gravity="center_horizontal|top"
|
||||
android:background="?android:attr/listDivider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/listFavorites"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_favorites"
|
||||
/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/listFavorites"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="@dimen/controls_management_list_margin"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_all"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_all"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/controls_management_list_margin"
|
||||
android:text="ALL"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/divider2"
|
||||
app:layout_constraintTop_toBottomOf="@id/listFavorites"
|
||||
/>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/controls_app_divider_height"
|
||||
android:layout_gravity="center_horizontal|top"
|
||||
android:background="?android:attr/listDivider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/listAll"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_all"
|
||||
/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/listAll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="@dimen/controls_management_list_margin"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider2"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,3 +1,18 @@
|
||||
<!--
|
||||
~ Copyright (C) 2019 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.
|
||||
-->
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
@@ -20,6 +20,6 @@ import android.service.controls.Control
|
||||
|
||||
data class ControlStatus(
|
||||
val control: Control,
|
||||
val favorite: Boolean,
|
||||
var favorite: Boolean,
|
||||
val removed: Boolean = false
|
||||
)
|
||||
@@ -26,10 +26,18 @@ interface ControlsController : UserAwareController {
|
||||
val available: Boolean
|
||||
|
||||
fun getFavoriteControls(): List<ControlInfo>
|
||||
fun loadForComponent(componentName: ComponentName, callback: (List<ControlStatus>) -> Unit)
|
||||
fun loadForComponent(
|
||||
componentName: ComponentName,
|
||||
callback: (List<ControlStatus>, List<String>) -> Unit
|
||||
)
|
||||
|
||||
fun subscribeToFavorites()
|
||||
fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean)
|
||||
fun countFavoritesForComponent(componentName: ComponentName): Int = 0
|
||||
fun replaceFavoritesForComponent(componentName: ComponentName, favorites: List<ControlInfo>)
|
||||
|
||||
fun getFavoritesForComponent(componentName: ComponentName): List<ControlInfo>
|
||||
fun countFavoritesForComponent(componentName: ComponentName): Int
|
||||
|
||||
fun unsubscribe()
|
||||
fun action(controlInfo: ControlInfo, action: ControlAction)
|
||||
fun refreshStatus(componentName: ComponentName, control: Control)
|
||||
|
||||
@@ -70,9 +70,10 @@ class ControlsControllerImpl @Inject constructor (
|
||||
}
|
||||
|
||||
// Map of map: ComponentName -> (String -> ControlInfo).
|
||||
// Only for current user
|
||||
//
|
||||
@GuardedBy("currentFavorites")
|
||||
private val currentFavorites = ArrayMap<ComponentName, MutableMap<String, ControlInfo>>()
|
||||
private val currentFavorites = ArrayMap<ComponentName, MutableList<ControlInfo>>()
|
||||
.withDefault { mutableListOf() }
|
||||
|
||||
private var userChanging: Boolean = true
|
||||
|
||||
@@ -180,15 +181,14 @@ class ControlsControllerImpl @Inject constructor (
|
||||
val infos = persistenceWrapper.readFavorites()
|
||||
synchronized(currentFavorites) {
|
||||
infos.forEach {
|
||||
currentFavorites.getOrPut(it.component, { ArrayMap<String, ControlInfo>() })
|
||||
.put(it.controlId, it)
|
||||
currentFavorites.getOrPut(it.component, { mutableListOf() }).add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadForComponent(
|
||||
componentName: ComponentName,
|
||||
callback: (List<ControlStatus>) -> Unit
|
||||
callback: (List<ControlStatus>, List<String>) -> Unit
|
||||
) {
|
||||
if (!confirmAvailability()) {
|
||||
if (userChanging) {
|
||||
@@ -200,29 +200,34 @@ class ControlsControllerImpl @Inject constructor (
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
} else {
|
||||
callback(emptyList())
|
||||
callback(emptyList(), emptyList())
|
||||
}
|
||||
return
|
||||
}
|
||||
bindingController.bindAndLoad(componentName) {
|
||||
synchronized(currentFavorites) {
|
||||
val favoritesForComponentKeys: Set<String> =
|
||||
currentFavorites.get(componentName)?.keys ?: emptySet()
|
||||
val changed = updateFavoritesLocked(componentName, it)
|
||||
val favoritesForComponentKeys: List<String> =
|
||||
currentFavorites.getValue(componentName).map { it.controlId }
|
||||
val changed = updateFavoritesLocked(componentName, it, favoritesForComponentKeys)
|
||||
if (changed) {
|
||||
persistenceWrapper.storeFavorites(favoritesAsListLocked())
|
||||
}
|
||||
val removed = findRemovedLocked(favoritesForComponentKeys, it)
|
||||
callback(removed.map { currentFavorites.getValue(componentName).getValue(it) }
|
||||
.map(::createRemovedStatus) +
|
||||
it.map { ControlStatus(it, it.controlId in favoritesForComponentKeys) })
|
||||
val removed = findRemovedLocked(favoritesForComponentKeys.toSet(), it)
|
||||
val controlsWithFavorite =
|
||||
it.map { ControlStatus(it, it.controlId in favoritesForComponentKeys) }
|
||||
callback(
|
||||
currentFavorites.getValue(componentName)
|
||||
.filter { it.controlId in removed }
|
||||
.map(::createRemovedStatus) + controlsWithFavorite,
|
||||
favoritesForComponentKeys
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRemovedStatus(controlInfo: ControlInfo): ControlStatus {
|
||||
val intent = Intent(context, ControlsFavoritingActivity::class.java).apply {
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, controlInfo.component)
|
||||
putExtra(Intent.EXTRA_COMPONENT_NAME, controlInfo.component)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(context,
|
||||
@@ -243,17 +248,24 @@ class ControlsControllerImpl @Inject constructor (
|
||||
}
|
||||
|
||||
@GuardedBy("currentFavorites")
|
||||
private fun updateFavoritesLocked(componentName: ComponentName, list: List<Control>): Boolean {
|
||||
val favorites = currentFavorites.get(componentName) ?: mutableMapOf()
|
||||
val favoriteKeys = favorites.keys
|
||||
private fun updateFavoritesLocked(
|
||||
componentName: ComponentName,
|
||||
list: List<Control>,
|
||||
favoriteKeys: List<String>
|
||||
): Boolean {
|
||||
val favorites = currentFavorites.get(componentName) ?: mutableListOf()
|
||||
if (favoriteKeys.isEmpty()) return false // early return
|
||||
var changed = false
|
||||
list.forEach {
|
||||
if (it.controlId in favoriteKeys) {
|
||||
val value = favorites.getValue(it.controlId)
|
||||
if (value.controlTitle != it.title || value.deviceType != it.deviceType) {
|
||||
favorites[it.controlId] = value.copy(controlTitle = it.title,
|
||||
deviceType = it.deviceType)
|
||||
list.forEach { control ->
|
||||
if (control.controlId in favoriteKeys) {
|
||||
val index = favorites.indexOfFirst { it.controlId == control.controlId }
|
||||
val value = favorites[index]
|
||||
if (value.controlTitle != control.title ||
|
||||
value.deviceType != control.deviceType) {
|
||||
favorites[index] = value.copy(
|
||||
controlTitle = control.title,
|
||||
deviceType = control.deviceType
|
||||
)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
@@ -263,14 +275,14 @@ class ControlsControllerImpl @Inject constructor (
|
||||
|
||||
@GuardedBy("currentFavorites")
|
||||
private fun favoritesAsListLocked(): List<ControlInfo> {
|
||||
return currentFavorites.flatMap { it.value.values }
|
||||
return currentFavorites.flatMap { it.value }
|
||||
}
|
||||
|
||||
override fun subscribeToFavorites() {
|
||||
if (!confirmAvailability()) return
|
||||
// Make a copy of the favorites list
|
||||
val favorites = synchronized(currentFavorites) {
|
||||
currentFavorites.flatMap { it.value.values.toList() }
|
||||
currentFavorites.flatMap { it.value }
|
||||
}
|
||||
bindingController.subscribe(favorites)
|
||||
}
|
||||
@@ -286,22 +298,19 @@ class ControlsControllerImpl @Inject constructor (
|
||||
val listOfControls = synchronized(currentFavorites) {
|
||||
if (state) {
|
||||
if (controlInfo.component !in currentFavorites) {
|
||||
currentFavorites.put(controlInfo.component, ArrayMap<String, ControlInfo>())
|
||||
currentFavorites.put(controlInfo.component, mutableListOf())
|
||||
changed = true
|
||||
}
|
||||
val controlsForComponent = currentFavorites.getValue(controlInfo.component)
|
||||
if (controlInfo.controlId !in controlsForComponent) {
|
||||
controlsForComponent.put(controlInfo.controlId, controlInfo)
|
||||
if (controlsForComponent.firstOrNull {
|
||||
it.controlId == controlInfo.controlId
|
||||
} == null) {
|
||||
controlsForComponent.add(controlInfo)
|
||||
changed = true
|
||||
} else {
|
||||
if (controlsForComponent.getValue(controlInfo.controlId) != controlInfo) {
|
||||
controlsForComponent.put(controlInfo.controlId, controlInfo)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
changed = currentFavorites.get(controlInfo.component)
|
||||
?.remove(controlInfo.controlId) != null
|
||||
?.remove(controlInfo) != null
|
||||
}
|
||||
favoritesAsListLocked()
|
||||
}
|
||||
@@ -310,6 +319,19 @@ class ControlsControllerImpl @Inject constructor (
|
||||
}
|
||||
}
|
||||
|
||||
override fun replaceFavoritesForComponent(
|
||||
componentName: ComponentName,
|
||||
favorites: List<ControlInfo>
|
||||
) {
|
||||
if (!confirmAvailability()) return
|
||||
val filtered = favorites.filter { it.component == componentName }
|
||||
val listOfControls = synchronized(currentFavorites) {
|
||||
currentFavorites.put(componentName, filtered.toMutableList())
|
||||
favoritesAsListLocked()
|
||||
}
|
||||
persistenceWrapper.storeFavorites(listOfControls)
|
||||
}
|
||||
|
||||
override fun refreshStatus(componentName: ComponentName, control: Control) {
|
||||
if (!confirmAvailability()) {
|
||||
Log.d(TAG, "Controls not available")
|
||||
@@ -317,7 +339,13 @@ class ControlsControllerImpl @Inject constructor (
|
||||
}
|
||||
executor.execute {
|
||||
synchronized(currentFavorites) {
|
||||
val changed = updateFavoritesLocked(componentName, listOf(control))
|
||||
val favoriteKeysForComponent =
|
||||
currentFavorites.get(componentName)?.map { it.controlId } ?: emptyList()
|
||||
val changed = updateFavoritesLocked(
|
||||
componentName,
|
||||
listOf(control),
|
||||
favoriteKeysForComponent
|
||||
)
|
||||
if (changed) {
|
||||
persistenceWrapper.storeFavorites(favoritesAsListLocked())
|
||||
}
|
||||
@@ -361,6 +389,12 @@ class ControlsControllerImpl @Inject constructor (
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFavoritesForComponent(componentName: ComponentName): List<ControlInfo> {
|
||||
return synchronized(currentFavorites) {
|
||||
currentFavorites.get(componentName) ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
|
||||
pw.println("ControlsController state:")
|
||||
pw.println(" Available: $available")
|
||||
@@ -368,10 +402,8 @@ class ControlsControllerImpl @Inject constructor (
|
||||
pw.println(" Current user: ${currentUser.identifier}")
|
||||
pw.println(" Favorites:")
|
||||
synchronized(currentFavorites) {
|
||||
currentFavorites.forEach {
|
||||
it.value.forEach {
|
||||
pw.println(" ${it.value}")
|
||||
}
|
||||
favoritesAsListLocked().forEach {
|
||||
pw.println(" ${ it }")
|
||||
}
|
||||
}
|
||||
pw.println(bindingController.toString())
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.settingslib.applications.DefaultAppInfo
|
||||
import com.android.settingslib.widget.CandidateInfo
|
||||
import com.android.systemui.R
|
||||
import java.text.Collator
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
@@ -44,23 +45,27 @@ import java.util.concurrent.Executor
|
||||
* @param onAppSelected a callback to indicate that an app has been selected in the list.
|
||||
*/
|
||||
class AppAdapter(
|
||||
backgroundExecutor: Executor,
|
||||
uiExecutor: Executor,
|
||||
lifecycle: Lifecycle,
|
||||
controlsListingController: ControlsListingController,
|
||||
private val layoutInflater: LayoutInflater,
|
||||
private val onAppSelected: (ComponentName?) -> Unit = {},
|
||||
private val favoritesRenderer: FavoritesRenderer
|
||||
private val favoritesRenderer: FavoritesRenderer,
|
||||
private val resources: Resources
|
||||
) : RecyclerView.Adapter<AppAdapter.Holder>() {
|
||||
|
||||
private var listOfServices = emptyList<CandidateInfo>()
|
||||
|
||||
private val callback = object : ControlsListingController.ControlsListingCallback {
|
||||
override fun onServicesUpdated(list: List<CandidateInfo>) {
|
||||
uiExecutor.execute {
|
||||
listOfServices = list.sortedBy {
|
||||
it.loadLabel().toString()
|
||||
backgroundExecutor.execute {
|
||||
val collator = Collator.getInstance(resources.getConfiguration().locale)
|
||||
val localeComparator = compareBy<CandidateInfo, CharSequence>(collator) {
|
||||
it.loadLabel()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
listOfServices = list.sortedWith(localeComparator)
|
||||
uiExecutor.execute(::notifyDataSetChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,101 +25,150 @@ import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.controls.ControlStatus
|
||||
import com.android.systemui.controls.controller.ControlInfo
|
||||
import com.android.systemui.controls.ui.RenderInfo
|
||||
|
||||
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
|
||||
* [changeFavoritesModel]. This allows for updating the model if there's a reload.
|
||||
*
|
||||
* @param layoutInflater an inflater for the views in the containing [RecyclerView]
|
||||
* @param favoriteCallback a callback to be called when the favorite status of a [Control] is
|
||||
* changed. The callback will take a [ControlInfo.Builder] that's
|
||||
* pre-populated with the [Control] information and the new favorite
|
||||
* status.
|
||||
* @param onlyFavorites set to true to only display favorites instead of all controls
|
||||
*/
|
||||
class ControlAdapter(
|
||||
private val layoutInflater: LayoutInflater,
|
||||
private val favoriteCallback: (ControlInfo.Builder, Boolean) -> Unit
|
||||
) : RecyclerView.Adapter<ControlAdapter.Holder>() {
|
||||
private val onlyFavorites: Boolean = false
|
||||
) : RecyclerView.Adapter<Holder>() {
|
||||
|
||||
var listOfControls = emptyList<ControlStatus>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, i: Int): Holder {
|
||||
return Holder(layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
|
||||
layoutParams.apply {
|
||||
width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
elevation = 15f
|
||||
})
|
||||
companion object {
|
||||
private const val TYPE_ZONE = 0
|
||||
private const val TYPE_CONTROL = 1
|
||||
}
|
||||
|
||||
override fun getItemCount() = listOfControls.size
|
||||
val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return if (getItemViewType(position) == TYPE_ZONE) 2 else 1
|
||||
}
|
||||
}
|
||||
|
||||
var modelList: List<ElementWrapper> = emptyList()
|
||||
private var favoritesModel: FavoriteModel? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return when (viewType) {
|
||||
TYPE_CONTROL -> {
|
||||
ControlHolder(
|
||||
layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
|
||||
layoutParams.apply {
|
||||
width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
elevation = 15f
|
||||
},
|
||||
{ id, favorite ->
|
||||
favoritesModel?.changeFavoriteStatus(id, favorite)
|
||||
})
|
||||
}
|
||||
TYPE_ZONE -> {
|
||||
ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
|
||||
}
|
||||
else -> throw IllegalStateException("Wrong viewType: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
fun changeFavoritesModel(favoritesModel: FavoriteModel) {
|
||||
this.favoritesModel = favoritesModel
|
||||
if (onlyFavorites) {
|
||||
modelList = favoritesModel.favorites
|
||||
} else {
|
||||
modelList = favoritesModel.all
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemCount() = modelList.size
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, index: Int) {
|
||||
holder.bindData(listOfControls[index], favoriteCallback)
|
||||
holder.bindData(modelList[index])
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (modelList[position]) {
|
||||
is ZoneNameWrapper -> TYPE_ZONE
|
||||
is ControlWrapper -> TYPE_CONTROL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder for binding views in the [RecyclerView]-
|
||||
* @param view the [View] for this [Holder]
|
||||
*/
|
||||
sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
/**
|
||||
* Holder for binding views in the [RecyclerView]-
|
||||
* Bind the data from the model into the view
|
||||
*/
|
||||
class Holder(view: View) : RecyclerView.ViewHolder(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)
|
||||
private val removed: TextView = itemView.requireViewById(R.id.status)
|
||||
private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
abstract fun bindData(wrapper: ElementWrapper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind data to the view
|
||||
* @param data information about the [Control]
|
||||
* @param callback a callback to be called when the favorite status of the [Control] is
|
||||
* changed. The callback will take a [ControlInfo.Builder] that's
|
||||
* pre-populated with the [Control] information and the new favorite status.
|
||||
*/
|
||||
fun bindData(data: ControlStatus, callback: (ControlInfo.Builder, Boolean) -> Unit) {
|
||||
val renderInfo = getRenderInfo(data.control.deviceType, data.favorite)
|
||||
title.text = data.control.title
|
||||
subtitle.text = data.control.subtitle
|
||||
favorite.isChecked = data.favorite
|
||||
removed.text = if (data.removed) "Removed" else ""
|
||||
favorite.setOnClickListener {
|
||||
val infoBuilder = ControlInfo.Builder().apply {
|
||||
controlId = data.control.controlId
|
||||
controlTitle = data.control.title
|
||||
deviceType = data.control.deviceType
|
||||
}
|
||||
callback(infoBuilder, favorite.isChecked)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
favorite.performClick()
|
||||
}
|
||||
applyRenderInfo(renderInfo)
|
||||
}
|
||||
/**
|
||||
* Holder for using with [ZoneNameWrapper] to display names of zones.
|
||||
*/
|
||||
private class ZoneHolder(view: View) : Holder(view) {
|
||||
private val zone: TextView = itemView as TextView
|
||||
|
||||
private fun getRenderInfo(
|
||||
@DeviceTypes.DeviceType deviceType: Int,
|
||||
favorite: Boolean
|
||||
): RenderInfo {
|
||||
return RenderInfo.lookup(deviceType, favorite)
|
||||
}
|
||||
override fun bindData(wrapper: ElementWrapper) {
|
||||
wrapper as ZoneNameWrapper
|
||||
zone.text = wrapper.zoneName
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyRenderInfo(ri: RenderInfo) {
|
||||
val context = itemView.context
|
||||
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
|
||||
|
||||
icon.setImageIcon(Icon.createWithResource(context, ri.iconResourceId))
|
||||
icon.setImageTintList(fg)
|
||||
}
|
||||
/**
|
||||
* Holder for using with [ControlWrapper] 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) {
|
||||
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)
|
||||
private val removed: TextView = itemView.requireViewById(R.id.status)
|
||||
private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun setItems(list: List<ControlStatus>) {
|
||||
listOfControls = list
|
||||
notifyDataSetChanged()
|
||||
override fun bindData(wrapper: ElementWrapper) {
|
||||
wrapper as ControlWrapper
|
||||
val data = wrapper.controlStatus
|
||||
val renderInfo = getRenderInfo(data.control.deviceType)
|
||||
title.text = data.control.title
|
||||
subtitle.text = data.control.subtitle
|
||||
favorite.isChecked = data.favorite
|
||||
removed.text = if (data.removed) "Removed" else ""
|
||||
favorite.setOnClickListener {
|
||||
favoriteCallback(data.control.controlId, favorite.isChecked)
|
||||
}
|
||||
applyRenderInfo(renderInfo)
|
||||
}
|
||||
|
||||
private fun getRenderInfo(
|
||||
@DeviceTypes.DeviceType deviceType: Int
|
||||
): RenderInfo {
|
||||
return RenderInfo.lookup(deviceType, true)
|
||||
}
|
||||
|
||||
private fun applyRenderInfo(ri: RenderInfo) {
|
||||
val context = itemView.context
|
||||
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
|
||||
|
||||
icon.setImageIcon(Icon.createWithResource(context, ri.iconResourceId))
|
||||
icon.setImageTintList(fg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ 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.LayoutInflater
|
||||
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
|
||||
@@ -41,13 +45,36 @@ class ControlsFavoritingActivity @Inject constructor(
|
||||
companion object {
|
||||
private const val TAG = "ControlsFavoritingActivity"
|
||||
const val EXTRA_APP = "extra_app_label"
|
||||
const val EXTRA_COMPONENT = "extra_component"
|
||||
}
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var adapter: ControlAdapter
|
||||
private lateinit var recyclerViewAll: RecyclerView
|
||||
private lateinit var adapterAll: ControlAdapter
|
||||
private lateinit var recyclerViewFavorites: RecyclerView
|
||||
private lateinit var adapterFavorites: ControlAdapter
|
||||
private var component: ComponentName? = null
|
||||
|
||||
private var currentModel: FavoriteModel? = null
|
||||
private var itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
|
||||
/* dragDirs */ ItemTouchHelper.UP
|
||||
or ItemTouchHelper.DOWN
|
||||
or ItemTouchHelper.LEFT
|
||||
or ItemTouchHelper.RIGHT,
|
||||
/* swipeDirs */0
|
||||
) {
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return currentModel?.onMoveItem(
|
||||
viewHolder.adapterPosition, target.adapterPosition) != null
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||
|
||||
override fun isItemViewSwipeEnabled() = false
|
||||
}
|
||||
|
||||
private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
|
||||
private val startingUser = controller.currentUserId
|
||||
|
||||
@@ -62,41 +89,77 @@ class ControlsFavoritingActivity @Inject constructor(
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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>(EXTRA_COMPONENT)
|
||||
component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
|
||||
|
||||
// If we have no component name, there's not much we can do.
|
||||
val callback = component?.let {
|
||||
{ infoBuilder: ControlInfo.Builder, status: Boolean ->
|
||||
infoBuilder.componentName = it
|
||||
controller.changeFavoriteStatus(infoBuilder.build(), status)
|
||||
}
|
||||
} ?: { _, _ -> Unit }
|
||||
|
||||
recyclerView = requireViewById(R.id.list)
|
||||
adapter = ControlAdapter(LayoutInflater.from(applicationContext), callback)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager = GridLayoutManager(applicationContext, 2)
|
||||
val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
|
||||
recyclerView.addItemDecoration(MarginItemDecorator(margin, margin))
|
||||
setUpRecyclerViews()
|
||||
|
||||
requireViewById<TextView>(R.id.title).text = app?.let { it }
|
||||
?: resources.getText(R.string.controls_favorite_default_title)
|
||||
requireViewById<TextView>(R.id.subtitle).text =
|
||||
resources.getText(R.string.controls_favorite_subtitle)
|
||||
|
||||
currentUserTracker.startTracking()
|
||||
}
|
||||
requireViewById<Button>(R.id.done).setOnClickListener {
|
||||
if (component == null) return@setOnClickListener
|
||||
val favoritesForStorage = currentModel?.favorites?.map {
|
||||
with(it.controlStatus.control) {
|
||||
ControlInfo(component!!, controlId, title, deviceType)
|
||||
}
|
||||
}
|
||||
if (favoritesForStorage != null) {
|
||||
controller.replaceFavoritesForComponent(component!!, favoritesForStorage)
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
component?.let {
|
||||
controller.loadForComponent(it) {
|
||||
controller.loadForComponent(it) { allControls, favoriteKeys ->
|
||||
executor.execute {
|
||||
adapter.setItems(it)
|
||||
val favoriteModel = FavoriteModel(
|
||||
allControls,
|
||||
favoriteKeys,
|
||||
allAdapter = adapterAll,
|
||||
favoritesAdapter = adapterFavorites)
|
||||
adapterAll.changeFavoritesModel(favoriteModel)
|
||||
adapterFavorites.changeFavoritesModel(favoriteModel)
|
||||
currentModel = favoriteModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentUserTracker.startTracking()
|
||||
}
|
||||
|
||||
private fun setUpRecyclerViews() {
|
||||
val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
|
||||
val itemDecorator = MarginItemDecorator(margin, margin)
|
||||
val layoutInflater = LayoutInflater.from(applicationContext)
|
||||
|
||||
adapterAll = ControlAdapter(layoutInflater)
|
||||
recyclerViewAll = requireViewById<RecyclerView>(R.id.listAll).apply {
|
||||
adapter = adapterAll
|
||||
layoutManager = GridLayoutManager(applicationContext, 2).apply {
|
||||
spanSizeLookup = adapterAll.spanSizeLookup
|
||||
}
|
||||
addItemDecoration(itemDecorator)
|
||||
}
|
||||
|
||||
adapterFavorites = ControlAdapter(layoutInflater, true)
|
||||
recyclerViewFavorites = requireViewById<RecyclerView>(R.id.listFavorites).apply {
|
||||
layoutManager = GridLayoutManager(applicationContext, 2)
|
||||
adapter = adapterFavorites
|
||||
addItemDecoration(itemDecorator)
|
||||
}
|
||||
ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerViewFavorites)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
currentUserTracker.stopTracking()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewStub
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -63,11 +64,21 @@ class ControlsProviderSelectorActivity @Inject constructor(
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.controls_management)
|
||||
requireViewById<ViewStub>(R.id.stub).apply {
|
||||
layoutResource = R.layout.controls_management_apps
|
||||
inflate()
|
||||
}
|
||||
|
||||
recyclerView = requireViewById(R.id.list)
|
||||
recyclerView.adapter = AppAdapter(executor, lifecycle, listingController,
|
||||
LayoutInflater.from(this), ::launchFavoritingActivity,
|
||||
FavoritesRenderer(resources, controlsController::countFavoritesForComponent))
|
||||
recyclerView.adapter = AppAdapter(
|
||||
backExecutor,
|
||||
executor,
|
||||
lifecycle,
|
||||
listingController,
|
||||
LayoutInflater.from(this),
|
||||
::launchFavoritingActivity,
|
||||
FavoritesRenderer(resources, controlsController::countFavoritesForComponent),
|
||||
resources)
|
||||
recyclerView.layoutManager = LinearLayoutManager(applicationContext)
|
||||
|
||||
requireViewById<TextView>(R.id.title).text =
|
||||
@@ -89,11 +100,16 @@ class ControlsProviderSelectorActivity @Inject constructor(
|
||||
.apply {
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_APP,
|
||||
listingController.getAppLabel(it))
|
||||
putExtra(ControlsFavoritingActivity.EXTRA_COMPONENT, it)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
putExtra(Intent.EXTRA_COMPONENT_NAME, it)
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
currentUserTracker.stopTracking()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper classes for the different types of elements shown in the [RecyclerView]s in
|
||||
* [ControlsFavoritingActivity].
|
||||
*/
|
||||
sealed class ElementWrapper
|
||||
data class ZoneNameWrapper(val zoneName: CharSequence) : ElementWrapper()
|
||||
data class ControlWrapper(val controlStatus: ControlStatus) : ElementWrapper()
|
||||
@@ -39,6 +39,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -225,7 +226,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
|
||||
val control = builderFromInfo(newControlInfo).build()
|
||||
|
||||
controller.loadForComponent(TEST_COMPONENT) {}
|
||||
controller.loadForComponent(TEST_COMPONENT) { _, _ -> Unit }
|
||||
|
||||
reset(persistenceWrapper)
|
||||
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
|
||||
@@ -293,11 +294,13 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
var loaded = false
|
||||
val control = builderFromInfo(TEST_CONTROL_INFO).build()
|
||||
|
||||
controller.loadForComponent(TEST_COMPONENT) {
|
||||
controller.loadForComponent(TEST_COMPONENT) { controls, favorites ->
|
||||
loaded = true
|
||||
assertEquals(1, it.size)
|
||||
val controlStatus = it[0]
|
||||
assertEquals(1, controls.size)
|
||||
val controlStatus = controls[0]
|
||||
assertEquals(ControlStatus(control, false), controlStatus)
|
||||
|
||||
assertTrue(favorites.isEmpty())
|
||||
}
|
||||
|
||||
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
|
||||
@@ -315,14 +318,17 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
val control2 = builderFromInfo(TEST_CONTROL_INFO_2).build()
|
||||
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
|
||||
|
||||
controller.loadForComponent(TEST_COMPONENT) {
|
||||
controller.loadForComponent(TEST_COMPONENT) { controls, favorites ->
|
||||
loaded = true
|
||||
assertEquals(2, it.size)
|
||||
val controlStatus = it.first { it.control.controlId == TEST_CONTROL_ID }
|
||||
assertEquals(2, controls.size)
|
||||
val controlStatus = controls.first { it.control.controlId == TEST_CONTROL_ID }
|
||||
assertEquals(ControlStatus(control, true), controlStatus)
|
||||
|
||||
val controlStatus2 = it.first { it.control.controlId == TEST_CONTROL_ID_2 }
|
||||
val controlStatus2 = controls.first { it.control.controlId == TEST_CONTROL_ID_2 }
|
||||
assertEquals(ControlStatus(control2, false), controlStatus2)
|
||||
|
||||
assertEquals(1, favorites.size)
|
||||
assertEquals(TEST_CONTROL_ID, favorites[0])
|
||||
}
|
||||
|
||||
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
|
||||
@@ -338,13 +344,16 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
var loaded = false
|
||||
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
|
||||
|
||||
controller.loadForComponent(TEST_COMPONENT) {
|
||||
controller.loadForComponent(TEST_COMPONENT) { controls, favorites ->
|
||||
loaded = true
|
||||
assertEquals(1, it.size)
|
||||
val controlStatus = it[0]
|
||||
assertEquals(1, controls.size)
|
||||
val controlStatus = controls[0]
|
||||
assertEquals(TEST_CONTROL_ID, controlStatus.control.controlId)
|
||||
assertTrue(controlStatus.favorite)
|
||||
assertTrue(controlStatus.removed)
|
||||
|
||||
assertEquals(1, favorites.size)
|
||||
assertEquals(TEST_CONTROL_ID, favorites[0])
|
||||
}
|
||||
|
||||
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
|
||||
@@ -361,7 +370,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
|
||||
val control = builderFromInfo(newControlInfo).build()
|
||||
|
||||
controller.loadForComponent(TEST_COMPONENT) {}
|
||||
controller.loadForComponent(TEST_COMPONENT) { _, _ -> Unit }
|
||||
|
||||
verify(bindingController).bindAndLoad(eq(TEST_COMPONENT),
|
||||
capture(controlLoadCallbackCaptor))
|
||||
@@ -483,4 +492,81 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
|
||||
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT_2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetFavoritesForComponent() {
|
||||
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
|
||||
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetFavoritesForComponent_otherComponent() {
|
||||
controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
|
||||
assertTrue(controller.getFavoritesForComponent(TEST_COMPONENT).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetFavoritesForComponent_multipleInOrder() {
|
||||
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
|
||||
|
||||
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
|
||||
controller.changeFavoriteStatus(controlInfo, true)
|
||||
|
||||
assertEquals(listOf(TEST_CONTROL_INFO, controlInfo),
|
||||
controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
|
||||
controller.clearFavorites()
|
||||
|
||||
controller.changeFavoriteStatus(controlInfo, true)
|
||||
controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
|
||||
|
||||
assertEquals(listOf(controlInfo, TEST_CONTROL_INFO),
|
||||
controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplaceFavoritesForComponent_noFavorites() {
|
||||
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOf(TEST_CONTROL_INFO))
|
||||
|
||||
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
|
||||
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplaceFavoritesForComponent_differentComponentsAreFilteredOut() {
|
||||
controller.replaceFavoritesForComponent(TEST_COMPONENT,
|
||||
listOf(TEST_CONTROL_INFO, TEST_CONTROL_INFO_2))
|
||||
|
||||
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
|
||||
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplaceFavoritesForComponent_oldFavoritesRemoved() {
|
||||
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
|
||||
assertNotEquals(TEST_CONTROL_INFO, controlInfo)
|
||||
|
||||
controller.changeFavoriteStatus(controlInfo, true)
|
||||
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOf(TEST_CONTROL_INFO))
|
||||
|
||||
assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
|
||||
assertEquals(listOf(TEST_CONTROL_INFO), controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplaceFavoritesForComponent_favoritesInOrder() {
|
||||
val controlInfo = ControlInfo(TEST_COMPONENT, "id", "title", 0)
|
||||
|
||||
val listOrder1 = listOf(TEST_CONTROL_INFO, controlInfo)
|
||||
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOrder1)
|
||||
|
||||
assertEquals(2, controller.countFavoritesForComponent(TEST_COMPONENT))
|
||||
assertEquals(listOrder1, controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
|
||||
val listOrder2 = listOf(controlInfo, TEST_CONTROL_INFO)
|
||||
controller.replaceFavoritesForComponent(TEST_COMPONENT, listOrder2)
|
||||
|
||||
assertEquals(2, controller.countFavoritesForComponent(TEST_COMPONENT))
|
||||
assertEquals(listOrder2, controller.getFavoritesForComponent(TEST_COMPONENT))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.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(),
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user