• 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 com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.createFile;
19 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.makeArrayOfBytesContent;
20 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.readFileInBytes;
21 import static com.google.common.truth.Truth.assertThat;
22 import static org.junit.Assert.assertThrows;
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.Mockito.mock;
25 import static org.mockito.Mockito.verify;
26 import static org.mockito.Mockito.when;
27 
28 import android.accounts.Account;
29 import android.content.Context;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.util.Pair;
33 import androidx.test.core.app.ApplicationProvider;
34 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
35 import com.google.android.libraries.mobiledatadownload.file.common.FileStorageUnavailableException;
36 import com.google.android.libraries.mobiledatadownload.file.common.LockScope;
37 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
38 import com.google.android.libraries.mobiledatadownload.file.common.testing.BackendTestBase;
39 import com.google.android.libraries.mobiledatadownload.file.openers.NativeReadOpener;
40 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener;
41 import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
42 import com.google.common.collect.ImmutableList;
43 import com.google.common.util.concurrent.Futures;
44 import java.io.ByteArrayInputStream;
45 import java.io.Closeable;
46 import java.io.File;
47 import java.io.IOException;
48 import java.util.List;
49 import java.util.concurrent.ExecutionException;
50 import org.junit.Test;
51 import org.junit.runner.RunWith;
52 import org.robolectric.RobolectricTestRunner;
53 import org.robolectric.annotation.Config;
54 
55 /** Tests for {@link AndroidFileBackend} */
56 @RunWith(RobolectricTestRunner.class)
57 @Config(
58     shadows = {},
59     sdk = Build.VERSION_CODES.N)
60 public class AndroidFileBackendTest extends BackendTestBase {
61 
62   private final Context context = ApplicationProvider.getApplicationContext();
63   private final Backend backend = AndroidFileBackend.builder(context).build();
64   private static final byte[] TEST_CONTENT = makeArrayOfBytesContent();
65 
66   @Override
backend()67   protected Backend backend() {
68     return backend;
69   }
70 
71   @Override
legalUriBase()72   protected Uri legalUriBase() {
73     return Uri.parse("android://" + context.getPackageName() + "/files/common/shared/");
74   }
75 
76   @Override
illegalUrisToRead()77   protected List<Uri> illegalUrisToRead() {
78     return ImmutableList.of(
79         Uri.parse(legalUriBase() + "uriWithQuery?q=a"),
80         Uri.parse("android:///null/uriWithInvalidLogicalLocation"),
81         Uri.parse("android://" + context.getPackageName()),
82         Uri.parse("android://" + context.getPackageName()));
83   }
84 
85   @Override
illegalUrisToWrite()86   protected List<Uri> illegalUrisToWrite() {
87     return ImmutableList.of(
88         Uri.parse("android://com.thirdparty.app/files/common/shared/uriAcrossAuthority"));
89   }
90 
91   @Override
illegalUrisToAppend()92   protected List<Uri> illegalUrisToAppend() {
93     return illegalUrisToWrite();
94   }
95 
96   /** Minimal tests verifying default builder behavior */
97   @Test
builder_withNullContext_shouldThrowException()98   public void builder_withNullContext_shouldThrowException() {
99     assertThrows(IllegalArgumentException.class, () -> AndroidFileBackend.builder(null));
100   }
101 
102   @Test
builder_remoteBackend_isNullByDefault()103   public void builder_remoteBackend_isNullByDefault() {
104     Uri uri = Uri.parse("android://com.thirdparty.app/files/common/shared/file");
105     AndroidFileBackend backend = AndroidFileBackend.builder(context).build();
106     assertThrows(FileStorageUnavailableException.class, () -> backend.openForRead(uri));
107   }
108 
109   @Test
builder_accountManager_isNullByDefault()110   public void builder_accountManager_isNullByDefault() {
111     Account account = new Account("<internal>@gmail.com", "google.com");
112     Uri uri = AndroidUri.builder(context).setManagedLocation().setAccount(account).build();
113     AndroidFileBackend backend = AndroidFileBackend.builder(context).build();
114     assertThrows(MalformedUriException.class, () -> backend.openForRead(uri));
115   }
116 
117   /** Tests verifying backend behavior */
118   @Test
openForWrite_shouldUseContextAuthorityIfWithoutAuthority()119   public void openForWrite_shouldUseContextAuthorityIfWithoutAuthority() throws Exception {
120     final Uri uri = Uri.parse("android:///files/writing/shared/missingAuthority");
121 
122     assertThat(storage().exists(uri)).isFalse();
123 
124     createFile(storage(), uri, TEST_CONTENT);
125 
126     assertThat(storage().exists(uri)).isTrue();
127     assertThat(readFileInBytes(storage(), uri)).isEqualTo(TEST_CONTENT);
128   }
129 
130   @Test
rename_shouldNotRenameAcrossAuthority()131   public void rename_shouldNotRenameAcrossAuthority() throws Exception {
132     final Uri from =
133         Uri.parse("android://" + context.getPackageName() + "/files/localfrom/shared/file");
134     final Uri to = Uri.parse("android://com.thirdparty.app/files/remoteto/shared/file");
135 
136     createFile(storage(), from, TEST_CONTENT);
137 
138     assertThat(storage().exists(from)).isTrue();
139     assertThrows(MalformedUriException.class, () -> storage().rename(from, to));
140     assertThat(storage().exists(from)).isTrue();
141     assertThat(readFileInBytes(storage(), from)).isEqualTo(TEST_CONTENT);
142   }
143 
144   @Test
145   @Config(sdk = Build.VERSION_CODES.N)
openForRead_directBootFilesOnNShouldUseDeviceProtectedStorageContext()146   public void openForRead_directBootFilesOnNShouldUseDeviceProtectedStorageContext()
147       throws Exception {
148     Uri uri =
149         AndroidUri.builder(context)
150             .setDirectBootFilesLocation()
151             .setModule("testboot")
152             .setRelativePath("inDirectBoot.txt")
153             .build();
154     File directBootFile =
155         new File(
156             context.createDeviceProtectedStorageContext().getFilesDir(),
157             "testboot/shared/inDirectBoot.txt");
158     File filesFile = new File(context.getFilesDir(), "testboot/shared/inDirectBoot.txt");
159 
160     createFile(storage(), uri, TEST_CONTENT);
161 
162     assertThat(filesFile.exists()).isFalse();
163     assertThat(directBootFile.exists()).isTrue();
164     assertThat(readFileInBytes(storage(), uri)).isEqualTo(TEST_CONTENT);
165   }
166 
167   @Test
168   @Config(sdk = Build.VERSION_CODES.N)
openForRead_directBootCacheOnNShouldUseDeviceProtectedStorageContext()169   public void openForRead_directBootCacheOnNShouldUseDeviceProtectedStorageContext()
170       throws Exception {
171     Uri uri =
172         AndroidUri.builder(context)
173             .setDirectBootCacheLocation()
174             .setModule("testboot")
175             .setRelativePath("inDirectBoot.txt")
176             .build();
177     File directBootFile =
178         new File(
179             context.createDeviceProtectedStorageContext().getCacheDir(),
180             "testboot/shared/inDirectBoot.txt");
181     File cacheFile = new File(context.getCacheDir(), "testboot/shared/inDirectBoot.txt");
182 
183     createFile(storage(), uri, TEST_CONTENT);
184 
185     assertThat(cacheFile.exists()).isFalse();
186     assertThat(directBootFile.exists()).isTrue();
187     assertThat(readFileInBytes(storage(), uri)).isEqualTo(TEST_CONTENT);
188   }
189 
190   @Test
191   @Config(sdk = Build.VERSION_CODES.M)
openForRead_directBootFilesBeforeNShouldThrowException()192   public void openForRead_directBootFilesBeforeNShouldThrowException() throws Exception {
193     Uri uri =
194         AndroidUri.builder(context)
195             .setDirectBootFilesLocation()
196             .setModule("testboot")
197             .setRelativePath("inDirectBoot.txt")
198             .build();
199 
200     assertThrows(MalformedUriException.class, () -> storage().open(uri, ReadStreamOpener.create()));
201   }
202 
203   @Test
204   @Config(sdk = Build.VERSION_CODES.M)
openForRead_directBootCacheBeforeNShouldThrowException()205   public void openForRead_directBootCacheBeforeNShouldThrowException() throws Exception {
206     Uri uri =
207         AndroidUri.builder(context)
208             .setDirectBootCacheLocation()
209             .setModule("testboot")
210             .setRelativePath("inDirectBoot.txt")
211             .build();
212 
213     assertThrows(MalformedUriException.class, () -> storage().open(uri, ReadStreamOpener.create()));
214   }
215 
216   @Test
openForRead_remoteAuthorityShouldUseRemoteBackend()217   public void openForRead_remoteAuthorityShouldUseRemoteBackend()
218       throws IOException, ExecutionException, InterruptedException {
219     Backend remoteBackend = mock(Backend.class);
220     when(remoteBackend.openForRead(any(Uri.class)))
221         .thenReturn(new ByteArrayInputStream(new byte[0]));
222     SynchronousFileStorage remoteStorage =
223         new SynchronousFileStorage(
224             ImmutableList.of(
225                 AndroidFileBackend.builder(context).setRemoteBackend(remoteBackend).build()));
226     Uri uri = Uri.parse("android://com.thirdparty.app/files/reading/file");
227 
228     Closeable unused = remoteStorage.open(uri, ReadStreamOpener.create());
229 
230     verify(remoteBackend).openForRead(uri);
231   }
232 
233   @Test
openForNativeRead_remoteAuthorityShouldUseRemoteBackend()234   public void openForNativeRead_remoteAuthorityShouldUseRemoteBackend()
235       throws IOException, ExecutionException, InterruptedException {
236     Backend remoteBackend = mock(Backend.class);
237     when(remoteBackend.openForNativeRead(any(Uri.class)))
238         .thenReturn(Pair.create(Uri.parse("fd:123"), (Closeable) null));
239     SynchronousFileStorage remoteStorage =
240         new SynchronousFileStorage(
241             ImmutableList.of(
242                 AndroidFileBackend.builder(context).setRemoteBackend(remoteBackend).build()));
243     Uri uri = Uri.parse("android://com.thirdparty.app/files/reading/file");
244 
245     Closeable unused = remoteStorage.open(uri, NativeReadOpener.create());
246 
247     verify(remoteBackend).openForNativeRead(uri);
248   }
249 
250   @Test
exists_remoteAuthorityShouldUseRemoteBackend()251   public void exists_remoteAuthorityShouldUseRemoteBackend()
252       throws IOException, ExecutionException, InterruptedException {
253     Backend remoteBackend = mock(Backend.class);
254     when(remoteBackend.exists(any(Uri.class))).thenReturn(true);
255     SynchronousFileStorage remoteStorage =
256         new SynchronousFileStorage(
257             ImmutableList.of(
258                 AndroidFileBackend.builder(context).setRemoteBackend(remoteBackend).build()));
259     Uri uri = Uri.parse("android://com.thirdparty.app/files/reading/file");
260 
261     assertThat(remoteStorage.exists(uri)).isTrue();
262 
263     verify(remoteBackend).exists(uri);
264   }
265 
266   @Test
managedUris_isSerializedAsIntegerOnDisk()267   public void managedUris_isSerializedAsIntegerOnDisk() throws Exception {
268     Account account = new Account("<internal>@gmail.com", "google.com");
269     AccountManager mockManager = mock(AccountManager.class);
270     when(mockManager.getAccountId(account)).thenReturn(Futures.immediateFuture(123));
271 
272     AndroidFileBackend backend =
273         AndroidFileBackend.builder(context).setAccountManager(mockManager).build();
274     SynchronousFileStorage storage = new SynchronousFileStorage(ImmutableList.of(backend));
275 
276     Uri uri =
277         Uri.parse(
278             "android://"
279                 + context.getPackageName()
280                 + "/managed/common/google.com%3Ayou%40gmail.com/file");
281     createFile(storage, uri, TEST_CONTENT);
282     assertThat(storage.exists(uri)).isTrue();
283 
284     File file = new File(context.getFilesDir(), "managed/common/123/file");
285     assertThat(file.exists()).isTrue();
286   }
287 
288   @Test
managedLocation_worksWithChildren()289   public void managedLocation_worksWithChildren() throws Exception {
290     Account account = new Account("<internal>@gmail.com", "google.com");
291     AccountManager mockManager = mock(AccountManager.class);
292     when(mockManager.getAccount(123)).thenReturn(Futures.immediateFuture(account));
293     when(mockManager.getAccountId(account)).thenReturn(Futures.immediateFuture(123));
294 
295     AndroidFileBackend backend =
296         AndroidFileBackend.builder(context).setAccountManager(mockManager).build();
297 
298     Uri dirUri =
299         Uri.parse(
300             "android://"
301                 + context.getPackageName()
302                 + "/managed/common/google.com%3Ayou%40gmail.com/dir");
303     Uri fileUri0 = Uri.withAppendedPath(dirUri, "file-0");
304     Uri fileUri1 = Uri.withAppendedPath(dirUri, "file-1");
305     backend.createDirectory(dirUri);
306     backend.openForWrite(fileUri0).close();
307     backend.openForWrite(fileUri1).close();
308 
309     assertThat(backend.children(dirUri)).containsExactly(fileUri0, fileUri1);
310   }
311 
312   @Test
managedUris_worksWithToFile()313   public void managedUris_worksWithToFile() throws Exception {
314     Account account = new Account("<internal>@gmail.com", "google.com");
315     AccountManager mockManager = mock(AccountManager.class);
316     when(mockManager.getAccountId(account)).thenReturn(Futures.immediateFuture(123));
317 
318     AndroidFileBackend backend =
319         AndroidFileBackend.builder(context).setAccountManager(mockManager).build();
320 
321     Uri uri =
322         Uri.parse(
323             "android://"
324                 + context.getPackageName()
325                 + "/managed/common/google.com%3Ayou%40gmail.com/file");
326     File file = backend.toFile(uri);
327     assertThat(file.getPath()).endsWith("/files/managed/common/123/file");
328   }
329 
330   @Test
lockScope_returnsNonNullLockScope()331   public void lockScope_returnsNonNullLockScope() throws IOException {
332     assertThat(backend.lockScope()).isNotNull();
333   }
334 
335   @Test
lockScope_canBeOverridden()336   public void lockScope_canBeOverridden() throws IOException {
337     LockScope lockScope = new LockScope();
338     AndroidFileBackend backend =
339         AndroidFileBackend.builder(context).setLockScope(lockScope).build();
340     assertThat(backend.lockScope()).isSameInstanceAs(lockScope);
341   }
342 }
343