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