| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.healthconnect.storage.datatypehelpers; |
| |
| import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER; |
| import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_UNIQUE; |
| import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY; |
| import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL; |
| |
| import android.annotation.NonNull; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.healthconnect.HealthDataCategory; |
| import android.healthconnect.HealthPermissions; |
| import android.os.UserHandle; |
| import android.util.Pair; |
| import android.util.Slog; |
| |
| import com.android.server.healthconnect.permission.HealthConnectPermissionHelper; |
| import com.android.server.healthconnect.storage.TransactionManager; |
| import com.android.server.healthconnect.storage.request.CreateTableRequest; |
| import com.android.server.healthconnect.storage.request.DeleteTableRequest; |
| import com.android.server.healthconnect.storage.request.ReadTableRequest; |
| import com.android.server.healthconnect.storage.request.UpsertTableRequest; |
| import com.android.server.healthconnect.storage.utils.StorageUtils; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Helper class to get priority of the apps for each {@link HealthDataCategory} |
| * |
| * @hide |
| */ |
| public class HealthDataCategoryPriorityHelper { |
| private static final String TABLE_NAME = "health_data_category_priority_table"; |
| private static final String HEALTH_DATA_CATEGORY_COLUMN_NAME = "health_data_category"; |
| private static final String APP_ID_PRIORITY_ORDER_COLUMN_NAME = "app_id_priority_order"; |
| private static final String TAG = "HealthConnectPrioHelper"; |
| private static final String DEFAULT_APP_RESOURCE_NAME = |
| "android:string/config_defaultHealthConnectApp"; |
| private static HealthDataCategoryPriorityHelper sHealthDataCategoryPriorityHelper; |
| |
| /** |
| * map of {@link HealthDataCategory} to list of app ids from {@link AppInfoHelper}, in the order |
| * of their priority |
| */ |
| private ConcurrentHashMap<Integer, List<Long>> mHealthDataCategoryToAppIdPriorityMap; |
| |
| private HealthDataCategoryPriorityHelper() {} |
| |
| @NonNull |
| public static HealthDataCategoryPriorityHelper getInstance() { |
| if (sHealthDataCategoryPriorityHelper == null) { |
| sHealthDataCategoryPriorityHelper = new HealthDataCategoryPriorityHelper(); |
| } |
| |
| return sHealthDataCategoryPriorityHelper; |
| } |
| |
| // Called on DB update. |
| public void onUpgrade(int newVersion, @NonNull SQLiteDatabase db) { |
| // empty by default |
| } |
| |
| /** |
| * Returns a requests representing the tables that should be created corresponding to this |
| * helper |
| */ |
| @NonNull |
| public final CreateTableRequest getCreateTableRequest() { |
| return new CreateTableRequest(TABLE_NAME, getColumnInfo()); |
| } |
| |
| public void appendToPriorityList( |
| @NonNull String packageName, |
| @HealthDataCategory.Type int dataCategory, |
| Context context) { |
| List<Long> newPriorityOrder; |
| getHealthDataCategoryToAppIdPriorityMap().putIfAbsent(dataCategory, new ArrayList<>()); |
| long appInfoId = AppInfoHelper.getInstance().getOrInsertAppInfoId(packageName, context); |
| if (getHealthDataCategoryToAppIdPriorityMap().get(dataCategory).contains(appInfoId)) { |
| return; |
| } |
| newPriorityOrder = |
| new ArrayList<>(getHealthDataCategoryToAppIdPriorityMap().get(dataCategory)); |
| |
| String defaultApp = |
| context.getResources() |
| .getString( |
| Resources.getSystem() |
| .getIdentifier(DEFAULT_APP_RESOURCE_NAME, null, null)); |
| if (Objects.equals(packageName, defaultApp)) { |
| newPriorityOrder.add(0, appInfoId); |
| } else { |
| newPriorityOrder.add(appInfoId); |
| } |
| safelyUpdateDBAndUpdateCache( |
| new UpsertTableRequest( |
| TABLE_NAME, getContentValuesFor(dataCategory, newPriorityOrder)), |
| dataCategory, |
| newPriorityOrder); |
| } |
| |
| public void removeFromPriorityList( |
| @NonNull String packageName, |
| @HealthDataCategory.Type int dataCategory, |
| HealthConnectPermissionHelper permissionHelper, |
| UserHandle userHandle) { |
| |
| final List<String> grantedPermissions = |
| permissionHelper.getGrantedHealthPermissions(packageName, userHandle); |
| for (String permission : HealthPermissions.getWriteHealthPermissionsFor(dataCategory)) { |
| if (grantedPermissions.contains(permission)) { |
| return; |
| } |
| } |
| |
| List<Long> newPriorityList = |
| new ArrayList<>( |
| getHealthDataCategoryToAppIdPriorityMap() |
| .getOrDefault(dataCategory, Collections.emptyList())); |
| if (newPriorityList.isEmpty()) { |
| return; |
| } |
| |
| newPriorityList.remove(AppInfoHelper.getInstance().getAppInfoId(packageName)); |
| if (newPriorityList.isEmpty()) { |
| safelyUpdateDBAndUpdateCache( |
| new DeleteTableRequest(TABLE_NAME) |
| .setId(HEALTH_DATA_CATEGORY_COLUMN_NAME, String.valueOf(dataCategory)), |
| dataCategory); |
| return; |
| } |
| |
| safelyUpdateDBAndUpdateCache( |
| new UpsertTableRequest( |
| TABLE_NAME, getContentValuesFor(dataCategory, newPriorityList)), |
| dataCategory, |
| newPriorityList); |
| } |
| |
| @NonNull |
| public List<String> getPriorityOrder(@HealthDataCategory.Type int type) { |
| List<Long> packageIds = getHealthDataCategoryToAppIdPriorityMap().get(type); |
| if (packageIds == null) { |
| return Collections.emptyList(); |
| } |
| |
| return AppInfoHelper.getInstance().getPackageNames(packageIds); |
| } |
| |
| public void setPriorityOrder(int dataCategory, @NonNull List<String> packagePriorityOrder) { |
| List<Long> currentPriorityOrder = |
| getHealthDataCategoryToAppIdPriorityMap() |
| .getOrDefault(dataCategory, Collections.emptyList()); |
| List<Long> newPriorityOrder = |
| AppInfoHelper.getInstance().getAppInfoIds(packagePriorityOrder); |
| |
| // Remove appId from the priority order if it is not part of the current priority order, |
| // this is because in the time app tried to update the order an app permission might |
| // have been removed, and we only store priority order of apps with permission. |
| newPriorityOrder.removeIf(priorityOrder -> !currentPriorityOrder.contains(priorityOrder)); |
| newPriorityOrder.addAll(currentPriorityOrder); |
| // Make sure we don't remove any new entries. So append old priority in new priority and |
| // remove duplicates |
| newPriorityOrder = newPriorityOrder.stream().distinct().collect(Collectors.toList()); |
| |
| safelyUpdateDBAndUpdateCache( |
| new UpsertTableRequest( |
| TABLE_NAME, getContentValuesFor(dataCategory, newPriorityOrder)), |
| dataCategory, |
| newPriorityOrder); |
| } |
| |
| private Map<Integer, List<Long>> getHealthDataCategoryToAppIdPriorityMap() { |
| if (mHealthDataCategoryToAppIdPriorityMap == null) { |
| populateDataCategoryToAppIdPriorityMap(); |
| } |
| |
| return mHealthDataCategoryToAppIdPriorityMap; |
| } |
| |
| private synchronized void populateDataCategoryToAppIdPriorityMap() { |
| if (mHealthDataCategoryToAppIdPriorityMap != null) { |
| return; |
| } |
| |
| ConcurrentHashMap<Integer, List<Long>> healthDataCategoryToAppIdPriorityMap = |
| new ConcurrentHashMap<>(); |
| final TransactionManager transactionManager = TransactionManager.getInitialisedInstance(); |
| final SQLiteDatabase db = transactionManager.getReadableDb(); |
| try (Cursor cursor = transactionManager.read(db, new ReadTableRequest(TABLE_NAME))) { |
| while (cursor.moveToNext()) { |
| int dataCategory = |
| cursor.getInt(cursor.getColumnIndex(HEALTH_DATA_CATEGORY_COLUMN_NAME)); |
| List<Long> appIdsInOrder = |
| StorageUtils.getCursorLongList( |
| cursor, APP_ID_PRIORITY_ORDER_COLUMN_NAME, DELIMITER); |
| |
| healthDataCategoryToAppIdPriorityMap.put(dataCategory, appIdsInOrder); |
| } |
| } |
| |
| mHealthDataCategoryToAppIdPriorityMap = healthDataCategoryToAppIdPriorityMap; |
| } |
| |
| private void safelyUpdateDBAndUpdateCache( |
| UpsertTableRequest request, |
| @HealthDataCategory.Type int dataCategory, |
| List<Long> newList) { |
| try { |
| TransactionManager.getInitialisedInstance().insertOrReplace(request); |
| getHealthDataCategoryToAppIdPriorityMap().put(dataCategory, newList); |
| } catch (Exception e) { |
| Slog.e(TAG, "Priority update failed", e); |
| throw e; |
| } |
| } |
| |
| private void safelyUpdateDBAndUpdateCache( |
| DeleteTableRequest request, @HealthDataCategory.Type int dataCategory) { |
| try { |
| TransactionManager.getInitialisedInstance().delete(request); |
| getHealthDataCategoryToAppIdPriorityMap().remove(dataCategory); |
| } catch (Exception e) { |
| Slog.e(TAG, "Delete from priority DB failed: ", e); |
| throw e; |
| } |
| } |
| |
| private ContentValues getContentValuesFor( |
| @HealthDataCategory.Type int dataCategory, List<Long> priorityList) { |
| ContentValues contentValues = new ContentValues(); |
| contentValues.put(HEALTH_DATA_CATEGORY_COLUMN_NAME, dataCategory); |
| contentValues.put( |
| APP_ID_PRIORITY_ORDER_COLUMN_NAME, StorageUtils.flattenLongList(priorityList)); |
| |
| return contentValues; |
| } |
| |
| /** |
| * This implementation should return the column names with which the table should be created. |
| * |
| * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table |
| * already exists on the device |
| * |
| * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS |
| */ |
| @NonNull |
| private List<Pair<String, String>> getColumnInfo() { |
| ArrayList<Pair<String, String>> columnInfo = new ArrayList<>(); |
| columnInfo.add(new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY)); |
| columnInfo.add(new Pair<>(HEALTH_DATA_CATEGORY_COLUMN_NAME, INTEGER_UNIQUE)); |
| columnInfo.add(new Pair<>(APP_ID_PRIORITY_ORDER_COLUMN_NAME, TEXT_NOT_NULL)); |
| |
| return columnInfo; |
| } |
| } |