• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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