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