• 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 android.annotation.Nullable;
20 import android.content.Context;
21 import android.database.sqlite.SQLiteDatabase;
22 import android.health.connect.internal.datatypes.RecordInternal;
23 import android.health.connect.migration.AppInfoMigrationPayload;
24 import android.health.connect.migration.MetadataMigrationPayload;
25 import android.health.connect.migration.MigrationEntity;
26 import android.health.connect.migration.MigrationPayload;
27 import android.health.connect.migration.PermissionMigrationPayload;
28 import android.health.connect.migration.PriorityMigrationPayload;
29 import android.health.connect.migration.RecordMigrationPayload;
30 import android.os.UserHandle;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.server.healthconnect.permission.FirstGrantTimeManager;
34 import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
35 import com.android.server.healthconnect.storage.TransactionManager;
36 import com.android.server.healthconnect.storage.datatypehelpers.ActivityDateHelper;
37 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper;
38 import com.android.server.healthconnect.storage.datatypehelpers.DeviceInfoHelper;
39 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
40 import com.android.server.healthconnect.storage.datatypehelpers.MigrationEntityHelper;
41 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
42 import com.android.server.healthconnect.storage.utils.InternalHealthConnectMappings;
43 import com.android.server.healthconnect.storage.utils.PreferencesManager;
44 import com.android.server.healthconnect.storage.utils.StorageUtils;
45 
46 import java.util.ArrayList;
47 import java.util.Collection;
48 import java.util.List;
49 import java.util.stream.Collectors;
50 import java.util.stream.Stream;
51 
52 /**
53  * Controls the data migration flow. Accepts and applies collections of {@link MigrationEntity}.
54  *
55  * @hide
56  */
57 public final class DataMigrationManager {
58 
59     private static final Object sLock = new Object();
60 
61     private final Context mUserContext;
62     private final TransactionManager mTransactionManager;
63     private final HealthConnectPermissionHelper mPermissionHelper;
64     private final FirstGrantTimeManager mFirstGrantTimeManager;
65     private final DeviceInfoHelper mDeviceInfoHelper;
66     private final AppInfoHelper mAppInfoHelper;
67     private final PriorityMigrationHelper mPriorityMigrationHelper;
68     private final HealthDataCategoryPriorityHelper mHealthDataCategoryPriorityHelper;
69     private final MigrationEntityHelper mMigrationEntityHelper;
70     private final PreferencesManager mPreferencesManager;
71 
DataMigrationManager( Context userContext, TransactionManager transactionManager, HealthConnectPermissionHelper permissionHelper, FirstGrantTimeManager firstGrantTimeManager, DeviceInfoHelper deviceInfoHelper, AppInfoHelper appInfoHelper, HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper, PriorityMigrationHelper priorityMigrationHelper, MigrationEntityHelper migrationEntityHelper, PreferencesManager preferencesManager)72     public DataMigrationManager(
73             Context userContext,
74             TransactionManager transactionManager,
75             HealthConnectPermissionHelper permissionHelper,
76             FirstGrantTimeManager firstGrantTimeManager,
77             DeviceInfoHelper deviceInfoHelper,
78             AppInfoHelper appInfoHelper,
79             HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper,
80             PriorityMigrationHelper priorityMigrationHelper,
81             MigrationEntityHelper migrationEntityHelper,
82             PreferencesManager preferencesManager) {
83         mUserContext = userContext;
84         mTransactionManager = transactionManager;
85         mPermissionHelper = permissionHelper;
86         mFirstGrantTimeManager = firstGrantTimeManager;
87         mDeviceInfoHelper = deviceInfoHelper;
88         mAppInfoHelper = appInfoHelper;
89         mHealthDataCategoryPriorityHelper = healthDataCategoryPriorityHelper;
90         mPriorityMigrationHelper = priorityMigrationHelper;
91         mMigrationEntityHelper = migrationEntityHelper;
92         mPreferencesManager = preferencesManager;
93     }
94 
95     /**
96      * Parses and applies the provided migration entities.
97      *
98      * @param entities a collection of {@link MigrationEntity} to be applied.
99      */
apply(Collection<MigrationEntity> entities)100     public void apply(Collection<MigrationEntity> entities) throws EntityWriteException {
101         synchronized (sLock) {
102             mTransactionManager.runAsTransaction(
103                     db -> {
104                         // Grab the lock again to make sure error-prone is happy, and so that tests
105                         // break if the following code is run asynchronously
106                         synchronized (sLock) {
107                             for (MigrationEntity entity : entities) {
108                                 migrateEntity(db, entity);
109                             }
110                         }
111                     });
112         }
113     }
114 
115     /** Migrates the provided {@link MigrationEntity}. Must be called inside a DB transaction. */
116     @GuardedBy("sLock")
migrateEntity(SQLiteDatabase db, MigrationEntity entity)117     private void migrateEntity(SQLiteDatabase db, MigrationEntity entity)
118             throws EntityWriteException {
119         try {
120             if (checkEntityForDuplicates(db, entity)) {
121                 return;
122             }
123 
124             final MigrationPayload payload = entity.getPayload();
125             if (payload instanceof RecordMigrationPayload) {
126                 migrateRecord(db, (RecordMigrationPayload) payload);
127             } else if (payload instanceof PermissionMigrationPayload) {
128                 migratePermissions((PermissionMigrationPayload) payload);
129             } else if (payload instanceof AppInfoMigrationPayload) {
130                 migrateAppInfo((AppInfoMigrationPayload) payload);
131             } else if (payload instanceof PriorityMigrationPayload) {
132                 migratePriority((PriorityMigrationPayload) payload);
133             } else if (payload instanceof MetadataMigrationPayload) {
134                 migrateMetadata((MetadataMigrationPayload) payload);
135             } else {
136                 throw new IllegalArgumentException("Unsupported payload type: " + payload);
137             }
138         } catch (RuntimeException e) {
139             throw new EntityWriteException(entity.getEntityId(), e);
140         }
141     }
142 
143     @GuardedBy("sLock")
migrateRecord(SQLiteDatabase db, RecordMigrationPayload payload)144     private void migrateRecord(SQLiteDatabase db, RecordMigrationPayload payload) {
145         long recordRowId = mTransactionManager.insertOrIgnoreOnConflict(db, parseRecord(payload));
146         if (recordRowId != -1) {
147             mTransactionManager.insertOrIgnoreOnConflict(
148                     db, ActivityDateHelper.getUpsertTableRequest(payload.getRecordInternal()));
149         }
150     }
151 
parseRecord(RecordMigrationPayload payload)152     private UpsertTableRequest parseRecord(RecordMigrationPayload payload) {
153         final RecordInternal<?> record = payload.getRecordInternal();
154         mAppInfoHelper.populateAppInfoId(record, /* requireAllFields */ false);
155         mDeviceInfoHelper.populateDeviceInfoId(record);
156 
157         if (record.getUuid() == null) {
158             StorageUtils.addNameBasedUUIDTo(record);
159         }
160 
161         return InternalHealthConnectMappings.getInstance()
162                 .getRecordHelper(record.getRecordType())
163                 .getUpsertTableRequest(record);
164     }
165 
166     @GuardedBy("sLock")
migratePermissions(PermissionMigrationPayload payload)167     private void migratePermissions(PermissionMigrationPayload payload) {
168         final String packageName = payload.getHoldingPackageName();
169         final List<String> permissions = payload.getPermissions();
170         final UserHandle userHandle = mUserContext.getUser();
171 
172         if (permissions.isEmpty()
173                 || mPermissionHelper.hasGrantedHealthPermissions(packageName, userHandle)) {
174             return;
175         }
176 
177         final List<Exception> errors = new ArrayList<>();
178 
179         for (String permissionName : permissions) {
180             try {
181                 mPermissionHelper.grantHealthPermission(packageName, permissionName, userHandle);
182             } catch (Exception e) {
183                 errors.add(e);
184             }
185         }
186 
187         // Throw if no permissions were migrated
188         if (errors.size() == permissions.size()) {
189             final RuntimeException error =
190                     new RuntimeException(
191                             "Error migrating permissions for "
192                                     + packageName
193                                     + ": "
194                                     + String.join(", ", payload.getPermissions()));
195             for (Exception e : errors) {
196                 error.addSuppressed(e);
197             }
198             throw error;
199         }
200 
201         mFirstGrantTimeManager.setFirstGrantTime(
202                 packageName, payload.getFirstGrantTime(), userHandle);
203     }
204 
205     @GuardedBy("sLock")
migrateAppInfo(AppInfoMigrationPayload payload)206     private void migrateAppInfo(AppInfoMigrationPayload payload) {
207         mAppInfoHelper.updateAppInfoIfNotInstalled(
208                 payload.getPackageName(), payload.getAppName(), payload.getAppIcon());
209     }
210 
211     /**
212      * Checks the provided entity for duplicates by {@code entityId}. Modifies {@link
213      * MigrationEntityHelper} table as a side effect.
214      *
215      * <p>Entities with the following payload types are exempt from deduplication checks (the result
216      * is always {@code false}): {@link RecordMigrationPayload}.
217      *
218      * @return {@code true} if the entity is duplicated and thus should be ignored, {@code false}
219      *     otherwise.
220      */
221     @GuardedBy("sLock")
checkEntityForDuplicates(SQLiteDatabase db, MigrationEntity entity)222     private boolean checkEntityForDuplicates(SQLiteDatabase db, MigrationEntity entity) {
223         final MigrationPayload payload = entity.getPayload();
224 
225         if (payload instanceof RecordMigrationPayload) {
226             return false; // Do not deduplicate records by entityId
227         }
228 
229         return !insertEntityIdIfNotPresent(db, entity.getEntityId());
230     }
231 
232     /**
233      * Inserts the provided {@code entity} into the database if it doesn't exist yet. Used for data
234      * deduplication.
235      *
236      * @return {@code true} if inserted successfully, {@code false} otherwise.
237      */
238     @GuardedBy("sLock")
insertEntityIdIfNotPresent(SQLiteDatabase db, String entityId)239     private boolean insertEntityIdIfNotPresent(SQLiteDatabase db, String entityId) {
240         final UpsertTableRequest request = mMigrationEntityHelper.getInsertRequest(entityId);
241         return mTransactionManager.insertOrIgnoreOnConflict(db, request) != -1;
242     }
243 
244     /** Indicates an error during entity migration. */
245     public static final class EntityWriteException extends Exception {
246         private final String mEntityId;
247 
EntityWriteException(String entityId, @Nullable Throwable cause)248         private EntityWriteException(String entityId, @Nullable Throwable cause) {
249             super("Error writing entity: " + entityId, cause);
250 
251             mEntityId = entityId;
252         }
253 
254         /**
255          * Returns an identifier of the failed entity, as specified in {@link
256          * MigrationEntity#getEntityId()}.
257          */
getEntityId()258         public String getEntityId() {
259             return mEntityId;
260         }
261     }
262 
263     /**
264      * Internal method to migrate priority list of packages for data category
265      *
266      * @param priorityMigrationPayload contains data category and priority list
267      */
migratePriority(PriorityMigrationPayload priorityMigrationPayload)268     private void migratePriority(PriorityMigrationPayload priorityMigrationPayload) {
269         if (priorityMigrationPayload.getDataOrigins().isEmpty()) {
270             return;
271         }
272 
273         List<String> priorityToMigrate =
274                 priorityMigrationPayload.getDataOrigins().stream()
275                         .map(dataOrigin -> dataOrigin.getPackageName())
276                         .toList();
277 
278         List<String> preMigrationPriority =
279                 mAppInfoHelper.getPackageNames(
280                         mPriorityMigrationHelper.getPreMigrationPriority(
281                                 priorityMigrationPayload.getDataCategory()));
282 
283         /*
284         The combined priority would contain priority order from module appended by additional
285         packages from apk priority order.
286         */
287         List<String> combinedPriorityOrder =
288                 Stream.concat(preMigrationPriority.stream(), priorityToMigrate.stream())
289                         .distinct()
290                         .collect(Collectors.toList());
291 
292         /*
293          * setPriorityOrder removes any additional packages that were not present already in
294          * priority, and it adds any package in priority that was present earlier but missing in
295          * updated priority. This means it will remove any package that don't have required
296          * permission for category as well as it will remove any package that is uninstalled.
297          */
298         mHealthDataCategoryPriorityHelper.setPriorityOrder(
299                 priorityMigrationPayload.getDataCategory(), combinedPriorityOrder);
300     }
301 
302     /**
303      * Migrates Metadata like recordRetentionPeriod
304      *
305      * @param payload of type MetadataMigrationPayload having retention period.
306      */
migrateMetadata(MetadataMigrationPayload payload)307     private void migrateMetadata(MetadataMigrationPayload payload) {
308         mPreferencesManager.setRecordRetentionPeriodInDays(payload.getRecordRetentionPeriodDays());
309     }
310 }
311