package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.P;
import static org.robolectric.RuntimeEnvironment.castNativePtr;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.SuppressLint;
import android.content.res.ApkAssets;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.os.Build.VERSION_CODES;
import android.os.ParcelFileDescriptor;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import com.google.common.collect.Ordering;
import dalvik.system.VMRuntime;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nonnull;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.XmlResourceParserImpl;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.res.AttrData;
import org.robolectric.res.AttributeResource;
import org.robolectric.res.EmptyStyle;
import org.robolectric.res.FileTypedResource;
import org.robolectric.res.Fs;
import org.robolectric.res.ResName;
import org.robolectric.res.ResType;
import org.robolectric.res.ResourceIds;
import org.robolectric.res.ResourceTable;
import org.robolectric.res.Style;
import org.robolectric.res.StyleData;
import org.robolectric.res.StyleResolver;
import org.robolectric.res.ThemeStyleSet;
import org.robolectric.res.TypedResource;
import org.robolectric.res.android.Asset;
import org.robolectric.res.android.Registries;
import org.robolectric.res.android.ResTable_config;
import org.robolectric.res.builder.XmlBlock;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowAssetManager.Picker;
import org.robolectric.util.Logger;
import org.robolectric.util.TempDirectory;

@SuppressLint("NewApi")
@Implements(value = AssetManager.class, /* this one works for P too... maxSdk = VERSION_CODES.O_MR1,*/
    looseSignatures = true, shadowPicker = Picker.class)
public class ShadowLegacyAssetManager extends ShadowAssetManager {

  public static final Ordering<String> ATTRIBUTE_TYPE_PRECIDENCE =
      Ordering.explicit(
          "reference",
          "color",
          "boolean",
          "integer",
          "fraction",
          "dimension",
          "float",
          "enum",
          "flag",
          "flags",
          "string");

  static boolean strictErrors = false;

  private static final int STYLE_NUM_ENTRIES = 6;
  private static final int STYLE_TYPE = 0;
  private static final int STYLE_DATA = 1;
  private static final int STYLE_ASSET_COOKIE = 2;
  private static final int STYLE_RESOURCE_ID = 3;
  private static final int STYLE_CHANGING_CONFIGURATIONS = 4;
  private static final int STYLE_DENSITY = 5;

  private static long nextInternalThemeId = 1000;
  private static final Map<Long, NativeTheme> nativeThemes = new HashMap<>();

  @RealObject
  protected AssetManager realObject;

  private ResourceTable resourceTable;

  class NativeTheme {
    private ThemeStyleSet themeStyleSet;

    public NativeTheme(ThemeStyleSet themeStyleSet) {
      this.themeStyleSet = themeStyleSet;
    }

    public ShadowLegacyAssetManager getShadowAssetManager() {
      return ShadowLegacyAssetManager.this;
    }
  }

  ResTable_config config = new ResTable_config();
  private final Set<Path> assetDirs = new CopyOnWriteArraySet<>();

  private void convertAndFill(AttributeResource attribute, TypedValue outValue, ResTable_config config, boolean resolveRefs) {
    if (attribute.isNull()) {
      outValue.type = TypedValue.TYPE_NULL;
      outValue.data = TypedValue.DATA_NULL_UNDEFINED;
      return;
    } else if (attribute.isEmpty()) {
      outValue.type = TypedValue.TYPE_NULL;
      outValue.data = TypedValue.DATA_NULL_EMPTY;
      return;
    }

    // short-circuit Android caching of loaded resources cuz our string positions don't remain stable...
    outValue.assetCookie = Converter.getNextStringCookie();
    outValue.changingConfigurations = 0;

    // TODO: Handle resource and style references
    if (attribute.isStyleReference()) {
      return;
    }

    while (attribute.isResourceReference()) {
      Integer resourceId;
      ResName resName = attribute.getResourceReference();
      if (attribute.getReferenceResId() != null) {
        resourceId = attribute.getReferenceResId();
      } else {
        resourceId = resourceTable.getResourceId(resName);
      }

      if (resourceId == null) {
        throw new Resources.NotFoundException("unknown resource " + resName);
      }
      outValue.type = TypedValue.TYPE_REFERENCE;
      if (!resolveRefs) {
        // Just return the resourceId if resolveRefs is false.
        outValue.data = resourceId;
        return;
      }

      outValue.resourceId = resourceId;

      TypedResource dereferencedRef = resourceTable.getValue(resName, config);
      if (dereferencedRef == null) {
        Logger.strict("couldn't resolve %s from %s", resName.getFullyQualifiedName(), attribute);
        return;
      } else {
        if (dereferencedRef.isFile()) {
          outValue.type = TypedValue.TYPE_STRING;
          outValue.data = 0;
          outValue.assetCookie = Converter.getNextStringCookie();
          outValue.string = dereferencedRef.asString();
          return;
        } else if (dereferencedRef.getData() instanceof String) {
          attribute = new AttributeResource(attribute.resName, dereferencedRef.asString(), resName.packageName);
          if (attribute.isResourceReference()) {
            continue;
          }
          if (resolveRefs) {
            Converter.getConverter(dereferencedRef.getResType()).fillTypedValue(attribute.value, outValue);
            return;
          }
        }
      }
      break;
    }

    if (attribute.isNull()) {
      outValue.type = TypedValue.TYPE_NULL;
      return;
    }

    TypedResource attrTypeData = getAttrTypeData(attribute.resName);
    if (attrTypeData != null) {
      AttrData attrData = (AttrData) attrTypeData.getData();
      String format = attrData.getFormat();
      String[] types = format.split("\\|");
      Arrays.sort(types, ATTRIBUTE_TYPE_PRECIDENCE);
      for (String type : types) {
        if ("reference".equals(type)) continue; // already handled above
        Converter converter = Converter.getConverterFor(attrData, type);

        if (converter != null) {
          if (converter.fillTypedValue(attribute.value, outValue)) {
            return;
          }
        }
      }
    } else {
      /**
       * In cases where the runtime framework doesn't know this attribute, e.g: viewportHeight (added in 21) on a
       * KitKat runtine, then infer the attribute type from the value.
       *
       * TODO: When we are able to pass the SDK resources from the build environment then we can remove this
       * and replace the NullResourceLoader with simple ResourceProvider that only parses attribute type information.
       */
      ResType resType = ResType.inferFromValue(attribute.value);
      Converter.getConverter(resType).fillTypedValue(attribute.value, outValue);
    }
  }

  public TypedResource getAttrTypeData(ResName resName) {
    return resourceTable.getValue(resName, config);
  }

  @Implementation
  protected void __constructor__() {
    resourceTable = RuntimeEnvironment.getAppResourceTable();

    if (RuntimeEnvironment.getApiLevel() >= P) {
      invokeConstructor(AssetManager.class, realObject);
    }

  }

  @Implementation
  protected void __constructor__(boolean isSystem) {
    resourceTable = isSystem ? RuntimeEnvironment.getSystemResourceTable() : RuntimeEnvironment.getAppResourceTable();

    if (RuntimeEnvironment.getApiLevel() >= P) {
      invokeConstructor(AssetManager.class, realObject, from(boolean.class, isSystem));
    }

  }

