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.CustomIconCache
25 import com.android.systemui.controls.controller.ControlInfo
26 import java.util.Collections
27 
28 /**
29  * Model used to show and rearrange favorites.
30  *
31  * The model will show all the favorite controls and a divider that can be toggled visible/gone.
32  * It will place the items selected as favorites before the divider and the ones unselected after.
33  *
34  * @property componentName used by the [ControlAdapter] to retrieve resources.
35  * @property favorites list of current favorites
36  * @property favoritesModelCallback callback to notify on first change and empty favorites
37  */
38 class FavoritesModel(
39     private val customIconCache: CustomIconCache,
40     private val componentName: ComponentName,
41     favorites: List<ControlInfo>,
42     private val favoritesModelCallback: FavoritesModelCallback
43 ) : ControlsModel {
44 
45     companion object {
46         private const val TAG = "FavoritesModel"
47     }
48 
49     private var adapter: RecyclerView.Adapter<*>? = null
50     private var modified = false
51 
52     override val moveHelper = object : ControlsModel.MoveHelper {
canMoveBeforenull53         override fun canMoveBefore(position: Int): Boolean {
54             return position > 0 && position < dividerPosition
55         }
56 
canMoveAfternull57         override fun canMoveAfter(position: Int): Boolean {
58             return position >= 0 && position < dividerPosition - 1
59         }
60 
moveBeforenull61         override fun moveBefore(position: Int) {
62             if (!canMoveBefore(position)) {
63                 Log.w(TAG, "Cannot move position $position before")
64             } else {
65                 onMoveItem(position, position - 1)
66             }
67         }
68 
moveAfternull69         override fun moveAfter(position: Int) {
70             if (!canMoveAfter(position)) {
71                 Log.w(TAG, "Cannot move position $position after")
72             } else {
73                 onMoveItem(position, position + 1)
74             }
75         }
76     }
77 
attachAdapternull78     override fun attachAdapter(adapter: RecyclerView.Adapter<*>) {
79         this.adapter = adapter
80     }
81 
82     override val favorites: List<ControlInfo>
<lambda>null83         get() = elements.take(dividerPosition).map {
84             (it as ControlInfoWrapper).controlInfo
85         }
86 
<lambda>null87     override val elements: List<ElementWrapper> = favorites.map {
88         ControlInfoWrapper(componentName, it, true, customIconCache::retrieve)
89     } + DividerWrapper()
90 
91     /**
92      * Indicates the position of the divider to determine
93      */
94     private var dividerPosition = elements.size - 1
95 
changeFavoriteStatusnull96     override fun changeFavoriteStatus(controlId: String, favorite: Boolean) {
97         val position = elements.indexOfFirst { it is ControlInterface && it.controlId == controlId }
98         if (position == -1) {
99             return // controlId not found
100         }
101         if (position < dividerPosition && favorite || position > dividerPosition && !favorite) {
102             return // Does not change favorite status
103         }
104         if (favorite) {
105             onMoveItemInternal(position, dividerPosition)
106         } else {
107             onMoveItemInternal(position, elements.size - 1)
108         }
109     }
110 
onMoveItemnull111     override fun onMoveItem(from: Int, to: Int) {
112         onMoveItemInternal(from, to)
113     }
114 
updateDividerNonenull115     private fun updateDividerNone(oldDividerPosition: Int, show: Boolean) {
116         (elements[oldDividerPosition] as DividerWrapper).showNone = show
117         favoritesModelCallback.onNoneChanged(show)
118     }
119 
updateDividerShownull120     private fun updateDividerShow(oldDividerPosition: Int, show: Boolean) {
121         (elements[oldDividerPosition] as DividerWrapper).showDivider = show
122     }
123 
124     /**
125      * Performs the update in the model.
126      *
127      *   * update the favorite field of the [ControlInterface]
128      *   * update the fields of the [DividerWrapper]
129      *   * move the corresponding element in [elements]
130      *
131      * It may emit the following signals:
132      *   * [RecyclerView.Adapter.notifyItemChanged] if a [ControlInterface.favorite] has changed
133      *     (in the new position) or if the information in [DividerWrapper] has changed (in the
134      *     old position).
135      *   * [RecyclerView.Adapter.notifyItemMoved]
136      *   * [FavoritesModelCallback.onNoneChanged] whenever we go from 1 to 0 favorites and back
137      *   * [ControlsModel.ControlsModelCallback.onFirstChange] upon the first change in the model
138      */
onMoveItemInternalnull139     private fun onMoveItemInternal(from: Int, to: Int) {
140         if (from == dividerPosition) return // divider does not move
141         var changed = false
142         if (from < dividerPosition && to >= dividerPosition ||
143                 from > dividerPosition && to <= dividerPosition) {
144             if (from < dividerPosition && to >= dividerPosition) {
145                 // favorite to not favorite
146                 (elements[from] as ControlInfoWrapper).favorite = false
147             } else if (from > dividerPosition && to <= dividerPosition) {
148                 // not favorite to favorite
149                 (elements[from] as ControlInfoWrapper).favorite = true
150             }
151             changed = true
152             updateDivider(from, to)
153         }
154         moveElement(from, to)
155         adapter?.notifyItemMoved(from, to)
156         if (changed) {
157             adapter?.notifyItemChanged(to, Any())
158         }
159         if (!modified) {
160             modified = true
161             favoritesModelCallback.onFirstChange()
162         }
163     }
164 
updateDividernull165     private fun updateDivider(from: Int, to: Int) {
166         var dividerChanged = false
167         val oldDividerPosition = dividerPosition
168         if (from < dividerPosition && to >= dividerPosition) { // favorite to not favorite
169             dividerPosition--
170             if (dividerPosition == 0) {
171                 updateDividerNone(oldDividerPosition, true)
172                 dividerChanged = true
173             }
174             if (dividerPosition == elements.size - 2) {
175                 updateDividerShow(oldDividerPosition, true)
176                 dividerChanged = true
177             }
178         } else if (from > dividerPosition && to <= dividerPosition) { // not favorite to favorite
179             dividerPosition++
180             if (dividerPosition == 1) {
181                 updateDividerNone(oldDividerPosition, false)
182                 dividerChanged = true
183             }
184             if (dividerPosition == elements.size - 1) {
185                 updateDividerShow(oldDividerPosition, false)
186                 dividerChanged = true
187             }
188         }
189         if (dividerChanged) {
190             adapter?.notifyItemChanged(oldDividerPosition)
191         }
192     }
193 
moveElementnull194     private fun moveElement(from: Int, to: Int) {
195         if (from < to) {
196             for (i in from until to) {
197                 Collections.swap(elements, i, i + 1)
198             }
199         } else {
200             for (i in from downTo to + 1) {
201                 Collections.swap(elements, i, i - 1)
202             }
203         }
204     }
205 
206     /**
207      * Touch helper to facilitate dragging in the [RecyclerView].
208      *
209      * Only views above the divider line (favorites) can be dragged or accept drops.
210      */
211     val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(0, 0) {
212 
213         private val MOVEMENT = ItemTouchHelper.UP or
214                 ItemTouchHelper.DOWN or
215                 ItemTouchHelper.LEFT or
216                 ItemTouchHelper.RIGHT
217 
onMovenull218         override fun onMove(
219             recyclerView: RecyclerView,
220             viewHolder: RecyclerView.ViewHolder,
221             target: RecyclerView.ViewHolder
222         ): Boolean {
223             onMoveItem(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
224             return true
225         }
226 
getMovementFlagsnull227         override fun getMovementFlags(
228             recyclerView: RecyclerView,
229             viewHolder: RecyclerView.ViewHolder
230         ): Int {
231             if (viewHolder.bindingAdapterPosition < dividerPosition) {
232                 return ItemTouchHelper.Callback.makeMovementFlags(MOVEMENT, 0)
233             } else {
234                 return ItemTouchHelper.Callback.makeMovementFlags(0, 0)
235             }
236         }
237 
canDropOvernull238         override fun canDropOver(
239             recyclerView: RecyclerView,
240             current: RecyclerView.ViewHolder,
241             target: RecyclerView.ViewHolder
242         ): Boolean {
243             return target.bindingAdapterPosition < dividerPosition
244         }
245 
onSwipednull246         override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
247 
isItemViewSwipeEnablednull248         override fun isItemViewSwipeEnabled() = false
249     }
250 
251     interface FavoritesModelCallback : ControlsModel.ControlsModelCallback {
252         fun onNoneChanged(showNoFavorites: Boolean)
253     }
254 }