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