• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.accounts.Account;
19 import android.content.Context;
20 import android.net.Uri;
21 import android.os.Build;
22 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
23 import com.google.android.libraries.mobiledatadownload.file.common.internal.LiteTransformFragments;
24 import com.google.android.libraries.mobiledatadownload.file.common.internal.Preconditions;
25 import com.google.android.libraries.mobiledatadownload.file.transforms.TransformProtos;
26 import com.google.common.collect.ImmutableList;
27 import com.google.errorprone.annotations.CanIgnoreReturnValue;
28 import com.google.mobiledatadownload.TransformProto;
29 import java.io.File;
30 import java.util.Arrays;
31 import java.util.Collections;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Set;
35 import java.util.concurrent.ExecutionException;
36 import java.util.regex.Pattern;
37 import javax.annotation.Nullable;
38 
39 /** Helper class for "android:" URIs. */
40 public final class AndroidUri {
41 
42   /**
43    * Returns an android: scheme URI builder for package {@code packageName}. If no setter is called
44    * before {@link Builder#build}, the resultant URI will point to the common internal app storage,
45    * i.e. "android://<packageName>/files/common/shared/"
46    *
47    * @param context The android environment.
48    */
builder(Context context)49   public static Builder builder(Context context) {
50     return new Builder(context);
51   }
52 
AndroidUri()53   private AndroidUri() {}
54 
55   // Module names are non-empty strings of [a-z] with interleaved underscores
56   private static final Pattern MODULE_PATTERN = Pattern.compile("[a-z]+(_[a-z]+)*");
57 
58   // Name registered for the Android backend
59   static final String SCHEME_NAME = "android";
60 
61   // URI path fragments with special meaning
62   static final String FILES_LOCATION = "files";
63   static final String MANAGED_LOCATION = "managed";
64   static final String CACHE_LOCATION = "cache";
65   // See https://developer.android.com/training/articles/direct-boot.html
66   static final String DIRECT_BOOT_FILES_LOCATION = "directboot-files";
67   static final String DIRECT_BOOT_CACHE_LOCATION = "directboot-cache";
68   static final String EXTERNAL_LOCATION = "external";
69 
70   // The "managed" location maps to a subdirectory within /files/.
71   static final String MANAGED_FILES_DIR_SUBDIRECTORY = "managed";
72 
73   static final String COMMON_MODULE = "common";
74   static final Account SHARED_ACCOUNT = AccountSerialization.SHARED_ACCOUNT;
75 
76   // Module names reserved for future use or that are otherwise disallowed. Note that ImmutableSet
77   // is avoided in order to avoid guava dependency.
78   private static final Set<String> RESERVED_MODULES =
79       Collections.unmodifiableSet(
80           new HashSet<>(
81               Arrays.asList(
82                   "default", "unused", "special", "reserved", "shared", "virtual", "managed")));
83 
84   private static final Set<String> VALID_LOCATIONS =
85       Collections.unmodifiableSet(
86           new HashSet<>(
87               Arrays.asList(
88                   FILES_LOCATION,
89                   CACHE_LOCATION,
90                   MANAGED_LOCATION,
91                   DIRECT_BOOT_FILES_LOCATION,
92                   DIRECT_BOOT_CACHE_LOCATION,
93                   EXTERNAL_LOCATION)));
94 
95   /**
96    * Validates the {@code location} of an Android URI path; "files" and "directboot" are the only
97    * valid strings.
98    */
validateLocation(String location)99   static void validateLocation(String location) {
100     Preconditions.checkArgument(
101         VALID_LOCATIONS.contains(location),
102         "The only supported locations are %s: %s",
103         VALID_LOCATIONS,
104         location);
105   }
106   /**
107    * Validates the {@code module} of an Android URI path. Any non-empty string of [a-z] with
108    * interleaved underscores that is not listed as reserved is valid.
109    */
validateModule(String module)110   static void validateModule(String module) {
111     Preconditions.checkArgument(
112         MODULE_PATTERN.matcher(module).matches(), "Module must match [a-z]+(_[a-z]+)*: %s", module);
113     Preconditions.checkArgument(
114         !RESERVED_MODULES.contains(module),
115         "Module name is reserved and cannot be used: %s",
116         module);
117   }
118 
119   /**
120    * Validates the {@code unusedRelativePath} of an Android URI path. At present time this is a
121    * no-op.
122    *
123    * @param unusedRelativePath Not used.
124    */
validateRelativePath(String unusedRelativePath)125   static void validateRelativePath(String unusedRelativePath) {
126     // No-op
127   }
128 
129   /** Builder for Android Uris. */
130   public static class Builder {
131 
132     // URI authority; required
133     private final Context context;
134 
135     // URI path components; optional
136     private String packageName; // TODO: should default be ""?
137     private String location = AndroidUri.FILES_LOCATION;
138     private String module = AndroidUri.COMMON_MODULE;
139     private Account account = AndroidUri.SHARED_ACCOUNT;
140     private String relativePath = "";
141 
142     private final ImmutableList.Builder<String> encodedSpecs = ImmutableList.builder();
143 
Builder(Context context)144     private Builder(Context context) {
145       Preconditions.checkArgument(context != null, "Context cannot be null");
146       this.context = context;
147       this.packageName = context.getPackageName();
148     }
149 
150     /**
151      * Sets the package to use in the android uri AUTHORITY. Default is context.getPackageName().
152      */
153     @CanIgnoreReturnValue
setPackage(String packageName)154     public Builder setPackage(String packageName) {
155       this.packageName = packageName;
156       return this;
157     }
158 
159     @CanIgnoreReturnValue
setLocation(String location)160     private Builder setLocation(String location) {
161       AndroidUri.validateLocation(location);
162       this.location = location;
163       return this;
164     }
165 
166     @CanIgnoreReturnValue
setManagedLocation()167     public Builder setManagedLocation() {
168       return setLocation(MANAGED_LOCATION);
169     }
170 
171     @CanIgnoreReturnValue
setExternalLocation()172     public Builder setExternalLocation() {
173       return setLocation(EXTERNAL_LOCATION);
174     }
175 
176     @CanIgnoreReturnValue
setDirectBootFilesLocation()177     public Builder setDirectBootFilesLocation() {
178       return setLocation(DIRECT_BOOT_FILES_LOCATION);
179     }
180 
181     @CanIgnoreReturnValue
setDirectBootCacheLocation()182     public Builder setDirectBootCacheLocation() {
183       return setLocation(DIRECT_BOOT_CACHE_LOCATION);
184     }
185 
186     /** Internal location, aka "files", is the default location. */
187     @CanIgnoreReturnValue
setInternalLocation()188     public Builder setInternalLocation() {
189       return setLocation(FILES_LOCATION);
190     }
191 
192     @CanIgnoreReturnValue
setCacheLocation()193     public Builder setCacheLocation() {
194       return setLocation(CACHE_LOCATION);
195     }
196 
197     @CanIgnoreReturnValue
setModule(String module)198     public Builder setModule(String module) {
199       AndroidUri.validateModule(module);
200       this.module = module;
201       return this;
202     }
203 
204     /**
205      * Sets the account. AndroidUri.SHARED_ACCOUNT is the default, and it shows up as "shared" on
206      * the filesystem.
207      *
208      * <p>This method performs some account validation. Android Account itself requires that both
209      * the type and name fields be present. In addition to this requirement, this backend requires
210      * that the type contain no colons (as these are the delimiter used internally for the account
211      * serialization), and that neither the type nor the name include any slashes (as these are file
212      * separators).
213      *
214      * <p>The account will be URL encoded in its URI representation (so, eg, "<internal>@gmail.com"
215      * will appear as "you%40gmail.com"), but not in the file path representation used to access
216      * disk.
217      *
218      * <p>Note the Linux filesystem accepts filenames composed of any bytes except "/" and NULL.
219      *
220      * @param account The account to set.
221      * @return The fluent Builder.
222      */
223     @CanIgnoreReturnValue
setAccount(Account account)224     public Builder setAccount(Account account) {
225       AccountSerialization.serialize(account); // performs validation internally
226       this.account = account;
227       return this;
228     }
229 
230     /**
231      * Sets the component of the path after location, module and account. A single leading slash
232      * will be trimmed if present.
233      */
234     @CanIgnoreReturnValue
setRelativePath(String relativePath)235     public Builder setRelativePath(String relativePath) {
236       if (relativePath.startsWith("/")) {
237         relativePath = relativePath.substring(1);
238       }
239       AndroidUri.validateRelativePath(relativePath);
240       this.relativePath = relativePath;
241       return this;
242     }
243 
244     /**
245      * Updates builder with multiple fields from file param: location, module, account and relative
246      * path. This method will fail on "managed" paths (see {@link fromFile(File, AccountManager)}).
247      */
248     @CanIgnoreReturnValue
fromFile(File file)249     public Builder fromFile(File file) {
250       return fromAbsolutePath(file.getAbsolutePath(), /* accountManager= */ null);
251     }
252 
253     /**
254      * Updates builder with multiple fields from file param: location, module, account and relative
255      * path. A non-null {@code accountManager} is required to handle "managed" paths.
256      */
257     @CanIgnoreReturnValue
fromFile(File file, @Nullable AccountManager accountManager)258     public Builder fromFile(File file, @Nullable AccountManager accountManager) {
259       return fromAbsolutePath(file.getAbsolutePath(), accountManager);
260     }
261 
262     /**
263      * Updates builder with multiple fields from absolute path param: location, module, account and
264      * relative path. This method will fail on "managed" paths (see {@link fromAbsolutePath(String,
265      * AccountManager)}).
266      */
267     @CanIgnoreReturnValue
fromAbsolutePath(String absolutePath)268     public Builder fromAbsolutePath(String absolutePath) {
269       return fromAbsolutePath(absolutePath, /* accountManager= */ null);
270     }
271 
272     /**
273      * Updates builder with multiple fields from absolute path param: location, module, account and
274      * relative path. A non-null {@code accountManager} is required to handle "managed" paths.
275      */
276     // TODO(b/129467051): remove requirement for segments after 0th (logical location)
277     @CanIgnoreReturnValue
fromAbsolutePath(String absolutePath, @Nullable AccountManager accountManager)278     public Builder fromAbsolutePath(String absolutePath, @Nullable AccountManager accountManager) {
279       // Get the file's path within internal files, /module/account</relativePath>
280       File filesDir = AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context);
281       String filesDirPath = filesDir.getAbsolutePath();
282       String cacheDirPath = context.getCacheDir().getAbsolutePath();
283       String managedDirPath = new File(filesDir, MANAGED_FILES_DIR_SUBDIRECTORY).getAbsolutePath();
284       String externalDirPath = null;
285       File externalFilesDir = context.getExternalFilesDir(null);
286       if (externalFilesDir != null) {
287         externalDirPath = externalFilesDir.getAbsolutePath();
288       }
289       String directBootFilesPath = null;
290       String directBootCachePath = null;
291       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
292         // TODO(b/143610872): run after checking other dirs to minimize impact of new Context()'s
293         File dpsDataDir = AndroidFileEnvironment.getDeviceProtectedDataDir(context);
294         directBootFilesPath = new File(dpsDataDir, "files").getAbsolutePath();
295         directBootCachePath = new File(dpsDataDir, "cache").getAbsolutePath();
296       }
297 
298       String internalPath;
299       if (absolutePath.startsWith(managedDirPath)) {
300         // managedDirPath must be checked before filesDirPath because filesDirPath is a prefix.
301         setLocation(AndroidUri.MANAGED_LOCATION);
302         internalPath = absolutePath.substring(managedDirPath.length());
303       } else if (absolutePath.startsWith(filesDirPath)) {
304         setLocation(AndroidUri.FILES_LOCATION);
305         internalPath = absolutePath.substring(filesDirPath.length());
306       } else if (absolutePath.startsWith(cacheDirPath)) {
307         setLocation(AndroidUri.CACHE_LOCATION);
308         internalPath = absolutePath.substring(cacheDirPath.length());
309       } else if (externalDirPath != null && absolutePath.startsWith(externalDirPath)) {
310         setLocation(AndroidUri.EXTERNAL_LOCATION);
311         internalPath = absolutePath.substring(externalDirPath.length());
312       } else if (directBootFilesPath != null && absolutePath.startsWith(directBootFilesPath)) {
313         setLocation(AndroidUri.DIRECT_BOOT_FILES_LOCATION);
314         internalPath = absolutePath.substring(directBootFilesPath.length());
315       } else if (directBootCachePath != null && absolutePath.startsWith(directBootCachePath)) {
316         setLocation(AndroidUri.DIRECT_BOOT_CACHE_LOCATION);
317         internalPath = absolutePath.substring(directBootCachePath.length());
318       } else {
319         throw new IllegalArgumentException(
320             "Path must be in app-private files dir or external files dir: " + absolutePath);
321       }
322 
323       // Extract components according to android: file layout. The 0th element of split() will be
324       // an empty string preceding the first character "/"
325       List<String> pathFragments = Arrays.asList(internalPath.split(File.separator));
326       Preconditions.checkArgument(
327           pathFragments.size() >= 3,
328           "Path must be in module and account subdirectories: %s",
329           absolutePath);
330       setModule(pathFragments.get(1));
331 
332       String accountStr = pathFragments.get(2);
333       if (MANAGED_LOCATION.equals(location) && !AccountSerialization.isSharedAccount(accountStr)) {
334         int accountId;
335         try {
336           accountId = Integer.parseInt(accountStr);
337         } catch (NumberFormatException e) {
338           throw new IllegalArgumentException(e);
339         }
340 
341         // Blocks on disk IO to read account table.
342         // TODO(b/115940396): surface bad account as FileNotFoundException (change API signature?)
343         Preconditions.checkArgument(accountManager != null, "AccountManager cannot be null");
344         try {
345           setAccount(accountManager.getAccount(accountId).get());
346         } catch (InterruptedException e) {
347           Thread.currentThread().interrupt();
348           throw new IllegalArgumentException(new MalformedUriException(e));
349         } catch (ExecutionException e) {
350           throw new IllegalArgumentException(new MalformedUriException(e.getCause()));
351         }
352       } else {
353         setAccount(AccountSerialization.deserialize(accountStr));
354       }
355 
356       setRelativePath(internalPath.substring(module.length() + accountStr.length() + 2));
357       return this;
358     }
359 
360     @CanIgnoreReturnValue
withTransform(TransformProto.Transform spec)361     public Builder withTransform(TransformProto.Transform spec) {
362       encodedSpecs.add(TransformProtos.toEncodedSpec(spec));
363       return this;
364     }
365 
366     // TODO(b/115940396): add MalformedUriException to signature
build()367     public Uri build() {
368       String uriPath =
369           "/"
370               + location
371               + "/"
372               + module
373               + "/"
374               + AccountSerialization.serialize(account)
375               + "/"
376               + relativePath;
377       String fragment = LiteTransformFragments.joinTransformSpecs(encodedSpecs.build());
378 
379       return new Uri.Builder()
380           .scheme(AndroidUri.SCHEME_NAME)
381           .authority(packageName)
382           .path(uriPath)
383           .encodedFragment(fragment)
384           .build();
385     }
386   }
387 }
388