• 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.annotation.TargetApi;
19 import android.content.Context;
20 import android.net.Uri;
21 import android.os.Build;
22 import android.text.TextUtils;
23 import android.util.Pair;
24 import androidx.annotation.VisibleForTesting;
25 import com.google.android.libraries.mobiledatadownload.file.common.FileStorageUnavailableException;
26 import com.google.android.libraries.mobiledatadownload.file.common.LockScope;
27 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
28 import com.google.android.libraries.mobiledatadownload.file.common.internal.Preconditions;
29 import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
30 import com.google.android.libraries.mobiledatadownload.file.spi.ForwardingBackend;
31 import com.google.errorprone.annotations.CanIgnoreReturnValue;
32 import com.google.errorprone.annotations.concurrent.GuardedBy;
33 import java.io.Closeable;
34 import java.io.File;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import javax.annotation.Nullable;
38 
39 /** A backend that implements "android:" scheme using {@link JavaFileBackend}. */
40 public final class AndroidFileBackend extends ForwardingBackend {
41 
42   private final Context context;
43   private final Backend backend;
44   private final DirectBootChecker directBootChecker;
45   @Nullable private final Backend remoteBackend;
46   @Nullable private final AccountManager accountManager;
47 
48   private final Object lock = new Object();
49 
50   @GuardedBy("lock")
51   @Nullable
52   private String lazyDpsDataDirPath; // Initialized and accessed via getDpsDataDirPath()
53 
54   /**
55    * Returns an {@link AndroidFileBackend} builder for the calling {@code context}. Most options are
56    * disabled by default; see javadoc in {@link Builder} for further configuration documentation.
57    */
builder(Context context)58   public static Builder builder(Context context) {
59     return new Builder(context);
60   }
61 
62   /**
63    * Returns an {@link AndroidFileBackend} with the customized {@code backend}. Should only be used
64    * in test where a customized backend is needed for simulating file operation failures or delays.
65    */
66   @VisibleForTesting
builderWithOverrideForTest(Context context, Backend backend)67   public static Builder builderWithOverrideForTest(Context context, Backend backend) {
68     Preconditions.checkArgument(
69         backend != null, "Cannot invoke builderWithOverrideForTest with null supplied as Backend.");
70     Builder builder = new Builder(context);
71     builder.backend = backend;
72     return builder;
73   }
74 
75   /** Builder for the {@link AndroidFileBackend} class. */
76   public static final class Builder {
77     // Required parameters
78     private final Context context;
79 
80     // Optional parameters
81     @Nullable private Backend remoteBackend;
82     @Nullable private AccountManager accountManager;
83     @Nullable private Backend backend;
84     private LockScope lockScope = new LockScope();
85 
Builder(Context context)86     private Builder(Context context) {
87       Preconditions.checkArgument(context != null, "Context cannot be null");
88       this.context = context.getApplicationContext();
89     }
90 
91     /**
92      * Sets the remote backend that is invoked when the URI's authority refers to a package other
93      * than your own. The only methods called on {@code remoteBackend} are {@link #openForRead} and
94      * {@link #openForNativeRead}, though this may expand in the future. Defaults to {@code null}.
95      */
96     @CanIgnoreReturnValue
setRemoteBackend(Backend remoteBackend)97     public Builder setRemoteBackend(Backend remoteBackend) {
98       this.remoteBackend = remoteBackend;
99       return this;
100     }
101 
102     /**
103      * Sets the {@link AccountManager} invoked to resolve "managed" URIs. Defaults to {@code null},
104      * in which case operations on "managed" URIs will fail.
105      */
106     @CanIgnoreReturnValue
setAccountManager(AccountManager accountManager)107     public Builder setAccountManager(AccountManager accountManager) {
108       this.accountManager = accountManager;
109       return this;
110     }
111 
112     /**
113      * Overrides the default backend-scoped {@link LockScope} with the given {@code lockScope}. This
114      * injection is only necessary if there are multiple backend instances in the same process and
115      * there's a risk of them acquiring a lock on the same underlying file.
116      */
117     @CanIgnoreReturnValue
setLockScope(LockScope lockScope)118     public Builder setLockScope(LockScope lockScope) {
119       Preconditions.checkArgument(
120           backend == null,
121           "LockScope will not be used in the custom backend. Only call builderWithOverrideForTest"
122               + " if you want to override the backend for testing, or call builder together with"
123               + " setLockScope to set a new lock scope.");
124       this.lockScope = lockScope;
125       return this;
126     }
127 
build()128     public AndroidFileBackend build() {
129       return new AndroidFileBackend(this);
130     }
131   }
132 
AndroidFileBackend(Builder builder)133   private AndroidFileBackend(Builder builder) {
134     backend = builder.backend != null ? builder.backend : new JavaFileBackend(builder.lockScope);
135     context = builder.context;
136     remoteBackend = builder.remoteBackend;
137     accountManager = builder.accountManager;
138 
139     directBootChecker = unusedContext -> true;
140   }
141 
142   @Override
delegate()143   protected Backend delegate() {
144     return backend;
145   }
146 
147   @Override
name()148   public String name() {
149     return "android";
150   }
151 
152   /**
153    * {@inheritDoc}
154    *
155    * <p>URI may belong to a different authority.
156    */
157   @Override
openForRead(Uri uri)158   public InputStream openForRead(Uri uri) throws IOException {
159     if (isRemoteAuthority(uri)) {
160       throwIfRemoteBackendUnavailable();
161       return remoteBackend.openForRead(uri);
162     }
163     return super.openForRead(uri);
164   }
165 
166   /**
167    * {@inheritDoc}
168    *
169    * <p>URI may belong to a different authority.
170    */
171   @Override
openForNativeRead(Uri uri)172   public Pair<Uri, Closeable> openForNativeRead(Uri uri) throws IOException {
173     if (isRemoteAuthority(uri)) {
174       throwIfRemoteBackendUnavailable();
175       return remoteBackend.openForNativeRead(uri);
176     }
177     return super.openForNativeRead(uri);
178   }
179 
180   /**
181    * {@inheritDoc}
182    *
183    * <p>URI may belong to a different authority.
184    */
185   @Override
exists(Uri uri)186   public boolean exists(Uri uri) throws IOException {
187     if (isRemoteAuthority(uri)) {
188       throwIfRemoteBackendUnavailable();
189       return remoteBackend.exists(uri);
190     }
191     return super.exists(uri);
192   }
193 
isRemoteAuthority(Uri uri)194   private boolean isRemoteAuthority(Uri uri) {
195     return !TextUtils.isEmpty(uri.getAuthority())
196         && !context.getPackageName().equals(uri.getAuthority());
197   }
198 
throwIfRemoteUri(Uri uri)199   private void throwIfRemoteUri(Uri uri) throws IOException {
200     if (isRemoteAuthority(uri)) {
201       throw new IOException("operation is not permitted in other authorities.");
202     }
203   }
204 
throwIfRemoteBackendUnavailable()205   private void throwIfRemoteBackendUnavailable() throws FileStorageUnavailableException {
206     if (remoteBackend == null) {
207       throw new FileStorageUnavailableException(
208           "Android backend cannot perform remote operations without a remote backend");
209     }
210   }
211 
212   @Override
rewriteUri(Uri uri)213   protected Uri rewriteUri(Uri uri) throws IOException {
214     // Converts from android -> file
215     if (isRemoteAuthority(uri)) {
216       throw new MalformedUriException("Operation across authorities is not allowed.");
217     }
218     File file = toFile(uri);
219     Uri fileUri = FileUri.builder().fromFile(file).build();
220     return fileUri;
221   }
222 
223   @Override
reverseRewriteUri(Uri uri)224   protected Uri reverseRewriteUri(Uri uri) throws IOException {
225     // Converts from file -> android
226     try {
227       return AndroidUri.builder(context).fromAbsolutePath(uri.getPath(), accountManager).build();
228     } catch (IllegalArgumentException e) {
229       throw new MalformedUriException(e);
230     }
231   }
232 
233   @Override
toFile(Uri uri)234   public File toFile(Uri uri) throws IOException {
235     throwIfRemoteUri(uri);
236     File file = AndroidUriAdapter.forContext(context, accountManager).toFile(uri);
237     throwIfStorageIsLocked(file);
238     return file;
239   }
240 
241   /** Utilities for interacting with Android Direct Boot mode. */
242   private interface DirectBootChecker {
243     /** Returns true if the device doesn't support direct boot or the user is unlocked. */
isUserUnlocked(Context context)244     boolean isUserUnlocked(Context context);
245   }
246 
throwIfStorageIsLocked(File file)247   private void throwIfStorageIsLocked(File file) throws FileStorageUnavailableException {
248     // If the device doesn't support DirectBoot or has been unlocked, all files are available.
249     if (directBootChecker.isUserUnlocked(context)) {
250       return;
251     }
252 
253     // During DirectBoot, only files in device-protected storage are available.
254     String dpsDataDirPath = getDpsDataDirPath();
255     String filePath = file.getAbsolutePath();
256     if (!filePath.startsWith(dpsDataDirPath)) {
257       throw new FileStorageUnavailableException(
258           "Cannot access credential-protected data from direct boot");
259     }
260   }
261 
262   @TargetApi(Build.VERSION_CODES.N)
getDpsDataDirPath()263   private String getDpsDataDirPath() {
264     synchronized (lock) {
265       if (lazyDpsDataDirPath == null) {
266         File dpsDataDir = AndroidFileEnvironment.getDeviceProtectedDataDir(context);
267         lazyDpsDataDirPath = dpsDataDir.getAbsolutePath();
268       }
269       return lazyDpsDataDirPath;
270     }
271   }
272 }
273