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