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.NonNull; 25 import android.content.ContentValues; 26 import android.database.Cursor; 27 import android.database.sqlite.SQLiteDatabase; 28 import android.health.connect.HealthDataCategory; 29 import android.util.Pair; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.server.healthconnect.storage.TransactionManager; 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.DeleteTableRequest; 36 import com.android.server.healthconnect.storage.request.ReadTableRequest; 37 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 38 import com.android.server.healthconnect.storage.utils.StorageUtils; 39 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * Helper class to get migrate priority of the apps for each {@link HealthDataCategory} from 48 * migration aware apk to module. 49 * 50 * @hide 51 */ 52 public final class PriorityMigrationHelper { 53 54 @VisibleForTesting 55 public static final String PRE_MIGRATION_TABLE_NAME = "pre_migration_category_priority_table"; 56 57 @VisibleForTesting static final String CATEGORY_COLUMN_NAME = "category"; 58 @VisibleForTesting static final String PRIORITY_ORDER_COLUMN_NAME = "priority_order"; 59 private static final List<Pair<String, Integer>> UNIQUE_COLUMN_INFO = 60 Collections.singletonList(new Pair<>(CATEGORY_COLUMN_NAME, TYPE_STRING)); 61 62 private static final Object sPriorityMigrationHelperLock = new Object(); 63 private static volatile PriorityMigrationHelper sPriorityMigrationHelper; 64 65 private final Object mPriorityMigrationHelperInstanceLock = new Object(); 66 private Map<Integer, List<Long>> mPreMigrationPriorityCache; 67 PriorityMigrationHelper()68 private PriorityMigrationHelper() {} 69 70 /** 71 * Populate the pre-migration priority table by copying entries from priority table at the start 72 * of migration. 73 */ populatePreMigrationPriority()74 public void populatePreMigrationPriority() { 75 synchronized (mPriorityMigrationHelperInstanceLock) { 76 // Populating table only if it was not already populated. 77 if (TransactionManager.getInitialisedInstance() 78 .getNumberOfEntriesInTheTable(PRE_MIGRATION_TABLE_NAME) 79 == 0) { 80 populatePreMigrationTable(); 81 } 82 } 83 } 84 85 /** 86 * Returns priority order stored for data category in module at the time migration was started. 87 */ getPreMigrationPriority(int dataCategory)88 public List<Long> getPreMigrationPriority(int dataCategory) { 89 synchronized (mPriorityMigrationHelperInstanceLock) { 90 if (mPreMigrationPriorityCache == null) { 91 cachePreMigrationTable(); 92 } 93 94 return Collections.unmodifiableList( 95 mPreMigrationPriorityCache.getOrDefault(dataCategory, new ArrayList<>())); 96 } 97 } 98 99 /** 100 * Read pre-migration table and populate cache which would be used for writing priority 101 * migration. 102 */ cachePreMigrationTable()103 private void cachePreMigrationTable() { 104 Map<Integer, List<Long>> preMigrationCategoryPriorityMap = new HashMap<>(); 105 TransactionManager transactionManager = TransactionManager.getInitialisedInstance(); 106 try (Cursor cursor = 107 transactionManager.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 mPreMigrationPriorityCache = preMigrationCategoryPriorityMap; 117 } 118 119 /** Delete pre-migration priority data when migration is finished. */ clearData(@onNull TransactionManager transactionManager)120 public void clearData(@NonNull TransactionManager transactionManager) { 121 synchronized (mPriorityMigrationHelperInstanceLock) { 122 transactionManager.delete(new DeleteTableRequest(PRE_MIGRATION_TABLE_NAME)); 123 mPreMigrationPriorityCache = null; 124 } 125 } 126 127 /** Returns a requests for creating pre-migration priority table. */ 128 @NonNull getCreateTableRequest()129 public CreateTableRequest getCreateTableRequest() { 130 return new CreateTableRequest(PRE_MIGRATION_TABLE_NAME, getColumnInfo()); 131 } 132 133 /** Upgrades the database to the latest version. */ onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db)134 public void onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db) {} 135 136 /** 137 * Populate the pre-migration priority table if table is newly created by copying entries from 138 * priority table. 139 */ populatePreMigrationTable()140 private void populatePreMigrationTable() { 141 Map<Integer, List<Long>> existingPriority = 142 HealthDataCategoryPriorityHelper.getInstance() 143 .getHealthDataCategoryToAppIdPriorityMapImmutable(); 144 145 TransactionManager transactionManager = TransactionManager.getInitialisedInstance(); 146 existingPriority.forEach( 147 (category, priority) -> { 148 if (!priority.isEmpty()) { 149 UpsertTableRequest request = 150 new UpsertTableRequest( 151 PRE_MIGRATION_TABLE_NAME, 152 getContentValuesFor(category, priority), 153 UNIQUE_COLUMN_INFO); 154 transactionManager.insert(request); 155 } 156 }); 157 if (existingPriority.values().stream() 158 .filter(priority -> !priority.isEmpty()) 159 .findAny() 160 .isEmpty()) { 161 /* 162 Adding placeholder row to signify that pre-migration have no priority for 163 any category and the table should not be repopulated even after multiple calls to 164 startMigration 165 */ 166 UpsertTableRequest request = 167 new UpsertTableRequest( 168 PRE_MIGRATION_TABLE_NAME, 169 getContentValuesFor(HealthDataCategory.UNKNOWN, new ArrayList<>()), 170 UNIQUE_COLUMN_INFO); 171 transactionManager.insert(request); 172 } 173 } 174 175 /** 176 * This implementation should return the column names with which the table should be created. 177 */ 178 @NonNull getColumnInfo()179 private List<Pair<String, String>> getColumnInfo() { 180 ArrayList<Pair<String, String>> columnInfo = new ArrayList<>(); 181 columnInfo.add(new Pair<>(CATEGORY_COLUMN_NAME, INTEGER_UNIQUE)); 182 columnInfo.add(new Pair<>(PRIORITY_ORDER_COLUMN_NAME, TEXT_NOT_NULL)); 183 184 return columnInfo; 185 } 186 187 /** Create content values for storing priority in the database. */ getContentValuesFor( @ealthDataCategory.Type int dataCategory, List<Long> priorityList)188 private ContentValues getContentValuesFor( 189 @HealthDataCategory.Type int dataCategory, List<Long> priorityList) { 190 ContentValues contentValues = new ContentValues(); 191 contentValues.put(CATEGORY_COLUMN_NAME, dataCategory); 192 contentValues.put(PRIORITY_ORDER_COLUMN_NAME, StorageUtils.flattenLongList(priorityList)); 193 194 return contentValues; 195 } 196 197 /** Creates(if it was not already created) and returns instance of PriorityMigrationHelper. */ 198 @NonNull getInstance()199 public static PriorityMigrationHelper getInstance() { 200 if (sPriorityMigrationHelper == null) { 201 synchronized (sPriorityMigrationHelperLock) { 202 if (sPriorityMigrationHelper == null) { 203 sPriorityMigrationHelper = new PriorityMigrationHelper(); 204 } 205 } 206 } 207 208 return sPriorityMigrationHelper; 209 } 210 } 211