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