• 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.internal;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 import static java.nio.charset.StandardCharsets.UTF_16;
20 
21 import android.accounts.Account;
22 import android.content.Context;
23 import android.net.Uri;
24 import androidx.test.core.app.ApplicationProvider;
25 import androidx.test.ext.junit.runners.AndroidJUnit4;
26 import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
27 import com.google.mobiledatadownload.internal.MetadataProto.DataFile.ChecksumType;
28 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
29 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal.AllowedReaders;
30 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions;
31 import com.google.mobiledatadownload.internal.MetadataProto.FileStatus;
32 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
33 import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey;
34 import com.google.mobiledatadownload.internal.MetadataProto.SharedFile;
35 import com.google.android.libraries.mobiledatadownload.SilentFeedback;
36 import com.google.android.libraries.mobiledatadownload.account.AccountUtil;
37 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
38 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend;
39 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidUri;
40 import com.google.android.libraries.mobiledatadownload.file.common.testing.FakeFileBackend;
41 import com.google.android.libraries.mobiledatadownload.file.common.testing.TemporaryUri;
42 import com.google.android.libraries.mobiledatadownload.file.openers.WriteByteArrayOpener;
43 import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader;
44 import com.google.android.libraries.mobiledatadownload.internal.experimentation.NoOpDownloadStageManager;
45 import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
46 import com.google.android.libraries.mobiledatadownload.internal.logging.LoggingStateStore;
47 import com.google.android.libraries.mobiledatadownload.internal.util.SymlinkUtil;
48 import com.google.android.libraries.mobiledatadownload.monitor.NetworkUsageMonitor;
49 import com.google.android.libraries.mobiledatadownload.testing.BlockingFileDownloader;
50 import com.google.android.libraries.mobiledatadownload.testing.FakeTimeSource;
51 import com.google.android.libraries.mobiledatadownload.testing.MddTestDependencies;
52 import com.google.android.libraries.mobiledatadownload.testing.TestFlags;
53 import com.google.common.base.Optional;
54 import com.google.common.util.concurrent.AsyncFunction;
55 import com.google.common.util.concurrent.Futures;
56 import com.google.common.util.concurrent.ListenableFuture;
57 import com.google.common.util.concurrent.ListeningExecutorService;
58 import com.google.common.util.concurrent.MoreExecutors;
59 import java.util.Arrays;
60 import java.util.Random;
61 import java.util.concurrent.Executor;
62 import java.util.concurrent.Executors;
63 import org.junit.Before;
64 import org.junit.Rule;
65 import org.junit.Test;
66 import org.junit.runner.RunWith;
67 import org.mockito.Mock;
68 import org.mockito.junit.MockitoJUnit;
69 import org.mockito.junit.MockitoRule;
70 
71 /**
72  * Emulator tests for MDD isolated structures support. This is separate from the other robolectric
73  * tests because android.os.symlink and android.os.readlink do not work with robolectric.
74  */
75 @RunWith(AndroidJUnit4.class)
76 public final class MddIsolatedStructuresTest {
77 
78   private static final String TEST_GROUP = "test-group";
79 
80   private static final String TEST_ACCOUNT_1 =
81       AccountUtil.serialize(new Account("com.google", "test1"));
82   private static final String TEST_ACCOUNT_2 =
83       AccountUtil.serialize(new Account("com.google", "test2"));
84 
85   @Rule public TemporaryUri tempUri = new TemporaryUri();
86 
87   private Context context;
88   private FileGroupManager fileGroupManager;
89   private FileGroupsMetadata fileGroupsMetadata;
90   private SharedFileManager sharedFileManager;
91   private SharedFilesMetadata sharedFilesMetadata;
92   private FakeTimeSource testClock;
93   private SynchronousFileStorage fileStorage;
94   private FakeFileBackend fakeAndroidFileBackend;
95   private BlockingFileDownloader blockingFileDownloader;
96   private MddFileDownloader mddFileDownloader;
97   private LoggingStateStore loggingStateStore;
98 
99   GroupKey defaultGroupKey;
100   DataFileGroupInternal defaultFileGroup;
101   DataFile file;
102   NewFileKey newFileKey;
103   SharedFile existingDownloadedSharedFile;
104 
105   @Mock SilentFeedback mockSilentFeedback;
106   @Mock EventLogger mockLogger;
107   @Mock NetworkUsageMonitor mockNetworkUsageMonitor;
108   @Rule public final MockitoRule mockito = MockitoJUnit.rule();
109 
110   private static final Executor SEQUENTIAL_CONTROL_EXECUTOR =
111       Executors.newSingleThreadScheduledExecutor();
112 
113   // Create a download executor separate from the sequential control executor
114   private static final ListeningExecutorService DOWNLOAD_EXECUTOR =
115       MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor());
116 
117   @Before
setUp()118   public void setUp() throws Exception {
119     context = ApplicationProvider.getApplicationContext();
120 
121     testClock = new FakeTimeSource();
122 
123     TestFlags flags = new TestFlags();
124 
125     blockingFileDownloader = new BlockingFileDownloader(DOWNLOAD_EXECUTOR);
126 
127     fakeAndroidFileBackend = new FakeFileBackend(AndroidFileBackend.builder(context).build());
128     fileStorage = new SynchronousFileStorage(Arrays.asList(fakeAndroidFileBackend));
129 
130     loggingStateStore =
131         MddTestDependencies.LoggingStateStoreImpl.SHARED_PREFERENCES.loggingStateStore(
132             context,
133             Optional.absent(),
134             new FakeTimeSource(),
135             SEQUENTIAL_CONTROL_EXECUTOR,
136             new Random());
137 
138     mddFileDownloader =
139         new MddFileDownloader(
140             context,
141             () -> blockingFileDownloader,
142             fileStorage,
143             mockNetworkUsageMonitor,
144             Optional.absent(),
145             loggingStateStore,
146             SEQUENTIAL_CONTROL_EXECUTOR,
147             flags);
148 
149     fileGroupsMetadata =
150         new SharedPreferencesFileGroupsMetadata(
151             context,
152             testClock,
153             mockSilentFeedback,
154             Optional.absent(),
155             MoreExecutors.directExecutor());
156     sharedFilesMetadata =
157         new SharedPreferencesSharedFilesMetadata(
158             context, mockSilentFeedback, Optional.absent(), flags);
159     sharedFileManager =
160         new SharedFileManager(
161             context,
162             mockSilentFeedback,
163             sharedFilesMetadata,
164             fileStorage,
165             mddFileDownloader,
166             Optional.absent(),
167             Optional.absent(),
168             mockLogger,
169             flags,
170             fileGroupsMetadata,
171             Optional.absent(),
172             MoreExecutors.directExecutor());
173 
174     fileGroupManager =
175         new FileGroupManager(
176             context,
177             mockLogger,
178             mockSilentFeedback,
179             fileGroupsMetadata,
180             sharedFileManager,
181             new FakeTimeSource(),
182             Optional.absent(),
183             SEQUENTIAL_CONTROL_EXECUTOR,
184             Optional.absent(),
185             fileStorage,
186             new NoOpDownloadStageManager(),
187             flags);
188 
189     defaultGroupKey =
190         GroupKey.newBuilder()
191             .setGroupName(TEST_GROUP)
192             .setOwnerPackage(context.getPackageName())
193             .build();
194     file =
195         DataFile.newBuilder()
196             .setChecksumType(ChecksumType.NONE)
197             .setUrlToDownload("https://test.file")
198             .setFileId("my-file")
199             .setRelativeFilePath("mycustom/file.txt")
200             .build();
201     defaultFileGroup =
202         MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 0).toBuilder()
203             .setPreserveFilenamesAndIsolateFiles(true)
204             .addFile(file)
205             .build();
206 
207     newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS);
208 
209     existingDownloadedSharedFile =
210         SharedFile.newBuilder()
211             .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
212             .setFileName("fileName")
213             .setAndroidShared(false)
214             .build();
215   }
216 
217   @Test
testSymlinkUtil()218   public void testSymlinkUtil() throws Exception {
219     Uri targetUri = AndroidUri.builder(context).setRelativePath("targetFile").build();
220     // Write some data so the target file exists.
221     Void unused =
222         fileStorage.open(targetUri, WriteByteArrayOpener.create("some bytes".getBytes(UTF_16)));
223 
224     Uri linkUri = AndroidUri.builder(context).setRelativePath("linkFile").build();
225 
226     SymlinkUtil.createSymlink(context, linkUri, targetUri);
227 
228     // Make sure the symlink points to the original target
229     assertThat(SymlinkUtil.readSymlink(context, linkUri)).isEqualTo(targetUri);
230   }
231 
232   @Test
testFileGroupManager_createsIsolatedStructures()233   public void testFileGroupManager_createsIsolatedStructures() throws Exception {
234     writePendingFileGroup(defaultGroupKey, defaultFileGroup);
235     sharedFilesMetadata.write(newFileKey, existingDownloadedSharedFile).get();
236 
237     Uri onDeviceUri = fileGroupManager.getOnDeviceUri(file, defaultFileGroup).get();
238     // Actually write something to disk so the symlink points to something.
239     Void unused =
240         fileStorage.open(onDeviceUri, WriteByteArrayOpener.create("some content".getBytes(UTF_16)));
241 
242     // Download the file group so MDD creates the structures
243     fileGroupManager
244         .downloadFileGroup(
245             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
246         .get();
247 
248     Uri isolatedFileUri = fileGroupManager.getIsolatedFileUris(defaultFileGroup).get(file);
249 
250     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUri)).isEqualTo(onDeviceUri);
251   }
252 
253   @Test
testFileGroupManager_repairsIsolatedStructuresOnMaintenance()254   public void testFileGroupManager_repairsIsolatedStructuresOnMaintenance() throws Exception {
255     writePendingFileGroup(defaultGroupKey, defaultFileGroup);
256     sharedFilesMetadata.write(newFileKey, existingDownloadedSharedFile).get();
257 
258     fileGroupManager
259         .downloadFileGroup(
260             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
261         .get();
262 
263     Uri onDeviceUri = fileGroupManager.getOnDeviceUri(file, defaultFileGroup).get();
264     Uri isolatedFileUri = fileGroupManager.getIsolatedFileUris(defaultFileGroup).get(file);
265 
266     assertThat(fileGroupManager.getFileGroup(defaultGroupKey, true).get()).isNotNull();
267 
268     fileStorage.deleteFile(isolatedFileUri);
269 
270     fileGroupManager.verifyAndAttemptToRepairIsolatedFiles().get();
271 
272     assertThat(fileGroupManager.getFileGroup(defaultGroupKey, true).get()).isNotNull();
273 
274     isolatedFileUri = fileGroupManager.getIsolatedFileUris(defaultFileGroup).get(file);
275 
276     assertThat(fileStorage.exists(isolatedFileUri)).isTrue();
277     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUri)).isEqualTo(onDeviceUri);
278   }
279 
280   @Test
testFileGroupManager_withIsolatedRoot_isolateForDifferentVariants()281   public void testFileGroupManager_withIsolatedRoot_isolateForDifferentVariants() throws Exception {
282     DataFileGroupInternal fileGroupVariant1 =
283         defaultFileGroup.toBuilder().setVariantId("variant1").build();
284     DataFileGroupInternal fileGroupVariant2 =
285         defaultFileGroup.toBuilder().setVariantId("variant2").build();
286 
287     sharedFilesMetadata.write(newFileKey, existingDownloadedSharedFile).get();
288 
289     // Get the actual uri on device (this should be the same for both variants).
290     Uri onDeviceUri = fileGroupManager.getOnDeviceUri(file, fileGroupVariant1).get();
291     // Actually write something to disk so the symlink points to something.
292     Void unused =
293         fileStorage.open(onDeviceUri, WriteByteArrayOpener.create("some content".getBytes(UTF_16)));
294 
295     // Add the first variant and download it to create the isolated structure
296     fileGroupManager.addGroupForDownload(defaultGroupKey, fileGroupVariant1).get();
297     fileGroupManager
298         .downloadFileGroup(
299             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
300         .get();
301     DataFileGroupInternal storedFileGroupVariant1 =
302         fileGroupManager.getFileGroup(defaultGroupKey, /* downloaded= */ true).get();
303 
304     Uri isolatedFileUriVariant1 =
305         fileGroupManager.getIsolatedFileUris(storedFileGroupVariant1).get(file);
306 
307     // Add the second variant and download it to create another isolated structure
308     fileGroupManager.addGroupForDownload(defaultGroupKey, fileGroupVariant2).get();
309     fileGroupManager
310         .downloadFileGroup(
311             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
312         .get();
313     DataFileGroupInternal storedFileGroupVariant2 =
314         fileGroupManager.getFileGroup(defaultGroupKey, /* downloaded= */ true).get();
315 
316     Uri isolatedFileUriVariant2 =
317         fileGroupManager.getIsolatedFileUris(storedFileGroupVariant2).get(file);
318 
319     // Check that both symlinks exist and point to the right file
320     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUriVariant1)).isEqualTo(onDeviceUri);
321     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUriVariant2)).isEqualTo(onDeviceUri);
322 
323     // Check that the symlinks are not equal to each other (since the roots are different);
324     assertThat(isolatedFileUriVariant1).isNotEqualTo(isolatedFileUriVariant2);
325   }
326 
327   @Test
testFileGroupManager_withIsolatedRoot_isolateForDifferentAccounts()328   public void testFileGroupManager_withIsolatedRoot_isolateForDifferentAccounts() throws Exception {
329     GroupKey account1GroupKey = defaultGroupKey.toBuilder().setAccount(TEST_ACCOUNT_1).build();
330     GroupKey account2GroupKey = defaultGroupKey.toBuilder().setAccount(TEST_ACCOUNT_2).build();
331 
332     sharedFilesMetadata.write(newFileKey, existingDownloadedSharedFile).get();
333 
334     Uri onDeviceUri = fileGroupManager.getOnDeviceUri(file, defaultFileGroup).get();
335     // Actually write something to disk so the symlink points to something.
336     Void unused =
337         fileStorage.open(onDeviceUri, WriteByteArrayOpener.create("some content".getBytes(UTF_16)));
338 
339     // Add the first account group and download it to create the isolated structure
340     fileGroupManager.addGroupForDownload(account1GroupKey, defaultFileGroup).get();
341     fileGroupManager
342         .downloadFileGroup(
343             account1GroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
344         .get();
345     DataFileGroupInternal storedFileGroupAccount1 =
346         fileGroupManager.getFileGroup(account1GroupKey, /* downloaded= */ true).get();
347 
348     Uri isolatedFileUriAccount1 =
349         fileGroupManager.getIsolatedFileUris(storedFileGroupAccount1).get(file);
350 
351     // Add the second account group and download it to create another isolated structure
352     fileGroupManager.addGroupForDownload(account2GroupKey, defaultFileGroup).get();
353     fileGroupManager
354         .downloadFileGroup(
355             account2GroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
356         .get();
357     DataFileGroupInternal storedFileGroupAccount2 =
358         fileGroupManager.getFileGroup(account2GroupKey, /* downloaded= */ true).get();
359 
360     Uri isolatedFileUriAccount2 =
361         fileGroupManager.getIsolatedFileUris(storedFileGroupAccount2).get(file);
362 
363     // Check that both symlinks exist and point to the right file
364     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUriAccount1)).isEqualTo(onDeviceUri);
365     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUriAccount2)).isEqualTo(onDeviceUri);
366 
367     // Check that the symlinks are not equal to each other (since the roots are different);
368     assertThat(isolatedFileUriAccount1).isNotEqualTo(isolatedFileUriAccount2);
369   }
370 
371   @Test
testFileGroupManager_withIsolatedRoot_isolateForDifferentBuilds()372   public void testFileGroupManager_withIsolatedRoot_isolateForDifferentBuilds() throws Exception {
373     DataFileGroupInternal fileGroupBuild1 = defaultFileGroup.toBuilder().setBuildId(1).build();
374     DataFileGroupInternal fileGroupBuild2 = defaultFileGroup.toBuilder().setBuildId(2).build();
375 
376     sharedFilesMetadata.write(newFileKey, existingDownloadedSharedFile).get();
377 
378     // Get the actual uri on device (this should be the same for both variants).
379     Uri onDeviceUri = fileGroupManager.getOnDeviceUri(file, fileGroupBuild1).get();
380     // Actually write something to disk so the symlink points to something.
381     Void unused =
382         fileStorage.open(onDeviceUri, WriteByteArrayOpener.create("some content".getBytes(UTF_16)));
383 
384     // Add the first build and download it to create the isolated structure
385     fileGroupManager.addGroupForDownload(defaultGroupKey, fileGroupBuild1).get();
386     fileGroupManager
387         .downloadFileGroup(
388             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
389         .get();
390     DataFileGroupInternal storedFileGroupBuild1 =
391         fileGroupManager.getFileGroup(defaultGroupKey, /* downloaded= */ true).get();
392 
393     Uri isolatedFileUriBuild1 =
394         fileGroupManager.getIsolatedFileUris(storedFileGroupBuild1).get(file);
395 
396     // Add the second build and download it to create another isolated structure
397     fileGroupManager.addGroupForDownload(defaultGroupKey, fileGroupBuild2).get();
398     fileGroupManager
399         .downloadFileGroup(
400             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation())
401         .get();
402     DataFileGroupInternal storedFileGroupBuild2 =
403         fileGroupManager.getFileGroup(defaultGroupKey, /* downloaded= */ true).get();
404 
405     Uri isolatedFileUriBuild2 =
406         fileGroupManager.getIsolatedFileUris(storedFileGroupBuild2).get(file);
407 
408     // Check that both symlinks exist and point to the right file
409     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUriBuild1)).isEqualTo(onDeviceUri);
410     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUriBuild2)).isEqualTo(onDeviceUri);
411 
412     // Check that the symlinks are not equal to each other (since the roots are different);
413     assertThat(isolatedFileUriBuild1).isNotEqualTo(isolatedFileUriBuild2);
414   }
415 
416   @Test
testFileGroupManager_duplicateDownloadCalls_handlesIsolatedStructureCreation()417   public void testFileGroupManager_duplicateDownloadCalls_handlesIsolatedStructureCreation()
418       throws Exception {
419     writePendingFileGroup(defaultGroupKey, defaultFileGroup);
420     // Write an in progress file because we want to invoke the downloader and simulate a
421     // long-running download. This ensures that both download futures run their post-download
422     // workflow at the same time.
423     SharedFile existingInProgressSharedFile =
424         SharedFile.newBuilder()
425             .setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS)
426             .setFileName("fileName")
427             .setAndroidShared(false)
428             .build();
429     sharedFilesMetadata.write(newFileKey, existingInProgressSharedFile).get();
430 
431     Uri onDeviceUri = fileGroupManager.getOnDeviceUri(file, defaultFileGroup).get();
432     // Actually write something to disk so the symlink points to something.
433     Void unused =
434         fileStorage.open(onDeviceUri, WriteByteArrayOpener.create("some content".getBytes(UTF_16)));
435 
436     // Start 2 downloads and wait for file download to start
437     ListenableFuture<?> downloadFuture1 =
438         fileGroupManager.downloadFileGroup(
439             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation());
440 
441     ListenableFuture<?> downloadFuture2 =
442         fileGroupManager.downloadFileGroup(
443             defaultGroupKey, DownloadConditions.getDefaultInstance(), noCustomValidation());
444 
445     blockingFileDownloader.waitForDownloadStarted();
446 
447     // Both downloads should be waiting for the same file download, so finish downloading to get
448     // both performing the same post download process at the same time.
449     blockingFileDownloader.finishDownloading();
450 
451     // Wait for both futures to complete.
452     downloadFuture1.get();
453     downloadFuture2.get();
454 
455     Uri isolatedFileUri = fileGroupManager.getIsolatedFileUris(defaultFileGroup).get(file);
456 
457     assertThat(SymlinkUtil.readSymlink(context, isolatedFileUri)).isEqualTo(onDeviceUri);
458   }
459 
writePendingFileGroup(GroupKey key, DataFileGroupInternal group)460   private void writePendingFileGroup(GroupKey key, DataFileGroupInternal group) throws Exception {
461     GroupKey duplicateGroupKey = key.toBuilder().setDownloaded(false).build();
462     fileGroupsMetadata.write(duplicateGroupKey, group).get();
463   }
464 
noCustomValidation()465   private AsyncFunction<DataFileGroupInternal, Boolean> noCustomValidation() {
466     return unused -> Futures.immediateFuture(true);
467   }
468 }
469