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