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.content.Context; 19 import android.net.Uri; 20 import android.os.ParcelFileDescriptor; 21 import android.util.Pair; 22 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException; 23 import com.google.android.libraries.mobiledatadownload.file.common.internal.Preconditions; 24 import com.google.android.libraries.mobiledatadownload.file.spi.Backend; 25 import com.google.errorprone.annotations.CanIgnoreReturnValue; 26 import java.io.Closeable; 27 import java.io.IOException; 28 import java.io.InputStream; 29 30 /** 31 * A backend for accessing remote content that uses the Android platform content resolver framework. 32 * It can be used standalone or as a remote URI resolver within the {@link AndroidFileBackend}. 33 * 34 * <p>Usage: <code> 35 * AndroidFileBackend backend = 36 * AndroidFileBackend.builder(context) 37 * .setRemoteBackend(ContentResolverBackend.builder(context).setEmbedded(true).build()) 38 * .build(); 39 * </code> 40 * 41 * <p>NOTE: In most cases, you'll want to use the GmsClientBackend for accessing files from GMS 42 * core. This backend is used to access files from other Apps. Since there are possible security 43 * concerns with doing so, ContentResolverBackend is restricted to the "content_resolver_whitelist". 44 * See <internal> for more information. 45 */ 46 public final class ContentResolverBackend implements Backend { 47 48 private static final String CONTENT_SCHEME = "content"; 49 50 private final Context context; 51 private final boolean isEmbedded; 52 builder(Context context)53 public static Builder builder(Context context) { 54 return new Builder(context); 55 } 56 57 /** Builder for {@code ContentResolverBackend}. */ 58 public static class Builder { 59 private final Context context; 60 private boolean isEmbedded = false; 61 62 /** Construct a new builder instance. */ Builder(Context context)63 private Builder(Context context) { 64 this.context = context; 65 } 66 67 /** 68 * Tells whether this backend is expected to be embedded in another backend. If so, rewrites the 69 * scheme to "content"; if not, requires that the scheme be "content". 70 */ 71 @CanIgnoreReturnValue setEmbedded(boolean isEmbedded)72 public Builder setEmbedded(boolean isEmbedded) { 73 this.isEmbedded = isEmbedded; 74 return this; 75 } 76 build()77 public ContentResolverBackend build() { 78 return new ContentResolverBackend(context, isEmbedded); 79 } 80 } 81 ContentResolverBackend(Context context, boolean isEmbedded)82 private ContentResolverBackend(Context context, boolean isEmbedded) { 83 this.context = context.getApplicationContext(); 84 this.isEmbedded = isEmbedded; 85 } 86 87 @Override name()88 public String name() { 89 Preconditions.checkState(!isEmbedded, "Misconfigured embedded backend."); 90 return CONTENT_SCHEME; 91 } 92 93 @Override openForRead(Uri uri)94 public InputStream openForRead(Uri uri) throws IOException { 95 Uri contentUri = rewriteAndCheckUri(uri); 96 return context.getContentResolver().openInputStream(contentUri); 97 } 98 99 @Override openForNativeRead(Uri uri)100 public Pair<Uri, Closeable> openForNativeRead(Uri uri) throws IOException { 101 Uri contentUri = rewriteAndCheckUri(uri); 102 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(contentUri, "r"); 103 return FileDescriptorUri.fromParcelFileDescriptor(pfd); 104 } 105 rewriteAndCheckUri(Uri uri)106 private Uri rewriteAndCheckUri(Uri uri) throws MalformedUriException { 107 if (isEmbedded) { 108 return uri.buildUpon().scheme(CONTENT_SCHEME).build(); 109 } 110 if (!CONTENT_SCHEME.equals(uri.getScheme())) { 111 throw new MalformedUriException("Expected scheme to be " + CONTENT_SCHEME); 112 } 113 return uri; 114 } 115 } 116