1 /* 2 * Copyright (C) 2024 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.art; 18 19 import static com.android.server.art.model.ArtFlags.ScheduleStatus; 20 import static com.android.server.art.prereboot.PreRebootDriver.PreRebootResult; 21 import static com.android.server.art.proto.PreRebootStats.Status; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.SuppressLint; 26 import android.app.job.JobInfo; 27 import android.app.job.JobParameters; 28 import android.app.job.JobScheduler; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.os.Binder; 32 import android.os.Build; 33 import android.os.CancellationSignal; 34 import android.os.PersistableBundle; 35 import android.os.RemoteException; 36 import android.os.ServiceSpecificException; 37 import android.os.SystemProperties; 38 import android.os.UpdateEngine; 39 import android.provider.DeviceConfig; 40 41 import androidx.annotation.RequiresApi; 42 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.server.art.model.ArtFlags; 46 import com.android.server.art.model.ArtServiceJobInterface; 47 import com.android.server.art.prereboot.PreRebootDriver; 48 import com.android.server.art.prereboot.PreRebootStatsReporter; 49 50 import java.time.Duration; 51 import java.util.Objects; 52 import java.util.UUID; 53 import java.util.concurrent.CompletableFuture; 54 import java.util.concurrent.ExecutorService; 55 import java.util.concurrent.Executors; 56 import java.util.concurrent.LinkedBlockingQueue; 57 import java.util.concurrent.ThreadPoolExecutor; 58 import java.util.concurrent.TimeUnit; 59 60 /** 61 * The Pre-reboot Dexopt job. 62 * 63 * During Pre-reboot Dexopt, the old version of this code is run. 64 * 65 * @hide 66 */ 67 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) 68 public class PreRebootDexoptJob implements ArtServiceJobInterface { 69 /** 70 * "android" is the package name for a <service> declared in 71 * frameworks/base/core/res/AndroidManifest.xml 72 */ 73 private static final String JOB_PKG_NAME = Utils.PLATFORM_PACKAGE_NAME; 74 /** An arbitrary number. Must be unique among all jobs owned by the system uid. */ 75 public static final int JOB_ID = 27873781; 76 77 private static final long UPDATE_ENGINE_TIMEOUT_MS = 10000; 78 79 @NonNull private final Injector mInjector; 80 81 // Job state variables. 82 // The monitor of `this` is notified when `mRunningJob` or `mIsUpdateEngineReady` is changed. 83 // Also, an optimization to make `triggerUpdateEnginePostinstallAndWait` return early, if 84 // `mCancellationSignal` is fired **before `triggerUpdateEnginePostinstallAndWait` returns**, it 85 // should be guaranteed that the monitor of `this` is notified when it happens. 86 // `mRunningJob` and `mCancellationSignal` have the same nullness. 87 @GuardedBy("this") @Nullable private CompletableFuture<Void> mRunningJob = null; 88 @GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null; 89 /** Whether update_engine has mapped snapshot devices. Only applicable to an OTA update. */ 90 @GuardedBy("this") private boolean mIsUpdateEngineReady = false; 91 92 /** Whether `mRunningJob` is running from the job scheduler's perspective. */ 93 @GuardedBy("this") private boolean mIsRunningJobKnownByJobScheduler = false; 94 95 /** The slot that contains the OTA update, "_a" or "_b", or null for a Mainline update. */ 96 @GuardedBy("this") @Nullable private String mOtaSlot = null; 97 98 /** 99 * Whether to map/unmap snapshots ourselves rather than using update_engine. Only applicable to 100 * an OTA update. For legacy use only. 101 */ 102 @GuardedBy("this") private boolean mMapSnapshotsForOta = false; 103 104 /** 105 * Offloads `onStartJob` and `onStopJob` calls from the main thread while keeping the execution 106 * order as the main thread does. 107 * Also offloads `onUpdateReady` calls from the package manager thread. We reuse this executor 108 * just for simplicity. The execution order does not matter. 109 */ 110 @NonNull 111 private final ThreadPoolExecutor mSerializedExecutor = 112 new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */, 113 60 /* keepAliveTime */, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 114 115 /** 116 * A separate thread for executing `mRunningJob`. We avoid using any known thread / thread pool 117 * such as {@link java.util.concurrent.ForkJoinPool} and {@link 118 * com.android.internal.os.BackgroundThread} because we don't want to block other things that 119 * use known threads / thread pools. 120 */ 121 @NonNull 122 private final ThreadPoolExecutor mExecutor = 123 new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */, 124 60 /* keepAliveTime */, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 125 126 // Mutations to the global state of Pre-reboot Dexopt, including mounts, staged files, and 127 // stats, should only be done when there is no job running and the `this` lock is held, or by 128 // the job itself. 129 PreRebootDexoptJob(@onNull Context context, @NonNull ArtManagerLocal artManagerLocal)130 public PreRebootDexoptJob(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal) { 131 this(new Injector(context, artManagerLocal)); 132 } 133 134 @VisibleForTesting PreRebootDexoptJob(@onNull Injector injector)135 public PreRebootDexoptJob(@NonNull Injector injector) { 136 mInjector = injector; 137 // Recycle the thread if it's not used for `keepAliveTime`. 138 mSerializedExecutor.allowsCoreThreadTimeOut(); 139 mExecutor.allowsCoreThreadTimeOut(); 140 if (hasStarted()) { 141 maybeCleanUpChrootAsyncForStartup(); 142 } 143 } 144 145 @Override onStartJob( @onNull BackgroundDexoptJobService jobService, @NonNull JobParameters params)146 public boolean onStartJob( 147 @NonNull BackgroundDexoptJobService jobService, @NonNull JobParameters params) { 148 mSerializedExecutor.execute(() -> onStartJobImpl(jobService, params)); 149 // "true" means the job will continue running until `jobFinished` is called. 150 return true; 151 } 152 153 @VisibleForTesting onStartJobImpl( @onNull BackgroundDexoptJobService jobService, @NonNull JobParameters params)154 public synchronized void onStartJobImpl( 155 @NonNull BackgroundDexoptJobService jobService, @NonNull JobParameters params) { 156 JobInfo pendingJob = mInjector.getJobScheduler().getPendingJob(JOB_ID); 157 if (pendingJob == null 158 || !params.getExtras().getString("ticket").equals( 159 pendingJob.getExtras().getString("ticket"))) { 160 // Job expired. We can only get here due to a race, and this should be very rare. 161 Utils.check(!mIsRunningJobKnownByJobScheduler); 162 return; 163 } 164 165 mIsRunningJobKnownByJobScheduler = true; 166 @SuppressWarnings("GuardedBy") // https://errorprone.info/bugpattern/GuardedBy#limitations 167 Runnable onJobFinishedLocked = () -> { 168 Utils.check(mIsRunningJobKnownByJobScheduler); 169 mIsRunningJobKnownByJobScheduler = false; 170 // There can be four cases when we reach here: 171 // 1. The job has completed: No need to reschedule. 172 // 2. The job failed: It means something went wrong, so we don't reschedule the job 173 // because it will likely fail again. 174 // 3. The job was killed by update_engine, probably because the OTA was revoked: We 175 // should definitely give up. 176 // 4. The job was cancelled by the job scheduler: The job will be rescheduled regardless 177 // of the arguments we pass here because the return value of `onStopJob` will be 178 // respected, and this call will be ignored. 179 // Therefore, we can always pass `false` to the `wantsReschedule` parameter. 180 jobService.jobFinished(params, false /* wantsReschedule */); 181 }; 182 startLocked(onJobFinishedLocked, false /* isUpdateEngineReady */).exceptionally(t -> { 183 AsLog.e("Fatal error", t); 184 return null; 185 }); 186 } 187 188 @Override onStopJob(@onNull JobParameters params)189 public boolean onStopJob(@NonNull JobParameters params) { 190 mSerializedExecutor.execute(() -> onStopJobImpl(params)); 191 // "true" means to execute again with the default retry policy. 192 return true; 193 } 194 195 @VisibleForTesting onStopJobImpl(@onNull JobParameters params)196 public synchronized void onStopJobImpl(@NonNull JobParameters params) { 197 if (mIsRunningJobKnownByJobScheduler) { 198 cancelGivenLocked(mRunningJob, false /* expectInterrupt */); 199 } 200 } 201 202 /** 203 * Notifies this class that an update (OTA or Mainline) is ready. 204 * 205 * @param otaSlot The slot that contains the OTA update, "_a" or "_b", or null for a Mainline 206 * update. 207 */ onUpdateReady(@ullable String otaSlot)208 public synchronized void onUpdateReady(@Nullable String otaSlot) { 209 // `onUpdateReadyImpl` can take time, especially on `resetLocked` when there are staged 210 // files from a previous run to be cleaned up, so we put it on a separate thread. 211 mSerializedExecutor.execute(() -> onUpdateReadyImpl(otaSlot)); 212 } 213 214 /** For internal and testing use only. */ onUpdateReadyImpl(@ullable String otaSlot)215 public synchronized @ScheduleStatus int onUpdateReadyImpl(@Nullable String otaSlot) { 216 cancelAnyLocked(); 217 resetLocked(); 218 updateOtaSlotLocked(otaSlot); 219 // If we can't call update_engine to map snapshot devices, then we have to map snapshot 220 // devices ourselves. This only happens on a few OEM devices that have 221 // "dalvik.vm.pr_dexopt_async_for_ota=true" and only on Android V. 222 mMapSnapshotsForOta = !android.os.Flags.updateEngineApi(); 223 return scheduleLocked(); 224 } 225 226 /** 227 * Same as {@link #onUpdateReady}, but starts the job immediately, instead of going through the 228 * job scheduler. 229 * 230 * @param isUpdateEngineReady whether update_engine has mapped snapshot devices. Only applicable 231 * to an OTA update. 232 * @return The future of the job, or null if Pre-reboot Dexopt is not enabled. 233 */ 234 @Nullable onUpdateReadyStartNow( @ullable String otaSlot, boolean isUpdateEngineReady)235 public synchronized CompletableFuture<Void> onUpdateReadyStartNow( 236 @Nullable String otaSlot, boolean isUpdateEngineReady) { 237 cancelAnyLocked(); 238 resetLocked(); 239 updateOtaSlotLocked(otaSlot); 240 // If update_engine hasn't mapped snapshot devices and we can't call update_engine to map 241 // snapshot devices, then we have to map snapshot devices ourselves. This only happens on 242 // the `pm art pr-dexopt-job --run` command for local development purposes and only on 243 // Android V. 244 mMapSnapshotsForOta = !isUpdateEngineReady && !android.os.Flags.updateEngineApi(); 245 if (!isEnabled()) { 246 mInjector.getStatsReporter().recordJobNotScheduled( 247 Status.STATUS_NOT_SCHEDULED_DISABLED, isOtaUpdate()); 248 return null; 249 } 250 mInjector.getStatsReporter().recordJobScheduled(false /* isAsync */, isOtaUpdate()); 251 return startLocked(null /* onJobFinishedLocked */, isUpdateEngineReady); 252 } 253 test()254 public synchronized void test() { 255 cancelAnyLocked(); 256 mInjector.getPreRebootDriver().test(); 257 } 258 259 /** @see #cancelGivenLocked */ cancelGiven( @onNull CompletableFuture<Void> job, boolean expectInterrupt)260 public synchronized void cancelGiven( 261 @NonNull CompletableFuture<Void> job, boolean expectInterrupt) { 262 cancelGivenLocked(job, expectInterrupt); 263 } 264 265 /** @see #cancelAnyLocked */ cancelAny()266 public synchronized void cancelAny() { 267 cancelAnyLocked(); 268 } 269 270 /** Cleans up chroot if it exists. Only expected to be called on system server startup. */ maybeCleanUpChrootAsyncForStartup()271 private synchronized void maybeCleanUpChrootAsyncForStartup() { 272 // We only get here when there was a system server restart (probably due to a crash). In 273 // this case, it's possible that a previous Pre-reboot Dexopt job didn't end normally and 274 // left over a chroot, so we need to clean it up. 275 // We assign this operation to `mRunningJob` to block other operations on their calls to 276 // `cancelAnyLocked`. 277 // `mCancellationSignal` is a placeholder and the signal actually ignored. It's created just 278 // for keeping the invariant that `mRunningJob` and `mCancellationSignal` have the same 279 // nullness, to make other code simpler. 280 mCancellationSignal = new CancellationSignal(); 281 mRunningJob = new CompletableFuture().runAsync(() -> { 282 try { 283 mInjector.getPreRebootDriver().maybeCleanUpChroot(); 284 } finally { 285 synchronized (this) { 286 mRunningJob = null; 287 mCancellationSignal = null; 288 this.notifyAll(); 289 } 290 } 291 }, mExecutor); 292 this.notifyAll(); 293 } 294 295 @VisibleForTesting waitForRunningJob()296 public synchronized void waitForRunningJob() { 297 while (mRunningJob != null) { 298 try { 299 this.wait(); 300 } catch (InterruptedException e) { 301 AsLog.wtf("Interrupted", e); 302 } 303 } 304 } 305 306 @VisibleForTesting hasRunningJob()307 public synchronized boolean hasRunningJob() { 308 return mRunningJob != null; 309 } 310 311 @GuardedBy("this") scheduleLocked()312 private @ScheduleStatus int scheduleLocked() { 313 if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { 314 throw new IllegalStateException("This job cannot be scheduled"); 315 } 316 317 if (!isEnabled()) { 318 mInjector.getStatsReporter().recordJobNotScheduled( 319 Status.STATUS_NOT_SCHEDULED_DISABLED, isOtaUpdate()); 320 return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP; 321 } 322 323 String ticket = UUID.randomUUID().toString(); 324 PersistableBundle extras = new PersistableBundle(1 /* capacity */); 325 extras.putString("ticket", ticket); 326 JobInfo info = new JobInfo 327 .Builder(JOB_ID, 328 new ComponentName(JOB_PKG_NAME, 329 BackgroundDexoptJobService.class.getName())) 330 .setExtras(extras) 331 .setRequiresDeviceIdle(true) 332 .setRequiresCharging(true) 333 .setRequiresBatteryNotLow(true) 334 // The latency is to wait for update_engine to finish. 335 .setMinimumLatency(Duration.ofMinutes(10).toMillis()) 336 .build(); 337 338 /* @JobScheduler.Result */ int result; 339 340 // This operation requires the uid to be "system" (1000). 341 long identityToken = Binder.clearCallingIdentity(); 342 try { 343 result = mInjector.getJobScheduler().schedule(info); 344 } finally { 345 Binder.restoreCallingIdentity(identityToken); 346 } 347 348 if (result == JobScheduler.RESULT_SUCCESS) { 349 AsLog.i("Pre-reboot Dexopt Job scheduled"); 350 mInjector.getStatsReporter().recordJobScheduled(true /* isAsync */, isOtaUpdate()); 351 return ArtFlags.SCHEDULE_SUCCESS; 352 } else { 353 AsLog.i("Failed to schedule Pre-reboot Dexopt Job"); 354 mInjector.getStatsReporter().recordJobNotScheduled( 355 Status.STATUS_NOT_SCHEDULED_JOB_SCHEDULER, isOtaUpdate()); 356 return ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE; 357 } 358 } 359 360 @GuardedBy("this") unscheduleLocked()361 private void unscheduleLocked() { 362 if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { 363 throw new IllegalStateException("This job cannot be unscheduled"); 364 } 365 366 // This operation requires the uid to be "system" (1000). 367 long identityToken = Binder.clearCallingIdentity(); 368 try { 369 mInjector.getJobScheduler().cancel(JOB_ID); 370 } finally { 371 Binder.restoreCallingIdentity(identityToken); 372 } 373 } 374 375 /** 376 * The future returns true if the job is cancelled by the job scheduler. 377 * 378 * Can only be called when there is no running job. 379 */ 380 @GuardedBy("this") 381 @NonNull startLocked( @ullable Runnable onJobFinishedLocked, boolean isUpdateEngineReady)382 private CompletableFuture<Void> startLocked( 383 @Nullable Runnable onJobFinishedLocked, boolean isUpdateEngineReady) { 384 Utils.check(mRunningJob == null); 385 386 String otaSlot = mOtaSlot; 387 boolean mapSnapshotsForOta = mMapSnapshotsForOta; 388 var cancellationSignal = mCancellationSignal = new CancellationSignal(); 389 mIsUpdateEngineReady = isUpdateEngineReady; 390 mRunningJob = new CompletableFuture().runAsync(() -> { 391 markHasStarted(true); 392 PreRebootStatsReporter statsReporter = mInjector.getStatsReporter(); 393 try { 394 statsReporter.recordJobStarted(); 395 if (otaSlot != null && !isUpdateEngineReady && !mapSnapshotsForOta) { 396 triggerUpdateEnginePostinstallAndWait(); 397 synchronized (this) { 398 // This check is not strictly necessary, but is an optimization to return 399 // early. 400 if (mCancellationSignal.isCanceled()) { 401 // The stats reporter translates success=true to STATUS_CANCELLED. 402 statsReporter.recordJobEnded(new PreRebootResult(true /* success */)); 403 return; 404 } 405 Utils.check(mIsUpdateEngineReady); 406 } 407 } 408 PreRebootResult result = mInjector.getPreRebootDriver().run( 409 otaSlot, mapSnapshotsForOta, cancellationSignal); 410 statsReporter.recordJobEnded(result); 411 } catch (UpdateEngineException e) { 412 AsLog.e("update_engine error", e); 413 statsReporter.recordJobEnded(new PreRebootResult(false /* success */)); 414 } catch (RuntimeException e) { 415 statsReporter.recordJobEnded(new PreRebootResult(false /* success */)); 416 throw e; 417 } finally { 418 synchronized (this) { 419 if (onJobFinishedLocked != null) { 420 try { 421 onJobFinishedLocked.run(); 422 } catch (RuntimeException e) { 423 AsLog.wtf("Unexpected exception", e); 424 } 425 } 426 mRunningJob = null; 427 mCancellationSignal = null; 428 mIsUpdateEngineReady = false; 429 this.notifyAll(); 430 } 431 } 432 }, mExecutor); 433 this.notifyAll(); 434 return mRunningJob; 435 } 436 437 // The new API usage is safe because it's guarded by a flag. The "NewApi" lint is wrong because 438 // it's meaningless (b/380891026). We can't change the flag check to `isAtLeastB` because we use 439 // `SetFlagsRule` in tests to test the behavior with and without the API support. 440 @SuppressLint("NewApi") triggerUpdateEnginePostinstallAndWait()441 private void triggerUpdateEnginePostinstallAndWait() throws UpdateEngineException { 442 if (!android.os.Flags.updateEngineApi()) { 443 // Should never happen. 444 throw new UnsupportedOperationException(); 445 } 446 // When we need snapshot devices, we trigger update_engine postinstall. update_engine will 447 // map the snapshot devices for us and run the postinstall script, which will call 448 // `pm art on-ota-staged --start` to notify us that the snapshot device are ready. 449 // See art/libartservice/service/README.internal.md for typical flows. 450 AsLog.i("Waiting for update_engine to map snapshots..."); 451 try { 452 mInjector.getUpdateEngine().triggerPostinstall("system" /* partition */); 453 } catch (ServiceSpecificException e) { 454 throw new UpdateEngineException("Failed to trigger postinstall: " + e.getMessage()); 455 } 456 long startTime = System.currentTimeMillis(); 457 synchronized (this) { 458 while (true) { 459 if (mIsUpdateEngineReady || mCancellationSignal.isCanceled()) { 460 return; 461 } 462 long remainingTime = 463 UPDATE_ENGINE_TIMEOUT_MS - (System.currentTimeMillis() - startTime); 464 if (remainingTime <= 0) { 465 throw new UpdateEngineException("Timed out while waiting for update_engine"); 466 } 467 try { 468 this.wait(remainingTime); 469 } catch (InterruptedException e) { 470 AsLog.wtf("Interrupted", e); 471 } 472 } 473 } 474 } 475 476 @Nullable notifyUpdateEngineReady()477 public CompletableFuture<Void> notifyUpdateEngineReady() { 478 synchronized (this) { 479 if (mRunningJob == null) { 480 AsLog.e("No waiting job found"); 481 return null; 482 } 483 AsLog.i("update_engine finished mapping snapshots"); 484 mIsUpdateEngineReady = true; 485 // Notify triggerUpdateEnginePostinstallAndWait to stop waiting. 486 this.notifyAll(); 487 return mRunningJob; 488 } 489 } 490 491 /** 492 * Cancels the given job and waits for it to exit, if it's running. Temporarily releases the 493 * lock when waiting for the job to exit. 494 * 495 * When this method exits, it's guaranteed that the given job is not running, but another job 496 * might be running. 497 * 498 * @param expectInterrupt if true, this method returns immediately when the thread is 499 * interrupted, with no guarantee on the job state 500 */ 501 @GuardedBy("this") cancelGivenLocked(@onNull CompletableFuture<Void> job, boolean expectInterrupt)502 private void cancelGivenLocked(@NonNull CompletableFuture<Void> job, boolean expectInterrupt) { 503 while (mRunningJob == job) { 504 if (!mCancellationSignal.isCanceled()) { 505 mCancellationSignal.cancel(); 506 // This is not strictly necessary, but is an optimization to make 507 // `triggerUpdateEnginePostinstallAndWait` return early. 508 this.notifyAll(); 509 AsLog.i("Job cancelled"); 510 } 511 try { 512 this.wait(); 513 } catch (InterruptedException e) { 514 if (expectInterrupt) { 515 return; 516 } 517 AsLog.wtf("Interrupted", e); 518 } 519 } 520 } 521 522 /** 523 * Cancels any running job, prevents the pending job (if any) from being started by the job 524 * scheduler, and waits for the running job to exit. Temporarily releases the lock when waiting 525 * for the job to exit. 526 * 527 * When this method exits, it's guaranteed that no job is running. 528 */ 529 @GuardedBy("this") cancelAnyLocked()530 private void cancelAnyLocked() { 531 unscheduleLocked(); 532 while (mRunningJob != null) { 533 if (!mCancellationSignal.isCanceled()) { 534 mCancellationSignal.cancel(); 535 // This is not strictly necessary, but is an optimization to make 536 // `triggerUpdateEnginePostinstallAndWait` return early. 537 this.notifyAll(); 538 AsLog.i("Job cancelled"); 539 } 540 try { 541 this.wait(); 542 } catch (InterruptedException e) { 543 AsLog.wtf("Interrupted", e); 544 } 545 } 546 } 547 548 @GuardedBy("this") updateOtaSlotLocked(@ullable String value)549 private void updateOtaSlotLocked(@Nullable String value) { 550 Utils.check(value == null || value.equals("_a") || value.equals("_b")); 551 // It's not possible that this method is called with two different slots. 552 Utils.check(mOtaSlot == null || value == null || Objects.equals(mOtaSlot, value)); 553 // An OTA update has a higher priority than a Mainline update. When there are both a pending 554 // OTA update and a pending Mainline update, the system discards the Mainline update on the 555 // reboot. 556 if (mOtaSlot == null && value != null) { 557 mOtaSlot = value; 558 } 559 } 560 isEnabled()561 private boolean isEnabled() { 562 boolean syspropEnable = 563 SystemProperties.getBoolean("dalvik.vm.enable_pr_dexopt", false /* def */); 564 boolean deviceConfigEnable = mInjector.getDeviceConfigBoolean( 565 DeviceConfig.NAMESPACE_RUNTIME, "enable_pr_dexopt", false /* defaultValue */); 566 boolean deviceConfigForceDisable = 567 mInjector.getDeviceConfigBoolean(DeviceConfig.NAMESPACE_RUNTIME, 568 "force_disable_pr_dexopt", false /* defaultValue */); 569 if ((!syspropEnable && !deviceConfigEnable) || deviceConfigForceDisable) { 570 AsLog.i(String.format( 571 "Pre-reboot Dexopt Job is not enabled (sysprop:dalvik.vm.enable_pr_dexopt=%b, " 572 + "device_config:enable_pr_dexopt=%b, " 573 + "device_config:force_disable_pr_dexopt=%b)", 574 syspropEnable, deviceConfigEnable, deviceConfigForceDisable)); 575 return false; 576 } 577 // If `pm.dexopt.disable_bg_dexopt` is set, the user probably means to disable any dexopt 578 // jobs in the background. 579 if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) { 580 AsLog.i("Pre-reboot Dexopt Job is disabled by system property " 581 + "'pm.dexopt.disable_bg_dexopt'"); 582 return false; 583 } 584 return true; 585 } 586 isAsyncForOta()587 public boolean isAsyncForOta() { 588 if (android.os.Flags.updateEngineApi()) { 589 return true; 590 } 591 // Legacy flag in Android V. 592 return SystemProperties.getBoolean("dalvik.vm.pr_dexopt_async_for_ota", false /* def */); 593 } 594 595 @GuardedBy("this") resetLocked()596 private void resetLocked() { 597 mInjector.getStatsReporter().delete(); 598 if (hasStarted()) { 599 try { 600 mInjector.getArtd().cleanUpPreRebootStagedFiles(); 601 } catch (ServiceSpecificException | RemoteException e) { 602 AsLog.e("Failed to clean up obsolete Pre-reboot staged files", e); 603 } 604 markHasStarted(false); 605 } 606 } 607 608 /** 609 * Whether the job has started at least once, meaning the device is expected to have staged 610 * files, no matter it succeed, failed, or cancelled. 611 * 612 * This flag is survives across system server restarts, but not device reboots. 613 */ hasStarted()614 public boolean hasStarted() { 615 return SystemProperties.getBoolean("dalvik.vm.pre-reboot.has-started", false /* def */); 616 } 617 markHasStarted(boolean value)618 private void markHasStarted(boolean value) { 619 ArtJni.setProperty("dalvik.vm.pre-reboot.has-started", String.valueOf(value)); 620 } 621 622 @GuardedBy("this") isOtaUpdate()623 private boolean isOtaUpdate() { 624 return mOtaSlot != null; 625 } 626 627 private static class UpdateEngineException extends Exception { UpdateEngineException(@onNull String message)628 public UpdateEngineException(@NonNull String message) { 629 super(message); 630 } 631 } 632 633 /** 634 * Injector pattern for testing purpose. 635 * 636 * @hide 637 */ 638 @VisibleForTesting 639 public static class Injector { 640 @NonNull private final Context mContext; 641 @NonNull private final ArtManagerLocal mArtManagerLocal; 642 Injector(@onNull Context context, @NonNull ArtManagerLocal artManagerLocal)643 Injector(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal) { 644 mContext = context; 645 mArtManagerLocal = artManagerLocal; 646 } 647 648 @NonNull getJobScheduler()649 public JobScheduler getJobScheduler() { 650 return Objects.requireNonNull(mContext.getSystemService(JobScheduler.class)); 651 } 652 653 @NonNull getPreRebootDriver()654 public PreRebootDriver getPreRebootDriver() { 655 return new PreRebootDriver(mContext, mArtManagerLocal); 656 } 657 658 @NonNull getStatsReporter()659 public PreRebootStatsReporter getStatsReporter() { 660 return new PreRebootStatsReporter(); 661 } 662 663 @NonNull getArtd()664 public IArtd getArtd() { 665 return ArtdRefCache.getInstance().getArtd(); 666 } 667 668 // Wrap `DeviceConfig` to avoid mocking it directly in tests. `DeviceConfig` backs 669 // read-write Trunk Stable flags used by the framework. 670 @NonNull getDeviceConfigBoolean( @onNull String namespace, @NonNull String name, boolean defaultValue)671 public boolean getDeviceConfigBoolean( 672 @NonNull String namespace, @NonNull String name, boolean defaultValue) { 673 return DeviceConfig.getBoolean(namespace, name, defaultValue); 674 } 675 676 @NonNull getUpdateEngine()677 public UpdateEngine getUpdateEngine() { 678 return new UpdateEngine(); 679 } 680 } 681 } 682