summaryrefslogtreecommitdiff
path: root/java/com/google/android/libraries/mobiledatadownload/file/backends/AndroidFileBackend.java
blob: e1d8b9d6edaba2298f9d450ba2e4634699d7dd51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/*
 * Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.libraries.mobiledatadownload.file.backends;

import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import com.google.android.libraries.mobiledatadownload.file.common.FileStorageUnavailableException;
import com.google.android.libraries.mobiledatadownload.file.common.LockScope;
import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
import com.google.android.libraries.mobiledatadownload.file.common.internal.Preconditions;
import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
import com.google.android.libraries.mobiledatadownload.file.spi.ForwardingBackend;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;

/** A backend that implements "android:" scheme using {@link JavaFileBackend}. */
public final class AndroidFileBackend extends ForwardingBackend {

  private final Context context;
  private final Backend backend;
  private final DirectBootChecker directBootChecker;
  @Nullable private final Backend remoteBackend;
  @Nullable private final AccountManager accountManager;

  private final Object lock = new Object();

  @GuardedBy("lock")
  @Nullable
  private String lazyDpsDataDirPath; // Initialized and accessed via getDpsDataDirPath()

  /**
   * Returns an {@link AndroidFileBackend} builder for the calling {@code context}. Most options are
   * disabled by default; see javadoc in {@link Builder} for further configuration documentation.
   */
  public static Builder builder(Context context) {
    return new Builder(context);
  }

  /**
   * Returns an {@link AndroidFileBackend} with the customized {@code backend}. Should only be used
   * in test where a customized backend is needed for simulating file operation failures or delays.
   */
  @VisibleForTesting
  public static Builder builderWithOverrideForTest(Context context, Backend backend) {
    Preconditions.checkArgument(
        backend != null, "Cannot invoke builderWithOverrideForTest with null supplied as Backend.");
    Builder builder = new Builder(context);
    builder.backend = backend;
    return builder;
  }

  /** Builder for the {@link AndroidFileBackend} class. */
  public static final class Builder {
    // Required parameters
    private final Context context;

    // Optional parameters
    @Nullable private Backend remoteBackend;
    @Nullable private AccountManager accountManager;
    @Nullable private Backend backend;
    private LockScope lockScope = new LockScope();

    private Builder(Context context) {
      Preconditions.checkArgument(context != null, "Context cannot be null");
      this.context = context.getApplicationContext();
    }

    /**
     * Sets the remote backend that is invoked when the URI's authority refers to a package other
     * than your own. The only methods called on {@code remoteBackend} are {@link #openForRead} and
     * {@link #openForNativeRead}, though this may expand in the future. Defaults to {@code null}.
     */
    public Builder setRemoteBackend(Backend remoteBackend) {
      this.remoteBackend = remoteBackend;
      return this;
    }

    /**
     * Sets the {@link AccountManager} invoked to resolve "managed" URIs. Defaults to {@code null},
     * in which case operations on "managed" URIs will fail.
     */
    public Builder setAccountManager(AccountManager accountManager) {
      this.accountManager = accountManager;
      return this;
    }

    /**
     * Overrides the default backend-scoped {@link LockScope} with the given {@code lockScope}. This
     * injection is only necessary if there are multiple backend instances in the same process and
     * there's a risk of them acquiring a lock on the same underlying file.
     */
    public Builder setLockScope(LockScope lockScope) {
      Preconditions.checkArgument(
          backend == null,
          "LockScope will not be used in the custom backend. Only call builderWithOverrideForTest"
              + " if you want to override the backend for testing, or call builder together with"
              + " setLockScope to set a new lock scope.");
      this.lockScope = lockScope;
      return this;
    }

    public AndroidFileBackend build() {
      return new AndroidFileBackend(this);
    }
  }

  private AndroidFileBackend(Builder builder) {
    backend = builder.backend != null ? builder.backend : new JavaFileBackend(builder.lockScope);
    context = builder.context;
    remoteBackend = builder.remoteBackend;
    accountManager = builder.accountManager;

    directBootChecker = unusedContext -> true;
  }

