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