• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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