• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google LLC
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.google.android.libraries.mobiledatadownload.file.backends;
17 
18 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
19 import static com.google.android.libraries.mobiledatadownload.file.common.internal.Charsets.UTF_8;
20 import static com.google.common.truth.Truth.assertThat;
21 import static org.junit.Assert.assertThrows;
22 
23 import android.app.blob.BlobStoreManager;
24 import android.content.Context;
25 import android.net.Uri;
26 import android.os.ParcelFileDescriptor;
27 import android.util.Log;
28 import android.util.Pair;
29 import androidx.test.core.app.ApplicationProvider;
30 import androidx.test.uiautomator.UiDevice;
31 import com.google.android.libraries.mobiledatadownload.file.common.LimitExceededException;
32 import com.google.common.io.BaseEncoding;
33 import com.google.common.io.ByteStreams;
34 import java.io.Closeable;
35 import java.io.FileInputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.OutputStream;
39 import java.security.MessageDigest;
40 import java.util.Random;
41 import org.junit.After;
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.junit.runners.JUnit4;
46 
47 @RunWith(JUnit4.class)
48 public class BlobStoreBackendTest {
49   public static final String TAG = "BlobStoreBackendTest";
50 
51   private Context context;
52   private BlobStoreBackend backend;
53   private BlobStoreManager blobStoreManager;
54 
55   @Before
setUpStorage()56   public final void setUpStorage() {
57     context = ApplicationProvider.getApplicationContext();
58     backend = new BlobStoreBackend(context);
59     blobStoreManager = (BlobStoreManager) context.getSystemService(Context.BLOB_STORE_SERVICE);
60   }
61 
62   @After
tearDown()63   public void tearDown() throws Exception {
64     // Commands to clean up the blob storage.
65     runShellCmd("cmd blob_store clear-all-sessions");
66     runShellCmd("cmd blob_store clear-all-blobs");
67     context.getFilesDir().delete();
68   }
69 
70   @Test
nativeReadAfterWrite_succeeds()71   public void nativeReadAfterWrite_succeeds() throws Exception {
72     byte[] content = "nativeReadAfterWrite_succeeds".getBytes(UTF_8);
73     String checksum = computeDigest(content);
74     Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build();
75     int numOfLeases = blobStoreManager.getLeasedBlobs().size();
76 
77     try (OutputStream out = backend.openForWrite(uri)) {
78       assertThat(out).isNotNull();
79       out.write(content);
80     }
81     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases);
82 
83     Uri uriForRead = BlobUri.builder(context).setBlobParameters(checksum).build();
84     Pair<Uri, Closeable> closeableUri = backend.openForNativeRead(uriForRead);
85     assertThat(closeableUri.second).isNotNull();
86     int nativeFd = FileDescriptorUri.getFd(closeableUri.first);
87     try (InputStream in =
88             new FileInputStream(ParcelFileDescriptor.fromFd(nativeFd).getFileDescriptor());
89         Closeable c = closeableUri.second) {
90       assertThat(ByteStreams.toByteArray(in)).isEqualTo(content);
91     }
92   }
93 
94   @Test
readAfterWrite_succeeds()95   public void readAfterWrite_succeeds() throws Exception {
96     byte[] content = "readAfterWrite_succeeds".getBytes(UTF_8);
97     String checksum = computeDigest(content);
98     Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build();
99     int numOfLeases = blobStoreManager.getLeasedBlobs().size();
100 
101     try (OutputStream out = backend.openForWrite(uri)) {
102       assertThat(out).isNotNull();
103       out.write(content);
104     }
105     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases);
106 
107     Uri uriForRead = BlobUri.builder(context).setBlobParameters(checksum).build();
108     try (InputStream in = backend.openForRead(uriForRead)) {
109       assertThat(in).isNotNull();
110       assertThat(ByteStreams.toByteArray(in)).isEqualTo(content);
111     }
112   }
113 
114   @Test
exists()115   public void exists() throws Exception {
116     byte[] content = "exists".getBytes(UTF_8);
117     String checksum = computeDigest(content);
118     Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build();
119 
120     assertThat(backend.exists(uri)).isFalse();
121 
122     try (OutputStream out = backend.openForWrite(uri)) {
123       assertThat(out).isNotNull();
124       out.write(content);
125     }
126 
127     assertThat(backend.exists(uri)).isTrue();
128   }
129 
130   @Test
writeLease_succeeds()131   public void writeLease_succeeds() throws Exception {
132     byte[] content = "writeLease_succeeds".getBytes(UTF_8);
133     String checksum = computeDigest(content);
134     Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build();
135     int numOfLeases = blobStoreManager.getLeasedBlobs().size();
136     try (OutputStream out = backend.openForWrite(blobUri)) {
137       assertThat(out).isNotNull();
138       out.write(content);
139     }
140 
141     Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build();
142 
143     OutputStream out = backend.openForWrite(leaseUri);
144     assertThat(out).isNull();
145 
146     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 1);
147   }
148 
149   @Test
writeLeaseNonExistentFile_shouldThrow()150   public void writeLeaseNonExistentFile_shouldThrow() throws Exception {
151     byte[] content = "writeLeaseNonExistentFile_shouldThrow".getBytes(UTF_8);
152     String checksum = computeDigest(content);
153 
154     Uri uri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build();
155 
156     assertThrows(SecurityException.class, () -> backend.openForWrite(uri));
157   }
158 
159   @Test
delete_succeeds()160   public void delete_succeeds() throws Exception {
161     byte[] content = "delete_succeeds".getBytes(UTF_8);
162     String checksum = computeDigest(content);
163     Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build();
164     int numOfLeases = blobStoreManager.getLeasedBlobs().size();
165 
166     try (OutputStream out = backend.openForWrite(blobUri)) {
167       assertThat(out).isNotNull();
168       out.write(content);
169     }
170 
171     Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build();
172     try (OutputStream out = backend.openForWrite(leaseUri)) {
173       assertThat(out).isNull();
174     }
175     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 1);
176 
177     backend.deleteFile(leaseUri);
178 
179     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases);
180   }
181 
182   @Test
releaseAllLeases_succeeds()183   public void releaseAllLeases_succeeds() throws Exception {
184     // Write and acquire lease on first file
185     byte[] content = "releaseAllLeases_succeeds_1".getBytes(UTF_8);
186     String checksum = computeDigest(content);
187     Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build();
188     int numOfLeases = blobStoreManager.getLeasedBlobs().size();
189     try (OutputStream out = backend.openForWrite(blobUri)) {
190       assertThat(out).isNotNull();
191       out.write(content);
192     }
193     Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build();
194     try (OutputStream out = backend.openForWrite(leaseUri)) {
195       assertThat(out).isNull();
196     }
197     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 1);
198 
199     // Write and acquire lease on second file
200     content = "releaseAllLeases_succeeds_2".getBytes(UTF_8);
201     checksum = computeDigest(content);
202     blobUri = BlobUri.builder(context).setBlobParameters(checksum).build();
203     try (OutputStream out = backend.openForWrite(blobUri)) {
204       assertThat(out).isNotNull();
205       out.write(content);
206     }
207     leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build();
208     try (OutputStream out = backend.openForWrite(leaseUri)) {
209       assertThat(out).isNull();
210     }
211     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + 2);
212 
213     // Release all leases
214     Uri allLeases = BlobUri.builder(context).setAllLeasesParameters().build();
215 
216     backend.deleteFile(allLeases);
217 
218     assertThat(blobStoreManager.getLeasedBlobs()).isEmpty();
219   }
220 
221   @Test
deleteNonExistentFile_shouldThrow()222   public void deleteNonExistentFile_shouldThrow() throws Exception {
223     BlobStoreBackend backend = new BlobStoreBackend(context);
224     byte[] content = "deleteNonExistentFile_shouldThrow".getBytes(UTF_8);
225     String checksum = computeDigest(content);
226 
227     Uri uri = BlobUri.builder(context).setBlobParameters(checksum).build();
228 
229     assertThrows(IOException.class, () -> backend.deleteFile(uri));
230   }
231 
computeDigest(byte[] byteContent)232   private static String computeDigest(byte[] byteContent) throws Exception {
233     MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
234     if (messageDigest == null) {
235       return "";
236     }
237     return BaseEncoding.base16().lowerCase().encode(messageDigest.digest(byteContent));
238   }
239 
240   @Test
writeLeaseExceedsLimit_shouldThrow()241   public void writeLeaseExceedsLimit_shouldThrow() throws Exception {
242     long initialRemainingQuota = blobStoreManager.getRemainingLeaseQuotaBytes();
243     int numOfLeases = blobStoreManager.getLeasedBlobs().size();
244     int numberOfFiles = 20;
245     int singleFileSize = (int) initialRemainingQuota / numberOfFiles;
246     long expectedRemainingQuota = initialRemainingQuota - singleFileSize * numberOfFiles;
247 
248     // Create small files rather than one big file to avoid OutOfMemoryError
249     for (int i = 0; i < numberOfFiles; i++) {
250       byte[] content = generateRandomBytes(singleFileSize);
251       String checksum = computeDigest(content);
252       Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build();
253 
254       try (OutputStream out = backend.openForWrite(blobUri)) {
255         assertThat(out).isNotNull();
256         out.write(content);
257       }
258       Uri leaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build();
259       OutputStream out = backend.openForWrite(leaseUri);
260       assertThat(out).isNull();
261     }
262     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + numberOfFiles);
263     assertThat(blobStoreManager.getRemainingLeaseQuotaBytes()).isEqualTo(expectedRemainingQuota);
264 
265     // Write one more file bigger than available quota. Acquiring the lease on it will throw
266     // LimitExceededException.
267     byte[] content = generateRandomBytes((int) expectedRemainingQuota + 1);
268     String checksum = computeDigest(content);
269     Uri blobUri = BlobUri.builder(context).setBlobParameters(checksum).build();
270     try (OutputStream out = backend.openForWrite(blobUri)) {
271       assertThat(out).isNotNull();
272       out.write(content);
273     }
274     Uri exceedingLeaseUri = BlobUri.builder(context).setLeaseParameters(checksum, 0).build();
275 
276     assertThrows(LimitExceededException.class, () -> backend.openForWrite(exceedingLeaseUri));
277 
278     assertThat(blobStoreManager.getLeasedBlobs()).hasSize(numOfLeases + numberOfFiles);
279     assertThat(blobStoreManager.getRemainingLeaseQuotaBytes()).isEqualTo(expectedRemainingQuota);
280   }
281 
generateRandomBytes(int length)282   private static byte[] generateRandomBytes(int length) {
283     byte[] content = new byte[length];
284     new Random().nextBytes(content);
285     return content;
286   }
287 
runShellCmd(String cmd)288   private static String runShellCmd(String cmd) throws IOException {
289     final UiDevice uiDevice = UiDevice.getInstance(getInstrumentation());
290     final String result = uiDevice.executeShellCommand(cmd).trim();
291     Log.i(TAG, "Output of '" + cmd + "': '" + result + "'");
292     return result;
293   }
294 }
295