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