package org.robolectric.shadows;

import static android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP;
import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA;
import static android.content.pm.ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING;
import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE;
import static android.content.pm.ApplicationInfo.FLAG_HAS_CODE;
import static android.content.pm.ApplicationInfo.FLAG_KILL_AFTER_RESTORE;
import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
import static android.content.pm.ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_RESTORE_ANY_VERSION;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
import static android.content.pm.ApplicationInfo.FLAG_VM_SAFE_MODE;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.PatternMatcher.PATTERN_LITERAL;
import static android.os.PatternMatcher.PATTERN_PREFIX;
import static android.os.PatternMatcher.PATTERN_SIMPLE_GLOB;
import static java.util.Arrays.asList;

import android.content.IntentFilter.MalformedMimeTypeException;
import android.content.pm.ActivityInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.Activity;
import android.content.pm.PackageParser.ActivityIntentInfo;
import android.content.pm.PackageParser.IntentInfo;
import android.content.pm.PackageParser.Package;
import android.content.pm.PackageParser.Permission;
import android.content.pm.PackageParser.PermissionGroup;
import android.content.pm.PackageParser.Service;
import android.content.pm.PackageParser.ServiceIntentInfo;
import android.content.pm.PathPermission;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Process;
import android.util.Pair;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.manifest.ActivityData;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.manifest.BroadcastReceiverData;
import org.robolectric.manifest.ContentProviderData;
import org.robolectric.manifest.IntentFilterData;
import org.robolectric.manifest.IntentFilterData.DataAuthority;
import org.robolectric.manifest.PackageItemData;
import org.robolectric.manifest.PathPermissionData;
import org.robolectric.manifest.PermissionGroupItemData;
import org.robolectric.manifest.PermissionItemData;
import org.robolectric.manifest.ServiceData;
import org.robolectric.res.AttributeResource;
import org.robolectric.res.ResName;
import org.robolectric.util.ReflectionHelpers;

/** Creates a {@link PackageInfo} from a {@link AndroidManifest} */
@SuppressWarnings("NewApi")
public class LegacyManifestParser {

  private static final List<Pair<String, Integer>> APPLICATION_FLAGS =
      asList(
          Pair.create("android:allowBackup", FLAG_ALLOW_BACKUP),
          Pair.create("android:allowClearUserData", FLAG_ALLOW_CLEAR_USER_DATA),
          Pair.create("android:allowTaskReparenting", FLAG_ALLOW_TASK_REPARENTING),
          Pair.create("android:debuggable", FLAG_DEBUGGABLE),
          Pair.create("android:hasCode", FLAG_HAS_CODE),
          Pair.create("android:killAfterRestore", FLAG_KILL_AFTER_RESTORE),
          Pair.create("android:persistent", FLAG_PERSISTENT),
          Pair.create("android:resizeable", FLAG_RESIZEABLE_FOR_SCREENS),
          Pair.create("android:restoreAnyVersion", FLAG_RESTORE_ANY_VERSION),
          Pair.create("android:largeScreens", FLAG_SUPPORTS_LARGE_SCREENS),
          Pair.create("android:normalScreens", FLAG_SUPPORTS_NORMAL_SCREENS),
          Pair.create("android:anyDensity", FLAG_SUPPORTS_SCREEN_DENSITIES),
          Pair.create("android:smallScreens", FLAG_SUPPORTS_SMALL_SCREENS),
          Pair.create("android:testOnly", FLAG_TEST_ONLY),
          Pair.create("android:vmSafeMode", FLAG_VM_SAFE_MODE));
  private static final List<Pair<String, Integer>> CONFIG_OPTIONS =
      asList(
          Pair.create("mcc", ActivityInfo.CONFIG_MCC),
          Pair.create("mnc", ActivityInfo.CONFIG_MNC),
          Pair.create("locale", ActivityInfo.CONFIG_LOCALE),
          Pair.create("touchscreen", ActivityInfo.CONFIG_TOUCHSCREEN),
          Pair.create("keyboard", ActivityInfo.CONFIG_KEYBOARD),
          Pair.create("keyboardHidden", ActivityInfo.CONFIG_KEYBOARD_HIDDEN),
          Pair.create("navigation", ActivityInfo.CONFIG_NAVIGATION),
          Pair.create("screenLayout", ActivityInfo.CONFIG_SCREEN_LAYOUT),
          Pair.create("fontScale", ActivityInfo.CONFIG_FONT_SCALE),
          Pair.create("uiMode", ActivityInfo.CONFIG_UI_MODE),
          Pair.create("orientation", ActivityInfo.CONFIG_ORIENTATION),
          Pair.create("screenSize", ActivityInfo.CONFIG_SCREEN_SIZE),
          Pair.create("smallestScreenSize", ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE));

  public static Package createPackage(AndroidManifest androidManifest) {

    Package pkg = new Package(androidManifest.getPackageName());

    pkg.mVersionName = androidManifest.getVersionName();
    pkg.mVersionCode = androidManifest.getVersionCode();

    Map<String, PermissionItemData> permissionItemData = androidManifest.getPermissions();
    for (PermissionItemData itemData : permissionItemData.values()) {
      Permission permission = new Permission(pkg, createPermissionInfo(pkg, itemData));
      permission.metaData = permission.info.metaData;
      pkg.permissions.add(permission);
    }

    Map<String, PermissionGroupItemData> permissionGroupItemData =
        androidManifest.getPermissionGroups();
    for (PermissionGroupItemData itemData : permissionGroupItemData.values()) {
      PermissionGroup permissionGroup =
          new PermissionGroup(pkg, createPermissionGroupInfo(pkg, itemData));
      permissionGroup.metaData = permissionGroup.info.metaData;
      pkg.permissionGroups.add(permissionGroup);
    }

    pkg.requestedPermissions.addAll(androidManifest.getUsedPermissions());
    if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.M) {
      List<Boolean> permissionsRequired =
          ReflectionHelpers.getField(pkg, "requestedPermissionsRequired");
      permissionsRequired.addAll(buildBooleanList(pkg.requestedPermissions.size(), true));
    }

