1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.healthconnect.migration; 18 19 import static com.android.server.healthconnect.storage.request.UpsertTableRequest.TYPE_STRING; 20 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER; 21 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_UNIQUE; 22 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL; 23 24 import android.annotation.Nullable; 25 import android.content.ContentValues; 26 import android.database.Cursor; 27 import android.health.connect.HealthDataCategory; 28 import android.util.Pair; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.server.healthconnect.storage.TransactionManager; 32 import com.android.server.healthconnect.storage.datatypehelpers.DatabaseHelper; 33 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper; 34 import com.android.server.healthconnect.storage.request.CreateTableRequest; 35 import com.android.server.healthconnect.storage.request.ReadTableRequest; 36 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 37 import com.android.server.healthconnect.storage.utils.StorageUtils; 38 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 45 /** 46 * Helper class to get migrate priority of the apps for each {@link HealthDataCategory} from 47 * migration aware apk to module. 48 * 49 * @hide 50 */ 51 public final class PriorityMigrationHelper extends DatabaseHelper { 52 53 @VisibleForTesting 54 public static final String PRE_MIGRATION_TABLE_NAME = "pre_migration_category_priority_table"; 55 56 @VisibleForTesting static final String CATEGORY_COLUMN_NAME = "category"; 57 @VisibleForTesting static final String PRIORITY_ORDER_COLUMN_NAME = "priority_order"; 58 private static final List<Pair<String, Integer>> UNIQUE_COLUMN_INFO = 59 Collections.singletonList(new Pair<>(CATEGORY_COLUMN_NAME, TYPE_STRING)); 60 61 private static final Object sPriorityMigrationHelperLock = new Object(); 62 63 @Nullable private Map<Integer, List<Long>> mPreMigrationPriorityCache; 64 65 private final HealthDataCategoryPriorityHelper mHealthDataCategoryPriorityHelper; 66 private final TransactionManager mTransactionManager; 67 PriorityMigrationHelper( HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper, TransactionManager transactionManager, DatabaseHelpers databaseHelpers)68 public PriorityMigrationHelper( 69 HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper, 70 TransactionManager transactionManager, 71 DatabaseHelpers databaseHelpers) { 72 super(databaseHelpers); 73 mHealthDataCategoryPriorityHelper = healthDataCategoryPriorityHelper; 74 mTransactionManager = transactionManager; 75 } 76 77 /** 78 * Populate the pre-migration priority table by copying entries from priority table at the start 79 * of migration. 80 */ populatePreMigrationPriority()81 public synchronized void populatePreMigrationPriority() { 82 // Populating table only if it was not already populated. 83 if (mTransactionManager.queryNumEntries(PRE_MIGRATION_TABLE_NAME) == 0) { 84 populatePreMigrationTable(); 85 } 86 } 87 88 /** 89 * Returns priority order stored for data category in module at the time migration was started. 90 */ getPreMigrationPriority(int dataCategory)91 public synchronized List<Long> getPreMigrationPriority(int dataCategory) { 92 if (mPreMigrationPriorityCache == null) { 93 mPreMigrationPriorityCache = createPreMigrationTable(); 94 } 95 96 return Collections.unmodifiableList( 97 mPreMigrationPriorityCache.getOrDefault(dataCategory, new ArrayList<>())); 98 } 99 100 /** 101 * Read pre-migration table and populate cache which would be used for writing priority 102 * migration. 103 */ createPreMigrationTable()104 private Map<Integer, List<Long>> createPreMigrationTable() { 105 Map<Integer, List<Long>> preMigrationCategoryPriorityMap = new HashMap<>(); 106 try (Cursor cursor = 107 mTransactionManager.read(new ReadTableRequest(PRE_MIGRATION_TABLE_NAME))) { 108 while (cursor.moveToNext()) { 109 int dataCategory = cursor.getInt(cursor.getColumnIndex(CATEGORY_COLUMN_NAME)); 110 List<Long> appIdsInOrder = 111 StorageUtils.getCursorLongList( 112 cursor, PRIORITY_ORDER_COLUMN_NAME, DELIMITER); 113 preMigrationCategoryPriorityMap.put(dataCategory, appIdsInOrder); 114 } 115 } 116 return preMigrationCategoryPriorityMap; 117 } 118 119 @Override clearCache()120 protected synchronized void clearCache() { 121 mPreMigrationPriorityCache = null; 122 } 123 124 /** Returns a requests for creating pre-migration priority table. */ getCreateTableRequest()125 public static CreateTableRequest getCreateTableRequest() { 126 return new CreateTableRequest(PRE_MIGRATION_TABLE_NAME, getColumnInfo()); 127 } 128 129 @Override getMainTableName()130 protected String getMainTableName() { 131 return PRE_MIGRATION_TABLE_NAME; 132 } 133 134 /** 135 * Populate the pre-migration priority table if table is newly created by copying entries from 136 * priority table. 137 */ populatePreMigrationTable()138 private void populatePreMigrationTable() { 139 Map<Integer, List<Long>> existingPriority = 140 mHealthDataCategoryPriorityHelper 141 .getHealthDataCategoryToAppIdPriorityMapImmutable(); 142 143 existingPriority.forEach( 144 (category, priority) -> { 145 if (!priority.isEmpty()) { 146 UpsertTableRequest request = 147 new UpsertTableRequest( 148 PRE_MIGRATION_TABLE_NAME, 149 getContentValuesFor(category, priority), 150 UNIQUE_COLUMN_INFO); 151 mTransactionManager.insert(request); 152 } 153 }); 154 if (existingPriority.values().stream() 155 .filter(priority -> !priority.isEmpty()) 156 .findAny() 157 .isEmpty()) { 158 /* 159 Adding placeholder row to signify that pre-migration have no priority for 160 any category and the table should not be repopulated even after multiple calls to 161 startMigration 162 */ 163 UpsertTableRequest request = 164 new UpsertTableRequest( 165 PRE_MIGRATION_TABLE_NAME, 166 getContentValuesFor(HealthDataCategory.UNKNOWN, new ArrayList<>()), 167 UNIQUE_COLUMN_INFO); 168 mTransactionManager.insert(request); 169 } 170 } 171 172 /** 173 * This implementation should return the column names with which the table should be created. 174 */ getColumnInfo()175 private static List<Pair<String, String>> getColumnInfo() { 176 ArrayList<Pair<String, String>> columnInfo = new ArrayList<>(); 177 columnInfo.add(new Pair<>(CATEGORY_COLUMN_NAME, INTEGER_UNIQUE)); 178 columnInfo.add(new Pair<>(PRIORITY_ORDER_COLUMN_NAME, TEXT_NOT_NULL)); 179 180 return columnInfo; 181 } 182 183 /** Create content values for storing priority in the database. */ getContentValuesFor( @ealthDataCategory.Type int dataCategory, List<Long> priorityList)184 private ContentValues getContentValuesFor( 185 @HealthDataCategory.Type int dataCategory, List<Long> priorityList) { 186 ContentValues contentValues = new ContentValues(); 187 contentValues.put(CATEGORY_COLUMN_NAME, dataCategory); 188 contentValues.put(PRIORITY_ORDER_COLUMN_NAME, StorageUtils.flattenLongList(priorityList)); 189 190 return contentValues; 191 } 192 } 193