1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.controls.management
18 
19 import android.content.ComponentName
20 import android.util.Log
21 import androidx.recyclerview.widget.ItemTouchHelper
22 import androidx.recyclerview.widget.RecyclerView
23 import com.android.systemui.controls.ControlInterface
24 import com.android.systemui.controls.controller.ControlInfo
25 import java.util.Collections
26 
27 /**
28  * Model used to show and rearrange favorites.
29  *
30  * The model will show all the favorite controls and a divider that can be toggled visible/gone.
31  * It will place the items selected as favorites before the divider and the ones unselected after.
32  *
33  * @property componentName used by the [ControlAdapter] to retrieve resources.
34  * @property favorites list of current favorites
35  * @property favoritesModelCallback callback to notify on first change and empty favorites
36  */
37 class FavoritesModel(
38     private val componentName: ComponentName,
39     favorites: List<ControlInfo>,
40     private val favoritesModelCallback: FavoritesModelCallback
41 ) : ControlsModel {
42 
43     companion object {
44         private const val TAG = "FavoritesModel"
45     }
46 
47     private var adapter: RecyclerView.Adapter<*>? = null
48     private var modified = false
49 
50     override val moveHelper = object : ControlsModel.MoveHelper {
canMoveBeforenull51         override fun canMoveBefore(position: Int): Boolean {
52             return position > 0 && position < dividerPosition
53         }
54 
canMoveAfternull55         override fun canMoveAfter(position: Int): Boolean {
56             return position >= 0 && position < dividerPosition - 1
57         }
58 
moveBeforenull59         override fun moveBefore(position: Int) {
60             if (!canMoveBefore(position)) {
61                 Log.w(TAG, "Cannot move position $position before")
62             } else {
63                 onMoveItem(position, position - 1)
64             }
65         }
66 
moveAfternull67         override fun moveAfter(position: Int) {
68             if (!canMoveAfter(position)) {
69                 Log.w(TAG, "Cannot move position $position after")
70             } else {
71                 onMoveItem(position, position + 1)
72             }
73         }
74     }
75 
attachAdapternull76     override fun attachAdapter(adapter: RecyclerView.Adapter<*>) {
77         this.adapter = adapter
78     }
79 
80     override val favorites: List<ControlInfo>
<lambda>null81         get() = elements.take(dividerPosition).map {
82             (it as ControlInfoWrapper).controlInfo
83         }
84 
<lambda>null85     override val elements: List<ElementWrapper> = favorites.map {
86         ControlInfoWrapper(componentName, it, true)
87     } + DividerWrapper()
88 
89     /**
90      * Indicates the position of the divider to determine
91      */
92     private var dividerPosition = elements.size - 1
93 
changeFavoriteStatusnull94     override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
95         val position = elements.indexOfFirst { it is ControlInterface && it.controlId == controlId }
96         if (position == -1) {
97             return // controlId not found
98         }
99         if (position < dividerPosition && favorite || position > dividerPosition && !favorite) {
100             return // Does not change favorite status
101         }
102         if (favorite) {
103             onMoveItemInternal(position, dividerPosition)
104         } else {
105             onMoveItemInternal(position, elements.size - 1)
106         }
107     }
108 
onMoveItemnull109     override fun onMoveItem(from: Int, to: Int) {
110         onMoveItemInternal(from, to)
111     }
112 
updateDividerNonenull113     private fun updateDividerNone(oldDividerPosition: Int, show: Boolean) {
114         (elements[oldDividerPosition] as DividerWrapper).showNone = show
115         favoritesModelCallback.onNoneChanged(show)
116     }
117 
updateDividerShownull118     private fun updateDividerShow(oldDividerPosition: Int, show: Boolean) {
119         (elements[oldDividerPosition] as DividerWrapper).showDivider = show
120     }
121 
122     /**
123      * Performs the update in the model.
124      *
125      *   * update the favorite field of the [ControlInterface]
126      *   * update the fields of the [DividerWrapper]
127      *   * move the corresponding element in [elements]
128      *
129      * It may emit the following signals:
130      *   * [RecyclerView.Adapter.notifyItemChanged] if a [ControlInterface.favorite] has changed
131      *     (in the new position) or if the information in [DividerWrapper] has changed (in the
132      *     old position).
133      *   * [RecyclerView.Adapter.notifyItemMoved]
134      *   * [FavoritesModelCallback.onNoneChanged] whenever we go from 1 to 0 favorites and back
135      *   * [ControlsModel.ControlsModelCallback.onFirstChange] upon the first change in the model
136      */
onMoveItemInternalnull137     private fun onMoveItemInternal(from: Int, to: Int) {
138         if (from == dividerPosition) return // divider does not move
139         var changed = false
140         if (from < dividerPosition && to >= dividerPosition ||
141                 from > dividerPosition && to <= dividerPosition) {
142             if (from < dividerPosition && to >= dividerPosition) {
143                 // favorite to not favorite
144                 (elements[from] as ControlInfoWrapper).favorite = false
145             } else if (from > dividerPosition && to <= dividerPosition) {
146                 // not favorite to favorite
147                 (elements[from] as ControlInfoWrapper).favorite = true
148             }
149             changed = true
150             updateDivider(from, to)
151         }
152         moveElement(from, to)
153         adapter?.notifyItemMoved(from, to)
154         if (changed) {
155             adapter?.notifyItemChanged(to, Any())
156         }
157         if (!modified) {
158             modified = true
159             favoritesModelCallback.onFirstChange()
160         }
161     }
162 
updateDividernull163     private fun updateDivider(from: Int, to: Int) {
164         var dividerChanged = false
165         val oldDividerPosition = dividerPosition
166         if (from < dividerPosition && to >= dividerPosition) { // favorite to not favorite
167             dividerPosition--
168             if (dividerPosition == 0) {
169                 updateDividerNone(oldDividerPosition, true)
170                 dividerChanged = true
171             }
172             if (dividerPosition == elements.size - 2) {
173                 updateDividerShow(oldDividerPosition, true)
174                 dividerChanged = true
175             }
176         } else if (from > dividerPosition && to <= dividerPosition) { // not favorite to favorite
177             dividerPosition++
178             if (dividerPosition == 1) {
179                 updateDividerNone(oldDividerPosition, false)
180                 dividerChanged = true
181             }
182             if (dividerPosition == elements.size - 1) {
183                 updateDividerShow(oldDividerPosition, false)
184                 dividerChanged = true
185             }
186         }
187         if (dividerChanged) {
188             adapter?.notifyItemChanged(oldDividerPosition)
189         }
190     }
191 
moveElementnull192     private fun moveElement(from: Int, to: Int) {
193         if (from < to) {
194             for (i in from until to) {
195                 Collections.swap(elements, i, i + 1)
196             }
197         } else {
198             for (i in from downTo to + 1) {
199                 Collections.swap(elements, i, i - 1)
200             }
201         }
202     }
203 
204     /**
205      * Touch helper to facilitate dragging in the [RecyclerView].
206      *
207      * Only views above the divider line (favorites) can be dragged or accept drops.
208      */
209     val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(0, 0) {
210 
211         private val MOVEMENT = ItemTouchHelper.UP or
212                 ItemTouchHelper.DOWN or
213                 ItemTouchHelper.LEFT or
214                 ItemTouchHelper.RIGHT
215 
onMovenull216         override fun onMove(
217             recyclerView: RecyclerView,
218             viewHolder: RecyclerView.ViewHolder,
219             target: RecyclerView.ViewHolder
220         ): Boolean {
221             onMoveItem(viewHolder.adapterPosition, target.adapterPosition)
222             return true
223         }
224 
getMovementFlagsnull225         override fun getMovementFlags(
226             recyclerView: RecyclerView,
227             viewHolder: RecyclerView.ViewHolder
228         ): Int {
229             if (viewHolder.adapterPosition < dividerPosition) {
230                 return ItemTouchHelper.Callback.makeMovementFlags(MOVEMENT, 0)
231             } else {
232                 return ItemTouchHelper.Callback.makeMovementFlags(0, 0)
233             }
234         }
235 
canDropOvernull236         override fun canDropOver(
237             recyclerView: RecyclerView,
238             current: RecyclerView.ViewHolder,
239             target: RecyclerView.ViewHolder
240         ): Boolean {
241             return target.adapterPosition < dividerPosition
242         }
243 
onSwipednull244         override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
245 
isItemViewSwipeEnablednull246         override fun isItemViewSwipeEnabled() = false
247     }
248 
249     interface FavoritesModelCallback : ControlsModel.ControlsModelCallback {
250         fun onNoneChanged(showNoFavorites: Boolean)
251     }
252 }