    pkg.applicationInfo.flags = decodeFlags(androidManifest.getApplicationAttributes());
    pkg.applicationInfo.targetSdkVersion = androidManifest.getTargetSdkVersion();
    pkg.applicationInfo.packageName = androidManifest.getPackageName();
    pkg.applicationInfo.processName = androidManifest.getProcessName();
    if (!Strings.isNullOrEmpty(androidManifest.getApplicationName())) {
      pkg.applicationInfo.className =
          buildClassName(pkg.applicationInfo.packageName, androidManifest.getApplicationName());
      if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.N_MR1) {
        pkg.applicationInfo.name = pkg.applicationInfo.className;
      }
    }
    pkg.applicationInfo.metaData = metaDataToBundle(androidManifest.getApplicationMetaData());
    pkg.applicationInfo.uid = Process.myUid();
    if (androidManifest.getThemeRef() != null) {
      pkg.applicationInfo.theme =
          RuntimeEnvironment.getAppResourceTable()
              .getResourceId(
                  ResName.qualifyResName(
                      androidManifest.getThemeRef().replace("@", ""), pkg.packageName, "style"));
    }

    int labelRes = 0;
    if (androidManifest.getLabelRef() != null) {
      String fullyQualifiedName =
          ResName.qualifyResName(androidManifest.getLabelRef(), androidManifest.getPackageName());
      Integer id =
          fullyQualifiedName == null
              ? null
              : RuntimeEnvironment.getAppResourceTable()
                  .getResourceId(new ResName(fullyQualifiedName));
      labelRes = id != null ? id : 0;
    }

    pkg.applicationInfo.labelRes = labelRes;
    String labelRef = androidManifest.getLabelRef();
    if (labelRef != null && !labelRef.startsWith("@")) {
      pkg.applicationInfo.nonLocalizedLabel = labelRef;
    }

    Map<String, ActivityData> activityDatas = androidManifest.getActivityDatas();
    for (ActivityData data : activityDatas.values()) {
      ActivityInfo activityInfo = new ActivityInfo();
      activityInfo.name = buildClassName(pkg.packageName, data.getName());
      activityInfo.packageName = pkg.packageName;
      activityInfo.configChanges = getConfigChanges(data);
      activityInfo.parentActivityName = data.getParentActivityName();
      activityInfo.metaData = metaDataToBundle(data.getMetaData().getValueMap());
      activityInfo.applicationInfo = pkg.applicationInfo;
      activityInfo.targetActivity = data.getTargetActivityName();
      activityInfo.exported = data.isExported();
      activityInfo.permission = data.getPermission();
      activityInfo.enabled = data.isEnabled();
      String themeRef;

      // Based on ShadowActivity
      if (data.getThemeRef() != null) {
        themeRef = data.getThemeRef();
      } else {
        themeRef = androidManifest.getThemeRef();
      }
      if (themeRef != null) {
        activityInfo.theme =
            RuntimeEnvironment.getAppResourceTable()
                .getResourceId(
                    ResName.qualifyResName(themeRef.replace("@", ""), pkg.packageName, "style"));
      }

      if (data.getLabel() != null) {
        activityInfo.labelRes =
            RuntimeEnvironment.getAppResourceTable()
                .getResourceId(
                    ResName.qualifyResName(
                        data.getLabel().replace("@", ""), pkg.packageName, "string"));
        if (activityInfo.labelRes == 0) {
          activityInfo.nonLocalizedLabel = data.getLabel();
        }
      }

      Activity activity = createActivity(pkg, activityInfo);
      for (IntentFilterData intentFilterData : data.getIntentFilters()) {
        ActivityIntentInfo outInfo = new ActivityIntentInfo(activity);
        populateIntentInfo(intentFilterData, outInfo);
        activity.intents.add(outInfo);
      }
      pkg.activities.add(activity);
    }

    for (ContentProviderData data : androidManifest.getContentProviders()) {
      ProviderInfo info = new ProviderInfo();
      populateComponentInfo(info, pkg, data);
      info.authority = data.getAuthorities();

      List<PathPermission> permissions = new ArrayList<>();
      for (PathPermissionData permissionData : data.getPathPermissionDatas()) {
        permissions.add(createPathPermission(permissionData));
      }
      info.pathPermissions = permissions.toArray(new PathPermission[permissions.size()]);
      info.readPermission = data.getReadPermission();
      info.writePermission = data.getWritePermission();
      info.grantUriPermissions = data.getGrantUriPermissions();
      info.enabled = data.isEnabled();
      pkg.providers.add(createProvider(pkg, info));
    }

    for (BroadcastReceiverData data : androidManifest.getBroadcastReceivers()) {
      ActivityInfo info = new ActivityInfo();
      populateComponentInfo(info, pkg, data);
      info.permission = data.getPermission();
      info.exported = data.isExported();
      info.enabled = data.isEnabled();
      Activity receiver = createActivity(pkg, info);
      for (IntentFilterData intentFilterData : data.getIntentFilters()) {
        ActivityIntentInfo outInfo = new ActivityIntentInfo(receiver);
        populateIntentInfo(intentFilterData, outInfo);
        receiver.intents.add(outInfo);
      }
      pkg.receivers.add(receiver);
    }

    for (ServiceData data : androidManifest.getServices()) {
      ServiceInfo info = new ServiceInfo();
      populateComponentInfo(info, pkg, data);
      info.permission = data.getPermission();
      info.exported = data.isExported();
      info.enabled = data.isEnabled();

      Service service = createService(pkg, info);
      for (IntentFilterData intentFilterData : data.getIntentFilters()) {
        ServiceIntentInfo outInfo = new ServiceIntentInfo(service);
        populateIntentInfo(intentFilterData, outInfo);
        service.intents.add(outInfo);
      }
      pkg.services.add(service);
    }

    String codePath = RuntimeEnvironment.getTempDirectory()
        .createIfNotExists(pkg.packageName + "-codePath")
        .toAbsolutePath()
        .toString();
    if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) {
      pkg.codePath = codePath;
    } else {
      ReflectionHelpers.setField(Package.class, pkg, "mPath", codePath);
    }

    return pkg;
  }

  private static PathPermission createPathPermission(PathPermissionData data) {
    if (!Strings.isNullOrEmpty(data.pathPattern)) {
      return new PathPermission(
          data.pathPattern, PATTERN_SIMPLE_GLOB, data.readPermission, data.writePermission);
    } else if (!Strings.isNullOrEmpty(data.path)) {
      return new PathPermission(
          data.path, PATTERN_LITERAL, data.readPermission, data.writePermission);
    } else if (!Strings.isNullOrEmpty(data.pathPrefix)) {
      return new PathPermission(
          data.pathPrefix, PATTERN_PREFIX, data.readPermission, data.writePermission);
    } else {
      throw new IllegalStateException("Permission without type");
    }
  }

  private static void populateComponentInfo(
      ComponentInfo outInfo, Package owner, PackageItemData itemData) {
    populatePackageItemInfo(outInfo, owner, itemData);
    outInfo.applicationInfo = owner.applicationInfo;
  }

  private static void populatePackageItemInfo(
      PackageItemInfo outInfo, Package owner, PackageItemData itemData) {
    outInfo.name = buildClassName(owner.packageName, itemData.getName());
    outInfo.packageName = owner.packageName;
    outInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());
  }

  private static List<Boolean> buildBooleanList(int size, boolean defaultVal) {
    Boolean[] barray = new Boolean[size];
    Arrays.fill(barray, defaultVal);
    return Arrays.asList(barray);
  }

  private static PackageParser.Provider createProvider(Package pkg, ProviderInfo info) {
    PackageParser.Provider provider =
        ReflectionHelpers.callConstructor(PackageParser.Provider.class);
    populateComponent(pkg, info, provider);
    return provider;
  }

  private static Activity createActivity(Package pkg, ActivityInfo activityInfo) {
    Activity activity = ReflectionHelpers.callConstructor(Activity.class);
    populateComponent(pkg, activityInfo, activity);
    return activity;
  }

  private static Service createService(Package pkg, ServiceInfo info) {
    PackageParser.Service service = ReflectionHelpers.callConstructor(PackageParser.Service.class);
    populateComponent(pkg, info, service);
    return service;
  }

  private static void populateComponent(
      Package pkg, ComponentInfo info, PackageParser.Component component) {
    ReflectionHelpers.setField(component, "info", info);
    ReflectionHelpers.setField(component, "intents", new ArrayList<>());
    ReflectionHelpers.setField(component, "owner", pkg);
    ReflectionHelpers.setField(component, "className", info.name);
  }

  private static void populateIntentInfo(IntentFilterData intentFilterData, IntentInfo outInfo) {
    for (String action : intentFilterData.getActions()) {
      outInfo.addAction(action);
    }
    for (String category : intentFilterData.getCategories()) {
      outInfo.addCategory(category);
    }
    for (DataAuthority dataAuthority : intentFilterData.getAuthorities()) {
      outInfo.addDataAuthority(dataAuthority.getHost(), dataAuthority.getPort());
    }
    for (String mimeType : intentFilterData.getMimeTypes()) {
      try {
        outInfo.addDataType(mimeType);
      } catch (MalformedMimeTypeException e) {
        throw new RuntimeException(e);
      }
    }
    for (String scheme : intentFilterData.getSchemes()) {
      outInfo.addDataScheme(scheme);
    }
    for (String pathPattern : intentFilterData.getPathPatterns()) {
      outInfo.addDataPath(pathPattern, PATTERN_SIMPLE_GLOB);
    }
    for (String pathPattern : intentFilterData.getPathPrefixes()) {
      outInfo.addDataPath(pathPattern, PATTERN_PREFIX);
    }
    for (String pathPattern : intentFilterData.getPaths()) {
      outInfo.addDataPath(pathPattern, PATTERN_LITERAL);
    }
  }

  private static int getConfigChanges(ActivityData activityData) {
    String s = activityData.getConfigChanges();

    int res = 0;

    // quick sanity check.
    if (s == null || "".equals(s)) {
      return res;
    }

    String[] pieces = s.split("\\|", 0);

    for (String s1 : pieces) {
      s1 = s1.trim();

      for (Pair<String, Integer> pair : CONFIG_OPTIONS) {
        if (s1.equals(pair.first)) {
          res |= pair.second;
          break;
        }
      }
    }

    // Matches platform behavior
    if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.O) {
      res |= ActivityInfo.CONFIG_MNC;
      res |= ActivityInfo.CONFIG_MCC;
    }

    return res;
  }

  private static int decodeFlags(Map<String, String> applicationAttributes) {
    int applicationFlags = 0;
    for (Pair<String, Integer> pair : APPLICATION_FLAGS) {
      if ("true".equals(applicationAttributes.get(pair.first))) {
        applicationFlags |= pair.second;
      }
    }
    return applicationFlags;
  }

  private static PermissionInfo createPermissionInfo(Package owner, PermissionItemData itemData) {
    PermissionInfo permissionInfo = new PermissionInfo();
    populatePackageItemInfo(permissionInfo, owner, itemData);

    permissionInfo.group = itemData.getPermissionGroup();
    permissionInfo.protectionLevel = decodeProtectionLevel(itemData.getProtectionLevel());
    permissionInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());

    String descriptionRef = itemData.getDescription();
    if (descriptionRef != null) {
      ResName descResName =
          AttributeResource.getResourceReference(descriptionRef, owner.packageName, "string");
      permissionInfo.descriptionRes =
          RuntimeEnvironment.getAppResourceTable().getResourceId(descResName);
    }

    String labelRefOrString = itemData.getLabel();
    if (labelRefOrString != null) {
      if (AttributeResource.isResourceReference(labelRefOrString)) {
        ResName labelResName =
            AttributeResource.getResourceReference(labelRefOrString, owner.packageName, "string");
        permissionInfo.labelRes =
            RuntimeEnvironment.getAppResourceTable().getResourceId(labelResName);
      } else {
        permissionInfo.nonLocalizedLabel = labelRefOrString;
      }
    }

    return permissionInfo;
  }

  private static PermissionGroupInfo createPermissionGroupInfo(Package owner,
      PermissionGroupItemData itemData) {
    PermissionGroupInfo permissionGroupInfo = new PermissionGroupInfo();
    populatePackageItemInfo(permissionGroupInfo, owner, itemData);

    permissionGroupInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());

    String descriptionRef = itemData.getDescription();
    if (descriptionRef != null) {
      ResName descResName =
          AttributeResource.getResourceReference(descriptionRef, owner.packageName, "string");
      permissionGroupInfo.descriptionRes =
          RuntimeEnvironment.getAppResourceTable().getResourceId(descResName);
    }

    String labelRefOrString = itemData.getLabel();
    if (labelRefOrString != null) {
      if (AttributeResource.isResourceReference(labelRefOrString)) {
        ResName labelResName =
            AttributeResource.getResourceReference(labelRefOrString, owner.packageName, "string");
        permissionGroupInfo.labelRes =
            RuntimeEnvironment.getAppResourceTable().getResourceId(labelResName);
      } else {
        permissionGroupInfo.nonLocalizedLabel = labelRefOrString;
      }
    }

    return permissionGroupInfo;
  }

  private static int decodeProtectionLevel(String protectionLevel) {
    if (protectionLevel == null) {
      return PermissionInfo.PROTECTION_NORMAL;
    }

    int permissions = PermissionInfo.PROTECTION_NORMAL;
    String[] levels = protectionLevel.split("\\|", 0);

    for (String level : levels) {
      switch (level) {
        case "normal":
          permissions |= PermissionInfo.PROTECTION_NORMAL;
          break;
        case "dangerous":
          permissions |= PermissionInfo.PROTECTION_DANGEROUS;
          break;
        case "signature":
          permissions |= PermissionInfo.PROTECTION_SIGNATURE;
          break;
        case "signatureOrSystem":
          permissions |= PermissionInfo.PROTECTION_SIGNATURE_OR_SYSTEM;
          break;
        case "privileged":
          permissions |= PermissionInfo.PROTECTION_FLAG_PRIVILEGED;
          break;
        case "system":
          permissions |= PermissionInfo.PROTECTION_FLAG_SYSTEM;
          break;
        case "development":
          permissions |= PermissionInfo.PROTECTION_FLAG_DEVELOPMENT;
          break;
        case "appop":
          permissions |= PermissionInfo.PROTECTION_FLAG_APPOP;
          break;
        case "pre23":
          permissions |= PermissionInfo.PROTECTION_FLAG_PRE23;
          break;
        case "installer":
          permissions |= PermissionInfo.PROTECTION_FLAG_INSTALLER;
          break;
        case "verifier":
          permissions |= PermissionInfo.PROTECTION_FLAG_VERIFIER;
          break;
        case "preinstalled":
          permissions |= PermissionInfo.PROTECTION_FLAG_PREINSTALLED;
          break;
        case "setup":
          permissions |= PermissionInfo.PROTECTION_FLAG_SETUP;
          break;
        case "instant":
          permissions |= PermissionInfo.PROTECTION_FLAG_INSTANT;
          break;
        case "runtime":
          permissions |= PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY;
          break;
        case "oem":
          permissions |= PermissionInfo.PROTECTION_FLAG_OEM;
          break;
        case "vendorPrivileged":
          permissions |= PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED;
          break;
        case "textClassifier":
          permissions |= PermissionInfo.PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER;
          break;
        default:
          throw new IllegalArgumentException("unknown protection level " + protectionLevel);
      }
    }
    return permissions;
  }

  /**
   * Goes through the meta data and puts each value in to a bundle as the correct type.
   *
   * <p>Note that this will convert resource identifiers specified via the value attribute as well.
   *
   * @param meta Meta data to put in to a bundle
   * @return bundle containing the meta data
   */
  private static Bundle metaDataToBundle(Map<String, Object> meta) {
    if (meta.size() == 0) {
      return null;
    }

    Bundle bundle = new Bundle();

    for (Map.Entry<String, Object> entry : meta.entrySet()) {
      String key = entry.getKey();
      Object value = entry.getValue();
      if (Boolean.class.isInstance(value)) {
        bundle.putBoolean(key, (Boolean) value);
      } else if (Float.class.isInstance(value)) {
        bundle.putFloat(key, (Float) value);
      } else if (Integer.class.isInstance(value)) {
        bundle.putInt(key, (Integer) value);
      } else {
        bundle.putString(key, value == null ? null : value.toString());
      }
    }
    return bundle;
  }

  private static String buildClassName(String pkg, String cls) {
    if (Strings.isNullOrEmpty(cls)) {
      throw new IllegalArgumentException("Empty class name in package " + pkg);
    }
    char c = cls.charAt(0);
    if (c == '.') {
      return (pkg + cls).intern();
    }
    if (cls.indexOf('.') < 0) {
      StringBuilder b = new StringBuilder(pkg);
      b.append('.');
      b.append(cls);
      return b.toString();
    }
    return cls;
    // TODO: consider reenabling this for stricter platform-complaint checking
    // if (c >= 'a' && c <= 'z') {
    // return cls;
    // }
    // throw new IllegalArgumentException("Bad class name " + cls + " in package " + pkg);
  }
}


