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