1 /** <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package com.android.healthconnect.controller.datasources.appsources 15 16 import android.annotation.SuppressLint 17 import android.content.Context 18 import android.view.LayoutInflater 19 import android.view.MotionEvent 20 import android.view.View 21 import android.view.ViewGroup 22 import android.widget.ImageView 23 import android.widget.TextView 24 import androidx.recyclerview.widget.ItemTouchHelper 25 import androidx.recyclerview.widget.RecyclerView 26 import com.android.healthconnect.controller.R 27 import com.android.healthconnect.controller.datasources.DataSourcesViewModel 28 import com.android.healthconnect.controller.shared.HealthDataCategoryInt 29 import com.android.healthconnect.controller.shared.app.AppMetadata 30 import com.android.healthconnect.controller.shared.app.AppUtils 31 import com.android.healthconnect.controller.utils.AttributeResolver 32 import com.android.healthconnect.controller.utils.logging.DataSourcesElement 33 import com.android.healthconnect.controller.utils.logging.ElementName 34 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger 35 import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint 36 import dagger.hilt.android.EntryPointAccessors 37 import java.text.NumberFormat 38 39 /** RecyclerView adapter that holds the list of app sources for this [HealthDataCategory]. */ 40 class AppSourcesAdapter( 41 private val context: Context, 42 private val appUtils: AppUtils, 43 priorityList: List<AppMetadata>, 44 potentialAppSourcesList: List<AppMetadata>, 45 private val dataSourcesViewModel: DataSourcesViewModel, 46 private val category: @HealthDataCategoryInt Int, 47 private val onAppRemovedListener: OnAppRemovedFromPriorityListListener, 48 private val itemMoveAttachCallbackListener: ItemMoveAttachCallbackListener, 49 ) : RecyclerView.Adapter<AppSourcesAdapter.AppSourcesItemViewHolder?>() { 50 51 private var listener: ItemTouchHelper? = null 52 private var priorityList = priorityList.toMutableList() 53 private var potentialAppSourcesList = potentialAppSourcesList.toMutableList() 54 private var isEditMode = false 55 56 private val POSITION_CHANGED_PAYLOAD = Any() 57 58 private var logger: HealthConnectLogger 59 var logName: ElementName = DataSourcesElement.DATA_TOTALS_CARD 60 61 init { 62 val hiltEntryPoint = 63 EntryPointAccessors.fromApplication( 64 context.applicationContext, HealthConnectLoggerEntryPoint::class.java) 65 logger = hiltEntryPoint.logger() 66 } 67 68 interface OnAppRemovedFromPriorityListListener { 69 fun onAppRemovedFromPriorityList() 70 } 71 72 /** 73 * Used for re-attaching the onItemMovedCallback to the RecyclerView when we exit the edit mode 74 */ 75 interface ItemMoveAttachCallbackListener { 76 fun attachCallback() 77 } 78 79 override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): AppSourcesItemViewHolder { 80 return AppSourcesItemViewHolder( 81 LayoutInflater.from(viewGroup.context) 82 .inflate(R.layout.widget_app_source_layout, viewGroup, false), 83 listener) 84 } 85 86 override fun onBindViewHolder(viewHolder: AppSourcesItemViewHolder, position: Int) { 87 viewHolder.bind(position, priorityList[position], isOnlyApp = priorityList.size == 1) 88 } 89 90 override fun getItemCount(): Int { 91 return priorityList.size 92 } 93 94 fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { 95 val movedAppInfo: AppMetadata = priorityList.removeAt(fromPosition) 96 priorityList.add( 97 if (toPosition > fromPosition + 1) toPosition - 1 else toPosition, movedAppInfo) 98 notifyItemMoved(fromPosition, toPosition) 99 if (toPosition < fromPosition) { 100 notifyItemRangeChanged( 101 toPosition, fromPosition - toPosition + 1, POSITION_CHANGED_PAYLOAD) 102 } else { 103 notifyItemRangeChanged( 104 fromPosition, toPosition - fromPosition + 1, POSITION_CHANGED_PAYLOAD) 105 } 106 dataSourcesViewModel.updatePriorityList(priorityList.map { it.packageName }, category) 107 return true 108 } 109 110 fun setOnItemDragStartedListener(listener: ItemTouchHelper) { 111 this.listener = listener 112 } 113 114 private fun removeOnItemDragStartedListener() { 115 listener = null 116 } 117 118 fun toggleEditMode(isEditMode: Boolean) { 119 this.isEditMode = isEditMode 120 if (!isEditMode) { 121 itemMoveAttachCallbackListener.attachCallback() 122 } else { 123 removeOnItemDragStartedListener() 124 } 125 notifyDataSetChanged() 126 } 127 128 fun removeItem(position: Int) { 129 priorityList.removeAt(position) 130 notifyItemRemoved(position) 131 } 132 133 /** Shows a single item of the priority list. */ 134 inner class AppSourcesItemViewHolder( 135 itemView: View, 136 private val onItemDragStartedListener: ItemTouchHelper? 137 ) : RecyclerView.ViewHolder(itemView) { 138 private val EDIT_MODE_TAG = "edit_mode" 139 private val DRAG_MODE_TAG = "drag_mode" 140 private val appPositionView: TextView 141 private val appNameView: TextView 142 private val appSourceSummary: TextView 143 private val actionView: View 144 private val actionIconBackground: ImageView 145 146 init { 147 appPositionView = itemView.findViewById(R.id.app_position) 148 appNameView = itemView.findViewById(R.id.app_name) 149 actionView = itemView.findViewById(R.id.action_icon) 150 actionIconBackground = itemView.findViewById(R.id.action_icon_background) 151 appSourceSummary = itemView.findViewById(R.id.app_source_summary) 152 } 153 154 fun bind(appPosition: Int, appMetadata: AppMetadata, isOnlyApp: Boolean) { 155 // Adding 1 to position as position starts from 0 but should show to the user starting 156 // from 1. 157 val positionString: String = NumberFormat.getIntegerInstance().format(appPosition + 1) 158 appPositionView.text = positionString 159 appNameView.text = appMetadata.appName 160 actionIconBackground.contentDescription = 161 context.getString(R.string.reorder_button_content_description, appNameView.text) 162 163 if (appUtils.isDefaultApp(context, appMetadata.packageName)) { 164 appSourceSummary.visibility = View.VISIBLE 165 appSourceSummary.text = context.getString(R.string.default_app_summary) 166 } else { 167 appSourceSummary.visibility = View.GONE 168 } 169 170 if (isEditMode) { 171 setupItemForEditMode(appPosition) 172 } else { 173 setupItemForDragMode(isOnlyApp) 174 } 175 176 logger.logImpression(DataSourcesElement.APP_SOURCE_BUTTON) 177 } 178 179 private fun setupItemForEditMode(appPosition: Int) { 180 actionView.isClickable = true 181 actionView.visibility = View.VISIBLE 182 actionIconBackground.background = 183 AttributeResolver.getDrawable(itemView.context, R.attr.closeIcon) 184 actionIconBackground.contentDescription = 185 context.getString(R.string.remove_button_content_description, appNameView.text) 186 actionIconBackground.tag = EDIT_MODE_TAG 187 actionView.setOnTouchListener(null) 188 actionView.setOnClickListener { 189 logger.logInteraction(DataSourcesElement.REMOVE_APP_SOURCE_BUTTON) 190 191 val currentPriority = priorityList.toMutableList() 192 val removedItem = currentPriority.removeAt(appPosition) 193 dataSourcesViewModel.setEditedPriorityList(currentPriority) 194 dataSourcesViewModel.updatePriorityList( 195 currentPriority.map { it.packageName }, category) 196 197 potentialAppSourcesList.add(removedItem) 198 dataSourcesViewModel.loadPotentialAppSources(category, false) 199 dataSourcesViewModel.setEditedPotentialAppSources(potentialAppSourcesList) 200 201 removeItem(appPosition) 202 onAppRemovedListener.onAppRemovedFromPriorityList() 203 } 204 logger.logImpression(DataSourcesElement.REMOVE_APP_SOURCE_BUTTON) 205 } 206 207 // These items are not clickable and so onTouch does not need to reimplement click 208 // conditions. 209 // Drag&drop in accessibility mode (talk back) is implemented as custom actions. 210 @SuppressLint("ClickableViewAccessibility") 211 private fun setupItemForDragMode(isOnlyApp: Boolean) { 212 // Hide drag icon if this is the only app in the list 213 if (isOnlyApp) { 214 actionView.visibility = View.INVISIBLE 215 actionView.isClickable = false 216 } else { 217 actionIconBackground.background = 218 AttributeResolver.getDrawable(itemView.context, R.attr.priorityItemDragIcon) 219 actionIconBackground.tag = DRAG_MODE_TAG 220 actionView.setOnClickListener(null) 221 actionView.setOnTouchListener { _, event -> 222 logger.logInteraction(DataSourcesElement.REORDER_APP_SOURCE_BUTTON) 223 if (event.action == MotionEvent.ACTION_DOWN || 224 event.action == MotionEvent.ACTION_UP) { 225 onItemDragStartedListener?.startDrag(this) 226 } 227 false 228 } 229 logger.logImpression(DataSourcesElement.REORDER_APP_SOURCE_BUTTON) 230 } 231 } 232 } 233 } 234