package org.robolectric.util;

/**
 * Logger for Robolectric. For now, it simply prints messages to stdout.
 *
 * Logging can be enabled by setting the property: {@code robolectric.logging.enabled = true}.
 */
public class Logger {
  private static final String LOGGING_ENABLED = "robolectric.logging.enabled";

  public static void strict(String message, Throwable e) {
    if (loggingEnabled()) {
      System.out.print("WARNING: ");
      System.out.println(message);
      e.printStackTrace();
    }
  }

  public static void strict(String message, Object... args) {
    if (loggingEnabled()) {
      System.out.print("WARNING: ");
      System.out.println(String.format(message, args));
    }
  }

  /**
   * Log an info message.
   *
   * @param message Message text.
   * @param args    Message arguments.
   */
  public static void info(String message, Object... args) {
    if (loggingEnabled()) {
      System.out.print("INFO: ");
      System.out.println(String.format(message, args));
    }
  }

  /**
   * Log a warning message.
   *
   * @param message Message text.
   * @param args    Message arguments.
   */
  public static void warn(String message, Object... args) {
    if (loggingEnabled()) {
      System.out.print("WARN: ");
      System.out.println(String.format(message, args));
    }
  }

  /**
   * Log an error message.
   *
   * @param message Message text.
   * @param e       The exception.
   */
  public static void error(String message, Throwable e) {
    System.err.print("ERROR: ");
    System.err.println(message);
    e.printStackTrace();
  }

