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 }