summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libartservice/service/Android.bp5
-rw-r--r--libartservice/service/java/com/android/server/art/ArtManagerLocal.java5
-rw-r--r--libartservice/service/java/com/android/server/art/ArtShellCommand.java70
-rw-r--r--libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java154
-rw-r--r--libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java256
5 files changed, 486 insertions, 4 deletions
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index fe0c3af5e0..a9a0a1a206 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -82,6 +82,7 @@ java_defaults {
libs: [
"androidx.annotation_annotation",
"auto_value_annotations",
+ "sdk_module-lib_current_framework-configinfrastructure",
"sdk_module-lib_current_framework-permission-s",
// TODO(b/256866172): Transitive dependency, for r8 only.
"framework-statsd.stubs.module_lib",
@@ -235,6 +236,10 @@ android_test {
"art_module_source_build_java_defaults",
],
+ libs: [
+ "sdk_module-lib_current_framework-configinfrastructure",
+ ],
+
static_libs: [
"androidx.test.ext.junit",
"androidx.test.ext.truth",
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index afe2ab0538..727290f995 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -930,6 +930,11 @@ public final class ArtManagerLocal {
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void onApexStaged(@NonNull String[] stagedApexModuleNames) {
// TODO(b/311377497): Check system requirements.
+ mInjector.getPreRebootDexoptJob().unschedule();
+ // Although `unschedule` implies `cancel`, we explicitly call `cancel` here to wait for
+ // the job to exit, if it's running.
+ mInjector.getPreRebootDexoptJob().cancel(true /* blocking */);
+ mInjector.getPreRebootDexoptJob().updateOtaSlot(null);
mInjector.getPreRebootDexoptJob().schedule();
}
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 9203143abe..ff714ee1b8 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -190,6 +190,9 @@ public final class ArtShellCommand extends BasicShellCommandHandler {
// TODO(b/311377497): Remove this command once the development is done.
return handlePreRebootDexopt(pw);
}
+ case "on-ota-staged": {
+ return handleOnOtaStaged(pw);
+ }
default:
pw.printf("Error: Unknown 'art' sub-command '%s'\n", subcmd);
pw.println("See 'pm help' for help");
@@ -667,6 +670,67 @@ public final class ArtShellCommand extends BasicShellCommandHandler {
}
}
+ private int handleOnOtaStaged(@NonNull PrintWriter pw) {
+ if (!SdkLevel.isAtLeastV()) {
+ pw.println("Error: Unsupported command 'on-ota-staged'");
+ return 1;
+ }
+
+ int uid = Binder.getCallingUid();
+ if (uid != Process.ROOT_UID) {
+ throw new SecurityException("Only root can call 'on-ota-staged'");
+ }
+
+ String otaSlot = null;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "--slot":
+ otaSlot = getNextArgRequired();
+ break;
+ default:
+ pw.println("Error: Unknown option: " + opt);
+ return 1;
+ }
+ }
+
+ if (otaSlot == null) {
+ pw.println("Error: '--slot' must be specified");
+ return 1;
+ }
+
+ int code;
+
+ // This operation requires the uid to be "system" (1000).
+ long identityToken = Binder.clearCallingIdentity();
+ try {
+ mArtManagerLocal.getPreRebootDexoptJob().unschedule();
+ // Although `unschedule` implies `cancel`, we explicitly call `cancel` here to wait for
+ // the job to exit, if it's running.
+ mArtManagerLocal.getPreRebootDexoptJob().cancel(true /* blocking */);
+ mArtManagerLocal.getPreRebootDexoptJob().updateOtaSlot(otaSlot);
+ code = mArtManagerLocal.getPreRebootDexoptJob().schedule();
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ switch (code) {
+ case ArtFlags.SCHEDULE_SUCCESS:
+ pw.println("Job scheduled");
+ return 0;
+ case ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP:
+ pw.println("Job disabled by system property");
+ return 1;
+ case ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE:
+ pw.println("Failed to schedule job");
+ return 1;
+ default:
+ // Can't happen.
+ throw new IllegalStateException("Unknown result code: " + code);
+ }
+ }
+
@Override
public void onHelp() {
// No one should call this. The help text should be printed by the `onHelp` handler of `cmd
@@ -816,6 +880,12 @@ public final class ArtShellCommand extends BasicShellCommandHandler {
pw.println(" This command is different from 'pm compile -r REASON -a'. For example, it");
pw.println(" only dexopts a subset of apps, and it runs dexopt in parallel. See the");
pw.println(" API documentation for 'ArtManagerLocal.dexoptPackages' for details.");
+ pw.println();
+ pw.println(" on-ota-staged --slot SLOT");
+ pw.println(" Notifies ART Service that an OTA update is staged. A Pre-reboot Dexopt");
+ pw.println(" job is scheduled for it.");
+ pw.println(" Options:");
+ pw.println(" --slot SLOT The slot that contains the OTA update, '_a' or '_b'.");
}
private void enforceRootOrShell() {
diff --git a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java
index b8a55af7e0..08ff12c0b2 100644
--- a/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java
+++ b/libartservice/service/java/com/android/server/art/PreRebootDexoptJob.java
@@ -19,15 +19,29 @@ package com.android.server.art;
import static com.android.server.art.model.ArtFlags.ScheduleStatus;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.job.JobInfo;
import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
+import android.os.CancellationSignal;
+import android.os.SystemProperties;
+import android.provider.DeviceConfig;
+import android.util.Log;
+import android.util.Slog;
import androidx.annotation.RequiresApi;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.ArtServiceJobInterface;
+import com.android.server.art.prereboot.PreRebootDriver;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
/**
* The Pre-reboot Dexopt job.
@@ -50,6 +64,13 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface {
@NonNull private final Injector mInjector;
+ // Job state variables.
+ @GuardedBy("this") @Nullable private CompletableFuture<Void> mRunningJob = null;
+ @GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null;
+
+ /** The slot that contains the OTA update, "_a" or "_b", or null for a Mainline update. */
+ @GuardedBy("this") @Nullable private String mOtaSlot = null;
+
public PreRebootDexoptJob(@NonNull Context context) {
this(new Injector(context));
}
@@ -62,19 +83,134 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface {
@Override
public boolean onStartJob(
@NonNull BackgroundDexoptJobService jobService, @NonNull JobParameters params) {
+ // No need to handle exceptions thrown by the future because exceptions are handled inside
+ // the future itself.
+ var unused = start().thenRunAsync(() -> {
+ try {
+ // If it failed, it means something went wrong, so we don't reschedule the job
+ // because it will likely fail again. If it's cancelled, the job will be rescheduled
+ // because the return value of `onStopJob` will be respected, and this call will be
+ // ignored.
+ jobService.jobFinished(params, false /* wantsReschedule */);
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG, "Unexpected exception", e);
+ }
+ });
// "true" means the job will continue running until `jobFinished` is called.
- return false;
+ return true;
}
@Override
public boolean onStopJob(@NonNull JobParameters params) {
- // "true" means to execute again in the same interval with the default retry policy.
+ cancel(false /* blocking */);
+ // "true" means to execute again with the default retry policy.
return true;
}
public @ScheduleStatus int schedule() {
- // TODO(b/311377497): Schedule the job.
- return ArtFlags.SCHEDULE_SUCCESS;
+ if (this != BackgroundDexoptJobService.getJob(JOB_ID)) {
+ throw new IllegalStateException("This job cannot be scheduled");
+ }
+
+ if (!SystemProperties.getBoolean("dalvik.vm.enable_pr_dexopt", false /* def */)
+ && !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_RUNTIME, "enable_pr_dexopt",
+ false /* defaultValue */)) {
+ Log.i(TAG, "Pre-reboot Dexopt Job is not enabled by system property");
+ return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP;
+ }
+
+ // If `pm.dexopt.disable_bg_dexopt` is set, the user probably means to disable any dexopt
+ // jobs in the background.
+ if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) {
+ Log.i(TAG,
+ "Pre-reboot Dexopt Job is disabled by system property "
+ + "'pm.dexopt.disable_bg_dexopt'");
+ return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP;
+ }
+
+ JobInfo info = new JobInfo
+ .Builder(JOB_ID,
+ new ComponentName(JOB_PKG_NAME,
+ BackgroundDexoptJobService.class.getName()))
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setRequiresBatteryNotLow(true)
+ .build();
+
+ /* @JobScheduler.Result */ int result = mInjector.getJobScheduler().schedule(info);
+ if (result == JobScheduler.RESULT_SUCCESS) {
+ Log.i(TAG, "Pre-reboot Dexopt Job scheduled");
+ return ArtFlags.SCHEDULE_SUCCESS;
+ } else {
+ Log.i(TAG, "Failed to schedule Pre-reboot Dexopt Job");
+ return ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE;
+ }
+ }
+
+ public void unschedule() {
+ if (this != BackgroundDexoptJobService.getJob(JOB_ID)) {
+ throw new IllegalStateException("This job cannot be unscheduled");
+ }
+
+ mInjector.getJobScheduler().cancel(JOB_ID);
+ }
+
+ @NonNull
+ public synchronized CompletableFuture<Void> start() {
+ if (mRunningJob != null) {
+ Log.i(TAG, "Job is already running");
+ return mRunningJob;
+ }
+
+ String otaSlot = mOtaSlot;
+ var cancellationSignal = mCancellationSignal = new CancellationSignal();
+ mRunningJob = new CompletableFuture().runAsync(() -> {
+ try {
+ // TODO(b/336239721): Consume the result and report metrics.
+ mInjector.getPreRebootDriver().run(otaSlot, cancellationSignal);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Fatal error", e);
+ } finally {
+ synchronized (this) {
+ mRunningJob = null;
+ mCancellationSignal = null;
+ }
+ }
+ });
+ return mRunningJob;
+ }
+
+ /**
+ * Cancels the job.
+ *
+ * @param blocking whether to wait for the job to exit.
+ */
+ public void cancel(boolean blocking) {
+ CompletableFuture<Void> runningJob = null;
+ synchronized (this) {
+ if (mRunningJob == null) {
+ return;
+ }
+
+ mCancellationSignal.cancel();
+ Log.i(TAG, "Job cancelled");
+ runningJob = mRunningJob;
+ }
+ if (blocking) {
+ Utils.getFuture(runningJob);
+ }
+ }
+
+ public synchronized void updateOtaSlot(@NonNull String value) {
+ Utils.check(value == null || value.equals("_a") || value.equals("_b"));
+ // It's not possible that this method is called with two different slots.
+ Utils.check(mOtaSlot == null || value == null || Objects.equals(mOtaSlot, value));
+ // An OTA update has a higher priority than a Mainline update. When there are both a pending
+ // OTA update and a pending Mainline update, the system discards the Mainline update on the
+ // reboot.
+ if (mOtaSlot == null && value != null) {
+ mOtaSlot = value;
+ }
}
/**
@@ -89,5 +225,15 @@ public class PreRebootDexoptJob implements ArtServiceJobInterface {
Injector(@NonNull Context context) {
mContext = context;
}
+
+ @NonNull
+ public JobScheduler getJobScheduler() {
+ return Objects.requireNonNull(mContext.getSystemService(JobScheduler.class));
+ }
+
+ @NonNull
+ public PreRebootDriver getPreRebootDriver() {
+ return new PreRebootDriver(mContext);
+ }
}
}
diff --git a/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java b/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java
new file mode 100644
index 0000000000..bc30d2c058
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/PreRebootDexoptJobTest.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.art;
+
+import static com.android.server.art.PreRebootDexoptJob.JOB_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.isNull;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.os.CancellationSignal;
+import android.os.SystemProperties;
+import android.provider.DeviceConfig;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.prereboot.PreRebootDriver;
+import com.android.server.art.testing.StaticMockitoRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class PreRebootDexoptJobTest {
+ private static final long TIMEOUT_SEC = 10;
+
+ @Rule
+ public StaticMockitoRule mockitoRule = new StaticMockitoRule(
+ SystemProperties.class, BackgroundDexoptJobService.class, DeviceConfig.class);
+
+ @Mock private PreRebootDexoptJob.Injector mInjector;
+ @Mock private JobScheduler mJobScheduler;
+ @Mock private PreRebootDriver mPreRebootDriver;
+ private PreRebootDexoptJob mPreRebootDexoptJob;
+
+ @Before
+ public void setUp() throws Exception {
+ // By default, the job is enabled by a build-time flag.
+ lenient()
+ .when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean()))
+ .thenReturn(false);
+ lenient()
+ .when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean()))
+ .thenReturn(true);
+ lenient()
+ .when(DeviceConfig.getBoolean(
+ eq(DeviceConfig.NAMESPACE_RUNTIME), eq("enable_pr_dexopt"), anyBoolean()))
+ .thenReturn(false);
+
+ lenient().when(mInjector.getJobScheduler()).thenReturn(mJobScheduler);
+ lenient().when(mInjector.getPreRebootDriver()).thenReturn(mPreRebootDriver);
+
+ mPreRebootDexoptJob = new PreRebootDexoptJob(mInjector);
+ lenient().when(BackgroundDexoptJobService.getJob(JOB_ID)).thenReturn(mPreRebootDexoptJob);
+ }
+
+ @Test
+ public void testSchedule() throws Exception {
+ var captor = ArgumentCaptor.forClass(JobInfo.class);
+ when(mJobScheduler.schedule(captor.capture())).thenReturn(JobScheduler.RESULT_SUCCESS);
+
+ assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS);
+
+ JobInfo jobInfo = captor.getValue();
+ assertThat(jobInfo.isPeriodic()).isFalse();
+ assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+ assertThat(jobInfo.isRequireCharging()).isTrue();
+ assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+ }
+
+ @Test
+ public void testScheduleDisabled() {
+ when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean()))
+ .thenReturn(true);
+
+ assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP);
+
+ verify(mJobScheduler, never()).schedule(any());
+ }
+
+ @Test
+ public void testScheduleNotEnabled() {
+ when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean()))
+ .thenReturn(false);
+
+ assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP);
+
+ verify(mJobScheduler, never()).schedule(any());
+ }
+
+ @Test
+ public void testScheduleEnabledByPhenotypeFlag() {
+ lenient()
+ .when(SystemProperties.getBoolean(eq("dalvik.vm.enable_pr_dexopt"), anyBoolean()))
+ .thenReturn(false);
+ lenient()
+ .when(DeviceConfig.getBoolean(
+ eq(DeviceConfig.NAMESPACE_RUNTIME), eq("enable_pr_dexopt"), anyBoolean()))
+ .thenReturn(true);
+ when(mJobScheduler.schedule(any())).thenReturn(JobScheduler.RESULT_SUCCESS);
+
+ assertThat(mPreRebootDexoptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS);
+
+ verify(mJobScheduler).schedule(any());
+ }
+
+ @Test
+ public void testUnschedule() {
+ mPreRebootDexoptJob.unschedule();
+ verify(mJobScheduler).cancel(JOB_ID);
+ }
+
+ @Test
+ public void testStart() {
+ when(mPreRebootDriver.run(any(), any())).thenReturn(true);
+
+ Utils.getFuture(mPreRebootDexoptJob.start());
+ }
+
+ @Test
+ public void testStartAlreadyRunning() {
+ Semaphore dexoptDone = new Semaphore(0);
+ when(mPreRebootDriver.run(any(), any())).thenAnswer(invocation -> {
+ assertThat(dexoptDone.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
+ return true;
+ });
+
+ Future<Void> future1 = mPreRebootDexoptJob.start();
+ Future<Void> future2 = mPreRebootDexoptJob.start();
+ assertThat(future1).isSameInstanceAs(future2);
+
+ dexoptDone.release();
+ Utils.getFuture(future1);
+
+ verify(mPreRebootDriver, times(1)).run(any(), any());
+ }
+
+ @Test
+ public void testStartAnother() {
+ when(mPreRebootDriver.run(any(), any())).thenReturn(true);
+
+ Future<Void> future1 = mPreRebootDexoptJob.start();
+ Utils.getFuture(future1);
+ Future<Void> future2 = mPreRebootDexoptJob.start();
+ Utils.getFuture(future2);
+ assertThat(future1).isNotSameInstanceAs(future2);
+ }
+
+ @Test
+ public void testCancel() {
+ Semaphore dexoptCancelled = new Semaphore(0);
+ Semaphore jobExited = new Semaphore(0);
+ when(mPreRebootDriver.run(any(), any())).thenAnswer(invocation -> {
+ assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
+ var cancellationSignal = invocation.<CancellationSignal>getArgument(1);
+ assertThat(cancellationSignal.isCanceled()).isTrue();
+ jobExited.release();
+ return true;
+ });
+
+ var unused = mPreRebootDexoptJob.start();
+ Future<Void> future = new CompletableFuture().runAsync(
+ () -> { mPreRebootDexoptJob.cancel(true /* blocking */); });
+ dexoptCancelled.release();
+ Utils.getFuture(future);
+ // Check that `cancel` is really blocking.
+ assertThat(jobExited.tryAcquire()).isTrue();
+ }
+
+ @Test
+ public void testUpdateOtaSlotOtaThenMainline() {
+ mPreRebootDexoptJob.updateOtaSlot("_b");
+ mPreRebootDexoptJob.updateOtaSlot(null);
+
+ when(mPreRebootDriver.run(eq("_b"), any())).thenReturn(true);
+
+ Utils.getFuture(mPreRebootDexoptJob.start());
+ }
+
+ @Test
+ public void testUpdateOtaSlotMainlineThenOta() {
+ mPreRebootDexoptJob.updateOtaSlot(null);
+ mPreRebootDexoptJob.updateOtaSlot("_a");
+
+ when(mPreRebootDriver.run(eq("_a"), any())).thenReturn(true);
+
+ Utils.getFuture(mPreRebootDexoptJob.start());
+ }
+
+ @Test
+ public void testUpdateOtaSlotMainlineThenMainline() {
+ mPreRebootDexoptJob.updateOtaSlot(null);
+ mPreRebootDexoptJob.updateOtaSlot(null);
+
+ when(mPreRebootDriver.run(isNull(), any())).thenReturn(true);
+
+ Utils.getFuture(mPreRebootDexoptJob.start());
+ }
+
+ @Test
+ public void testUpdateOtaSlotOtaThenOta() {
+ mPreRebootDexoptJob.updateOtaSlot("_b");
+ mPreRebootDexoptJob.updateOtaSlot("_b");
+
+ when(mPreRebootDriver.run(eq("_b"), any())).thenReturn(true);
+
+ Utils.getFuture(mPreRebootDexoptJob.start());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testUpdateOtaSlotOtaThenOtaDifferentSlots() {
+ mPreRebootDexoptJob.updateOtaSlot("_b");
+ mPreRebootDexoptJob.updateOtaSlot("_a");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testUpdateOtaSlotOtaBogusSlot() {
+ mPreRebootDexoptJob.updateOtaSlot("_bogus");
+ }
+}