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