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