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 }