  @Implementation(minSdk = P)
  protected static long nativeCreate() {
    // Return a fake pointer, must not be 0.
    return 1;
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected void init() {
    // no op
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected void init(boolean isSystem) {
    // no op
  }

  protected ResourceTable getResourceTable() {
    return resourceTable;
  }

  @HiddenApi @Implementation
  public CharSequence getResourceText(int ident) {
    TypedResource value = getAndResolve(ident, config, true);
    if (value == null) return null;
    return (CharSequence) value.getData();
  }

  @HiddenApi @Implementation
  public CharSequence getResourceBagText(int ident, int bagEntryId) {
    throw new UnsupportedOperationException(); // todo
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected int getStringBlockCount() {
    return 0;
  }

  @HiddenApi @Implementation
  public String[] getResourceStringArray(final int id) {
    CharSequence[] resourceTextArray = getResourceTextArray(id);
    if (resourceTextArray == null) return null;
    String[] strings = new String[resourceTextArray.length];
    for (int i = 0; i < strings.length; i++) {
      strings[i] = resourceTextArray[i].toString();
    }
    return strings;
  }

  @HiddenApi @Implementation
  public int getResourceIdentifier(String name, String defType, String defPackage) {
    Integer resourceId = resourceTable.getResourceId(ResName.qualifyResName(name, defPackage, defType));
    return resourceId == null ? 0 : resourceId;
  }

  @HiddenApi @Implementation
  public boolean getResourceValue(int ident, int density, TypedValue outValue, boolean resolveRefs) {
    TypedResource value = getAndResolve(ident, config, resolveRefs);
    if (value == null) return false;

    getConverter(value).fillTypedValue(value.getData(), outValue);
    return true;
  }

  private Converter getConverter(TypedResource value) {
    if (value instanceof FileTypedResource.Image
        || (value instanceof FileTypedResource
            && ((FileTypedResource) value).getPath().getFileName().toString().endsWith(".xml"))) {
      return new Converter.FromFilePath();
    }
    return Converter.getConverter(value.getResType());
  }

  @HiddenApi @Implementation
  public CharSequence[] getResourceTextArray(int resId) {
    TypedResource value = getAndResolve(resId, config, true);
    if (value == null) return null;
    List<TypedResource> items = getConverter(value).getItems(value);
    CharSequence[] charSequences = new CharSequence[items.size()];
    for (int i = 0; i < items.size(); i++) {
      TypedResource typedResource = resolve(items.get(i), config, resId);
      charSequences[i] = getConverter(typedResource).asCharSequence(typedResource);
    }
    return charSequences;
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  public boolean getThemeValue(int themePtr, int ident, TypedValue outValue, boolean resolveRefs) {
    return getThemeValue((long) themePtr, ident, outValue, resolveRefs);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP)
  public boolean getThemeValue(long themePtr, int ident, TypedValue outValue, boolean resolveRefs) {
    ResName resName = resourceTable.getResName(ident);

    ThemeStyleSet themeStyleSet = getNativeTheme(themePtr).themeStyleSet;
    AttributeResource attrValue = themeStyleSet.getAttrValue(resName);
    while(attrValue != null && attrValue.isStyleReference()) {
      ResName attrResName = attrValue.getStyleReference();
      if (attrValue.resName.equals(attrResName)) {
        Logger.info("huh... circular reference for %s?", attrResName.getFullyQualifiedName());
        return false;
      }
      attrValue = themeStyleSet.getAttrValue(attrResName);
    }
    if (attrValue != null) {
      convertAndFill(attrValue, outValue, config, resolveRefs);
      return true;
    }
    return false;
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected Object ensureStringBlocks() {
    return null;
  }

  @Implementation
  protected final InputStream open(String fileName) throws IOException {
    return Fs.getInputStream(findAssetFile(fileName));
  }

  @Implementation
  protected final InputStream open(String fileName, int accessMode) throws IOException {
    return Fs.getInputStream(findAssetFile(fileName));
  }

  @Implementation
  protected final AssetFileDescriptor openFd(String fileName) throws IOException {
    Path path = findAssetFile(fileName);
    if (path.getFileSystem().provider().getScheme().equals("jar")) {
      path = getFileFromZip(path);
    }
    ParcelFileDescriptor parcelFileDescriptor =
        ParcelFileDescriptor.open(path.toFile(), ParcelFileDescriptor.MODE_READ_ONLY);
    return new AssetFileDescriptor(parcelFileDescriptor, 0, Files.size(path));
  }

  private Path findAssetFile(String fileName) throws IOException {
    for (Path assetDir : getAllAssetDirs()) {
      Path assetFile = assetDir.resolve(fileName);
      if (Files.exists(assetFile)) {
        return assetFile;
      }
    }

    throw new FileNotFoundException("Asset file " + fileName + " not found");
  }

  /**
   * Extract an asset from a zipped up assets provided by the build system, this is required because
   * there is no way to get a FileDescriptor from a zip entry. This is a temporary measure for Bazel
   * which can be removed once binary resources are supported.
   */
  private static Path getFileFromZip(Path path) {
    byte[] buffer = new byte[1024];
    try {
      Path outputDir = new TempDirectory("robolectric_assets").create("fromzip");
      try (InputStream zis = Fs.getInputStream(path)) {
        Path fileFromZip = outputDir.resolve(path.getFileName().toString());

        try (OutputStream fos = Files.newOutputStream(fileFromZip)) {
          int len;
          while ((len = zis.read(buffer)) > 0) {
            fos.write(buffer, 0, len);
          }
        }
        return fileFromZip;
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Implementation
  protected final String[] list(String path) throws IOException {
    List<String> assetFiles = new ArrayList<>();

    for (Path assetsDir : getAllAssetDirs()) {
      Path file;
      if (path.isEmpty()) {
        file = assetsDir;
      } else {
        file = assetsDir.resolve(path);
      }

      if (Files.isDirectory(file)) {
        Collections.addAll(assetFiles, Fs.listFileNames(file));
      }
    }
    return assetFiles.toArray(new String[assetFiles.size()]);
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected Number openAsset(String fileName, int mode) throws FileNotFoundException {
    return 0;
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected ParcelFileDescriptor openAssetFd(String fileName, long[] outOffsets) throws IOException {
    return null;
  }

  @HiddenApi @Implementation
  public final InputStream openNonAsset(int cookie, String fileName, int accessMode) throws IOException {
    final ResName resName = qualifyFromNonAssetFileName(fileName);

    final FileTypedResource typedResource =
        (FileTypedResource) resourceTable.getValue(resName, config);

    if (typedResource == null) {
      throw new IOException("Unable to find resource for " + fileName);
    }

    InputStream stream;
    if (accessMode == AssetManager.ACCESS_STREAMING) {
      stream = Fs.getInputStream(typedResource.getPath());
    } else {
      stream = new ByteArrayInputStream(Fs.getBytes(typedResource.getPath()));
    }

    if (RuntimeEnvironment.getApiLevel() >= P) {
      Asset asset = Asset.newFileAsset(typedResource);
      long assetPtr = Registries.NATIVE_ASSET_REGISTRY.register(asset);
      // Camouflage the InputStream as an AssetInputStream so subsequent instanceof checks pass.
      stream = ShadowAssetInputStream.createAssetInputStream(stream, assetPtr, realObject);
    }

    return stream;
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected Number openNonAssetNative(int cookie, String fileName, int accessMode)
      throws FileNotFoundException {
    throw new IllegalStateException();
  }

  private ResName qualifyFromNonAssetFileName(String fileName) {
    // Resources from a jar belong to the "android" namespace, except when they come from "resource_files.zip"
    // when they are application resources produced by Bazel.
    if (fileName.startsWith("jar:") && !fileName.contains("resource_files.zip")) {
      // Must remove "jar:" prefix, or else qualifyFromFilePath fails on Windows
      if (File.separatorChar == '\\') {
        fileName = windowsWorkaround(fileName);
      }
      return ResName.qualifyFromFilePath("android", fileName.replaceFirst("jar:", ""));
    } else {
      return ResName.qualifyFromFilePath(RuntimeEnvironment.application.getPackageName(), fileName);
    }
  }

  private String windowsWorkaround(String fileWithinJar) {
    try {
      String path = new URL(new URL(fileWithinJar).getPath()).getPath();
      int bangI = path.indexOf('!');
      String jarPath = path.substring(1, bangI);
      return URLDecoder.decode(URLDecoder.decode(jarPath, "UTF-8"), "UTF-8")
          + "!" + path.substring(bangI + 1);
    } catch (MalformedURLException | UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

  @HiddenApi @Implementation
  public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException {
    throw new IllegalStateException();
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected ParcelFileDescriptor openNonAssetFdNative(int cookie, String fileName, long[] outOffsets)
      throws IOException {
    throw new IllegalStateException();
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected Number openXmlAssetNative(int cookie, String fileName) throws FileNotFoundException {
    throw new IllegalStateException();
  }

  @Implementation
  protected final XmlResourceParser openXmlResourceParser(int cookie, String fileName)
      throws IOException {
    XmlBlock xmlBlock = XmlBlock.create(Fs.fromUrl(fileName), resourceTable.getPackageName());
    if (xmlBlock == null) {
      throw new Resources.NotFoundException(fileName);
    }
    return getXmlResourceParser(resourceTable, xmlBlock, resourceTable.getPackageName());
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected final long seekAsset(int asset, long offset, int whence) {
    return seekAsset((long) asset, offset, whence);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected long seekAsset(long asset, long offset, int whence) {
    return 0;
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected final long getAssetLength(int asset) {
    return getAssetLength((long) asset);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected long getAssetLength(long asset) {
    return 0;
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected final long getAssetRemainingLength(int asset) {
    return getAssetRemainingLength((long) asset);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected long getAssetRemainingLength(long assetHandle) {
    return 0;
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected final void destroyAsset(int asset) {
    destroyAsset((long) asset);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected void destroyAsset(long asset) {
    // no op
  }

  protected XmlResourceParser loadXmlResourceParser(int resId, String type) throws Resources.NotFoundException {
    ResName resName = getResName(resId);
    ResName resolvedResName = resolveResName(resName, config);
    if (resolvedResName == null) {
      throw new RuntimeException("couldn't resolve " + resName.getFullyQualifiedName());
    }
    resName = resolvedResName;

    XmlBlock block = resourceTable.getXml(resName, config);
    if (block == null) {
      throw new Resources.NotFoundException(resName.getFullyQualifiedName());
    }

    ResourceTable resourceProvider = ResourceIds.isFrameworkResource(resId) ? RuntimeEnvironment.getSystemResourceTable() : RuntimeEnvironment.getCompileTimeResourceTable();

    return getXmlResourceParser(resourceProvider, block, resName.packageName);
  }

  private XmlResourceParser getXmlResourceParser(ResourceTable resourceProvider, XmlBlock block, String packageName) {
    return new XmlResourceParserImpl(
        block.getDocument(),
        block.getPath(),
        block.getPackageName(),
        packageName,
        resourceProvider);
  }

  @HiddenApi @Implementation
  public int addAssetPath(String path) {
    assetDirs.add(Fs.fromUrl(path));
    return 1;
  }

  @HiddenApi @Implementation(minSdk = JELLY_BEAN_MR2, maxSdk = M)
  final protected int addAssetPathNative(String path) {
    return addAssetPathNative(path, false);
  }

  @HiddenApi @Implementation(minSdk = N, maxSdk = O_MR1)
  protected int addAssetPathNative(String path, boolean appAsLib) {
    return 0;
  }

  @HiddenApi @Implementation(minSdk = P)
  public void setApkAssets(Object apkAssetsObject, Object invalidateCachesObject) {
    ApkAssets[] apkAssets = (ApkAssets[]) apkAssetsObject;
    boolean invalidateCaches = (boolean) invalidateCachesObject;

    for (ApkAssets apkAsset : apkAssets) {
      assetDirs.add(Fs.fromUrl(apkAsset.getAssetPath()));
    }
    directlyOn(realObject, AssetManager.class).setApkAssets(apkAssets, invalidateCaches);
  }

  @HiddenApi @Implementation
  public boolean isUpToDate() {
    return true;
  }

  @HiddenApi @Implementation(maxSdk = M)
  public void setLocale(String locale) {
  }

  @Implementation
  protected String[] getLocales() {
    return new String[0]; // todo
  }

  @HiddenApi @Implementation(maxSdk = N_MR1)
  final public void setConfiguration(int mcc, int mnc, String locale,
      int orientation, int touchscreen, int density, int keyboard,
      int keyboardHidden, int navigation, int screenWidth, int screenHeight,
      int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp,
      int screenLayout, int uiMode, int sdkVersion) {
    setConfiguration(mcc, mnc, locale,
        orientation, touchscreen, density, keyboard,
        keyboardHidden, navigation, screenWidth, screenHeight,
        smallestScreenWidthDp, screenWidthDp, screenHeightDp,
        screenLayout, uiMode, 0, sdkVersion);
  }

  @HiddenApi @Implementation(minSdk = VERSION_CODES.O)
  public void setConfiguration(int mcc, int mnc, String locale,
      int orientation, int touchscreen, int density, int keyboard,
      int keyboardHidden, int navigation, int screenWidth, int screenHeight,
      int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp,
      int screenLayout, int uiMode, int colorMode, int majorVersion) {
    // AssetManager* am = assetManagerForJavaObject(env, clazz);

    ResTable_config config = new ResTable_config();

    // Constants duplicated from Java class android.content.res.Configuration.
    final int kScreenLayoutRoundMask = 0x300;
    final int kScreenLayoutRoundShift = 8;

    config.mcc = mcc;
    config.mnc = mnc;
    config.orientation = orientation;
    config.touchscreen = touchscreen;
    config.density = density;
    config.keyboard = keyboard;
    config.inputFlags = keyboardHidden;
    config.navigation = navigation;
    config.screenWidth = screenWidth;
    config.screenHeight = screenHeight;
    config.smallestScreenWidthDp = smallestScreenWidthDp;
    config.screenWidthDp = screenWidthDp;
    config.screenHeightDp = screenHeightDp;
    config.screenLayout = screenLayout;
    config.uiMode = uiMode;
    // config.colorMode = colorMode; // todo
    config.sdkVersion = majorVersion;
    config.minorVersion = 0;

    // In Java, we use a 32bit integer for screenLayout, while we only use an 8bit integer
    // in C++. We must extract the round qualifier out of the Java screenLayout and put it
    // into screenLayout2.
    config.screenLayout2 =
        (byte)((screenLayout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift);

    if (locale != null) {
      config.setBcp47Locale(locale);
    }
    // am->setConfiguration(config, locale8);

    this.config = config;
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  public int[] getArrayIntResource(int resId) {
    TypedResource value = getAndResolve(resId, config, true);
    if (value == null) return null;
    List<TypedResource> items = getConverter(value).getItems(value);
    int[] ints = new int[items.size()];
    for (int i = 0; i < items.size(); i++) {
      TypedResource typedResource = resolve(items.get(i), config, resId);
      ints[i] = getConverter(typedResource).asInt(typedResource);
    }
    return ints;
  }

  @HiddenApi @Implementation(minSdk = P)
  protected int[] getResourceIntArray(int resId) {
    return getArrayIntResource(resId);
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected String[] getArrayStringResource(int arrayResId) {
    return new String[0];
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected int[] getArrayStringInfo(int arrayResId) {
    return new int[0];
  }

  @HiddenApi @Implementation(maxSdk = O_MR1)
  protected Number newTheme() {
    return null;
  }

  protected TypedArray getTypedArrayResource(Resources resources, int resId) {
    TypedResource value = getAndResolve(resId, config, true);
    if (value == null) {
      return null;
    }
    List<TypedResource> items = getConverter(value).getItems(value);
    return getTypedArray(resources, items, resId);
  }

  private TypedArray getTypedArray(Resources resources, List<TypedResource> typedResources, int resId) {
    final CharSequence[] stringData = new CharSequence[typedResources.size()];
    final int totalLen = typedResources.size() * STYLE_NUM_ENTRIES;
    final int[] data = new int[totalLen];

    for (int i = 0; i < typedResources.size(); i++) {
      final int offset = i * STYLE_NUM_ENTRIES;
      TypedResource typedResource = typedResources.get(i);

      // Classify the item.
      int type = getResourceType(typedResource);
      if (type == -1) {
        // This type is unsupported; leave empty.
        continue;
      }

      final TypedValue typedValue = new TypedValue();

      if (type == TypedValue.TYPE_REFERENCE) {
        final String reference = typedResource.asString();
        ResName refResName = AttributeResource.getResourceReference(reference,
            typedResource.getXmlContext().getPackageName(), null);
        typedValue.resourceId = resourceTable.getResourceId(refResName);
        typedValue.data = typedValue.resourceId;
        typedResource = resolve(typedResource, config, typedValue.resourceId);

        if (typedResource != null) {
          // Reclassify to a non-reference type.
          type = getResourceType(typedResource);
          if (type == TypedValue.TYPE_ATTRIBUTE) {
            type = TypedValue.TYPE_REFERENCE;
          } else if (type == -1) {
            // This type is unsupported; leave empty.
            continue;
          }
        }
      }

      if (type == TypedValue.TYPE_ATTRIBUTE) {
        final String reference = typedResource.asString();
        final ResName attrResName = AttributeResource.getStyleReference(reference,
            typedResource.getXmlContext().getPackageName(), "attr");
        typedValue.data = resourceTable.getResourceId(attrResName);
      }

      if (typedResource != null && type != TypedValue.TYPE_NULL && type != TypedValue.TYPE_ATTRIBUTE) {
        getConverter(typedResource).fillTypedValue(typedResource.getData(), typedValue);
      }

      data[offset + STYLE_TYPE] = type;
      data[offset + STYLE_RESOURCE_ID] = typedValue.resourceId;
      data[offset + STYLE_DATA] = typedValue.data;
      data[offset + STYLE_ASSET_COOKIE] = typedValue.assetCookie;
      data[offset + STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations;
      data[offset + STYLE_DENSITY] = typedValue.density;
      stringData[i] = typedResource == null ? null : typedResource.asString();
    }

    int[] indices = new int[typedResources.size() + 1]; /* keep zeroed out */
    return ShadowTypedArray.create(resources, null, data, indices, typedResources.size(), stringData);
  }

  private int getResourceType(TypedResource typedResource) {
    if (typedResource == null) {
      return -1;
    }
    final ResType resType = typedResource.getResType();
    int type;
    if (typedResource.getData() == null || resType == ResType.NULL) {
      type = TypedValue.TYPE_NULL;
    } else if (typedResource.isReference()) {
      type = TypedValue.TYPE_REFERENCE;
    } else if (resType == ResType.STYLE) {
      type = TypedValue.TYPE_ATTRIBUTE;
    } else if (resType == ResType.CHAR_SEQUENCE || resType == ResType.DRAWABLE) {
      type = TypedValue.TYPE_STRING;
    } else if (resType == ResType.INTEGER) {
      type = TypedValue.TYPE_INT_DEC;
    } else if (resType == ResType.FLOAT || resType == ResType.FRACTION) {
      type = TypedValue.TYPE_FLOAT;
    } else if (resType == ResType.BOOLEAN) {
      type = TypedValue.TYPE_INT_BOOLEAN;
    } else if (resType == ResType.DIMEN) {
      type = TypedValue.TYPE_DIMENSION;
    } else if (resType == ResType.COLOR) {
      type = TypedValue.TYPE_INT_COLOR_ARGB8;
    } else if (resType == ResType.TYPED_ARRAY || resType == ResType.CHAR_SEQUENCE_ARRAY) {
      type = TypedValue.TYPE_REFERENCE;
    } else {
      type = -1;
    }
    return type;
  }

  @HiddenApi @Implementation
  public Number createTheme() {
    synchronized (nativeThemes) {
      long nativePtr = nextInternalThemeId++;
      nativeThemes.put(nativePtr, new NativeTheme(new ThemeStyleSet()));
      return castNativePtr(nativePtr);
    }
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected static void dumpTheme(long theme, int priority, String tag, String prefix) {
    throw new UnsupportedOperationException("not yet implemented");
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  public void releaseTheme(int themePtr) {
    // no op
  }

  private static NativeTheme getNativeTheme(long themePtr) {
    NativeTheme nativeTheme;
    synchronized (nativeThemes) {
      nativeTheme = nativeThemes.get(themePtr);
    }
    if (nativeTheme == null) {
      throw new RuntimeException("no theme " + themePtr + " found in AssetManager");
    }
    return nativeTheme;
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP)
  public void releaseTheme(long themePtr) {
    synchronized (nativeThemes) {
      nativeThemes.remove(themePtr);
    }
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected void deleteTheme(int theme) {
    deleteTheme((long) theme);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected void deleteTheme(long theme) {
    // no op
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  public static void applyThemeStyle(int themePtr, int styleRes, boolean force) {
    applyThemeStyle((long) themePtr, styleRes, force);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  public static void applyThemeStyle(long themePtr, int styleRes, boolean force) {
    NativeTheme nativeTheme = getNativeTheme(themePtr);
    Style style = nativeTheme.getShadowAssetManager().resolveStyle(styleRes, null);
    nativeTheme.themeStyleSet.apply(style, force);
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  public static void copyTheme(int destPtr, int sourcePtr) {
    copyTheme((long) destPtr, (long) sourcePtr);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  public static void copyTheme(long destPtr, long sourcePtr) {
    NativeTheme destNativeTheme = getNativeTheme(destPtr);
    NativeTheme sourceNativeTheme = getNativeTheme(sourcePtr);
    destNativeTheme.themeStyleSet = sourceNativeTheme.themeStyleSet.copy();
  }

  @HiddenApi @Implementation(minSdk = P, maxSdk = P)
  protected static void nativeThemeCopy(long destPtr, long sourcePtr) {
    copyTheme(destPtr, sourcePtr);
  }

  @HiddenApi @Implementation(minSdk = VERSION_CODES.Q)
  protected static void nativeThemeCopy(long dstAssetManagerPtr, long dstThemePtr,
      long srcAssetManagerPtr, long srcThemePtr) {
    copyTheme(dstThemePtr, srcThemePtr);
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected static boolean applyStyle(int themeToken, int defStyleAttr, int defStyleRes,
      int xmlParserToken, int[] attrs, int[] outValues, int[] outIndices) {
    return applyStyle((long)themeToken, defStyleAttr, defStyleRes, (long)xmlParserToken, attrs,
        outValues, outIndices);
  }

  @HiddenApi @Implementation(minSdk = O, maxSdk = O_MR1)
  protected static void applyStyle(long themeToken, int defStyleAttr, int defStyleRes,
      long xmlParserToken, int[] inAttrs, int length, long outValuesAddress,
      long outIndicesAddress) {
    ShadowVMRuntime shadowVMRuntime = Shadow.extract(VMRuntime.getRuntime());
    int[] outValues = (int[])shadowVMRuntime.getObjectForAddress(outValuesAddress);
    int[] outIndices = (int[])shadowVMRuntime.getObjectForAddress(outIndicesAddress);
    applyStyle(themeToken, defStyleAttr, defStyleRes, xmlParserToken, inAttrs,
        outValues, outIndices);
  }

  @HiddenApi @Implementation(minSdk = P)
  protected void applyStyleToTheme(long themePtr, int resId, boolean force) {
    applyThemeStyle(themePtr, resId, force);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
  protected static boolean applyStyle(long themeToken, int defStyleAttr, int defStyleRes,
      long xmlParserToken, int[] attrs, int[] outValues, int[] outIndices) {
    // no-op
    return false;
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected static boolean resolveAttrs(long themeToken,
      int defStyleAttr, int defStyleRes, int[] inValues,
      int[] attrs, int[] outValues, int[] outIndices) {
    // no-op
    return false;
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected boolean retrieveAttributes(
      int xmlParserToken, int[] attrs, int[] outValues, int[] outIndices) {
    return retrieveAttributes((long)xmlParserToken, attrs, outValues, outIndices);
  }

  @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected boolean retrieveAttributes(long xmlParserToken, int[] attrs, int[] outValues,
      int[] outIndices) {
    return false;
  }

  @HiddenApi @Implementation(maxSdk = KITKAT_WATCH)
  protected static int loadThemeAttributeValue(int themeHandle, int ident,
      TypedValue outValue, boolean resolve) {
    return loadThemeAttributeValue((long) themeHandle, ident, outValue, resolve);
  }

  @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected static int loadThemeAttributeValue(long themeHandle, int ident,
      TypedValue outValue, boolean resolve) {
    // no-op
    return 0;
  }

  /////////////////////////

  Style resolveStyle(int resId, Style themeStyleSet) {
    return resolveStyle(getResName(resId), themeStyleSet);
  }

  private Style resolveStyle(@Nonnull ResName themeStyleName, Style themeStyleSet) {
    TypedResource themeStyleResource = resourceTable.getValue(themeStyleName, config);
    if (themeStyleResource == null) return null;
    StyleData themeStyleData = (StyleData) themeStyleResource.getData();
    if (themeStyleSet == null) {
      themeStyleSet = new ThemeStyleSet();
    }
    return new StyleResolver(resourceTable, legacyShadowOf(AssetManager.getSystem()).getResourceTable(),
        themeStyleData, themeStyleSet, themeStyleName, config);
  }

  private TypedResource getAndResolve(int resId, ResTable_config config, boolean resolveRefs) {
    TypedResource value = resourceTable.getValue(resId, config);
    if (resolveRefs) {
      value = resolve(value, config, resId);
    }
    return value;
  }

  TypedResource resolve(TypedResource value, ResTable_config config, int resId) {
    return resolveResourceValue(value, config, resId);
  }

  protected ResName resolveResName(ResName resName, ResTable_config config) {
    TypedResource value = resourceTable.getValue(resName, config);
    return resolveResource(value, config, resName);
  }

  // todo: DRY up #resolveResource vs #resolveResourceValue
  private ResName resolveResource(TypedResource value, ResTable_config config, ResName resName) {
    while (value != null && value.isReference()) {
      String s = value.asString();
      if (AttributeResource.isNull(s) || AttributeResource.isEmpty(s)) {
        value = null;
      } else {
        String refStr = s.substring(1).replace("+", "");
        resName = ResName.qualifyResName(refStr, resName);
        value = resourceTable.getValue(resName, config);
      }
    }

    return resName;
  }

  private TypedResource resolveResourceValue(TypedResource value, ResTable_config config, ResName resName) {
    while (value != null && value.isReference()) {
      String s = value.asString();
      if (AttributeResource.isNull(s) || AttributeResource.isEmpty(s)) {
        value = null;
      } else {
        String refStr = s.substring(1).replace("+", "");
        resName = ResName.qualifyResName(refStr, resName);
        value = resourceTable.getValue(resName, config);
      }
    }

    return value;
  }

  protected TypedResource resolveResourceValue(TypedResource value, ResTable_config config, int resId) {
    ResName resName = getResName(resId);
    return resolveResourceValue(value, config, resName);
  }

  private TypedValue buildTypedValue(AttributeSet set, int resId, int defStyleAttr, Style themeStyleSet, int defStyleRes) {
    /*
     * When determining the final value of a particular attribute, there are four inputs that come into play:
     *
     * 1. Any attribute values in the given AttributeSet.
     * 2. The style resource specified in the AttributeSet (named "style").
     * 3. The default style specified by defStyleAttr and defStyleRes
     * 4. The base values in this theme.
     */
    Style defStyleFromAttr = null;
    Style defStyleFromRes = null;
    Style styleAttrStyle = null;

    if (defStyleAttr != 0) {
      // Load the theme attribute for the default style attributes. E.g., attr/buttonStyle
      ResName defStyleName = getResName(defStyleAttr);

      // Load the style for the default style attribute. E.g. "@style/Widget.Robolectric.Button";
      AttributeResource defStyleAttribute = themeStyleSet.getAttrValue(defStyleName);
      if (defStyleAttribute != null) {
        while (defStyleAttribute.isStyleReference()) {
          AttributeResource other = themeStyleSet.getAttrValue(defStyleAttribute.getStyleReference());
          if (other == null) {
            throw new RuntimeException("couldn't dereference " + defStyleAttribute);
          }
          defStyleAttribute = other;
        }

        if (defStyleAttribute.isResourceReference()) {
          ResName defStyleResName = defStyleAttribute.getResourceReference();
          defStyleFromAttr = resolveStyle(defStyleResName, themeStyleSet);
        }
      }
    }

    if (set != null && set.getStyleAttribute() != 0) {
      ResName styleAttributeResName = getResName(set.getStyleAttribute());
      while (styleAttributeResName.type.equals("attr")) {
        AttributeResource attrValue = themeStyleSet.getAttrValue(styleAttributeResName);
        if (attrValue == null) {
          throw new RuntimeException(
              "no value for " + styleAttributeResName.getFullyQualifiedName()
                  + " in " + themeStyleSet);
        }
        if (attrValue.isResourceReference()) {
          styleAttributeResName = attrValue.getResourceReference();
        } else if (attrValue.isStyleReference()) {
          styleAttributeResName = attrValue.getStyleReference();
        }
      }
      styleAttrStyle = resolveStyle(styleAttributeResName, themeStyleSet);
    }

    if (defStyleRes != 0) {
      ResName resName = getResName(defStyleRes);
      if (resName.type.equals("attr")) {
        // todo: this should be a style resId, not an attr
        System.out.println("WARN: " + resName.getFullyQualifiedName() + " should be a style resId");
        // AttributeResource attributeValue = findAttributeValue(defStyleRes, set, styleAttrStyle, defStyleFromAttr, defStyleFromAttr, themeStyleSet);
        // if (attributeValue != null) {
        //   if (attributeValue.isStyleReference()) {
        //     resName = themeStyleSet.getAttrValue(attributeValue.getStyleReference()).getResourceReference();
        //   } else if (attributeValue.isResourceReference()) {
        //     resName = attributeValue.getResourceReference();
        //   }
        // }
      } else if (resName.type.equals("style")) {
        defStyleFromRes = resolveStyle(resName, themeStyleSet);
      }
    }

    AttributeResource attribute = findAttributeValue(resId, set, styleAttrStyle, defStyleFromAttr, defStyleFromRes, themeStyleSet);
    while (attribute != null && attribute.isStyleReference()) {
      ResName otherAttrName = attribute.getStyleReference();
      if (attribute.resName.equals(otherAttrName)) {
        Logger.info("huh... circular reference for %s?", attribute.resName.getFullyQualifiedName());
        return null;
      }
      ResName resName = resourceTable.getResName(resId);

      AttributeResource otherAttr = themeStyleSet.getAttrValue(otherAttrName);
      if (otherAttr == null) {
        strictError("no such attr %s in %s while resolving value for %s", attribute.value, themeStyleSet, resName.getFullyQualifiedName());
        attribute = null;
      } else {
        attribute = new AttributeResource(resName, otherAttr.value, otherAttr.contextPackageName);
      }
    }

    if (attribute == null || attribute.isNull()) {
      return null;
    } else {
      TypedValue typedValue = new TypedValue();
      convertAndFill(attribute, typedValue, config, true);
      return typedValue;
    }
  }

  private void strictError(String message, Object... args) {
    if (strictErrors) {
      throw new RuntimeException(String.format(message, args));
    } else {
      Logger.strict(message, args);
    }
  }

  TypedArray attrsToTypedArray(Resources resources, AttributeSet set, int[] attrs, int defStyleAttr, long nativeTheme, int defStyleRes) {
    CharSequence[] stringData = new CharSequence[attrs.length];
    int[] data = new int[attrs.length * STYLE_NUM_ENTRIES];
    int[] indices = new int[attrs.length + 1];
    int nextIndex = 0;

    Style themeStyleSet = nativeTheme == 0
        ? new EmptyStyle()
        : getNativeTheme(nativeTheme).themeStyleSet;

    for (int i = 0; i < attrs.length; i++) {
      int offset = i * STYLE_NUM_ENTRIES;

      TypedValue typedValue = buildTypedValue(set, attrs[i], defStyleAttr, themeStyleSet, defStyleRes);
      if (typedValue != null) {
        //noinspection PointlessArithmeticExpression
        data[offset + STYLE_TYPE] = typedValue.type;
        data[offset + STYLE_DATA] = typedValue.type == TypedValue.TYPE_STRING ? i : typedValue.data;
        data[offset + STYLE_ASSET_COOKIE] = typedValue.assetCookie;
        data[offset + STYLE_RESOURCE_ID] = typedValue.resourceId;
        data[offset + STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations;
        data[offset + STYLE_DENSITY] = typedValue.density;
        stringData[i] = typedValue.string;

        indices[nextIndex + 1] = i;
        nextIndex++;
      }
    }

    indices[0] = nextIndex;

    TypedArray typedArray = ShadowTypedArray.create(resources, attrs, data, indices, nextIndex, stringData);
    if (set != null) {
      ShadowTypedArray shadowTypedArray = Shadow.extract(typedArray);
      shadowTypedArray.positionDescription = set.getPositionDescription();
    }
    return typedArray;
  }

  private AttributeResource findAttributeValue(int resId, AttributeSet attributeSet, Style styleAttrStyle, Style defStyleFromAttr, Style defStyleFromRes, @Nonnull Style themeStyleSet) {
    if (attributeSet != null) {
      for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
        if (attributeSet.getAttributeNameResource(i) == resId) {
          String attributeValue;
          try {
            attributeValue = attributeSet.getAttributeValue(i);
          } catch (IndexOutOfBoundsException e) {
            // type is TypedValue.TYPE_NULL, ignore...
            continue;
          }
          if (attributeValue != null) {
            String defaultPackageName = ResourceIds.isFrameworkResource(resId) ? "android" : RuntimeEnvironment.application.getPackageName();
            ResName resName = ResName.qualifyResName(attributeSet.getAttributeName(i), defaultPackageName, "attr");
            Integer referenceResId = null;
            if (AttributeResource.isResourceReference(attributeValue)) {
              referenceResId = attributeSet.getAttributeResourceValue(i, -1);
              // binary AttributeSet references have a string value of @resId rather than fully qualified resource name
              if (referenceResId != 0) {
                ResName refResName = resourceTable.getResName(referenceResId);
                if (refResName != null) {
                  attributeValue = "@" + refResName.getFullyQualifiedName();
                }
              }
            }
            return new AttributeResource(resName, attributeValue, "fixme!!!", referenceResId);
          }
        }
      }
    }

    ResName attrName = resourceTable.getResName(resId);
    if (attrName == null) return null;

    if (styleAttrStyle != null) {
      AttributeResource attribute = styleAttrStyle.getAttrValue(attrName);
      if (attribute != null) {
        return attribute;
      }
    }

    // else if attr in defStyleFromAttr, use its value
    if (defStyleFromAttr != null) {
      AttributeResource attribute = defStyleFromAttr.getAttrValue(attrName);
      if (attribute != null) {
        return attribute;
      }
    }

    if (defStyleFromRes != null) {
      AttributeResource attribute = defStyleFromRes.getAttrValue(attrName);
      if (attribute != null) {
        return attribute;
      }
    }

    // else if attr in theme, use its value
    return themeStyleSet.getAttrValue(attrName);
  }

  @Override
  Collection<Path> getAllAssetDirs() {
    return assetDirs;
  }

  @Nonnull private ResName getResName(int id) {
    ResName resName = resourceTable.getResName(id);
    if (resName == null) {
      throw new Resources.NotFoundException("Resource ID #0x" + Integer.toHexString(id));
    }
    return resName;
  }

  @Implementation
  protected String getResourceName(int resid) {
    return getResName(resid).getFullyQualifiedName();
  }

  @Implementation
  protected String getResourcePackageName(int resid) {
    return getResName(resid).packageName;
  }

  @Implementation
  protected String getResourceTypeName(int resid) {
    return getResName(resid).type;
  }

  @Implementation
  protected String getResourceEntryName(int resid) {
    return getResName(resid).name;
  }

  @Implementation(maxSdk = O_MR1)
  protected int getArraySize(int id) {
    return 0;
  }

  @Implementation(maxSdk = O_MR1)
  protected int retrieveArray(int id, int[] outValues) {
    return 0;
  }

  @Implementation(maxSdk = O_MR1)
  protected Number getNativeStringBlock(int block) {
    throw new IllegalStateException();
  }

  @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
  protected final SparseArray<String> getAssignedPackageIdentifiers() {
    return new SparseArray<>();
  }

  @Implementation(maxSdk = O_MR1)
  protected int loadResourceValue(int ident, short density, TypedValue outValue, boolean resolve) {
    return 0;
  }

  @Implementation(maxSdk = O_MR1)
  protected int loadResourceBagValue(int ident, int bagEntryId, TypedValue outValue, boolean resolve) {
    return 0;
  }

  // static void NativeAssetDestroy(JNIEnv* /*env*/, jclass /*clazz*/, jlong asset_ptr) {
  @Implementation(minSdk = P)
  protected static void nativeAssetDestroy(long asset_ptr) {
    ShadowArscAssetManager9.nativeAssetDestroy(asset_ptr);
  }

  // static jint NativeAssetReadChar(JNIEnv* /*env*/, jclass /*clazz*/, jlong asset_ptr) {
  @Implementation(minSdk = P)
  protected static int nativeAssetReadChar(long asset_ptr) {
    return ShadowArscAssetManager9.nativeAssetReadChar(asset_ptr);
  }

  // static jint NativeAssetRead(JNIEnv* env, jclass /*clazz*/, jlong asset_ptr, jbyteArray java_buffer,
//                             jint offset, jint len) {
  @Implementation(minSdk = P)
  protected static int nativeAssetRead(long asset_ptr, byte[] java_buffer, int offset, int len)
      throws IOException {
    return ShadowArscAssetManager9.nativeAssetRead(asset_ptr, java_buffer, offset, len);
  }

  // static jlong NativeAssetSeek(JNIEnv* env, jclass /*clazz*/, jlong asset_ptr, jlong offset,
//                              jint whence) {
  @Implementation(minSdk = P)
  protected static long nativeAssetSeek(long asset_ptr, long offset, int whence) {
    return ShadowArscAssetManager9.nativeAssetSeek(asset_ptr, offset, whence);
  }

  // static jlong NativeAssetGetLength(JNIEnv* /*env*/, jclass /*clazz*/, jlong asset_ptr) {
  @Implementation(minSdk = P)
  protected static long nativeAssetGetLength(long asset_ptr) {
    return ShadowArscAssetManager9.nativeAssetGetLength(asset_ptr);
  }

  // static jlong NativeAssetGetRemainingLength(JNIEnv* /*env*/, jclass /*clazz*/, jlong asset_ptr) {
  @Implementation(minSdk = P)
  protected static long nativeAssetGetRemainingLength(long asset_ptr) {
    return ShadowArscAssetManager9.nativeAssetGetRemainingLength(asset_ptr);
  }

  @Implementation(minSdk = VERSION_CODES.Q)
  protected static String[] nativeCreateIdmapsForStaticOverlaysTargetingAndroid() {
    return new String[0];
  }

  @Resetter
  public static void reset() {
    // todo: ShadowPicker doesn't discriminate properly between concrete shadow classes for resetters...
    if (useLegacy()) {
      if (RuntimeEnvironment.getApiLevel() >= P) {
        _AssetManager28_ _assetManagerStatic_ = reflector(_AssetManager28_.class);
        _assetManagerStatic_.setSystemApkAssetsSet(null);
        _assetManagerStatic_.setSystemApkAssets(null);
      }
      reflector(_AssetManager_.class).setSystem(null);
    }
  }

}


package org.robolectric.shadows.httpclient;

import java.util.List;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;

/**
 * Collection of static methods used interact with HTTP requests / responses.
 */
public class FakeHttp {
  private static FakeHttpLayer instance = new FakeHttpLayer();

  /**
   * Sets up an HTTP response to be returned by calls to Apache's {@code HttpClient} implementers.
   *
   * @param statusCode   the status code of the response
   * @param responseBody the body of the response
   * @param headers      optional headers for the request
   */
  public static void addPendingHttpResponse(int statusCode, String responseBody, Header... headers) {
    getFakeHttpLayer().addPendingHttpResponse(statusCode, responseBody, headers);
  }

  /**
   * Sets up an HTTP response to be returned by calls to Apache's {@code HttpClient} implementers.
   *
   * @param statusCode   the status code of the response
   * @param responseBody the body of the response
   * @param contentType  the contentType of the response
   * @deprecated use {@link #addPendingHttpResponse(int, String, org.apache.http.Header...)} instead
   */
  @Deprecated
  public static void addPendingHttpResponseWithContentType(int statusCode, String responseBody, Header contentType) {
    getFakeHttpLayer().addPendingHttpResponse(statusCode, responseBody, contentType);
  }

  /**
   * Sets up an HTTP response to be returned by calls to Apache's {@code HttpClient} implementers.
   *
   * @param httpResponse the response
   */
  public static void addPendingHttpResponse(HttpResponse httpResponse) {
    getFakeHttpLayer().addPendingHttpResponse(httpResponse);
  }

  /**
   * Sets up an HTTP response to be returned by calls to Apache's {@code HttpClient} implementers.
   *
   * @param httpResponseGenerator an HttpResponseGenerator that will provide responses
   */
  public static void addPendingHttpResponse(HttpResponseGenerator httpResponseGenerator) {
    getFakeHttpLayer().addPendingHttpResponse(httpResponseGenerator);
  }

  /**
   * Accessor to obtain HTTP requests made during the current test in the order in which they were made.
   *
   * @param index index of the request to retrieve.
   * @return the requested request.
   */
  public static HttpRequest getSentHttpRequest(int index) {
    return getFakeHttpLayer().getSentHttpRequestInfo(index).getHttpRequest();
  }

  public static HttpRequest getLatestSentHttpRequest() {
    return ShadowDefaultRequestDirector.getLatestSentHttpRequest();
  }

  /**
   * Accessor to find out if HTTP requests were made during the current test.
   *
   * @return whether a request was made.
   */
  public static boolean httpRequestWasMade() {
    return getFakeHttpLayer().hasRequestInfos();
  }

  public static boolean httpRequestWasMade(String uri) {
    return getFakeHttpLayer().hasRequestMatchingRule(
        new FakeHttpLayer.UriRequestMatcher(uri));
  }

  /**
   * Accessor to obtain metadata for an HTTP request made during the current test in the order in which they were made.
   *
   * @param index index of the request to retrieve.
   * @return the requested request metadata.
   */
  public static HttpRequestInfo getSentHttpRequestInfo(int index) {
    return getFakeHttpLayer().getSentHttpRequestInfo(index);
  }

  /**
   * Accessor to obtain HTTP requests made during the current test in the order in which they were made.
   *
   * @return the requested request or null if there are none.
   */
  public static HttpRequest getNextSentHttpRequest() {
    HttpRequestInfo httpRequestInfo = getFakeHttpLayer().getNextSentHttpRequestInfo();
    return httpRequestInfo == null ? null : httpRequestInfo.getHttpRequest();
  }

  /**
   * Accessor to obtain metadata for an HTTP request made during the current test in the order in which they were made.
   *
   * @return the requested request metadata or null if there are none.
   */
  public static HttpRequestInfo getNextSentHttpRequestInfo() {
    return getFakeHttpLayer().getNextSentHttpRequestInfo();
  }

  /**
   * Adds an HTTP response rule. The response will be returned when the rule is matched.
   *
   * @param method   method to match.
   * @param uri      uri to match.
   * @param response response to return when a match is found.
   */
  public static void addHttpResponseRule(String method, String uri, HttpResponse response) {
    getFakeHttpLayer().addHttpResponseRule(method, uri, response);
  }

  /**
   * Adds an HTTP response rule with a default method of GET. The response will be returned when the rule is matched.
   *
   * @param uri      uri to match.
   * @param response response to return when a match is found.
   */
  public static void addHttpResponseRule(String uri, HttpResponse response) {
    getFakeHttpLayer().addHttpResponseRule(uri, response);
  }

  /**
   * Adds an HTTP response rule. The response will be returned when the rule is matched.
   *
   * @param uri      uri to match.
   * @param response response to return when a match is found.
   */
  public static void addHttpResponseRule(String uri, String response) {
    getFakeHttpLayer().addHttpResponseRule(uri, response);
  }

  /**
   * Adds an HTTP response rule. The response will be returned when the rule is matched.
   *
   * @param requestMatcher custom {@code RequestMatcher}.
   * @param response       response to return when a match is found.
   */
  public static void addHttpResponseRule(RequestMatcher requestMatcher, HttpResponse response) {
    getFakeHttpLayer().addHttpResponseRule(requestMatcher, response);
  }

  /**
   * Adds an HTTP response rule. For each time the rule is matched, responses will be shifted
   * off the list and returned. When all responses have been given and the rule is matched again,
   * an exception will be thrown.
   *
   * @param requestMatcher custom {@code RequestMatcher}.
   * @param responses      responses to return in order when a match is found.
   */
  public static void addHttpResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses) {
    getFakeHttpLayer().addHttpResponseRule(requestMatcher, responses);
  }

  public static FakeHttpLayer getFakeHttpLayer() {
    return instance;
  }

  public static void setDefaultHttpResponse(int statusCode, String responseBody) {
    getFakeHttpLayer().setDefaultHttpResponse(statusCode, responseBody);
  }

  public static void setDefaultHttpResponse(HttpResponse defaultHttpResponse) {
    getFakeHttpLayer().setDefaultHttpResponse(defaultHttpResponse);
  }

  public static void clearHttpResponseRules() {
    getFakeHttpLayer().clearHttpResponseRules();
  }

  public static void clearPendingHttpResponses() {
    getFakeHttpLayer().clearPendingHttpResponses();
  }

  public static void reset() {
    instance = new FakeHttpLayer();
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;

import android.annotation.NonNull;
import android.media.MediaCodec;
import android.media.MediaMuxer;
import android.media.MediaMuxer.Format;
import dalvik.system.CloseGuard;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;

/**
 * Implementation of {@link android.media.MediaMuxer} which directly passes input bytes to the
 * specified file, with no modification.
 */
@Implements(value = MediaMuxer.class, minSdk = LOLLIPOP)
public class ShadowMediaMuxer {
  // Maps between 'native' ids and corresponding output streams.
  private static final ConcurrentHashMap<Long, FileOutputStream> outputStreams =
      new ConcurrentHashMap<>();

  // Maps between 'native' ids and AtomicInteger objects tracking next track indices.
  private static final ConcurrentHashMap<Long, AtomicInteger> nextTrackIndices =
      new ConcurrentHashMap<>();

  // Maps between file descriptors and their original output stream.
  private static final ConcurrentHashMap<FileDescriptor, FileOutputStream> fdToStream =
      new ConcurrentHashMap<>();

  private static final Random random = new Random();

  // Keep in sync with MediaMuxer.java.
  private static final int MUXER_STATE_INITIALIZED = 0;

  @RealObject private MediaMuxer realMuxer;

  /**
   * Opens a FileOutputStream for the given path, and sets muxer state.
   *
   * <p>This needs to be shadowed, because the current MediaMuxer constructor opens a
   * RandomAccessFile, passes only the FileDescriptor along, and then closes the file. Since there
   * does not appear to be an easy way to go from FileDescriptor to a writeable stream in Java, this
   * method overrides that behavior to instead open and maintain a FileOutputStream.
   */
  @Implementation
  protected void __constructor__(@NonNull String path, @Format int format) throws IOException {
    if (path == null) {
      throw new IllegalArgumentException("path must not be null");
    }

    // Create a stream, and cache a mapping from file descriptor to stream.
    FileOutputStream stream = new FileOutputStream(path);
    FileDescriptor fd = stream.getFD();
    fdToStream.put(fd, stream);

    // Initialize the CloseGuard and last track index, since they are otherwise null and 0.
    CloseGuard guard = CloseGuard.get();
    ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mCloseGuard", guard);
    ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mLastTrackIndex", -1);

    // Pre-OREO jumps straight to nativeSetup inside the constructor.
    if (RuntimeEnvironment.getApiLevel() < O) {
      long nativeObject = nativeSetup(fd, format);
      ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mNativeObject", nativeObject);
      ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mState", MUXER_STATE_INITIALIZED);
      guard.open("release");
    } else {
      ReflectionHelpers.callInstanceMethod(
          MediaMuxer.class,
          realMuxer,
          "setUpMediaMuxer",
          ReflectionHelpers.ClassParameter.from(FileDescriptor.class, fd),
          ReflectionHelpers.ClassParameter.from(int.class, format));
    }
  }

  /**
   * Generates and returns an internal id to track the FileOutputStream corresponding to individual
   * MediaMuxer instances.
   */
  @Implementation
  protected static long nativeSetup(@NonNull FileDescriptor fd, int format) throws IOException {
    FileOutputStream outputStream = fdToStream.get(fd);

    long potentialKey;
    do {
      potentialKey = random.nextLong();
    } while (potentialKey == 0 || outputStreams.putIfAbsent(potentialKey, outputStream) != null);

    nextTrackIndices.put(potentialKey, new AtomicInteger(0));
    return potentialKey;
  }

  /** Returns an incremented track id for the associated muxer. */
  @Implementation
  protected static int nativeAddTrack(
      long nativeObject, @NonNull String[] keys, @NonNull Object[] values) {
    AtomicInteger nextTrackIndex = nextTrackIndices.get(nativeObject);
    if (nextTrackIndex == null) {
      throw new IllegalStateException("No next track index configured for key: " + nativeObject);
    }

    return nextTrackIndex.getAndIncrement();
  }

  /** Writes the given data to the FileOutputStream for the associated muxer. */
  @Implementation
  protected static void nativeWriteSampleData(
      long nativeObject,
      int trackIndex,
      @NonNull ByteBuffer byteBuf,
      int offset,
      int size,
      long presentationTimeUs,
      @MediaCodec.BufferFlag int flags) {
    int toWrite = size - offset;
    byte[] bytes = new byte[toWrite];
    byteBuf.get(bytes, 0, toWrite);

    try {
      getStream(nativeObject).write(bytes);
    } catch (IOException e) {
      throw new RuntimeException("Unable to write to temporary file.", e);
    }
  }

  /** Closes the FileOutputStream for the associated muxer. */
  @Implementation
  protected static void nativeStop(long nativeObject) {
    try {
      // Close the output stream.
      getStream(nativeObject).close();

      // Clear the stream from both internal caches.
      fdToStream.remove(outputStreams.remove(nativeObject).getFD());
    } catch (IOException e) {
      throw new RuntimeException("Unable to close temporary file.", e);
    }
  }

  private static FileOutputStream getStream(long streamKey) {
    FileOutputStream stream = outputStreams.get(streamKey);
    if (stream == null) {
      throw new IllegalStateException("No output stream configured for key: " + streamKey);
    }

    return stream;
  }
}


package org.robolectric.shadows.httpclient;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.RequestDirector;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;

public class FakeHttpLayer {
  private final List<HttpResponseGenerator> pendingHttpResponses = new ArrayList<>();
  private final List<HttpRequestInfo> httpRequestInfos = new ArrayList<>();
  private final List<HttpResponse> httpResponses = new ArrayList<>();
  private final List<HttpEntityStub.ResponseRule> httpResponseRules = new ArrayList<>();
  private HttpResponse defaultHttpResponse;
  private boolean interceptHttpRequests = true;
  private boolean logHttpRequests = false;
  private List<byte[]> httpResposeContent = new ArrayList<>();
  private boolean interceptResponseContent;

  public HttpRequestInfo getLastSentHttpRequestInfo() {
    List<HttpRequestInfo> requestInfos = getSentHttpRequestInfos();
    if (requestInfos.isEmpty()) {
      return null;
    }
    return requestInfos.get(requestInfos.size() - 1);
  }

  public void addPendingHttpResponse(int statusCode, String responseBody, Header... headers) {
    addPendingHttpResponse(new TestHttpResponse(statusCode, responseBody, headers));
  }

  public void addPendingHttpResponse(final HttpResponse httpResponse) {
    addPendingHttpResponse(new HttpResponseGenerator() {
      @Override
      public HttpResponse getResponse(HttpRequest request) {
        return httpResponse;
      }
    });
  }

  public void addPendingHttpResponse(HttpResponseGenerator httpResponseGenerator) {
    pendingHttpResponses.add(httpResponseGenerator);
  }

  public void addHttpResponseRule(String method, String uri, HttpResponse response) {
    addHttpResponseRule(new DefaultRequestMatcher(method, uri), response);
  }

  public void addHttpResponseRule(String uri, HttpResponse response) {
    addHttpResponseRule(new UriRequestMatcher(uri), response);
  }

  public void addHttpResponseRule(String uri, String response) {
    addHttpResponseRule(new UriRequestMatcher(uri), new TestHttpResponse(200, response));
  }

  public void addHttpResponseRule(RequestMatcher requestMatcher, HttpResponse response) {
    addHttpResponseRule(new RequestMatcherResponseRule(requestMatcher, response));
  }

  /**
   * Add a response rule.
   *
   * @param requestMatcher Request matcher
   * @param responses      A list of responses that are returned to matching requests in order from first to last.
   */
  public void addHttpResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses) {
    addHttpResponseRule(new RequestMatcherResponseRule(requestMatcher, responses));
  }

  public void addHttpResponseRule(HttpEntityStub.ResponseRule responseRule) {
    httpResponseRules.add(0, responseRule);
  }

  public void setDefaultHttpResponse(HttpResponse defaultHttpResponse) {
    this.defaultHttpResponse = defaultHttpResponse;
  }

  public void setDefaultHttpResponse(int statusCode, String responseBody) {
    setDefaultHttpResponse(new TestHttpResponse(statusCode, responseBody));
  }

  private HttpResponse findResponse(HttpRequest httpRequest) throws HttpException, IOException {
    if (!pendingHttpResponses.isEmpty()) {
      return pendingHttpResponses.remove(0).getResponse(httpRequest);
    }

    for (HttpEntityStub.ResponseRule httpResponseRule : httpResponseRules) {
      if (httpResponseRule.matches(httpRequest)) {
        return httpResponseRule.getResponse();
      }
    }

    System.err.println("Unexpected HTTP call " + httpRequest.getRequestLine());

    return defaultHttpResponse;
  }

  public HttpResponse emulateRequest(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext, RequestDirector requestDirector) throws HttpException, IOException {
    if (logHttpRequests) {
      System.out.println("  <-- " + httpRequest.getRequestLine());
    }
    HttpResponse httpResponse = findResponse(httpRequest);
    if (logHttpRequests) {
      System.out.println("  --> " + (httpResponse == null ? null : httpResponse.getStatusLine().getStatusCode()));
    }

    if (httpResponse == null) {
      throw new RuntimeException("Unexpected call to execute, no pending responses are available. See Robolectric.addPendingResponse(). Request was: " +
          httpRequest.getRequestLine().getMethod() + " " + httpRequest.getRequestLine().getUri());
    } else {
      HttpParams params = httpResponse.getParams();

      if (HttpConnectionParams.getConnectionTimeout(params) < 0) {
        throw new ConnectTimeoutException("Socket is not connected");
      } else if (HttpConnectionParams.getSoTimeout(params) < 0) {
        throw new ConnectTimeoutException("The operation timed out");
      }
    }

    addRequestInfo(new HttpRequestInfo(httpRequest, httpHost, httpContext, requestDirector));
    addHttpResponse(httpResponse);
    return httpResponse;
  }
  public boolean hasPendingResponses() {
    return !pendingHttpResponses.isEmpty();
  }

  public boolean hasRequestInfos() {
    return !httpRequestInfos.isEmpty();
  }

  public void clearRequestInfos() {
    httpRequestInfos.clear();
  }

  /**
   * This method is not supposed to be consumed by tests. This exists solely for the purpose of
   * logging real HTTP requests, so that functional/integration tests can verify if those were made, without
   * messing with the fake http layer to actually perform the http call, instead of returning a mocked response.
   *
   * If you are just using mocked http calls, you should not even notice this method here.
   *
   * @param requestInfo Request info object to add.
   */
  public void addRequestInfo(HttpRequestInfo requestInfo) {
    httpRequestInfos.add(requestInfo);
  }

  public boolean hasResponseRules() {
    return !httpResponseRules.isEmpty();
  }

  public boolean hasRequestMatchingRule(RequestMatcher rule) {
    for (HttpRequestInfo requestInfo : httpRequestInfos) {
      if (rule.matches(requestInfo.httpRequest)) {
        return true;
      }
    }
    return false;
  }

  public HttpRequestInfo getSentHttpRequestInfo(int index) {
    return httpRequestInfos.get(index);
  }

  public HttpRequestInfo getNextSentHttpRequestInfo() {
    return httpRequestInfos.size() > 0 ? httpRequestInfos.remove(0) : null;
  }

  public void logHttpRequests() {
    logHttpRequests = true;
  }

  public void silence() {
    logHttpRequests = false;
  }

  public List<HttpRequestInfo> getSentHttpRequestInfos() {
    return new ArrayList<>(httpRequestInfos);
  }

  public void clearHttpResponseRules() {
    httpResponseRules.clear();
  }

  public void clearPendingHttpResponses() {
    pendingHttpResponses.clear();
  }

  /**
   * This method return a list containing all the HTTP responses logged by the fake http layer, be it
   * mocked http responses, be it real http calls (if {code}interceptHttpRequests{/code} is set to false).
   *
   * It doesn't make much sense to call this method if said property is set to true, as you yourself are
   * providing the response, but it's here nonetheless.
   *
   * @return List of all HTTP Responses logged by the fake http layer.
   */
  public List<HttpResponse> getHttpResponses() {
    return new ArrayList<>(httpResponses);
  }

  /**
   * As a consumer of the fake http call, you should never call this method. This should be used solely
   * by components that exercises http calls.
   *
   * @param response The final response received by the server
   */
  public void addHttpResponse(HttpResponse response) {
    this.httpResponses.add(response);
  }

  public void addHttpResponseContent(byte[] content) {
    this.httpResposeContent.add(content);
  }

  public List<byte[]> getHttpResposeContentList() {
    return httpResposeContent;
  }

  /**
   * Helper method that returns the latest received response from the server.
   * @return The latest HTTP response or null, if no responses are available
   */
  public HttpResponse getLastHttpResponse() {
    if (httpResponses.isEmpty()) return null;
    return httpResponses.get(httpResponses.size()-1) ;
  }

  /**
   * Call this method if you want to ensure that there's no http responses logged from this point until
   * the next response arrives. Helpful to ensure that the state is "clear" before actions are executed.
   */
  public void clearHttpResponses() {
    this.httpResponses.clear();
  }

  /**
   * You can disable Robolectric's fake HTTP layer temporarily
   * by calling this method.
   * @param interceptHttpRequests whether all HTTP requests should be
   *                              intercepted (true by default)
   */
  public void interceptHttpRequests(boolean interceptHttpRequests) {
    this.interceptHttpRequests = interceptHttpRequests;
  }

  public boolean isInterceptingHttpRequests() {
    return interceptHttpRequests;
  }

  public void interceptResponseContent(boolean interceptResponseContent) {
    this.interceptResponseContent = interceptResponseContent;
  }

  public boolean isInterceptingResponseContent() {
    return interceptResponseContent;
  }

  public static class RequestMatcherResponseRule implements HttpEntityStub.ResponseRule {
    private RequestMatcher requestMatcher;
    private HttpResponse responseToGive;
    private IOException ioException;
    private HttpException httpException;
    private List<? extends HttpResponse> responses;

    public RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpResponse responseToGive) {
      this.requestMatcher = requestMatcher;
      this.responseToGive = responseToGive;
    }

    public RequestMatcherResponseRule(RequestMatcher requestMatcher, IOException ioException) {
      this.requestMatcher = requestMatcher;
      this.ioException = ioException;
    }

    public RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpException httpException) {
      this.requestMatcher = requestMatcher;
      this.httpException = httpException;
    }

    public RequestMatcherResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses) {
      this.requestMatcher = requestMatcher;
      this.responses = responses;
    }

    @Override
    public boolean matches(HttpRequest request) {
      return requestMatcher.matches(request);
    }

    @Override
    public HttpResponse getResponse() throws HttpException, IOException {
      if (httpException != null) throw httpException;
      if (ioException != null) throw ioException;
      if (responseToGive != null) {
        return responseToGive;
      } else {
        if (responses.isEmpty()) {
          throw new RuntimeException("No more responses left to give");
        }
        return responses.remove(0);
      }
    }
  }

  public static class DefaultRequestMatcher implements RequestMatcher {
    private String method;
    private String uri;

    public DefaultRequestMatcher(String method, String uri) {
      this.method = method;
      this.uri = uri;
    }

    @Override
    public boolean matches(HttpRequest request) {
      return request.getRequestLine().getMethod().equals(method) &&
          request.getRequestLine().getUri().equals(uri);
    }
  }

  public static class UriRequestMatcher implements RequestMatcher {
    private String uri;

    public UriRequestMatcher(String uri) {
      this.uri = uri;
    }

    @Override
    public boolean matches(HttpRequest request) {
      return request.getRequestLine().getUri().equals(uri);
    }
  }

  public static class RequestMatcherBuilder implements RequestMatcher {
    private String method, hostname, path;
    private boolean noParams;
    private Map<String, String> params = new HashMap<>();
    private Map<String, String> headers = new HashMap<>();
    private PostBodyMatcher postBodyMatcher;

    public interface PostBodyMatcher {
      /**
       * Hint: you can use EntityUtils.toString(actualPostBody) to help you implement your matches method.
       *
       * @param actualPostBody The post body of the actual request that we are matching against.
       * @return true if you consider the body to match
       * @throws IOException Get turned into a RuntimeException to cause your test to fail.
       */
      boolean matches(HttpEntity actualPostBody) throws IOException;
    }

    public RequestMatcherBuilder method(String method) {
      this.method = method;
      return this;
    }

    public RequestMatcherBuilder host(String hostname) {
      this.hostname = hostname;
      return this;
    }

    public RequestMatcherBuilder path(String path) {
      if (path.startsWith("/")) {
        throw new RuntimeException("Path should not start with '/'");
      }
      this.path = "/" + path;
      return this;
    }

    public RequestMatcherBuilder param(String name, String value) {
      params.put(name, value);
      return this;
    }

    public RequestMatcherBuilder noParams() {
      noParams = true;
      return this;
    }

    public RequestMatcherBuilder postBody(PostBodyMatcher postBodyMatcher) {
      this.postBodyMatcher = postBodyMatcher;
      return this;
    }

    public RequestMatcherBuilder header(String name, String value) {
      headers.put(name, value);
      return this;
    }

    @Override
    public boolean matches(HttpRequest request) {
      URI uri = URI.create(request.getRequestLine().getUri());
      if (method != null && !method.equals(request.getRequestLine().getMethod())) {
        return false;
      }
      if (hostname != null && !hostname.equals(uri.getHost())) {
        return false;
      }
      if (path != null && !path.equals(uri.getRawPath())) {
        return false;
      }
      if (noParams && !uri.getRawQuery().equals(null)) {
        return false;
      }
      if (params.size() > 0) {
        Map<String, String> requestParams = ParamsParser.parseParams(request);
        if (!requestParams.equals(params)) {
          return false;
        }
      }
      if (headers.size() > 0) {
        Map<String, String> actualRequestHeaders = new HashMap<>();
        for (Header header : request.getAllHeaders()) {
          actualRequestHeaders.put(header.getName(), header.getValue());
        }
        if (!headers.equals(actualRequestHeaders)) {
          return false;
        }
      }
      if (postBodyMatcher != null) {
        if (!(request instanceof HttpEntityEnclosingRequestBase)) {
          return false;
        }
        HttpEntityEnclosingRequestBase postOrPut = (HttpEntityEnclosingRequestBase) request;
        try {
          if (!postBodyMatcher.matches(postOrPut.getEntity())) {
            return false;
          }
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }
      return true;
    }

    public String getHostname() {
      return hostname;
    }

    public String getPath() {
      return path;
    }

    public String getParam(String key) {
      return params.get(key);
    }

    public String getHeader(String key) {
      return headers.get(key);
    }

    public boolean isNoParams() {
      return noParams;
    }

    public String getMethod() {
      return method;
    }
  }

  public static class UriRegexMatcher implements RequestMatcher {
    private String method;
    private final Pattern uriRegex;

    public UriRegexMatcher(String method, String uriRegex) {
      this.method = method;
      this.uriRegex = Pattern.compile(uriRegex);
    }

    @Override
    public boolean matches(HttpRequest request) {
      return request.getRequestLine().getMethod().equals(method) &&
          uriRegex.matcher(request.getRequestLine().getUri()).matches();
    }
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;

import android.media.AudioRoutesInfo;
import android.media.MediaRouter;
import android.media.MediaRouter.RouteInfo;
import android.os.Parcel;
import android.text.TextUtils;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/** Shadow class for {@link android.media.MediaRouter}. */
@Implements(MediaRouter.class)
public class ShadowMediaRouter {
  public static final String BLUETOOTH_DEVICE_NAME = "TestBluetoothDevice";

  private @RealObject MediaRouter realObject;

  /**
   * Adds the Bluetooth A2DP route and ensures it's the selected route, simulating connecting a
   * Bluetooth device.
   */
  public void addBluetoothRoute() {
    updateBluetoothAudioRoute(BLUETOOTH_DEVICE_NAME);

    if (RuntimeEnvironment.getApiLevel() <= JELLY_BEAN_MR1) {
      ReflectionHelpers.callInstanceMethod(
          MediaRouter.class,
          realObject,
          "selectRouteInt",
          ClassParameter.from(int.class, MediaRouter.ROUTE_TYPE_LIVE_AUDIO),
          ClassParameter.from(RouteInfo.class, getBluetoothA2dpRoute()));
    } else {
      realObject.selectRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO, getBluetoothA2dpRoute());
    }
  }

  /** Removes the Bluetooth A2DP route, simulating disconnecting the Bluetooth device. */
  public void removeBluetoothRoute() {
    // Android's AudioService passes a null Bluetooth device name to MediaRouter to signal that the
    // A2DP route should be removed.
    updateBluetoothAudioRoute(null);
  }

  /** Returns whether the Bluetooth A2DP route is the currently selected route. */
  public boolean isBluetoothRouteSelected(int type) {
    return realObject.getSelectedRoute(type).equals(getBluetoothA2dpRoute());
  }

  private static RouteInfo getBluetoothA2dpRoute() {
    return ReflectionHelpers.getField(
        ReflectionHelpers.getStaticField(MediaRouter.class, "sStatic"), "mBluetoothA2dpRoute");
  }

  /**
   * Updates the MediaRouter's Bluetooth audio route.
   *
   * @param bluetoothDeviceName the name of the Bluetooth device or null to indicate that the
   *     already-existing Bluetooth A2DP device should be removed
   */
  private void updateBluetoothAudioRoute(@Nullable String bluetoothDeviceName) {
    callUpdateAudioRoutes(newAudioRouteInfo(bluetoothDeviceName));
  }

  /**
   * Creates a new {@link AudioRoutesInfo} to be used for updating the Bluetooth audio route.
   *
   * @param bluetoothDeviceName the name of the Bluetooth device or null to indicate that the
   *     already-existing Bluetooth A2DP device should be removed
   */
  private static AudioRoutesInfo newAudioRouteInfo(@Nullable String bluetoothDeviceName) {
    Parcel p = Parcel.obtain();
    TextUtils.writeToParcel(bluetoothDeviceName, p, /* parcelableFlags= */ 0);
    p.setDataPosition(0);
    return AudioRoutesInfo.CREATOR.createFromParcel(p);
  }

  private void callUpdateAudioRoutes(AudioRoutesInfo routesInfo) {
    ReflectionHelpers.callInstanceMethod(
        ReflectionHelpers.getStaticField(MediaRouter.class, "sStatic"),
        RuntimeEnvironment.getApiLevel() <= JELLY_BEAN
            ? "updateRoutes"
            : "updateAudioRoutes",
        ClassParameter.from(AudioRoutesInfo.class, routesInfo));
  }

  @Resetter
  public static void reset() {
    ReflectionHelpers.setStaticField(MediaRouter.class, "sStatic", null);
  }
}


package org.robolectric.shadows;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import javax.annotation.concurrent.GuardedBy;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;

@Implements(android.os.Process.class)
public class ShadowProcess {
  private static int pid;
  private static int uid = getRandomApplicationUid();
  private static int tid = getRandomApplicationUid();
  private static final Object threadPrioritiesLock = new Object();

  @GuardedBy("threadPrioritiesLock")
  private static final Map<Integer, Integer> threadPriorities = new HashMap<Integer, Integer>();

  @Implementation
  protected static final int myPid() {
    return pid;
  }

  /**
   * Returns the identifier of this process's uid. Unlike Android UIDs are randomly initialized to
   * prevent tests from depending on any given value. Tests should access the current process UID
   * via {@link android.os.Process#myUid()}. You can override this value by calling {@link
   * #setUid(int)}.
   */
  @Implementation
  protected static final int myUid() {
    return uid;
  }

  /**
   * Returns the identifier ({@link java.lang.Thread#getId()}) of the current thread ({@link
   * java.lang.Thread#currentThread()}).
   */
  @Implementation
  protected static final int myTid() {
    return (int) Thread.currentThread().getId();
  }

  /**
   * Stores priority for the current thread, but doesn't actually change it to not mess up with test
   * runner. Unlike real implementation does not throw any exceptions.
   */
  @Implementation
  protected static final void setThreadPriority(int priority) {
    synchronized (threadPrioritiesLock) {
      threadPriorities.put(ShadowProcess.myTid(), priority);
    }
  }

  /**
   * Stores priority for the given thread, but doesn't actually change it to not mess up with test
   * runner. Unlike real implementation does not throw any exceptions.
   *
   * @param tid The identifier of the thread. If equals zero, the identifier of the calling thread
   *     will be used.
   */
  @Implementation
  protected static final void setThreadPriority(int tid, int priority) {
    checkArgument(
        priority >= android.os.Process.THREAD_PRIORITY_URGENT_AUDIO
            && priority <= android.os.Process.THREAD_PRIORITY_LOWEST,
        "priority %s out of range. Use a Process.THREAD_PRIORITY_* constant.",
        priority);

    if (tid == 0) {
      tid = ShadowProcess.myTid();
    }
    synchronized (threadPrioritiesLock) {
      threadPriorities.put(tid, priority);
    }
  }

  /**
   * Returns priority stored for the given thread.
   *
   * @param tid The identifier of the thread. If equals zero, the identifier of the calling thread
   *     will be used.
   */
  @Implementation
  protected static final int getThreadPriority(int tid) {
    if (tid == 0) {
      tid = ShadowProcess.myTid();
    }
    synchronized (threadPrioritiesLock) {
      return threadPriorities.getOrDefault(tid, 0);
    }
  }

  /**
   * Sets the identifier of this process.
   */
  public static void setUid(int uid) {
    ShadowProcess.uid = uid;
  }

  /**
   * Sets the identifier of this process.
   */
  public static void setPid(int pid) {
    ShadowProcess.pid = pid;
  }

  @Resetter
  public static void reset() {
    ShadowProcess.pid = 0;
    synchronized (threadPrioritiesLock) {
      threadPriorities.clear();
    }
    // We cannot re-randomize uid, because it would break code that statically depends on
    // android.os.Process.myUid(), which persists between tests.
  }

  static int getRandomApplicationUid() {
    // UIDs are randomly initialized to prevent tests from depending on any given value. Tests
    // should access the current process UID via android.os.Process::myUid().
    return ThreadLocalRandom.current()
        .nextInt(
            android.os.Process.FIRST_APPLICATION_UID, android.os.Process.LAST_APPLICATION_UID + 1);
  }
}


package org.robolectric.shadows;

/**
 * Transliteration of native BitSet64.
 *
 * <p>Unlike the native code stores value inline as opposed to a manipulating data via series of
 * static methods passed values by reference.
 *
 * @see "system/core/libutils/include/utils/BitSet.h"
 */
public class NativeBitSet64 {

  private long value;

  NativeBitSet64(long value) {
    this.value = value;
  }

  NativeBitSet64(NativeBitSet64 other) {
    this.value = other.value;
  }

  NativeBitSet64() {
    this(0);
  }

  /** Gets the value associated with a particular bit index. */
  static long valueForBit(int n) {
    return 0x8000000000000000L >>> n;
  }

  /** Clears the bit set. */
  void clear() {
    value = 0;
  }

  /** Returns the number of marked bits in the set. */
  int count() {
    int count = 0;
    for (int n = 0; n < 64; n++) {
      if (hasBit(n)) {
        count++;
      }
    }
    return count;
  }

  /** Returns true if the bit set does not contain any marked bits. */
  boolean isEmpty() {
    return value == 0;
  }

  /** Returns true if the specified bit is marked. */
  boolean hasBit(int n) {
    return (value & valueForBit(n)) != 0;
  }

  /** Marks the specified bit. */
  void markBit(int n) {
    value |= valueForBit(n);
  }

  /** Clears the specified bit. */
  void clearBit(int n) {
    value &= ~valueForBit(n);
  }

  /** Finds the first marked bit in the set. Result is undefined if all bits are unmarked. */
  int firstMarkedBit() {
    for (int n = 0; n < 64; n++) {
      if (hasBit(n)) {
        return n;
      }
    }
    return 0;
  }

  /**
   * Finds the first marked bit in the set and clears it. Returns the bit index. Result is undefined
   * if all bits are unmarked.
   */
  int clearFirstMarkedBit() {
    int n = firstMarkedBit();
    clearBit(n);
    return n;
  }

  /**
   * Gets the index of the specified bit in the set, which is the number of marked bits that appear
   * before the specified bit.
   */
  int getIndexOfBit(int n) {
    // return __builtin_popcountll(value & ~(0xffffffffffffffffULL >> n));
    int numMarkedBits = 0;
    for (int i = 0; i < n; i++) {
      if (hasBit(i)) {
        numMarkedBits++;
      }
    }
    return numMarkedBits;
  }

  public void setValue(long l) {
    value = l;
  }

  public long getValue() {
    return value;
  }
}