/* * 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 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()))); } }