• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.cts.blob;
17 
18 import static android.os.storage.StorageManager.UUID_DEFAULT;
19 
20 import static com.android.utils.blob.Utils.TAG;
21 import static com.android.utils.blob.Utils.acquireLease;
22 import static com.android.utils.blob.Utils.assertLeasedBlobs;
23 import static com.android.utils.blob.Utils.assertNoLeasedBlobs;
24 import static com.android.utils.blob.Utils.releaseLease;
25 import static com.android.utils.blob.Utils.runShellCmd;
26 import static com.android.utils.blob.Utils.triggerIdleMaintenance;
27 
28 import static com.google.common.truth.Truth.assertThat;
29 import static com.google.common.truth.Truth.assertWithMessage;
30 
31 import static org.junit.Assert.assertFalse;
32 import static org.junit.Assert.assertTrue;
33 import static org.testng.Assert.assertThrows;
34 
35 import android.app.blob.BlobHandle;
36 import android.app.blob.BlobStoreManager;
37 import android.app.usage.StorageStats;
38 import android.app.usage.StorageStatsManager;
39 import android.content.ComponentName;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.ServiceConnection;
43 import android.os.Environment;
44 import android.os.IBinder;
45 import android.os.LimitExceededException;
46 import android.os.ParcelFileDescriptor;
47 import android.os.Process;
48 import android.os.SystemClock;
49 import android.provider.DeviceConfig;
50 import android.util.ArrayMap;
51 import android.util.Log;
52 import android.util.LongSparseArray;
53 import android.util.Pair;
54 
55 import androidx.test.platform.app.InstrumentationRegistry;
56 
57 import com.android.compatibility.common.util.AmUtils;
58 import com.android.compatibility.common.util.SystemUtil;
59 import com.android.compatibility.common.util.ThrowingRunnable;
60 import com.android.cts.blob.ICommandReceiver;
61 import com.android.utils.blob.FakeBlobData;
62 import com.android.utils.blob.Utils;
63 
64 import com.google.common.io.BaseEncoding;
65 
66 import org.junit.After;
67 import org.junit.Before;
68 import org.junit.Test;
69 import org.junit.runner.RunWith;
70 
71 import java.io.IOException;
72 import java.io.OutputStream;
73 import java.nio.charset.StandardCharsets;
74 import java.util.ArrayList;
75 import java.util.Collections;
76 import java.util.Map;
77 import java.util.Objects;
78 import java.util.Random;
79 import java.util.concurrent.BlockingQueue;
80 import java.util.concurrent.CompletableFuture;
81 import java.util.concurrent.ExecutorService;
82 import java.util.concurrent.Executors;
83 import java.util.concurrent.LinkedBlockingQueue;
84 import java.util.concurrent.TimeUnit;
85 
86 @RunWith(BlobStoreTestRunner.class)
87 public class BlobStoreManagerTest {
88 
89     private static final long TIMEOUT_COMMIT_CALLBACK_SEC = 100;
90 
91     private static final long TIMEOUT_BIND_SERVICE_SEC = 2;
92 
93     private static final long TIMEOUT_WAIT_FOR_IDLE_MS = 2_000;
94 
95     // TODO: Make it a @TestApi or move the test using this to a different location.
96     // Copy of DeviceConfig.NAMESPACE_BLOBSTORE constant
97     private static final String NAMESPACE_BLOBSTORE = "blobstore";
98     private static final String KEY_SESSION_EXPIRY_TIMEOUT_MS = "session_expiry_timeout_ms";
99     private static final String KEY_LEASE_ACQUISITION_WAIT_DURATION_MS =
100             "lease_acquisition_wait_time_ms";
101     private static final String KEY_DELETE_ON_LAST_LEASE_DELAY_MS =
102             "delete_on_last_lease_delay_ms";
103     private static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
104             "total_bytes_per_app_limit_floor";
105     private static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION =
106             "total_bytes_per_app_limit_fraction";
107     private static final String KEY_MAX_ACTIVE_SESSIONS = "max_active_sessions";
108     private static final String KEY_MAX_COMMITTED_BLOBS = "max_committed_blobs";
109     private static final String KEY_MAX_LEASED_BLOBS = "max_leased_blobs";
110     private static final String KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = "max_permitted_pks";
111 
112     private static final String HELPER_PKG = "com.android.cts.blob.helper";
113     private static final String HELPER_PKG2 = "com.android.cts.blob.helper2";
114     private static final String HELPER_PKG3 = "com.android.cts.blob.helper3";
115 
116     private static final String HELPER_SERVICE = HELPER_PKG + ".BlobStoreTestService";
117 
118     private static final byte[] HELPER_PKG2_CERT_SHA256 = BaseEncoding.base16().decode(
119             "187E3D3172F2177D6FEC2EA53785BF1E25DFF7B2E5F6E59807E365A7A837E6C3");
120     private static final byte[] HELPER_PKG3_CERT_SHA256 = BaseEncoding.base16().decode(
121             "D760873D812FE1CFC02C15ED416AB774B2D4C2E936DF6D8B6707277479D4812F");
122 
123     private Context mContext;
124     int mUserId;
125     private BlobStoreManager mBlobStoreManager;
126 
127     private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
128 
129     private static final long DELTA_BYTES = 100 * 1024L;
130 
131     @Before
setUp()132     public void setUp() {
133         mContext = InstrumentationRegistry.getInstrumentation().getContext();
134         mUserId = mContext.getUser().getIdentifier();
135         mBlobStoreManager = (BlobStoreManager) mContext.getSystemService(
136                 Context.BLOB_STORE_SERVICE);
137         // Wait for any previous package/uid related broadcasts to be handled before proceeding
138         // with the verifications.
139         AmUtils.waitForBroadcastBarrier();
140     }
141 
142     @After
tearDown()143     public void tearDown() throws Exception {
144         runShellCmd("cmd blob_store clear-all-sessions -u " + mUserId);
145         runShellCmd("cmd blob_store clear-all-blobs -u " + mUserId);
146         mContext.getFilesDir().delete();
147         for (String pkg : new String[] {HELPER_PKG, HELPER_PKG2, HELPER_PKG3}) {
148             runShellCmd("cmd package clear " + pkg + " -u " + mUserId);
149         }
150     }
151 
152     @Test
testGetCreateSession()153     public void testGetCreateSession() throws Exception {
154         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
155         blobData.prepare();
156         try {
157             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
158             assertThat(sessionId).isGreaterThan(0L);
159             assertThat(mBlobStoreManager.openSession(sessionId)).isNotNull();
160         } finally {
161             blobData.delete();
162         }
163     }
164 
165     @Test
testCreateBlobHandle_invalidArguments()166     public void testCreateBlobHandle_invalidArguments() throws Exception {
167         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
168         blobData.prepare();
169         final BlobHandle handle = blobData.getBlobHandle();
170         try {
171             assertThrows(IllegalArgumentException.class, () -> BlobHandle.createWithSha256(
172                     handle.getSha256Digest(), null, handle.getExpiryTimeMillis(), handle.getTag()));
173             assertThrows(IllegalArgumentException.class, () -> BlobHandle.createWithSha256(
174                     handle.getSha256Digest(), handle.getLabel(), handle.getExpiryTimeMillis(),
175                     null));
176             assertThrows(IllegalArgumentException.class, () -> BlobHandle.createWithSha256(
177                     handle.getSha256Digest(), handle.getLabel(), -1, handle.getTag()));
178             assertThrows(IllegalArgumentException.class, () -> BlobHandle.createWithSha256(
179                     EMPTY_BYTE_ARRAY, handle.getLabel(), handle.getExpiryTimeMillis(),
180                     handle.getTag()));
181         } finally {
182             blobData.delete();
183         }
184     }
185 
186     @Test
testGetCreateSession_invalidArguments()187     public void testGetCreateSession_invalidArguments() throws Exception {
188         assertThrows(NullPointerException.class, () -> mBlobStoreManager.createSession(null));
189     }
190 
191     @Test
testOpenSession_invalidArguments()192     public void testOpenSession_invalidArguments() throws Exception {
193         assertThrows(IllegalArgumentException.class, () -> mBlobStoreManager.openSession(-1));
194     }
195 
196     @Test
testAbandonSession_invalidArguments()197     public void testAbandonSession_invalidArguments() throws Exception {
198         assertThrows(IllegalArgumentException.class, () -> mBlobStoreManager.abandonSession(-1));
199     }
200 
201     @Test
testAbandonSession()202     public void testAbandonSession() throws Exception {
203         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
204         blobData.prepare();
205         try {
206             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
207             assertThat(sessionId).isGreaterThan(0L);
208             // Verify that session can be opened.
209             assertThat(mBlobStoreManager.openSession(sessionId)).isNotNull();
210 
211             mBlobStoreManager.abandonSession(sessionId);
212             // Verify that trying to open session after it is deleted will throw.
213             assertThrows(SecurityException.class, () -> mBlobStoreManager.openSession(sessionId));
214         } finally {
215             blobData.delete();
216         }
217     }
218 
219     @Test
testOpenReadWriteSession()220     public void testOpenReadWriteSession() throws Exception {
221         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
222         blobData.prepare();
223         try {
224             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
225             assertThat(sessionId).isGreaterThan(0L);
226             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
227                 blobData.writeToSession(session, 0, blobData.getFileSize());
228                 blobData.readFromSessionAndVerifyDigest(session);
229                 blobData.readFromSessionAndVerifyBytes(session,
230                         101 /* offset */, 1001 /* length */);
231 
232                 blobData.writeToSession(session, 202 /* offset */, 2002 /* length */,
233                         blobData.getFileSize());
234                 blobData.readFromSessionAndVerifyBytes(session,
235                         202 /* offset */, 2002 /* length */);
236 
237                 commitSession(sessionId, session, blobData.getBlobHandle());
238             }
239         } finally {
240             blobData.delete();
241         }
242     }
243 
244     @Test
testOpenSession_fromAnotherPkg()245     public void testOpenSession_fromAnotherPkg() throws Exception {
246         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
247         blobData.prepare();
248         try {
249             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
250             assertThat(sessionId).isGreaterThan(0L);
251             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
252                 assertThat(session).isNotNull();
253                 session.allowPublicAccess();
254             }
255             assertThrows(SecurityException.class, () -> openSessionFromPkg(sessionId, HELPER_PKG));
256             assertThrows(SecurityException.class, () -> openSessionFromPkg(sessionId, HELPER_PKG2));
257 
258             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
259                 blobData.writeToSession(session, 0, blobData.getFileSize());
260                 blobData.readFromSessionAndVerifyDigest(session);
261                 session.allowPublicAccess();
262             }
263             assertThrows(SecurityException.class, () -> openSessionFromPkg(sessionId, HELPER_PKG));
264             assertThrows(SecurityException.class, () -> openSessionFromPkg(sessionId, HELPER_PKG2));
265         } finally {
266             blobData.delete();
267         }
268     }
269 
270     @Test
testOpenSessionAndAbandon()271     public void testOpenSessionAndAbandon() throws Exception {
272         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
273         blobData.prepare();
274         try {
275             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
276             assertThat(sessionId).isGreaterThan(0L);
277 
278             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
279                 // Verify session can be opened for read/write.
280                 assertThat(session).isNotNull();
281                 assertThat(session.openWrite(0, 0)).isNotNull();
282                 assertThat(session.openRead()).isNotNull();
283 
284                 // Verify that trying to read/write to the session after it is abandoned will throw.
285                 session.abandon();
286                 assertThrows(IllegalStateException.class, () -> session.openWrite(0, 0));
287                 assertThrows(IllegalStateException.class, () -> session.openRead());
288             }
289 
290             // Verify that trying to open the session after it is abandoned will throw.
291             assertThrows(SecurityException.class, () -> mBlobStoreManager.openSession(sessionId));
292         } finally {
293             blobData.delete();
294         }
295     }
296 
297     @Test
testCloseSession()298     public void testCloseSession() throws Exception {
299         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
300         blobData.prepare();
301         try {
302             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
303             assertThat(sessionId).isGreaterThan(0L);
304 
305             // Verify session can be opened for read/write.
306             BlobStoreManager.Session session = null;
307             try {
308                 session = mBlobStoreManager.openSession(sessionId);
309                 assertThat(session).isNotNull();
310                 assertThat(session.openWrite(0, 0)).isNotNull();
311                 assertThat(session.openRead()).isNotNull();
312             } finally {
313                 session.close();
314             }
315 
316             // Verify trying to read/write to session after it is closed will throw.
317             // an exception.
318             final BlobStoreManager.Session closedSession = session;
319             assertThrows(IllegalStateException.class, () -> closedSession.openWrite(0, 0));
320             assertThrows(IllegalStateException.class, () -> closedSession.openRead());
321 
322             // Verify that the session can be opened again for read/write.
323             try {
324                 session = mBlobStoreManager.openSession(sessionId);
325                 assertThat(session).isNotNull();
326                 assertThat(session.openWrite(0, 0)).isNotNull();
327                 assertThat(session.openRead()).isNotNull();
328             } finally {
329                 session.close();
330             }
331         } finally {
332             blobData.delete();
333         }
334     }
335 
336     @Test
testAllowPublicAccess()337     public void testAllowPublicAccess() throws Exception {
338         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
339         blobData.prepare();
340         try {
341             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
342             assertThat(sessionId).isGreaterThan(0L);
343 
344             commitBlob(blobData, session -> {
345                 session.allowPublicAccess();
346                 assertThat(session.isPublicAccessAllowed()).isTrue();
347             });
348 
349             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG);
350             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG2);
351             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG3);
352         } finally {
353             blobData.delete();
354         }
355     }
356 
357     @Test
testAllowPublicAccess_abandonedSession()358     public void testAllowPublicAccess_abandonedSession() throws Exception {
359         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
360         blobData.prepare();
361         try {
362             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
363             assertThat(sessionId).isGreaterThan(0L);
364 
365             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
366                 session.allowPublicAccess();
367                 assertThat(session.isPublicAccessAllowed()).isTrue();
368 
369                 session.abandon();
370                 assertThrows(IllegalStateException.class,
371                         () -> session.allowPublicAccess());
372                 assertThrows(IllegalStateException.class,
373                         () -> session.isPublicAccessAllowed());
374             }
375         } finally {
376             blobData.delete();
377         }
378     }
379 
380     @Test
testAllowSameSignatureAccess()381     public void testAllowSameSignatureAccess() throws Exception {
382         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
383         blobData.prepare();
384         try {
385             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
386             assertThat(sessionId).isGreaterThan(0L);
387 
388             commitBlob(blobData, session -> {
389                 session.allowSameSignatureAccess();
390                 assertThat(session.isSameSignatureAccessAllowed()).isTrue();
391             });
392 
393             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG);
394             assertPkgCannotAccess(blobData, HELPER_PKG2);
395             assertPkgCannotAccess(blobData, HELPER_PKG3);
396         } finally {
397             blobData.delete();
398         }
399     }
400 
401     @Test
testAllowSameSignatureAccess_abandonedSession()402     public void testAllowSameSignatureAccess_abandonedSession() throws Exception {
403         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
404         blobData.prepare();
405         try {
406             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
407             assertThat(sessionId).isGreaterThan(0L);
408 
409             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
410                 session.allowSameSignatureAccess();
411                 assertThat(session.isSameSignatureAccessAllowed()).isTrue();
412 
413                 session.abandon();
414                 assertThrows(IllegalStateException.class,
415                         () -> session.allowSameSignatureAccess());
416                 assertThrows(IllegalStateException.class,
417                         () -> session.isSameSignatureAccessAllowed());
418             }
419         } finally {
420             blobData.delete();
421         }
422     }
423 
424     @Test
testAllowPackageAccess()425     public void testAllowPackageAccess() throws Exception {
426         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
427         blobData.prepare();
428         try {
429             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
430             assertThat(sessionId).isGreaterThan(0L);
431 
432             commitBlob(blobData, session -> {
433                 session.allowPackageAccess(HELPER_PKG2, HELPER_PKG2_CERT_SHA256);
434                 assertThat(session.isPackageAccessAllowed(HELPER_PKG2, HELPER_PKG2_CERT_SHA256))
435                         .isTrue();
436             });
437 
438             assertPkgCannotAccess(blobData, HELPER_PKG);
439             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG2);
440             assertPkgCannotAccess(blobData, HELPER_PKG3);
441         } finally {
442             blobData.delete();
443         }
444     }
445 
446     @Test
testAllowPackageAccess_allowMultiple()447     public void testAllowPackageAccess_allowMultiple() throws Exception {
448         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
449         blobData.prepare();
450         try {
451             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
452             assertThat(sessionId).isGreaterThan(0L);
453 
454             commitBlob(blobData, session -> {
455                 session.allowPackageAccess(HELPER_PKG2, HELPER_PKG2_CERT_SHA256);
456                 session.allowPackageAccess(HELPER_PKG3, HELPER_PKG3_CERT_SHA256);
457                 assertThat(session.isPackageAccessAllowed(HELPER_PKG2, HELPER_PKG2_CERT_SHA256))
458                         .isTrue();
459                 assertThat(session.isPackageAccessAllowed(HELPER_PKG3, HELPER_PKG3_CERT_SHA256))
460                         .isTrue();
461             });
462 
463             assertPkgCannotAccess(blobData, HELPER_PKG);
464             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG2);
465             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG3);
466         } finally {
467             blobData.delete();
468         }
469     }
470 
471     @Test
testAllowPackageAccess_abandonedSession()472     public void testAllowPackageAccess_abandonedSession() throws Exception {
473         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
474         blobData.prepare();
475         try {
476             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
477             assertThat(sessionId).isGreaterThan(0L);
478 
479             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
480                 session.allowPackageAccess("com.example", "test_bytes".getBytes());
481                 assertThat(session.isPackageAccessAllowed("com.example", "test_bytes".getBytes()))
482                         .isTrue();
483 
484                 session.abandon();
485                 assertThrows(IllegalStateException.class,
486                         () -> session.allowPackageAccess(
487                                 "com.example2", "test_bytes2".getBytes()));
488                 assertThrows(IllegalStateException.class,
489                         () -> session.isPackageAccessAllowed(
490                                 "com.example", "test_bytes".getBytes()));
491             }
492         } finally {
493             blobData.delete();
494         }
495     }
496 
497     @Test
testPrivateAccess()498     public void testPrivateAccess() throws Exception {
499         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
500         blobData.prepare();
501         final TestServiceConnection connection1 = bindToHelperService(HELPER_PKG);
502         final TestServiceConnection connection2 = bindToHelperService(HELPER_PKG2);
503         final TestServiceConnection connection3 = bindToHelperService(HELPER_PKG3);
504         try {
505             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
506             assertThat(sessionId).isGreaterThan(0L);
507 
508             commitBlob(blobData);
509 
510             assertPkgCannotAccess(blobData, connection1);
511             assertPkgCannotAccess(blobData, connection2);
512             assertPkgCannotAccess(blobData, connection3);
513 
514             commitBlobFromPkg(blobData, connection1);
515             acquireLeaseAndAssertPkgCanAccess(blobData, connection1);
516             assertPkgCannotAccess(blobData, connection2);
517             assertPkgCannotAccess(blobData, connection3);
518 
519             commitBlobFromPkg(blobData, connection2);
520             acquireLeaseAndAssertPkgCanAccess(blobData, connection1);
521             acquireLeaseAndAssertPkgCanAccess(blobData, connection2);
522             assertPkgCannotAccess(blobData, connection3);
523         } finally {
524             blobData.delete();
525             connection1.unbind();
526             connection2.unbind();
527             connection3.unbind();
528         }
529     }
530 
531     @Test
testMixedAccessType()532     public void testMixedAccessType() throws Exception {
533         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
534         blobData.prepare();
535         try {
536             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
537             assertThat(sessionId).isGreaterThan(0L);
538 
539             commitBlob(blobData, session -> {
540                 session.allowSameSignatureAccess();
541                 session.allowPackageAccess(HELPER_PKG3, HELPER_PKG3_CERT_SHA256);
542                 assertThat(session.isSameSignatureAccessAllowed()).isTrue();
543                 assertThat(session.isPackageAccessAllowed(HELPER_PKG3, HELPER_PKG3_CERT_SHA256))
544                         .isTrue();
545                 assertThat(session.isPublicAccessAllowed()).isFalse();
546             });
547 
548             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG);
549             assertPkgCannotAccess(blobData, HELPER_PKG2);
550             acquireLeaseAndAssertPkgCanAccess(blobData, HELPER_PKG3);
551         } finally {
552             blobData.delete();
553         }
554     }
555 
556     @Test
testMixedAccessType_fromMultiplePackages()557     public void testMixedAccessType_fromMultiplePackages() throws Exception {
558         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
559         blobData.prepare();
560         final TestServiceConnection connection1 = bindToHelperService(HELPER_PKG);
561         final TestServiceConnection connection2 = bindToHelperService(HELPER_PKG2);
562         final TestServiceConnection connection3 = bindToHelperService(HELPER_PKG3);
563         try {
564             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
565             assertThat(sessionId).isGreaterThan(0L);
566 
567             commitBlob(blobData, session -> {
568                 session.allowSameSignatureAccess();
569                 session.allowPackageAccess(HELPER_PKG2, HELPER_PKG2_CERT_SHA256);
570                 assertThat(session.isSameSignatureAccessAllowed()).isTrue();
571                 assertThat(session.isPackageAccessAllowed(HELPER_PKG2, HELPER_PKG2_CERT_SHA256))
572                         .isTrue();
573                 assertThat(session.isPublicAccessAllowed()).isFalse();
574             });
575 
576             acquireLeaseAndAssertPkgCanAccess(blobData, connection1);
577             acquireLeaseAndAssertPkgCanAccess(blobData, connection2);
578             assertPkgCannotAccess(blobData, connection3);
579 
580             commitBlobFromPkg(blobData, ICommandReceiver.FLAG_ACCESS_TYPE_PUBLIC, connection2);
581 
582             acquireLeaseAndAssertPkgCanAccess(blobData, connection1);
583             acquireLeaseAndAssertPkgCanAccess(blobData, connection2);
584             acquireLeaseAndAssertPkgCanAccess(blobData, connection3);
585         } finally {
586             blobData.delete();
587             connection1.unbind();
588             connection2.unbind();
589             connection3.unbind();
590         }
591     }
592 
593     @Test
testSessionCommit()594     public void testSessionCommit() throws Exception {
595         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
596         blobData.prepare();
597         try {
598             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
599             assertThat(sessionId).isGreaterThan(0L);
600 
601             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
602                 blobData.writeToSession(session);
603 
604                 final ParcelFileDescriptor pfd = session.openWrite(
605                         0L /* offset */, 0L /* length */);
606                 assertThat(pfd).isNotNull();
607                 blobData.writeToFd(pfd.getFileDescriptor(), 0 /* offset */, 100 /* length */);
608 
609                 commitSession(sessionId, session, blobData.getBlobHandle());
610 
611                 // Verify that writing to the session after commit will throw.
612                 assertThrows(IOException.class, () -> blobData.writeToFd(
613                         pfd.getFileDescriptor() /* length */, 0 /* offset */, 100 /* length */));
614                 assertThrows(IllegalStateException.class, () -> session.openWrite(0L, 0L));
615             }
616         } finally {
617             blobData.delete();
618         }
619     }
620 
621     @Test
testSessionCommit_incompleteData()622     public void testSessionCommit_incompleteData() throws Exception {
623         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
624         blobData.prepare();
625         try {
626             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
627             assertThat(sessionId).isGreaterThan(0L);
628 
629             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
630                 blobData.writeToSession(session, 0, blobData.getFileSize() - 2);
631 
632                 commitSession(sessionId, session, blobData.getBlobHandle(),
633                         false /* expectSuccess */);
634             }
635         } finally {
636             blobData.delete();
637         }
638     }
639 
640     @Test
testSessionCommit_incorrectData()641     public void testSessionCommit_incorrectData() throws Exception {
642         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
643         blobData.prepare();
644         try {
645             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
646             assertThat(sessionId).isGreaterThan(0L);
647 
648             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
649                 blobData.writeToSession(session, 0, blobData.getFileSize());
650                 try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
651                         session.openWrite(0, blobData.getFileSize()))) {
652                     out.write("wrong_data".getBytes(StandardCharsets.UTF_8));
653                 }
654 
655                 commitSession(sessionId, session, blobData.getBlobHandle(),
656                         false /* expectSuccess */);
657             }
658         } finally {
659             blobData.delete();
660         }
661     }
662 
663     @Test
testRecommitBlob()664     public void testRecommitBlob() throws Exception {
665         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
666         blobData.prepare();
667 
668         try {
669             commitBlob(blobData);
670             // Verify that blob can be accessed after committing.
671             try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobData.getBlobHandle())) {
672                 assertThat(pfd).isNotNull();
673                 blobData.verifyBlob(pfd);
674             }
675 
676             commitBlob(blobData);
677             // Verify that blob can be accessed after re-committing.
678             try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobData.getBlobHandle())) {
679                 assertThat(pfd).isNotNull();
680                 blobData.verifyBlob(pfd);
681             }
682         } finally {
683             blobData.delete();
684         }
685     }
686 
687     @Test
testRecommitBlob_fromMultiplePackages()688     public void testRecommitBlob_fromMultiplePackages() throws Exception {
689         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
690         blobData.prepare();
691         final TestServiceConnection connection = bindToHelperService(HELPER_PKG);
692         try {
693             commitBlob(blobData);
694             // Verify that blob can be accessed after committing.
695             try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobData.getBlobHandle())) {
696                 assertThat(pfd).isNotNull();
697                 blobData.verifyBlob(pfd);
698             }
699 
700             commitBlobFromPkg(blobData, connection);
701             // Verify that blob can be accessed after re-committing.
702             try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobData.getBlobHandle())) {
703                 assertThat(pfd).isNotNull();
704                 blobData.verifyBlob(pfd);
705             }
706             acquireLeaseAndAssertPkgCanAccess(blobData, connection);
707         } finally {
708             blobData.delete();
709             connection.unbind();
710         }
711     }
712 
713     @Test
testSessionCommit_largeBlob()714     public void testSessionCommit_largeBlob() throws Exception {
715         final long fileSizeBytes = Math.min(mBlobStoreManager.getRemainingLeaseQuotaBytes(),
716                 150 * 1024L * 1024L);
717         final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
718                 .setFileSize(fileSizeBytes)
719                 .build();
720         blobData.prepare();
721         final long commitTimeoutSec = TIMEOUT_COMMIT_CALLBACK_SEC * 2;
722         try {
723             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
724             assertThat(sessionId).isGreaterThan(0L);
725             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
726                 blobData.writeToSession(session);
727 
728                 Log.d(TAG, "Committing session: " + sessionId
729                         + "; blob: " + blobData.getBlobHandle());
730                 final CompletableFuture<Integer> callback = new CompletableFuture<>();
731                 session.commit(mContext.getMainExecutor(), callback::complete);
732                 assertThat(callback.get(commitTimeoutSec, TimeUnit.SECONDS))
733                         .isEqualTo(0);
734             }
735 
736             // Verify that blob can be accessed after committing.
737             try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobData.getBlobHandle())) {
738                 assertThat(pfd).isNotNull();
739                 blobData.verifyBlob(pfd);
740             }
741         } finally {
742             blobData.delete();
743         }
744     }
745 
746     @Test
testCommitSession_multipleWrites()747     public void testCommitSession_multipleWrites() throws Exception {
748         final int numThreads = 2;
749         final Random random = new Random(0);
750         final ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
751         final CompletableFuture<Throwable>[] completableFutures = new CompletableFuture[numThreads];
752         for (int i = 0; i < numThreads; ++i) {
753             completableFutures[i] = CompletableFuture.supplyAsync(() -> {
754                 final int minSizeMb = 30;
755                 final long fileSizeBytes = (minSizeMb + random.nextInt(minSizeMb)) * 1024L * 1024L;
756                 final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
757                         .setFileSize(fileSizeBytes)
758                         .build();
759                 try {
760                     blobData.prepare();
761                     commitAndVerifyBlob(blobData);
762                 } catch (Throwable t) {
763                     return t;
764                 } finally {
765                     blobData.delete();
766                 }
767                 return null;
768             }, executorService);
769         }
770         final ArrayList<Throwable> invalidResults = new ArrayList<>();
771         for (int i = 0; i < numThreads; ++i) {
772              final Throwable result = completableFutures[i].get();
773              if (result != null) {
774                  invalidResults.add(result);
775              }
776         }
777         assertThat(invalidResults).isEmpty();
778     }
779 
780     @Test
testCommitSession_multipleReadWrites()781     public void testCommitSession_multipleReadWrites() throws Exception {
782         final int numThreads = 2;
783         final Random random = new Random(0);
784         final ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
785         final CompletableFuture<Throwable>[] completableFutures = new CompletableFuture[numThreads];
786         for (int i = 0; i < numThreads; ++i) {
787             completableFutures[i] = CompletableFuture.supplyAsync(() -> {
788                 final int minSizeMb = 30;
789                 final long fileSizeBytes = (minSizeMb + random.nextInt(minSizeMb)) * 1024L * 1024L;
790                 final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
791                         .setFileSize(fileSizeBytes)
792                         .build();
793                 try {
794                     blobData.prepare();
795                     final long sessionId = mBlobStoreManager.createSession(
796                             blobData.getBlobHandle());
797                     assertThat(sessionId).isGreaterThan(0L);
798                     try (BlobStoreManager.Session session = mBlobStoreManager.openSession(
799                             sessionId)) {
800                         final long partialFileSizeBytes = minSizeMb * 1024L * 1024L;
801                         blobData.writeToSession(session, 0L, partialFileSizeBytes,
802                                 blobData.getFileSize());
803                         blobData.readFromSessionAndVerifyBytes(session, 0L,
804                                 (int) partialFileSizeBytes);
805                         blobData.writeToSession(session, partialFileSizeBytes,
806                                 blobData.getFileSize() - partialFileSizeBytes,
807                                 blobData.getFileSize());
808                         blobData.readFromSessionAndVerifyBytes(session, partialFileSizeBytes,
809                                 (int) (blobData.getFileSize() - partialFileSizeBytes));
810 
811                         commitSession(sessionId, session, blobData.getBlobHandle());
812                     }
813 
814                     // Verify that blob can be accessed after committing.
815                     try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(
816                             blobData.getBlobHandle())) {
817                         assertThat(pfd).isNotNull();
818                         blobData.verifyBlob(pfd);
819                     }
820                 } catch (Throwable t) {
821                     return t;
822                 } finally {
823                     blobData.delete();
824                 }
825                 return null;
826             }, executorService);
827         }
828         final ArrayList<Throwable> invalidResults = new ArrayList<>();
829         for (int i = 0; i < numThreads; ++i) {
830             final Throwable result = completableFutures[i].get();
831             if (result != null) {
832                 invalidResults.add(result);
833             }
834         }
835         assertThat(invalidResults).isEmpty();
836     }
837 
838     @Test
testOpenBlob()839     public void testOpenBlob() throws Exception {
840         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
841         blobData.prepare();
842         try {
843             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
844             assertThat(sessionId).isGreaterThan(0L);
845 
846             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
847                 blobData.writeToSession(session);
848 
849                 // Verify that trying to accessed the blob before commit throws
850                 assertThrows(SecurityException.class,
851                         () -> mBlobStoreManager.openBlob(blobData.getBlobHandle()));
852 
853                 commitSession(sessionId, session, blobData.getBlobHandle());
854             }
855 
856             // Verify that blob can be accessed after committing.
857             try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobData.getBlobHandle())) {
858                 assertThat(pfd).isNotNull();
859 
860                 blobData.verifyBlob(pfd);
861             }
862         } finally {
863             blobData.delete();
864         }
865     }
866 
867     @Test
testOpenBlob_invalidArguments()868     public void testOpenBlob_invalidArguments() throws Exception {
869         assertThrows(NullPointerException.class, () -> mBlobStoreManager.openBlob(null));
870     }
871 
872     @Test
testAcquireReleaseLease()873     public void testAcquireReleaseLease() throws Exception {
874         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
875         blobData.prepare();
876         try {
877             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
878             assertThat(sessionId).isGreaterThan(0L);
879 
880             commitBlob(blobData);
881 
882             assertThrows(IllegalArgumentException.class, () ->
883                     acquireLease(mContext, blobData.getBlobHandle(),
884                             R.string.test_desc, blobData.getExpiryTimeMillis() + 1000));
885             assertNoLeasedBlobs(mBlobStoreManager);
886 
887             acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
888                     blobData.getExpiryTimeMillis() - 1000);
889             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
890             acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc);
891             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
892             releaseLease(mContext, blobData.getBlobHandle());
893             assertNoLeasedBlobs(mBlobStoreManager);
894 
895             acquireLease(mContext, blobData.getBlobHandle(), "Test description",
896                     blobData.getExpiryTimeMillis() - 20000);
897             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
898             acquireLease(mContext, blobData.getBlobHandle(), "Test description two");
899             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
900             releaseLease(mContext, blobData.getBlobHandle());
901             assertNoLeasedBlobs(mBlobStoreManager);
902         } finally {
903             blobData.delete();
904         }
905     }
906 
907     @Test
testAcquireLease_multipleLeases()908     public void testAcquireLease_multipleLeases() throws Exception {
909         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
910         final FakeBlobData blobData2 = new FakeBlobData.Builder(mContext)
911                 .setRandomSeed(42)
912                 .build();
913         blobData.prepare();
914         blobData2.prepare();
915         try {
916             commitBlob(blobData);
917 
918             acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
919                     blobData.getExpiryTimeMillis() - 1000);
920             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
921 
922             commitBlob(blobData2);
923 
924             acquireLease(mContext, blobData2.getBlobHandle(), "Test desc2",
925                     blobData.getExpiryTimeMillis() - 2000);
926             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle(),
927                     blobData2.getBlobHandle());
928 
929             releaseLease(mContext, blobData.getBlobHandle());
930             assertLeasedBlobs(mBlobStoreManager, blobData2.getBlobHandle());
931 
932             releaseLease(mContext, blobData2.getBlobHandle());
933             assertNoLeasedBlobs(mBlobStoreManager);
934         } finally {
935             blobData.delete();
936             blobData2.delete();
937         }
938     }
939 
940     @Test
testAcquireLease_multipleLeasees()941     public void testAcquireLease_multipleLeasees() throws Exception {
942         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
943         blobData.prepare();
944         final TestServiceConnection serviceConnection = bindToHelperService(HELPER_PKG);
945         try {
946             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
947             assertThat(sessionId).isGreaterThan(0L);
948 
949             commitBlob(blobData, session -> {
950                 session.allowPublicAccess();
951                 assertThat(session.isPublicAccessAllowed()).isTrue();
952             });
953 
954             acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc);
955             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
956             acquireLeaseFrmPkg(blobData, serviceConnection);
957             assertPkgCanAccess(blobData, serviceConnection);
958 
959             // Release the lease from this package
960             releaseLease(mContext, blobData.getBlobHandle());
961             assertNoLeasedBlobs(mBlobStoreManager);
962 
963             // Helper package should still be able to read the blob
964             assertPkgCanAccess(blobData, serviceConnection);
965         } finally {
966             blobData.delete();
967             serviceConnection.unbind();
968         }
969     }
970 
971     @Test
testAcquireLease_leaseExpired()972     public void testAcquireLease_leaseExpired() throws Exception {
973         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
974         blobData.prepare();
975         final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
976         try {
977             commitBlob(blobData);
978 
979             acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
980                     System.currentTimeMillis() + waitDurationMs);
981             assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
982 
983             waitForLeaseExpiration(waitDurationMs, blobData.getBlobHandle());
984             assertNoLeasedBlobs(mBlobStoreManager);
985         } finally {
986             blobData.delete();
987         }
988     }
989 
990     @Test
testAcquireRelease_deleteAfterDelay()991     public void testAcquireRelease_deleteAfterDelay() throws Exception {
992         final long waitDurationMs = TimeUnit.SECONDS.toMillis(1);
993         runWithKeyValues(() -> {
994             final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
995             blobData.prepare();
996             try {
997                 commitBlob(blobData);
998 
999                 acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
1000                         blobData.getExpiryTimeMillis());
1001                 assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
1002 
1003                 SystemClock.sleep(waitDurationMs);
1004 
1005                 releaseLease(mContext, blobData.getBlobHandle());
1006                 assertNoLeasedBlobs(mBlobStoreManager);
1007 
1008                 SystemClock.sleep(waitDurationMs);
1009                 SystemUtil.runWithShellPermissionIdentity(() ->
1010                         mBlobStoreManager.waitForIdle(TIMEOUT_WAIT_FOR_IDLE_MS));
1011 
1012                 assertThrows(SecurityException.class, () -> mBlobStoreManager.acquireLease(
1013                         blobData.getBlobHandle(), R.string.test_desc,
1014                         blobData.getExpiryTimeMillis()));
1015                 assertNoLeasedBlobs(mBlobStoreManager);
1016             } finally {
1017                 blobData.delete();
1018             }
1019         }, Pair.create(KEY_LEASE_ACQUISITION_WAIT_DURATION_MS, String.valueOf(waitDurationMs)),
1020                 Pair.create(KEY_DELETE_ON_LAST_LEASE_DELAY_MS, String.valueOf(waitDurationMs)));
1021     }
1022 
1023     @Test
testAcquireReleaseLease_invalidArguments()1024     public void testAcquireReleaseLease_invalidArguments() throws Exception {
1025         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1026         blobData.prepare();
1027         try {
1028             assertThrows(NullPointerException.class, () -> mBlobStoreManager.acquireLease(
1029                     null, R.string.test_desc, blobData.getExpiryTimeMillis()));
1030             assertThrows(IllegalArgumentException.class, () -> mBlobStoreManager.acquireLease(
1031                     blobData.getBlobHandle(), R.string.test_desc, -1));
1032             assertThrows(IllegalArgumentException.class, () -> mBlobStoreManager.acquireLease(
1033                     blobData.getBlobHandle(), -1));
1034             assertThrows(IllegalArgumentException.class, () -> mBlobStoreManager.acquireLease(
1035                     blobData.getBlobHandle(), null));
1036             assertThrows(IllegalArgumentException.class, () -> mBlobStoreManager.acquireLease(
1037                     blobData.getBlobHandle(), null, blobData.getExpiryTimeMillis()));
1038         } finally {
1039             blobData.delete();
1040         }
1041 
1042         assertThrows(NullPointerException.class, () -> mBlobStoreManager.releaseLease(null));
1043     }
1044 
1045     @Test
testStorageAttributedToSelf()1046     public void testStorageAttributedToSelf() throws Exception {
1047         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1048         blobData.prepare();
1049         final long partialFileSize = 3373L;
1050 
1051         final StorageStatsManager storageStatsManager = mContext.getSystemService(
1052                 StorageStatsManager.class);
1053         StorageStats beforeStatsForPkg = storageStatsManager
1054                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1055         StorageStats beforeStatsForUid = storageStatsManager
1056                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1057 
1058         // Create a session and write some data.
1059         final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
1060         assertThat(sessionId).isGreaterThan(0L);
1061         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1062             blobData.writeToSession(session, 0, partialFileSize, partialFileSize);
1063         }
1064 
1065         StorageStats afterStatsForPkg = storageStatsManager
1066                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1067         StorageStats afterStatsForUid = storageStatsManager
1068                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1069 
1070         // 'partialFileSize' bytes were written, verify the size increase.
1071         assertSizeBytesMostlyEquals(partialFileSize,
1072                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1073         assertSizeBytesMostlyEquals(partialFileSize,
1074                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1075 
1076         // Complete writing data.
1077         final long totalFileSize = blobData.getFileSize();
1078         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1079             blobData.writeToSession(session, partialFileSize, totalFileSize - partialFileSize,
1080                     totalFileSize);
1081         }
1082 
1083         afterStatsForPkg = storageStatsManager
1084                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1085         afterStatsForUid = storageStatsManager
1086                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1087 
1088         // 'totalFileSize' bytes were written so far, verify the size increase.
1089         assertSizeBytesMostlyEquals(totalFileSize,
1090                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1091         assertSizeBytesMostlyEquals(totalFileSize,
1092                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1093 
1094         // Commit the session.
1095         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1096             blobData.writeToSession(session, partialFileSize,
1097                     session.getSize() - partialFileSize, blobData.getFileSize());
1098             commitSession(sessionId, session, blobData.getBlobHandle());
1099         }
1100 
1101         acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc);
1102         assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
1103 
1104         afterStatsForPkg = storageStatsManager
1105                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1106         afterStatsForUid = storageStatsManager
1107                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1108 
1109         // Session was committed but no one else is using it, verify the size increase stays
1110         // the same as earlier.
1111         assertSizeBytesMostlyEquals(totalFileSize,
1112                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1113         assertSizeBytesMostlyEquals(totalFileSize,
1114                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1115 
1116         releaseLease(mContext, blobData.getBlobHandle());
1117         assertNoLeasedBlobs(mBlobStoreManager);
1118 
1119         afterStatsForPkg = storageStatsManager
1120                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1121         afterStatsForUid = storageStatsManager
1122                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1123 
1124         // No leases on the blob, so it should not be attributed.
1125         assertSizeBytesMostlyEquals(0L,
1126                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1127         assertSizeBytesMostlyEquals(0L,
1128                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1129     }
1130 
1131     @Test
testStorageAttribution_acquireLease()1132     public void testStorageAttribution_acquireLease() throws Exception {
1133         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1134         blobData.prepare();
1135 
1136         final StorageStatsManager storageStatsManager = mContext.getSystemService(
1137                 StorageStatsManager.class);
1138         StorageStats beforeStatsForPkg = storageStatsManager
1139                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1140         StorageStats beforeStatsForUid = storageStatsManager
1141                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1142 
1143         final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
1144         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1145             blobData.writeToSession(session);
1146             session.allowPublicAccess();
1147 
1148             commitSession(sessionId, session, blobData.getBlobHandle());
1149         }
1150 
1151         StorageStats afterStatsForPkg = storageStatsManager
1152                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1153         StorageStats afterStatsForUid = storageStatsManager
1154                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1155 
1156         // No leases on the blob, so it should not be attributed.
1157         assertSizeBytesMostlyEquals(0L,
1158                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1159         assertSizeBytesMostlyEquals(0L,
1160                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1161 
1162         final TestServiceConnection serviceConnection = bindToHelperService(HELPER_PKG);
1163         final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
1164         try {
1165             StorageStats beforeStatsForHelperPkg = commandReceiver.queryStatsForPackage();
1166             StorageStats beforeStatsForHelperUid = commandReceiver.queryStatsForUid();
1167 
1168             commandReceiver.acquireLease(blobData.getBlobHandle());
1169 
1170             StorageStats afterStatsForHelperPkg = commandReceiver.queryStatsForPackage();
1171             StorageStats afterStatsForHelperUid = commandReceiver.queryStatsForUid();
1172 
1173             assertSizeBytesMostlyEquals(blobData.getFileSize(),
1174                     afterStatsForHelperPkg.getDataBytes() - beforeStatsForHelperPkg.getDataBytes());
1175             assertSizeBytesMostlyEquals(blobData.getFileSize(),
1176                     afterStatsForHelperUid.getDataBytes() - beforeStatsForHelperUid.getDataBytes());
1177 
1178             afterStatsForPkg = storageStatsManager
1179                     .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(),
1180                             mContext.getUser());
1181             afterStatsForUid = storageStatsManager
1182                     .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1183 
1184             // There shouldn't be no change in stats for this package
1185             assertSizeBytesMostlyEquals(0L,
1186                     afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1187             assertSizeBytesMostlyEquals(0L,
1188                     afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1189 
1190             commandReceiver.releaseLease(blobData.getBlobHandle());
1191 
1192             afterStatsForHelperPkg = commandReceiver.queryStatsForPackage();
1193             afterStatsForHelperUid = commandReceiver.queryStatsForUid();
1194 
1195             // Lease is released, so it should not be attributed anymore.
1196             assertSizeBytesMostlyEquals(0L,
1197                     afterStatsForHelperPkg.getDataBytes() - beforeStatsForHelperPkg.getDataBytes());
1198             assertSizeBytesMostlyEquals(0L,
1199                     afterStatsForHelperUid.getDataBytes() - beforeStatsForHelperUid.getDataBytes());
1200         } finally {
1201             serviceConnection.unbind();
1202         }
1203     }
1204 
1205     @Test
testStorageAttribution_withExpiredLease()1206     public void testStorageAttribution_withExpiredLease() throws Exception {
1207         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1208         blobData.prepare();
1209 
1210         final StorageStatsManager storageStatsManager = mContext.getSystemService(
1211                 StorageStatsManager.class);
1212         StorageStats beforeStatsForPkg = storageStatsManager
1213                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1214         StorageStats beforeStatsForUid = storageStatsManager
1215                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1216 
1217         commitBlob(blobData);
1218 
1219         StorageStats afterStatsForPkg = storageStatsManager
1220                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1221         StorageStats afterStatsForUid = storageStatsManager
1222                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1223 
1224         // No leases on the blob, so it should not be attributed.
1225         assertSizeBytesMostlyEquals(0L,
1226                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1227         assertSizeBytesMostlyEquals(0L,
1228                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1229 
1230         final long leaseExpiryDurationMs = TimeUnit.SECONDS.toMillis(5);
1231         acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
1232                 System.currentTimeMillis() + leaseExpiryDurationMs);
1233 
1234         final long startTimeMs = System.currentTimeMillis();
1235         afterStatsForPkg = storageStatsManager
1236                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1237         afterStatsForUid = storageStatsManager
1238                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1239 
1240         assertSizeBytesMostlyEquals(blobData.getFileSize(),
1241                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1242         assertSizeBytesMostlyEquals(blobData.getFileSize(),
1243                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1244 
1245         waitForLeaseExpiration(
1246                 Math.abs(leaseExpiryDurationMs - (System.currentTimeMillis() - startTimeMs)),
1247                 blobData.getBlobHandle());
1248 
1249         afterStatsForPkg = storageStatsManager
1250                 .queryStatsForPackage(UUID_DEFAULT, mContext.getPackageName(), mContext.getUser());
1251         afterStatsForUid = storageStatsManager
1252                 .queryStatsForUid(UUID_DEFAULT, Process.myUid());
1253 
1254         // Lease is expired, so it should not be attributed anymore.
1255         assertSizeBytesMostlyEquals(0L,
1256                 afterStatsForPkg.getDataBytes() - beforeStatsForPkg.getDataBytes());
1257         assertSizeBytesMostlyEquals(0L,
1258                 afterStatsForUid.getDataBytes() - beforeStatsForUid.getDataBytes());
1259 
1260         blobData.delete();
1261     }
1262 
1263     @Test
testLeaseQuotaExceeded()1264     public void testLeaseQuotaExceeded() throws Exception {
1265         final long totalBytes = Environment.getDataDirectory().getTotalSpace();
1266         final long limitBytes = 100 * Utils.MB_IN_BYTES;
1267         runWithKeyValues(() -> {
1268             final LongSparseArray<BlobHandle> blobs = new LongSparseArray<>();
1269             for (long blobSize : new long[] {20L, 30L, 40L}) {
1270                 final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
1271                         .setFileSize(blobSize * Utils.MB_IN_BYTES)
1272                         .build();
1273                 blobData.prepare();
1274 
1275                 commitBlob(blobData);
1276                 acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
1277                         blobData.getExpiryTimeMillis());
1278                 blobs.put(blobSize, blobData.getBlobHandle());
1279             }
1280             final long blobSize = 40L;
1281             final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
1282                     .setFileSize(blobSize * Utils.MB_IN_BYTES)
1283                     .build();
1284             blobData.prepare();
1285             commitBlob(blobData);
1286             assertThrows(LimitExceededException.class, () -> mBlobStoreManager.acquireLease(
1287                     blobData.getBlobHandle(), R.string.test_desc, blobData.getExpiryTimeMillis()));
1288 
1289             releaseLease(mContext, blobs.get(20L));
1290             assertThrows(LimitExceededException.class, () -> mBlobStoreManager.acquireLease(
1291                     blobData.getBlobHandle(), R.string.test_desc, blobData.getExpiryTimeMillis()));
1292 
1293             releaseLease(mContext, blobs.get(30L));
1294             acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
1295                     blobData.getExpiryTimeMillis());
1296         }, Pair.create(KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, String.valueOf(limitBytes)),
1297                 Pair.create(KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION,
1298                         String.valueOf((double) limitBytes / totalBytes)));
1299     }
1300 
1301     @Test
testLeaseQuotaExceeded_singleFileExceedsLimit()1302     public void testLeaseQuotaExceeded_singleFileExceedsLimit() throws Exception {
1303         final long totalBytes = Environment.getDataDirectory().getTotalSpace();
1304         final long limitBytes = 50 * Utils.MB_IN_BYTES;
1305         runWithKeyValues(() -> {
1306             final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
1307                     .setFileSize(limitBytes + (5 * Utils.MB_IN_BYTES))
1308                     .build();
1309             blobData.prepare();
1310             commitBlob(blobData);
1311             assertThrows(LimitExceededException.class, () -> mBlobStoreManager.acquireLease(
1312                     blobData.getBlobHandle(), R.string.test_desc, blobData.getExpiryTimeMillis()));
1313         }, Pair.create(KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, String.valueOf(limitBytes)),
1314                 Pair.create(KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION,
1315                         String.valueOf((double) limitBytes / totalBytes)));
1316     }
1317 
1318     @Test
testLeaseQuotaExceeded_withExpiredLease()1319     public void testLeaseQuotaExceeded_withExpiredLease() throws Exception {
1320         final long totalBytes = Environment.getDataDirectory().getTotalSpace();
1321         final long limitBytes = 50 * Utils.MB_IN_BYTES;
1322         final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
1323         runWithKeyValues(() -> {
1324             final FakeBlobData blobData1 = new FakeBlobData.Builder(mContext)
1325                     .setFileSize(40 * Utils.MB_IN_BYTES)
1326                     .build();
1327             blobData1.prepare();
1328             final FakeBlobData blobData2 = new FakeBlobData.Builder(mContext)
1329                     .setFileSize(30 * Utils.MB_IN_BYTES)
1330                     .build();
1331             blobData2.prepare();
1332 
1333             commitBlob(blobData1);
1334             commitBlob(blobData2);
1335             acquireLease(mContext, blobData1.getBlobHandle(), R.string.test_desc,
1336                     System.currentTimeMillis() + waitDurationMs);
1337 
1338             assertThrows(LimitExceededException.class, () -> mBlobStoreManager.acquireLease(
1339                     blobData2.getBlobHandle(), R.string.test_desc));
1340 
1341             waitForLeaseExpiration(waitDurationMs, blobData1.getBlobHandle());
1342             acquireLease(mContext, blobData2.getBlobHandle(), R.string.test_desc);
1343         }, Pair.create(KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, String.valueOf(limitBytes)),
1344                 Pair.create(KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION,
1345                         String.valueOf((double) limitBytes / totalBytes)));
1346     }
1347 
1348     @Test
testRemainingLeaseQuota()1349     public void testRemainingLeaseQuota() throws Exception {
1350         final long initialRemainingQuota = mBlobStoreManager.getRemainingLeaseQuotaBytes();
1351         final long blobSize = 100 * Utils.MB_IN_BYTES;
1352         final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
1353                 .setFileSize(blobSize)
1354                 .build();
1355         blobData.prepare();
1356 
1357         commitBlob(blobData);
1358         acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
1359                 blobData.getExpiryTimeMillis());
1360         assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
1361 
1362         assertThat(mBlobStoreManager.getRemainingLeaseQuotaBytes())
1363                 .isEqualTo(initialRemainingQuota - blobSize);
1364 
1365         releaseLease(mContext, blobData.getBlobHandle());
1366         assertNoLeasedBlobs(mBlobStoreManager);
1367 
1368         assertThat(mBlobStoreManager.getRemainingLeaseQuotaBytes())
1369                 .isEqualTo(initialRemainingQuota);
1370     }
1371 
1372     @Test
testRemainingLeaseQuota_withExpiredLease()1373     public void testRemainingLeaseQuota_withExpiredLease() throws Exception {
1374         final long initialRemainingQuota = mBlobStoreManager.getRemainingLeaseQuotaBytes();
1375         final long blobSize = 100 * Utils.MB_IN_BYTES;
1376         final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
1377                 .setFileSize(blobSize)
1378                 .build();
1379         blobData.prepare();
1380 
1381         final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
1382         commitBlob(blobData);
1383         acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
1384                 System.currentTimeMillis() + waitDurationMs);
1385         assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
1386 
1387         assertThat(mBlobStoreManager.getRemainingLeaseQuotaBytes())
1388                 .isEqualTo(initialRemainingQuota - blobSize);
1389 
1390         waitForLeaseExpiration(waitDurationMs, blobData.getBlobHandle());
1391         assertNoLeasedBlobs(mBlobStoreManager);
1392 
1393         assertThat(mBlobStoreManager.getRemainingLeaseQuotaBytes())
1394                 .isEqualTo(initialRemainingQuota);
1395     }
1396 
1397     @Test
testAccessExpiredBlob()1398     public void testAccessExpiredBlob() throws Exception {
1399         final long expiryDurationMs = TimeUnit.SECONDS.toMillis(6);
1400         final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
1401                 .setExpiryDurationMs(expiryDurationMs)
1402                 .build();
1403         blobData.prepare();
1404 
1405         final long startTimeMs = System.currentTimeMillis();
1406         final long blobId = commitBlob(blobData);
1407         assertTrue(blobExists(blobId, mUserId));
1408 
1409         // Wait for the blob to expire
1410         SystemClock.sleep(expiryDurationMs);
1411 
1412         assertThrows(SecurityException.class,
1413                 () -> mBlobStoreManager.openBlob(blobData.getBlobHandle()));
1414         assertThrows(SecurityException.class,
1415                 () -> mBlobStoreManager.acquireLease(blobData.getBlobHandle(),
1416                         R.string.test_desc));
1417 
1418         triggerIdleMaintenance();
1419         assertFalse(blobExists(blobId, mUserId));
1420     }
1421 
1422     @Test
testAccessExpiredBlob_withLeaseAcquired()1423     public void testAccessExpiredBlob_withLeaseAcquired() throws Exception {
1424         final long expiryDurationMs = TimeUnit.SECONDS.toMillis(6);
1425         final FakeBlobData blobData = new FakeBlobData.Builder(mContext)
1426                 .setExpiryDurationMs(expiryDurationMs)
1427                 .build();
1428         blobData.prepare();
1429 
1430         final long startTimeMs = System.currentTimeMillis();
1431         final long blobId = commitBlob(blobData);
1432         assertTrue(blobExists(blobId, mUserId));
1433 
1434         final long commitDurationMs = System.currentTimeMillis() - startTimeMs;
1435         acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc);
1436 
1437         SystemClock.sleep(Math.abs(expiryDurationMs - commitDurationMs));
1438 
1439         assertThrows(SecurityException.class,
1440                 () -> mBlobStoreManager.openBlob(blobData.getBlobHandle()));
1441         assertThrows(SecurityException.class,
1442                 () -> mBlobStoreManager.acquireLease(blobData.getBlobHandle(),
1443                         R.string.test_desc));
1444 
1445         triggerIdleMaintenance();
1446         assertFalse(blobExists(blobId, mUserId));
1447     }
1448 
1449     @Test
testAccessBlob_withExpiredLease()1450     public void testAccessBlob_withExpiredLease() throws Exception {
1451         final long leaseExpiryDurationMs = TimeUnit.SECONDS.toMillis(2);
1452         final long leaseAcquisitionWaitDurationMs = TimeUnit.SECONDS.toMillis(1);
1453         runWithKeyValues(() -> {
1454             final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1455             blobData.prepare();
1456             try {
1457                 final long blobId = commitBlob(blobData);
1458                 assertTrue(blobExists(blobId, mUserId));
1459 
1460                 acquireLease(mContext, blobData.getBlobHandle(), R.string.test_desc,
1461                         System.currentTimeMillis() + leaseExpiryDurationMs);
1462                 assertLeasedBlobs(mBlobStoreManager, blobData.getBlobHandle());
1463 
1464                 waitForLeaseExpiration(leaseExpiryDurationMs, blobData.getBlobHandle());
1465                 assertNoLeasedBlobs(mBlobStoreManager);
1466 
1467                 triggerIdleMaintenance();
1468                 assertFalse(blobExists(blobId, mUserId));
1469 
1470                 assertThrows(SecurityException.class, () -> mBlobStoreManager.acquireLease(
1471                         blobData.getBlobHandle(), R.string.test_desc,
1472                         blobData.getExpiryTimeMillis()));
1473                 assertNoLeasedBlobs(mBlobStoreManager);
1474             } finally {
1475                 blobData.delete();
1476             }
1477         }, Pair.create(KEY_LEASE_ACQUISITION_WAIT_DURATION_MS,
1478                 String.valueOf(leaseAcquisitionWaitDurationMs)));
1479     }
1480 
1481     @Test
testCommitBlobAfterIdleMaintenance()1482     public void testCommitBlobAfterIdleMaintenance() throws Exception {
1483         final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1484         blobData.prepare();
1485         final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
1486         final long partialFileSize = 100L;
1487         final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
1488         assertThat(sessionId).isGreaterThan(0L);
1489 
1490         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1491             blobData.writeToSession(session, 0, partialFileSize, blobData.getFileSize());
1492         }
1493 
1494         SystemClock.sleep(waitDurationMs);
1495 
1496         // Trigger idle maintenance which takes of deleting expired sessions.
1497         triggerIdleMaintenance();
1498 
1499         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1500             blobData.writeToSession(session, partialFileSize,
1501                     blobData.getFileSize() - partialFileSize, blobData.getFileSize());
1502             commitSession(sessionId, session, blobData.getBlobHandle());
1503         }
1504     }
1505 
1506     @Test
testExpiredSessionsDeleted()1507     public void testExpiredSessionsDeleted() throws Exception {
1508         final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
1509         runWithKeyValues(() -> {
1510             final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1511             blobData.prepare();
1512 
1513             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
1514             assertThat(sessionId).isGreaterThan(0L);
1515 
1516             SystemClock.sleep(waitDurationMs);
1517 
1518             // Trigger idle maintenance which takes of deleting expired sessions.
1519             triggerIdleMaintenance();
1520 
1521             assertThrows(SecurityException.class, () -> mBlobStoreManager.openSession(sessionId));
1522         }, Pair.create(KEY_SESSION_EXPIRY_TIMEOUT_MS, String.valueOf(waitDurationMs)));
1523     }
1524 
1525     @Test
testExpiredSessionsDeleted_withPartialData()1526     public void testExpiredSessionsDeleted_withPartialData() throws Exception {
1527         final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
1528         runWithKeyValues(() -> {
1529             final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1530             blobData.prepare();
1531 
1532             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
1533             assertThat(sessionId).isGreaterThan(0L);
1534 
1535             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1536                 blobData.writeToSession(session, 0, 100, blobData.getFileSize());
1537             }
1538 
1539             SystemClock.sleep(waitDurationMs);
1540 
1541             // Trigger idle maintenance which takes of deleting expired sessions.
1542             triggerIdleMaintenance();
1543 
1544             assertThrows(SecurityException.class, () -> mBlobStoreManager.openSession(sessionId));
1545         }, Pair.create(KEY_SESSION_EXPIRY_TIMEOUT_MS, String.valueOf(waitDurationMs)));
1546     }
1547 
1548     @Test
testCreateSession_countLimitExceeded()1549     public void testCreateSession_countLimitExceeded() throws Exception {
1550         runWithKeyValues(() -> {
1551             final FakeBlobData blobData1 = new FakeBlobData.Builder(mContext).build();
1552             blobData1.prepare();
1553             final FakeBlobData blobData2 = new FakeBlobData.Builder(mContext).build();
1554             blobData2.prepare();
1555 
1556             mBlobStoreManager.createSession(blobData1.getBlobHandle());
1557             assertThrows(LimitExceededException.class,
1558                     () -> mBlobStoreManager.createSession(blobData2.getBlobHandle()));
1559         }, Pair.create(KEY_MAX_ACTIVE_SESSIONS, String.valueOf(1)));
1560     }
1561 
1562     @Test
testCommitSession_countLimitExceeded()1563     public void testCommitSession_countLimitExceeded() throws Exception {
1564         runWithKeyValues(() -> {
1565             final FakeBlobData blobData1 = new FakeBlobData.Builder(mContext).build();
1566             blobData1.prepare();
1567             final FakeBlobData blobData2 = new FakeBlobData.Builder(mContext).build();
1568             blobData2.prepare();
1569 
1570             commitBlob(blobData1, null /* accessModifier */, true /* expectSuccess */);
1571             commitBlob(blobData2, null /* accessModifier */, false /* expectSuccess */);
1572         }, Pair.create(KEY_MAX_COMMITTED_BLOBS, String.valueOf(1)));
1573     }
1574 
1575     @Test
testLeaseBlob_countLimitExceeded()1576     public void testLeaseBlob_countLimitExceeded() throws Exception {
1577         runWithKeyValues(() -> {
1578             final FakeBlobData blobData1 = new FakeBlobData.Builder(mContext).build();
1579             blobData1.prepare();
1580             final FakeBlobData blobData2 = new FakeBlobData.Builder(mContext).build();
1581             blobData2.prepare();
1582 
1583             commitBlob(blobData1);
1584             commitBlob(blobData2);
1585 
1586             acquireLease(mContext, blobData1.getBlobHandle(), "test desc");
1587             assertThrows(LimitExceededException.class,
1588                     () -> acquireLease(mContext, blobData2.getBlobHandle(), "test desc"));
1589         }, Pair.create(KEY_MAX_LEASED_BLOBS, String.valueOf(1)));
1590     }
1591 
1592     @Test
testExpiredLease_countLimitExceeded()1593     public void testExpiredLease_countLimitExceeded() throws Exception {
1594         runWithKeyValues(() -> {
1595             final FakeBlobData blobData1 = new FakeBlobData.Builder(mContext).build();
1596             blobData1.prepare();
1597             final FakeBlobData blobData2 = new FakeBlobData.Builder(mContext).build();
1598             blobData2.prepare();
1599             final FakeBlobData blobData3 = new FakeBlobData.Builder(mContext).build();
1600             blobData3.prepare();
1601             final long waitDurationMs = TimeUnit.SECONDS.toMillis(2);
1602 
1603             commitBlob(blobData1);
1604             commitBlob(blobData2);
1605             commitBlob(blobData3);
1606 
1607             acquireLease(mContext, blobData1.getBlobHandle(), "test desc1",
1608                     System.currentTimeMillis() + waitDurationMs);
1609             assertThrows(LimitExceededException.class,
1610                     () -> acquireLease(mContext, blobData2.getBlobHandle(), "test desc2",
1611                             System.currentTimeMillis() + TimeUnit.HOURS.toMillis(4)));
1612 
1613             waitForLeaseExpiration(waitDurationMs, blobData1.getBlobHandle());
1614 
1615             acquireLease(mContext, blobData2.getBlobHandle(), "test desc2",
1616                     System.currentTimeMillis() + TimeUnit.HOURS.toMillis(4));
1617             assertThrows(LimitExceededException.class,
1618                     () -> acquireLease(mContext, blobData3.getBlobHandle(), "test desc3"));
1619         }, Pair.create(KEY_MAX_LEASED_BLOBS, String.valueOf(1)));
1620     }
1621 
1622     @Test
testAllowPackageAccess_countLimitExceeded()1623     public void testAllowPackageAccess_countLimitExceeded() throws Exception {
1624         runWithKeyValues(() -> {
1625             final FakeBlobData blobData = new FakeBlobData.Builder(mContext).build();
1626             blobData.prepare();
1627 
1628             final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
1629             assertThat(sessionId).isGreaterThan(0L);
1630             try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1631                 blobData.writeToSession(session);
1632 
1633                 session.allowPackageAccess(HELPER_PKG2, HELPER_PKG2_CERT_SHA256);
1634                 assertThrows(LimitExceededException.class,
1635                         () -> session.allowPackageAccess(HELPER_PKG3, HELPER_PKG3_CERT_SHA256));
1636             }
1637         }, Pair.create(KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES, String.valueOf(1)));
1638     }
1639 
1640     @Test
testBlobHandleEquality()1641     public void testBlobHandleEquality() throws Exception {
1642         // Check that BlobHandle objects are considered equal when digest, label, expiry time
1643         // and tag are equal.
1644         {
1645             final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest".getBytes(),
1646                     "Fake blob", 1111L, "tag");
1647             final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest".getBytes(),
1648                     "Fake blob", 1111L, "tag");
1649             assertThat(blobHandle1).isEqualTo(blobHandle2);
1650         }
1651 
1652         // Check that BlobHandle objects are not equal if digests are not equal.
1653         {
1654             final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
1655                     "Fake blob", 1111L, "tag");
1656             final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest2".getBytes(),
1657                     "Fake blob", 1111L, "tag");
1658             assertThat(blobHandle1).isNotEqualTo(blobHandle2);
1659         }
1660 
1661         // Check that BlobHandle objects are not equal if expiry times are not equal.
1662         {
1663             final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest".getBytes(),
1664                     "Fake blob", 1111L, "tag");
1665             final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest".getBytes(),
1666                     "Fake blob", 1112L, "tag");
1667             assertThat(blobHandle1).isNotEqualTo(blobHandle2);
1668         }
1669 
1670         // Check that BlobHandle objects are not equal if labels are not equal.
1671         {
1672             final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest".getBytes(),
1673                     "Fake blob1", 1111L, "tag");
1674             final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest".getBytes(),
1675                     "Fake blob2", 1111L, "tag");
1676             assertThat(blobHandle1).isNotEqualTo(blobHandle2);
1677         }
1678 
1679         // Check that BlobHandle objects are not equal if tags are not equal.
1680         {
1681             final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest".getBytes(),
1682                     "Fake blob", 1111L, "tag1");
1683             final BlobHandle blobHandle2 = BlobHandle.createWithSha256("digest".getBytes(),
1684                     "Fake blob", 1111L, "tag2");
1685             assertThat(blobHandle1).isNotEqualTo(blobHandle2);
1686         }
1687     }
1688 
1689     @Test
testBlobHandleCreation()1690     public void testBlobHandleCreation() throws Exception {
1691         // Creating a BlobHandle with label > 100 chars will fail
1692         {
1693             final CharSequence label = String.join("", Collections.nCopies(101, "a"));
1694             assertThrows(IllegalArgumentException.class,
1695                     () -> BlobHandle.createWithSha256("digest".getBytes(), label, 1111L, "tag"));
1696         }
1697 
1698         // Creating a BlobHandle with tag > 128 chars will fail
1699         {
1700             final String tag = String.join("", Collections.nCopies(129, "a"));
1701             assertThrows(IllegalArgumentException.class,
1702                     () -> BlobHandle.createWithSha256("digest".getBytes(), "label", 1111L, tag));
1703         }
1704     }
1705 
runWithKeyValues(ThrowingRunnable runnable, Pair<String, String>... keyValues)1706     private static void runWithKeyValues(ThrowingRunnable runnable,
1707             Pair<String, String>... keyValues) throws Exception {
1708         final Map<String, String> previousValues = new ArrayMap();
1709         SystemUtil.runWithShellPermissionIdentity(() -> {
1710             for (Pair<String, String> pair : keyValues) {
1711                 final String key = pair.first;
1712                 final String value = pair.second;
1713                 final String previousValue = DeviceConfig.getProperty(NAMESPACE_BLOBSTORE, key);
1714                 if (!Objects.equals(previousValue, value)) {
1715                     previousValues.put(key, previousValue);
1716                     Log.i(TAG, key + " previous value: " + previousValue);
1717                     assertThat(DeviceConfig.setProperty(NAMESPACE_BLOBSTORE, key, value,
1718                             false /* makeDefault */)).isTrue();
1719                 }
1720                 Log.i(TAG, key + " value set: " + value);
1721             }
1722         });
1723         try {
1724             runnable.run();
1725         } finally {
1726             SystemUtil.runWithShellPermissionIdentity(() -> {
1727                 previousValues.forEach((key, previousValue) -> {
1728                     final String currentValue = DeviceConfig.getProperty(
1729                             NAMESPACE_BLOBSTORE, key);
1730                     if (!Objects.equals(previousValue, currentValue)) {
1731                         assertThat(DeviceConfig.setProperty(NAMESPACE_BLOBSTORE,
1732                                 key, previousValue, false /* makeDefault */)).isTrue();
1733                         Log.i(TAG, key + " value restored: " + previousValue);
1734                     }
1735                 });
1736             });
1737         }
1738     }
1739 
blobExists(long blobId, int userId)1740     private static boolean blobExists(long blobId, int userId) throws Exception {
1741         final String cmd = String.format(
1742                 "cmd blob_store query-blob-existence -u %d -b %d", userId, blobId);
1743         return "1".equals(runShellCmd(cmd));
1744     }
1745 
commitAndVerifyBlob(FakeBlobData blobData)1746     private void commitAndVerifyBlob(FakeBlobData blobData) throws Exception {
1747         commitBlob(blobData);
1748 
1749         // Verify that blob can be accessed after committing.
1750         try (ParcelFileDescriptor pfd = mBlobStoreManager.openBlob(blobData.getBlobHandle())) {
1751             assertThat(pfd).isNotNull();
1752             blobData.verifyBlob(pfd);
1753         }
1754     }
1755 
commitBlob(FakeBlobData blobData)1756     private long commitBlob(FakeBlobData blobData) throws Exception {
1757         return commitBlob(blobData, null);
1758     }
1759 
commitBlob(FakeBlobData blobData, AccessModifier accessModifier)1760     private long commitBlob(FakeBlobData blobData,
1761             AccessModifier accessModifier) throws Exception {
1762         return commitBlob(blobData, accessModifier, true /* expectSuccess */);
1763     }
1764 
commitBlob(FakeBlobData blobData, AccessModifier accessModifier, boolean expectSuccess)1765     private long commitBlob(FakeBlobData blobData,
1766             AccessModifier accessModifier, boolean expectSuccess) throws Exception {
1767         final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
1768         assertThat(sessionId).isGreaterThan(0L);
1769         try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
1770             blobData.writeToSession(session);
1771 
1772             if (accessModifier != null) {
1773                 accessModifier.modify(session);
1774             }
1775             commitSession(sessionId, session, blobData.getBlobHandle(), expectSuccess);
1776         }
1777         return sessionId;
1778     }
1779 
commitSession(long sessionId, BlobStoreManager.Session session, BlobHandle blobHandle)1780     private void commitSession(long sessionId, BlobStoreManager.Session session,
1781             BlobHandle blobHandle) throws Exception {
1782         commitSession(sessionId, session, blobHandle, true /* expectSuccess */);
1783     }
1784 
commitSession(long sessionId, BlobStoreManager.Session session, BlobHandle blobHandle, boolean expectSuccess)1785     private void commitSession(long sessionId, BlobStoreManager.Session session,
1786             BlobHandle blobHandle, boolean expectSuccess) throws Exception {
1787         Log.d(TAG, "Committing session: " + sessionId + "; blob: " + blobHandle);
1788         final CompletableFuture<Integer> callback = new CompletableFuture<>();
1789         session.commit(mContext.getMainExecutor(), callback::complete);
1790         final int result = callback.get(TIMEOUT_COMMIT_CALLBACK_SEC, TimeUnit.SECONDS);
1791         if (expectSuccess) {
1792             assertThat(result).isEqualTo(0);
1793         } else {
1794             assertThat(result).isNotEqualTo(0);
1795         }
1796     }
1797 
1798     private interface AccessModifier {
modify(BlobStoreManager.Session session)1799         void modify(BlobStoreManager.Session session) throws Exception;
1800     }
1801 
commitBlobFromPkg(FakeBlobData blobData, TestServiceConnection serviceConnection)1802     private void commitBlobFromPkg(FakeBlobData blobData, TestServiceConnection serviceConnection)
1803             throws Exception {
1804         commitBlobFromPkg(blobData, ICommandReceiver.FLAG_ACCESS_TYPE_PRIVATE, serviceConnection);
1805     }
1806 
commitBlobFromPkg(FakeBlobData blobData, int accessTypeFlags, TestServiceConnection serviceConnection)1807     private void commitBlobFromPkg(FakeBlobData blobData, int accessTypeFlags,
1808             TestServiceConnection serviceConnection) throws Exception {
1809         final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
1810         try (ParcelFileDescriptor pfd = blobData.openForRead()) {
1811             assertThat(commandReceiver.commit(blobData.getBlobHandle(),
1812                     pfd, accessTypeFlags, TIMEOUT_COMMIT_CALLBACK_SEC, blobData.getFileSize()))
1813                             .isEqualTo(0);
1814         }
1815     }
1816 
acquireLeaseFrmPkg(FakeBlobData blobData, TestServiceConnection serviceConnection)1817     private void acquireLeaseFrmPkg(FakeBlobData blobData, TestServiceConnection serviceConnection)
1818             throws Exception {
1819         final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
1820         commandReceiver.acquireLease(blobData.getBlobHandle());
1821     }
1822 
openSessionFromPkg(long sessionId, String pkg)1823     private void openSessionFromPkg(long sessionId, String pkg) throws Exception {
1824         final TestServiceConnection serviceConnection = bindToHelperService(pkg);
1825         try {
1826             final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
1827             commandReceiver.openSession(sessionId);
1828         } finally {
1829             serviceConnection.unbind();
1830         }
1831     }
1832 
acquireLeaseAndAssertPkgCanAccess(FakeBlobData blobData, String pkg)1833     private void acquireLeaseAndAssertPkgCanAccess(FakeBlobData blobData, String pkg)
1834             throws Exception {
1835         final TestServiceConnection serviceConnection = bindToHelperService(pkg);
1836         try {
1837             acquireLeaseAndAssertPkgCanAccess(blobData, serviceConnection);
1838         } finally {
1839             serviceConnection.unbind();
1840         }
1841     }
1842 
acquireLeaseAndAssertPkgCanAccess(FakeBlobData blobData, TestServiceConnection serviceConnection)1843     private void acquireLeaseAndAssertPkgCanAccess(FakeBlobData blobData,
1844             TestServiceConnection serviceConnection) throws Exception {
1845         final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
1846         commandReceiver.acquireLease(blobData.getBlobHandle());
1847         try (ParcelFileDescriptor pfd = commandReceiver.openBlob(blobData.getBlobHandle())) {
1848             assertThat(pfd).isNotNull();
1849             blobData.verifyBlob(pfd);
1850         }
1851     }
1852 
assertPkgCanAccess(FakeBlobData blobData, TestServiceConnection serviceConnection)1853     private void assertPkgCanAccess(FakeBlobData blobData, TestServiceConnection serviceConnection)
1854             throws Exception {
1855         final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
1856         try (ParcelFileDescriptor pfd = commandReceiver.openBlob(blobData.getBlobHandle())) {
1857             assertThat(pfd).isNotNull();
1858             blobData.verifyBlob(pfd);
1859         }
1860     }
1861 
assertPkgCannotAccess(FakeBlobData blobData, String pkg)1862     private void assertPkgCannotAccess(FakeBlobData blobData, String pkg) throws Exception {
1863         final TestServiceConnection serviceConnection = bindToHelperService(pkg);
1864         try {
1865             assertPkgCannotAccess(blobData, serviceConnection);
1866         } finally {
1867             serviceConnection.unbind();
1868         }
1869     }
1870 
assertPkgCannotAccess(FakeBlobData blobData, TestServiceConnection serviceConnection)1871     private void assertPkgCannotAccess(FakeBlobData blobData,
1872         TestServiceConnection serviceConnection) throws Exception {
1873         final ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
1874         assertThrows(SecurityException.class,
1875                 () -> commandReceiver.acquireLease(blobData.getBlobHandle()));
1876         assertThrows(SecurityException.class,
1877                 () -> commandReceiver.openBlob(blobData.getBlobHandle()));
1878     }
1879 
assertSizeBytesMostlyEquals(long expected, long actual)1880     private void assertSizeBytesMostlyEquals(long expected, long actual) {
1881         assertWithMessage("expected:" + expected + "; actual:" + actual)
1882                 .that(Math.abs(expected - actual))
1883                 .isLessThan(DELTA_BYTES);
1884     }
1885 
waitForLeaseExpiration(long waitDurationMs, BlobHandle leasedBlob)1886     private void waitForLeaseExpiration(long waitDurationMs, BlobHandle leasedBlob)
1887             throws Exception {
1888         SystemClock.sleep(waitDurationMs);
1889         assertThat(mBlobStoreManager.getLeaseInfo(leasedBlob)).isNull();
1890     }
1891 
bindToHelperService(String pkg)1892     private TestServiceConnection bindToHelperService(String pkg) throws Exception {
1893         final TestServiceConnection serviceConnection = new TestServiceConnection(mContext);
1894         final Intent intent = new Intent()
1895                 .setComponent(new ComponentName(pkg, HELPER_SERVICE));
1896         mContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
1897         return serviceConnection;
1898     }
1899 
1900     private class TestServiceConnection implements ServiceConnection {
1901         private final Context mContext;
1902         private final BlockingQueue<IBinder> mBlockingQueue = new LinkedBlockingQueue<>();
1903         private ICommandReceiver mCommandReceiver;
1904 
TestServiceConnection(Context context)1905         TestServiceConnection(Context context) {
1906             mContext = context;
1907         }
1908 
onServiceConnected(ComponentName componentName, IBinder service)1909         public void onServiceConnected(ComponentName componentName, IBinder service) {
1910             Log.i(TAG, "Service got connected: " + componentName);
1911             mBlockingQueue.offer(service);
1912         }
1913 
onServiceDisconnected(ComponentName componentName)1914         public void onServiceDisconnected(ComponentName componentName) {
1915             Log.e(TAG, "Service got disconnected: " + componentName);
1916         }
1917 
getService()1918         private IBinder getService() throws Exception {
1919             final IBinder service = mBlockingQueue.poll(TIMEOUT_BIND_SERVICE_SEC,
1920                     TimeUnit.SECONDS);
1921             return service;
1922         }
1923 
getCommandReceiver()1924         public ICommandReceiver getCommandReceiver() throws Exception {
1925             if (mCommandReceiver == null) {
1926                 mCommandReceiver = ICommandReceiver.Stub.asInterface(getService());
1927             }
1928             return mCommandReceiver;
1929         }
1930 
unbind()1931         public void unbind() {
1932             mCommandReceiver = null;
1933             mContext.unbindService(this);
1934         }
1935     }
1936 }
1937