• 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 android.health.connect.HealthConnectDataState.MIGRATION_STATE_ALLOWED;
20 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_APP_UPGRADE_REQUIRED;
21 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_COMPLETE;
22 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_IDLE;
23 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_IN_PROGRESS;
24 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_MODULE_UPGRADE_REQUIRED;
25 
26 import static com.android.server.healthconnect.migration.MigrationConstants.ALLOWED_STATE_START_TIME_KEY;
27 import static com.android.server.healthconnect.migration.MigrationConstants.CURRENT_STATE_START_TIME_KEY;
28 import static com.android.server.healthconnect.migration.MigrationConstants.HAVE_CANCELED_OLD_MIGRATION_JOBS_KEY;
29 import static com.android.server.healthconnect.migration.MigrationConstants.HAVE_RESET_MIGRATION_STATE_KEY;
30 import static com.android.server.healthconnect.migration.MigrationConstants.HC_PACKAGE_NAME_CONFIG_NAME;
31 import static com.android.server.healthconnect.migration.MigrationConstants.HC_RELEASE_CERT_CONFIG_NAME;
32 import static com.android.server.healthconnect.migration.MigrationConstants.IDLE_TIMEOUT_REACHED_KEY;
33 import static com.android.server.healthconnect.migration.MigrationConstants.IN_PROGRESS_TIMEOUT_REACHED_KEY;
34 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_COMPLETE_JOB_NAME;
35 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_PAUSE_JOB_NAME;
36 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_STARTS_COUNT_KEY;
37 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_STATE_PREFERENCE_KEY;
38 import static com.android.server.healthconnect.migration.MigrationConstants.MIN_DATA_MIGRATION_SDK_EXTENSION_VERSION_KEY;
39 import static com.android.server.healthconnect.migration.MigrationConstants.PREMATURE_MIGRATION_TIMEOUT_DATE;
40 import static com.android.server.healthconnect.migration.MigrationUtils.filterIntent;
41 import static com.android.server.healthconnect.migration.MigrationUtils.filterPermissions;
42 
43 import android.annotation.NonNull;
44 import android.annotation.UserIdInt;
45 import android.content.Context;
46 import android.content.Intent;
47 import android.content.pm.PackageInfo;
48 import android.content.pm.PackageManager;
49 import android.content.pm.ResolveInfo;
50 import android.content.res.Resources;
51 import android.health.connect.Constants;
52 import android.health.connect.HealthConnectDataState;
53 import android.health.connect.HealthConnectManager;
54 import android.os.Build;
55 import android.os.ext.SdkExtensions;
56 import android.util.Slog;
57 
58 import com.android.internal.annotations.GuardedBy;
59 import com.android.internal.annotations.VisibleForTesting;
60 import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
61 import com.android.server.healthconnect.HealthConnectThreadScheduler;
62 import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
63 
64 import java.time.Instant;
65 import java.time.LocalDate;
66 import java.time.ZoneOffset;
67 import java.util.Arrays;
68 import java.util.Collections;
69 import java.util.HashMap;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Objects;
73 import java.util.Optional;
74 import java.util.Set;
75 import java.util.concurrent.CopyOnWriteArraySet;
76 
77 /**
78  * A database operations helper for migration states management.
79  *
80  * @hide
81  */
82 public final class MigrationStateManager {
83     @GuardedBy("sInstanceLock")
84     private static MigrationStateManager sMigrationStateManager;
85 
86     private static final Object sInstanceLock = new Object();
87     private static final String TAG = "MigrationStateManager";
88     private final HealthConnectDeviceConfigManager mHealthConnectDeviceConfigManager =
89             HealthConnectDeviceConfigManager.getInitialisedInstance();
90 
91     @GuardedBy("mLock")
92     private final Set<StateChangedListener> mStateChangedListeners = new CopyOnWriteArraySet<>();
93 
94     private final Object mLock = new Object();
95     private volatile MigrationBroadcastScheduler mMigrationBroadcastScheduler;
96     private int mUserId;
97 
MigrationStateManager(@serIdInt int userId)98     private MigrationStateManager(@UserIdInt int userId) {
99         mUserId = userId;
100     }
101 
102     /**
103      * Initialises {@link MigrationStateManager} with the provided arguments and returns the
104      * instance.
105      */
106     @NonNull
initializeInstance(@serIdInt int userId)107     public static MigrationStateManager initializeInstance(@UserIdInt int userId) {
108         synchronized (sInstanceLock) {
109             if (Objects.isNull(sMigrationStateManager)) {
110                 sMigrationStateManager = new MigrationStateManager(userId);
111             }
112 
113             return sMigrationStateManager;
114         }
115     }
116 
117     /** Re-initialize this class instance with the new user */
onUserSwitching(@onNull Context context, @UserIdInt int userId)118     public void onUserSwitching(@NonNull Context context, @UserIdInt int userId) {
119         synchronized (mLock) {
120             MigrationStateChangeJob.cancelAllJobs(context);
121             mUserId = userId;
122         }
123     }
124 
125     /** Returns initialised instance of this class. */
126     @NonNull
getInitialisedInstance()127     public static MigrationStateManager getInitialisedInstance() {
128         synchronized (sInstanceLock) {
129             Objects.requireNonNull(sMigrationStateManager);
130             return sMigrationStateManager;
131         }
132     }
133 
134     /** Clears all registered {@link StateChangedListener}. Used in testing. */
135     @VisibleForTesting
clearListeners()136     void clearListeners() {
137         synchronized (mLock) {
138             mStateChangedListeners.clear();
139         }
140     }
141 
142     /** Registers {@link StateChangedListener} for observing migration state changes. */
addStateChangedListener(@onNull StateChangedListener listener)143     public void addStateChangedListener(@NonNull StateChangedListener listener) {
144         synchronized (mLock) {
145             mStateChangedListeners.add(listener);
146         }
147     }
148 
setMigrationBroadcastScheduler( MigrationBroadcastScheduler migrationBroadcastScheduler)149     public void setMigrationBroadcastScheduler(
150             MigrationBroadcastScheduler migrationBroadcastScheduler) {
151         mMigrationBroadcastScheduler = migrationBroadcastScheduler;
152     }
153 
154     /**
155      * Adds the min data migration sdk and updates the migration state to pending.
156      *
157      * @param minVersion the desired sdk version.
158      */
setMinDataMigrationSdkExtensionVersion(@onNull Context context, int minVersion)159     public void setMinDataMigrationSdkExtensionVersion(@NonNull Context context, int minVersion) {
160         synchronized (mLock) {
161             if (minVersion <= getUdcSdkExtensionVersion()) {
162                 updateMigrationState(context, MIGRATION_STATE_ALLOWED);
163                 return;
164             }
165             PreferenceHelper.getInstance()
166                     .insertOrReplacePreference(
167                             MIN_DATA_MIGRATION_SDK_EXTENSION_VERSION_KEY,
168                             String.valueOf(minVersion));
169             updateMigrationState(context, MIGRATION_STATE_MODULE_UPGRADE_REQUIRED);
170         }
171     }
172 
173     /**
174      * @return true when the migration state is in_progress.
175      */
isMigrationInProgress()176     public boolean isMigrationInProgress() {
177         return getMigrationState() == MIGRATION_STATE_IN_PROGRESS;
178     }
179 
180     @HealthConnectDataState.DataMigrationState
getMigrationState()181     public int getMigrationState() {
182         String migrationState =
183                 PreferenceHelper.getInstance().getPreference(MIGRATION_STATE_PREFERENCE_KEY);
184         if (Objects.isNull(migrationState)) {
185             return MIGRATION_STATE_IDLE;
186         }
187 
188         return Integer.parseInt(migrationState);
189     }
190 
switchToSetupForUser(@onNull Context context)191     public void switchToSetupForUser(@NonNull Context context) {
192         synchronized (mLock) {
193             cleanupOldPersistentMigrationJobsIfNeeded(context);
194             resetMigrationStateIfNeeded(context);
195             MigrationStateChangeJob.cancelAllJobs(context);
196             reconcilePackageChangesWithStates(context);
197             reconcileStateChangeJob(context);
198         }
199     }
200 
201     /** Updates the migration state. */
updateMigrationState( @onNull Context context, @HealthConnectDataState.DataMigrationState int state)202     public void updateMigrationState(
203             @NonNull Context context, @HealthConnectDataState.DataMigrationState int state) {
204         synchronized (mLock) {
205             updateMigrationStateGuarded(context, state, false);
206         }
207     }
208 
209     /**
210      * Updates the migration state and the timeout reached.
211      *
212      * @param timeoutReached Whether the previous state has timed out.
213      */
updateMigrationState( @onNull Context context, @HealthConnectDataState.DataMigrationState int state, boolean timeoutReached)214     void updateMigrationState(
215             @NonNull Context context,
216             @HealthConnectDataState.DataMigrationState int state,
217             boolean timeoutReached) {
218         synchronized (mLock) {
219             updateMigrationStateGuarded(context, state, timeoutReached);
220         }
221     }
222 
223     /**
224      * Atomically updates the migration state and the timeout reached.
225      *
226      * @param timeoutReached Whether the previous state has timed out.
227      */
228     @GuardedBy("mLock")
updateMigrationStateGuarded( @onNull Context context, @HealthConnectDataState.DataMigrationState int state, boolean timeoutReached)229     private void updateMigrationStateGuarded(
230             @NonNull Context context,
231             @HealthConnectDataState.DataMigrationState int state,
232             boolean timeoutReached) {
233 
234         if (state == getMigrationState()) {
235             if (Constants.DEBUG) {
236                 Slog.d(TAG, "The new state same as the current state.");
237             }
238             return;
239         }
240 
241         switch (state) {
242             case MIGRATION_STATE_IDLE:
243             case MIGRATION_STATE_APP_UPGRADE_REQUIRED:
244             case MIGRATION_STATE_MODULE_UPGRADE_REQUIRED:
245                 MigrationStateChangeJob.cancelAllJobs(context);
246                 updateMigrationStatePreference(context, state, timeoutReached);
247                 MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserId);
248                 return;
249             case MIGRATION_STATE_IN_PROGRESS:
250                 MigrationStateChangeJob.cancelAllJobs(context);
251                 updateMigrationStatePreference(
252                         context, MIGRATION_STATE_IN_PROGRESS, timeoutReached);
253                 MigrationStateChangeJob.scheduleMigrationPauseJob(context, mUserId);
254                 updateMigrationStartsCount();
255                 return;
256             case MIGRATION_STATE_ALLOWED:
257                 if (hasAllowedStateTimedOut()
258                         || getStartMigrationCount()
259                                 >= mHealthConnectDeviceConfigManager.getMaxStartMigrationCalls()) {
260                     updateMigrationState(context, MIGRATION_STATE_COMPLETE);
261                     return;
262                 }
263                 MigrationStateChangeJob.cancelAllJobs(context);
264                 updateMigrationStatePreference(context, MIGRATION_STATE_ALLOWED, timeoutReached);
265                 MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserId);
266                 return;
267             case MIGRATION_STATE_COMPLETE:
268                 updateMigrationStatePreference(context, MIGRATION_STATE_COMPLETE, timeoutReached);
269                 MigrationStateChangeJob.cancelAllJobs(context);
270                 return;
271             default:
272                 throw new IllegalArgumentException(
273                         "Cannot updated migration state. Unknown state: " + state);
274         }
275     }
276 
clearCaches(@onNull Context context)277     public void clearCaches(@NonNull Context context) {
278         synchronized (mLock) {
279             PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
280             updateMigrationStatePreference(context, MIGRATION_STATE_IDLE, false);
281             preferenceHelper.insertOrReplacePreference(
282                     MIGRATION_STARTS_COUNT_KEY, String.valueOf(0));
283             preferenceHelper.removeKey(ALLOWED_STATE_START_TIME_KEY);
284         }
285     }
286 
287     /** Thrown when an illegal migration state is detected. */
288     public static final class IllegalMigrationStateException extends Exception {
IllegalMigrationStateException(String message)289         public IllegalMigrationStateException(String message) {
290             super(message);
291         }
292     }
293 
294     /**
295      * Throws {@link IllegalMigrationStateException} if the migration can not be started in the
296      * current state. If migration can be started, it will change the state to
297      * MIGRATION_STATE_IN_PROGRESS
298      */
startMigration(@onNull Context context)299     public void startMigration(@NonNull Context context) throws IllegalMigrationStateException {
300         synchronized (mLock) {
301             validateStartMigrationGuarded();
302             updateMigrationStateGuarded(context, MIGRATION_STATE_IN_PROGRESS, false);
303         }
304     }
305 
306     @GuardedBy("mLock")
validateStartMigrationGuarded()307     private void validateStartMigrationGuarded() throws IllegalMigrationStateException {
308         throwIfMigrationIsComplete();
309     }
310 
311     /** Returns the number of times migration has started. */
getMigrationStartsCount()312     public int getMigrationStartsCount() {
313         synchronized (mLock) {
314             PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
315             int res =
316                     Integer.parseInt(
317                             Optional.ofNullable(
318                                             preferenceHelper.getPreference(
319                                                     MIGRATION_STARTS_COUNT_KEY))
320                                     .orElse("0"));
321             return res;
322         }
323     }
324 
325     /**
326      * Throws {@link IllegalMigrationStateException} if the migration can not be finished in the
327      * current state. If migration can be finished, it will change the state to
328      * MIGRATION_STATE_COMPLETE
329      */
finishMigration(@onNull Context context)330     public void finishMigration(@NonNull Context context) throws IllegalMigrationStateException {
331         synchronized (mLock) {
332             throwIfMigrationIsComplete();
333             if (getMigrationState() != MIGRATION_STATE_IN_PROGRESS
334                     && getMigrationState() != MIGRATION_STATE_ALLOWED) {
335                 throw new IllegalMigrationStateException("Migration is not started.");
336             }
337             updateMigrationStateGuarded(context, MIGRATION_STATE_COMPLETE, false);
338         }
339     }
340 
341     /**
342      * Throws {@link IllegalMigrationStateException} if the migration can not be performed in the
343      * current state.
344      */
validateWriteMigrationData()345     public void validateWriteMigrationData() throws IllegalMigrationStateException {
346         synchronized (mLock) {
347             throwIfMigrationIsComplete();
348             if (getMigrationState() != MIGRATION_STATE_IN_PROGRESS) {
349                 throw new IllegalMigrationStateException("Migration is not started.");
350             }
351         }
352     }
353 
354     /**
355      * Throws {@link IllegalMigrationStateException} if the sdk extension version can not be set in
356      * the current state.
357      */
validateSetMinSdkVersion()358     public void validateSetMinSdkVersion() throws IllegalMigrationStateException {
359         synchronized (mLock) {
360             throwIfMigrationIsComplete();
361             if (getMigrationState() == MIGRATION_STATE_IN_PROGRESS) {
362                 throw new IllegalMigrationStateException(
363                         "Cannot set the sdk extension version. Migration already in progress.");
364             }
365         }
366     }
367 
onPackageInstalledOrChanged(@onNull Context context, @NonNull String packageName)368     void onPackageInstalledOrChanged(@NonNull Context context, @NonNull String packageName) {
369         synchronized (mLock) {
370             onPackageInstalledOrChangedGuarded(context, packageName);
371         }
372     }
373 
374     @GuardedBy("mLock")
onPackageInstalledOrChangedGuarded( @onNull Context context, @NonNull String packageName)375     private void onPackageInstalledOrChangedGuarded(
376             @NonNull Context context, @NonNull String packageName) {
377 
378         String hcMigratorPackage = getDataMigratorPackageName(context);
379         if (!Objects.equals(hcMigratorPackage, packageName)) {
380             return;
381         }
382 
383         int migrationState = getMigrationState();
384         if ((migrationState == MIGRATION_STATE_IDLE
385                         || migrationState == MIGRATION_STATE_APP_UPGRADE_REQUIRED)
386                 && isMigrationAware(context, packageName)) {
387 
388             updateMigrationState(context, MIGRATION_STATE_ALLOWED);
389             return;
390         }
391 
392         if (migrationState == MIGRATION_STATE_IDLE
393                 && hasMigratorPackageKnownSignerSignature(context, packageName)
394                 && !MigrationUtils.isPackageStub(context, packageName)) {
395             // apk needs to upgrade
396             updateMigrationState(context, MIGRATION_STATE_APP_UPGRADE_REQUIRED);
397         }
398 
399         if (migrationState == MIGRATION_STATE_ALLOWED) {
400             for (StateChangedListener listener : mStateChangedListeners) {
401                 listener.onChanged(migrationState);
402             }
403         }
404     }
405 
onPackageRemoved(@onNull Context context, @NonNull String packageName)406     void onPackageRemoved(@NonNull Context context, @NonNull String packageName) {
407         synchronized (mLock) {
408             onPackageRemovedGuarded(context, packageName);
409         }
410     }
411 
412     @GuardedBy("mLock")
onPackageRemovedGuarded(@onNull Context context, @NonNull String packageName)413     private void onPackageRemovedGuarded(@NonNull Context context, @NonNull String packageName) {
414         String hcMigratorPackage = getDataMigratorPackageName(context);
415         if (!Objects.equals(hcMigratorPackage, packageName)) {
416             return;
417         }
418 
419         if (getMigrationState() != MIGRATION_STATE_COMPLETE) {
420             if (Constants.DEBUG) {
421                 Slog.d(TAG, "Migrator package uninstalled. Marking migration complete.");
422             }
423 
424             updateMigrationState(context, MIGRATION_STATE_COMPLETE);
425         }
426     }
427 
428     /**
429      * Updates the migration state preference and the timeout reached preferences.
430      *
431      * @param timeoutReached Whether the previous state has timed out.
432      */
433     @GuardedBy("mLock")
updateMigrationStatePreference( @onNull Context context, @HealthConnectDataState.DataMigrationState int migrationState, boolean timeoutReached)434     private void updateMigrationStatePreference(
435             @NonNull Context context,
436             @HealthConnectDataState.DataMigrationState int migrationState,
437             boolean timeoutReached) {
438 
439         @HealthConnectDataState.DataMigrationState int previousMigrationState = getMigrationState();
440 
441         HashMap<String, String> preferences =
442                 new HashMap<>(
443                         Map.of(
444                                 MIGRATION_STATE_PREFERENCE_KEY,
445                                 String.valueOf(migrationState),
446                                 CURRENT_STATE_START_TIME_KEY,
447                                 Instant.now().toString()));
448 
449         if (migrationState == MIGRATION_STATE_IN_PROGRESS) {
450             // Reset the in progress timeout key reached if we move to In Progress
451             preferences.put(IN_PROGRESS_TIMEOUT_REACHED_KEY, String.valueOf(false));
452         }
453 
454         if (migrationState == MIGRATION_STATE_ALLOWED && timeoutReached) {
455             preferences.put(IN_PROGRESS_TIMEOUT_REACHED_KEY, String.valueOf(true));
456         }
457 
458         if (migrationState == MIGRATION_STATE_COMPLETE
459                 && previousMigrationState == MIGRATION_STATE_IDLE
460                 && timeoutReached) {
461             preferences.put(IDLE_TIMEOUT_REACHED_KEY, String.valueOf(true));
462         }
463 
464         // If we are setting the migration state to ALLOWED for the first time.
465         if (migrationState == MIGRATION_STATE_ALLOWED && Objects.isNull(getAllowedStateTimeout())) {
466             preferences.put(ALLOWED_STATE_START_TIME_KEY, Instant.now().toString());
467         }
468         PreferenceHelper.getInstance().insertOrReplacePreferencesTransaction(preferences);
469 
470         if (mMigrationBroadcastScheduler != null) {
471             //noinspection Convert2Lambda
472             HealthConnectThreadScheduler.scheduleInternalTask(
473                     new Runnable() {
474                         @Override
475                         public void run() {
476                             try {
477                                 mMigrationBroadcastScheduler.scheduleNewJobs(context);
478                             } catch (Exception e) {
479                                 Slog.e(TAG, "Migration broadcast schedule failed", e);
480                             }
481                         }
482                     });
483         } else if (Constants.DEBUG) {
484             Slog.d(
485                     TAG,
486                     "Unable to schedule migration broadcasts: "
487                             + "MigrationBroadcastScheduler object is null");
488         }
489 
490         for (StateChangedListener listener : mStateChangedListeners) {
491             listener.onChanged(migrationState);
492         }
493     }
494 
495     /**
496      * Checks if the original {@link MIGRATION_STATE_ALLOWED} timeout period has passed. We do not
497      * want to reset the ALLOWED_STATE timeout everytime state changes to this state, hence
498      * persisting the original timeout time.
499      */
hasAllowedStateTimedOut()500     boolean hasAllowedStateTimedOut() {
501         String allowedStateTimeout = getAllowedStateTimeout();
502         if (!Objects.isNull(allowedStateTimeout)
503                 && Instant.now().isAfter(Instant.parse(allowedStateTimeout))) {
504             Slog.e(TAG, "Allowed state period has timed out.");
505             return true;
506         }
507         return false;
508     }
509 
510     /** Checks if the IN_PROGRESS_TIMEOUT has passed. */
hasInProgressStateTimedOut()511     boolean hasInProgressStateTimedOut() {
512         synchronized (mLock) {
513             String inProgressTimeoutReached =
514                     PreferenceHelper.getInstance().getPreference(IN_PROGRESS_TIMEOUT_REACHED_KEY);
515 
516             if (!Objects.isNull(inProgressTimeoutReached)) {
517                 return Boolean.parseBoolean(inProgressTimeoutReached);
518             }
519             return false;
520         }
521     }
522 
523     /** Checks if the IDLE state has timed out. */
hasIdleStateTimedOut()524     boolean hasIdleStateTimedOut() {
525         synchronized (mLock) {
526             String idleStateTimeoutReached =
527                     PreferenceHelper.getInstance().getPreference(IDLE_TIMEOUT_REACHED_KEY);
528 
529             if (!Objects.isNull(idleStateTimeoutReached)) {
530                 return Boolean.parseBoolean(idleStateTimeoutReached);
531             }
532             return false;
533         }
534     }
535 
536     /**
537      * Reconcile migration state to the current migrator package status in case we missed a package
538      * change broadcast.
539      */
540     @GuardedBy("mLock")
reconcilePackageChangesWithStates(Context context)541     private void reconcilePackageChangesWithStates(Context context) {
542         int migrationState = getMigrationState();
543         if (migrationState == MIGRATION_STATE_APP_UPGRADE_REQUIRED
544                 && existsMigrationAwarePackage(context)) {
545             updateMigrationState(context, MIGRATION_STATE_ALLOWED);
546             return;
547         }
548 
549         if (migrationState == MIGRATION_STATE_IDLE) {
550             if (existsMigrationAwarePackage(context)) {
551                 updateMigrationState(context, MIGRATION_STATE_ALLOWED);
552                 return;
553             }
554 
555             if (existsMigratorPackage(context)
556                     && !MigrationUtils.isPackageStub(
557                             context, getDataMigratorPackageName(context))) {
558                 updateMigrationState(context, MIGRATION_STATE_APP_UPGRADE_REQUIRED);
559                 return;
560             }
561         }
562         if (migrationState != MIGRATION_STATE_IDLE && migrationState != MIGRATION_STATE_COMPLETE) {
563             completeMigrationIfNoMigratorPackageAvailable(context);
564         }
565     }
566 
567     /** Reconcile the current state with its appropriate state change job. */
568     @GuardedBy("mLock")
reconcileStateChangeJob(@onNull Context context)569     private void reconcileStateChangeJob(@NonNull Context context) {
570         switch (getMigrationState()) {
571             case MIGRATION_STATE_IDLE:
572             case MIGRATION_STATE_APP_UPGRADE_REQUIRED:
573             case MIGRATION_STATE_ALLOWED:
574                 if (!MigrationStateChangeJob.existsAStateChangeJob(
575                         context, MIGRATION_COMPLETE_JOB_NAME)) {
576                     MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserId);
577                 }
578                 return;
579             case MIGRATION_STATE_MODULE_UPGRADE_REQUIRED:
580                 handleIsUpgradeStillRequired(context);
581                 return;
582 
583             case MIGRATION_STATE_IN_PROGRESS:
584                 if (!MigrationStateChangeJob.existsAStateChangeJob(
585                         context, MIGRATION_PAUSE_JOB_NAME)) {
586                     MigrationStateChangeJob.scheduleMigrationPauseJob(context, mUserId);
587                 }
588                 return;
589 
590             case MIGRATION_STATE_COMPLETE:
591                 MigrationStateChangeJob.cancelAllJobs(context);
592         }
593     }
594 
595     /**
596      * Checks if the version set by the migrator apk is the current module version and send a {@link
597      * HealthConnectManager.ACTION_HEALTH_CONNECT_MIGRATION_READY intent. If not, re-sync the state
598      * update job.}
599      */
600     @GuardedBy("mLock")
handleIsUpgradeStillRequired(@onNull Context context)601     private void handleIsUpgradeStillRequired(@NonNull Context context) {
602         if (Integer.parseInt(
603                         PreferenceHelper.getInstance()
604                                 .getPreference(MIN_DATA_MIGRATION_SDK_EXTENSION_VERSION_KEY))
605                 <= getUdcSdkExtensionVersion()) {
606             updateMigrationState(context, MIGRATION_STATE_ALLOWED);
607             return;
608         }
609         if (!MigrationStateChangeJob.existsAStateChangeJob(context, MIGRATION_COMPLETE_JOB_NAME)) {
610             MigrationStateChangeJob.scheduleMigrationCompletionJob(context, mUserId);
611         }
612     }
613 
getAllowedStateTimeout()614     String getAllowedStateTimeout() {
615         String allowedStateStartTime =
616                 PreferenceHelper.getInstance().getPreference(ALLOWED_STATE_START_TIME_KEY);
617         if (allowedStateStartTime != null) {
618             return Instant.parse(allowedStateStartTime)
619                     .plusMillis(
620                             mHealthConnectDeviceConfigManager
621                                     .getNonIdleStateTimeoutPeriod()
622                                     .toMillis())
623                     .toString();
624         }
625         return null;
626     }
627 
throwIfMigrationIsComplete()628     private void throwIfMigrationIsComplete() throws IllegalMigrationStateException {
629         if (getMigrationState() == MIGRATION_STATE_COMPLETE) {
630             throw new IllegalMigrationStateException("Migration already marked complete.");
631         }
632     }
633 
634     /**
635      * Tracks the number of times migration is started from {@link MIGRATION_STATE_ALLOWED}. If more
636      * than 3 times, the migration is marked as complete
637      */
638     @GuardedBy("mLock")
updateMigrationStartsCount()639     private void updateMigrationStartsCount() {
640         PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
641         String migrationStartsCount =
642                 Optional.ofNullable(preferenceHelper.getPreference(MIGRATION_STARTS_COUNT_KEY))
643                         .orElse("0");
644 
645         preferenceHelper.insertOrReplacePreference(
646                 MIGRATION_STARTS_COUNT_KEY,
647                 String.valueOf(Integer.parseInt(migrationStartsCount) + 1));
648     }
649 
getDataMigratorPackageName(@onNull Context context)650     private String getDataMigratorPackageName(@NonNull Context context) {
651         return context.getString(
652                 context.getResources().getIdentifier(HC_PACKAGE_NAME_CONFIG_NAME, null, null));
653     }
654 
completeMigrationIfNoMigratorPackageAvailable(@onNull Context context)655     private void completeMigrationIfNoMigratorPackageAvailable(@NonNull Context context) {
656         if (existsMigrationAwarePackage(context)) {
657             if (Constants.DEBUG) {
658                 Slog.d(TAG, "There is a migration aware package.");
659             }
660             return;
661         }
662 
663         if (existsMigratorPackage(context)) {
664             if (Constants.DEBUG) {
665                 Slog.d(TAG, "There is a package with migration known signers certificate.");
666             }
667             return;
668         }
669 
670         if (Constants.DEBUG) {
671             Slog.d(
672                     TAG,
673                     "There is no migration aware package or any package with migration known "
674                             + "signers certificate. Marking migration as complete.");
675         }
676         updateMigrationState(context, MIGRATION_STATE_COMPLETE);
677     }
678 
679     /** Returns whether there exists a package that is aware of migration. */
existsMigrationAwarePackage(@onNull Context context)680     public boolean existsMigrationAwarePackage(@NonNull Context context) {
681         List<String> filteredPackages =
682                 filterIntent(
683                         context,
684                         filterPermissions(context),
685                         PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS);
686         String dataMigratorPackageName = getDataMigratorPackageName(context);
687         List<String> filteredDataMigratorPackageNames =
688                 filteredPackages.stream()
689                         .filter(packageName -> packageName.equals(dataMigratorPackageName))
690                         .toList();
691 
692         return filteredDataMigratorPackageNames.size() != 0;
693     }
694 
695     /**
696      * Returns whether there exists a package that is signed with the correct signatures for
697      * migration.
698      */
existsMigratorPackage(@onNull Context context)699     public boolean existsMigratorPackage(@NonNull Context context) {
700         // Search through all packages by known signer certificate.
701         List<PackageInfo> allPackages =
702                 context.getPackageManager()
703                         .getInstalledPackages(PackageManager.GET_SIGNING_CERTIFICATES);
704         String[] knownSignerCerts = getMigrationKnownSignerCertificates(context);
705 
706         for (PackageInfo packageInfo : allPackages) {
707             if (hasMatchingSignatures(getPackageSignatures(packageInfo), knownSignerCerts)) {
708                 return true;
709             }
710         }
711         return false;
712     }
713 
isMigrationAware(@onNull Context context, @NonNull String packageName)714     private boolean isMigrationAware(@NonNull Context context, @NonNull String packageName) {
715         List<String> permissionFilteredPackages = filterPermissions(context);
716         List<String> filteredPackages =
717                 filterIntent(
718                         context,
719                         permissionFilteredPackages,
720                         PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS);
721         int numPackages = filteredPackages.size();
722 
723         if (numPackages == 0) {
724             Slog.i(TAG, "There are no migration aware apps");
725         } else if (numPackages == 1) {
726             return Objects.equals(filteredPackages.get(0), packageName);
727         }
728         return false;
729     }
730 
731     /** Checks whether the APK migration flag is on. */
doesMigratorHandleInfoIntent(@onNull Context context)732     boolean doesMigratorHandleInfoIntent(@NonNull Context context) {
733         String packageName = getDataMigratorPackageName(context);
734         Intent intent =
735                 new Intent(HealthConnectManager.ACTION_SHOW_MIGRATION_INFO).setPackage(packageName);
736         PackageManager pm = context.getPackageManager();
737         List<ResolveInfo> allComponents =
738                 pm.queryIntentActivities(
739                         intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL));
740         return !allComponents.isEmpty();
741     }
742 
hasMigratorPackageKnownSignerSignature( @onNull Context context, @NonNull String packageName)743     private static boolean hasMigratorPackageKnownSignerSignature(
744             @NonNull Context context, @NonNull String packageName) {
745         List<String> stringSignatures;
746         try {
747             stringSignatures =
748                     getPackageSignatures(
749                             context.getPackageManager()
750                                     .getPackageInfo(
751                                             packageName, PackageManager.GET_SIGNING_CERTIFICATES));
752 
753         } catch (PackageManager.NameNotFoundException e) {
754             Slog.i(TAG, "Could not get package signatures. Package not found");
755             return false;
756         }
757 
758         if (stringSignatures.isEmpty()) {
759             return false;
760         }
761         return hasMatchingSignatures(
762                 stringSignatures, getMigrationKnownSignerCertificates(context));
763     }
764 
hasMatchingSignatures( List<String> stringSignatures, String[] migrationKnownSignerCertificates)765     private static boolean hasMatchingSignatures(
766             List<String> stringSignatures, String[] migrationKnownSignerCertificates) {
767 
768         return !Collections.disjoint(
769                 stringSignatures.stream().map(String::toLowerCase).toList(),
770                 Arrays.stream(migrationKnownSignerCertificates).map(String::toLowerCase).toList());
771     }
772 
getMigrationKnownSignerCertificates(Context context)773     private static String[] getMigrationKnownSignerCertificates(Context context) {
774         return context.getResources()
775                 .getStringArray(
776                         Resources.getSystem()
777                                 .getIdentifier(HC_RELEASE_CERT_CONFIG_NAME, null, null));
778     }
779 
getPackageSignatures(PackageInfo packageInfo)780     private static List<String> getPackageSignatures(PackageInfo packageInfo) {
781         return Arrays.stream(packageInfo.signingInfo.getApkContentsSigners())
782                 .map(signature -> MigrationUtils.computeSha256DigestBytes(signature.toByteArray()))
783                 .filter(signature -> signature != null)
784                 .toList();
785     }
786 
getUdcSdkExtensionVersion()787     private int getUdcSdkExtensionVersion() {
788         return SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
789     }
790 
getStartMigrationCount()791     private int getStartMigrationCount() {
792         return Integer.parseInt(
793                 Optional.ofNullable(
794                                 PreferenceHelper.getInstance()
795                                         .getPreference(MIGRATION_STARTS_COUNT_KEY))
796                         .orElse("0"));
797     }
798 
cleanupOldPersistentMigrationJobsIfNeeded(@onNull Context context)799     private void cleanupOldPersistentMigrationJobsIfNeeded(@NonNull Context context) {
800         PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
801 
802         if (!Boolean.parseBoolean(
803                 preferenceHelper.getPreference(HAVE_CANCELED_OLD_MIGRATION_JOBS_KEY))) {
804             MigrationStateChangeJob.cleanupOldPersistentMigrationJobs(context);
805             preferenceHelper.insertOrReplacePreference(
806                     HAVE_CANCELED_OLD_MIGRATION_JOBS_KEY, String.valueOf(true));
807         }
808     }
809 
810     /**
811      * Resets migration state to IDLE state for early users whose migration might have timed out
812      * before they migrate data.
813      */
resetMigrationStateIfNeeded(@onNull Context context)814     void resetMigrationStateIfNeeded(@NonNull Context context) {
815         PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
816 
817         if (!Boolean.parseBoolean(preferenceHelper.getPreference(HAVE_RESET_MIGRATION_STATE_KEY))
818                 && hasMigrationTimedOutPrematurely()) {
819             updateMigrationState(context, MIGRATION_STATE_IDLE);
820             preferenceHelper.insertOrReplacePreference(
821                     HAVE_RESET_MIGRATION_STATE_KEY, String.valueOf(true));
822         }
823     }
824 
hasMigrationTimedOutPrematurely()825     private boolean hasMigrationTimedOutPrematurely() {
826         String currentStateStartTime =
827                 PreferenceHelper.getInstance().getPreference(CURRENT_STATE_START_TIME_KEY);
828 
829         if (!Objects.isNull(currentStateStartTime)) {
830             return getMigrationState() == MIGRATION_STATE_COMPLETE
831                     && LocalDate.ofInstant(Instant.parse(currentStateStartTime), ZoneOffset.MIN)
832                             .isBefore(PREMATURE_MIGRATION_TIMEOUT_DATE);
833         }
834         return false;
835     }
836 
837     /**
838      * A listener for observing migration state changes.
839      *
840      * @see MigrationStateManager#addStateChangedListener(StateChangedListener)
841      */
842     public interface StateChangedListener {
843 
844         /**
845          * Called on every migration state change.
846          *
847          * @param state the new migration state.
848          */
onChanged(@ealthConnectDataState.DataMigrationState int state)849         void onChanged(@HealthConnectDataState.DataMigrationState int state);
850     }
851 }
852