Merge "Fix ClassCastException from toArray()."
diff --git a/apct-tests/perftests/blobstore/Android.bp b/apct-tests/perftests/blobstore/Android.bp
new file mode 100644
index 0000000..be5072c
--- /dev/null
+++ b/apct-tests/perftests/blobstore/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2020 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.
+
+android_test {
+ name: "BlobStorePerfTests",
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "BlobStoreTestUtils",
+ "androidx.test.rules",
+ "androidx.annotation_annotation",
+ "apct-perftests-utils",
+ "ub-uiautomator",
+ ],
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ certificate: "platform",
+}
\ No newline at end of file
diff --git a/apct-tests/perftests/blobstore/AndroidManifest.xml b/apct-tests/perftests/blobstore/AndroidManifest.xml
new file mode 100644
index 0000000..21d0726
--- /dev/null
+++ b/apct-tests/perftests/blobstore/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.perftests.blob">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.perftests.blob"/>
+
+</manifest>
\ No newline at end of file
diff --git a/apct-tests/perftests/blobstore/AndroidTest.xml b/apct-tests/perftests/blobstore/AndroidTest.xml
new file mode 100644
index 0000000..19456c6
--- /dev/null
+++ b/apct-tests/perftests/blobstore/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<configuration description="Runs BlobStorePerfTests metric instrumentation.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-metric-instrumentation" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="BlobStorePerfTests.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.perftests.blob" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
\ No newline at end of file
diff --git a/apct-tests/perftests/blobstore/src/com/android/perftests/blob/AtraceUtils.java b/apct-tests/perftests/blobstore/src/com/android/perftests/blob/AtraceUtils.java
new file mode 100644
index 0000000..0208dab
--- /dev/null
+++ b/apct-tests/perftests/blobstore/src/com/android/perftests/blob/AtraceUtils.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2020 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.perftests.blob;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.ParcelFileDescriptor;
+import android.perftests.utils.TraceMarkParser;
+import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+// Copy of com.android.frameworks.perftests.am.util.AtraceUtils. TODO: avoid this duplication.
+public class AtraceUtils {
+ private static final String TAG = "AtraceUtils";
+ private static final boolean VERBOSE = true;
+
+ private static final String ATRACE_START = "atrace --async_start -b %d -c %s";
+ private static final String ATRACE_DUMP = "atrace --async_dump";
+ private static final String ATRACE_STOP = "atrace --async_stop";
+ private static final int DEFAULT_ATRACE_BUF_SIZE = 1024;
+
+ private UiAutomation mAutomation;
+ private static AtraceUtils sUtils = null;
+ private boolean mStarted = false;
+
+ private AtraceUtils(Instrumentation instrumentation) {
+ mAutomation = instrumentation.getUiAutomation();
+ }
+
+ public static AtraceUtils getInstance(Instrumentation instrumentation) {
+ if (sUtils == null) {
+ sUtils = new AtraceUtils(instrumentation);
+ }
+ return sUtils;
+ }
+
+ /**
+ * @param categories The list of the categories to trace, separated with space.
+ */
+ public void startTrace(String categories) {
+ synchronized (this) {
+ if (mStarted) {
+ throw new IllegalStateException("atrace already started");
+ }
+ runShellCommand(String.format(
+ ATRACE_START, DEFAULT_ATRACE_BUF_SIZE, categories));
+ mStarted = true;
+ }
+ }
+
+ public void stopTrace() {
+ synchronized (this) {
+ mStarted = false;
+ runShellCommand(ATRACE_STOP);
+ }
+ }
+
+ private String runShellCommand(String cmd) {
+ try {
+ return UiDevice.getInstance(
+ InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @param parser The function that can accept the buffer of atrace dump and parse it.
+ * @param handler The parse result handler
+ */
+ public void performDump(TraceMarkParser parser,
+ BiConsumer<String, List<TraceMarkSlice>> handler) {
+ parser.reset();
+ try {
+ if (VERBOSE) {
+ Log.i(TAG, "Collecting atrace dump...");
+ }
+ writeDataToBuf(mAutomation.executeShellCommand(ATRACE_DUMP), parser);
+ } catch (IOException e) {
+ Log.e(TAG, "Error in reading dump", e);
+ }
+ parser.forAllSlices(handler);
+ }
+
+ // The given file descriptor here will be closed by this function
+ private void writeDataToBuf(ParcelFileDescriptor pfDescriptor,
+ TraceMarkParser parser) throws IOException {
+ InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfDescriptor);
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ parser.visit(line);
+ }
+ }
+ }
+}
diff --git a/apct-tests/perftests/blobstore/src/com/android/perftests/blob/BlobStorePerfTests.java b/apct-tests/perftests/blobstore/src/com/android/perftests/blob/BlobStorePerfTests.java
new file mode 100644
index 0000000..a7b69c4
--- /dev/null
+++ b/apct-tests/perftests/blobstore/src/com/android/perftests/blob/BlobStorePerfTests.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2020 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.perftests.blob;
+
+import android.app.blob.BlobStoreManager;
+import android.content.Context;
+import android.perftests.utils.ManualBenchmarkState;
+import android.perftests.utils.PerfManualStatusReporter;
+import android.perftests.utils.TraceMarkParser;
+import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.utils.blob.DummyBlobData;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class BlobStorePerfTests {
+ // From frameworks/native/cmds/atrace/atrace.cpp
+ private static final String ATRACE_CATEGORY_SYSTEM_SERVER = "ss";
+ // From f/b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
+ private static final String ATRACE_COMPUTE_DIGEST_PREFIX = "computeBlobDigest-";
+
+ private Context mContext;
+ private BlobStoreManager mBlobStoreManager;
+ private AtraceUtils mAtraceUtils;
+ private ManualBenchmarkState mState;
+
+ @Rule
+ public PerfManualStatusReporter mPerfManualStatusReporter = new PerfManualStatusReporter();
+
+ @Parameterized.Parameter(0)
+ public int fileSizeInMb;
+
+ @Parameterized.Parameters(name = "{0}MB")
+ public static Collection<Object[]> getParameters() {
+ return Arrays.asList(new Object[][] {
+ { 25 },
+ { 50 },
+ { 100 },
+ { 200 },
+ });
+ }
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mBlobStoreManager = (BlobStoreManager) mContext.getSystemService(
+ Context.BLOB_STORE_SERVICE);
+ mAtraceUtils = AtraceUtils.getInstance(InstrumentationRegistry.getInstrumentation());
+ mState = mPerfManualStatusReporter.getBenchmarkState();
+ }
+
+ @After
+ public void tearDown() {
+ // TODO: Add a blob_store shell command to trigger idle maintenance to avoid hardcoding
+ // job id like this.
+ // From BlobStoreConfig.IDLE_JOB_ID = 191934935.
+ runShellCommand("cmd jobscheduler run -f android 191934935");
+ }
+
+ @Test
+ public void testComputeDigest() throws Exception {
+ mAtraceUtils.startTrace(ATRACE_CATEGORY_SYSTEM_SERVER);
+ try {
+ final List<Long> durations = new ArrayList<>();
+ final DummyBlobData blobData = prepareDataBlob(fileSizeInMb);
+ final TraceMarkParser parser = new TraceMarkParser(
+ line -> line.name.startsWith(ATRACE_COMPUTE_DIGEST_PREFIX));
+ while (mState.keepRunning(durations)) {
+ commitBlob(blobData);
+
+ durations.clear();
+ collectDigestDurationsFromTrace(parser, durations);
+ // get and delete blobId
+ }
+ } finally {
+ mAtraceUtils.stopTrace();
+ }
+ }
+
+ private void collectDigestDurationsFromTrace(TraceMarkParser parser, List<Long> durations) {
+ mAtraceUtils.performDump(parser, (key, slices) -> {
+ for (TraceMarkSlice slice : slices) {
+ durations.add(TimeUnit.MICROSECONDS.toNanos(slice.getDurationInMicroseconds()));
+ }
+ });
+ }
+
+ private DummyBlobData prepareDataBlob(int fileSizeInMb) throws Exception {
+ final DummyBlobData blobData = new DummyBlobData(mContext,
+ fileSizeInMb * 1024 * 1024 /* bytes */);
+ blobData.prepare();
+ return blobData;
+ }
+
+ private void commitBlob(DummyBlobData blobData) throws Exception {
+ final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
+ try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
+ blobData.writeToSession(session);
+ final CompletableFuture<Integer> callback = new CompletableFuture<>();
+ session.commit(mContext.getMainExecutor(), callback::complete);
+ // Ignore commit callback result.
+ callback.get();
+ }
+ }
+
+ private String runShellCommand(String cmd) {
+ try {
+ return UiDevice.getInstance(
+ InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/apex/blobstore/TEST_MAPPING b/apex/blobstore/TEST_MAPPING
index cfe19a5..25a1537 100644
--- a/apex/blobstore/TEST_MAPPING
+++ b/apex/blobstore/TEST_MAPPING
@@ -4,7 +4,7 @@
"name": "CtsBlobStoreTestCases"
},
{
- "name": "FrameworksServicesTests",
+ "name": "FrameworksMockingServicesTests",
"options": [
{
"include-filter": "com.android.server.blob"
diff --git a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
index f110b36..d339afa 100644
--- a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
+++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java
@@ -257,6 +257,11 @@
return Base64.encodeToString(digest, Base64.NO_WRAP);
}
+ /** @hide */
+ public boolean isExpired() {
+ return expiryTimeMillis != 0 && expiryTimeMillis < System.currentTimeMillis();
+ }
+
public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() {
@Override
public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) {
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
index aba3e8c..c12e0ec 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -64,9 +64,9 @@
private final Context mContext;
- public final long blobId;
- public final BlobHandle blobHandle;
- public final int userId;
+ private final long mBlobId;
+ private final BlobHandle mBlobHandle;
+ private final int mUserId;
@GuardedBy("mMetadataLock")
private final ArraySet<Committer> mCommitters = new ArraySet<>();
@@ -90,9 +90,21 @@
BlobMetadata(Context context, long blobId, BlobHandle blobHandle, int userId) {
mContext = context;
- this.blobId = blobId;
- this.blobHandle = blobHandle;
- this.userId = userId;
+ this.mBlobId = blobId;
+ this.mBlobHandle = blobHandle;
+ this.mUserId = userId;
+ }
+
+ long getBlobId() {
+ return mBlobId;
+ }
+
+ BlobHandle getBlobHandle() {
+ return mBlobHandle;
+ }
+
+ int getUserId() {
+ return mUserId;
}
void addCommitter(@NonNull Committer committer) {
@@ -159,7 +171,7 @@
boolean hasLeases() {
synchronized (mMetadataLock) {
- return mLeasees.isEmpty();
+ return !mLeasees.isEmpty();
}
}
@@ -196,7 +208,7 @@
File getBlobFile() {
if (mBlobFile == null) {
- mBlobFile = BlobStoreConfig.getBlobFile(blobId);
+ mBlobFile = BlobStoreConfig.getBlobFile(mBlobId);
}
return mBlobFile;
}
@@ -244,7 +256,7 @@
void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) {
fout.println("blobHandle:");
fout.increaseIndent();
- blobHandle.dump(fout, dumpArgs.shouldDumpFull());
+ mBlobHandle.dump(fout, dumpArgs.shouldDumpFull());
fout.decreaseIndent();
fout.println("Committers:");
@@ -274,11 +286,11 @@
void writeToXml(XmlSerializer out) throws IOException {
synchronized (mMetadataLock) {
- XmlUtils.writeLongAttribute(out, ATTR_ID, blobId);
- XmlUtils.writeIntAttribute(out, ATTR_USER_ID, userId);
+ XmlUtils.writeLongAttribute(out, ATTR_ID, mBlobId);
+ XmlUtils.writeIntAttribute(out, ATTR_USER_ID, mUserId);
out.startTag(null, TAG_BLOB_HANDLE);
- blobHandle.writeToXml(out);
+ mBlobHandle.writeToXml(out);
out.endTag(null, TAG_BLOB_HANDLE);
for (int i = 0, count = mCommitters.size(); i < count; ++i) {
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
index eb414b0..ba2e559 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
@@ -18,12 +18,15 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Environment;
+import android.util.Log;
import android.util.Slog;
import java.io.File;
+import java.util.concurrent.TimeUnit;
class BlobStoreConfig {
public static final String TAG = "BlobStore";
+ public static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
public static final int CURRENT_XML_VERSION = 1;
@@ -32,6 +35,20 @@
private static final String SESSIONS_INDEX_FILE_NAME = "sessions_index.xml";
private static final String BLOBS_INDEX_FILE_NAME = "blobs_index.xml";
+ /**
+ * Job Id for idle maintenance job ({@link BlobStoreIdleJobService}).
+ */
+ public static final int IDLE_JOB_ID = 0xB70B1D7; // 191934935L
+ /**
+ * Max time period (in millis) between each idle maintenance job run.
+ */
+ public static final long IDLE_JOB_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
+
+ /**
+ * Timeout in millis after which sessions with no updates will be deleted.
+ */
+ public static final long SESSION_EXPIRY_TIMEOUT_MILLIS = TimeUnit.DAYS.toMillis(7);
+
@Nullable
public static File prepareBlobFile(long sessionId) {
final File blobsDir = prepareBlobsDir();
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
new file mode 100644
index 0000000..460e776
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 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.blob;
+
+import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_ID;
+import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_PERIOD_MILLIS;
+import static com.android.server.blob.BlobStoreConfig.LOGV;
+import static com.android.server.blob.BlobStoreConfig.TAG;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+
+/**
+ * Maintenance job to clean up stale sessions and blobs.
+ */
+public class BlobStoreIdleJobService extends JobService {
+ @Override
+ public boolean onStartJob(final JobParameters params) {
+ AsyncTask.execute(() -> {
+ final BlobStoreManagerInternal blobStoreManagerInternal = LocalServices.getService(
+ BlobStoreManagerInternal.class);
+ blobStoreManagerInternal.onIdleMaintenance();
+ jobFinished(params, false);
+ });
+ return false;
+ }
+
+ @Override
+ public boolean onStopJob(final JobParameters params) {
+ Slog.d(TAG, "Idle maintenance job is stopped; id=" + params.getJobId()
+ + ", reason=" + JobParameters.getReasonCodeDescription(params.getStopReason()));
+ return false;
+ }
+
+ static void schedule(Context context) {
+ final JobScheduler jobScheduler = (JobScheduler) context.getSystemService(
+ Context.JOB_SCHEDULER_SERVICE);
+ final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID,
+ new ComponentName(context, BlobStoreIdleJobService.class))
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setPeriodic(IDLE_JOB_PERIOD_MILLIS)
+ .build();
+ jobScheduler.schedule(job);
+ if (LOGV) {
+ Slog.v(TAG, "Scheduling the idle maintenance job");
+ }
+ }
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java
new file mode 100644
index 0000000..5358245
--- /dev/null
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerInternal.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 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.blob;
+
+/**
+ * BlobStoreManager local system service interface.
+ *
+ * Only for use within the system server.
+ */
+public abstract class BlobStoreManagerInternal {
+ /**
+ * Triggered from idle maintenance job to cleanup stale blobs and sessions.
+ */
+ public abstract void onIdleMaintenance();
+}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index 13f095e..0ba34ca 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -28,6 +28,8 @@
import static android.os.UserHandle.USER_NULL;
import static com.android.server.blob.BlobStoreConfig.CURRENT_XML_VERSION;
+import static com.android.server.blob.BlobStoreConfig.LOGV;
+import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
import static com.android.server.blob.BlobStoreConfig.TAG;
import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED;
import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
@@ -61,6 +63,7 @@
import android.os.UserHandle;
import android.os.UserManagerInternal;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.ExceptionUtils;
import android.util.LongSparseArray;
@@ -94,8 +97,10 @@
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
* Service responsible for maintaining and facilitating access to data blobs published by apps.
@@ -115,6 +120,10 @@
@GuardedBy("mBlobsLock")
private final SparseArray<ArrayMap<BlobHandle, BlobMetadata>> mBlobsMap = new SparseArray<>();
+ // Contains all ids that are currently in use.
+ @GuardedBy("mBlobsLock")
+ private final ArraySet<Long> mKnownBlobIds = new ArraySet<>();
+
private final Context mContext;
private final Handler mHandler;
private final Injector mInjector;
@@ -151,6 +160,7 @@
@Override
public void onStart() {
publishBinderService(Context.BLOB_STORE_SERVICE, new Stub());
+ LocalServices.addService(BlobStoreManagerInternal.class, new LocalService());
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
registerReceivers();
@@ -164,6 +174,8 @@
readBlobSessionsLocked(allPackages);
readBlobsInfoLocked(allPackages);
}
+ } else if (phase == PHASE_BOOT_COMPLETED) {
+ BlobStoreIdleJobService.schedule(mContext);
}
}
@@ -215,6 +227,40 @@
}
}
+ @VisibleForTesting
+ void addKnownIdsForTest(long... knownIds) {
+ synchronized (mBlobsLock) {
+ for (long id : knownIds) {
+ mKnownBlobIds.add(id);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ Set<Long> getKnownIdsForTest() {
+ synchronized (mBlobsLock) {
+ return mKnownBlobIds;
+ }
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void addSessionForUserLocked(BlobStoreSession session, int userId) {
+ getUserSessionsLocked(userId).put(session.getSessionId(), session);
+ mKnownBlobIds.add(session.getSessionId());
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void addBlobForUserLocked(BlobMetadata blobMetadata, int userId) {
+ addBlobForUserLocked(blobMetadata, getUserBlobsLocked(userId));
+ }
+
+ @GuardedBy("mBlobsLock")
+ private void addBlobForUserLocked(BlobMetadata blobMetadata,
+ ArrayMap<BlobHandle, BlobMetadata> userBlobs) {
+ userBlobs.put(blobMetadata.getBlobHandle(), blobMetadata);
+ mKnownBlobIds.add(blobMetadata.getBlobId());
+ }
+
private long createSessionInternal(BlobHandle blobHandle,
int callingUid, String callingPackage) {
synchronized (mBlobsLock) {
@@ -223,7 +269,11 @@
final BlobStoreSession session = new BlobStoreSession(mContext,
sessionId, blobHandle, callingUid, callingPackage,
mSessionStateChangeListener);
- getUserSessionsLocked(UserHandle.getUserId(callingUid)).put(sessionId, session);
+ addSessionForUserLocked(session, UserHandle.getUserId(callingUid));
+ if (LOGV) {
+ Slog.v(TAG, "Created session for " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
writeBlobSessionsAsync();
return sessionId;
}
@@ -251,7 +301,10 @@
callingUid, callingPackage);
session.open();
session.abandon();
-
+ if (LOGV) {
+ Slog.v(TAG, "Deleted session with id " + sessionId
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
writeBlobSessionsAsync();
}
}
@@ -286,6 +339,10 @@
}
blobMetadata.addLeasee(callingPackage, callingUid,
descriptionResId, leaseExpiryTimeMillis);
+ if (LOGV) {
+ Slog.v(TAG, "Acquired lease on " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
writeBlobsInfoAsync();
}
}
@@ -301,6 +358,10 @@
+ "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
}
blobMetadata.removeLeasee(callingPackage, callingUid);
+ if (LOGV) {
+ Slog.v(TAG, "Released lease on " + blobHandle
+ + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage);
+ }
writeBlobsInfoAsync();
}
}
@@ -329,6 +390,10 @@
session.getSessionFile().delete();
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
.remove(session.getSessionId());
+ mKnownBlobIds.remove(session.getSessionId());
+ if (LOGV) {
+ Slog.v(TAG, "Session is invalid; deleted " + session);
+ }
break;
case STATE_COMMITTED:
session.verifyBlobData();
@@ -340,7 +405,7 @@
if (blob == null) {
blob = new BlobMetadata(mContext,
session.getSessionId(), session.getBlobHandle(), userId);
- userBlobs.put(session.getBlobHandle(), blob);
+ addBlobForUserLocked(blob, userBlobs);
}
final Committer newCommitter = new Committer(session.getOwnerPackageName(),
session.getOwnerUid(), session.getBlobAccessMode());
@@ -355,6 +420,9 @@
}
getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid()))
.remove(session.getSessionId());
+ if (LOGV) {
+ Slog.v(TAG, "Successfully committed session " + session);
+ }
break;
default:
Slog.wtf(TAG, "Invalid session state: "
@@ -397,6 +465,9 @@
out.endTag(null, TAG_SESSIONS);
out.endDocument();
sessionsIndexFile.finishWrite(fos);
+ if (LOGV) {
+ Slog.v(TAG, "Finished persisting sessions data");
+ }
} catch (Exception e) {
sessionsIndexFile.failWrite(fos);
Slog.wtf(TAG, "Error writing sessions data", e);
@@ -437,8 +508,8 @@
if (userPackages != null
&& session.getOwnerPackageName().equals(
userPackages.get(session.getOwnerUid()))) {
- getUserSessionsLocked(UserHandle.getUserId(session.getOwnerUid())).put(
- session.getSessionId(), session);
+ addSessionForUserLocked(session,
+ UserHandle.getUserId(session.getOwnerUid()));
} else {
// Unknown package or the session data does not belong to this package.
session.getSessionFile().delete();
@@ -446,6 +517,9 @@
mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, session.getSessionId());
}
}
+ if (LOGV) {
+ Slog.v(TAG, "Finished reading sessions data");
+ }
} catch (Exception e) {
Slog.wtf(TAG, "Error reading sessions data", e);
}
@@ -479,6 +553,9 @@
out.endTag(null, TAG_BLOBS);
out.endDocument();
blobsIndexFile.finishWrite(fos);
+ if (LOGV) {
+ Slog.v(TAG, "Finished persisting blobs data");
+ }
} catch (Exception e) {
blobsIndexFile.failWrite(fos);
Slog.wtf(TAG, "Error writing blobs data", e);
@@ -510,18 +587,21 @@
if (TAG_BLOB.equals(in.getName())) {
final BlobMetadata blobMetadata = BlobMetadata.createFromXml(mContext, in);
- final SparseArray<String> userPackages = allPackages.get(blobMetadata.userId);
+ final SparseArray<String> userPackages = allPackages.get(
+ blobMetadata.getUserId());
if (userPackages == null) {
blobMetadata.getBlobFile().delete();
} else {
- getUserBlobsLocked(blobMetadata.userId).put(
- blobMetadata.blobHandle, blobMetadata);
+ addBlobForUserLocked(blobMetadata, blobMetadata.getUserId());
blobMetadata.removeInvalidCommitters(userPackages);
blobMetadata.removeInvalidLeasees(userPackages);
}
- mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.blobId);
+ mCurrentMaxSessionId = Math.max(mCurrentMaxSessionId, blobMetadata.getBlobId());
}
}
+ if (LOGV) {
+ Slog.v(TAG, "Finished reading blobs data");
+ }
} catch (Exception e) {
Slog.wtf(TAG, "Error reading blobs data", e);
}
@@ -614,6 +694,7 @@
if (session.getOwnerUid() == uid
&& session.getOwnerPackageName().equals(packageName)) {
session.getSessionFile().delete();
+ mKnownBlobIds.remove(session.getSessionId());
indicesToRemove.add(i);
}
}
@@ -633,6 +714,7 @@
// Delete the blob if it doesn't have any active leases.
if (!blobMetadata.hasLeases()) {
blobMetadata.getBlobFile().delete();
+ mKnownBlobIds.remove(blobMetadata.getBlobId());
indicesToRemove.add(i);
}
}
@@ -640,6 +722,10 @@
userBlobs.removeAt(indicesToRemove.get(i));
}
writeBlobsInfoAsync();
+ if (LOGV) {
+ Slog.v(TAG, "Removed blobs data associated with pkg="
+ + packageName + ", uid=" + uid);
+ }
}
}
@@ -651,6 +737,7 @@
for (int i = 0, count = userSessions.size(); i < count; ++i) {
final BlobStoreSession session = userSessions.valueAt(i);
session.getSessionFile().delete();
+ mKnownBlobIds.remove(session.getSessionId());
}
}
@@ -660,11 +747,107 @@
for (int i = 0, count = userBlobs.size(); i < count; ++i) {
final BlobMetadata blobMetadata = userBlobs.valueAt(i);
blobMetadata.getBlobFile().delete();
+ mKnownBlobIds.remove(blobMetadata.getBlobId());
}
}
+ if (LOGV) {
+ Slog.v(TAG, "Removed blobs data in user " + userId);
+ }
}
}
+ @GuardedBy("mBlobsLock")
+ @VisibleForTesting
+ void handleIdleMaintenanceLocked() {
+ // Cleanup any left over data on disk that is not part of index.
+ final ArrayList<Long> deletedBlobIds = new ArrayList<>();
+ final ArrayList<File> filesToDelete = new ArrayList<>();
+ final File blobsDir = BlobStoreConfig.getBlobsDir();
+ if (blobsDir.exists()) {
+ for (File file : blobsDir.listFiles()) {
+ try {
+ final long id = Long.parseLong(file.getName());
+ if (mKnownBlobIds.indexOf(id) < 0) {
+ filesToDelete.add(file);
+ deletedBlobIds.add(id);
+ }
+ } catch (NumberFormatException e) {
+ Slog.wtf(TAG, "Error parsing the file name: " + file, e);
+ filesToDelete.add(file);
+ }
+ }
+ for (int i = 0, count = filesToDelete.size(); i < count; ++i) {
+ filesToDelete.get(i).delete();
+ }
+ }
+
+ // Cleanup any stale blobs.
+ for (int i = 0, userCount = mBlobsMap.size(); i < userCount; ++i) {
+ final ArrayMap<BlobHandle, BlobMetadata> userBlobs = mBlobsMap.valueAt(i);
+ userBlobs.entrySet().removeIf(entry -> {
+ final BlobHandle blobHandle = entry.getKey();
+ final BlobMetadata blobMetadata = entry.getValue();
+ boolean shouldRemove = false;
+
+ // Cleanup expired data blobs.
+ if (blobHandle.isExpired()) {
+ shouldRemove = true;
+ }
+
+ // Cleanup blobs with no active leases.
+ // TODO: Exclude blobs which were just committed.
+ if (!blobMetadata.hasLeases()) {
+ shouldRemove = true;
+ }
+
+ if (shouldRemove) {
+ blobMetadata.getBlobFile().delete();
+ mKnownBlobIds.remove(blobMetadata.getBlobId());
+ deletedBlobIds.add(blobMetadata.getBlobId());
+ }
+ return shouldRemove;
+ });
+ }
+ writeBlobsInfoAsync();
+
+ // Cleanup any stale sessions.
+ final ArrayList<Integer> indicesToRemove = new ArrayList<>();
+ for (int i = 0, userCount = mSessions.size(); i < userCount; ++i) {
+ final LongSparseArray<BlobStoreSession> userSessions = mSessions.valueAt(i);
+ indicesToRemove.clear();
+ for (int j = 0, sessionsCount = userSessions.size(); j < sessionsCount; ++j) {
+ final BlobStoreSession blobStoreSession = userSessions.valueAt(j);
+ boolean shouldRemove = false;
+
+ // Cleanup sessions which haven't been modified in a while.
+ if (blobStoreSession.getSessionFile().lastModified()
+ < System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS) {
+ shouldRemove = true;
+ }
+
+ // Cleanup sessions with already expired data.
+ if (blobStoreSession.getBlobHandle().isExpired()) {
+ shouldRemove = true;
+ }
+
+ if (shouldRemove) {
+ blobStoreSession.getSessionFile().delete();
+ mKnownBlobIds.remove(blobStoreSession.getSessionId());
+ indicesToRemove.add(j);
+ deletedBlobIds.add(blobStoreSession.getSessionId());
+ }
+ }
+ for (int j = 0; j < indicesToRemove.size(); ++j) {
+ userSessions.removeAt(indicesToRemove.get(j));
+ }
+ }
+ if (LOGV) {
+ Slog.v(TAG, "Completed idle maintenance; deleted "
+ + Arrays.toString(deletedBlobIds.toArray()));
+ }
+ writeBlobSessionsAsync();
+ }
+
void runClearAllSessions(@UserIdInt int userId) {
synchronized (mBlobsLock) {
if (userId == UserHandle.USER_ALL) {
@@ -727,10 +910,10 @@
fout.increaseIndent();
for (int j = 0, blobsCount = userBlobs.size(); j < blobsCount; ++j) {
final BlobMetadata blobMetadata = userBlobs.valueAt(j);
- if (!dumpArgs.shouldDumpBlob(blobMetadata.blobId)) {
+ if (!dumpArgs.shouldDumpBlob(blobMetadata.getBlobId())) {
continue;
}
- fout.println("Blob #" + blobMetadata.blobId);
+ fout.println("Blob #" + blobMetadata.getBlobId());
fout.increaseIndent();
blobMetadata.dump(fout, dumpArgs);
fout.decreaseIndent();
@@ -742,6 +925,9 @@
private class PackageChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
+ if (LOGV) {
+ Slog.v(TAG, "Received " + intent);
+ }
switch (intent.getAction()) {
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
case Intent.ACTION_PACKAGE_DATA_CLEARED:
@@ -893,6 +1079,14 @@
final DumpArgs dumpArgs = DumpArgs.parse(args);
final IndentingPrintWriter fout = new IndentingPrintWriter(writer, " ");
+ if (dumpArgs.shouldDumpHelp()) {
+ writer.println("dumpsys blob_store [options]:");
+ fout.increaseIndent();
+ dumpArgs.dumpArgsUsage(fout);
+ fout.decreaseIndent();
+ return;
+ }
+
synchronized (mBlobsLock) {
fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId);
fout.println();
@@ -926,6 +1120,7 @@
private boolean mDumpOnlySelectedSections;
private boolean mDumpSessions;
private boolean mDumpBlobs;
+ private boolean mDumpHelp;
public boolean shouldDumpSession(String packageName, int uid, long blobId) {
if (!CollectionUtils.isEmpty(mDumpPackages)
@@ -971,6 +1166,10 @@
|| mDumpUserIds.indexOf(userId) >= 0;
}
+ public boolean shouldDumpHelp() {
+ return mDumpHelp;
+ }
+
private DumpArgs() {}
public static DumpArgs parse(String[] args) {
@@ -1000,6 +1199,8 @@
dumpArgs.mDumpUserIds.add(getIntArgRequired(args, ++i, "userId"));
} else if ("--blob".equals(opt) || "-b".equals(opt)) {
dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, ++i, "blobId"));
+ } else if ("--help".equals(opt) || "-h".equals(opt)) {
+ dumpArgs.mDumpHelp = true;
} else {
// Everything else is assumed to be blob ids.
dumpArgs.mDumpBlobIds.add(getLongArgRequired(args, i, "blobId"));
@@ -1040,6 +1241,40 @@
}
return value;
}
+
+ private void dumpArgsUsage(IndentingPrintWriter pw) {
+ pw.println("--help | -h");
+ printWithIndent(pw, "Dump this help text");
+ pw.println("--sessions");
+ printWithIndent(pw, "Dump only the sessions info");
+ pw.println("--blobs");
+ printWithIndent(pw, "Dump only the committed blobs info");
+ pw.println("--package | -p [package-name]");
+ printWithIndent(pw, "Dump blobs info associated with the given package");
+ pw.println("--uid | -u [uid]");
+ printWithIndent(pw, "Dump blobs info associated with the given uid");
+ pw.println("--user [user-id]");
+ printWithIndent(pw, "Dump blobs info in the given user");
+ pw.println("--blob | -b [session-id | blob-id]");
+ printWithIndent(pw, "Dump blob info corresponding to the given ID");
+ pw.println("--full | -f");
+ printWithIndent(pw, "Dump full unredacted blobs data");
+ }
+
+ private void printWithIndent(IndentingPrintWriter pw, String str) {
+ pw.increaseIndent();
+ pw.println(str);
+ pw.decreaseIndent();
+ }
+ }
+
+ private class LocalService extends BlobStoreManagerInternal {
+ @Override
+ public void onIdleMaintenance() {
+ synchronized (mBlobsLock) {
+ handleIdleMaintenanceLocked();
+ }
+ }
}
@VisibleForTesting
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
index 54a2997..bd35b86 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
@@ -21,11 +21,13 @@
import static android.app.blob.XmlTags.ATTR_UID;
import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
+import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
import static android.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_RDWR;
import static android.system.OsConstants.SEEK_SET;
+import static com.android.server.blob.BlobStoreConfig.LOGV;
import static com.android.server.blob.BlobStoreConfig.TAG;
import android.annotation.BytesLong;
@@ -40,6 +42,7 @@
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.RevocableFileDescriptor;
+import android.os.Trace;
import android.os.storage.StorageManager;
import android.system.ErrnoException;
import android.system.Os;
@@ -381,15 +384,22 @@
void verifyBlobData() {
byte[] actualDigest = null;
try {
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER,
+ "computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length());
actualDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm);
} catch (IOException | NoSuchAlgorithmException e) {
Slog.e(TAG, "Error computing the digest", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
}
synchronized (mSessionLock) {
if (actualDigest != null && Arrays.equals(actualDigest, mBlobHandle.digest)) {
mState = STATE_VERIFIED_VALID;
// Commit callback will be sent once the data is persisted.
} else {
+ if (LOGV) {
+ Slog.v(TAG, "Digest of the data didn't match the given BlobHandle.digest");
+ }
mState = STATE_VERIFIED_INVALID;
sendCommitCallbackResult(COMMIT_RESULT_ERROR);
}
@@ -447,6 +457,16 @@
}
}
+ @Override
+ public String toString() {
+ return "BlobStoreSession {"
+ + "id:" + mSessionId
+ + ",handle:" + mBlobHandle
+ + ",uid:" + mOwnerUid
+ + ",pkg:" + mOwnerPackageName
+ + "}";
+ }
+
private void assertCallerIsOwner() {
final int callingUid = Binder.getCallingUid();
if (callingUid != mOwnerUid) {
diff --git a/api/current.txt b/api/current.txt
index 02fac79..81c4c92 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -35687,6 +35687,7 @@
field public static final String INCREMENTAL;
field public static final int PREVIEW_SDK_INT;
field public static final String RELEASE;
+ field @NonNull public static final String RELEASE_OR_CODENAME;
field @Deprecated public static final String SDK;
field public static final int SDK_INT;
field public static final String SECURITY_PATCH;
diff --git a/cmds/statsd/Android.bp b/cmds/statsd/Android.bp
index b31fe62..3a2472e 100644
--- a/cmds/statsd/Android.bp
+++ b/cmds/statsd/Android.bp
@@ -124,7 +124,6 @@
"libprotoutil",
"libstatslog",
"libstatsmetadata",
- "libstatssocket",
"libsysutils",
],
shared_libs: [
@@ -133,6 +132,7 @@
"libincident",
"liblog",
"libservices",
+ "libstatssocket",
"libutils",
],
}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index a291734..70b2db7 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -240,6 +240,13 @@
public static final String RELEASE = getString("ro.build.version.release");
/**
+ * The version string we show to the user; may be {@link #RELEASE} or
+ * {@link #CODENAME} if not a final release build.
+ */
+ @NonNull public static final String RELEASE_OR_CODENAME = getString(
+ "ro.build.version.release_or_codename");
+
+ /**
* The base OS build the product is based on.
*/
public static final String BASE_OS = SystemProperties.get("ro.build.version.base_os", "");
diff --git a/core/java/android/os/incremental/IncrementalManager.java b/core/java/android/os/incremental/IncrementalManager.java
index 8990bdf..35fa37a 100644
--- a/core/java/android/os/incremental/IncrementalManager.java
+++ b/core/java/android/os/incremental/IncrementalManager.java
@@ -23,8 +23,6 @@
import android.content.Context;
import android.content.pm.DataLoaderParams;
import android.os.RemoteException;
-import android.system.ErrnoException;
-import android.system.Os;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
@@ -193,116 +191,54 @@
}
/**
- * Renames an Incremental path to a new path. If source path is a file, make a link from the old
- * Incremental file to the new one. If source path is a dir, unbind old dir from Incremental
- * Storage and bind the new one.
- * <ol>
- * <li> For renaming a dir, dest dir will be created if not exists, and does not need to
- * be on the same Incremental storage as the source. </li>
- * <li> For renaming a file, dest file must be on the same Incremental storage as source.
- * </li>
- * </ol>
+ * Set up an app's code path. The expected outcome of this method is:
+ * 1) The actual apk directory under /data/incremental is bind-mounted to the parent directory
+ * of {@code afterCodeFile}.
+ * 2) All the files under {@code beforeCodeFile} will show up under {@code afterCodeFile}.
*
- * @param sourcePath Absolute path to the source. Should be the same type as the destPath (file
- * or dir). Expected to already exist and is an Incremental path.
- * @param destPath Absolute path to the destination.
- * @throws IllegalArgumentException when 1) source does not exist, or 2) source and dest type
- * mismatch (one is file and the other is dir), or 3) source
- * path is not on Incremental File System,
- * @throws IOException when 1) cannot find the root path of the Incremental storage
- * of source, or 2) cannot retrieve the Incremental storage
- * instance of the source, or 3) renaming a file, but dest is
- * not on the same Incremental Storage, or 4) renaming a dir,
- * dest dir does not exist but fails to be created.
- * <p>
- * TODO(b/136132412): add unit tests
+ * @param beforeCodeFile Path that is currently bind-mounted and have APKs under it.
+ * Should no longer have any APKs after this method is called.
+ * Example: /data/app/vmdl*tmp
+ * @param afterCodeFile Path that should will have APKs after this method is called. Its parent
+ * directory should be bind-mounted to a directory under /data/incremental.
+ * Example: /data/app/~~[randomStringA]/[packageName]-[randomStringB]
+ * @throws IllegalArgumentException
+ * @throws IOException
+ * TODO(b/147371381): add unit tests
*/
- public void rename(@NonNull String sourcePath, @NonNull String destPath) throws IOException {
- final File source = new File(sourcePath);
- final File dest = new File(destPath);
- if (!source.exists()) {
- throw new IllegalArgumentException("Path not exist: " + sourcePath);
+ public void renameCodePath(File beforeCodeFile, File afterCodeFile)
+ throws IllegalArgumentException, IOException {
+ final String beforeCodePath = beforeCodeFile.getAbsolutePath();
+ final String afterCodePathParent = afterCodeFile.getParentFile().getAbsolutePath();
+ if (!isIncrementalPath(beforeCodePath)) {
+ throw new IllegalArgumentException("Not an Incremental path: " + beforeCodePath);
}
- if (dest.exists()) {
- throw new IllegalArgumentException("Target path already exists: " + destPath);
+ final String afterCodePathName = afterCodeFile.getName();
+ final Path apkStoragePath = Paths.get(beforeCodePath);
+ if (apkStoragePath == null || apkStoragePath.toAbsolutePath() == null) {
+ throw new IOException("Invalid source storage path for: " + beforeCodePath);
}
- if (source.isDirectory() && dest.exists() && dest.isFile()) {
- throw new IllegalArgumentException(
- "Trying to rename a dir but destination is a file: " + destPath);
- }
- if (source.isFile() && dest.exists() && dest.isDirectory()) {
- throw new IllegalArgumentException(
- "Trying to rename a file but destination is a dir: " + destPath);
- }
- if (!isIncrementalPath(sourcePath)) {
- throw new IllegalArgumentException("Not an Incremental path: " + sourcePath);
- }
-
- Path storagePath = Paths.get(sourcePath);
- if (source.isFile()) {
- storagePath = getStoragePathForFile(source);
- }
- if (storagePath == null || storagePath.toAbsolutePath() == null) {
- throw new IOException("Invalid source storage path for: " + sourcePath);
- }
- final IncrementalStorage storage = openStorage(storagePath.toAbsolutePath().toString());
- if (storage == null) {
+ final IncrementalStorage apkStorage =
+ openStorage(apkStoragePath.toAbsolutePath().toString());
+ if (apkStorage == null) {
throw new IOException("Failed to retrieve storage from Incremental Service.");
}
-
- if (source.isFile()) {
- renameFile(storage, storagePath, source, dest);
- } else {
- renameDir(storage, storagePath, source, dest);
+ final IncrementalStorage linkedApkStorage = createStorage(afterCodePathParent, apkStorage,
+ IncrementalManager.CREATE_MODE_CREATE
+ | IncrementalManager.CREATE_MODE_PERMANENT_BIND);
+ if (linkedApkStorage == null) {
+ throw new IOException("Failed to create linked storage at dir: " + afterCodePathParent);
}
- }
-
- private void renameFile(IncrementalStorage storage, Path storagePath,
- File source, File dest) throws IOException {
- Path sourcePath = source.toPath();
- Path destPath = dest.toPath();
- if (!sourcePath.startsWith(storagePath)) {
- throw new IOException("Path: " + source.getAbsolutePath() + " is not on storage at: "
- + storagePath.toString());
- }
- if (!destPath.startsWith(storagePath)) {
- throw new IOException("Path: " + dest.getAbsolutePath() + " is not on storage at: "
- + storagePath.toString());
- }
- final Path sourceRelativePath = storagePath.relativize(sourcePath);
- final Path destRelativePath = storagePath.relativize(destPath);
- storage.moveFile(sourceRelativePath.toString(), destRelativePath.toString());
-
- }
-
- private void renameDir(IncrementalStorage storage, Path storagePath,
- File source, File dest) throws IOException {
- Path destPath = dest.toPath();
- boolean usedMkdir = false;
- try {
- Os.mkdir(dest.getAbsolutePath(), 0755);
- usedMkdir = true;
- } catch (ErrnoException e) {
- // Traditional mkdir fails but maybe we can create it on Incremental File System if
- // the dest path is on the same Incremental storage as the source.
- if (destPath.startsWith(storagePath)) {
- storage.makeDirectories(storagePath.relativize(destPath).toString());
- } else {
- throw new IOException("Failed to create directory: " + dest.getAbsolutePath(), e);
+ linkedApkStorage.makeDirectory(afterCodePathName);
+ File[] files = beforeCodeFile.listFiles();
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].isFile()) {
+ String fileName = files[i].getName();
+ apkStorage.makeLink(
+ fileName, linkedApkStorage, afterCodePathName + "/" + fileName);
}
}
- try {
- storage.moveDir(source.getAbsolutePath(), dest.getAbsolutePath());
- } catch (Exception ex) {
- if (usedMkdir) {
- try {
- Os.remove(dest.getAbsolutePath());
- } catch (ErrnoException ignored) {
- }
- }
- throw new IOException(
- "Failed to move " + source.getAbsolutePath() + " to " + dest.getAbsolutePath());
- }
+ apkStorage.unBind(beforeCodePath);
}
/**
diff --git a/core/java/android/service/textclassifier/TextClassifierService.java b/core/java/android/service/textclassifier/TextClassifierService.java
index 848868a..3ff6f54 100644
--- a/core/java/android/service/textclassifier/TextClassifierService.java
+++ b/core/java/android/service/textclassifier/TextClassifierService.java
@@ -41,6 +41,7 @@
import android.view.textclassifier.ConversationActions;
import android.view.textclassifier.SelectionEvent;
import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassificationConstants;
import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassificationSessionId;
@@ -404,22 +405,27 @@
*/
@NonNull
public static TextClassifier getDefaultTextClassifierImplementation(@NonNull Context context) {
- final String defaultTextClassifierPackageName =
- context.getPackageManager().getDefaultTextClassifierPackageName();
- if (TextUtils.isEmpty(defaultTextClassifierPackageName)) {
- return TextClassifier.NO_OP;
- }
- if (defaultTextClassifierPackageName.equals(context.getPackageName())) {
- throw new RuntimeException(
- "The default text classifier itself should not call the"
- + "getDefaultTextClassifierImplementation() method.");
- }
final TextClassificationManager tcm =
context.getSystemService(TextClassificationManager.class);
- if (tcm != null) {
- return tcm.getTextClassifier(TextClassifier.DEFAULT_SERVICE);
+ if (tcm == null) {
+ return TextClassifier.NO_OP;
}
- return TextClassifier.NO_OP;
+ TextClassificationConstants settings = new TextClassificationConstants();
+ if (settings.getUseDefaultTextClassifierAsDefaultImplementation()) {
+ final String defaultTextClassifierPackageName =
+ context.getPackageManager().getDefaultTextClassifierPackageName();
+ if (TextUtils.isEmpty(defaultTextClassifierPackageName)) {
+ return TextClassifier.NO_OP;
+ }
+ if (defaultTextClassifierPackageName.equals(context.getPackageName())) {
+ throw new RuntimeException(
+ "The default text classifier itself should not call the"
+ + "getDefaultTextClassifierImplementation() method.");
+ }
+ return tcm.getTextClassifier(TextClassifier.DEFAULT_SERVICE);
+ } else {
+ return tcm.getTextClassifier(TextClassifier.LOCAL);
+ }
}
/** @hide **/
diff --git a/core/java/android/view/textclassifier/TextClassificationConstants.java b/core/java/android/view/textclassifier/TextClassificationConstants.java
index ed69513..3d5ac58 100644
--- a/core/java/android/view/textclassifier/TextClassificationConstants.java
+++ b/core/java/android/view/textclassifier/TextClassificationConstants.java
@@ -17,6 +17,7 @@
package android.view.textclassifier;
import android.annotation.Nullable;
+import android.content.Context;
import android.provider.DeviceConfig;
import com.android.internal.annotations.VisibleForTesting;
@@ -167,6 +168,16 @@
static final String TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE =
"textclassifier_service_package_override";
+ /**
+ * Whether to use the default system text classifier as the default text classifier
+ * implementation. The local text classifier is used if it is {@code false}.
+ *
+ * @see android.service.textclassifier.TextClassifierService#getDefaultTextClassifierImplementation(Context)
+ */
+ // TODO: Once the system health experiment is done, remove this together with local TC.
+ private static final String USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL =
+ "use_default_system_text_classifier_as_default_impl";
+
private static final String DEFAULT_TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE = null;
private static final boolean LOCAL_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
private static final boolean SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
@@ -209,7 +220,8 @@
private static final boolean TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT = true;
private static final boolean TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT = true;
private static final boolean DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT = true;
- private static final float[] LANG_ID_CONTEXT_SETTINGS_DEFAULT = new float[] {20f, 1.0f, 0.4f};
+ private static final float[] LANG_ID_CONTEXT_SETTINGS_DEFAULT = new float[]{20f, 1.0f, 0.4f};
+ private static final boolean USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL_DEFAULT = true;
@Nullable
public String getTextClassifierServicePackageOverride() {
@@ -331,6 +343,13 @@
LANG_ID_CONTEXT_SETTINGS, LANG_ID_CONTEXT_SETTINGS_DEFAULT);
}
+ public boolean getUseDefaultTextClassifierAsDefaultImplementation() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL,
+ USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL_DEFAULT);
+ }
+
void dump(IndentingPrintWriter pw) {
pw.println("TextClassificationConstants:");
pw.increaseIndent();
@@ -378,6 +397,8 @@
.println();
pw.printPair("textclassifier_service_package_override",
getTextClassifierServicePackageOverride()).println();
+ pw.printPair("use_default_system_text_classifier_as_default_impl",
+ getUseDefaultTextClassifierAsDefaultImplementation()).println();
pw.decreaseIndent();
}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index a43e4fe..65cad83 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2424,6 +2424,10 @@
offset += findViewById(R.id.content_preview_container).getHeight();
}
+ if (hasWorkProfile() && ENABLE_TABBED_VIEW) {
+ offset += findViewById(R.id.tabs).getHeight();
+ }
+
int directShareHeight = 0;
rowsToShow = Math.min(4, rowsToShow);
for (int i = 0, childCount = recyclerView.getChildCount();
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index c6ed624..518911e 100644
--- a/core/java/com/android/internal/os/RuntimeInit.java
+++ b/core/java/com/android/internal/os/RuntimeInit.java
@@ -323,7 +323,7 @@
result.append(System.getProperty("java.vm.version")); // such as 1.1.0
result.append(" (Linux; U; Android ");
- String version = Build.VERSION.RELEASE; // "1.0" or "3.4b5"
+ String version = Build.VERSION.RELEASE_OR_CODENAME; // "1.0" or "3.4b5"
result.append(version.length() > 0 ? version : "1.0");
// add the model for the release build
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index efa7d59..6944b2b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4878,6 +4878,19 @@
<permission android:name="android.permission.ACCESS_SHARED_LIBRARIES"
android:protectionLevel="signature|installer" />
+ <!-- Allows an app to log compat change usage.
+ @hide <p>Not for use by third-party applications.</p> -->
+ <permission android:name="android.permission.LOG_COMPAT_CHANGE"
+ android:protectionLevel="signature|privileged" />
+ <!-- Allows an app to read compat change config.
+ @hide <p>Not for use by third-party applications.</p> -->
+ <permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"
+ android:protectionLevel="signature|privileged" />
+ <!-- Allows an app to override compat change config.
+ @hide <p>Not for use by third-party applications.</p> -->
+ <permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"
+ android:protectionLevel="signature|privileged" />
+
<!-- Allows input events to be monitored. Very dangerous! @hide -->
<permission android:name="android.permission.MONITOR_INPUT"
android:protectionLevel="signature" />
@@ -5315,6 +5328,10 @@
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
+ <service android:name="com.android.server.blob.BlobStoreIdleJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE">
+ </service>
+
<service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader">
<intent-filter>
<action android:name="android.intent.action.LOAD_DATA" />
diff --git a/core/res/res/layout/accessibility_button_chooser_item.xml b/core/res/res/layout/accessibility_button_chooser_item.xml
index d19e313..d6fd7aa 100644
--- a/core/res/res/layout/accessibility_button_chooser_item.xml
+++ b/core/res/res/layout/accessibility_button_chooser_item.xml
@@ -20,6 +20,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:clickable="true"
android:gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b2d08a9..31e68e8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4343,4 +4343,7 @@
Determines whether the specified key groups can be used to wake up the device. -->
<bool name="config_wakeOnDpadKeyPress">true</bool>
<bool name="config_wakeOnAssistKeyPress">true</bool>
+
+ <!-- Whether to default to an expanded list of users on the lock screen user switcher. -->
+ <bool name="config_expandLockScreenUserSwitcher">false</bool>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c59d25f..2453bb1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3863,4 +3863,7 @@
<!-- Toast message for background started foreground service while-in-use permission restriction feature -->
<java-symbol type="string" name="allow_while_in_use_permission_in_fgs" />
+
+ <!-- Whether to expand the lock screen user switcher by default -->
+ <java-symbol type="bool" name="config_expandLockScreenUserSwitcher" />
</resources>
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index ee93b39..59335a5 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -111,6 +111,10 @@
<uses-permission android:name="android.permission.MOVE_PACKAGE" />
<uses-permission android:name="android.permission.PACKAGE_VERIFICATION_AGENT" />
+ <!-- gating and logging permissions -->
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+
<!-- os storage test permissions -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.ASEC_ACCESS" />
diff --git a/core/tests/coretests/src/android/content/integrity/AtomicFormulaTest.java b/core/tests/coretests/src/android/content/integrity/AtomicFormulaTest.java
index bf78203..7733559 100644
--- a/core/tests/coretests/src/android/content/integrity/AtomicFormulaTest.java
+++ b/core/tests/coretests/src/android/content/integrity/AtomicFormulaTest.java
@@ -90,18 +90,18 @@
}
@Test
- public void testValidAtomicFormula_stringValue_appCertificateAutoHashed() {
+ public void testValidAtomicFormula_stringValue_appCertificateIsNotAutoHashed() {
String appCert = "cert";
StringAtomicFormula stringAtomicFormula =
new StringAtomicFormula(AtomicFormula.APP_CERTIFICATE, appCert);
assertThat(stringAtomicFormula.getKey()).isEqualTo(AtomicFormula.APP_CERTIFICATE);
- assertThat(stringAtomicFormula.getValue()).doesNotMatch(appCert);
- assertThat(stringAtomicFormula.getIsHashedValue()).isTrue();
+ assertThat(stringAtomicFormula.getValue()).matches(appCert);
+ assertThat(stringAtomicFormula.getIsHashedValue()).isFalse();
}
@Test
- public void testValidAtomicFormula_stringValue_installerCertificateAutoHashed() {
+ public void testValidAtomicFormula_stringValue_installerCertificateIsNotAutoHashed() {
String installerCert = "cert";
StringAtomicFormula stringAtomicFormula =
new StringAtomicFormula(AtomicFormula.INSTALLER_CERTIFICATE,
@@ -109,8 +109,8 @@
assertThat(stringAtomicFormula.getKey()).isEqualTo(
AtomicFormula.INSTALLER_CERTIFICATE);
- assertThat(stringAtomicFormula.getValue()).doesNotMatch(installerCert);
- assertThat(stringAtomicFormula.getIsHashedValue()).isTrue();
+ assertThat(stringAtomicFormula.getValue()).matches(installerCert);
+ assertThat(stringAtomicFormula.getIsHashedValue()).isFalse();
}
@Test
diff --git a/core/tests/coretests/src/android/content/integrity/IntegrityFormulaTest.java b/core/tests/coretests/src/android/content/integrity/IntegrityFormulaTest.java
index c180602..dc03167 100644
--- a/core/tests/coretests/src/android/content/integrity/IntegrityFormulaTest.java
+++ b/core/tests/coretests/src/android/content/integrity/IntegrityFormulaTest.java
@@ -40,7 +40,7 @@
assertThat(stringAtomicFormula.getKey()).isEqualTo(AtomicFormula.PACKAGE_NAME);
assertThat(stringAtomicFormula.getValue()).isEqualTo(packageName);
- assertThat(stringAtomicFormula.getIsHashedValue()).isEqualTo(false);
+ assertThat(stringAtomicFormula.getIsHashedValue()).isFalse();
}
@Test
@@ -53,8 +53,8 @@
(AtomicFormula.StringAtomicFormula) formula;
assertThat(stringAtomicFormula.getKey()).isEqualTo(AtomicFormula.APP_CERTIFICATE);
- assertThat(stringAtomicFormula.getValue()).doesNotMatch(appCertificate);
- assertThat(stringAtomicFormula.getIsHashedValue()).isEqualTo(true);
+ assertThat(stringAtomicFormula.getValue()).matches(appCertificate);
+ assertThat(stringAtomicFormula.getIsHashedValue()).isFalse();
}
@Test
@@ -68,7 +68,7 @@
assertThat(stringAtomicFormula.getKey()).isEqualTo(AtomicFormula.INSTALLER_NAME);
assertThat(stringAtomicFormula.getValue()).isEqualTo(installerName);
- assertThat(stringAtomicFormula.getIsHashedValue()).isEqualTo(false);
+ assertThat(stringAtomicFormula.getIsHashedValue()).isFalse();
}
@Test
@@ -81,8 +81,8 @@
(AtomicFormula.StringAtomicFormula) formula;
assertThat(stringAtomicFormula.getKey()).isEqualTo(AtomicFormula.INSTALLER_CERTIFICATE);
- assertThat(stringAtomicFormula.getValue()).doesNotMatch(installerCertificate);
- assertThat(stringAtomicFormula.getIsHashedValue()).isEqualTo(true);
+ assertThat(stringAtomicFormula.getValue()).matches(installerCertificate);
+ assertThat(stringAtomicFormula.getIsHashedValue()).isFalse();
}
@Test
diff --git a/core/tests/coretests/src/android/os/BuildTest.java b/core/tests/coretests/src/android/os/BuildTest.java
index decc768..2295eb9 100644
--- a/core/tests/coretests/src/android/os/BuildTest.java
+++ b/core/tests/coretests/src/android/os/BuildTest.java
@@ -60,7 +60,7 @@
assertNotEmpty("BRAND", Build.BRAND);
assertNotEmpty("MODEL", Build.MODEL);
assertNotEmpty("VERSION.INCREMENTAL", Build.VERSION.INCREMENTAL);
- assertNotEmpty("VERSION.RELEASE", Build.VERSION.RELEASE);
+ assertNotEmpty("VERSION.RELEASE", Build.VERSION.RELEASE_OR_CODENAME);
assertNotEmpty("TYPE", Build.TYPE);
Assert.assertNotNull("TAGS", Build.TAGS); // TAGS is allowed to be empty.
assertNotEmpty("FINGERPRINT", Build.FINGERPRINT);
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
index df6b906..ce71beb 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -38,6 +38,7 @@
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -355,6 +356,7 @@
// enable the work tab feature flag
ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
Intent sendIntent = createSendTextIntent();
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(2);
@@ -1209,17 +1211,24 @@
// enable the work tab feature flag
ResolverActivity.ENABLE_TABBED_VIEW = true;
int personalProfileTargets = 3;
+ int otherProfileTargets = 1;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTest(personalProfileTargets);
+ createResolvedComponentsForTestWithOtherProfile(
+ personalProfileTargets + otherProfileTargets, /* userID */ 10);
int workProfileTargets = 4;
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(
workProfileTargets);
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos);
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos);
+ Mockito.isA(List.class))).thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM))).thenReturn(new ArrayList<>(personalResolvedComponentInfos));
Intent sendIntent = createSendTextIntent();
sendIntent.setType("TestType");
markWorkProfileUserAvailable();
@@ -1229,8 +1238,6 @@
waitForIdle();
assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0));
- // The work list adapter must only be filled when we open the work tab
- assertThat(activity.getWorkListAdapter().getCount(), is(0));
onView(withText(R.string.resolver_work_tab)).perform(click());
assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets));
@@ -1243,11 +1250,22 @@
ResolverActivity.ENABLE_TABBED_VIEW = true;
markWorkProfileUserAvailable();
int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos =
createResolvedComponentsForTest(workProfileTargets);
+ when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos);
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
Intent sendIntent = createSendTextIntent();
sendIntent.setType("TestType");
@@ -1357,6 +1375,20 @@
return infoList;
}
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults, int userId) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(
+ ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+ }
+ }
+ return infoList;
+ }
+
private List<ResolvedComponentInfo> createResolvedComponentsForTestWithUserId(
int numberOfResults, int userId) {
List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
index eee62bb..a68b5908 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
@@ -16,10 +16,15 @@
package com.android.internal.app;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
import android.app.usage.UsageStatsManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -40,6 +45,8 @@
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import org.mockito.Mockito;
+
import java.util.function.Function;
public class ChooserWrapperActivity extends ChooserActivity {
@@ -173,6 +180,12 @@
return mMultiProfilePagerAdapter.getCurrentUserHandle();
}
+ @Override
+ public Context createContextAsUser(UserHandle user, int flags) {
+ // return the current context as a work profile doesn't really exist in these tests
+ return getApplicationContext();
+ }
+
/**
* We cannot directly mock the activity created since instrumentation creates it.
* <p>
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index 911490f..5f4194a 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -225,6 +225,7 @@
// enable the work tab feature flag
ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
Intent sendIntent = createSendImageIntent();
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(2);
@@ -246,7 +247,6 @@
chosen[0] = targetInfo.getResolveInfo();
return true;
};
-
// Make a stable copy of the components as the original list may be modified
List<ResolvedComponentInfo> stableCopy =
createResolvedComponentsForTestWithOtherProfile(2);
@@ -443,7 +443,7 @@
// enable the work tab feature flag
ResolverActivity.ENABLE_TABBED_VIEW = true;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTest(3);
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
@@ -451,6 +451,11 @@
when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos);
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
Intent sendIntent = createSendImageIntent();
markWorkProfileUserAvailable();
@@ -478,17 +483,20 @@
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
- eq(sOverrides.workProfileUserHandle))).thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ eq(sOverrides.workProfileUserHandle)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
- eq(sOverrides.workProfileUserHandle))).thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ eq(sOverrides.workProfileUserHandle)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(new ArrayList<>(workResolvedComponentInfos));
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
@@ -502,7 +510,7 @@
onView(withText(R.string.resolver_work_tab)).perform(click());
assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
- assertThat(activity.getPersonalListAdapter().getCount(), is(3));
+ assertThat(activity.getPersonalListAdapter().getCount(), is(2));
}
@Test
@@ -511,14 +519,20 @@
ResolverActivity.ENABLE_TABBED_VIEW = true;
markWorkProfileUserAvailable();
List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTest(3);
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos);
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos);
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
Intent sendIntent = createSendImageIntent();
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
@@ -536,14 +550,20 @@
ResolverActivity.ENABLE_TABBED_VIEW = true;
markWorkProfileUserAvailable();
List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTest(3);
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos);
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos);
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
Intent sendIntent = createSendImageIntent();
ResolveInfo[] chosen = new ResolveInfo[1];
sOverrides.onSafelyStartCallback = targetInfo -> {
@@ -587,17 +607,20 @@
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
- eq(sOverrides.workProfileUserHandle))).thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ eq(sOverrides.workProfileUserHandle)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
- eq(sOverrides.workProfileUserHandle))).thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ eq(sOverrides.workProfileUserHandle)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(new ArrayList<>(workResolvedComponentInfos));
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
@@ -678,6 +701,20 @@
return infoList;
}
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults, int userId) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(
+ ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+ }
+ }
+ return infoList;
+ }
+
private void waitForIdle() {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml
index a200a51..fe1182e 100644
--- a/data/etc/com.android.settings.xml
+++ b/data/etc/com.android.settings.xml
@@ -26,6 +26,7 @@
<permission name="android.permission.DELETE_PACKAGES"/>
<permission name="android.permission.FORCE_STOP_PACKAGES"/>
<permission name="android.permission.LOCAL_MAC_ADDRESS"/>
+ <permission name="android.permission.LOG_COMPAT_CHANGE" />
<permission name="android.permission.MANAGE_DEBUGGING"/>
<permission name="android.permission.MANAGE_DEVICE_ADMINS"/>
<permission name="android.permission.MANAGE_FINGERPRINT"/>
@@ -37,8 +38,10 @@
<permission name="android.permission.MODIFY_PHONE_STATE"/>
<permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<permission name="android.permission.MOVE_PACKAGE"/>
+ <permission name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG" />
<permission name="android.permission.OVERRIDE_WIFI_CONFIG"/>
<permission name="android.permission.PACKAGE_USAGE_STATS"/>
+ <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
<permission name="android.permission.READ_SEARCH_INDEXABLES"/>
<permission name="android.permission.REBOOT"/>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index b5eba09..b9a7e22 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -366,6 +366,10 @@
<permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
<!-- Permission required for Tethering CTS tests. -->
<permission name="android.permission.TETHER_PRIVILEGED"/>
+ <!-- Permissions required for ganting and logging -->
+ <permission name="android.permission.LOG_COMPAT_CHANGE" />
+ <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+ <permission name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG" />
<!-- Permissions required to test ambient display. -->
<permission name="android.permission.READ_DREAM_STATE" />
<permission name="android.permission.WRITE_DREAM_STATE" />
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 6a89b71..1d679c7 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -223,6 +223,11 @@
<!-- permissions required for CTS test - PhoneStateListenerTest -->
<uses-permission android:name="android.permission.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH" />
+ <!-- Permissions required for ganting and logging -->
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
+ <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
+
<!-- Permission required for CTS test - UiModeManagerTest -->
<uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED"/>
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 139a8c3..1fe967b 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -645,7 +645,6 @@
<activity android:name=".controls.management.ControlsProviderSelectorActivity"
android:label="Controls Providers"
android:theme="@style/Theme.ControlsManagement"
- android:exported="true"
android:showForAllUsers="true"
android:excludeFromRecents="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
diff --git a/packages/SystemUI/res/drawable/ic_device_unknown_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_device_unknown_gm2_24px.xml
new file mode 100644
index 0000000..24e0635
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_device_unknown_gm2_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2l-5.5,9h11L12,2zM12,5.84L13.93,9h-3.87L12,5.84zM17.5,13c-2.49,0 -4.5,2.01 -4.5,4.5s2.01,4.5 4.5,4.5 4.5,-2.01 4.5,-4.5 -2.01,-4.5 -4.5,-4.5zM17.5,20c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5zM3,21.5h8v-8L3,13.5v8zM5,15.5h4v4L5,19.5v-4z"/>
+</vector>
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index a6f1d84..7de1557 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -19,9 +19,12 @@
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ComponentName
+import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.database.ContentObserver
+import android.net.Uri
import android.os.Environment
import android.os.UserHandle
import android.provider.Settings
@@ -30,6 +33,7 @@
import android.util.ArrayMap
import android.util.Log
import com.android.internal.annotations.GuardedBy
+import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.DumpController
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -53,15 +57,16 @@
private val uiController: ControlsUiController,
private val bindingController: ControlsBindingController,
private val listingController: ControlsListingController,
- broadcastDispatcher: BroadcastDispatcher,
+ private val broadcastDispatcher: BroadcastDispatcher,
optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
dumpController: DumpController
) : Dumpable, ControlsController {
companion object {
private const val TAG = "ControlsControllerImpl"
- const val CONTROLS_AVAILABLE = "systemui.controls_available"
- const val USER_CHANGE_RETRY_DELAY = 500L // ms
+ internal const val CONTROLS_AVAILABLE = "systemui.controls_available"
+ internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
+ private const val USER_CHANGE_RETRY_DELAY = 500L // ms
}
// Map of map: ComponentName -> (String -> ControlInfo).
@@ -69,9 +74,11 @@
@GuardedBy("currentFavorites")
private val currentFavorites = ArrayMap<ComponentName, MutableMap<String, ControlInfo>>()
- private var userChanging = true
- override var available = Settings.Secure.getInt(
- context.contentResolver, CONTROLS_AVAILABLE, 0) != 0
+ private var userChanging: Boolean = true
+
+ private val contentResolver: ContentResolver
+ get() = context.contentResolver
+ override var available = Settings.Secure.getInt(contentResolver, CONTROLS_AVAILABLE, 0) != 0
private set
private var currentUser = context.user
@@ -95,8 +102,8 @@
val fileName = Environment.buildPath(
userContext.filesDir, ControlsFavoritePersistenceWrapper.FILE_NAME)
persistenceWrapper.changeFile(fileName)
- available = Settings.Secure.getIntForUser(
- context.contentResolver, CONTROLS_AVAILABLE, 0) != 0
+ available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
+ /* default */ 0, newUser.identifier) != 0
synchronized(currentFavorites) {
currentFavorites.clear()
}
@@ -123,6 +130,25 @@
}
}
+ @VisibleForTesting
+ internal val settingObserver = object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean, uri: Uri, userId: Int) {
+ // Do not listen to changes in the middle of user change, those will be read by the
+ // user-switch receiver.
+ if (userChanging || userId != currentUserId) {
+ return
+ }
+ available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
+ /* default */ 0, currentUserId) != 0
+ synchronized(currentFavorites) {
+ currentFavorites.clear()
+ }
+ if (available) {
+ loadFavorites()
+ }
+ }
+ }
+
init {
dumpController.registerDumpable(this)
if (available) {
@@ -135,6 +161,7 @@
executor,
UserHandle.ALL
)
+ contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
}
private fun confirmAvailability(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
index fad2d94..88b19b5 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -34,14 +34,17 @@
import android.widget.TextView
import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.R
const val MIN_LEVEL = 0
const val MAX_LEVEL = 10000
+private const val UPDATE_DELAY_IN_MILLIS = 2000L
class ControlViewHolder(
val layout: ViewGroup,
- val controlsController: ControlsController
+ val controlsController: ControlsController,
+ val uiExecutor: DelayableExecutor
) {
val icon: ImageView = layout.requireViewById(R.id.icon)
val status: TextView = layout.requireViewById(R.id.status)
@@ -52,6 +55,7 @@
val clipLayer: ClipDrawable
val gd: GradientDrawable
lateinit var cws: ControlWithState
+ var cancelUpdate: Runnable? = null
init {
val ld = layout.getBackground() as LayerDrawable
@@ -63,6 +67,8 @@
fun bindData(cws: ControlWithState) {
this.cws = cws
+ cancelUpdate?.run()
+
val (status, template) = cws.control?.let {
title.setText(it.getTitle())
subtitle.setText(it.getSubtitle())
@@ -86,6 +92,27 @@
findBehavior(status, template).apply(this, cws)
}
+ fun actionResponse(@ControlAction.ResponseResult response: Int) {
+ val text = when (response) {
+ ControlAction.RESPONSE_OK -> "Success"
+ ControlAction.RESPONSE_FAIL -> "Error"
+ else -> ""
+ }
+
+ if (!text.isEmpty()) {
+ val previousText = status.getText()
+ val previousTextExtra = statusExtra.getText()
+
+ cancelUpdate = uiExecutor.executeDelayed({
+ status.setText(previousText)
+ statusExtra.setText(previousTextExtra)
+ }, UPDATE_DELAY_IN_MILLIS)
+
+ status.setText(text)
+ statusExtra.setText("")
+ }
+ }
+
fun action(action: ControlAction) {
controlsController.action(cws.ci, action)
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index b07a75d..d70c86f 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -22,6 +22,8 @@
import android.view.ViewGroup
interface ControlsUiController {
+ val available: Boolean
+
fun show(parent: ViewGroup)
fun hide()
fun onRefreshState(componentName: ComponentName, controls: List<Control>)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index a777faf..ed521e3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -38,10 +38,10 @@
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.R
+import com.android.systemui.util.concurrency.DelayableExecutor
import dagger.Lazy
-import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
@@ -104,18 +104,23 @@
}
}
+private data class ControlKey(val componentName: ComponentName, val controlId: String)
+
@Singleton
class ControlsUiControllerImpl @Inject constructor (
val controlsController: Lazy<ControlsController>,
val context: Context,
- @Main val uiExecutor: Executor
+ @Main val uiExecutor: DelayableExecutor
) : ControlsUiController {
private lateinit var controlInfos: List<ControlInfo>
- private val controlsById = mutableMapOf<Pair<ComponentName, String>, ControlWithState>()
- private val controlViewsById = mutableMapOf<String, ControlViewHolder>()
+ private val controlsById = mutableMapOf<ControlKey, ControlWithState>()
+ private val controlViewsById = mutableMapOf<ControlKey, ControlViewHolder>()
private lateinit var parent: ViewGroup
+ override val available: Boolean
+ get() = controlsController.get().available
+
override fun show(parent: ViewGroup) {
Log.d(TAG, "show()")
@@ -125,7 +130,7 @@
controlInfos.map {
ControlWithState(it, null)
- }.associateByTo(controlsById) { Pair(it.ci.component, it.ci.controlId) }
+ }.associateByTo(controlsById) { ControlKey(it.ci.component, it.ci.controlId) }
if (controlInfos.isEmpty()) {
showInitialSetupView()
@@ -178,9 +183,10 @@
val item = inflater.inflate(
R.layout.controls_base_item, lastRow, false) as ViewGroup
lastRow.addView(item)
- val cvh = ControlViewHolder(item, controlsController.get())
- cvh.bindData(controlsById.get(Pair(it.component, it.controlId))!!)
- controlViewsById.put(it.controlId, cvh)
+ val cvh = ControlViewHolder(item, controlsController.get(), uiExecutor)
+ val key = ControlKey(it.component, it.controlId)
+ cvh.bindData(controlsById.getValue(key))
+ controlViewsById.put(key, cvh)
}
if ((controlInfos.size % 2) == 1) {
@@ -205,21 +211,24 @@
override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
Log.d(TAG, "onRefreshState()")
controls.forEach { c ->
- controlsById.get(Pair(componentName, c.getControlId()))?.let {
+ controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
Log.d(TAG, "onRefreshState() for id: " + c.getControlId())
val cws = ControlWithState(it.ci, c)
- controlsById.put(Pair(componentName, c.getControlId()), cws)
+ val key = ControlKey(componentName, c.getControlId())
+ controlsById.put(key, cws)
uiExecutor.execute {
- controlViewsById.get(c.getControlId())?.bindData(cws)
+ controlViewsById.get(key)?.bindData(cws)
}
}
}
}
override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
- Log.d(TAG, "onActionResponse()")
- TODO("not implemented")
+ val key = ControlKey(componentName, controlId)
+ uiExecutor.execute {
+ controlViewsById.get(key)?.actionResponse(response)
+ }
}
private fun createRow(inflater: LayoutInflater, parent: ViewGroup): ViewGroup {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
index 093c99f..da52c6f 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
@@ -108,10 +108,18 @@
DeviceTypes.TYPE_OUTLET to IconState(
R.drawable.ic_power_off_gm2_24px,
R.drawable.ic_power_gm2_24px
+ ),
+ DeviceTypes.TYPE_VACUUM to IconState(
+ R.drawable.ic_vacuum_gm2_24px,
+ R.drawable.ic_vacuum_gm2_24px
+ ),
+ DeviceTypes.TYPE_MOP to IconState(
+ R.drawable.ic_vacuum_gm2_24px,
+ R.drawable.ic_vacuum_gm2_24px
)
).withDefault {
IconState(
- R.drawable.ic_light_off_gm2_24px,
- R.drawable.ic_lightbulb_outline_gm2_24px
+ R.drawable.ic_device_unknown_gm2_24px,
+ R.drawable.ic_device_unknown_gm2_24px
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 45c07a3..082b065 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -1899,9 +1899,7 @@
}
private boolean shouldShowControls() {
- return isCurrentUserOwner()
- && !mKeyguardManager.isDeviceLocked()
- && Settings.Secure.getInt(mContext.getContentResolver(),
- "systemui.controls_available", 0) == 1;
+ return !mKeyguardManager.isDeviceLocked()
+ && mControlsUiController.getAvailable();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java
index 0134aa3..5de6d1c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java
@@ -169,7 +169,7 @@
if (DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(mContext)) {
v.setText(mContext.getString(
com.android.internal.R.string.bugreport_status,
- Build.VERSION.RELEASE,
+ Build.VERSION.RELEASE_OR_CODENAME,
Build.ID));
v.setVisibility(View.VISIBLE);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
index 9e3e94c..6c69718 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
@@ -36,6 +36,7 @@
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
+import android.os.Handler;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -370,6 +371,13 @@
if (mSeamless == null) {
return;
}
+ Handler handler = mSeamless.getHandler();
+ handler.post(() -> {
+ updateChipInternal(device);
+ });
+ }
+
+ private void updateChipInternal(MediaDevice device) {
ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
// Update the outline color
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 2907cd4..8dfcb0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -134,6 +134,8 @@
mBroadcastDispatcher.registerReceiver(
mReceiver, filter, null /* handler */, UserHandle.SYSTEM);
+ mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
+
mSecondaryUserServiceIntent = new Intent(context, SystemUISecondaryUserService.class);
filter = new IntentFilter();
@@ -258,22 +260,20 @@
&& mUserManager.canAddMoreUsers();
boolean createIsRestricted = !addUsersWhenLocked;
- if (!mSimpleUserSwitcher) {
- if (guestRecord == null) {
- if (canCreateGuest) {
- guestRecord = new UserRecord(null /* info */, null /* picture */,
- true /* isGuest */, false /* isCurrent */,
- false /* isAddUser */, createIsRestricted, canSwitchUsers);
- checkIfAddUserDisallowedByAdminOnly(guestRecord);
- records.add(guestRecord);
- }
- } else {
- int index = guestRecord.isCurrent ? 0 : records.size();
- records.add(index, guestRecord);
+ if (guestRecord == null) {
+ if (canCreateGuest) {
+ guestRecord = new UserRecord(null /* info */, null /* picture */,
+ true /* isGuest */, false /* isCurrent */,
+ false /* isAddUser */, createIsRestricted, canSwitchUsers);
+ checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ records.add(guestRecord);
}
+ } else {
+ int index = guestRecord.isCurrent ? 0 : records.size();
+ records.add(index, guestRecord);
}
- if (!mSimpleUserSwitcher && canCreateUser) {
+ if (canCreateUser) {
UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
createIsRestricted, canSwitchUsers);
@@ -562,8 +562,7 @@
private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
public void onChange(boolean selfChange) {
- mSimpleUserSwitcher = Settings.Global.getInt(mContext.getContentResolver(),
- SIMPLE_USER_SWITCHER_GLOBAL_SETTING, 0) != 0;
+ mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
mAddUsersWhenLocked = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0;
refreshUsers(UserHandle.USER_NULL);
@@ -579,6 +578,7 @@
final UserRecord u = mUsers.get(i);
pw.print(" "); pw.println(u.toString());
}
+ pw.println("mSimpleUserSwitcher=" + mSimpleUserSwitcher);
}
public String getCurrentUserName(Context context) {
@@ -717,6 +717,13 @@
}
}
+ private boolean shouldUseSimpleUserSwitcher() {
+ int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0;
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ SIMPLE_USER_SWITCHER_GLOBAL_SETTING, defaultSimpleUserSwitcher) != 0;
+ }
+
public void startActivity(Intent intent) {
mActivityStarter.startActivity(intent, true);
}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
index a60ca62..49ada1a 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
@@ -158,7 +158,7 @@
String demoTime = "1010"; // 10:10, a classic choice of horologists
try {
- String[] versionParts = android.os.Build.VERSION.RELEASE.split("\\.");
+ String[] versionParts = android.os.Build.VERSION.RELEASE_OR_CODENAME.split("\\.");
int majorVersion = Integer.valueOf(versionParts[0]);
demoTime = String.format("%02d00", majorVersion % 24);
} catch (IllegalArgumentException ex) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 897091f..e3bcdc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -32,12 +32,13 @@
import com.android.systemui.DumpController
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ControlStatus
+import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -82,7 +83,7 @@
private lateinit var broadcastReceiverCaptor: ArgumentCaptor<BroadcastReceiver>
private lateinit var delayableExecutor: FakeExecutor
- private lateinit var controller: ControlsController
+ private lateinit var controller: ControlsControllerImpl
companion object {
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
@@ -416,5 +417,70 @@
verify(listingController).changeUser(UserHandle.of(otherUser))
assertTrue(controller.getFavoriteControls().isEmpty())
assertEquals(otherUser, controller.currentUserId)
+ assertTrue(controller.available)
}
-}
\ No newline at end of file
+
+ @Test
+ fun testDisableFeature_notAvailable() {
+ Settings.Secure.putIntForUser(mContext.contentResolver,
+ ControlsControllerImpl.CONTROLS_AVAILABLE, 0, user)
+ controller.settingObserver.onChange(false, ControlsControllerImpl.URI, 0)
+ assertFalse(controller.available)
+ }
+
+ @Test
+ fun testDisableFeature_clearFavorites() {
+ controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+ assertFalse(controller.getFavoriteControls().isEmpty())
+
+ Settings.Secure.putIntForUser(mContext.contentResolver,
+ ControlsControllerImpl.CONTROLS_AVAILABLE, 0, user)
+ controller.settingObserver.onChange(false, ControlsControllerImpl.URI, user)
+ assertTrue(controller.getFavoriteControls().isEmpty())
+ }
+
+ @Test
+ fun testDisableFeature_noChangeForNotCurrentUser() {
+ controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+ Settings.Secure.putIntForUser(mContext.contentResolver,
+ ControlsControllerImpl.CONTROLS_AVAILABLE, 0, otherUser)
+ controller.settingObserver.onChange(false, ControlsControllerImpl.URI, otherUser)
+
+ assertTrue(controller.available)
+ assertFalse(controller.getFavoriteControls().isEmpty())
+ }
+
+ @Test
+ fun testCorrectUserSettingOnUserChange() {
+ Settings.Secure.putIntForUser(mContext.contentResolver,
+ ControlsControllerImpl.CONTROLS_AVAILABLE, 0, otherUser)
+
+ val intent = Intent(Intent.ACTION_USER_SWITCHED).apply {
+ putExtra(Intent.EXTRA_USER_HANDLE, otherUser)
+ }
+ val pendingResult = mock(BroadcastReceiver.PendingResult::class.java)
+ `when`(pendingResult.sendingUserId).thenReturn(otherUser)
+ broadcastReceiverCaptor.value.pendingResult = pendingResult
+
+ broadcastReceiverCaptor.value.onReceive(mContext, intent)
+
+ assertFalse(controller.available)
+ }
+
+ @Test
+ fun testCountFavoritesForComponent_singleComponent() {
+ controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+
+ assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
+ assertEquals(0, controller.countFavoritesForComponent(TEST_COMPONENT_2))
+ }
+
+ @Test
+ fun testCountFavoritesForComponent_multipleComponents() {
+ controller.changeFavoriteStatus(TEST_CONTROL_INFO, true)
+ controller.changeFavoriteStatus(TEST_CONTROL_INFO_2, true)
+
+ assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT))
+ assertEquals(1, controller.countFavoritesForComponent(TEST_COMPONENT_2))
+ }
+}
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 2fc9d04..c4e6427 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -16,6 +16,11 @@
package com.android.server.compat;
+import static android.Manifest.permission.LOG_COMPAT_CHANGE;
+import static android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG;
+import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
import android.app.ActivityManager;
import android.app.IActivityManager;
import android.content.Context;
@@ -67,12 +72,14 @@
@Override
public void reportChange(long changeId, ApplicationInfo appInfo) {
+ checkCompatChangeLogPermission();
reportChange(changeId, appInfo.uid,
ChangeReporter.STATE_LOGGED);
}
@Override
public void reportChangeByPackageName(long changeId, String packageName, int userId) {
+ checkCompatChangeLogPermission();
ApplicationInfo appInfo = getApplicationInfo(packageName, userId);
if (appInfo == null) {
return;
@@ -82,11 +89,13 @@
@Override
public void reportChangeByUid(long changeId, int uid) {
+ checkCompatChangeLogPermission();
reportChange(changeId, uid, ChangeReporter.STATE_LOGGED);
}
@Override
public boolean isChangeEnabled(long changeId, ApplicationInfo appInfo) {
+ checkCompatChangeReadAndLogPermission();
if (mCompatConfig.isChangeEnabled(changeId, appInfo)) {
reportChange(changeId, appInfo.uid,
ChangeReporter.STATE_ENABLED);
@@ -99,6 +108,7 @@
@Override
public boolean isChangeEnabledByPackageName(long changeId, String packageName, int userId) {
+ checkCompatChangeReadAndLogPermission();
ApplicationInfo appInfo = getApplicationInfo(packageName, userId);
if (appInfo == null) {
return true;
@@ -108,6 +118,7 @@
@Override
public boolean isChangeEnabledByUid(long changeId, int uid) {
+ checkCompatChangeReadAndLogPermission();
String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
if (packages == null || packages.length == 0) {
return true;
@@ -140,6 +151,7 @@
@Override
public void setOverrides(CompatibilityChangeConfig overrides, String packageName)
throws RemoteException, SecurityException {
+ checkCompatChangeOverridePermission();
mCompatConfig.addOverrides(overrides, packageName);
killPackage(packageName);
}
@@ -147,11 +159,13 @@
@Override
public void setOverridesForTest(CompatibilityChangeConfig overrides, String packageName)
throws RemoteException, SecurityException {
+ checkCompatChangeOverridePermission();
mCompatConfig.addOverrides(overrides, packageName);
}
@Override
public void clearOverrides(String packageName) throws RemoteException, SecurityException {
+ checkCompatChangeOverridePermission();
mCompatConfig.removePackageOverrides(packageName);
killPackage(packageName);
}
@@ -159,12 +173,14 @@
@Override
public void clearOverridesForTest(String packageName)
throws RemoteException, SecurityException {
+ checkCompatChangeOverridePermission();
mCompatConfig.removePackageOverrides(packageName);
}
@Override
public boolean clearOverride(long changeId, String packageName)
throws RemoteException, SecurityException {
+ checkCompatChangeOverridePermission();
boolean existed = mCompatConfig.removeOverride(changeId, packageName);
killPackage(packageName);
return existed;
@@ -172,11 +188,13 @@
@Override
public CompatibilityChangeConfig getAppConfig(ApplicationInfo appInfo) {
+ checkCompatChangeReadAndLogPermission();
return mCompatConfig.getAppConfig(appInfo);
}
@Override
public CompatibilityChangeInfo[] listAllChanges() {
+ checkCompatChangeReadPermission();
return mCompatConfig.dumpChanges();
}
@@ -215,6 +233,7 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ checkCompatChangeReadAndLogPermission();
if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, "platform_compat", pw)) return;
mCompatConfig.dumpConfig(pw);
}
@@ -272,4 +291,30 @@
Binder.restoreCallingIdentity(identity);
}
}
+
+ private void checkCompatChangeLogPermission() throws SecurityException {
+ if (mContext.checkCallingOrSelfPermission(LOG_COMPAT_CHANGE)
+ != PERMISSION_GRANTED) {
+ throw new SecurityException("Cannot log compat change usage");
+ }
+ }
+
+ private void checkCompatChangeReadPermission() throws SecurityException {
+ if (mContext.checkCallingOrSelfPermission(READ_COMPAT_CHANGE_CONFIG)
+ != PERMISSION_GRANTED) {
+ throw new SecurityException("Cannot read compat change");
+ }
+ }
+
+ private void checkCompatChangeOverridePermission() throws SecurityException {
+ if (mContext.checkCallingOrSelfPermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
+ != PERMISSION_GRANTED) {
+ throw new SecurityException("Cannot override compat change");
+ }
+ }
+
+ private void checkCompatChangeReadAndLogPermission() throws SecurityException {
+ checkCompatChangeReadPermission();
+ checkCompatChangeLogPermission();
+ }
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index a97a5b0..0cf8b42 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -558,10 +558,22 @@
mStagedSessionErrorMessage =
stagedSessionErrorMessage != null ? stagedSessionErrorMessage : "";
- if (isStreamingInstallation()
- && this.params.dataLoaderParams.getComponentName().getPackageName()
- == SYSTEM_DATA_LOADER_PACKAGE) {
- assertShellOrSystemCalling("System data loaders");
+ if (isDataLoaderInstallation()) {
+ if (isApexInstallation()) {
+ throw new IllegalArgumentException(
+ "DataLoader installation of APEX modules is not allowed.");
+ }
+ }
+
+ if (isStreamingInstallation()) {
+ if (!isIncrementalInstallationAllowed(mPackageName)) {
+ throw new IllegalArgumentException(
+ "Incremental installation of this package is not allowed.");
+ }
+ if (this.params.dataLoaderParams.getComponentName().getPackageName()
+ == SYSTEM_DATA_LOADER_PACKAGE) {
+ assertShellOrSystemCalling("System data loaders");
+ }
}
}
@@ -1174,6 +1186,19 @@
}
/**
+ * Checks if the package can be installed on IncFs.
+ */
+ private static boolean isIncrementalInstallationAllowed(String packageName) {
+ final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+ final AndroidPackage existingPackage = pmi.getPackage(packageName);
+ if (existingPackage == null) {
+ return true;
+ }
+
+ return !PackageManagerService.isSystemApp(existingPackage);
+ }
+
+ /**
* If this was not already called, the session will be sealed.
*
* This method may be called multiple times to update the status receiver validate caller
@@ -1362,14 +1387,10 @@
return false;
}
- final PackageInfo pkgInfo = mPm.getPackageInfo(
- params.appPackageName, PackageManager.GET_SIGNATURES
- | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId);
-
if (isApexInstallation()) {
validateApexInstallLocked();
} else {
- validateApkInstallLocked(pkgInfo);
+ validateApkInstallLocked();
}
}
@@ -1786,8 +1807,7 @@
* {@link PackageManagerService}.
*/
@GuardedBy("mLock")
- private void validateApkInstallLocked(@Nullable PackageInfo pkgInfo)
- throws PackageManagerException {
+ private void validateApkInstallLocked() throws PackageManagerException {
ApkLite baseApk = null;
mPackageName = null;
mVersionCode = -1;
@@ -1797,6 +1817,10 @@
mResolvedStagedFiles.clear();
mResolvedInheritedFiles.clear();
+ final PackageInfo pkgInfo = mPm.getPackageInfo(
+ params.appPackageName, PackageManager.GET_SIGNATURES
+ | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId);
+
// Partial installs must be consistent with existing install
if (params.mode == SessionParams.MODE_INHERIT_EXISTING
&& (pkgInfo == null || pkgInfo.applicationInfo == null)) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index e4ec374..c8585907 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -150,6 +150,7 @@
import android.content.pm.AuxiliaryResolveInfo;
import android.content.pm.ChangedPackages;
import android.content.pm.ComponentInfo;
+import android.content.pm.DataLoaderType;
import android.content.pm.FallbackCategoryProvider;
import android.content.pm.FeatureInfo;
import android.content.pm.IDexModuleRegisterCallback;
@@ -1658,7 +1659,8 @@
handlePackagePostInstall(parentRes, grantPermissions,
killApp, virtualPreload, grantedPermissions,
whitelistedRestrictedPermissions, didRestore,
- args.installSource.installerPackageName, args.observer);
+ args.installSource.installerPackageName, args.observer,
+ args.mDataLoaderType);
// Handle the child packages
final int childCount = (parentRes.addedChildPackages != null)
@@ -1668,7 +1670,8 @@
handlePackagePostInstall(childRes, grantPermissions,
killApp, virtualPreload, grantedPermissions,
whitelistedRestrictedPermissions, false /*didRestore*/,
- args.installSource.installerPackageName, args.observer);
+ args.installSource.installerPackageName, args.observer,
+ args.mDataLoaderType);
}
// Log tracing if needed
@@ -1995,7 +1998,7 @@
boolean killApp, boolean virtualPreload,
String[] grantedPermissions, List<String> whitelistedRestrictedPermissions,
boolean launchedForRestore, String installerPackage,
- IPackageInstallObserver2 installObserver) {
+ IPackageInstallObserver2 installObserver, int dataLoaderType) {
final boolean succeeded = res.returnCode == PackageManager.INSTALL_SUCCEEDED;
final boolean update = res.removedInfo != null && res.removedInfo.removedPackage != null;
@@ -2096,11 +2099,14 @@
if (update) {
extras.putBoolean(Intent.EXTRA_REPLACING, true);
}
+ extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, dataLoaderType);
+ // Send to all running apps.
sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
extras, 0 /*flags*/,
null /*targetPackage*/, null /*finishedReceiver*/,
updateUserIds, instantUserIds);
if (installerPackageName != null) {
+ // Send to the installer, even if it's not running.
sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
extras, 0 /*flags*/,
installerPackageName, null /*finishedReceiver*/,
@@ -14117,13 +14123,14 @@
MultiPackageInstallParams mParentInstallParams;
final long requiredInstalledVersionCode;
final boolean forceQueryableOverride;
+ final int mDataLoaderType;
InstallParams(OriginInfo origin, MoveInfo move, IPackageInstallObserver2 observer,
int installFlags, InstallSource installSource, String volumeUuid,
VerificationInfo verificationInfo, UserHandle user, String packageAbiOverride,
String[] grantedPermissions, List<String> whitelistedRestrictedPermissions,
SigningDetails signingDetails, int installReason,
- long requiredInstalledVersionCode) {
+ long requiredInstalledVersionCode, int dataLoaderType) {
super(user);
this.origin = origin;
this.move = move;
@@ -14139,40 +14146,42 @@
this.installReason = installReason;
this.requiredInstalledVersionCode = requiredInstalledVersionCode;
this.forceQueryableOverride = false;
+ this.mDataLoaderType = dataLoaderType;
}
InstallParams(ActiveInstallSession activeInstallSession) {
super(activeInstallSession.getUser());
+ final PackageInstaller.SessionParams sessionParams =
+ activeInstallSession.getSessionParams();
if (DEBUG_INSTANT) {
- if ((activeInstallSession.getSessionParams().installFlags
+ if ((sessionParams.installFlags
& PackageManager.INSTALL_INSTANT_APP) != 0) {
Slog.d(TAG, "Ephemeral install of " + activeInstallSession.getPackageName());
}
}
verificationInfo = new VerificationInfo(
- activeInstallSession.getSessionParams().originatingUri,
- activeInstallSession.getSessionParams().referrerUri,
- activeInstallSession.getSessionParams().originatingUid,
+ sessionParams.originatingUri,
+ sessionParams.referrerUri,
+ sessionParams.originatingUid,
activeInstallSession.getInstallerUid());
origin = OriginInfo.fromStagedFile(activeInstallSession.getStagedDir());
move = null;
installReason = fixUpInstallReason(
activeInstallSession.getInstallSource().installerPackageName,
activeInstallSession.getInstallerUid(),
- activeInstallSession.getSessionParams().installReason);
+ sessionParams.installReason);
observer = activeInstallSession.getObserver();
- installFlags = activeInstallSession.getSessionParams().installFlags;
+ installFlags = sessionParams.installFlags;
installSource = activeInstallSession.getInstallSource();
- volumeUuid = activeInstallSession.getSessionParams().volumeUuid;
- packageAbiOverride = activeInstallSession.getSessionParams().abiOverride;
- grantedRuntimePermissions = activeInstallSession.getSessionParams()
- .grantedRuntimePermissions;
- whitelistedRestrictedPermissions = activeInstallSession.getSessionParams()
- .whitelistedRestrictedPermissions;
+ volumeUuid = sessionParams.volumeUuid;
+ packageAbiOverride = sessionParams.abiOverride;
+ grantedRuntimePermissions = sessionParams.grantedRuntimePermissions;
+ whitelistedRestrictedPermissions = sessionParams.whitelistedRestrictedPermissions;
signingDetails = activeInstallSession.getSigningDetails();
- requiredInstalledVersionCode = activeInstallSession.getSessionParams()
- .requiredInstalledVersionCode;
- forceQueryableOverride = activeInstallSession.getSessionParams().forceQueryableOverride;
+ requiredInstalledVersionCode = sessionParams.requiredInstalledVersionCode;
+ forceQueryableOverride = sessionParams.forceQueryableOverride;
+ mDataLoaderType = (sessionParams.dataLoaderParams != null)
+ ? sessionParams.dataLoaderParams.getType() : DataLoaderType.NONE;
}
@Override
@@ -14775,6 +14784,7 @@
final int installReason;
final boolean forceQueryableOverride;
@Nullable final MultiPackageInstallParams mMultiPackageInstallParams;
+ final int mDataLoaderType;
// The list of instruction sets supported by this app. This is currently
// only used during the rmdex() phase to clean up resources. We can get rid of this
@@ -14788,7 +14798,7 @@
List<String> whitelistedRestrictedPermissions,
String traceMethod, int traceCookie, SigningDetails signingDetails,
int installReason, boolean forceQueryableOverride,
- MultiPackageInstallParams multiPackageInstallParams) {
+ MultiPackageInstallParams multiPackageInstallParams, int dataLoaderType) {
this.origin = origin;
this.move = move;
this.installFlags = installFlags;
@@ -14806,6 +14816,7 @@
this.installReason = installReason;
this.forceQueryableOverride = forceQueryableOverride;
this.mMultiPackageInstallParams = multiPackageInstallParams;
+ this.mDataLoaderType = dataLoaderType;
}
/** New install */
@@ -14815,7 +14826,8 @@
params.getUser(), null /*instructionSets*/, params.packageAbiOverride,
params.grantedRuntimePermissions, params.whitelistedRestrictedPermissions,
params.traceMethod, params.traceCookie, params.signingDetails,
- params.installReason, params.forceQueryableOverride, params.mParentInstallParams);
+ params.installReason, params.forceQueryableOverride,
+ params.mParentInstallParams, params.mDataLoaderType);
}
abstract int copyApk();
@@ -14906,7 +14918,8 @@
super(OriginInfo.fromNothing(), null, null, 0, InstallSource.EMPTY,
null, null, instructionSets, null, null, null, null, 0,
PackageParser.SigningDetails.UNKNOWN,
- PackageManager.INSTALL_REASON_UNKNOWN, false, null /* parent */);
+ PackageManager.INSTALL_REASON_UNKNOWN, false, null /* parent */,
+ DataLoaderType.NONE);
this.codeFile = (codePath != null) ? new File(codePath) : null;
this.resourceFile = (resourcePath != null) ? new File(resourcePath) : null;
}
@@ -14986,9 +14999,7 @@
try {
makeDirRecursive(afterCodeFile.getParentFile(), 0775);
if (onIncremental) {
- // TODO(b/147371381): fix incremental installation
- mIncrementalManager.rename(beforeCodeFile.getAbsolutePath(),
- afterCodeFile.getAbsolutePath());
+ mIncrementalManager.renameCodePath(beforeCodeFile, afterCodeFile);
} else {
Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath());
}
@@ -22257,7 +22268,8 @@
installSource, volumeUuid, null /*verificationInfo*/, user,
packageAbiOverride, null /*grantedPermissions*/,
null /*whitelistedRestrictedPermissions*/, PackageParser.SigningDetails.UNKNOWN,
- PackageManager.INSTALL_REASON_UNKNOWN, PackageManager.VERSION_CODE_HIGHEST);
+ PackageManager.INSTALL_REASON_UNKNOWN, PackageManager.VERSION_CODE_HIGHEST,
+ DataLoaderType.NONE);
params.setTraceMethod("movePackage").setTraceCookie(System.identityHashCode(params));
msg.obj = params;
diff --git a/services/core/java/com/android/server/policy/LegacyGlobalActions.java b/services/core/java/com/android/server/policy/LegacyGlobalActions.java
index 6daf516..6eba59a 100644
--- a/services/core/java/com/android/server/policy/LegacyGlobalActions.java
+++ b/services/core/java/com/android/server/policy/LegacyGlobalActions.java
@@ -402,7 +402,7 @@
public String getStatus() {
return mContext.getString(
com.android.internal.R.string.bugreport_status,
- Build.VERSION.RELEASE,
+ Build.VERSION.RELEASE_OR_CODENAME,
Build.ID);
}
}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 96f1219..5c79f6e 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -2480,7 +2480,7 @@
.writeString(Build.BRAND)
.writeString(Build.PRODUCT)
.writeString(Build.DEVICE)
- .writeString(Build.VERSION.RELEASE)
+ .writeString(Build.VERSION.RELEASE_OR_CODENAME)
.writeString(Build.ID)
.writeString(Build.VERSION.INCREMENTAL)
.writeString(Build.TYPE)
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 75d87ed..f35ba9e 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -386,6 +386,8 @@
} else {
callingPid = callingUid = -1;
}
+ final int filterCallingUid = ActivityStarter.computeResolveFilterUid(
+ callingUid, realCallingUid, UserHandle.USER_NULL);
final SparseArray<String> startingUidPkgs = new SparseArray<>();
final long origId = Binder.clearCallingIdentity();
try {
@@ -408,9 +410,7 @@
// Collect information about the target of the Intent.
ActivityInfo aInfo = mSupervisor.resolveActivity(intent, resolvedTypes[i],
- 0 /* startFlags */, null /* profilerInfo */, userId,
- ActivityStarter.computeResolveFilterUid(
- callingUid, realCallingUid, UserHandle.USER_NULL));
+ 0 /* startFlags */, null /* profilerInfo */, userId, filterCallingUid);
aInfo = mService.mAmInternal.getActivityInfoForUser(aInfo, userId);
if (aInfo != null) {
@@ -457,6 +457,7 @@
Slog.wtf(TAG, sb.toString());
}
+ final IBinder sourceResultTo = resultTo;
final ActivityRecord[] outActivity = new ActivityRecord[1];
// Lock the loop to ensure the activities launched in a sequence.
synchronized (mService.mGlobalLock) {
@@ -470,7 +471,18 @@
}
return startResult;
}
- resultTo = outActivity[0] != null ? outActivity[0].appToken : null;
+ final ActivityRecord started = outActivity[0];
+ if (started != null && started.getUid() == filterCallingUid) {
+ // Only the started activity which has the same uid as the source caller can
+ // be the caller of next activity.
+ resultTo = started.appToken;
+ } else {
+ resultTo = sourceResultTo;
+ // Different apps not adjacent to the caller are forced to be new task.
+ if (i < starters.length - 1) {
+ starters[i + 1].getIntent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ }
}
}
} finally {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 9e3292b..c7270f2 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2493,7 +2493,6 @@
return this;
}
- @VisibleForTesting
Intent getIntent() {
return mRequest.intent;
}
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 3d9f11f..339ff6b 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -22,6 +22,7 @@
"services.net",
"service-jobscheduler",
"service-permission",
+ "service-blobstore",
"androidx.test.runner",
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml
index 0e24b03..44eb828 100644
--- a/services/tests/mockingservicestests/AndroidManifest.xml
+++ b/services/tests/mockingservicestests/AndroidManifest.xml
@@ -17,6 +17,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.frameworks.mockingservicestests">
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<uses-permission android:name="android.permission.HARDWARE_TEST"/>
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
new file mode 100644
index 0000000..16dde42
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2020 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.blob;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.app.blob.BlobHandle;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArrayMap;
+import android.util.LongSparseArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.blob.BlobStoreManagerService.Injector;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class BlobStoreManagerServiceTest {
+ private Context mContext;
+ private Handler mHandler;
+ private BlobStoreManagerService mService;
+
+ private MockitoSession mMockitoSession;
+
+ @Mock
+ private File mBlobsDir;
+
+ private LongSparseArray<BlobStoreSession> mUserSessions;
+ private ArrayMap<BlobHandle, BlobMetadata> mUserBlobs;
+
+ private static final String TEST_PKG1 = "com.example1";
+ private static final String TEST_PKG2 = "com.example2";
+ private static final String TEST_PKG3 = "com.example3";
+
+ private static final int TEST_UID1 = 10001;
+ private static final int TEST_UID2 = 10002;
+ private static final int TEST_UID3 = 10003;
+
+ @Before
+ public void setUp() {
+ // Share classloader to allow package private access.
+ System.setProperty("dexmaker.share_classloader", "true");
+
+ mMockitoSession = mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .mockStatic(BlobStoreConfig.class)
+ .startMocking();
+
+ doReturn(mBlobsDir).when(() -> BlobStoreConfig.getBlobsDir());
+ doReturn(true).when(mBlobsDir).exists();
+ doReturn(new File[0]).when(mBlobsDir).listFiles();
+
+ mContext = InstrumentationRegistry.getTargetContext();
+ mHandler = new TestHandler(Looper.getMainLooper());
+ mService = new BlobStoreManagerService(mContext, new TestInjector());
+ mUserSessions = new LongSparseArray<>();
+ mUserBlobs = new ArrayMap<>();
+
+ mService.addUserSessionsForTest(mUserSessions, UserHandle.myUserId());
+ mService.addUserBlobsForTest(mUserBlobs, UserHandle.myUserId());
+ }
+
+ @After
+ public void tearDown() {
+ if (mMockitoSession != null) {
+ mMockitoSession.finishMocking();
+ }
+ }
+
+ @Test
+ public void testHandlePackageRemoved() throws Exception {
+ // Setup sessions
+ final File sessionFile1 = mock(File.class);
+ final long sessionId1 = 11;
+ final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
+ sessionId1, sessionFile1);
+ mUserSessions.append(sessionId1, session1);
+
+ final File sessionFile2 = mock(File.class);
+ final long sessionId2 = 25;
+ final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
+ sessionId2, sessionFile2);
+ mUserSessions.append(sessionId2, session2);
+
+ final File sessionFile3 = mock(File.class);
+ final long sessionId3 = 37;
+ final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
+ sessionId3, sessionFile3);
+ mUserSessions.append(sessionId3, session3);
+
+ final File sessionFile4 = mock(File.class);
+ final long sessionId4 = 48;
+ final BlobStoreSession session4 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
+ sessionId4, sessionFile4);
+ mUserSessions.append(sessionId4, session4);
+
+ // Setup blobs
+ final long blobId1 = 978;
+ final File blobFile1 = mock(File.class);
+ final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
+ "label1", System.currentTimeMillis(), "tag1");
+ final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobId1, blobFile1, true);
+ mUserBlobs.put(blobHandle1, blobMetadata1);
+
+ final long blobId2 = 347;
+ final File blobFile2 = mock(File.class);
+ final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
+ "label2", System.currentTimeMillis(), "tag2");
+ final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobId2, blobFile2, false);
+ mUserBlobs.put(blobHandle2, blobMetadata2);
+
+ mService.addKnownIdsForTest(sessionId1, sessionId2, sessionId3, sessionId4,
+ blobId1, blobId2);
+
+ // Invoke test method
+ mService.handlePackageRemoved(TEST_PKG1, TEST_UID1);
+
+ // Verify sessions are removed
+ verify(sessionFile1).delete();
+ verify(sessionFile2, never()).delete();
+ verify(sessionFile3, never()).delete();
+ verify(sessionFile4).delete();
+
+ assertThat(mUserSessions.size()).isEqualTo(2);
+ assertThat(mUserSessions.get(sessionId1)).isNull();
+ assertThat(mUserSessions.get(sessionId2)).isNotNull();
+ assertThat(mUserSessions.get(sessionId3)).isNotNull();
+ assertThat(mUserSessions.get(sessionId4)).isNull();
+
+ // Verify blobs are removed
+ verify(blobMetadata1).removeCommitter(TEST_PKG1, TEST_UID1);
+ verify(blobMetadata1).removeLeasee(TEST_PKG1, TEST_UID1);
+ verify(blobMetadata2).removeCommitter(TEST_PKG1, TEST_UID1);
+ verify(blobMetadata2).removeLeasee(TEST_PKG1, TEST_UID1);
+
+ verify(blobFile1, never()).delete();
+ verify(blobFile2).delete();
+
+ assertThat(mUserBlobs.size()).isEqualTo(1);
+ assertThat(mUserBlobs.get(blobHandle1)).isNotNull();
+ assertThat(mUserBlobs.get(blobHandle2)).isNull();
+
+ assertThat(mService.getKnownIdsForTest()).containsExactly(
+ sessionId2, sessionId3, blobId1);
+ }
+
+ @Test
+ public void testHandleIdleMaintenance_deleteUnknownBlobs() throws Exception {
+ // Setup blob files
+ final long testId1 = 286;
+ final File file1 = mock(File.class);
+ doReturn(String.valueOf(testId1)).when(file1).getName();
+ final long testId2 = 349;
+ final File file2 = mock(File.class);
+ doReturn(String.valueOf(testId2)).when(file2).getName();
+ final long testId3 = 7355;
+ final File file3 = mock(File.class);
+ doReturn(String.valueOf(testId3)).when(file3).getName();
+
+ doReturn(new File[] {file1, file2, file3}).when(mBlobsDir).listFiles();
+ mService.addKnownIdsForTest(testId1, testId3);
+
+ // Invoke test method
+ mService.handleIdleMaintenanceLocked();
+
+ // Verify unknown blobs are delete
+ verify(file1, never()).delete();
+ verify(file2).delete();
+ verify(file3, never()).delete();
+ }
+
+ @Test
+ public void testHandleIdleMaintenance_deleteStaleSessions() throws Exception {
+ // Setup sessions
+ final File sessionFile1 = mock(File.class);
+ doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS + 1000)
+ .when(sessionFile1).lastModified();
+ final long sessionId1 = 342;
+ final BlobHandle blobHandle1 = mock(BlobHandle.class);
+ doReturn(System.currentTimeMillis() - 1000).when(blobHandle1).getExpiryTimeMillis();
+ final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
+ sessionId1, sessionFile1, blobHandle1);
+ mUserSessions.append(sessionId1, session1);
+
+ final File sessionFile2 = mock(File.class);
+ doReturn(System.currentTimeMillis() - 20000)
+ .when(sessionFile2).lastModified();
+ final long sessionId2 = 4597;
+ final BlobHandle blobHandle2 = mock(BlobHandle.class);
+ doReturn(System.currentTimeMillis() + 20000).when(blobHandle2).getExpiryTimeMillis();
+ final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
+ sessionId2, sessionFile2, blobHandle2);
+ mUserSessions.append(sessionId2, session2);
+
+ final File sessionFile3 = mock(File.class);
+ doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS - 2000)
+ .when(sessionFile3).lastModified();
+ final long sessionId3 = 9484;
+ final BlobHandle blobHandle3 = mock(BlobHandle.class);
+ doReturn(System.currentTimeMillis() + 30000).when(blobHandle3).getExpiryTimeMillis();
+ final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
+ sessionId3, sessionFile3, blobHandle3);
+ mUserSessions.append(sessionId3, session3);
+
+ mService.addKnownIdsForTest(sessionId1, sessionId2, sessionId3);
+
+ // Invoke test method
+ mService.handleIdleMaintenanceLocked();
+
+ // Verify stale sessions are removed
+ verify(sessionFile1).delete();
+ verify(sessionFile2, never()).delete();
+ verify(sessionFile3).delete();
+
+ assertThat(mUserSessions.size()).isEqualTo(1);
+ assertThat(mUserSessions.get(sessionId2)).isNotNull();
+
+ assertThat(mService.getKnownIdsForTest()).containsExactly(sessionId2);
+ }
+
+ @Test
+ public void testHandleIdleMaintenance_deleteStaleBlobs() throws Exception {
+ // Setup blobs
+ final long blobId1 = 3489;
+ final File blobFile1 = mock(File.class);
+ final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
+ "label1", System.currentTimeMillis() - 2000, "tag1");
+ final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobId1, blobFile1, true);
+ mUserBlobs.put(blobHandle1, blobMetadata1);
+
+ final long blobId2 = 78974;
+ final File blobFile2 = mock(File.class);
+ final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
+ "label2", System.currentTimeMillis() + 30000, "tag2");
+ final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobId2, blobFile2, true);
+ mUserBlobs.put(blobHandle2, blobMetadata2);
+
+ final long blobId3 = 97;
+ final File blobFile3 = mock(File.class);
+ final BlobHandle blobHandle3 = BlobHandle.createWithSha256("digest3".getBytes(),
+ "label3", System.currentTimeMillis() + 4400000, "tag3");
+ final BlobMetadata blobMetadata3 = createBlobMetadataMock(blobId3, blobFile3, false);
+ mUserBlobs.put(blobHandle3, blobMetadata3);
+
+ mService.addKnownIdsForTest(blobId1, blobId2, blobId3);
+
+ // Invoke test method
+ mService.handleIdleMaintenanceLocked();
+
+ // Verify stale blobs are removed
+ verify(blobFile1).delete();
+ verify(blobFile2, never()).delete();
+ verify(blobFile3).delete();
+
+ assertThat(mUserBlobs.size()).isEqualTo(1);
+ assertThat(mUserBlobs.get(blobHandle2)).isNotNull();
+
+ assertThat(mService.getKnownIdsForTest()).containsExactly(blobId2);
+ }
+
+ private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
+ long sessionId, File sessionFile) {
+ return createBlobStoreSessionMock(ownerPackageName, ownerUid, sessionId, sessionFile,
+ mock(BlobHandle.class));
+ }
+ private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
+ long sessionId, File sessionFile, BlobHandle blobHandle) {
+ final BlobStoreSession session = mock(BlobStoreSession.class);
+ doReturn(ownerPackageName).when(session).getOwnerPackageName();
+ doReturn(ownerUid).when(session).getOwnerUid();
+ doReturn(sessionId).when(session).getSessionId();
+ doReturn(sessionFile).when(session).getSessionFile();
+ doReturn(blobHandle).when(session).getBlobHandle();
+ return session;
+ }
+
+ private BlobMetadata createBlobMetadataMock(long blobId, File blobFile, boolean hasLeases) {
+ final BlobMetadata blobMetadata = mock(BlobMetadata.class);
+ doReturn(blobId).when(blobMetadata).getBlobId();
+ doReturn(blobFile).when(blobMetadata).getBlobFile();
+ doReturn(hasLeases).when(blobMetadata).hasLeases();
+ return blobMetadata;
+ }
+
+ private class TestHandler extends Handler {
+ TestHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void dispatchMessage(Message msg) {
+ // Ignore all messages
+ }
+ }
+
+ private class TestInjector extends Injector {
+ @Override
+ public Handler initializeMessageHandler() {
+ return mHandler;
+ }
+ }
+}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 8381205..f990810 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -46,7 +46,6 @@
"service-appsearch",
"service-jobscheduler",
"service-permission",
- "service-blobstore",
// TODO: remove once Android migrates to JUnit 4.12,
// which provides assertThrows
"testng",
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index d2ddff3..1212f20 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -65,6 +65,8 @@
<uses-permission android:name="android.permission.WATCH_APPOPS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.SUSPEND_APPS"/>
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<uses-permission android:name="android.permission.CONTROL_KEYGUARD"/>
<uses-permission android:name="android.permission.MANAGE_BIND_INSTANT_SERVICE"/>
<uses-permission android:name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" />
diff --git a/services/tests/servicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
deleted file mode 100644
index ff728e7..0000000
--- a/services/tests/servicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright 2020 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.blob;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.blob.BlobHandle;
-import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.UserHandle;
-import android.platform.test.annotations.Presubmit;
-import android.util.ArrayMap;
-import android.util.LongSparseArray;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.blob.BlobStoreManagerService.Injector;
-import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@Presubmit
-public class BlobStoreManagerServiceTest {
- private Context mContext;
- private Handler mHandler;
- private BlobStoreManagerService mService;
-
- private LongSparseArray<BlobStoreSession> mUserSessions;
- private ArrayMap<BlobHandle, BlobMetadata> mUserBlobs;
-
- private SessionStateChangeListener mStateChangeListener;
-
- private static final String TEST_PKG1 = "com.example1";
- private static final String TEST_PKG2 = "com.example2";
- private static final String TEST_PKG3 = "com.example3";
-
- private static final int TEST_UID1 = 10001;
- private static final int TEST_UID2 = 10002;
- private static final int TEST_UID3 = 10003;
-
- @Before
- public void setUp() {
- // Share classloader to allow package private access.
- System.setProperty("dexmaker.share_classloader", "true");
-
- mContext = InstrumentationRegistry.getTargetContext();
- mHandler = new TestHandler(Looper.getMainLooper());
- mService = new BlobStoreManagerService(mContext, new TestInjector());
- mUserSessions = new LongSparseArray<>();
- mUserBlobs = new ArrayMap<>();
-
- mService.addUserSessionsForTest(mUserSessions, UserHandle.myUserId());
- mService.addUserBlobsForTest(mUserBlobs, UserHandle.myUserId());
-
- mStateChangeListener = mService.new SessionStateChangeListener();
- }
-
- @Test
- public void testHandlePackageRemoved() throws Exception {
- // Setup sessions
- final File sessionFile1 = mock(File.class);
- final long sessionId1 = 11;
- final BlobStoreSession session1 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
- sessionId1, sessionFile1);
- mUserSessions.append(sessionId1, session1);
-
- final File sessionFile2 = mock(File.class);
- final long sessionId2 = 25;
- final BlobStoreSession session2 = createBlobStoreSessionMock(TEST_PKG2, TEST_UID2,
- sessionId2, sessionFile2);
- mUserSessions.append(sessionId2, session2);
-
- final File sessionFile3 = mock(File.class);
- final long sessionId3 = 37;
- final BlobStoreSession session3 = createBlobStoreSessionMock(TEST_PKG3, TEST_UID3,
- sessionId3, sessionFile3);
- mUserSessions.append(sessionId3, session3);
-
- final File sessionFile4 = mock(File.class);
- final long sessionId4 = 48;
- final BlobStoreSession session4 = createBlobStoreSessionMock(TEST_PKG1, TEST_UID1,
- sessionId4, sessionFile4);
- mUserSessions.append(sessionId4, session4);
-
- // Setup blobs
- final File blobFile1 = mock(File.class);
- final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
- "label1", System.currentTimeMillis(), "tag1");
- final BlobMetadata blobMetadata1 = createBlobMetadataMock(blobFile1, true);
- mUserBlobs.put(blobHandle1, blobMetadata1);
-
- final File blobFile2 = mock(File.class);
- final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
- "label2", System.currentTimeMillis(), "tag2");
- final BlobMetadata blobMetadata2 = createBlobMetadataMock(blobFile2, false);
- mUserBlobs.put(blobHandle2, blobMetadata2);
-
- // Invoke test method
- mService.handlePackageRemoved(TEST_PKG1, TEST_UID1);
-
- // Verify sessions are removed
- verify(sessionFile1).delete();
- verify(sessionFile2, never()).delete();
- verify(sessionFile3, never()).delete();
- verify(sessionFile4).delete();
-
- assertThat(mUserSessions.size()).isEqualTo(2);
- assertThat(mUserSessions.get(sessionId1)).isNull();
- assertThat(mUserSessions.get(sessionId2)).isNotNull();
- assertThat(mUserSessions.get(sessionId3)).isNotNull();
- assertThat(mUserSessions.get(sessionId4)).isNull();
-
- // Verify blobs are removed
- verify(blobMetadata1).removeCommitter(TEST_PKG1, TEST_UID1);
- verify(blobMetadata1).removeLeasee(TEST_PKG1, TEST_UID1);
- verify(blobMetadata2).removeCommitter(TEST_PKG1, TEST_UID1);
- verify(blobMetadata2).removeLeasee(TEST_PKG1, TEST_UID1);
-
- verify(blobFile1, never()).delete();
- verify(blobFile2).delete();
-
- assertThat(mUserBlobs.size()).isEqualTo(1);
- assertThat(mUserBlobs.get(blobHandle1)).isNotNull();
- assertThat(mUserBlobs.get(blobHandle2)).isNull();
- }
-
- private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
- long sessionId, File sessionFile) {
- final BlobStoreSession session = mock(BlobStoreSession.class);
- when(session.getOwnerPackageName()).thenReturn(ownerPackageName);
- when(session.getOwnerUid()).thenReturn(ownerUid);
- when(session.getSessionId()).thenReturn(sessionId);
- when(session.getSessionFile()).thenReturn(sessionFile);
- return session;
- }
-
- private BlobMetadata createBlobMetadataMock(File blobFile, boolean hasLeases) {
- final BlobMetadata blobMetadata = mock(BlobMetadata.class);
- when(blobMetadata.getBlobFile()).thenReturn(blobFile);
- when(blobMetadata.hasLeases()).thenReturn(hasLeases);
- return blobMetadata;
- }
-
- private class TestHandler extends Handler {
- TestHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void dispatchMessage(Message msg) {
- // Ignore all messages
- }
- }
-
- private class TestInjector extends Injector {
- @Override
- public Handler initializeMessageHandler() {
- return mHandler;
- }
- }
-}
diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml
index 180deb5..dab0a5f 100644
--- a/services/tests/uiservicestests/AndroidManifest.xml
+++ b/services/tests/uiservicestests/AndroidManifest.xml
@@ -28,6 +28,8 @@
<uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" />
<uses-permission android:name="android.permission.DEVICE_POWER" />
<uses-permission android:name="android.permission.ACCESS_CONTENT_PROVIDERS_EXTERNALLY" />
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS" />
<uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT"/>
diff --git a/tests/BlobStoreTestUtils/Android.bp b/tests/BlobStoreTestUtils/Android.bp
new file mode 100644
index 0000000..edd2b43
--- /dev/null
+++ b/tests/BlobStoreTestUtils/Android.bp
@@ -0,0 +1,20 @@
+// Copyright (C) 2020 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.
+
+java_library {
+ name: "BlobStoreTestUtils",
+ srcs: ["src/**/*.java"],
+ static_libs: ["truth-prebuilt"],
+ platform_apis: true
+}
\ No newline at end of file
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
new file mode 100644
index 0000000..f96766a
--- /dev/null
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2020 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.utils.blob;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.blob.BlobHandle;
+import android.app.blob.BlobStoreManager;
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.security.MessageDigest;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+public class DummyBlobData {
+ private static final long DEFAULT_SIZE_BYTES = 10 * 1024L * 1024L;
+ private static final int BUFFER_SIZE_BYTES = 16 * 1024;
+
+ private final Context mContext;
+ private final Random mRandom;
+ private final File mFile;
+ private final long mFileSize;
+ private final String mLabel;
+
+ byte[] mFileDigest;
+ long mExpiryTimeMs;
+
+ public DummyBlobData(Context context) {
+ this(context, new Random(0), "blob_" + System.nanoTime());
+ }
+
+ public DummyBlobData(Context context, long fileSize) {
+ this(context, fileSize, new Random(0), "blob_" + System.nanoTime(), "Test label");
+ }
+
+ public DummyBlobData(Context context, Random random, String fileName) {
+ this(context, DEFAULT_SIZE_BYTES, random, fileName, "Test label");
+ }
+
+ public DummyBlobData(Context context, Random random, String fileName, String label) {
+ this(context, DEFAULT_SIZE_BYTES, random, fileName, label);
+ }
+
+ public DummyBlobData(Context context, long fileSize, Random random, String fileName,
+ String label) {
+ mContext = context;
+ mRandom = random;
+ mFile = new File(mContext.getFilesDir(), fileName);
+ mFileSize = fileSize;
+ mLabel = label;
+ }
+
+ public void prepare() throws Exception {
+ try (RandomAccessFile file = new RandomAccessFile(mFile, "rw")) {
+ writeRandomData(file, mFileSize);
+ }
+ mFileDigest = FileUtils.digest(mFile, "SHA-256");
+ mExpiryTimeMs = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
+ }
+
+ public BlobHandle getBlobHandle() throws Exception {
+ return BlobHandle.createWithSha256(createSha256Digest(mFile), mLabel,
+ mExpiryTimeMs, "test_tag");
+ }
+
+ public long getFileSize() throws Exception {
+ return mFileSize;
+ }
+
+ public long getExpiryTimeMillis() {
+ return mExpiryTimeMs;
+ }
+
+ public void delete() {
+ mFile.delete();
+ }
+
+ public void writeToSession(BlobStoreManager.Session session) throws Exception {
+ writeToSession(session, 0, mFileSize);
+ }
+
+ public void writeToSession(BlobStoreManager.Session session,
+ long offsetBytes, long lengthBytes) throws Exception {
+ try (FileInputStream in = new FileInputStream(mFile)) {
+ in.getChannel().position(offsetBytes);
+ try (FileOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
+ session.openWrite(offsetBytes, lengthBytes))) {
+ copy(in, out, lengthBytes);
+ }
+ }
+ }
+
+ public void writeToFd(FileDescriptor fd, long offsetBytes, long lengthBytes) throws Exception {
+ try (FileInputStream in = new FileInputStream(mFile)) {
+ in.getChannel().position(offsetBytes);
+ try (FileOutputStream out = new FileOutputStream(fd)) {
+ copy(in, out, lengthBytes);
+ }
+ }
+ }
+
+ private void copy(InputStream in, OutputStream out, long lengthBytes) throws Exception {
+ final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
+ long bytesWrittern = 0;
+ while (bytesWrittern < lengthBytes) {
+ final int toWrite = (bytesWrittern + buffer.length <= lengthBytes)
+ ? buffer.length : (int) (lengthBytes - bytesWrittern);
+ in.read(buffer, 0, toWrite);
+ out.write(buffer, 0, toWrite);
+ bytesWrittern += toWrite;
+ }
+ }
+
+ public void readFromSessionAndVerifyBytes(BlobStoreManager.Session session,
+ long offsetBytes, int lengthBytes) throws Exception {
+ final byte[] expectedBytes = new byte[lengthBytes];
+ try (FileInputStream in = new FileInputStream(mFile)) {
+ read(in, expectedBytes, offsetBytes, lengthBytes);
+ }
+
+ final byte[] actualBytes = new byte[lengthBytes];
+ try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
+ session.openWrite(0L, 0L))) {
+ read(in, actualBytes, offsetBytes, lengthBytes);
+ }
+
+ assertThat(actualBytes).isEqualTo(expectedBytes);
+
+ }
+
+ private void read(FileInputStream in, byte[] buffer,
+ long offsetBytes, int lengthBytes) throws Exception {
+ in.getChannel().position(offsetBytes);
+ in.read(buffer, 0, lengthBytes);
+ }
+
+ public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session)
+ throws Exception {
+ readFromSessionAndVerifyDigest(session, 0, mFile.length());
+ }
+
+ public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session,
+ long offsetBytes, long lengthBytes) throws Exception {
+ final byte[] actualDigest;
+ try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
+ session.openWrite(0L, 0L))) {
+ actualDigest = createSha256Digest(in, offsetBytes, lengthBytes);
+ }
+
+ assertThat(actualDigest).isEqualTo(mFileDigest);
+ }
+
+ public void verifyBlob(ParcelFileDescriptor pfd) throws Exception {
+ final byte[] actualDigest;
+ try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+ actualDigest = FileUtils.digest(in, "SHA-256");
+ }
+ assertThat(actualDigest).isEqualTo(mFileDigest);
+ }
+
+ private byte[] createSha256Digest(FileInputStream in, long offsetBytes, long lengthBytes)
+ throws Exception {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ in.getChannel().position(offsetBytes);
+ final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
+ long bytesRead = 0;
+ while (bytesRead < lengthBytes) {
+ int toRead = (bytesRead + buffer.length <= lengthBytes)
+ ? buffer.length : (int) (lengthBytes - bytesRead);
+ toRead = in.read(buffer, 0, toRead);
+ digest.update(buffer, 0, toRead);
+ bytesRead += toRead;
+ }
+ return digest.digest();
+ }
+
+ private byte[] createSha256Digest(File file) throws Exception {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ try (BufferedInputStream in = new BufferedInputStream(
+ Files.newInputStream(file.toPath()))) {
+ final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) > 0) {
+ digest.update(buffer, 0, bytesRead);
+ }
+ }
+ return digest.digest();
+ }
+
+ private void writeRandomData(RandomAccessFile file, long fileSize)
+ throws Exception {
+ long bytesWritten = 0;
+ final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
+ while (bytesWritten < fileSize) {
+ mRandom.nextBytes(buffer);
+ final int toWrite = (bytesWritten + buffer.length <= fileSize)
+ ? buffer.length : (int) (fileSize - bytesWritten);
+ file.seek(bytesWritten);
+ file.write(buffer, 0, toWrite);
+ bytesWritten += toWrite;
+ }
+ }
+}
diff --git a/tests/PlatformCompatGating/Android.bp b/tests/PlatformCompatGating/Android.bp
index 5e9ef8e..74dfde8 100644
--- a/tests/PlatformCompatGating/Android.bp
+++ b/tests/PlatformCompatGating/Android.bp
@@ -18,14 +18,11 @@
name: "PlatformCompatGating",
// Only compile source java files in this apk.
srcs: ["src/**/*.java"],
- certificate: "platform",
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
static_libs: [
"junit",
- "android-support-test",
+ "androidx.test.runner",
+ "androidx.test.core",
+ "androidx.test.ext.junit",
"mockito-target-minus-junit4",
"truth-prebuilt",
"platform-compat-test-rules"
diff --git a/tests/PlatformCompatGating/AndroidManifest.xml b/tests/PlatformCompatGating/AndroidManifest.xml
index 7f14b83..c24dc31 100644
--- a/tests/PlatformCompatGating/AndroidManifest.xml
+++ b/tests/PlatformCompatGating/AndroidManifest.xml
@@ -6,6 +6,6 @@
<uses-library android:name="android.test.runner" />
</application>
- <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.tests.gating"/>
</manifest>
diff --git a/tests/PlatformCompatGating/AndroidTest.xml b/tests/PlatformCompatGating/AndroidTest.xml
index c626848..0c7485b 100644
--- a/tests/PlatformCompatGating/AndroidTest.xml
+++ b/tests/PlatformCompatGating/AndroidTest.xml
@@ -24,7 +24,6 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="package" value="com.android.tests.gating"/>
- <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
<option name="hidden-api-checks" value="false"/>
</test>
</configuration>
diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatGatingTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatGatingTest.java
index dc317f19..c1ce0e9 100644
--- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatGatingTest.java
+++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatGatingTest.java
@@ -18,8 +18,9 @@
import static com.google.common.truth.Truth.assertThat;
import android.compat.testing.PlatformCompatChangeRule;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compat.testing.DummyApi;
@@ -81,14 +82,14 @@
@Test
@EnableCompatChanges({DummyApi.CHANGE_SYSTEM_SERVER})
public void testDummyGatingPositiveSystemServer() {
- assertThat(
- DummyApi.dummySystemServer(InstrumentationRegistry.getTargetContext())).isTrue();
+ assertThat(DummyApi.dummySystemServer(
+ InstrumentationRegistry.getInstrumentation().getTargetContext())).isTrue();
}
@Test
@DisableCompatChanges({DummyApi.CHANGE_SYSTEM_SERVER})
public void testDummyGatingNegativeSystemServer() {
- assertThat(
- DummyApi.dummySystemServer(InstrumentationRegistry.getTargetContext())).isFalse();
+ assertThat(DummyApi.dummySystemServer(
+ InstrumentationRegistry.getInstrumentation().getTargetContext())).isFalse();
}
}
diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
new file mode 100644
index 0000000..9b9e581
--- /dev/null
+++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2020 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.tests.gating;
+
+import static android.Manifest.permission.LOG_COMPAT_CHANGE;
+import static android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG;
+import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.compat.Compatibility.ChangeConfig;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.os.ServiceManager;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.compat.CompatibilityChangeConfig;
+import com.android.internal.compat.IPlatformCompat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public final class PlatformCompatPermissionsTest {
+
+ // private Context mContext;
+ private IPlatformCompat mPlatformCompat;
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+ private Context mContext;
+ private UiAutomation mUiAutomation;
+ private PackageManager mPackageManager;
+
+ @Before
+ public void setUp() {
+ // mContext;
+ mPlatformCompat = IPlatformCompat.Stub
+ .asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ mUiAutomation = instrumentation.getUiAutomation();
+ mContext = instrumentation.getTargetContext();
+
+ mPackageManager = mContext.getPackageManager();
+ }
+
+ @After
+ public void tearDown() {
+
+ mUiAutomation.dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void reportChange_noLogCompatChangePermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+ }
+
+ @Test
+ public void reportChange_logCompatChangePermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+ }
+
+ @Test
+ public void reportChangeByPackageName_noLogCompatChangePermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+ }
+
+ @Test
+ public void reportChangeByPackageName_logCompatChangePermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+ }
+
+ @Test
+ public void reportChangeByUid_noLogCompatChangePermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+
+ mPlatformCompat.reportChangeByUid(1, Process.myUid());
+ }
+
+ @Test
+ public void reportChangeByUid_logCompatChangePermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
+
+ mPlatformCompat.reportChangeByUid(1, Process.myUid());
+ }
+
+ @Test
+ public void isChangeEnabled_noReadCompatConfigPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+ }
+
+ @Test
+ public void isChangeEnabled_noLogCompatChangeConfigPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+ }
+
+ @Test
+ public void isChangeEnabled_readAndLogCompatChangeConfigPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+ }
+
+ @Test
+ public void isChangeEnabledByPackageName_noReadCompatConfigPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+ }
+
+ @Test
+ public void isChangeEnabledByPackageName_noLogompatConfigPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+ }
+
+ @Test
+ public void isChangeEnabledByPackageName_readAndLogCompatChangeConfigPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
+ final String packageName = mContext.getPackageName();
+
+ mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+ }
+
+ @Test
+ public void isChangeEnabledByUid_noReadCompatConfigPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+
+ mPlatformCompat.isChangeEnabledByUid(1, Process.myUid());
+ }
+
+ @Test
+ public void isChangeEnabledByUid_noLogCompatChangePermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
+
+ mPlatformCompat.isChangeEnabledByUid(1, Process.myUid());
+ }
+
+ @Test
+ public void isChangeEnabledByUid_readAndLogCompatChangeConfigPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
+
+ mPlatformCompat.isChangeEnabledByUid(1, Process.myUid());
+ }
+
+ @Test
+ public void setOverrides_noOverridesPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ Set<Long> enabled = new HashSet<>();
+ Set<Long> disabled = new HashSet<>();
+ ChangeConfig changeConfig = new ChangeConfig(enabled, disabled);
+ CompatibilityChangeConfig compatibilityChangeConfig =
+ new CompatibilityChangeConfig(changeConfig);
+
+ mPlatformCompat.setOverrides(compatibilityChangeConfig, "foo.bar");
+ }
+ @Test
+ public void setOverrides_overridesPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(OVERRIDE_COMPAT_CHANGE_CONFIG);
+ Set<Long> enabled = new HashSet<>();
+ Set<Long> disabled = new HashSet<>();
+ ChangeConfig changeConfig = new ChangeConfig(enabled, disabled);
+ CompatibilityChangeConfig compatibilityChangeConfig =
+ new CompatibilityChangeConfig(changeConfig);
+
+ mPlatformCompat.setOverrides(compatibilityChangeConfig, "foo.bar");
+ }
+
+ @Test
+ public void setOverridesForTest_noOverridesPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+ Set<Long> enabled = new HashSet<>();
+ Set<Long> disabled = new HashSet<>();
+ ChangeConfig changeConfig = new ChangeConfig(enabled, disabled);
+ CompatibilityChangeConfig compatibilityChangeConfig =
+ new CompatibilityChangeConfig(changeConfig);
+
+ mPlatformCompat.setOverridesForTest(compatibilityChangeConfig, "foo.bar");
+ }
+ @Test
+ public void setOverridesForTest_overridesPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(OVERRIDE_COMPAT_CHANGE_CONFIG);
+ Set<Long> enabled = new HashSet<>();
+ Set<Long> disabled = new HashSet<>();
+ ChangeConfig changeConfig = new ChangeConfig(enabled, disabled);
+ CompatibilityChangeConfig compatibilityChangeConfig =
+ new CompatibilityChangeConfig(changeConfig);
+
+ mPlatformCompat.setOverridesForTest(compatibilityChangeConfig, "foo.bar");
+ }
+
+ @Test
+ public void clearOverrides_noOverridesPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+
+ mPlatformCompat.clearOverrides("foo.bar");
+ }
+ @Test
+ public void clearOverrides_overridesPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(OVERRIDE_COMPAT_CHANGE_CONFIG);
+
+ mPlatformCompat.clearOverrides("foo.bar");
+ }
+
+ @Test
+ public void clearOverridesForTest_noOverridesPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+
+ mPlatformCompat.clearOverridesForTest("foo.bar");
+ }
+ @Test
+ public void clearOverridesForTest_overridesPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(OVERRIDE_COMPAT_CHANGE_CONFIG);
+
+ mPlatformCompat.clearOverridesForTest("foo.bar");
+ }
+
+ @Test
+ public void clearOverride_noOverridesPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+
+ mPlatformCompat.clearOverride(1, "foo.bar");
+ }
+ @Test
+ public void clearOverride_overridesPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(OVERRIDE_COMPAT_CHANGE_CONFIG);
+
+ mPlatformCompat.clearOverride(1, "foo.bar");
+ }
+
+ @Test
+ public void listAllChanges_noReadCompatConfigPermission_throwsSecurityException()
+ throws Throwable {
+ thrown.expect(SecurityException.class);
+
+ mPlatformCompat.listAllChanges();
+ }
+ @Test
+ public void listAllChanges_readCompatConfigPermission_noThrow()
+ throws Throwable {
+ mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
+
+ mPlatformCompat.listAllChanges();
+ }
+}
diff --git a/tests/PlatformCompatGating/test-rules/Android.bp b/tests/PlatformCompatGating/test-rules/Android.bp
index 8211ef5..10fa2dc 100644
--- a/tests/PlatformCompatGating/test-rules/Android.bp
+++ b/tests/PlatformCompatGating/test-rules/Android.bp
@@ -19,7 +19,7 @@
srcs: ["src/**/*.java"],
static_libs: [
"junit",
- "android-support-test",
+ "androidx.test.core",
"truth-prebuilt",
"core-compat-test-rules"
],
diff --git a/tests/PlatformCompatGating/test-rules/src/android/compat/testing/PlatformCompatChangeRule.java b/tests/PlatformCompatGating/test-rules/src/android/compat/testing/PlatformCompatChangeRule.java
index 932ec64..d6846fa 100644
--- a/tests/PlatformCompatGating/test-rules/src/android/compat/testing/PlatformCompatChangeRule.java
+++ b/tests/PlatformCompatGating/test-rules/src/android/compat/testing/PlatformCompatChangeRule.java
@@ -16,13 +16,17 @@
package android.compat.testing;
+import android.Manifest;
import android.app.Instrumentation;
+import android.app.UiAutomation;
import android.compat.Compatibility;
import android.compat.Compatibility.ChangeConfig;
import android.content.Context;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.support.test.InstrumentationRegistry;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.IPlatformCompat;
@@ -83,12 +87,17 @@
@Override
public void evaluate() throws Throwable {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ UiAutomation uiAutomation = instrumentation.getUiAutomation();
String packageName = instrumentation.getTargetContext().getPackageName();
IPlatformCompat platformCompat = IPlatformCompat.Stub
.asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
if (platformCompat == null) {
throw new IllegalStateException("Could not get IPlatformCompat service!");
}
+ uiAutomation.adoptShellPermissionIdentity(
+ Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
Compatibility.setOverrides(mConfig);
try {
platformCompat.setOverridesForTest(new CompatibilityChangeConfig(mConfig),
@@ -101,6 +110,7 @@
} catch (RemoteException e) {
throw new RuntimeException("Could not call IPlatformCompat binder method!", e);
} finally {
+ uiAutomation.dropShellPermissionIdentity();
Compatibility.clearOverrides();
}
}