blob: 61c8912cd9d67f49cec5416c7526dbea1778375a [file] [log] [blame]
/*
* 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.file.backends;
import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.createFile;
import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.makeArrayOfBytesContent;
import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.readFileInBytes;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.accounts.Account;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.file.common.FileStorageUnavailableException;
import com.google.android.libraries.mobiledatadownload.file.common.LockScope;
import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
import com.google.android.libraries.mobiledatadownload.file.common.testing.BackendTestBase;
import com.google.android.libraries.mobiledatadownload.file.openers.NativeReadOpener;
import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener;
import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Tests for {@link AndroidFileBackend} */
@RunWith(RobolectricTestRunner.class)
@Config(sdk = Build.VERSION_CODES.N)
public class AndroidFileBackendTest extends BackendTestBase {
private final Context context = ApplicationProvider.getApplicationContext();
private final Backend backend = AndroidFileBackend.builder(context).build();
private static final byte[] TEST_CONTENT = makeArrayOfBytesContent();
@Override
protected Backend backend() {
return backend;
}
@Override
protected Uri legalUriBase() {
return Uri.parse("android://" + context.getPackageName() + "/files/common/shared/");
}
@Override
protected List<Uri> illegalUrisToRead() {
return ImmutableList.of(
Uri.parse(legalUriBase() + "uriWithQuery?q=a"),
Uri.parse("android:///null/uriWithInvalidLogicalLocation"),
Uri.parse("android://" + context.getPackageName()),
Uri.parse("android://" + context.getPackageName()));
}
@Override
protected List<Uri> illegalUrisToWrite() {
return ImmutableList.of(
Uri.parse("android://com.thirdparty.app/files/common/shared/uriAcrossAuthority"));
}
@Override
protected List<Uri> illegalUrisToAppend() {
return illegalUrisToWrite();
}
/** Minimal tests verifying default builder behavior */
@Test
public void builder_withNullContext_shouldThrowException() {
assertThrows(IllegalArgumentException.class, () -> AndroidFileBackend.builder(null));
}
@Test
public void builder_remoteBackend_isNullByDefault() {
Uri uri = Uri.parse("android://com.thirdparty.app/files/common/shared/file");
AndroidFileBackend backend = AndroidFileBackend.builder(context).build();
assertThrows(FileStorageUnavailableException.class, () -> backend.openForRead(uri));
}
@Test
public void builder_accountManager_isNullByDefault() {
Account account = new Account("<internal>@gmail.com", "google.com");
Uri uri = AndroidUri.builder(context).setManagedLocation().setAccount(account).build();
AndroidFileBackend backend = AndroidFileBackend.builder(context).build();
assertThrows(MalformedUriException.class, () -> backend.openForRead(uri));
}
/** Tests verifying backend behavior */
@Test
public void openForWrite_shouldUseContextAuthorityIfWithoutAuthority() throws Exception {
final Uri uri = Uri.parse("android:///files/writing/shared/missingAuthority");
assertThat(storage().exists(uri)).isFalse();
createFile(storage(), uri, TEST_CONTENT);
assertThat(storage().exists(uri)).isTrue();
assertThat(readFileInBytes(storage(), uri)).isEqualTo(TEST_CONTENT);
}
@Test
public void rename_shouldNotRenameAcrossAuthority() throws Exception {
final Uri from =
Uri.parse("android://" + context.getPackageName() + "/files/localfrom/shared/file");
final Uri to = Uri.parse("android://com.thirdparty.app/files/remoteto/shared/file");
createFile(storage(), from, TEST_CONTENT);
assertThat(storage().exists(from)).isTrue();
assertThrows(MalformedUriException.class, () -> storage().rename(from, to));
assertThat(storage().exists(from)).isTrue();
assertThat(readFileInBytes(storage(), from)).isEqualTo(TEST_CONTENT);
}
@Test
@Config(sdk = Build.VERSION_CODES.N)
public void openForRead_directBootFilesOnNShouldUseDeviceProtectedStorageContext()
throws Exception {
Uri uri =
AndroidUri.builder(context)
.setDirectBootFilesLocation()
.setModule("testboot")
.setRelativePath("inDirectBoot.txt")
.build();
File directBootFile =
new File(
context.createDeviceProtectedStorageContext().getFilesDir(),
"testboot/shared/inDirectBoot.txt");
File filesFile = new File(context.getFilesDir(), "testboot/shared/inDirectBoot.txt");
createFile(storage(), uri, TEST_CONTENT);
assertThat(filesFile.exists()).isFalse();
assertThat(directBootFile.exists()).isTrue();
assertThat(readFileInBytes(storage(), uri)).isEqualTo(TEST_CONTENT);
}
@Test
@Config(sdk = Build.VERSION_CODES.N)
public void openForRead_directBootCacheOnNShouldUseDeviceProtectedStorageContext()
throws Exception {
Uri uri =
AndroidUri.builder(context)
.setDirectBootCacheLocation()
.setModule("testboot")
.setRelativePath("inDirectBoot.txt")
.build();
File directBootFile =
new File(
context.createDeviceProtectedStorageContext().getCacheDir(),
"testboot/shared/inDirectBoot.txt");
File cacheFile = new File(context.getCacheDir(), "testboot/shared/inDirectBoot.txt");
createFile(storage(), uri, TEST_CONTENT);
assertThat(cacheFile.exists()).isFalse();
assertThat(directBootFile.exists()).isTrue();
assertThat(readFileInBytes(storage(), uri)).isEqualTo(TEST_CONTENT);
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void openForRead_directBootFilesBeforeNShouldThrowException() throws Exception {
Uri uri =
AndroidUri.builder(context)
.setDirectBootFilesLocation()
.setModule("testboot")
.setRelativePath("inDirectBoot.txt")
.build();
assertThrows(MalformedUriException.class, () -> storage().open(uri, ReadStreamOpener.create()));
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void openForRead_directBootCacheBeforeNShouldThrowException() throws Exception {
Uri uri =
AndroidUri.builder(context)
.setDirectBootCacheLocation()
.setModule("testboot")
.setRelativePath("inDirectBoot.txt")
.build();
assertThrows(MalformedUriException.class, () -> storage().open(uri, ReadStreamOpener.create()));
}
@Test
public void openForRead_remoteAuthorityShouldUseRemoteBackend()
throws IOException, ExecutionException, InterruptedException {
Backend remoteBackend = mock(Backend.class);
when(remoteBackend.openForRead(any(Uri.class)))
.thenReturn(new ByteArrayInputStream(new byte[0]));
SynchronousFileStorage remoteStorage =
new SynchronousFileStorage(
ImmutableList.of(
AndroidFileBackend.builder(context).setRemoteBackend(remoteBackend).build()));
Uri uri = Uri.parse("android://com.thirdparty.app/files/reading/file");
Closeable unused = remoteStorage.open(uri, ReadStreamOpener.create());
verify(remoteBackend).openForRead(uri);
}
@Test
public void openForNativeRead_remoteAuthorityShouldUseRemoteBackend()
throws IOException, ExecutionException, InterruptedException {
Backend remoteBackend = mock(Backend.class);
when(remoteBackend.openForNativeRead(any(Uri.class)))
.thenReturn(Pair.create(Uri.parse("fd:123"), (Closeable) null));
SynchronousFileStorage remoteStorage =
new SynchronousFileStorage(
ImmutableList.of(
AndroidFileBackend.builder(context).setRemoteBackend(remoteBackend).build()));
Uri uri = Uri.parse("android://com.thirdparty.app/files/reading/file");
Closeable unused = remoteStorage.open(uri, NativeReadOpener.create());
verify(remoteBackend).openForNativeRead(uri);
}
@Test
public void exists_remoteAuthorityShouldUseRemoteBackend()
throws IOException, ExecutionException, InterruptedException {
Backend remoteBackend = mock(Backend.class);
when(remoteBackend.exists(any(Uri.class))).thenReturn(true);
SynchronousFileStorage remoteStorage =
new SynchronousFileStorage(
ImmutableList.of(
AndroidFileBackend.builder(context).setRemoteBackend(remoteBackend).build()));
Uri uri = Uri.parse("android://com.thirdparty.app/files/reading/file");
assertThat(remoteStorage.exists(uri)).isTrue();
verify(remoteBackend).exists(uri);
}
@Test
public void lockScope_returnsNonNullLockScope() throws IOException {
assertThat(backend.lockScope()).isNotNull();
}
@Test
public void lockScope_canBeOverridden() throws IOException {
LockScope lockScope = new LockScope();
AndroidFileBackend backend =
AndroidFileBackend.builder(context).setLockScope(lockScope).build();
assertThat(backend.lockScope()).isSameInstanceAs(lockScope);
}
}