• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 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.content.Context
17 import android.view.MenuItem
18 import android.view.View
19 import androidx.appcompat.view.ContextThemeWrapper
20 import androidx.appcompat.widget.PopupMenu
21 import androidx.preference.PreferenceCategory
22 import com.android.healthconnect.controller.R
23 import com.android.healthconnect.controller.datasources.DataSourcesViewModel
24 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
25 import com.android.healthconnect.controller.shared.app.AppMetadata
26 import com.android.healthconnect.controller.shared.app.AppUtils
27 import com.android.healthconnect.controller.shared.preference.RankedActionPreference
28 import com.android.healthconnect.controller.utils.logging.DataSourcesElement
29 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
30 
31 class AppSourcesPreferenceCategory(
32     context: Context,
33     private val logger: HealthConnectLogger,
34     private val appUtils: AppUtils,
35     private val viewModel: DataSourcesViewModel,
36     private val category: @HealthDataCategoryInt Int,
37 ) : PreferenceCategory(context, null) {
38 
39     override fun onAttached() {
40         super.onAttached()
41         updatePreferences()
42     }
43 
44     private var priorityList: List<AppMetadata> = emptyList()
45 
46     private fun updatePreferences() {
47         removeAll()
48 
49         priorityList = viewModel.getPriorityList()
50         val showActionButtons = priorityList.size > 1
51         priorityList.forEachIndexed { index, appMetadata ->
52             addPreference(rankedActionPreference(index, appMetadata, showActionButtons))
53         }
54     }
55 
56     private fun showPopupMenu(view: View, position: Int) =
57         PopupMenu(ContextThemeWrapper(context, R.style.Widget_HealthConnect_PopUpMenu), view)
58             .apply {
59                 menuInflater.inflate(R.menu.app_source_menu, menu)
60                 logger.logInteraction(DataSourcesElement.OPEN_APP_SOURCE_MENU_BUTTON)
61                 setupMenuItems(position)
62                 setOnMenuItemClickListener { handleMenuClick(it, position) }
63                 show()
64             }
65 
66     private fun PopupMenu.setupMenuItems(position: Int) {
67         menu
68             .findItem(R.id.move_up)
69             .setVisibilityAndLog(
70                 isVisible = position > 0,
71                 logElement = DataSourcesElement.MOVE_APP_SOURCE_UP_MENU_BUTTON,
72             )
73         menu
74             .findItem(R.id.move_down)
75             .setVisibilityAndLog(
76                 isVisible = position < priorityList.size - 1,
77                 logElement = DataSourcesElement.MOVE_APP_SOURCE_DOWN_MENU_BUTTON,
78             )
79         menu
80             .findItem(R.id.remove)
81             .setVisibilityAndLog(
82                 isVisible = priorityList.size > 1,
83                 logElement = DataSourcesElement.REMOVE_APP_SOURCE_MENU_BUTTON,
84             )
85     }
86 
87     private fun handleMenuClick(item: MenuItem, position: Int): Boolean =
88         when (item.itemId) {
89             R.id.move_up -> {
90                 logger.logInteraction(DataSourcesElement.MOVE_APP_SOURCE_UP_MENU_BUTTON)
91                 swapListItems(position - 1, position)
92                 viewModel.updatePriorityList(priorityListPackages(), category)
93                 true
94             }
95             R.id.move_down -> {
96                 logger.logInteraction(DataSourcesElement.MOVE_APP_SOURCE_DOWN_MENU_BUTTON)
97                 swapListItems(position, position + 1)
98                 viewModel.updatePriorityList(priorityListPackages(), category)
99                 true
100             }
101             R.id.remove -> {
102                 logger.logInteraction(DataSourcesElement.REMOVE_APP_SOURCE_MENU_BUTTON)
103                 removeListItem(position)
104                 viewModel.updatePriorityList(priorityListPackages(), category)
105                 viewModel.showAddAnAppButton()
106                 true
107             }
108             else -> false
109         }
110 
111     private fun swapListItems(firstPosition: Int, secondPosition: Int) {
112         if (outOfRange(firstPosition) || outOfRange(secondPosition)) {
113             return
114         }
115         swapPreferences(firstPosition, secondPosition)
116 
117         priorityList = priorityList.toMutableList().apply { swap(firstPosition, secondPosition) }
118     }
119 
120     private fun swapPreferences(firstPosition: Int, secondPosition: Int) {
121         // Simply setting new order for the preferences does not update the expressive background,
122         // hence we need to remove and re-add them.
123         val firstPreference = this.getPreference(firstPosition)
124         val secondPreference = this.getPreference(secondPosition)
125         val firstAppMetaData = priorityList[firstPosition]
126         val secondAppMetadata = priorityList[secondPosition]
127         val newFirstPreference = rankedActionPreference(firstPosition, secondAppMetadata)
128         val newSecondPreference = rankedActionPreference(secondPosition, firstAppMetaData)
129 
130         this.removePreference(firstPreference)
131         this.removePreference(secondPreference)
132         this.addPreference(newFirstPreference)
133         this.addPreference(newSecondPreference)
134     }
135 
136     private fun removeListItem(position: Int) {
137         if (outOfRange(position)) {
138             return
139         }
140         updateRemainingPreferences(position)
141         this.removePreference(this.getPreference(position))
142         priorityList = priorityList.toMutableList().apply { removeAt(position) }
143     }
144 
145     private fun updateRemainingPreferences(indexToRemove: Int) {
146         if (preferenceCount == 2) {
147             val indexOfRemaining = if (indexToRemove == 0) 1 else 0
148             (getPreference(indexOfRemaining) as RankedActionPreference).hideActionButton()
149         }
150         preferencesInRange(indexToRemove + 1, preferenceCount).forEach { it.reduceIndex() }
151     }
152 
153     private fun preferencesInRange(start: Int, end: Int): List<RankedActionPreference> =
154         (start until end).map { getPreference(it) as RankedActionPreference }
155 
156     private fun priorityListPackages() = priorityList.map { it.packageName }
157 
158     private fun outOfRange(position: Int) = position !in priorityList.indices
159 
160     private fun rankedActionPreference(
161         position: Int,
162         appMetadata: AppMetadata,
163         showActionButtons: Boolean = true,
164     ): RankedActionPreference =
165         RankedActionPreference(
166                 context,
167                 appMetadata,
168                 appUtils,
169                 position,
170                 ::showPopupMenu,
171                 showActionButtons,
172             )
173             .also {
174                 logger.logImpression(DataSourcesElement.OPEN_APP_SOURCE_MENU_BUTTON)
175                 it.logName = DataSourcesElement.APP_SOURCE_BUTTON
176                 order = position
177                 isSelectable = false
178             }
179 
180     private fun MenuItem?.setVisibilityAndLog(isVisible: Boolean, logElement: DataSourcesElement) {
181         this?.apply {
182             this.isVisible = isVisible
183             if (isVisible) logger.logImpression(logElement)
184         }
185     }
186 
187     private fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
188         val tmp = this[index1]
189         this[index1] = this[index2]
190         this[index2] = tmp
191     }
192 }
193