  @Override
  protected Backend delegate() {
    return backend;
  }

  @Override
  public String name() {
    return "android";
  }

  /**
   * {@inheritDoc}
   *
   * <p>URI may belong to a different authority.
   */
  @Override
  public InputStream openForRead(Uri uri) throws IOException {
    if (isRemoteAuthority(uri)) {
      throwIfRemoteBackendUnavailable();
      return remoteBackend.openForRead(uri);
    }
    return super.openForRead(uri);
  }

  /**
   * {@inheritDoc}
   *
   * <p>URI may belong to a different authority.
   */
  @Override
  public Pair<Uri, Closeable> openForNativeRead(Uri uri) throws IOException {
    if (isRemoteAuthority(uri)) {
      throwIfRemoteBackendUnavailable();
      return remoteBackend.openForNativeRead(uri);
    }
    return super.openForNativeRead(uri);
  }

  /**
   * {@inheritDoc}
   *
   * <p>URI may belong to a different authority.
   */
  @Override
  public boolean exists(Uri uri) throws IOException {
    if (isRemoteAuthority(uri)) {
      throwIfRemoteBackendUnavailable();
      return remoteBackend.exists(uri);
    }
    return super.exists(uri);
  }

  private boolean isRemoteAuthority(Uri uri) {
    return !TextUtils.isEmpty(uri.getAuthority())
        && !context.getPackageName().equals(uri.getAuthority());
  }

  private void throwIfRemoteUri(Uri uri) throws IOException {
    if (isRemoteAuthority(uri)) {
      throw new IOException("operation is not permitted in other authorities.");
    }
  }

  private void throwIfRemoteBackendUnavailable() throws FileStorageUnavailableException {
    if (remoteBackend == null) {
      throw new FileStorageUnavailableException(
          "Android backend cannot perform remote operations without a remote backend");
    }
  }

  @Override
  protected Uri rewriteUri(Uri uri) throws IOException {
    // Converts from android -> file
    if (isRemoteAuthority(uri)) {
      throw new MalformedUriException("Operation across authorities is not allowed.");
    }
    File file = toFile(uri);
    Uri fileUri = FileUri.builder().fromFile(file).build();
    return fileUri;
  }

  @Override
  protected Uri reverseRewriteUri(Uri uri) throws IOException {
    // Converts from file -> android
    try {
      return AndroidUri.builder(context).fromAbsolutePath(uri.getPath(), accountManager).build();
    } catch (IllegalArgumentException e) {
      throw new MalformedUriException(e);
    }
  }

  @Override
  public File toFile(Uri uri) throws IOException {
    throwIfRemoteUri(uri);
    File file = AndroidUriAdapter.forContext(context, accountManager).toFile(uri);
    throwIfStorageIsLocked(file);
    return file;
  }

  /** Utilities for interacting with Android Direct Boot mode. */
  private interface DirectBootChecker {
    /** Returns true if the device doesn't support direct boot or the user is unlocked. */
    boolean isUserUnlocked(Context context);
  }

  private void throwIfStorageIsLocked(File file) throws FileStorageUnavailableException {
    // If the device doesn't support DirectBoot or has been unlocked, all files are available.
    if (directBootChecker.isUserUnlocked(context)) {
      return;
    }

    // During DirectBoot, only files in device-protected storage are available.
    String dpsDataDirPath = getDpsDataDirPath();
    String filePath = file.getAbsolutePath();
    if (!filePath.startsWith(dpsDataDirPath)) {
      throw new FileStorageUnavailableException(
          "Cannot access credential-protected data from direct boot");
    }
  }

  @TargetApi(Build.VERSION_CODES.N)
  private String getDpsDataDirPath() {
    synchronized (lock) {
      if (lazyDpsDataDirPath == null) {
        File dpsDataDir = AndroidFileEnvironment.getDeviceProtectedDataDir(context);
        lazyDpsDataDirPath = dpsDataDir.getAbsolutePath();
      }
      return lazyDpsDataDirPath;
    }
  }
}