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.common.truth.Truth.assertThat; 19 import static org.junit.Assert.assertThrows; 20 import static org.mockito.Mockito.mock; 21 import static org.mockito.Mockito.when; 22 23 import android.accounts.Account; 24 import android.content.Context; 25 import android.net.Uri; 26 import android.os.Build; 27 import android.os.Build.VERSION_CODES; 28 import androidx.test.core.app.ApplicationProvider; 29 import com.google.common.util.concurrent.Futures; 30 import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; 31 import java.io.File; 32 import org.junit.Test; 33 import org.junit.runner.RunWith; 34 import org.robolectric.annotation.Config; 35 36 /** Test {@link com.google.android.libraries.mobiledatadownload.file.backends.AndroidUri}. */ 37 @RunWith(GoogleRobolectricTestRunner.class) 38 @Config( 39 sdk = {VERSION_CODES.M, VERSION_CODES.N, VERSION_CODES.O}, 40 shadows = {}) 41 public final class AndroidUriTest { 42 43 private final Context context = ApplicationProvider.getApplicationContext(); 44 45 @Test builder_unsupportedLocationSuchAsCache_throwsException()46 public void builder_unsupportedLocationSuchAsCache_throwsException() { 47 File file = new File(context.getCacheDir(), "file"); 48 assertThrows(IllegalArgumentException.class, () -> AndroidUri.builder(context).fromFile(file)); 49 } 50 51 @Test builder_missingAccountDirectory_throwsException()52 public void builder_missingAccountDirectory_throwsException() { 53 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), "module/"); 54 assertThrows(IllegalArgumentException.class, () -> AndroidUri.builder(context).fromFile(file)); 55 } 56 57 @Test builder_filesLocation()58 public void builder_filesLocation() { 59 String filePath = "module/shared/directory/file"; 60 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 61 62 Uri uri = AndroidUri.builder(context).fromFile(file).build(); 63 assertThat(uri.getScheme()).isEqualTo("android"); 64 assertThat(uri.getAuthority()).isEqualTo(context.getPackageName()); 65 assertThat(uri.getPath()).isEqualTo("/files/" + filePath); 66 assertThat(uri.toString()) 67 .isEqualTo("android://" + context.getPackageName() + "/files/" + filePath); 68 } 69 70 @Test builder_cacheLocation()71 public void builder_cacheLocation() { 72 String filePath = "module/shared/directory/file"; 73 File file = new File(context.getCacheDir(), filePath); 74 75 Uri uri = AndroidUri.builder(context).fromFile(file).build(); 76 assertThat(uri.getScheme()).isEqualTo("android"); 77 assertThat(uri.getAuthority()).isEqualTo(context.getPackageName()); 78 assertThat(uri.getPath()).isEqualTo("/cache/" + filePath); 79 assertThat(uri.toString()) 80 .isEqualTo("android://" + context.getPackageName() + "/cache/" + filePath); 81 } 82 83 @Test builder_filesLocation_withEmailAccount()84 public void builder_filesLocation_withEmailAccount() { 85 String filePath = "module/google.com:<internal>@gmail.com/directory/file"; 86 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 87 88 Uri uri = AndroidUri.builder(context).fromFile(file).build(); 89 assertThat(uri.getScheme()).isEqualTo("android"); 90 assertThat(uri.getAuthority()).isEqualTo(context.getPackageName()); 91 assertThat(uri.getPath()).isEqualTo("/files/" + filePath); 92 assertThat(uri.toString()) 93 .isEqualTo("android://" + context.getPackageName() + "/files/" + Uri.encode(filePath, "/")); 94 } 95 96 @Test builder_filesLocation_withEmptyAccountName()97 public void builder_filesLocation_withEmptyAccountName() { 98 String filePath = "module/google.com:/directory/file"; 99 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 100 101 assertThrows( 102 IllegalArgumentException.class, () -> AndroidUri.builder(context).fromFile(file).build()); 103 } 104 105 @Test builder_filesLocation_withEmptyAccountType()106 public void builder_filesLocation_withEmptyAccountType() { 107 String filePath = "module/:<internal>@gmail.com/directory/file"; 108 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 109 110 assertThrows( 111 IllegalArgumentException.class, () -> AndroidUri.builder(context).fromFile(file).build()); 112 } 113 114 @Test builder_filesLocation_withMalformedAccount()115 public void builder_filesLocation_withMalformedAccount() { 116 String filePath = "module/MALFORMED/directory/file"; 117 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 118 119 assertThrows( 120 IllegalArgumentException.class, () -> AndroidUri.builder(context).fromFile(file).build()); 121 } 122 123 @Test 124 @Config(sdk = Build.VERSION_CODES.N) builder_directBootFilesDirectory()125 public void builder_directBootFilesDirectory() { 126 String filePath = "module/shared/directory/file"; 127 File root = new File(AndroidFileEnvironment.getDeviceProtectedDataDir(context), "files"); 128 File file = new File(root, filePath); 129 130 Uri uri = AndroidUri.builder(context).fromFile(file).build(); 131 assertThat(uri.getScheme()).isEqualTo("android"); 132 assertThat(uri.getAuthority()).isEqualTo(context.getPackageName()); 133 assertThat(uri.getPath()).isEqualTo("/directboot-files/" + filePath); 134 assertThat(uri.toString()) 135 .isEqualTo("android://" + context.getPackageName() + "/directboot-files/" + filePath); 136 } 137 138 @Test 139 @Config(sdk = Build.VERSION_CODES.N) builder_directBootCacheDirectory()140 public void builder_directBootCacheDirectory() { 141 String filePath = "module/shared/directory/file"; 142 File root = new File(AndroidFileEnvironment.getDeviceProtectedDataDir(context), "cache"); 143 File file = new File(root, filePath); 144 145 Uri uri = AndroidUri.builder(context).fromFile(file).build(); 146 assertThat(uri.getScheme()).isEqualTo("android"); 147 assertThat(uri.getAuthority()).isEqualTo(context.getPackageName()); 148 assertThat(uri.getPath()).isEqualTo("/directboot-cache/" + filePath); 149 assertThat(uri.toString()) 150 .isEqualTo("android://" + context.getPackageName() + "/directboot-cache/" + filePath); 151 } 152 153 @Test builder_allowsEmptyRelativePath()154 public void builder_allowsEmptyRelativePath() { 155 String filePath = "module/shared/"; 156 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 157 Uri uri = AndroidUri.builder(context).fromFile(file).build(); 158 assertThat(uri.getPath()).isEqualTo("/files/" + filePath); 159 } 160 161 @Test builder_unsupportedModule_throwsException()162 public void builder_unsupportedModule_throwsException() { 163 String filePath = "shared/shared/file"; 164 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 165 assertThrows(IllegalArgumentException.class, () -> AndroidUri.builder(context).fromFile(file)); 166 } 167 168 @Test builder_fromManagedSharedFile_doesNotRequireAccountManager()169 public void builder_fromManagedSharedFile_doesNotRequireAccountManager() { 170 String filePath = "/managed/module/shared/file"; 171 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 172 Uri uri = AndroidUri.builder(context).fromFile(file).build(); 173 assertThat(uri.getPath()).isEqualTo(filePath); 174 } 175 176 @Test builder_fromManagedAccountFile_requiresAccountManager()177 public void builder_fromManagedAccountFile_requiresAccountManager() { 178 String filePath = "/managed/module/0/file"; 179 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 180 181 assertThrows( 182 IllegalArgumentException.class, () -> AndroidUri.builder(context).fromFile(file).build()); 183 } 184 185 @Test builder_fromManagedFile_readsFromAccountManager()186 public void builder_fromManagedFile_readsFromAccountManager() { 187 Account account = new Account("<internal>@gmail.com", "google.com"); 188 AccountManager mockManager = mock(AccountManager.class); 189 when(mockManager.getAccount(123)).thenReturn(Futures.immediateFuture(account)); 190 191 String filePath = "/managed/module/123/file"; 192 File file = new File(AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context), filePath); 193 194 Uri uri = AndroidUri.builder(context).fromFile(file, mockManager).build(); 195 assertThat(getPathFragment(uri, 2)).isEqualTo("google.com:<internal>@gmail.com"); 196 } 197 198 @Test builder_componentsAreSetByDefault()199 public void builder_componentsAreSetByDefault() { 200 Uri uri = AndroidUri.builder(context).build(); 201 assertThat(uri.getScheme()).isEqualTo("android"); 202 assertThat(uri.getAuthority()).isEqualTo(context.getPackageName()); 203 assertThat(uri.getPath()).isEqualTo("/files/common/shared/"); 204 assertThat(uri.toString()) 205 .isEqualTo("android://" + context.getPackageName() + "/files/common/shared/"); 206 } 207 208 @Test builder_setLocation_expectedUsage()209 public void builder_setLocation_expectedUsage() { 210 Uri uri = AndroidUri.builder(context).setInternalLocation().build(); 211 assertThat(getPathFragment(uri, 0)).isEqualTo("files"); 212 } 213 214 @Test builder_setLocation_externalLocation()215 public void builder_setLocation_externalLocation() { 216 Uri uri = AndroidUri.builder(context).setExternalLocation().build(); 217 assertThat(getPathFragment(uri, 0)).isEqualTo("external"); 218 } 219 220 @Test builder_setLocation_managed()221 public void builder_setLocation_managed() { 222 Uri uri = AndroidUri.builder(context).setManagedLocation().build(); 223 assertThat(getPathFragment(uri, 0)).isEqualTo("managed"); 224 } 225 226 @Test builder_setModule_expectedUsage()227 public void builder_setModule_expectedUsage() { 228 Uri uri = AndroidUri.builder(context).setModule("testmodule").build(); 229 assertThat(getPathFragment(uri, 1)).isEqualTo("testmodule"); 230 } 231 232 @Test builder_setModule_isValidated()233 public void builder_setModule_isValidated() { 234 assertThrows( 235 IllegalArgumentException.class, () -> AndroidUri.builder(context).setModule("").build()); 236 } 237 238 @Test builder_setModule_doesNotCollideWithManagedLocation()239 public void builder_setModule_doesNotCollideWithManagedLocation() { 240 assertThrows( 241 IllegalArgumentException.class, 242 () -> AndroidUri.builder(context).setModule("managed").build()); 243 } 244 245 @Test builder_sharedAccount_isSerializedAsShared()246 public void builder_sharedAccount_isSerializedAsShared() { 247 Uri uri = AndroidUri.builder(context).setAccount(AndroidUri.SHARED_ACCOUNT).build(); 248 assertThat(getPathFragment(uri, 2)).isEqualTo("shared"); 249 } 250 251 @Test builder_setAccount_isValidated()252 public void builder_setAccount_isValidated() { 253 assertThrows( 254 IllegalArgumentException.class, 255 () -> AndroidUri.builder(context).setAccount(new Account("", "")).build()); 256 } 257 258 @Test builder_setRelativePath_expectedUsage()259 public void builder_setRelativePath_expectedUsage() { 260 Uri uri = AndroidUri.builder(context).setRelativePath("testfile").build(); 261 assertThat(getPathFragment(uri, 3)).isEqualTo("testfile"); 262 } 263 264 @Test validateLocation_onlyAllowsPermittedLocations()265 public void validateLocation_onlyAllowsPermittedLocations() { 266 AndroidUri.validateLocation("files"); 267 AndroidUri.validateLocation("cache"); 268 AndroidUri.validateLocation("external"); 269 AndroidUri.validateLocation("directboot-files"); 270 AndroidUri.validateLocation("directboot-cache"); 271 AndroidUri.validateLocation("managed"); 272 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateLocation("")); 273 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateLocation("other")); 274 } 275 276 @Test validateModule_disallowsReservedModules()277 public void validateModule_disallowsReservedModules() { 278 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("reserved")); 279 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("RESERVED")); 280 } 281 282 @Test validateModule_allowsNonEmptyLowercaseLetters()283 public void validateModule_allowsNonEmptyLowercaseLetters() { 284 AndroidUri.validateModule("a"); 285 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("")); 286 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("A")); 287 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("Aa")); 288 289 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("mymodule0")); 290 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("myModule")); 291 } 292 293 @Test validateModule_allowsInterleavedUnderscores()294 public void validateModule_allowsInterleavedUnderscores() { 295 AndroidUri.validateModule("mymodule"); 296 AndroidUri.validateModule("my_module"); 297 AndroidUri.validateModule("my_module_two"); 298 299 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("my module")); 300 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("my-module")); 301 302 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("mymodule_")); 303 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("_mymodule")); 304 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("my_module_")); 305 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("_my_module")); 306 assertThrows(IllegalArgumentException.class, () -> AndroidUri.validateModule("my__module")); 307 } 308 309 @Test validateRelativePath_isNoOp()310 public void validateRelativePath_isNoOp() { 311 AndroidUri.validateRelativePath(""); 312 AndroidUri.validateRelativePath("myFile"); 313 AndroidUri.validateRelativePath("myDir/myFile"); 314 AndroidUri.validateRelativePath("myDir/../myFile"); 315 AndroidUri.validateRelativePath("/myDir/myFile"); 316 } 317 318 @Test builder_setPackage_expectedUsage()319 public void builder_setPackage_expectedUsage() { 320 Uri uri = AndroidUri.builder(context).setPackage("testpackage").build(); 321 assertThat(uri.getAuthority()).isEqualTo("testpackage"); 322 } 323 324 /** 325 * Utility method to get the i'th path fragment of {@code URI}. May throw exception if the URI is 326 * null, its path is null, or it does not have {@code index} path fragments. 327 */ getPathFragment(Uri uri, int index)328 private static String getPathFragment(Uri uri, int index) { 329 // A valid path begins with "/", so +1 is required to offset the first split element ("") 330 return uri.getPath().split("/")[index + 1]; 331 } 332 } 333