  /**
   * Log an error message.
   *
   * @param message Message text.
   * @param args    Message arguments.
   */
  public static void error(String message, Object... args) {
    System.err.print("ERROR: ");
    System.err.println(String.format(message, args));
  }

  /**
   * Log a debug message.
   *
   * @param message Message text.
   * @param args    Message arguments.
   */
  public static void debug(String message, Object... args) {
    if (loggingEnabled()) {
      System.out.print("DEBUG: ");
      System.out.println(String.format(message, args));
    }
  }

  public static boolean loggingEnabled() {
    return Boolean.getBoolean(LOGGING_ENABLED);
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static org.robolectric.RuntimeEnvironment.castNativePtr;

import android.database.sqlite.SQLiteAbortException;
import android.database.sqlite.SQLiteAccessPermException;
import android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException;
import android.database.sqlite.SQLiteBlobTooBigException;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteCustomFunction;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDatabaseLockedException;
import android.database.sqlite.SQLiteDatatypeMismatchException;
import android.database.sqlite.SQLiteDiskIOException;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteFullException;
import android.database.sqlite.SQLiteMisuseException;
import android.database.sqlite.SQLiteOutOfMemoryException;
import android.database.sqlite.SQLiteReadOnlyDatabaseException;
import android.database.sqlite.SQLiteTableLockedException;
import android.os.OperationCanceledException;
import com.almworks.sqlite4java.SQLiteConnection;
import com.almworks.sqlite4java.SQLiteConstants;
import com.almworks.sqlite4java.SQLiteException;
import com.almworks.sqlite4java.SQLiteStatement;
import com.google.common.util.concurrent.Uninterruptibles;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadows.util.SQLiteLibraryLoader;

@Implements(value = android.database.sqlite.SQLiteConnection.class, isInAndroidSdk = false)
public class ShadowSQLiteConnection {

  private static final String IN_MEMORY_PATH = ":memory:";
  private static final Connections CONNECTIONS = new Connections();
  private static final Pattern COLLATE_LOCALIZED_UNICODE_PATTERN =
      Pattern.compile("\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE);

  // indicates an ignored statement
  private static final int IGNORED_REINDEX_STMT = -2;

  private static AtomicBoolean useInMemoryDatabase = new AtomicBoolean();

  public static void setUseInMemoryDatabase(boolean value) {
    useInMemoryDatabase.set(value);
  }

  @Implementation(maxSdk = O)
  public static Number nativeOpen(String path, int openFlags, String label, boolean enableTrace, boolean enableProfile) {
    SQLiteLibraryLoader.load();
    return castNativePtr(CONNECTIONS.open(path));
  }

  @Implementation(minSdk = O_MR1)
  public static long nativeOpen(String path, int openFlags, String label, boolean enableTrace,
                                boolean enableProfile, int lookasideSlotSize, int lookasideSlotCount) {
    return nativeOpen(path, openFlags, label, enableTrace, enableProfile).longValue();
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativePrepareStatement(int connectionPtr, String sql) {
    return (int) nativePrepareStatement((long) connectionPtr, sql);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static long nativePrepareStatement(long connectionPtr, String sql) {
    final String newSql = convertSQLWithLocalizedUnicodeCollator(sql);
    return CONNECTIONS.prepareStatement(connectionPtr, newSql);
  }

  /**
   * Convert SQL with phrase COLLATE LOCALIZED or COLLATE UNICODE to COLLATE NOCASE.
   */
  static String convertSQLWithLocalizedUnicodeCollator(String sql) {
    Matcher matcher = COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql);
    return matcher.replaceAll(" COLLATE NOCASE");
  }

  @Resetter
  public static void reset() {
    CONNECTIONS.reset();
    useInMemoryDatabase.set(false);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeClose(int connectionPtr) {
    nativeClose((long) connectionPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeClose(long connectionPtr) {
    CONNECTIONS.close(connectionPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeFinalizeStatement(int connectionPtr, int statementPtr) {
    nativeFinalizeStatement((long) connectionPtr, statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeFinalizeStatement(long connectionPtr, long statementPtr) {
    CONNECTIONS.finalizeStmt(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeGetParameterCount(int connectionPtr, int statementPtr) {
    return nativeGetParameterCount((long) connectionPtr, statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeGetParameterCount(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.getParameterCount(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static boolean nativeIsReadOnly(int connectionPtr, int statementPtr) {
    return nativeIsReadOnly((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static boolean nativeIsReadOnly(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.isReadOnly(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static long nativeExecuteForLong(int connectionPtr, int statementPtr) {
    return nativeExecuteForLong((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static long nativeExecuteForLong(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForLong(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeExecute(int connectionPtr, int statementPtr) {
    nativeExecute((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeExecute(final long connectionPtr, final long statementPtr) {
    CONNECTIONS.executeStatement(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static String nativeExecuteForString(int connectionPtr, int statementPtr) {
    return nativeExecuteForString((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static String nativeExecuteForString(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForString(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeGetColumnCount(int connectionPtr, int statementPtr) {
    return nativeGetColumnCount((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeGetColumnCount(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.getColumnCount(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static String nativeGetColumnName(int connectionPtr, int statementPtr, int index) {
    return nativeGetColumnName((long) connectionPtr, (long) statementPtr, index);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static String nativeGetColumnName(final long connectionPtr, final long statementPtr, final int index) {
    return CONNECTIONS.getColumnName(connectionPtr, statementPtr, index);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindNull(int connectionPtr, int statementPtr, int index) {
    nativeBindNull((long) connectionPtr, (long) statementPtr, index);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindNull(final long connectionPtr, final long statementPtr, final int index) {
    CONNECTIONS.bindNull(connectionPtr, statementPtr, index);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindLong(int connectionPtr, int statementPtr, int index, long value) {
    nativeBindLong((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
    CONNECTIONS.bindLong(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindDouble(int connectionPtr, int statementPtr, int index, double value) {
    nativeBindDouble((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
    CONNECTIONS.bindDouble(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindString(int connectionPtr, int statementPtr, int index, String value) {
    nativeBindString((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
    CONNECTIONS.bindString(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeBindBlob(int connectionPtr, int statementPtr, int index, byte[] value) {
    nativeBindBlob((long) connectionPtr, (long) statementPtr, index, value);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeBindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
    CONNECTIONS.bindBlob(connectionPtr, statementPtr, index, value);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeRegisterLocalizedCollators(int connectionPtr, String locale) {
    nativeRegisterLocalizedCollators((long) connectionPtr, locale);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeRegisterLocalizedCollators(long connectionPtr, String locale) {
    // TODO: find a way to create a collator
    // http://www.sqlite.org/c3ref/create_collation.html
    // xerial jdbc driver does not have a Java method for sqlite3_create_collation
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr) {
    return nativeExecuteForChangedRowCount((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeExecuteForChangedRowCount(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForChangedRowCount(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static long nativeExecuteForLastInsertedRowId(int connectionPtr, int statementPtr) {
    return nativeExecuteForLastInsertedRowId((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static long nativeExecuteForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
    return CONNECTIONS.executeForLastInsertedRowId(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static long nativeExecuteForCursorWindow(int connectionPtr, int statementPtr, int windowPtr,
                                                  int startPos, int requiredPos, boolean countAllRows) {
    return nativeExecuteForCursorWindow((long) connectionPtr, (long) statementPtr, (long) windowPtr,
        startPos, requiredPos, countAllRows);
}

  @Implementation(minSdk = LOLLIPOP)
  public static long nativeExecuteForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr,
                                                  final int startPos, final int requiredPos, final boolean countAllRows) {
    return CONNECTIONS.executeForCursorWindow(connectionPtr, statementPtr, windowPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeResetStatementAndClearBindings(int connectionPtr, int statementPtr) {
    nativeResetStatementAndClearBindings((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeResetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
    CONNECTIONS.resetStatementAndClearBindings(connectionPtr, statementPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeCancel(int connectionPtr) {
    nativeCancel((long) connectionPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeCancel(long connectionPtr) {
    CONNECTIONS.cancel(connectionPtr);
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeResetCancel(int connectionPtr, boolean cancelable) {
    nativeResetCancel((long) connectionPtr, cancelable);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeResetCancel(long connectionPtr, boolean cancelable) {
    // handled in com.almworks.sqlite4java.SQLiteConnection#exec
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeRegisterCustomFunction(int connectionPtr, SQLiteCustomFunction function) {
    nativeRegisterCustomFunction((long) connectionPtr, function);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static void nativeRegisterCustomFunction(long connectionPtr, SQLiteCustomFunction function) {
    // not supported
  }

  @Implementation (maxSdk = KITKAT_WATCH)
  public static int nativeExecuteForBlobFileDescriptor(int connectionPtr, int statementPtr) {
    return nativeExecuteForBlobFileDescriptor((long) connectionPtr, (long) statementPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeExecuteForBlobFileDescriptor(long connectionPtr, long statementPtr) {
    // impossible to support without native code?
    return -1;
  }

  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeGetDbLookaside(int connectionPtr) {
    return nativeGetDbLookaside((long) connectionPtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  public static int nativeGetDbLookaside(long connectionPtr) {
    // not supported by sqlite4java
    return 0;
  }
// VisibleForTesting
static class Connections {

  private final Object lock = new Object();
  private final AtomicLong pointerCounter = new AtomicLong(0);
  private final Map<Long, SQLiteStatement> statementsMap = new HashMap<>();
  private final Map<Long, SQLiteConnection> connectionsMap = new HashMap<>();
  private final Map<Long, List<Long>> statementPtrsForConnection = new HashMap<>();

  private ExecutorService dbExecutor = Executors.newSingleThreadExecutor();

  SQLiteConnection getConnection(final long connectionPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = connectionsMap.get(connectionPtr);
      if (connection == null) {
        throw new IllegalStateException("Illegal connection pointer " + connectionPtr
                + ". Current pointers for thread " + Thread.currentThread() + " " + connectionsMap.keySet());
      }
      return connection;
    }
  }

  SQLiteStatement getStatement(final long connectionPtr, final long statementPtr) {
    synchronized (lock) {
      // ensure connection is ok
      getConnection(connectionPtr);

      final SQLiteStatement statement = statementsMap.get(statementPtr);
      if (statement == null) {
        throw new IllegalArgumentException("Invalid prepared statement pointer: " + statementPtr + ". Current pointers: " + statementsMap.keySet());
      }
      if (statement.isDisposed()) {
        throw new IllegalStateException("Statement " + statementPtr + " " + statement + " is disposed");
      }
      return statement;
    }
  }

  long open(final String path) {
    synchronized (lock) {
      final SQLiteConnection dbConnection = execute("open SQLite connection", new Callable<SQLiteConnection>() {
        @Override
        public SQLiteConnection call() throws Exception {
          SQLiteConnection connection = useInMemoryDatabase.get() || IN_MEMORY_PATH.equals(path)
                  ? new SQLiteConnection()
                  : new SQLiteConnection(new File(path));

          connection.open();
          return connection;
        }
      });

      final long connectionPtr = pointerCounter.incrementAndGet();
      connectionsMap.put(connectionPtr, dbConnection);
      statementPtrsForConnection.put(connectionPtr, new ArrayList<>());
      return connectionPtr;
    }
  }

  long prepareStatement(final long connectionPtr, final String sql) {
    // TODO: find a way to create collators
    if ("REINDEX LOCALIZED".equals(sql)) {
      return IGNORED_REINDEX_STMT;
    }

    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
      final SQLiteStatement statement = execute("prepare statement", new Callable<SQLiteStatement>() {
        @Override
        public SQLiteStatement call() throws Exception {
          return connection.prepare(sql);
        }
      });

      final long statementPtr = pointerCounter.incrementAndGet();
      statementsMap.put(statementPtr, statement);
      statementPtrsForConnection.get(connectionPtr).add(statementPtr);
      return statementPtr;
    }
  }

  void close(final long connectionPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
        execute("close connection", new Callable<Void>() {
        @Override
        public Void call() throws Exception {
          connection.dispose();
          return null;
        }
      });
      connectionsMap.remove(connectionPtr);
      statementPtrsForConnection.remove(connectionPtr);
    }
  }

  void reset() {
    ExecutorService oldDbExecutor;
    Collection<SQLiteConnection> openConnections;

    synchronized (lock) {
      oldDbExecutor = dbExecutor;
      openConnections = new ArrayList<>(connectionsMap.values());

      dbExecutor = Executors.newSingleThreadExecutor();
      connectionsMap.clear();
      statementsMap.clear();
      statementPtrsForConnection.clear();
    }

    shutdownDbExecutor(oldDbExecutor, openConnections);
  }

  private static void shutdownDbExecutor(ExecutorService executorService, Collection<SQLiteConnection> connections) {
    for (final SQLiteConnection connection : connections) {
      getFuture("close connection on reset", executorService.submit(new Callable<Void>() {
        @Override
        public Void call() throws Exception {
          connection.dispose();
          return null;
        }
      }));
    }

    executorService.shutdown();
    try {
      executorService.awaitTermination(30, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  void finalizeStmt(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return;
    }

    synchronized (lock) {
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
      statementsMap.remove(statementPtr);

        execute("finalize statement", new Callable<Void>() {
        @Override
        public Void call() throws Exception {
          statement.dispose();
          return null;
        }
      });
    }
  }

  void cancel(final long connectionPtr) {
    synchronized (lock) {
      getConnection(connectionPtr); // check connection

      for (Long statementPtr : statementPtrsForConnection.get(connectionPtr)) {
        final SQLiteStatement statement = statementsMap.get(statementPtr);
        if (statement != null) {
            execute("cancel", new Callable<Void>() {
            @Override
            public Void call() throws Exception {
              statement.cancel();
              return null;
            }
          });
        }
      }
    }
  }

  int getParameterCount(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return 0;
    }

    return executeStatementOperation(connectionPtr, statementPtr, "get parameters count in prepared statement", new StatementOperation<Integer>() {
      @Override
      public Integer call(final SQLiteStatement statement) throws Exception {
        return statement.getBindParameterCount();
      }
    });
  }

  boolean isReadOnly(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return true;
    }

    return executeStatementOperation(connectionPtr, statementPtr, "call isReadOnly", new StatementOperation<Boolean>() {
      @Override
      public Boolean call(final SQLiteStatement statement) throws Exception {
        return statement.isReadOnly();
      }
    });
  }

  long executeForLong(final long connectionPtr, final long statementPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "execute for long", new StatementOperation<Long>() {
      @Override
      public Long call(final SQLiteStatement statement) throws Exception {
        if (!statement.step()) {
          throw new SQLiteException(SQLiteConstants.SQLITE_DONE, "No rows returned from query");
        }
        return statement.columnLong(0);
      }
    });
  }

  void executeStatement(final long connectionPtr, final long statementPtr) {
    if (statementPtr == IGNORED_REINDEX_STMT) {
      return;
    }

    executeStatementOperation(connectionPtr, statementPtr, "execute", new StatementOperation<Void>() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.stepThrough();
        return null;
      }
    });
  }

  String executeForString(final long connectionPtr, final long statementPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "execute for string", new StatementOperation<String>() {
      @Override
      public String call(final SQLiteStatement statement) throws Exception {
        if (!statement.step()) {
          throw new SQLiteException(SQLiteConstants.SQLITE_DONE, "No rows returned from query");
        }
        return statement.columnString(0);
      }
    });
  }

  int getColumnCount(final long connectionPtr, final long statementPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "get columns count", new StatementOperation<Integer>() {
      @Override
      public Integer call(final SQLiteStatement statement) throws Exception {
        return statement.columnCount();
      }
    });
  }

  String getColumnName(final long connectionPtr, final long statementPtr, final int index) {
    return executeStatementOperation(connectionPtr, statementPtr, "get column name at index " + index, new StatementOperation<String>() {
      @Override
      public String call(final SQLiteStatement statement) throws Exception {
        return statement.getColumnName(index);
      }
    });
  }

  void bindNull(final long connectionPtr, final long statementPtr, final int index) {
    executeStatementOperation(connectionPtr, statementPtr, "bind null at index " + index, new StatementOperation<Void>() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bindNull(index);
        return null;
      }
    });
  }

  void bindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind long at index " + index + " with value " + value, new StatementOperation<Void>() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  void bindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind double at index " + index + " with value " + value, new StatementOperation<Void>() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  void bindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind string at index " + index, new StatementOperation<Void>() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  void bindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
    executeStatementOperation(connectionPtr, statementPtr, "bind blob at index " + index, new StatementOperation<Void>() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.bind(index, value);
        return null;
      }
    });
  }

  int executeForChangedRowCount(final long connectionPtr, final long statementPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);

      return execute("execute for changed row count", new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
          statement.stepThrough();
          return connection.getChanges();
        }
      });
    }
  }

  long executeForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
    synchronized (lock) {
      final SQLiteConnection connection = getConnection(connectionPtr);
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);

      return execute("execute for last inserted row ID", new Callable<Long>() {
        @Override
        public Long call() throws Exception {
          statement.stepThrough();
          return connection.getLastInsertId();
        }
      });
    }
  }

  long executeForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr) {
    return executeStatementOperation(connectionPtr, statementPtr, "execute for cursor window", new StatementOperation<Integer>() {
      @Override
      public Integer call(final SQLiteStatement statement) throws Exception {
        return ShadowCursorWindow.setData(windowPtr, statement);
      }
    });
  }

  void resetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
    executeStatementOperation(connectionPtr, statementPtr, "reset statement", new StatementOperation<Void>() {
      @Override
      public Void call(final SQLiteStatement statement) throws Exception {
        statement.reset(true);
        return null;
      }
    });
  }

  interface StatementOperation<T> {
    T call(final SQLiteStatement statement) throws Exception;
  }

  private <T> T executeStatementOperation(final long connectionPtr,
                                          final long statementPtr,
                                          final String comment,
                                          final StatementOperation<T> statementOperation) {
    synchronized (lock) {
      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
      return execute(comment, new Callable<T>() {
        @Override
        public T call() throws Exception {
          return statementOperation.call(statement);
        }
      });
    }
  }

  /**
   * Any Callable passed in to execute must not synchronize on lock, as this will result in a deadlock
   */
  private <T> T execute(final String comment, final Callable<T> work) {
    synchronized (lock) {
      return getFuture(comment, dbExecutor.submit(work));
    }
  }

  private static <T> T getFuture(final String comment, final Future<T> future) {
    try {
      return Uninterruptibles.getUninterruptibly(future);
      // No need to catch cancellationexception - we never cancel these futures
    } catch (ExecutionException e) {
      Throwable t = e.getCause();
      if (t instanceof SQLiteException) {
        final RuntimeException sqlException = getSqliteException("Cannot " + comment, ((SQLiteException) t).getBaseErrorCode());
        sqlException.initCause(e);
        throw sqlException;
      } else {
        throw new RuntimeException(e);
      }
    }
  }

  private static RuntimeException getSqliteException(final String message, final int baseErrorCode) {
    // Mapping is from throw_sqlite3_exception in android_database_SQLiteCommon.cpp
    switch (baseErrorCode) {
      case SQLiteConstants.SQLITE_ABORT: return new SQLiteAbortException(message);
      case SQLiteConstants.SQLITE_PERM: return new SQLiteAccessPermException(message);
      case SQLiteConstants.SQLITE_RANGE: return new SQLiteBindOrColumnIndexOutOfRangeException(message);
      case SQLiteConstants.SQLITE_TOOBIG: return new SQLiteBlobTooBigException(message);
      case SQLiteConstants.SQLITE_CANTOPEN: return new SQLiteCantOpenDatabaseException(message);
      case SQLiteConstants.SQLITE_CONSTRAINT: return new SQLiteConstraintException(message);
      case SQLiteConstants.SQLITE_NOTADB: // fall through
      case SQLiteConstants.SQLITE_CORRUPT: return new SQLiteDatabaseCorruptException(message);
      case SQLiteConstants.SQLITE_BUSY: return new SQLiteDatabaseLockedException(message);
      case SQLiteConstants.SQLITE_MISMATCH: return new SQLiteDatatypeMismatchException(message);
      case SQLiteConstants.SQLITE_IOERR: return new SQLiteDiskIOException(message);
      case SQLiteConstants.SQLITE_DONE: return new SQLiteDoneException(message);
      case SQLiteConstants.SQLITE_FULL: return new SQLiteFullException(message);
      case SQLiteConstants.SQLITE_MISUSE: return new SQLiteMisuseException(message);
      case SQLiteConstants.SQLITE_NOMEM: return new SQLiteOutOfMemoryException(message);
      case SQLiteConstants.SQLITE_READONLY: return new SQLiteReadOnlyDatabaseException(message);
      case SQLiteConstants.SQLITE_LOCKED: return new SQLiteTableLockedException(message);
      case SQLiteConstants.SQLITE_INTERRUPT: return new OperationCanceledException(message);
      default: return new android.database.sqlite.SQLiteException(message
          + ", base error code: " + baseErrorCode);
    }
  }
}
}


package org.robolectric.shadows;

import static org.robolectric.shadows.util.DataSource.toDataSource;

import android.content.Context;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import java.io.FileDescriptor;
import java.util.HashMap;
import java.util.Map;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadows.util.DataSource;

@Implements(MediaMetadataRetriever.class)
public class ShadowMediaMetadataRetriever {
  private DataSource dataSource;
  private static final Map<DataSource, Map<Integer, String>> metadata = new HashMap<>();
  private static final Map<DataSource, Map<Long, Bitmap>> frames = new HashMap<>();
  private static final Map<DataSource, RuntimeException> exceptions = new HashMap<>();

  public void setDataSource(DataSource dataSource) {
    RuntimeException e = exceptions.get(dataSource);
    if (e != null) {
      e.fillInStackTrace();
      throw e;
    }
    this.dataSource = dataSource;
  }

  @Implementation
  protected void setDataSource(String path) {
    setDataSource(toDataSource(path));
  }

  @Implementation
  protected void setDataSource(Context context, Uri uri) {
    setDataSource(toDataSource(context, uri));
  }

  @Implementation
  protected void setDataSource(String uri, Map<String, String> headers) {
    setDataSource(toDataSource(uri, headers));
  }

  @Implementation
  protected void setDataSource(FileDescriptor fd, long offset, long length) {
    setDataSource(toDataSource(fd, offset, length));
  }

  @Implementation
  protected String extractMetadata(int keyCode) {
    if (metadata.containsKey(dataSource)) {
      return metadata.get(dataSource).get(keyCode);
    }
    return null;
  }

  @Implementation
  protected Bitmap getFrameAtTime(long timeUs, int option) {
    return (frames.containsKey(dataSource) ?
            frames.get(dataSource).get(timeUs) : null);
  }

  /**
   * Configures an exception to be thrown when {@link #setDataSource}
   * is called for the given data source.
   *
   * @param ds the data source that will trigger an exception
   * @param e the exception to trigger, or <tt>null</tt> to
   * avoid throwing an exception.
   */
  public static void addException(DataSource ds, RuntimeException e) {
    exceptions.put(ds, e);
  }

  public static void addMetadata(DataSource ds, int keyCode, String value) {
    if (!metadata.containsKey(ds)) {
      metadata.put(ds, new HashMap<Integer, String>());
    }
    metadata.get(ds).put(keyCode, value);
  }

  /**
   * Adds the given keyCode/value pair for the given data source.
   * Uses <tt>path</tt> to call {@link org.robolectric.shadows.util.DataSource#toDataSource(String)} and
   * then calls {@link #addMetadata(DataSource, int, String)}. This
   * method is retained mostly for backwards compatibility;
   * you can call {@link #addMetadata(DataSource, int, String)} directly.
   *
   * @param path the path to the data source whose metadata is being set.
   * @param keyCode the keyCode for the metadata being set, as used by {@link MediaMetadataRetriever#extractMetadata(int)}.
   * @param value the value for the specified metadata.
   */
  public static void addMetadata(String path, int keyCode, String value) {
    addMetadata(toDataSource(path), keyCode, value);
  }

  public static void addFrame(DataSource ds, long time, Bitmap bitmap) {
    if (!frames.containsKey(ds)) {
      frames.put(ds, new HashMap<Long, Bitmap>());
    }
    frames.get(ds).put(time, bitmap);
  }

  /**
   * Adds the given bitmap at the given time for the given data source.
   * Uses <tt>path</tt> to call {@link org.robolectric.shadows.util.DataSource#toDataSource(String)} and
   * then calls {@link #addFrame(DataSource, long, Bitmap)}. This
   * method is retained mostly for backwards compatibility;
   * you can call {@link #addFrame(DataSource, long, Bitmap)} directly.
   *
   * @param path the path to the data source.
   * @param time the playback time at which the specified bitmap
   * should be retrieved.
   * @param bitmap the bitmap to retrieve.
   */
  public static void addFrame(String path, long time, Bitmap bitmap) {
    addFrame(toDataSource(path), time, bitmap);
  }

  /**
   * Adds the given bitmap at the given time for the given data source.
   * Uses <tt>path</tt> to call {@link org.robolectric.shadows.util.DataSource#toDataSource(Context, Uri)} and
   * then calls {@link #addFrame(DataSource, long, Bitmap)}. This
   * method is retained mostly for backwards compatibility;
   * you can call {@link #addFrame(DataSource, long, Bitmap)} directly.
   *
   * @param context the Context object to match on the data source.
   * @param uri the Uri of the data source.
   * @param time the playback time at which the specified bitmap
   * should be retrieved.
   * @param bitmap the bitmap to retrieve.
   */
  public static void addFrame(Context context, Uri uri, long time, Bitmap bitmap) {
    addFrame(toDataSource(context, uri), time, bitmap);
  }

  /**
   * Adds the given bitmap at the given time for the given data source.
   * Uses <tt>path</tt> to call {@link org.robolectric.shadows.util.DataSource#toDataSource(String, Map)} and
   * then calls {@link #addFrame(DataSource, long, Bitmap)}. This
   * method is retained mostly for backwards compatibility;
   * you can call {@link #addFrame(DataSource, long, Bitmap)} directly.
   *
   * @param uri the Uri of the data source.
   * @param headers the headers to use when requesting the specified uri.
   * @param time the playback time at which the specified bitmap
   * should be retrieved.
   * @param bitmap the bitmap to retrieve.
   */
  public static void addFrame(String uri, Map<String, String> headers, long time, Bitmap bitmap) {
    addFrame(toDataSource(uri, headers), time, bitmap);
  }

  /**
   * Adds the given bitmap at the given time for the given data source.
   * Uses <tt>path</tt> to call {@link org.robolectric.shadows.util.DataSource#toDataSource(FileDescriptor)} and
   * then calls {@link #addFrame(DataSource, long, Bitmap)}. This
   * method is retained mostly for backwards compatibility;
   * you can call {@link #addFrame(DataSource, long, Bitmap)} directly.
   *
   * @param fd file descriptor of the data source.
   * @param time the playback time at which the specified bitmap
   * should be retrieved.
   * @param bitmap the bitmap to retrieve.
   */
  public static void addFrame(FileDescriptor fd, long time, Bitmap bitmap) {
    addFrame(toDataSource(fd), time, bitmap);
  }

  /**
   * Adds the given bitmap at the given time for the given data source.
   * Uses <tt>path</tt> to call {@link org.robolectric.shadows.util.DataSource#toDataSource(FileDescriptor, long, long)} and
   * then calls {@link #addFrame(DataSource, long, Bitmap)}. This
   * method is retained mostly for backwards compatibility;
   * you can call {@link #addFrame(DataSource, long, Bitmap)} directly.
   *
   * @param fd file descriptor of the data source.
   * @param offset the byte offset within the specified file from which to start reading the data.
   * @param length the number of bytes to read from the file.
   * @param time the playback time at which the specified bitmap
   * should be retrieved.
   * @param bitmap the bitmap to retrieve.
   */
  public static void addFrame(FileDescriptor fd, long offset, long length,
                              long time, Bitmap bitmap) {
    addFrame(toDataSource(fd, offset, length), time, bitmap);
  }

  @Resetter
  public static void reset() {
    metadata.clear();
    frames.clear();
    exceptions.clear();
  }
}