• 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.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