diff options
| author | 2020-02-06 16:22:52 +0000 | |
|---|---|---|
| committer | 2020-02-06 16:22:52 +0000 | |
| commit | 36faba3e7b7c385e72b47cc18d1beb5b11cc3131 (patch) | |
| tree | 6c29c835cd8b2d992bf081e0c8f405100474a13a | |
| parent | 6521c7449358b09eb6cd3d3894b577803974c72c (diff) | |
| parent | d4ea5e142fdea04999b67ceaeaa4ad0992f26bf7 (diff) | |
Merge changes from topic "blob-perf-compute-digest"
* changes:
Add perf test to measure duration of blob store digest computation.
Add verbose logging that can be eanbled to debug issues.
Schedule a maintenance job to clean up stale jobs.
20 files changed, 1354 insertions, 214 deletions
diff --git a/apct-tests/perftests/blobstore/Android.bp b/apct-tests/perftests/blobstore/Android.bp new file mode 100644 index 000000000000..be5072ce3d9d --- /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 000000000000..21d0726927af --- /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 000000000000..19456c6d81d7 --- /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 000000000000..0208dab33746 --- /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 000000000000..a7b69c420c59 --- /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 cfe19a530b27..25a15371ae47 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 f110b36c7e90..d339afac5c77 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 @@ public final class BlobHandle implements Parcelable { 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 aba3e8cadfa3..c12e0ec8aec9 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 @@ class BlobMetadata { 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 @@ class BlobMetadata { 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 @@ class BlobMetadata { boolean hasLeases() { synchronized (mMetadataLock) { - return mLeasees.isEmpty(); + return !mLeasees.isEmpty(); } } @@ -196,7 +208,7 @@ class BlobMetadata { File getBlobFile() { if (mBlobFile == null) { - mBlobFile = BlobStoreConfig.getBlobFile(blobId); + mBlobFile = BlobStoreConfig.getBlobFile(mBlobId); } return mBlobFile; } @@ -244,7 +256,7 @@ class BlobMetadata { 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 @@ class BlobMetadata { 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 eb414b0f11a6..ba2e559afdab 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 @@ package com.android.server.blob; 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 @@ class BlobStoreConfig { 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 000000000000..460e776b9ff6 --- /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 000000000000..5358245f517f --- /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 13f095e5a503..0ba34cab6560 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.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES; 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.SystemClock; 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.IOException; 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 @@ public class BlobStoreManagerService extends SystemService { @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 @@ public class BlobStoreManagerService extends SystemService { @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 @@ public class BlobStoreManagerService extends SystemService { readBlobSessionsLocked(allPackages); readBlobsInfoLocked(allPackages); } + } else if (phase == PHASE_BOOT_COMPLETED) { + BlobStoreIdleJobService.schedule(mContext); } } @@ -215,6 +227,40 @@ public class BlobStoreManagerService extends SystemService { } } + @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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { } blobMetadata.addLeasee(callingPackage, callingUid, descriptionResId, leaseExpiryTimeMillis); + if (LOGV) { + Slog.v(TAG, "Acquired lease on " + blobHandle + + "; callingUid=" + callingUid + ", callingPackage=" + callingPackage); + } writeBlobsInfoAsync(); } } @@ -301,6 +358,10 @@ public class BlobStoreManagerService extends SystemService { + "; 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { } 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { if (session.getOwnerUid() == uid && session.getOwnerPackageName().equals(packageName)) { session.getSessionFile().delete(); + mKnownBlobIds.remove(session.getSessionId()); indicesToRemove.add(i); } } @@ -633,6 +714,7 @@ public class BlobStoreManagerService extends SystemService { // 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 @@ public class BlobStoreManagerService extends SystemService { userBlobs.removeAt(indicesToRemove.get(i)); } writeBlobsInfoAsync(); + if (LOGV) { + Slog.v(TAG, "Removed blobs data associated with pkg=" + + packageName + ", uid=" + uid); + } } } @@ -651,6 +737,7 @@ public class BlobStoreManagerService extends SystemService { for (int i = 0, count = userSessions.size(); i < count; ++i) { final BlobStoreSession session = userSessions.valueAt(i); session.getSessionFile().delete(); + mKnownBlobIds.remove(session.getSessionId()); } } @@ -660,9 +747,105 @@ public class BlobStoreManagerService extends SystemService { 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) { @@ -727,10 +910,10 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { || mDumpUserIds.indexOf(userId) >= 0; } + public boolean shouldDumpHelp() { + return mDumpHelp; + } + private DumpArgs() {} public static DumpArgs parse(String[] args) { @@ -1000,6 +1199,8 @@ public class BlobStoreManagerService extends SystemService { 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 @@ public class BlobStoreManagerService extends SystemService { } 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 54a299722754..bd35b86babd8 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_PACKAGE; 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.FileUtils; 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 @@ class BlobStoreSession extends IBlobStoreSession.Stub { 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 @@ class BlobStoreSession extends IBlobStoreSession.Stub { } } + @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/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 27d41d4d5ca4..6944b2bf0b56 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5328,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/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 3d9f11ff6d2f..339ff6b8b526 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -22,6 +22,7 @@ android_test { "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/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java new file mode 100644 index 000000000000..16dde4203e91 --- /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 8381205fa48e..f99081024494 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -46,7 +46,6 @@ android_test { "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/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java deleted file mode 100644 index ff728e7a4017..000000000000 --- 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/tests/BlobStoreTestUtils/Android.bp b/tests/BlobStoreTestUtils/Android.bp new file mode 100644 index 000000000000..edd2b435f1da --- /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 000000000000..f96766a1d3ad --- /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; + } + } +} |