/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.libraries.mobiledatadownload.internal; import static com.google.android.libraries.mobiledatadownload.internal.SharedFileManager.MDD_SHARED_FILE_MANAGER_METADATA; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import com.google.mobiledatadownload.internal.MetadataProto.DataFile; import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal; import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal.AllowedReaders; import com.google.mobiledatadownload.internal.MetadataProto.DeltaFile; import com.google.mobiledatadownload.internal.MetadataProto.DeltaFile.DiffDecoder; import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions; import com.google.mobiledatadownload.internal.MetadataProto.FileStatus; import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey; import com.google.mobiledatadownload.internal.MetadataProto.SharedFile; import com.google.android.libraries.mobiledatadownload.DownloadException; import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; import com.google.android.libraries.mobiledatadownload.FileSource; import com.google.android.libraries.mobiledatadownload.SilentFeedback; import com.google.android.libraries.mobiledatadownload.delta.DeltaDecoder; import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend; import com.google.android.libraries.mobiledatadownload.file.backends.AndroidUri; import com.google.android.libraries.mobiledatadownload.file.backends.BlobUri; import com.google.android.libraries.mobiledatadownload.file.spi.Backend; import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform; import com.google.android.libraries.mobiledatadownload.internal.Migrations.FileKeyVersion; import com.google.android.libraries.mobiledatadownload.internal.downloader.DownloaderCallbackImpl; import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader; import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger; import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil; import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil; import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil; import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor; import com.google.android.libraries.mobiledatadownload.testing.TestFlags; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.mobiledatadownload.LogEnumsProto.MddClientEvent; import com.google.protobuf.ByteString; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; @RunWith(ParameterizedRobolectricTestRunner.class) @Config(shadows = {}) public class SharedFileManagerTest { @Parameters( name = "runAfterMigratedToAddDownloadTransform = {0}, runAfterMigratedToUseChecksumOnly = {1}") public static Collection parameters() { return Arrays.asList(new Object[][] {{false, false}, {true, false}, {true, true}}); } @Parameter(value = 0) public boolean runAfterMigratedToAddDownloadTransform; @Parameter(value = 1) public boolean runAfterMigratedToUseChecksumOnly; private static final DownloadConditions DOWNLOAD_CONDITIONS = DownloadConditions.getDefaultInstance(); private static final int TRAFFIC_TAG = 1000; private Context context; private SynchronousFileStorage fileStorage; private static final long FILE_GROUP_EXPIRATION_DATE_SECS = 10; private static final String TEST_GROUP = "test-group"; private static final int VERSION_NUMBER = 7; private static final long BUILD_ID = 0; private static final String VARIANT_ID = ""; private static final DataFileGroupInternal FILE_GROUP = MddTestUtil.createDataFileGroupInternal(TEST_GROUP, 1).toBuilder() .setFileGroupVersionNumber(VERSION_NUMBER) .build(); private static final GroupKey GROUP_KEY = FileGroupUtil.createGroupKey(FILE_GROUP.getGroupName(), FILE_GROUP.getOwnerPackage()); private static final Executor CONTROL_EXECUTOR = MoreExecutors.newSequentialExecutor(Executors.newCachedThreadPool()); private SharedFileManager sfm; // This is currently not mocked as the class was split from SharedFileManager, and this ensures // that all tests still run the same way. private SharedFilesMetadata sharedFilesMetadata; private File publicDirectory; private File privateDirectory; private Optional deltaDecoder; private final TestFlags flags = new TestFlags(); @Mock SilentFeedback mockSilentFeedback; @Mock MddFileDownloader mockDownloader; @Mock DownloadProgressMonitor mockDownloadMonitor; @Mock EventLogger eventLogger; @Mock FileGroupsMetadata fileGroupsMetadata; @Mock Backend mockBackend; @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Before public void setUp() throws Exception { context = ApplicationProvider.getApplicationContext(); when(mockBackend.name()).thenReturn("blobstore"); fileStorage = new SynchronousFileStorage( Arrays.asList(AndroidFileBackend.builder(context).build(), mockBackend), ImmutableList.of(new CompressTransform())); when(fileGroupsMetadata.read(any())).thenReturn(immediateFuture(null)); sharedFilesMetadata = new SharedPreferencesSharedFilesMetadata( context, mockSilentFeedback, Optional.absent(), flags); deltaDecoder = Optional.absent(); sfm = new SharedFileManager( context, mockSilentFeedback, sharedFilesMetadata, fileStorage, mockDownloader, deltaDecoder, Optional.of(mockDownloadMonitor), eventLogger, flags, fileGroupsMetadata, Optional.absent(), CONTROL_EXECUTOR); // TODO(b/117571083): Replace with fileStorage API. File downloadDirectory = new File(context.getFilesDir(), DirectoryUtil.MDD_STORAGE_MODULE + "/" + "shared"); publicDirectory = new File(downloadDirectory, DirectoryUtil.MDD_STORAGE_ALL_GOOGLE_APPS); privateDirectory = new File(downloadDirectory, DirectoryUtil.MDD_STORAGE_ONLY_GOOGLE_PLAY_SERVICES); publicDirectory.mkdirs(); privateDirectory.mkdirs(); if (runAfterMigratedToUseChecksumOnly) { Migrations.setCurrentVersion(context, FileKeyVersion.USE_CHECKSUM_ONLY); } else if (runAfterMigratedToAddDownloadTransform) { Migrations.setCurrentVersion(context, FileKeyVersion.ADD_DOWNLOAD_TRANSFORM); } } @After public void tearDown() throws Exception { SharedPreferencesUtil.getSharedPreferences( context, MDD_SHARED_FILE_MANAGER_METADATA, Optional.absent()) .edit() .clear() .commit(); // Reset to avoid exception in the call below. fileStorage.deleteRecursively( DirectoryUtil.getBaseDownloadDirectory(context, Optional.absent())); } @Test public void init_migrateToNewKey_enabled_v23ToV24() throws Exception { Migrations.setMigratedToNewFileKey(context, false); assertThat(Migrations.isMigratedToNewFileKey(context)).isFalse(); SharedPreferences sfmMetadata = SharedPreferencesUtil.getSharedPreferences( context, MDD_SHARED_FILE_MANAGER_METADATA, Optional.absent()); sfmMetadata .edit() .putBoolean(SharedFileManager.PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY, true) .commit(); assertThat(sfm.init().get()).isTrue(); assertThat(Migrations.isMigratedToNewFileKey(context)).isTrue(); } @Test public void testSubscribeAndUnsubscribeSingleFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); // Make sure the file entry was stored. assertThat(sharedFilesMetadata.read(newFileKey)).isNotNull(); // Unsubscribe and ensure entry for file was deleted. assertThat(sfm.removeFileEntry(newFileKey).get()).isTrue(); assertThat(sharedFilesMetadata.read(newFileKey).get()).isNull(); } @Test public void testMultipleSubscribes() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); // Unsubscribe once. It should not matter how many subscribes were previously called. An // unsubscribe should remove the entry. assertThat(sfm.removeFileEntry(newFileKey).get()).isTrue(); assertThat(sharedFilesMetadata.read(newFileKey).get()).isNull(); } @Test public void testRemoveFileEntry_nonexistentFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); // Try to unsubscribe from a file that was never subscribed to and ensure that this won't add // an entry for the file. NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.removeFileEntry(newFileKey).get()).isFalse(); assertThat(sharedFilesMetadata.read(newFileKey).get()).isNull(); verifyNoInteractions(mockDownloader); } @Test public void testRemoveFileEntry_partialDownloadFileNotDeleted() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); // Download the file, but do not update shared prefs to say it is downloaded. File onDeviceFile = simulateDownload(file, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); assertThat(onDeviceFile.exists()).isTrue(); Uri uri = sfm.getOnDeviceUri(newFileKey).get(); // Ensure that deregister has actually deleted the file on disk. assertThat(sfm.removeFileEntry(newFileKey).get()).isTrue(); assertThat(sharedFilesMetadata.read(newFileKey).get()).isNull(); // The partial download file should be deleted assertThat(onDeviceFile.exists()).isTrue(); verify(mockDownloader).stopDownloading(newFileKey.getChecksum(), uri); } @Test public void testStartImport_startsInlineFileCopy() throws Exception { FileSource inlineSource = FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")); DataFile file = MddTestUtil.createDataFile("fileId", 0).toBuilder() .setUrlToDownload("inlinefile:123") .build(); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); Uri fileUri = sfm.getOnDeviceUri(newFileKey).get(); when(fileGroupsMetadata.read(GROUP_KEY)).thenReturn(Futures.immediateFuture(FILE_GROUP)); when(mockDownloader.startCopying( eq(newFileKey.getChecksum()), eq(fileUri), eq(file.getUrlToDownload()), eq(file.getByteSize()), eq(DOWNLOAD_CONDITIONS), isA(DownloaderCallbackImpl.class), any())) .thenReturn(Futures.immediateVoidFuture()); sfm.startImport(GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, inlineSource).get(); SharedFile sharedFile = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFile.getFileStatus()).isEqualTo(FileStatus.DOWNLOAD_IN_PROGRESS); } @Test public void testStartImport_whenFileAlreadyDownloaded_returnsEarly() throws Exception { FileSource inlineSource = FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")); DataFile file = MddTestUtil.createDataFile("fileId", 0).toBuilder() .setUrlToDownload("inlinefile:123") .build(); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); File onDeviceFile = simulateDownload(file, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(newFileKey, FileStatus.DOWNLOAD_COMPLETE); // File is already downloaded, so we should return early sfm.startImport(GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, inlineSource).get(); onDeviceFile.delete(); verify(mockDownloader, times(0)) .startCopying(any(), any(), any(), anyInt(), any(), any(), any()); } @Test public void testStartImport_whenUnreservedEntry_throws() throws Exception { FileSource inlineSource = FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")); DataFile file = MddTestUtil.createDataFile("fileId", 0).toBuilder() .setUrlToDownload("inlinefile:123") .build(); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); ExecutionException ex = Assert.assertThrows( ExecutionException.class, () -> sfm.startImport(GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, inlineSource) .get()); assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class); DownloadException dex = (DownloadException) ex.getCause(); assertThat(dex.getDownloadResultCode()) .isEqualTo(DownloadResultCode.SHARED_FILE_NOT_FOUND_ERROR); } @Test public void testStartImport_whenNotInlineFileUrlScheme_throws() throws Exception { FileSource inlineSource = FileSource.ofByteString(ByteString.copyFromUtf8("TEST_CONTENT")); DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); ExecutionException ex = Assert.assertThrows( ExecutionException.class, () -> sfm.startImport(GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, inlineSource) .get()); assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class); DownloadException dex = (DownloadException) ex.getCause(); assertThat(dex.getDownloadResultCode()) .isEqualTo(DownloadResultCode.INVALID_INLINE_FILE_URL_SCHEME); } @Test public void testNotifyCurrentSize_partialDownloadFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); // Download the file, but do not update shared prefs to say it is downloaded. File onDeviceFile = simulateDownload(file, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); Uri fileUri = sfm.getOnDeviceUri(newFileKey).get(); when(fileGroupsMetadata.read(GROUP_KEY)).thenReturn(Futures.immediateFuture(FILE_GROUP)); when(mockDownloader.startDownloading( eq(newFileKey.getChecksum()), eq(GROUP_KEY), eq(VERSION_NUMBER), eq(BUILD_ID), eq(VARIANT_ID), eq(fileUri), eq(file.getUrlToDownload()), eq(file.getByteSize()), eq(DOWNLOAD_CONDITIONS), isA(DownloaderCallbackImpl.class), anyInt(), anyList())) .thenReturn(Futures.immediateFuture(null)); sfm.startDownload( GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, TRAFFIC_TAG, /* extraHttpHeaders= */ ImmutableList.of()) .get(); SharedFile sharedFile = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFile.getFileStatus()).isEqualTo(FileStatus.DOWNLOAD_IN_PROGRESS); verify(mockDownloadMonitor).notifyCurrentFileSize(TEST_GROUP, onDeviceFile.length()); } @Test public void testDontDeleteUnsubscribedFiles() throws Exception { DataFile datafile = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(datafile, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); // "download" the file and update sharedPrefs File onDeviceFile = simulateDownload(datafile, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(newFileKey, FileStatus.DOWNLOAD_COMPLETE); assertThat(onDeviceFile.exists()).isTrue(); Uri uri = sfm.getOnDeviceUri(newFileKey).get(); // Ensure that deregister has actually deleted the file on disk. assertThat(sfm.removeFileEntry(newFileKey).get()).isTrue(); assertThat(sharedFilesMetadata.read(newFileKey).get()).isNull(); // The file should not be deleted by the SFM because deletion is handled by ExpirationHandler. assertThat(onDeviceFile.exists()).isTrue(); verify(mockDownloader).stopDownloading(newFileKey.getChecksum(), uri); } @Test public void testStartDownload_whenInlineFileUrlScheme_fails() throws Exception { DataFile inlineFile = MddTestUtil.createDataFile("inlineFileId", 0).toBuilder() .setUrlToDownload("inlinefile:abc") .setChecksum("abc") .build(); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(inlineFile, AllowedReaders.ALL_GOOGLE_APPS); ExecutionException ex = Assert.assertThrows( ExecutionException.class, () -> sfm.startDownload( GROUP_KEY, inlineFile, newFileKey, DOWNLOAD_CONDITIONS, TRAFFIC_TAG, /* extraHttpHeaders= */ ImmutableList.of()) .get()); assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class); DownloadException dex = (DownloadException) ex.getCause(); assertThat(dex.getDownloadResultCode()) .isEqualTo(DownloadResultCode.INVALID_INLINE_FILE_URL_SCHEME); } @Test public void testStartDownload_unsubscribedFile() { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); ExecutionException ex = Assert.assertThrows( ExecutionException.class, () -> sfm.startDownload( GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, TRAFFIC_TAG, /* extraHttpHeaders= */ ImmutableList.of()) .get()); assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class); assertThat(ex).hasMessageThat().contains("SHARED_FILE_NOT_FOUND_ERROR"); verifyNoInteractions(mockDownloader); } @Test public void testStartDownload_newFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); Uri fileUri = sfm.getOnDeviceUri(newFileKey).get(); when(fileGroupsMetadata.read(GROUP_KEY)).thenReturn(Futures.immediateFuture(FILE_GROUP)); when(mockDownloader.startDownloading( eq(newFileKey.getChecksum()), eq(GROUP_KEY), eq(VERSION_NUMBER), eq(BUILD_ID), eq(VARIANT_ID), eq(fileUri), eq(file.getUrlToDownload()), eq(file.getByteSize()), eq(DOWNLOAD_CONDITIONS), isA(DownloaderCallbackImpl.class), anyInt(), anyList())) .thenReturn(Futures.immediateFuture(null)); sfm.startDownload( GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, TRAFFIC_TAG, /* extraHttpHeaders= */ ImmutableList.of()) .get(); SharedFile sharedFile = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFile.getFileStatus()).isEqualTo(FileStatus.DOWNLOAD_IN_PROGRESS); } @Test public void testStartDownload_downloadedFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); File onDeviceFile = simulateDownload(file, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(newFileKey, FileStatus.DOWNLOAD_COMPLETE); // The file is already downloaded, so we should just return DOWNLOADED. sfm.startDownload( GROUP_KEY, file, newFileKey, DOWNLOAD_CONDITIONS, TRAFFIC_TAG, /* extraHttpHeaders= */ ImmutableList.of()) .get(); onDeviceFile.delete(); verify(mockDownloadMonitor).notifyCurrentFileSize(TEST_GROUP, file.getByteSize()); verifyNoInteractions(mockDownloader); } @Test public void testVerifyDownload_nonExistentFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); ExecutionException ex = Assert.assertThrows(ExecutionException.class, () -> sfm.getFileStatus(newFileKey).get()); assertThat(ex).hasCauseThat().isInstanceOf(SharedFileMissingException.class); ex = Assert.assertThrows(ExecutionException.class, () -> sfm.getOnDeviceUri(newFileKey).get()); assertThat(ex).hasCauseThat().isInstanceOf(SharedFileMissingException.class); verifyNoInteractions(mockDownloader); } @Test public void testVerifyDownload_fileDownloaded() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); simulateDownload(file, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(newFileKey, FileStatus.DOWNLOAD_COMPLETE); // VerifyDownload should update the onDeviceUri fields for storedFile. assertThat(sfm.getFileStatus(newFileKey).get()).isEqualTo(FileStatus.DOWNLOAD_COMPLETE); } @Test public void testVerifyDownload_downloadNotAttempted() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); assertThat(sfm.getFileStatus(newFileKey).get()).isEqualTo(FileStatus.SUBSCRIBED); // getOnDeviceUri will populate the onDeviceUri even download was not attempted. assertThat(sfm.getOnDeviceUri(newFileKey).toString()).isNotEmpty(); verifyNoInteractions(mockDownloader); } @Test public void testVerifyDownload_alreadyDownloaded() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); File onDeviceFile = simulateDownload(file, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(newFileKey, FileStatus.DOWNLOAD_COMPLETE); assertThat(sfm.getFileStatus(newFileKey).get()).isEqualTo(FileStatus.DOWNLOAD_COMPLETE); assertThat(sfm.getOnDeviceUri(newFileKey).get()) .isEqualTo(AndroidUri.builder(context).fromFile(onDeviceFile).build()); onDeviceFile.delete(); verifyNoInteractions(mockDownloader); } @Test public void findNoDeltaFile_withNoBaseFileOnDevice() throws Exception { DataFile file = MddTestUtil.createDataFileWithDeltaFile("fileId", 0, 3); assertThat( sfm.findFirstDeltaFileWithBaseFileDownloaded(file, AllowedReaders.ALL_GOOGLE_APPS) .get()) .isNull(); } @Test public void findExpectedDeltaFile_withDifferentReaderBaseFile() throws Exception { DataFile file = MddTestUtil.createDataFileWithDeltaFile("fileId", 0, 3); markBaseFileDownloaded( file.getDeltaFile(1).getBaseFile().getChecksum(), AllowedReaders.ALL_GOOGLE_APPS); assertThat( sfm.findFirstDeltaFileWithBaseFileDownloaded( file, AllowedReaders.ONLY_GOOGLE_PLAY_SERVICES) .get()) .isNull(); } @Test public void findNoDeltaFile_whenDecoderNotSupported() throws Exception { deltaDecoder = Optional.of( new DeltaDecoder() { @Override public void decode(Uri baseUri, Uri deltaUri, Uri targetUri) { throw new UnsupportedOperationException("No delta decoder provided."); } @Override public DiffDecoder getDecoderName() { return DiffDecoder.UNSPECIFIED; } }); sfm = new SharedFileManager( context, mockSilentFeedback, sharedFilesMetadata, fileStorage, mockDownloader, deltaDecoder, Optional.of(mockDownloadMonitor), eventLogger, flags, fileGroupsMetadata, Optional.absent(), CONTROL_EXECUTOR); DataFile file = MddTestUtil.createDataFileWithDeltaFile("fileId", 0, 3); markBaseFileDownloaded( file.getDeltaFile(1).getBaseFile().getChecksum(), AllowedReaders.ALL_GOOGLE_APPS); DeltaFile deltaFile = sfm.findFirstDeltaFileWithBaseFileDownloaded(file, AllowedReaders.ALL_GOOGLE_APPS).get(); assertThat(deltaFile).isNull(); } private void markBaseFileDownloaded(String checksum, AllowedReaders allowedReaders) throws Exception { NewFileKey fileKey = NewFileKey.newBuilder().setChecksum(checksum).setAllowedReaders(allowedReaders).build(); assertThat(sfm.reserveFileEntry(fileKey).get()).isTrue(); changeFileStatusAs(fileKey, FileStatus.DOWNLOAD_COMPLETE); } @Test public void testClear() throws Exception { // Create two files, one downloaded and the other currently being downloaded. DataFile downloadedFile = MddTestUtil.createDataFile("file", 0); DataFile registeredFile = MddTestUtil.createDataFile("registered-file", 0); NewFileKey downloadedKey = SharedFilesMetadata.createKeyFromDataFile(downloadedFile, AllowedReaders.ALL_GOOGLE_APPS); NewFileKey registeredKey = SharedFilesMetadata.createKeyFromDataFile(registeredFile, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(downloadedKey).get()).isTrue(); File onDevicePublicFile = simulateDownload(downloadedFile, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(downloadedKey, FileStatus.DOWNLOAD_COMPLETE); assertThat(sfm.reserveFileEntry(registeredKey).get()).isTrue(); assertThat(sfm.getOnDeviceUri(downloadedKey).get()) .isEqualTo(AndroidUri.builder(context).fromFile(onDevicePublicFile).build()); assertThat(onDevicePublicFile.exists()).isTrue(); // Clear should delete all files in our directories. sfm.clear().get(); assertThat(onDevicePublicFile.exists()).isFalse(); } @Test public void testClear_sdkLessthanR() throws Exception { // Set scenario: SDK < R, enableAndroidFileSharing flag ON ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.Q); // Create two files, one downloaded and the other currently being downloaded. DataFile downloadedFile = MddTestUtil.createDataFile("file", 0); DataFile registeredFile = MddTestUtil.createDataFile("registered-file", 0); NewFileKey downloadedKey = SharedFilesMetadata.createKeyFromDataFile(downloadedFile, AllowedReaders.ALL_GOOGLE_APPS); NewFileKey registeredKey = SharedFilesMetadata.createKeyFromDataFile(registeredFile, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(downloadedKey).get()).isTrue(); File onDevicePublicFile = simulateDownload(downloadedFile, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(downloadedKey, FileStatus.DOWNLOAD_COMPLETE); assertThat(sfm.reserveFileEntry(registeredKey).get()).isTrue(); assertThat(sfm.getOnDeviceUri(downloadedKey).get()) .isEqualTo(AndroidUri.builder(context).fromFile(onDevicePublicFile).build()); assertThat(onDevicePublicFile.exists()).isTrue(); // Clear should delete all files in our directories. sfm.clear().get(); assertThat(onDevicePublicFile.exists()).isFalse(); verify(mockBackend, never()).deleteFile(any()); verify(eventLogger, never()).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED); } @Test public void testClear_withAndroidSharedFiles() throws Exception { // Set scenario: SDK >= R ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.R); // Create three files, one downloaded, the other currently being downloaded and one shared with // the Android Blob Sharing Service. DataFile downloadedFile = MddTestUtil.createDataFile("file", /* fileIndex= */ 0); DataFile registeredFile = MddTestUtil.createDataFile("registered-file", /* fileIndex= */ 1); DataFile sharedFile = MddTestUtil.createSharedDataFile("shared-file", /* fileIndex= */ 2); NewFileKey downloadedKey = SharedFilesMetadata.createKeyFromDataFile(downloadedFile, AllowedReaders.ALL_GOOGLE_APPS); NewFileKey registeredKey = SharedFilesMetadata.createKeyFromDataFile(registeredFile, AllowedReaders.ALL_GOOGLE_APPS); NewFileKey sharedFileKey = SharedFilesMetadata.createKeyFromDataFile(sharedFile, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(downloadedKey).get()).isTrue(); File onDevicePublicFile = simulateDownload(downloadedFile, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); changeFileStatusAs(downloadedKey, FileStatus.DOWNLOAD_COMPLETE); assertThat(sfm.reserveFileEntry(registeredKey).get()).isTrue(); assertThat(sfm.reserveFileEntry(sharedFileKey).get()).isTrue(); assertThat( sfm.setAndroidSharedDownloadedFileEntry( sharedFileKey, sharedFile.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS) .get()) .isTrue(); Uri allLeasesUri = DirectoryUtil.getBlobStoreAllLeasesUri(context); assertThat(sfm.getOnDeviceUri(downloadedKey).get()) .isEqualTo(AndroidUri.builder(context).fromFile(onDevicePublicFile).build()); assertThat(onDevicePublicFile.exists()).isTrue(); // Clear should delete all files in our directories. sfm.clear().get(); assertThat(onDevicePublicFile.exists()).isFalse(); verify(mockBackend).deleteFile(allLeasesUri); verify(eventLogger).logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED); } @Test public void cancelDownload_onDownloadedFile() throws Exception { DataFile downloadedFile = MddTestUtil.createDataFile("downloaded-file", 0); NewFileKey downloadedKey = SharedFilesMetadata.createKeyFromDataFile(downloadedFile, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(downloadedKey).get()).isTrue(); changeFileStatusAs(downloadedKey, FileStatus.DOWNLOAD_COMPLETE); // Calling cancelDownload on downloaded file is a no-op. sfm.cancelDownload(downloadedKey).get(); verifyNoInteractions(mockDownloader); } @Test public void cancelDownload_onRegisteredFile() throws Exception { DataFile registeredFile = MddTestUtil.createDataFile("registered-file", 0); NewFileKey registeredKey = SharedFilesMetadata.createKeyFromDataFile(registeredFile, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(registeredKey).get()).isTrue(); // Calling cancelDownload on registered file will stop the download. sfm.cancelDownload(registeredKey).get(); SharedFile sharedFile = sharedFilesMetadata.read(registeredKey).get(); assertThat(sharedFile).isNotNull(); Uri onDeviceUri = DirectoryUtil.getOnDeviceUri( context, registeredKey.getAllowedReaders(), sharedFile.getFileName(), registeredFile.getChecksum(), mockSilentFeedback, /* instanceId= */ Optional.absent(), false); verify(mockDownloader).stopDownloading(registeredKey.getChecksum(), onDeviceUri); } @Test public void testGetSharedFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", /* fileIndex= */ 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); SharedFile sharedFile = sfm.getSharedFile(newFileKey).get(); SharedFile expectedSharedFile = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFile).isNotNull(); assertThat(sharedFile).isEqualTo(expectedSharedFile); } @Test public void testGetSharedFile_nonExistentFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); ExecutionException ex = Assert.assertThrows(ExecutionException.class, () -> sfm.getSharedFile(newFileKey).get()); assertThat(ex).hasCauseThat().isInstanceOf(SharedFileMissingException.class); } @Test public void testUpdateMaxExpirationDateSecs() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); SharedFile sharedFileBeforeUpdate = sharedFilesMetadata.read(newFileKey).get(); SharedFile expectedSharedFileAfterUpdate = SharedFile.newBuilder(sharedFileBeforeUpdate) .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS) .build(); assertThat(sharedFileBeforeUpdate).isNotNull(); assertThat(sharedFileBeforeUpdate).isNotEqualTo(expectedSharedFileAfterUpdate); // updateMaxExpirationDateSecs updates maxExpirationDateSecs assertThat(sfm.updateMaxExpirationDateSecs(newFileKey, FILE_GROUP_EXPIRATION_DATE_SECS).get()) .isTrue(); SharedFile sharedFileAfterUpdate = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFileAfterUpdate).isNotNull(); assertThat(sharedFileAfterUpdate).isEqualTo(expectedSharedFileAfterUpdate); // updateMaxExpirationDateSecs doesn't update maxExpirationDateSecs assertThat( sfm.updateMaxExpirationDateSecs(newFileKey, FILE_GROUP_EXPIRATION_DATE_SECS - 1).get()) .isTrue(); SharedFile sharedFileAfterSecondUpdate = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFileAfterSecondUpdate).isNotNull(); assertThat(sharedFileAfterSecondUpdate).isEqualTo(expectedSharedFileAfterUpdate); } @Test public void testUpdateMaxExpirationDateSecs_nonExistentFile() throws Exception { DataFile file = MddTestUtil.createDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); ExecutionException ex = Assert.assertThrows( ExecutionException.class, () -> sfm.updateMaxExpirationDateSecs(newFileKey, FILE_GROUP_EXPIRATION_DATE_SECS).get()); assertThat(ex).hasCauseThat().isInstanceOf(SharedFileMissingException.class); } @Test public void testSetAndroidSharedDownloadedFileEntry() throws Exception { DataFile file = MddTestUtil.createSharedDataFile("fileId", 0); SharedFile expectedSharedFileAfterUpdate = SharedFile.newBuilder() .setFileStatus(FileStatus.DOWNLOAD_COMPLETE) .setFileName("android_shared_" + file.getAndroidSharingChecksum()) .setAndroidShared(true) .setMaxExpirationDateSecs(FILE_GROUP_EXPIRATION_DATE_SECS) .setAndroidSharingChecksum(file.getAndroidSharingChecksum()) .build(); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); SharedFile sharedFile = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFile).isNotNull(); assertThat(sharedFile).isNotEqualTo(expectedSharedFileAfterUpdate); assertThat( sfm.setAndroidSharedDownloadedFileEntry( newFileKey, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS) .get()) .isTrue(); sharedFile = sharedFilesMetadata.read(newFileKey).get(); assertThat(sharedFile).isNotNull(); assertThat(sharedFile).isEqualTo(expectedSharedFileAfterUpdate); } @Test public void testOnDeviceUri() throws Exception { DataFile file = MddTestUtil.createSharedDataFile("fileId", 0); NewFileKey newFileKey = SharedFilesMetadata.createKeyFromDataFile(file, AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.reserveFileEntry(newFileKey).get()).isTrue(); File onDeviceFile = simulateDownload(file, getLastFileName(), AllowedReaders.ALL_GOOGLE_APPS); assertThat(sfm.getOnDeviceUri(newFileKey).get()) .isEqualTo(AndroidUri.builder(context).fromFile(onDeviceFile).build()); assertThat( sfm.setAndroidSharedDownloadedFileEntry( newFileKey, file.getAndroidSharingChecksum(), FILE_GROUP_EXPIRATION_DATE_SECS) .get()) .isTrue(); assertThat(sfm.getOnDeviceUri(newFileKey).get()) .isEqualTo( BlobUri.builder(context).setBlobParameters(file.getAndroidSharingChecksum()).build()); } private File simulateDownload(DataFile dataFile, String fileName, AllowedReaders allowedReaders) throws IOException { File onDeviceFile; if (allowedReaders == AllowedReaders.ALL_GOOGLE_APPS) { onDeviceFile = new File(publicDirectory, fileName); } else { onDeviceFile = new File(privateDirectory, fileName); } FileOutputStream writer = new FileOutputStream(onDeviceFile); byte[] bytes = new byte[dataFile.getByteSize()]; writer.write(bytes); writer.close(); return onDeviceFile; } private void changeFileStatusAs(NewFileKey newFileKey, FileStatus fileStatus) throws InterruptedException, ExecutionException { synchronized (SharedFilesMetadata.class) { SharedFile sharedFile = sharedFilesMetadata.read(newFileKey).get(); sharedFile = sharedFile.toBuilder().setFileStatus(fileStatus).build(); assertThat(sharedFilesMetadata.write(newFileKey, sharedFile).get()).isTrue(); } } private String getLastFileName() { SharedPreferences sfmMetadata = SharedPreferencesUtil.getSharedPreferences( context, MDD_SHARED_FILE_MANAGER_METADATA, Optional.absent()); long lastName = sfmMetadata.getLong(SharedFileManager.PREFS_KEY_NEXT_FILE_NAME, 1) - 1; return SharedFileManager.FILE_NAME_PREFIX + lastName; } }