DelegatingContext.java

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 Lice`nse 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 androidx.test.rule.provider;

import static androidx.test.internal.util.Checks.checkArgument;
import static androidx.test.internal.util.Checks.checkNotNull;

import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.Display;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;

/**
 * This {@code DelegatingContext} renames files with a configurable prefix for file and database
 * operations. It wraps in a {@link ContentResolver} and delegates the supported methods for the
 * {@link Context} passed in when creating the {@code DelegatingContext}. The supported methods
 * include:
 *
 * <ul>
 *   <li>{@link Context#getResources()}
 *   <li>{@link Context#getAssets()}
 * </ul>
 */
class DelegatingContext extends ContextWrapper {

  private static final String TAG = "DelegatingContext";
  private static final int NO_OP_UID = -1;
  private static final int NO_OP_PID = -1;

  private final String prefix;
  private final Context context;
  private final ContentResolver contentResolver;
  private Set<String> databases = new HashSet<>();
  private Set<String> files = new HashSet<>();
  private Set<String> revokedPermissions = new HashSet<>();

  /**
   * Constructor of the {@code DelegatingContext} with the {@link Context} to be delegated, a
   * specific prefix to use when renaming files and databases, and the {@link ContentResolver} to
   * wrap in.
   *
   * @param context The {@link Context} to be delegated by this {@code DelegatingContext}.
   * @param prefix The prefix to use when renaming files and databases.
   * @param contentResolver The {@link ContentResolver} made available in this {@code
   *     DelegatingContext}.
   */
  public DelegatingContext(
      @NonNull Context context, @NonNull String prefix, @NonNull ContentResolver contentResolver) {
    super(checkNotNull(context));
    this.context = context;
    this.prefix = checkNotNull(prefix);
    this.contentResolver = checkNotNull(contentResolver);
  }

  /**
   * @return The {@link ContentResolver} wrapped in when creating current {@code DelegatingContext}.
   */
  @Override
  public ContentResolver getContentResolver() {
    return contentResolver;
  }

  @Override
  public File getDir(@NonNull String name, int mode) {
    checkArgument(!TextUtils.isEmpty(name), "Directory name cannot be empty or null");
    return context.getDir(getPrefixName(name), mode);
  }

  /**
   * Open an existing database with a specific name, or create it if the database is not visible in
   * the current {@link DelegatingContext}. The database is renamed using the prefix.
   */
  @Override
  public SQLiteDatabase openOrCreateDatabase(
      @NonNull String name, int mode, CursorFactory factory) {
    checkArgument(!TextUtils.isEmpty(name), "Database name cannot be empty or null");
    if (!databases.contains(name)) {
      addDatabase(name);
      String prefixName = getPrefixName(name);
      if (context.getDatabasePath(prefixName).exists() && !context.deleteDatabase(prefixName)) {
        Log.w(
            TAG,
            "Database with prefixed name " + prefixName + " already exists but failed to delete.");
      }
    }
    return context.openOrCreateDatabase(getPrefixName(name), mode, factory);
  }

  /**
   * Similar to {@link #openOrCreateDatabase(String, int, CursorFactory)}, but also allows to pass
   * in a {@code DatabaseErrorHandler}.
   */
  @Override
  public SQLiteDatabase openOrCreateDatabase(
      @NonNull String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
    checkArgument(!TextUtils.isEmpty(name), "Database name cannot be empty or null");
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
      String prefixName = getPrefixName(name);
      if (!databases.contains(name)) {
        addDatabase(name);
        if (context.getDatabasePath(prefixName).exists() && !context.deleteDatabase(prefixName)) {
          Log.w(
              TAG,
              "Database with prefixed name "
                  + prefixName
                  + " already exists and cannot be deleted.");
        }
      }
      return context.openOrCreateDatabase(prefixName, mode, factory, errorHandler);
    }
    throw new UnsupportedOperationException(
        "For API level < 11, use openOrCreateDatabase(String, int, CursorFactory) instead");
  }

  /**
   * @return The list of database names (without prefix) visible to this {@link DelegatingContext}.
   */
  @Override
  public String[] databaseList() {
    return databases.toArray(new String[databases.size()]);
  }

  /**
   * Delete a database from this {@link DelegatingContext}.
   *
   * @param name The name (without prefix) of database to delete.
   * @return {@code true} if database was visible and successfully deleted, and {@code false}
   *     otherwise.
   */
  @Override
  public boolean deleteDatabase(@NonNull String name) {
    checkArgument(!TextUtils.isEmpty(name), "Database name cannot be empty or null");
    if (databases.contains(name)) {
      if (context.deleteDatabase(getPrefixName(name))) {
        databases.remove(name);
        return true;
      }
    }
    return false;
  }

  @Override
  public File getDatabasePath(@NonNull String name) {
    checkArgument(!TextUtils.isEmpty(name), "Database name cannot be empty or null");
    return context.getDatabasePath(getPrefixName(name));
  }

  /**
   * Create {@link FileInputStream} from a file name visible in current {@link DelegatingContext}.
   *
   * @param name The name of the file (without prefix) to open.
   * @return The created {@link FileInputStream}.
   * @throws FileNotFoundException if the name of file is not visible in current {@link
   *     DelegatingContext}.
   */
  @Override
  public FileInputStream openFileInput(@NonNull String name) throws FileNotFoundException {
    checkArgument(!TextUtils.isEmpty(name), "File name cannot be empty or null");
    if (!files.contains(name)) {
      throw new FileNotFoundException(
          String.format("File %s is not found in current context", name));
    }
    return context.openFileInput(getPrefixName(name));
  }

  /**
   * Create {@link FileOutputStream} from a file name visible in current {@link DelegatingContext}.
   * If the file was not visible, add the file name to the file lists visible in current {@link
   * DelegatingContext}.
   *
   * @param name The name of the file (without prefix) to open.
   * @param mode Same mode as that in {@link Context#openFileOutput}.
   * @return The created {@link FileOutputStream}.
   */
  @Override
  public FileOutputStream openFileOutput(@NonNull String name, int mode)
      throws FileNotFoundException {
    checkArgument(!TextUtils.isEmpty(name), "File name cannot be empty or null");
    FileOutputStream fos = context.openFileOutput(getPrefixName(name), mode);
    if (fos != null) {
      files.add(name);
    }
    return fos;
  }

  @Override
  public String[] fileList() {
    return files.toArray(new String[files.size()]);
  }

  @Override
  public File getFileStreamPath(@NonNull String name) {
    checkArgument(!TextUtils.isEmpty(name), "File name cannot be empty or null");
    return context.getFileStreamPath(getPrefixName(name));
  }

  /**
   * Delete a file from this {@link DelegatingContext}.
   *
   * @param name The name (without prefix) of database to delete.
   * @return {@code true} if file was visible and successfully deleted, and {@code false} otherwise.
   */
  @Override
  public boolean deleteFile(@NonNull String name) {
    checkArgument(!TextUtils.isEmpty(name), "File name cannot be empty or null");
    if (files.contains(name)) {
      if (context.deleteFile(getPrefixName(name))) {
        files.remove(name);
        return true;
      }
    }
    return false;
  }

  /**
   * This method only supports retrieving {@link android.app.AppOpsManager}, which is needed by
   * {@link android.content.ContentProvider#attachInfo}.
   */
  @Override
  public Object getSystemService(@NonNull String name) {
    checkArgument(!TextUtils.isEmpty(name), "name cannot be empty or null");
    // getSystemService(Context.APP_OPS_SERVICE) is only used in ContentProvider#attachInfo for
    // API level >= 19.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
        && Context.APP_OPS_SERVICE.equals(name)) {
      return context.getSystemService(Context.APP_OPS_SERVICE);
    }
    throw new UnsupportedOperationException();
  }

  /**
   * The return value only depends on {@link #revokedPermissions}, and the argument {@code pid} and
   * {@code uid} are ignored.
   */
  @Override
  public int checkPermission(@NonNull String permission, int pid, int uid) {
    checkArgument(!TextUtils.isEmpty(permission), "permission cannot be null or empty");
    if (revokedPermissions.contains(permission)) {
      return PackageManager.PERMISSION_DENIED;
    }
    return PackageManager.PERMISSION_GRANTED;
  }

  @Override
  public int checkCallingPermission(@NonNull String permission) {
    return checkPermission(permission, NO_OP_PID, NO_OP_UID);
  }

  @Override
  public int checkCallingOrSelfPermission(@NonNull String permission) {
    return checkPermission(permission, NO_OP_PID, NO_OP_UID);
  }

  @Override
  public int checkSelfPermission(@NonNull String permission) {
    return checkPermission(permission, NO_OP_PID, NO_OP_UID);
  }

  @Override
  public void enforcePermission(@NonNull String permission, int pid, int uid, String message) {
    if (checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) {
      throw new SecurityException(
          (message != null ? (message + ": ") : "") + "No permission " + permission);
    }
  }

  @Override
  public void enforceCallingPermission(@NonNull String permission, String message) {
    enforcePermission(permission, NO_OP_PID, NO_OP_UID, message);
  }

  @Override
  public void enforceCallingOrSelfPermission(@NonNull String permission, String message) {
    enforcePermission(permission, NO_OP_PID, NO_OP_UID, message);
  }

  @Override
  public int checkUriPermission(@NonNull Uri uri, int pid, int uid, int modeFlags) {
    throw new UnsupportedOperationException();
  }

  @Override
  public int checkCallingUriPermission(@NonNull Uri uri, int modeFlags) {
    throw new UnsupportedOperationException();
  }

  @Override
  public int checkCallingOrSelfUriPermission(@NonNull Uri uri, int modeFlags) {
    throw new UnsupportedOperationException();
  }

  @Override
  public int checkUriPermission(
      @Nullable Uri uri,
      @Nullable String readPermission,
      @Nullable String writePermission,
      int pid,
      int uid,
      int modeFlags) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void enforceUriPermission(
      @NonNull Uri uri, int pid, int uid, int modeFlags, String message) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void enforceCallingUriPermission(Uri uri, int modeFlags, String message) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void enforceCallingOrSelfUriPermission(Uri uri, int modeFlags, String message) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void enforceUriPermission(
      Uri uri,
      String readPermission,
      String writePermission,
      int pid,
      int uid,
      int modeFlags,
      String message) {
    throw new UnsupportedOperationException();
  }

  @Override
  public Context getApplicationContext() {
    return this;
  }

  @Override
  public File getFilesDir() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File getNoBackupFilesDir() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File getExternalFilesDir(String s) {
    throw new UnsupportedOperationException();
  }

  @Override
  public File getObbDir() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File[] getObbDirs() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File getCacheDir() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File getCodeCacheDir() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File getExternalCacheDir() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File[] getExternalCacheDirs() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File[] getExternalMediaDirs() {
    throw new UnsupportedOperationException();
  }

  @Override
  public File[] getExternalFilesDirs(String s) {
    throw new UnsupportedOperationException();
  }

  @Override
  public PackageManager getPackageManager() {
    throw new UnsupportedOperationException();
  }

  @Override
  public Looper getMainLooper() {
    throw new UnsupportedOperationException();
  }

  @Override
  public void setTheme(int resID) {
    throw new UnsupportedOperationException();
  }

  @Override
  public Resources.Theme getTheme() {
    throw new UnsupportedOperationException();
  }

  @Override
  public ClassLoader getClassLoader() {
    throw new UnsupportedOperationException();
  }

  @Override
  public String getPackageName() {
    throw new UnsupportedOperationException();
  }

  @Override
  public ApplicationInfo getApplicationInfo() {
    throw new UnsupportedOperationException();
  }

  @Override
  public String getPackageResourcePath() {
    throw new UnsupportedOperationException();
  }

  @Override
  public String getPackageCodePath() {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendBroadcast(Intent intent, String s) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendBroadcast(Intent intent) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendOrderedBroadcast(
      @NonNull Intent intent,
      String s,
      BroadcastReceiver broadcastReceiver,
      Handler handler,
      int i,
      String s1,
      Bundle bundle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendOrderedBroadcast(Intent intent, String s) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendOrderedBroadcastAsUser(
      Intent intent,
      UserHandle userHandle,
      String s,
      BroadcastReceiver broadcastReceiver,
      Handler handler,
      int i,
      String s1,
      Bundle bundle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendBroadcastAsUser(Intent intent, UserHandle userHandle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String s) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendStickyBroadcast(Intent intent) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendStickyBroadcastAsUser(Intent intent, UserHandle userHandle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendStickyOrderedBroadcast(
      Intent intent,
      BroadcastReceiver broadcastReceiver,
      Handler handler,
      int i,
      String s,
      Bundle bundle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void sendStickyOrderedBroadcastAsUser(
      Intent intent,
      UserHandle userHandle,
      BroadcastReceiver broadcastReceiver,
      Handler handler,
      int i,
      String s,
      Bundle bundle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void removeStickyBroadcast(Intent intent) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void removeStickyBroadcastAsUser(Intent intent, UserHandle userHandle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void grantUriPermission(String toPackage, Uri uri, int modeFlags) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void revokeUriPermission(Uri uri, int modeFlags) {
    throw new UnsupportedOperationException();
  }

  @Override
  public SharedPreferences getSharedPreferences(String name, int mode) {
    throw new UnsupportedOperationException();
  }

  @Override
  public Drawable getWallpaper() {
    throw new UnsupportedOperationException();
  }

  @Override
  public Drawable peekWallpaper() {
    throw new UnsupportedOperationException();
  }

  @Override
  public int getWallpaperDesiredMinimumHeight() {
    throw new UnsupportedOperationException();
  }

  @Override
  public int getWallpaperDesiredMinimumWidth() {
    throw new UnsupportedOperationException();
  }

  @Override
  public void setWallpaper(Bitmap bitmap) throws IOException {
    throw new UnsupportedOperationException();
  }

  @Override
  public void setWallpaper(InputStream inputStream) throws IOException {
    throw new UnsupportedOperationException();
  }

  @Override
  public void clearWallpaper() throws IOException {
    throw new UnsupportedOperationException();
  }

  @Override
  public void startActivity(Intent intent) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void startActivity(Intent intent, Bundle bundle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void startActivities(Intent[] intents) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void startActivities(Intent[] intents, Bundle bundle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void startIntentSender(IntentSender intentSender, Intent intent, int i, int i1, int i2)
      throws SendIntentException {
    throw new UnsupportedOperationException();
  }

  @Override
  public void startIntentSender(
      IntentSender intentSender, Intent intent, int i, int i1, int i2, Bundle bundle)
      throws SendIntentException {
    throw new UnsupportedOperationException();
  }

  @Override
  public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
    throw new UnsupportedOperationException();
  }

  @Override
  public Intent registerReceiver(
      BroadcastReceiver broadcastReceiver, IntentFilter intentFilter, String s, Handler handler) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void unregisterReceiver(BroadcastReceiver receiver) {
    throw new UnsupportedOperationException();
  }

  @Override
  public ComponentName startService(Intent intent) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean stopService(Intent intent) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean bindService(Intent service, @NonNull ServiceConnection conn, int flags) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void unbindService(@NonNull ServiceConnection serviceConnection) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean startInstrumentation(
      @NonNull ComponentName componentName, String s, Bundle bundle) {
    throw new UnsupportedOperationException();
  }

  @Override
  public String getSystemServiceName(Class<?> aClass) {
    throw new UnsupportedOperationException();
  }

  @Override
  public Context createPackageContext(String packageName, int flags)
      throws PackageManager.NameNotFoundException {
    throw new UnsupportedOperationException();
  }

  @Override
  public Context createConfigurationContext(@NonNull Configuration overrideConfiguration) {
    throw new UnsupportedOperationException();
  }

  @Override
  public Context createDisplayContext(@NonNull Display display) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean isRestricted() {
    throw new UnsupportedOperationException();
  }

  boolean addDatabase(@NonNull String name) {
    checkArgument(!TextUtils.isEmpty(name), "Database name cannot be empty or null");
    return databases.add(name);
  }

  void addRevokedPermission(@NonNull String permission) {
    checkArgument(!TextUtils.isEmpty(permission), "permission cannot be null or empty");
    revokedPermissions.add(permission);
  }

  private String getPrefixName(@NonNull String name) {
    checkArgument(!TextUtils.isEmpty(name), "Name cannot be empty or null");
    return prefix + name;
  }
}