summaryrefslogtreecommitdiff
path: root/java/com/google/android/libraries/mobiledatadownload/file/backends/AndroidUriAdapter.java
blob: c7c8ff1eab9ff76015dc906013ce7e61852da905 (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
/*
 * 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.accounts.Account;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
import java.io.File;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;

/**
 * Adapter for converting "android:" URIs into java.io.File. This is considered dangerous since it
 * ignores parts of the Uri at the caller's peril, and thus is only available to whitelisted clients
 * (mostly internal).
 */
public final class AndroidUriAdapter implements UriAdapter {

  private final Context context;
  @Nullable private final AccountManager accountManager;

  private AndroidUriAdapter(Context context, @Nullable AccountManager accountManager) {
    this.context = context;
    this.accountManager = accountManager;
  }

  /** This adapter will fail on "managed" URIs (see {@link forContext(Context, AccountManager)}). */
  public static AndroidUriAdapter forContext(Context context) {
    return new AndroidUriAdapter(context, /* accountManager= */ null);
  }

  /** A non-null {@code accountManager} is required to handle "managed" paths. */
  public static AndroidUriAdapter forContext(Context context, AccountManager accountManager) {
    return new AndroidUriAdapter(context, accountManager);
  }

  /* @throws MalformedUriException if the uri is not valid. */
  public static void validate(Uri uri) throws MalformedUriException {
    if (!uri.getScheme().equals(AndroidUri.SCHEME_NAME)) {
      throw new MalformedUriException("Scheme must be 'android'");
    }
    if (uri.getPathSegments().isEmpty()) {
      throw new MalformedUriException(
          String.format("Path must start with a valid logical location: %s", uri));
    }
    if (!TextUtils.isEmpty(uri.getQuery())) {
      throw new MalformedUriException("Did not expect uri to have query");
    }
  }

  @Override
  public File toFile(Uri uri) throws MalformedUriException {
    validate(uri);
    ArrayList<String> pathSegments = new ArrayList<>(uri.getPathSegments()); // allow modification
    File rootLocation;
    switch (pathSegments.get(0)) {
      case AndroidUri.DIRECT_BOOT_FILES_LOCATION:
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
          rootLocation = context.createDeviceProtectedStorageContext().getFilesDir();
        } else {
          throw new MalformedUriException(
              String.format(
                  "Direct boot only exists on N or greater: current SDK %s",
                  Build.VERSION.SDK_INT));
        }

        break;
      case AndroidUri.DIRECT_BOOT_CACHE_LOCATION:
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
          rootLocation = context.createDeviceProtectedStorageContext().getCacheDir();
        } else {
          throw new MalformedUriException(
              String.format(
                  "Direct boot only exists on N or greater: current SDK %s",
                  Build.VERSION.SDK_INT));
        }

        break;
      case AndroidUri.FILES_LOCATION:
        rootLocation = AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context);
        break;
      case AndroidUri.CACHE_LOCATION:
        rootLocation = context.getCacheDir();
        break;
      case AndroidUri.MANAGED_LOCATION:
        File filesDir = AndroidFileEnvironment.getFilesDirWithPreNWorkaround(context);
        rootLocation = new File(filesDir, AndroidUri.MANAGED_FILES_DIR_SUBDIRECTORY);

        // Transform account segment from logical (plaintext) to physical (integer) representation.
        if (pathSegments.size() >= 3) {
          Account account;
          try {
            account = AccountSerialization.deserialize(pathSegments.get(2));
          } catch (IllegalArgumentException e) {
            throw new MalformedUriException(e);
          }
          if (!AccountSerialization.isSharedAccount(account)) {
            if (accountManager == null) {
              throw new MalformedUriException("AccountManager cannot be null");
            }
            // Blocks on disk IO to read account table.
            try {
              int accountId = accountManager.getAccountId(account).get();
              pathSegments.set(2, Integer.toString(accountId));
            } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              throw new MalformedUriException(e);
            } catch (ExecutionException e) {
              // TODO(b/115940396): surface bad account as FileNotFoundException (change signature?)
              throw new MalformedUriException(e.getCause());
            }
          }
        }

        break;
      case AndroidUri.EXTERNAL_LOCATION:
        rootLocation = context.getExternalFilesDir(null);
        break;
      default:
        throw new MalformedUriException(
            String.format("Path must start with a valid logical location: %s", uri));
    }
    return new File(
        rootLocation, TextUtils.join(File.separator, pathSegments.subList(1, pathSegments.size())));
  }
}