package org.robolectric.res.android;

import static org.robolectric.res.android.Util.isTruthy;

import com.google.common.base.Charsets;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * transliterated from
 * https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/tools/aapt2/ConfigDescription.cpp
 */
public class ConfigDescription {
  public static final int SDK_CUPCAKE = 3;
  public static final int SDK_DONUT = 4;
  public static final int SDK_ECLAIR = 5;
  public static final int SDK_ECLAIR_0_1 = 6;
  public static final int SDK_ECLAIR_MR1 = 7;
  public static final int SDK_FROYO = 8;
  public static final int SDK_GINGERBREAD = 9;
  public static final int SDK_GINGERBREAD_MR1 = 10;
  public static final int SDK_HONEYCOMB = 11;
  public static final int SDK_HONEYCOMB_MR1 = 12;
  public static final int SDK_HONEYCOMB_MR2 = 13;
  public static final int SDK_ICE_CREAM_SANDWICH = 14;
  public static final int SDK_ICE_CREAM_SANDWICH_MR1 = 15;
  public static final int SDK_JELLY_BEAN = 16;
  public static final int SDK_JELLY_BEAN_MR1 = 17;
  public static final int SDK_JELLY_BEAN_MR2 = 18;
  public static final int SDK_KITKAT = 19;
  public static final int SDK_KITKAT_WATCH = 20;
  public static final int SDK_LOLLIPOP = 21;
  public static final int SDK_LOLLIPOP_MR1 = 22;
  public static final int SDK_MNC = 23;
  public static final int SDK_NOUGAT = 24;
  public static final int SDK_NOUGAT_MR1 = 25;
  public static final int SDK_O = 26;

  /**
   * Constant used to to represent MNC (Mobile Network Code) zero.
   * 0 cannot be used, since it is used to represent an undefined MNC.
   */
  private static final int ACONFIGURATION_MNC_ZERO = 0xffff;

  private static final String kWildcardName = "any";

  private static final Pattern MCC_PATTERN = Pattern.compile("mcc([\\d]+)");
  private static final Pattern MNC_PATTERN = Pattern.compile("mnc([\\d]+)");
  private static final Pattern SMALLEST_SCREEN_WIDTH_PATTERN = Pattern.compile("^sw([0-9]+)dp");
  private static final Pattern SCREEN_WIDTH_PATTERN = Pattern.compile("^w([0-9]+)dp");
  private static final Pattern SCREEN_HEIGHT_PATTERN = Pattern.compile("^h([0-9]+)dp");
  private static final Pattern DENSITY_PATTERN = Pattern.compile("^([0-9]+)dpi");
  private static final Pattern HEIGHT_WIDTH_PATTERN = Pattern.compile("^([0-9]+)x([0-9]+)");
  private static final Pattern VERSION_QUALIFIER_PATTERN = Pattern.compile("v([0-9]+)$");

  public static class LocaleValue {

    String language;
    String region;
    String script;
    String variant;

    void set_language(String language_chars) {
      language = language_chars.trim().toLowerCase();
    }

    void set_region(String region_chars) {
      region = region_chars.trim().toUpperCase();
    }

    void set_script(String script_chars) {
      script = String.valueOf(Character.toUpperCase(script_chars.charAt(0))) +
          script_chars.substring(1).toLowerCase();
    }

    void set_variant(String variant_chars) {
      variant = variant_chars.trim();
    }

    static boolean is_alpha(final String str) {
      for (int i = 0; i < str.length(); i++) {
        if (!Character.isAlphabetic(str.charAt(i))) {
          return false;
        }
      }

      return true;
    }

    int initFromParts(PeekingIterator<String> iter) {

      String part = iter.peek();
      if (part.startsWith("b+")) {
        // This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags,
        // except that the separator is "+" and not "-".
        String[] subtags = part.substring(2).toLowerCase().split("\\+", 0);
        if (subtags.length == 1) {
          set_language(subtags[0]);
        } else if (subtags.length == 2) {
          set_language(subtags[0]);

          // The second tag can either be a region, a variant or a script.
          switch (subtags[1].length()) {
            case 2:
            case 3:
              set_region(subtags[1]);
              break;
            case 4:
              if ('0' <= subtags[1].charAt(0) && subtags[1].charAt(0) <= '9') {
                // This is a variant: fall through
              } else {
                set_script(subtags[1]);
                break;
              }
              // fall through
            case 5:
            case 6:
            case 7:
            case 8:
              set_variant(subtags[1]);
              break;
            default:
              return -1;
          }
        } else if (subtags.length == 3) {
          // The language is always the first subtag.
          set_language(subtags[0]);

          // The second subtag can either be a script or a region code.
          // If its size is 4, it's a script code, else it's a region code.
          if (subtags[1].length() == 4) {
            set_script(subtags[1]);
          } else if (subtags[1].length() == 2 || subtags[1].length() == 3) {
            set_region(subtags[1]);
          } else {
            return -1;
          }

          // The third tag can either be a region code (if the second tag was
          // a script), else a variant code.
          if (subtags[2].length() >= 4) {
            set_variant(subtags[2]);
          } else {
            set_region(subtags[2]);
          }
        } else if (subtags.length == 4) {
          set_language(subtags[0]);
          set_script(subtags[1]);
          set_region(subtags[2]);
          set_variant(subtags[3]);
        } else {
          return -1;
        }

        iter.next();

      } else {
        if ((part.length() == 2 || part.length() == 3) && is_alpha(part) &&
            !Objects.equals(part, "car")) {
          set_language(part);
          iter.next();

          if (iter.hasNext()) {
            final String region_part = iter.peek();
            if (region_part.charAt(0) == 'r' && region_part.length() == 3) {
              set_region(region_part.substring(1));
              iter.next();
            }
          }
        }
      }

      return 0;
    }

    public void writeTo(ResTable_config out) {
      out.packLanguage(language);
      out.packRegion(region);

      Arrays.fill(out.localeScript, (byte) 0);
      byte[] scriptBytes = script == null ? new byte[4] : script.getBytes(Charsets.UTF_8);
      System.arraycopy(scriptBytes, 0, out.localeScript, 0, scriptBytes.length);

      Arrays.fill(out.localeVariant, (byte) 0);
      byte[] variantBytes = variant == null ? new byte[8] : variant.getBytes(Charsets.UTF_8);
      System.arraycopy(variantBytes, 0, out.localeVariant, 0, variantBytes.length);
    }
  }

  public static boolean parse(final String str, ResTable_config out) {
    return parse(str, out, true);
  }

  public static boolean parse(final String str, ResTable_config out, boolean applyVersionForCompat) {
    PeekingIterator<String> part_iter = Iterators
        .peekingIterator(Arrays.asList(str.toLowerCase().split("-")).iterator());

    LocaleValue locale = new LocaleValue();

    boolean success = !part_iter.hasNext();
    if (part_iter.hasNext() && parseMcc(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseMnc(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext()) {
      // Locale spans a few '-' separators, so we let it
      // control the index.
      int parts_consumed = locale.initFromParts(part_iter);
      if (parts_consumed < 0) {
        return false;
      } else {
        locale.writeTo(out);
        if (!part_iter.hasNext()) {
          success = !part_iter.hasNext();
        }
      }
    }

    if (part_iter.hasNext() && parseLayoutDirection(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseSmallestScreenWidthDp(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseScreenWidthDp(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseScreenHeightDp(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseScreenLayoutSize(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseScreenLayoutLong(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseScreenRound(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseWideColorGamut(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseHdr(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseOrientation(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseUiModeType(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseUiModeNight(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseDensity(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseTouchscreen(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseKeysHidden(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseKeyboard(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseNavHidden(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseNavigation(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseScreenSize(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (part_iter.hasNext() && parseVersion(part_iter.peek(), out)) {
      part_iter.next();
      if (!part_iter.hasNext()) {
        success = !part_iter.hasNext();
      }
    }

    if (!success) {
      // Unrecognized.
      return false;
    }

    if (out != null && applyVersionForCompat) {
      applyVersionForCompatibility(out);
    }
    return true;
  }

  private static boolean parseLayoutDirection(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_LAYOUTDIR) |
                ResTable_config.LAYOUTDIR_ANY;
      }
      return true;
    } else if (Objects.equals(name, "ldltr")) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_LAYOUTDIR) |
                ResTable_config.LAYOUTDIR_LTR;
      }
      return true;
    } else if (Objects.equals(name, "ldrtl")) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_LAYOUTDIR) |
                ResTable_config.LAYOUTDIR_RTL;
      }
      return true;
    }

    return false;
  }

  private static boolean parseSmallestScreenWidthDp(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.smallestScreenWidthDp = ResTable_config.SCREENWIDTH_ANY;
      }
      return true;
    }

    Matcher matcher = SMALLEST_SCREEN_WIDTH_PATTERN.matcher(name);
    if (matcher.matches()) {
      out.smallestScreenWidthDp = Integer.parseInt(matcher.group(1));
      return true;
    }
    return false;
  }

  private static boolean parseScreenWidthDp(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.screenWidthDp = ResTable_config.SCREENWIDTH_ANY;
      }
      return true;
    }

    Matcher matcher = SCREEN_WIDTH_PATTERN.matcher(name);
    if (matcher.matches()) {
      out.screenWidthDp = Integer.parseInt(matcher.group(1));
      return true;
    }
    return false;
  }

  private static boolean parseScreenHeightDp(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.screenHeightDp = ResTable_config.SCREENWIDTH_ANY;
      }
      return true;
    }

    Matcher matcher = SCREEN_HEIGHT_PATTERN.matcher(name);
    if (matcher.matches()) {
      out.screenHeightDp = Integer.parseInt(matcher.group(1));
      return true;
    }
    return false;
  }

  private static boolean parseScreenLayoutSize(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_SCREENSIZE) |
                ResTable_config.SCREENSIZE_ANY;
      }
      return true;
    } else if (Objects.equals(name, "small")) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_SCREENSIZE) |
                ResTable_config.SCREENSIZE_SMALL;
      }
      return true;
    } else if (Objects.equals(name, "normal")) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_SCREENSIZE) |
                ResTable_config.SCREENSIZE_NORMAL;
      }
      return true;
    } else if (Objects.equals(name, "large")) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_SCREENSIZE) |
                ResTable_config.SCREENSIZE_LARGE;
      }
      return true;
    } else if (Objects.equals(name, "xlarge")) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout & ~ResTable_config.MASK_SCREENSIZE) |
                ResTable_config.SCREENSIZE_XLARGE;
      }
      return true;
    }

    return false;
  }

  static boolean parseScreenLayoutLong(final String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.screenLayout =
            (out.screenLayout&~ResTable_config.MASK_SCREENLONG)
                | ResTable_config.SCREENLONG_ANY;
      }
      return true;
    } else if (Objects.equals(name, "long")) {
      if (out != null) out.screenLayout =
          (out.screenLayout&~ResTable_config.MASK_SCREENLONG)
              | ResTable_config.SCREENLONG_YES;
      return true;
    } else if (Objects.equals(name, "notlong")) {
      if (out != null) out.screenLayout =
          (out.screenLayout&~ResTable_config.MASK_SCREENLONG)
              | ResTable_config.SCREENLONG_NO;
      return true;
    }
    return false;
  }

  private static boolean parseScreenRound(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.screenLayout2 =
            (byte) ((out.screenLayout2 & ~ResTable_config.MASK_SCREENROUND) |
                            ResTable_config.SCREENROUND_ANY);
      }
      return true;
    } else if (Objects.equals(name, "round")) {
      if (out != null) {
        out.screenLayout2 =
            (byte) ((out.screenLayout2 & ~ResTable_config.MASK_SCREENROUND) |
                            ResTable_config.SCREENROUND_YES);
      }
      return true;
    } else if (Objects.equals(name, "notround")) {
      if (out != null) {
        out.screenLayout2 =
            (byte) ((out.screenLayout2 & ~ResTable_config.MASK_SCREENROUND) |
                ResTable_config.SCREENROUND_NO);
      }
      return true;
    }
    return false;
  }

  private static boolean parseWideColorGamut(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null)
        out.colorMode =
            (byte) ((out.colorMode & ~ResTable_config.MASK_WIDE_COLOR_GAMUT) |
                            ResTable_config.WIDE_COLOR_GAMUT_ANY);
      return true;
    } else if (Objects.equals(name, "widecg")) {
      if (out != null)
        out.colorMode =
            (byte) ((out.colorMode & ~ResTable_config.MASK_WIDE_COLOR_GAMUT) |
                            ResTable_config.WIDE_COLOR_GAMUT_YES);
      return true;
    } else if (Objects.equals(name, "nowidecg")) {
      if (out != null)
        out.colorMode =
            (byte) ((out.colorMode & ~ResTable_config.MASK_WIDE_COLOR_GAMUT) |
                            ResTable_config.WIDE_COLOR_GAMUT_NO);
      return true;
    }
    return false;
  }

  private static boolean parseHdr(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null)
        out.colorMode =
            (byte) ((out.colorMode & ~ResTable_config.MASK_HDR) |
                            ResTable_config.HDR_ANY);
      return true;
    } else if (Objects.equals(name, "highdr")) {
      if (out != null)
        out.colorMode =
            (byte) ((out.colorMode & ~ResTable_config.MASK_HDR) |
                            ResTable_config.HDR_YES);
      return true;
    } else if (Objects.equals(name, "lowdr")) {
      if (out != null)
        out.colorMode =
            (byte) ((out.colorMode & ~ResTable_config.MASK_HDR) |
                            ResTable_config.HDR_NO);
      return true;
    }
    return false;
  }

  private static boolean parseOrientation(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.orientation = ResTable_config.ORIENTATION_ANY;
      }
      return true;
    } else if (Objects.equals(name, "port")) {
      if (out != null) {
        out.orientation = ResTable_config.ORIENTATION_PORT;
      }
      return true;
    } else if (Objects.equals(name, "land")) {
      if (out != null) {
        out.orientation = ResTable_config.ORIENTATION_LAND;
      }
      return true;
    } else if (Objects.equals(name, "square")) {
      if (out != null) {
        out.orientation = ResTable_config.ORIENTATION_SQUARE;
      }
      return true;
    }

    return false;
  }

  private static boolean parseUiModeType(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_TYPE) |
            ResTable_config.UI_MODE_TYPE_ANY;
      }
      return true;
    } else if (Objects.equals(name, "desk")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_TYPE) |
            ResTable_config.UI_MODE_TYPE_DESK;
      }
      return true;
    } else if (Objects.equals(name, "car")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_TYPE) |
            ResTable_config.UI_MODE_TYPE_CAR;
      }
      return true;
    } else if (Objects.equals(name, "television")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_TYPE) |
            ResTable_config.UI_MODE_TYPE_TELEVISION;
      }
      return true;
    } else if (Objects.equals(name, "appliance")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_TYPE) |
            ResTable_config.UI_MODE_TYPE_APPLIANCE;
      }
      return true;
    } else if (Objects.equals(name, "watch")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_TYPE) |
            ResTable_config.UI_MODE_TYPE_WATCH;
      }
      return true;
    } else if (Objects.equals(name, "vrheadset")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_TYPE) |
            ResTable_config.UI_MODE_TYPE_VR_HEADSET;
      }
      return true;
    }

    return false;
  }

  private static boolean parseUiModeNight(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_NIGHT) |
            ResTable_config.UI_MODE_NIGHT_ANY;
      }
      return true;
    } else if (Objects.equals(name, "night")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_NIGHT) |
            ResTable_config.UI_MODE_NIGHT_YES;
      }
      return true;
    } else if (Objects.equals(name, "notnight")) {
      if (out != null) {
        out.uiMode = (out.uiMode & ~ResTable_config.MASK_UI_MODE_NIGHT) |
            ResTable_config.UI_MODE_NIGHT_NO;
      }
      return true;
    }

    return false;
  }

  private static boolean parseDensity(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_DEFAULT;
      }
      return true;
    }

    if (Objects.equals(name, "anydpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_ANY;
      }
      return true;
    }

    if (Objects.equals(name, "nodpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_NONE;
      }
      return true;
    }

    if (Objects.equals(name, "ldpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_LOW;
      }
      return true;
    }

    if (Objects.equals(name, "mdpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_MEDIUM;
      }
      return true;
    }

    if (Objects.equals(name, "tvdpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_TV;
      }
      return true;
    }

    if (Objects.equals(name, "hdpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_HIGH;
      }
      return true;
    }

    if (Objects.equals(name, "xhdpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_XHIGH;
      }
      return true;
    }

    if (Objects.equals(name, "xxhdpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_XXHIGH;
      }
      return true;
    }

    if (Objects.equals(name, "xxxhdpi")) {
      if (out != null) {
        out.density = ResTable_config.DENSITY_XXXHIGH;
      }
      return true;
    }

    // check that we have 'dpi' after the last digit.
    Matcher matcher = DENSITY_PATTERN.matcher(name);
    if (matcher.matches()) {
      out.density = Integer.parseInt(matcher.group(1));
      return true;
    }
    return false;
  }

  private static boolean parseTouchscreen(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.touchscreen = ResTable_config.TOUCHSCREEN_ANY;
      }
      return true;
    } else if (Objects.equals(name, "notouch")) {
      if (out != null) {
        out.touchscreen = ResTable_config.TOUCHSCREEN_NOTOUCH;
      }
      return true;
    } else if (Objects.equals(name, "stylus")) {
      if (out != null) {
        out.touchscreen = ResTable_config.TOUCHSCREEN_STYLUS;
      }
      return true;
    } else if (Objects.equals(name, "finger")) {
      if (out != null) {
        out.touchscreen = ResTable_config.TOUCHSCREEN_FINGER;
      }
      return true;
    }

    return false;
  }

  private static boolean parseKeysHidden(String name, ResTable_config out) {
    byte mask = 0;
    byte value = 0;
    if (Objects.equals(name, kWildcardName)) {
      mask = ResTable_config.MASK_KEYSHIDDEN;
      value = ResTable_config.KEYSHIDDEN_ANY;
    } else if (Objects.equals(name, "keysexposed")) {
      mask = ResTable_config.MASK_KEYSHIDDEN;
      value = ResTable_config.KEYSHIDDEN_NO;
    } else if (Objects.equals(name, "keyshidden")) {
      mask = ResTable_config.MASK_KEYSHIDDEN;
      value = ResTable_config.KEYSHIDDEN_YES;
    } else if (Objects.equals(name, "keyssoft")) {
      mask = ResTable_config.MASK_KEYSHIDDEN;
      value = ResTable_config.KEYSHIDDEN_SOFT;
    }

    if (mask != 0) {
      if (out != null) {
        out.inputFlags = (out.inputFlags & ~mask) | value;
      }
      return true;
    }

    return false;
  }

  private static boolean parseKeyboard(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.keyboard = ResTable_config.KEYBOARD_ANY;
      }
      return true;
    } else if (Objects.equals(name, "nokeys")) {
      if (out != null) {
        out.keyboard = ResTable_config.KEYBOARD_NOKEYS;
      }
      return true;
    } else if (Objects.equals(name, "qwerty")) {
      if (out != null) {
        out.keyboard = ResTable_config.KEYBOARD_QWERTY;
      }
      return true;
    } else if (Objects.equals(name, "12key")) {
      if (out != null) {
        out.keyboard = ResTable_config.KEYBOARD_12KEY;
      }
      return true;
    }

    return false;
  }

  private static boolean parseNavHidden(String name, ResTable_config out) {
    byte mask = 0;
    byte value = 0;
    if (Objects.equals(name, kWildcardName)) {
      mask = ResTable_config.MASK_NAVHIDDEN;
      value = ResTable_config.NAVHIDDEN_ANY;
    } else if (Objects.equals(name, "navexposed")) {
      mask = ResTable_config.MASK_NAVHIDDEN;
      value = ResTable_config.NAVHIDDEN_NO;
    } else if (Objects.equals(name, "navhidden")) {
      mask = ResTable_config.MASK_NAVHIDDEN;
      value = ResTable_config.NAVHIDDEN_YES;
    }

    if (mask != 0) {
      if (out != null) {
        out.inputFlags = (out.inputFlags & ~mask) | value;
      }
      return true;
    }

    return false;
  }

  private static boolean parseNavigation(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.navigation = ResTable_config.NAVIGATION_ANY;
      }
      return true;
    } else if (Objects.equals(name, "nonav")) {
      if (out != null) {
        out.navigation = ResTable_config.NAVIGATION_NONAV;
      }
      return true;
    } else if (Objects.equals(name, "dpad")) {
      if (out != null) {
        out.navigation = ResTable_config.NAVIGATION_DPAD;
      }
      return true;
    } else if (Objects.equals(name, "trackball")) {
      if (out != null) {
        out.navigation = ResTable_config.NAVIGATION_TRACKBALL;
      }
      return true;
    } else if (Objects.equals(name, "wheel")) {
      if (out != null) {
        out.navigation = ResTable_config.NAVIGATION_WHEEL;
      }
      return true;
    }

    return false;
  }

  private static boolean parseScreenSize(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.screenWidth = ResTable_config.SCREENWIDTH_ANY;
        out.screenHeight = ResTable_config.SCREENHEIGHT_ANY;
      }
      return true;
    }

    Matcher matcher = HEIGHT_WIDTH_PATTERN.matcher(name);
    if (matcher.matches()) {
      int w = Integer.parseInt(matcher.group(1));
      int h = Integer.parseInt(matcher.group(2));
      if (w < h) {
        return false;
      }
      out.screenWidth = w;
      out.screenHeight = h;
      return true;
    }
    return false;
  }

  private static boolean parseVersion(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.sdkVersion = ResTable_config.SDKVERSION_ANY;
        out.minorVersion = ResTable_config.MINORVERSION_ANY;
      }
      return true;
    }

    Matcher matcher = VERSION_QUALIFIER_PATTERN.matcher(name);
    if (matcher.matches()) {
      out.sdkVersion = Integer.parseInt(matcher.group(1));
      out.minorVersion = 0;
      return true;
    }
    return false;
  }

  private static boolean parseMnc(String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.mnc = 0;
      }
      return true;
    }

    Matcher matcher = MNC_PATTERN.matcher(name);
    if (matcher.matches()) {
      out.mnc = Integer.parseInt(matcher.group(1));
      if (out.mnc == 0) {
        out.mnc = ACONFIGURATION_MNC_ZERO;
      }
      return true;
    }
    return false;
  }

  private static boolean parseMcc(final String name, ResTable_config out) {
    if (Objects.equals(name, kWildcardName)) {
      if (out != null) {
        out.mcc = 0;
      }
      return true;
    }

    Matcher matcher = MCC_PATTERN.matcher(name);
    if (matcher.matches()) {
      out.mcc = Integer.parseInt(matcher.group(1));
      return true;
    }
    return false;
  }

  // transliterated from https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/tools/aapt/AaptConfig.cpp
  private static void applyVersionForCompatibility(ResTable_config config) {
    if (config == null) {
      return;
    }
    int min_sdk = 0;
    if (((config.uiMode & ResTable_config.MASK_UI_MODE_TYPE)
        == ResTable_config.UI_MODE_TYPE_VR_HEADSET) ||
        (config.colorMode & ResTable_config.MASK_WIDE_COLOR_GAMUT) != 0 ||
            (config.colorMode & ResTable_config.MASK_HDR) != 0) {
      min_sdk = SDK_O;
    } else if (isTruthy(config.screenLayout2 & ResTable_config.MASK_SCREENROUND)) {
      min_sdk = SDK_MNC;
    } else if (config.density == ResTable_config.DENSITY_ANY) {
      min_sdk = SDK_LOLLIPOP;
    } else if (config.smallestScreenWidthDp != ResTable_config.SCREENWIDTH_ANY
        || config.screenWidthDp != ResTable_config.SCREENWIDTH_ANY
        || config.screenHeightDp != ResTable_config.SCREENHEIGHT_ANY) {
      min_sdk = SDK_HONEYCOMB_MR2;
    } else if ((config.uiMode & ResTable_config.MASK_UI_MODE_TYPE)
        != ResTable_config.UI_MODE_TYPE_ANY
        ||  (config.uiMode & ResTable_config.MASK_UI_MODE_NIGHT)
        != ResTable_config.UI_MODE_NIGHT_ANY) {
      min_sdk = SDK_FROYO;
    } else if ((config.screenLayout & ResTable_config.MASK_SCREENSIZE)
        != ResTable_config.SCREENSIZE_ANY
        ||  (config.screenLayout & ResTable_config.MASK_SCREENLONG)
        != ResTable_config.SCREENLONG_ANY
        || config.density != ResTable_config.DENSITY_DEFAULT) {
      min_sdk = SDK_DONUT;
    }
    if (min_sdk > config.sdkVersion) {
      config.sdkVersion = min_sdk;
    }
  }
}


package org.robolectric.shadows;

import static org.robolectric.util.reflector.Reflector.reflector;

import android.os.Message;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;

/**
 * The shadow API for {@link android.os.Message}.
 *
 *
Different shadow implementations will be used depending on the current {@link LooperMode}. See
 * {@link ShadowLegacyMessage} and {@link ShadowPausedMessage} for details.
 */
@Implements(value = Message.class, shadowPicker = ShadowMessage.Picker.class)
public abstract class ShadowMessage {

  /** The shadow Picker for this class */
  public static class Picker extends LooperShadowPicker<ShadowMessage> {

    public Picker() {
      super(ShadowLegacyMessage.class, ShadowPausedMessage.class);
    }
  }

  /** Exposes the package-private {@link Message#recycleUnchecked()} */
  public abstract void recycleUnchecked();

  /**
   * Stores the {@link Runnable} instance that has been scheduled to invoke this message. This is
   * called when the message is enqueued by {@link ShadowLegacyMessageQueue#enqueueMessage} and is
   * used when the message is recycled to ensure that the correct {@link Runnable} instance is
   * removed from the associated scheduler.
   *
   * @param r the {@link Runnable} instance that is scheduled to trigger this message.
   *
#if ($api >= 21) * @see #recycleUnchecked() #else * @see #recycle() #end
   *
Only supported in {@link LooperMode.Mode.LEGACY}.
   */
  public abstract void setScheduledRunnable(Runnable r);

  /**
   * Convenience method to provide getter access to the private field {@code Message.next}.
   *
   *
Only supported in {@link LooperMode.Mode.LEGACY}
   *
   * @return The next message in the current message chain.
   * @see #setNext(Message)
   */
  public abstract Message getNext();

  /**
   * Convenience method to provide setter access to the private field {@code Message.next}.
   *
   *
Only supported in {@link LooperMode.Mode.LEGACY}
   *
   * @param next the new next message for the current message.
   * @see #getNext()
   */
  public abstract void setNext(Message next);

  /**
   * Resets the static state of the {@link Message} class by
   * emptying the message pool.
   */
  @Resetter
  public static void reset() {
    Object lock = reflector(ReflectorMessage.class).getPoolSync();
    synchronized (lock) {
      reflector(ReflectorMessage.class).setPoolSize(0);
      reflector(ReflectorMessage.class).setPool(null);
    }
  }

  /** Accessor interface for {@link Message}'s internals. */
  @ForType(Message.class)
  private interface ReflectorMessage {
    @Static
    @Accessor("sPool")
    void setPool(Message o);

    @Static
    @Accessor("sPoolSize")
    void setPoolSize(int size);

    @Static
    @Accessor("sPoolSync")
    Object getPoolSync();
  }
}


package org.robolectric.shadows;

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

import android.annotation.TargetApi;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import android.os.Build.VERSION_CODES;
import android.os.Parcel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(AudioManager.class)
public class ShadowAudioManager {
  public static final int MAX_VOLUME_MUSIC_DTMF = 15;
  public static final int DEFAULT_MAX_VOLUME = 7;
  public static final int DEFAULT_VOLUME = 7;
  public static final int INVALID_VOLUME = 0;
  public static final int FLAG_NO_ACTION = 0;
  public static final int[] ALL_STREAMS = {
    AudioManager.STREAM_MUSIC,
    AudioManager.STREAM_ALARM,
    AudioManager.STREAM_NOTIFICATION,
    AudioManager.STREAM_RING,
    AudioManager.STREAM_SYSTEM,
    AudioManager.STREAM_VOICE_CALL,
    AudioManager.STREAM_DTMF
  };

  private AudioFocusRequest lastAudioFocusRequest;
  private int nextResponseValue = AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
  private AudioManager.OnAudioFocusChangeListener lastAbandonedAudioFocusListener;
  private android.media.AudioFocusRequest lastAbandonedAudioFocusRequest;
  private HashMap<Integer, AudioStream> streamStatus = new HashMap<>();
  private List<AudioPlaybackConfiguration> activePlaybackConfigurations = Collections.emptyList();
  private int ringerMode = AudioManager.RINGER_MODE_NORMAL;
  private int mode = AudioManager.MODE_NORMAL;
  private boolean bluetoothA2dpOn;
  private boolean isBluetoothScoOn;
  private boolean isSpeakerphoneOn;
  private boolean isMicrophoneMuted = false;
  private boolean isMusicActive;
  private boolean wiredHeadsetOn;
  private final Map<String, String> parameters = new HashMap<>();
  private final Map<Integer, Boolean> streamsMuteState = new HashMap<>();

  public ShadowAudioManager() {
    for (int stream : ALL_STREAMS) {
      streamStatus.put(stream, new AudioStream(DEFAULT_VOLUME, DEFAULT_MAX_VOLUME, FLAG_NO_ACTION));
    }
    streamStatus.get(AudioManager.STREAM_MUSIC).setMaxVolume(MAX_VOLUME_MUSIC_DTMF);
    streamStatus.get(AudioManager.STREAM_DTMF).setMaxVolume(MAX_VOLUME_MUSIC_DTMF);
  }

  @Implementation
  protected int getStreamMaxVolume(int streamType) {
    AudioStream stream = streamStatus.get(streamType);
    return (stream != null) ? stream.getMaxVolume() : INVALID_VOLUME;
  }

  @Implementation
  protected int getStreamVolume(int streamType) {
    AudioStream stream = streamStatus.get(streamType);
    return (stream != null) ? stream.getCurrentVolume() : INVALID_VOLUME;
  }

  @Implementation
  protected void setStreamVolume(int streamType, int index, int flags) {
    AudioStream stream = streamStatus.get(streamType);
    if (stream != null) {
      stream.setCurrentVolume(index);
      stream.setFlag(flags);
    }
  }

  @Implementation
  protected int requestAudioFocus(
      android.media.AudioManager.OnAudioFocusChangeListener l, int streamType, int durationHint) {
    lastAudioFocusRequest = new AudioFocusRequest(l, streamType, durationHint);
    return nextResponseValue;
  }

  /**
   * Provides a mock like interface for the requestAudioFocus method by storing the request object
   * for later inspection and returning the value specified in setNextFocusRequestResponse.
   */
  @Implementation(minSdk = O)
  protected int requestAudioFocus(android.media.AudioFocusRequest audioFocusRequest) {
    lastAudioFocusRequest = new AudioFocusRequest(audioFocusRequest);
    return nextResponseValue;
  }

  @Implementation
  protected int abandonAudioFocus(AudioManager.OnAudioFocusChangeListener l) {
    lastAbandonedAudioFocusListener = l;
    return nextResponseValue;
  }

  /**
   * Provides a mock like interface for the abandonAudioFocusRequest method by storing the request
   * object for later inspection and returning the value specified in setNextFocusRequestResponse.
   */
  @Implementation(minSdk = O)
  protected int abandonAudioFocusRequest(android.media.AudioFocusRequest audioFocusRequest) {
    lastAbandonedAudioFocusRequest = audioFocusRequest;
    return nextResponseValue;
  }

  @Implementation
  protected int getRingerMode() {
    return ringerMode;
  }

  @Implementation
  protected void setRingerMode(int ringerMode) {
    if (!AudioManager.isValidRingerMode(ringerMode)) {
      return;
    }
    this.ringerMode = ringerMode;
  }

  public static boolean isValidRingerMode(int ringerMode) {
    return ringerMode >= 0
        && ringerMode
            <= (int) ReflectionHelpers.getStaticField(AudioManager.class, "RINGER_MODE_MAX");
  }

  @Implementation
  protected void setMode(int mode) {
    this.mode = mode;
  }

  @Implementation
  protected int getMode() {
    return this.mode;
  }

  public void setStreamMaxVolume(int streamMaxVolume) {
    for (Map.Entry<Integer, AudioStream> entry : streamStatus.entrySet()) {
      entry.getValue().setMaxVolume(streamMaxVolume);
    }
  }

  public void setStreamVolume(int streamVolume) {
    for (Map.Entry<Integer, AudioStream> entry : streamStatus.entrySet()) {
      entry.getValue().setCurrentVolume(streamVolume);
    }
  }

  @Implementation
  protected void setWiredHeadsetOn(boolean on) {
    wiredHeadsetOn = on;
  }

  @Implementation
  protected boolean isWiredHeadsetOn() {
    return wiredHeadsetOn;
  }

  @Implementation
  protected void setBluetoothA2dpOn(boolean on) {
    bluetoothA2dpOn = on;
  }

  @Implementation
  protected boolean isBluetoothA2dpOn() {
    return bluetoothA2dpOn;
  }

  @Implementation
  protected void setSpeakerphoneOn(boolean on) {
    isSpeakerphoneOn = on;
  }

  @Implementation
  protected boolean isSpeakerphoneOn() {
    return isSpeakerphoneOn;
  }

  @Implementation
  protected void setMicrophoneMute(boolean on) {
    isMicrophoneMuted = on;
  }

  @Implementation
  protected boolean isMicrophoneMute() {
    return isMicrophoneMuted;
  }

  @Implementation
  protected boolean isBluetoothScoOn() {
    return isBluetoothScoOn;
  }

  @Implementation
  protected void setBluetoothScoOn(boolean isBluetoothScoOn) {
    this.isBluetoothScoOn = isBluetoothScoOn;
  }

  @Implementation
  protected boolean isMusicActive() {
    return isMusicActive;
  }

  @Implementation(minSdk = O)
  protected List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() {
    return new ArrayList<>(activePlaybackConfigurations);
  }

  @Implementation
  protected void setParameters(String keyValuePairs) {
    if (keyValuePairs.isEmpty()) {
      throw new IllegalArgumentException("keyValuePairs should not be empty");
    }

    if (keyValuePairs.charAt(keyValuePairs.length() - 1) != ';') {
      throw new IllegalArgumentException("keyValuePairs should end with a ';'");
    }

    String[] pairs = keyValuePairs.split(";", 0);

    for (String pair : pairs) {
      if (pair.isEmpty()) {
        continue;
      }

      String[] splittedPair = pair.split("=", 0);
      if (splittedPair.length != 2) {
        throw new IllegalArgumentException(
            "keyValuePairs: each pair should be in the format of key=value;");
      }
      parameters.put(splittedPair[0], splittedPair[1]);
    }
  }

  /**
   * The expected composition for keys is not well defined.
   *
   *
For testing purposes this method call always returns null.
   */
  @Implementation
  protected String getParameters(String keys) {
    return null;
  }

  /** Returns a single parameter that was set via {@link #setParameters(String)}. */
  public String getParameter(String key) {
    return parameters.get(key);
  }

  /**
   * Implements {@link AudioManager#adjustStreamVolume(int, int, int)}.
   *
   *
Currently supports only the directions {@link AudioManager#ADJUST_MUTE} and {@link
   * AudioManager#ADJUST_UNMUTE}.
   */
  @Implementation
  protected void adjustStreamVolume(int streamType, int direction, int flags) {
    switch (direction) {
      case AudioManager.ADJUST_MUTE:
        streamsMuteState.put(streamType, true);
        break;
      case AudioManager.ADJUST_UNMUTE:
        streamsMuteState.put(streamType, false);
        break;
      default:
        break;
    }
  }

  @Implementation(minSdk = M)
  protected boolean isStreamMute(int streamType) {
    if (!streamsMuteState.containsKey(streamType)) {
      return false;
    }
    return streamsMuteState.get(streamType);
  }

  public void setIsStreamMute(int streamType, boolean isMuted) {
    streamsMuteState.put(streamType, isMuted);
  }

  /**
   * Sets active playback configurations that will be served by {@link
   * AudioManager#getActivePlaybackConfigurations}.
   *
   *
Note that there is no public {@link AudioPlaybackConfiguration} constructor, so the
   * configurations returned are specified by their audio attributes only.
   */
  @TargetApi(VERSION_CODES.O)
  public void setActivePlaybackConfigurationsFor(List<AudioAttributes> audioAttributes) {
    activePlaybackConfigurations = new ArrayList<>(audioAttributes.size());
    for (AudioAttributes audioAttribute : audioAttributes) {
      Parcel p = Parcel.obtain();
      p.writeInt(0); // mPlayerIId
      p.writeInt(0); // mPlayerType
      p.writeInt(0); // mClientUid
      p.writeInt(0); // mClientPid
      p.writeInt(AudioPlaybackConfiguration.PLAYER_STATE_STARTED); // mPlayerState
      audioAttribute.writeToParcel(p, 0);
      p.writeStrongInterface(null);
      byte[] bytes = p.marshall();
      p.recycle();
      p = Parcel.obtain();
      p.unmarshall(bytes, 0, bytes.length);
      p.setDataPosition(0);
      AudioPlaybackConfiguration configuration =
          AudioPlaybackConfiguration.CREATOR.createFromParcel(p);
      p.recycle();
      activePlaybackConfigurations.add(configuration);
    }
  }

  public void setIsMusicActive(boolean isMusicActive) {
    this.isMusicActive = isMusicActive;
  }

  public AudioFocusRequest getLastAudioFocusRequest() {
    return lastAudioFocusRequest;
  }

  public void setNextFocusRequestResponse(int nextResponseValue) {
    this.nextResponseValue = nextResponseValue;
  }

  public AudioManager.OnAudioFocusChangeListener getLastAbandonedAudioFocusListener() {
    return lastAbandonedAudioFocusListener;
  }

  public android.media.AudioFocusRequest getLastAbandonedAudioFocusRequest() {
    return lastAbandonedAudioFocusRequest;
  }

  public static class AudioFocusRequest {
    public final AudioManager.OnAudioFocusChangeListener listener;
    public final int streamType;
    public final int durationHint;
    public final android.media.AudioFocusRequest audioFocusRequest;

    private AudioFocusRequest(
        AudioManager.OnAudioFocusChangeListener listener, int streamType, int durationHint) {
      this.listener = listener;
      this.streamType = streamType;
      this.durationHint = durationHint;
      this.audioFocusRequest = null;
    }

    private AudioFocusRequest(android.media.AudioFocusRequest audioFocusRequest) {
      this.listener = null;
      this.streamType = this.durationHint = -1;
      this.audioFocusRequest = audioFocusRequest;
    }
  }

  private static class AudioStream {
    private int currentVolume;
    private int maxVolume;
    private int flag;

    public AudioStream(int currVol, int maxVol, int flag) {
      setCurrentVolume(currVol);
      setMaxVolume(maxVol);
      setFlag(flag);
    }

    public int getCurrentVolume() {
      return currentVolume;
    }

    public int getMaxVolume() {
      return maxVolume;
    }

    public int getFlag() {
      return flag;
    }

    public void setCurrentVolume(int vol) {
      if (vol > maxVolume) {
        vol = maxVolume;
      } else if (vol < 0) {
        vol = 0;
      }
      currentVolume = vol;
    }

    public void setMaxVolume(int vol) {
      maxVolume = vol;
    }

    public void setFlag(int flag) {
      this.flag = flag;
    }
  }
}


package org.robolectric.shadows;

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

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import java.nio.ByteBuffer;
import java.util.HashMap;
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.MediaCodec} which only supports a passthrough asynchronous
 * encoding pipeline.
 *
 * <p>Once the pipeline is started, a format change will be reported, switching to an empty
 * {@link android.media.MediaFormat} with fake codec-specific info. Following this, the
 * implementation will present an input buffer, which will be directly copied to an output buffer
 * once queued, which will be subsequently presented to the callback handler.
 */
@Implements(value = MediaCodec.class, minSdk = JELLY_BEAN)
public class ShadowMediaCodec {
  private static final int BUFFER_SIZE = 512;

  // Must keep in sync with MediaCodec.java
  private static final int EVENT_CALLBACK = 1;
  private static final int CB_INPUT_AVAILABLE = 1;
  private static final int CB_OUTPUT_AVAILABLE = 2;
  private static final int CB_OUTPUT_FORMAT_CHANGE = 4;

  @RealObject private MediaCodec realCodec;
  private MediaCodec.Callback callback;

  private final ByteBuffer[] inputBuffers =
      new ByteBuffer[] {ByteBuffer.wrap(new byte[BUFFER_SIZE])};
  private final ByteBuffer[] outputBuffers =
      new ByteBuffer[] {ByteBuffer.wrap(new byte[BUFFER_SIZE])};

  private boolean reachedEos = false;

  // Member methods.

  /** Saves the callback to allow use inside the shadow. */
  @Implementation(minSdk = LOLLIPOP)
  protected void native_setCallback(MediaCodec.Callback callback) {
    this.callback = callback;
  }

  /**
   * Starts the async encoding process, by first reporting a format change event, and then
   * presenting an input buffer to the callback.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected void native_start() {
    // Currently only support async-mode.
    if (callback == null) {
      return;
    }

    // Report the format as changed, to simulate adding codec specific info before making input
    // buffers available.
    HashMap<String, Object> format = new HashMap<>();
    format.put("csd-0", ByteBuffer.wrap(new byte[]{0x13, 0x10}));
    format.put("csd-1", ByteBuffer.wrap(new byte[0]));
    postFakeNativeEvent(EVENT_CALLBACK, CB_OUTPUT_FORMAT_CHANGE, 0, format);

    // Reset state.
    reachedEos = false;

    makeInputBufferAvailable(0);
  }

  /** Returns the shadow buffers used for input or output. */
  @Implementation
  protected ByteBuffer[] getBuffers(boolean input) {
    return input ? inputBuffers : outputBuffers;
  }

  /** Returns the input or output buffer corresponding to the given index, or null if invalid. */
  @Implementation(minSdk = LOLLIPOP)
  protected ByteBuffer getBuffer(boolean input, int index) {
    ByteBuffer[] buffers = input ? inputBuffers : outputBuffers;

    return (index >= 0 && index < buffers.length) ? buffers[index] : null;
  }

  /**
   * Triggers presentation of the corresponding output buffer for the given input buffer, and passes
   * the given metadata as buffer info.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected void native_queueInputBuffer(
      int index, int offset, int size, long presentationTimeUs, int flags) {
    // Check if this should be the last buffer cycle.
    if ((flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
      reachedEos = true;
    }

    BufferInfo info = new BufferInfo();
    info.set(offset, size, presentationTimeUs, flags);

    makeOutputBufferAvailable(index, info);
  }

  /**
   * Triggers presentation of the corresponding input buffer for the given output buffer, if end of
   * stream has not yet been signaled.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected void releaseOutputBuffer(int index, boolean render, boolean updatePTS, long timeNs) {
    if (reachedEos) {
      return;
    }

    makeInputBufferAvailable(index);
  }

  private void makeInputBufferAvailable(int index) {
    if (index < 0 || index >= inputBuffers.length) {
      throw new IndexOutOfBoundsException("Cannot make non-existent input available.");
    }

    // Reset the input buffer.
    inputBuffers[index].clear();

    // Signal input buffer availability.
    postFakeNativeEvent(EVENT_CALLBACK, CB_INPUT_AVAILABLE, index, null);
  }

  private void makeOutputBufferAvailable(int index, BufferInfo info) {
    if (index < 0 || index >= outputBuffers.length) {
      throw new IndexOutOfBoundsException("Cannot make non-existent output buffer available.");
    }

    // Reset the output buffer.
    outputBuffers[index].clear();

    // Straight copy input -> output.
    inputBuffers[index].rewind();
    outputBuffers[index].put(inputBuffers[index]);
    outputBuffers[index].rewind();

    // Signal output buffer availability.
    postFakeNativeEvent(EVENT_CALLBACK, CB_OUTPUT_AVAILABLE, index, info);
  }

  private void postFakeNativeEvent(int what, int arg1, int arg2, @Nullable Object obj) {
    ReflectionHelpers.callInstanceMethod(
        MediaCodec.class,
        realCodec,
        "postEventFromNative",
        ReflectionHelpers.ClassParameter.from(int.class, what),
        ReflectionHelpers.ClassParameter.from(int.class, arg1),
        ReflectionHelpers.ClassParameter.from(int.class, arg2),
        ReflectionHelpers.ClassParameter.from(Object.class, obj));
  }

  /** Prevents calling Android-only methods on basic ByteBuffer objects. */
  @Implementation(minSdk = LOLLIPOP)
  protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}

  /** Prevents calling Android-only methods on basic ByteBuffer objects. */
  @Implementation(minSdk = LOLLIPOP)
  protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}

  /** Prevents calling Android-only methods on basic ByteBuffer objects. */
  @Implementation(minSdk = LOLLIPOP)
  protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}

  /**
   * Prevents calling Android-only methods on basic ByteBuffer objects. Replicates existing behavior
   * adjusting buffer positions and limits.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected void validateOutputByteBuffer(
      @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
    if (buffers != null && index >= 0 && index < buffers.length) {
      ByteBuffer buffer = buffers[index];
      if (buffer != null) {
        buffer.limit(info.offset + info.size).position(info.offset);
      }
    }
  }

  /** Prevents calling Android-only methods on basic ByteBuffer objects. */
  @Implementation(minSdk = LOLLIPOP)
  protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {}

  /** Prevents attempting to free non-direct ByteBuffer objects. */
  @Implementation(minSdk = LOLLIPOP)
  protected void freeByteBuffer(@Nullable ByteBuffer buffer) {}

  /** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */
  @Implements(className = "android.media.MediaCodec$BufferMap$CodecBuffer", minSdk = LOLLIPOP)
  protected static class ShadowCodecBuffer {

    // Seems to be required to work.
    public ShadowCodecBuffer() {}

    // Seems to be required to work.
    @Implementation
    protected void __constructor__() {}

    /** Prevents attempting to free non-direct ByteBuffer objects. */
    @Implementation
    protected void free() {}
  }
}


package org.robolectric.res.android;

// transliterated from
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/include/androidfw/ResourceUtils.h
// and https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/ResourceUtils.cpp
class ResourceUtils {
  // Extracts the package, type, and name from a string of the format: [[package:]type/]name
  // Validation must be performed on each extracted piece.
  // Returns false if there was a syntax error.
//  boolean ExtractResourceName(String& str, String* out_package, String* out_type,
//                           String* out_entry);

  static int fix_package_id(int resid, int package_id) {
    return (resid & 0x00ffffff) | (package_id << 24);
  }

  static int get_package_id(int resid) {
//    return static_cast<int>((resid >> 24) & 0x000000ff);
    return resid >>> 24;
  }

  // The type ID is 1-based, so if the returned value is 0 it is invalid.
  static int get_type_id(int resid) {
//    return static_cast<int>((resid >> 16) & 0x000000ff);
    return (resid & 0x00FF0000) >>> 16;
  }

  static int get_entry_id(int resid) {
//    return static_cast<uint16_t>(resid & 0x0000ffff);
    return (short) (resid & 0x0000FFFF);
  }

  static boolean is_internal_resid(int resid) {
    return (resid & 0xffff0000) != 0 && (resid & 0x00ff0000) == 0;
  }

  static boolean is_valid_resid(int resid) {
    return (resid & 0x00ff0000) != 0 && (resid & 0xff000000) != 0;
  }

  static int make_resid(byte package_id, byte type_id, short entry_id) {
//    return (static_cast<int>(package_id) << 24) | (static_cast<int>(type_id) << 16) |
//        entry_id;
    return package_id << 24 | type_id << 16 | entry_id;
  }

//   bool ExtractResourceName(const StringPiece& str, StringPiece* out_package, StringPiece* out_type,
//                          StringPiece* out_entry) {
//   *out_package = "";
//   *out_type = "";
//   bool has_package_separator = false;
//   bool has_type_separator = false;
//   const char* start = str.data();
//   const char* end = start + str.size();
//   const char* current = start;
//   while (current != end) {
//     if (out_type->size() == 0 && *current == '/') {
//       has_type_separator = true;
//       out_type->assign(start, current - start);
//       start = current + 1;
//     } else if (out_package->size() == 0 && *current == ':') {
//       has_package_separator = true;
//       out_package->assign(start, current - start);
//       start = current + 1;
//     }
//     current++;
//   }
//   out_entry->assign(start, end - start);
//
//   return !(has_package_separator && out_package->empty()) &&
//          !(has_type_separator && out_type->empty());
// }

  static boolean ExtractResourceName(String str, Ref<String> out_package, Ref<String> out_type,
                           final Ref<String> out_entry) {
    out_package.set("");
    out_type.set("");
    boolean has_package_separator = false;
    boolean has_type_separator = false;
    int start = 0;
    int end = start + str.length();
    int current = start;
    while (current != end) {
      if (out_type.get().length() == 0 && str.charAt(current) == '/') {
        has_type_separator = true;
        out_type.set(str.substring(start, current));
        start = current + 1;
      } else if (out_package.get().length() == 0 && str.charAt(current) == ':') {
        has_package_separator = true;
        out_package.set(str.substring(start, current));
        start = current + 1;
      }
      current++;
    }
    out_entry.set(str.substring(start, end));

    return !(has_package_separator && out_package.get().isEmpty()) &&
           !(has_type_separator && out_type.get().isEmpty());
  }

}


package org.robolectric.shadows;

import android.annotation.NonNull;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.app.usage.UsageEvents;
import android.app.usage.UsageEvents.Event;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.app.usage.UsageStatsManager.UsageSource;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Parcel;
import android.util.ArraySet;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Range;
import com.google.common.collect.SetMultimap;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.concurrent.TimeUnit;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;

/** Shadow of {@link UsageStatsManager}. */
@Implements(value = UsageStatsManager.class, minSdk = Build.VERSION_CODES.LOLLIPOP)
public class ShadowUsageStatsManager {
  private static @StandbyBuckets int currentAppStandbyBucket =
      UsageStatsManager.STANDBY_BUCKET_ACTIVE;

  @UsageSource
  private static int currentUsageSource = UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY;

  private static final NavigableMap<Long, Event> eventsByTimeStamp =
      Maps.synchronizedNavigableMap(Maps.newTreeMap());

  /**
   * Keys {@link UsageStats} objects by intervalType (e.g. {@link
   * UsageStatsManager#INTERVAL_WEEKLY}).
   */
  private SetMultimap<Integer, UsageStats> usageStatsByIntervalType =
      Multimaps.synchronizedSetMultimap(HashMultimap.create());

  private static final Map<String, Integer> appStandbyBuckets = Maps.newConcurrentMap();

  /**
   * App usage observer registered via {@link UsageStatsManager#registerAppUsageObserver(int,
   * String[], long, TimeUnit, PendingIntent)}.
   */
  public static final class AppUsageObserver {
    private final int observerId;
    private final Collection<String> packageNames;
    private final long timeLimit;
    private final TimeUnit timeUnit;
    private final PendingIntent callbackIntent;

    public AppUsageObserver(
        int observerId,
        @NonNull Collection<String> packageNames,
        long timeLimit,
        @NonNull TimeUnit timeUnit,
        @NonNull PendingIntent callbackIntent) {
      this.observerId = observerId;
      this.packageNames = packageNames;
      this.timeLimit = timeLimit;
      this.timeUnit = timeUnit;
      this.callbackIntent = callbackIntent;
    }

    public int getObserverId() {
      return observerId;
    }

    @NonNull
    public Collection<String> getPackageNames() {
      return packageNames;
    }

    public long getTimeLimit() {
      return timeLimit;
    }

    @NonNull
    public TimeUnit getTimeUnit() {
      return timeUnit;
    }

    @NonNull
    public PendingIntent getCallbackIntent() {
      return callbackIntent;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      AppUsageObserver that = (AppUsageObserver) o;
      return observerId == that.observerId
          && timeLimit == that.timeLimit
          && packageNames.equals(that.packageNames)
          && timeUnit == that.timeUnit
          && callbackIntent.equals(that.callbackIntent);
    }

    @Override
    public int hashCode() {
      int result = observerId;
      result = 31 * result + packageNames.hashCode();
      result = 31 * result + (int) (timeLimit ^ (timeLimit >>> 32));
      result = 31 * result + timeUnit.hashCode();
      result = 31 * result + callbackIntent.hashCode();
      return result;
    }
  }

  private static final Map<Integer, AppUsageObserver> appUsageObserversById =
      Maps.newConcurrentMap();

  /**
   * Usage session observer registered via {@link
   * UsageStatsManager#registerUsageSessionObserver(int, String[], long, TimeUnit, long, TimeUnit,
   * PendingIntent, PendingIntent)}.
   */
  public static final class UsageSessionObserver {
    private final int observerId;
    private final List<String> packageNames;
    private final Duration sessionStepDuration;
    private final Duration thresholdDuration;
    private final PendingIntent sessionStepTriggeredIntent;
    private final PendingIntent sessionEndedIntent;

    public UsageSessionObserver(
        int observerId,
        @NonNull List<String> packageNames,
        Duration sessionStepDuration,
        Duration thresholdDuration,
        @NonNull PendingIntent sessionStepTriggeredIntent,
        @NonNull PendingIntent sessionEndedIntent) {
      this.observerId = observerId;
      this.packageNames = packageNames;
      this.sessionStepDuration = sessionStepDuration;
      this.thresholdDuration = thresholdDuration;
      this.sessionStepTriggeredIntent = sessionStepTriggeredIntent;
      this.sessionEndedIntent = sessionEndedIntent;
    }

    public int getObserverId() {
      return observerId;
    }

    @NonNull
    public List<String> getPackageNames() {
      return packageNames;
    }

    public Duration getSessionStepDuration() {
      return sessionStepDuration;
    }

    public Duration getThresholdDuration() {
      return thresholdDuration;
    }

    @NonNull
    public PendingIntent getSessionStepTriggeredIntent() {
      return sessionStepTriggeredIntent;
    }

    @NonNull
    public PendingIntent getSessionEndedIntent() {
      return sessionEndedIntent;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      UsageSessionObserver that = (UsageSessionObserver) o;
      return observerId == that.observerId
          && packageNames.equals(that.packageNames)
          && sessionStepDuration.equals(that.sessionStepDuration)
          && thresholdDuration.equals(that.thresholdDuration)
          && sessionStepTriggeredIntent.equals(that.sessionStepTriggeredIntent)
          && sessionEndedIntent.equals(that.sessionEndedIntent);
    }

    @Override
    public int hashCode() {
      int result = observerId;
      result = 31 * result + packageNames.hashCode();
      result = 31 * result + sessionStepDuration.hashCode();
      result = 31 * result + thresholdDuration.hashCode();
      result = 31 * result + sessionStepTriggeredIntent.hashCode();
      result = 31 * result + sessionEndedIntent.hashCode();
      return result;
    }
  }

  protected static final Map<Integer, UsageSessionObserver> usageSessionObserversById =
      new LinkedHashMap<>();

  @Implementation
  protected UsageEvents queryEvents(long beginTime, long endTime) {
    List<Event> results =
        ImmutableList.copyOf(eventsByTimeStamp.subMap(beginTime, endTime).values());

    ArraySet<String> names = new ArraySet<>();
    for (Event result : results) {
      names.add(result.mPackage);
      if (result.mClass != null) {
        names.add(result.mClass);
      }
    }

    String[] table = names.toArray(new String[0]);
    Arrays.sort(table);

    // We can't directly construct usable UsageEvents, so we replicate what the framework does:
    // First the system marshalls the usage events into a Parcel.
    UsageEvents usageEvents = new UsageEvents(results, table);
    Parcel parcel = Parcel.obtain();
    usageEvents.writeToParcel(parcel, 0);
    // Then the app unmarshalls the usage events from the Parcel.
    parcel.setDataPosition(0);
    return new UsageEvents(parcel);
  }

  /**
   * Adds an event to be returned by {@link UsageStatsManager#queryEvents}.
   *
   *
This method won't affect the results of {@link #queryUsageStats} method.
   *
   * @deprecated Use {@link #addEvent(Event)} and {@link EventBuilder} instead.
   */
  @Deprecated
  public void addEvent(String packageName, long timeStamp, int eventType) {
    EventBuilder eventBuilder =
        EventBuilder.buildEvent()
            .setPackage(packageName)
            .setTimeStamp(timeStamp)
            .setEventType(eventType);
    if (eventType == Event.CONFIGURATION_CHANGE) {
      eventBuilder.setConfiguration(new Configuration());
    }
    addEvent(eventBuilder.build());
  }

  /**
   * Adds an event to be returned by {@link UsageStatsManager#queryEvents}.
   *
   *
This method won't affect the results of {@link #queryUsageStats} method.
   *
   *
The {@link Event} can be built by {@link EventBuilder}.
   */
  public void addEvent(Event event) {
    eventsByTimeStamp.put(event.getTimeStamp(), event);
  }

  /**
   * Simulates the operations done by the framework when there is a time change. If the time is
   * changed, the timestamps of all existing usage events will be shifted by the same offset as the
   * time change, in order to make sure they remain stable relative to the new time.
   *
   *
This method won't affect the results of {@link #queryUsageStats} method.
   *
   * @param offsetToAddInMillis the offset to be applied to all events. For example, if {@code
   *     offsetInMillis} is 60,000, then all {@link Event}s will be shifted forward by 1 minute
   *     (into the future). Likewise, if {@code offsetInMillis} is -60,000, then all {@link Event}s
   *     will be shifted backward by 1 minute (into the past).
   */
  public void simulateTimeChange(long offsetToAddInMillis) {
    ImmutableMap.Builder<Long, Event> eventMapBuilder = ImmutableMap.builder();
    for (Event event : eventsByTimeStamp.values()) {
      long newTimestamp = event.getTimeStamp() + offsetToAddInMillis;
      eventMapBuilder.put(
          newTimestamp, EventBuilder.fromEvent(event).setTimeStamp(newTimestamp).build());
    }
    eventsByTimeStamp.putAll(eventMapBuilder.build());
  }

  /**
   * Returns aggregated UsageStats added by calling {@link #addUsageStats}.
   *
   *
The real implementation creates these aggregated objects from individual {@link Event}. This
   * aggregation logic is nontrivial, so the shadow implementation just returns the aggregate data
   * added using {@link #addUsageStats}.
   */
  @Implementation
  protected List<UsageStats> queryUsageStats(int intervalType, long beginTime, long endTime) {
    List<UsageStats> results = new ArrayList<>();
    Range<Long> queryRange = Range.closed(beginTime, endTime);
    for (UsageStats stats : usageStatsByIntervalType.get(intervalType)) {
      Range<Long> statsRange = Range.closed(stats.getFirstTimeStamp(), stats.getLastTimeStamp());
      if (queryRange.isConnected(statsRange)) {
        results.add(stats);
      }
    }
    return results;
  }

  /**
   * Adds an aggregated {@code UsageStats} object, to be returned by {@link #queryUsageStats}.
   * Construct these objects with {@link UsageStatsBuilder}, and set the firstTimestamp and
   * lastTimestamp fields to make time filtering work in {@link #queryUsageStats}.
   *
   * @param intervalType An interval type constant, e.g. {@link UsageStatsManager#INTERVAL_WEEKLY}.
   */
  public void addUsageStats(int intervalType, UsageStats stats) {
    usageStatsByIntervalType.put(intervalType, stats);
  }

  /**
   * Returns the current standby bucket of the specified app that is set by {@code
   * setAppStandbyBucket}. If the standby bucket value has never been set, return {@link
   * UsageStatsManager.STANDBY_BUCKET_ACTIVE}.
   */
  @Implementation(minSdk = Build.VERSION_CODES.P)
  @HiddenApi
  public @StandbyBuckets int getAppStandbyBucket(String packageName) {
    Integer bucket = appStandbyBuckets.get(packageName);
    return (bucket == null) ? UsageStatsManager.STANDBY_BUCKET_ACTIVE : bucket;
  }

  @Implementation(minSdk = Build.VERSION_CODES.P)
  @HiddenApi
  public Map<String, Integer> getAppStandbyBuckets() {
    return new HashMap<>(appStandbyBuckets);
  }

  /** Sets the standby bucket of the specified app. */
  @Implementation(minSdk = Build.VERSION_CODES.P)
  @HiddenApi
  public void setAppStandbyBucket(String packageName, @StandbyBuckets int bucket) {
    appStandbyBuckets.put(packageName, bucket);
  }

  @Implementation(minSdk = Build.VERSION_CODES.P)
  @HiddenApi
  public void setAppStandbyBuckets(Map<String, Integer> appBuckets) {
    appStandbyBuckets.putAll(appBuckets);
  }

  @Implementation(minSdk = Build.VERSION_CODES.P)
  @HiddenApi
  protected void registerAppUsageObserver(
      int observerId,
      String[] packages,
      long timeLimit,
      TimeUnit timeUnit,
      PendingIntent callbackIntent) {
    appUsageObserversById.put(
        observerId,
        new AppUsageObserver(
            observerId, ImmutableList.copyOf(packages), timeLimit, timeUnit, callbackIntent));
  }

  @Implementation(minSdk = Build.VERSION_CODES.P)
  @HiddenApi
  protected void unregisterAppUsageObserver(int observerId) {
    appUsageObserversById.remove(observerId);
  }

  /** Returns the {@link AppUsageObserver}s currently registered in {@link UsageStatsManager}. */
  public Collection<AppUsageObserver> getRegisteredAppUsageObservers() {
    return ImmutableList.copyOf(appUsageObserversById.values());
  }

  /**
   * Triggers a currently registered {@link AppUsageObserver} with {@code observerId}.
   *
   *
The observer will be no longer registered afterwards.
   */
  public void triggerRegisteredAppUsageObserver(int observerId, long timeUsedInMillis) {
    AppUsageObserver observer = appUsageObserversById.remove(observerId);
    long timeLimitInMillis = observer.timeUnit.toMillis(observer.timeLimit);
    Intent intent =
        new Intent()
            .putExtra(UsageStatsManager.EXTRA_OBSERVER_ID, observerId)
            .putExtra(UsageStatsManager.EXTRA_TIME_LIMIT, timeLimitInMillis)
            .putExtra(UsageStatsManager.EXTRA_TIME_USED, timeUsedInMillis);
    try {
      observer.callbackIntent.send(RuntimeEnvironment.application, 0, intent);
    } catch (CanceledException e) {
      throw new RuntimeException(e);
    }
  }

  @Implementation(minSdk = Build.VERSION_CODES.Q)
  protected void registerUsageSessionObserver(
      int observerId,
      String[] packages,
      long sessionStepTime,
      TimeUnit sessionStepTimeUnit,
      long thresholdTime,
      TimeUnit thresholdTimeUnit,
      PendingIntent sessionStepTriggeredIntent,
      PendingIntent sessionEndedIntent) {
    usageSessionObserversById.put(
        observerId,
        new UsageSessionObserver(
            observerId,
            ImmutableList.copyOf(packages),
            Duration.ofMillis(sessionStepTimeUnit.toMillis(sessionStepTime)),
            Duration.ofMillis(thresholdTimeUnit.toMillis(thresholdTime)),
            sessionStepTriggeredIntent,
            sessionEndedIntent));
  }

  @Implementation(minSdk = Build.VERSION_CODES.Q)
  protected void unregisterUsageSessionObserver(int observerId) {
    usageSessionObserversById.remove(observerId);
  }

  /**
   * Returns the {@link UsageSessionObserver}s currently registered in {@link UsageStatsManager}.
   */
  public List<UsageSessionObserver> getRegisteredUsageSessionObservers() {
    return ImmutableList.copyOf(usageSessionObserversById.values());
  }

  /**
   * Triggers a currently registered {@link UsageSessionObserver} with {@code observerId}.
   *
   *
The observer SHOULD be registered afterwards.
   */
  public void triggerRegisteredSessionStepObserver(int observerId, long timeUsedInMillis) {
    UsageSessionObserver observer = usageSessionObserversById.get(observerId);
    long sessionStepTimeInMillis = observer.sessionStepDuration.toMillis();
    Intent intent =
        new Intent()
            .putExtra(UsageStatsManager.EXTRA_OBSERVER_ID, observerId)
            .putExtra(UsageStatsManager.EXTRA_TIME_LIMIT, sessionStepTimeInMillis)
            .putExtra(UsageStatsManager.EXTRA_TIME_USED, timeUsedInMillis);
    try {
      observer.sessionStepTriggeredIntent.send(RuntimeEnvironment.application, 0, intent);
    } catch (CanceledException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Triggers a currently registered {@link UsageSessionObserver} with {@code observerId}.
   *
   *
The observer SHOULD be registered afterwards.
   */
  public void triggerRegisteredSessionEndedObserver(int observerId) {
    UsageSessionObserver observer = usageSessionObserversById.get(observerId);
    Intent intent = new Intent().putExtra(UsageStatsManager.EXTRA_OBSERVER_ID, observerId);
    try {
      observer.sessionEndedIntent.send(RuntimeEnvironment.application, 0, intent);
    } catch (CanceledException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns the current app's standby bucket that is set by {@code setCurrentAppStandbyBucket}. If
   * the standby bucket value has never been set, return {@link
   * UsageStatsManager.STANDBY_BUCKET_ACTIVE}.
   */
  @Implementation(minSdk = Build.VERSION_CODES.P)
  @StandbyBuckets
  protected int getAppStandbyBucket() {
    return currentAppStandbyBucket;
  }

  /** Sets the current app's standby bucket */
  public void setCurrentAppStandbyBucket(@StandbyBuckets int bucket) {
    currentAppStandbyBucket = bucket;
  }

  @Implementation(minSdk = Build.VERSION_CODES.Q)
  @UsageSource
  @HiddenApi
  protected int getUsageSource() {
    return currentUsageSource;
  }

  /** Sets what app usage observers will consider the source of usage for an activity. */
  @TargetApi(Build.VERSION_CODES.Q)
  public void setUsageSource(@UsageSource int usageSource) {
    currentUsageSource = usageSource;
  }

  @Resetter
  public static void reset() {
    currentAppStandbyBucket = UsageStatsManager.STANDBY_BUCKET_ACTIVE;
    currentUsageSource = UsageStatsManager.USAGE_SOURCE_TASK_ROOT_ACTIVITY;
    eventsByTimeStamp.clear();

    appStandbyBuckets.clear();
    appUsageObserversById.clear();
    usageSessionObserversById.clear();
  }

  /**
   * Builder for constructing {@link UsageStats} objects. The constructor of UsageStats is not part
   * of the Android API.
   */
  public static class UsageStatsBuilder {
    private UsageStats usageStats = new UsageStats();

    // Use {@link #newBuilder} to construct builders.
    private UsageStatsBuilder() {}

    public static UsageStatsBuilder newBuilder() {
      return new UsageStatsBuilder();
    }

    public UsageStats build() {
      return usageStats;
    }

    public UsageStatsBuilder setPackageName(String packageName) {
      usageStats.mPackageName = packageName;
      return this;
    }

    public UsageStatsBuilder setFirstTimeStamp(long firstTimeStamp) {
      usageStats.mBeginTimeStamp = firstTimeStamp;
      return this;
    }

    public UsageStatsBuilder setLastTimeStamp(long lastTimeStamp) {
      usageStats.mEndTimeStamp = lastTimeStamp;
      return this;
    }

    public UsageStatsBuilder setTotalTimeInForeground(long totalTimeInForeground) {
      usageStats.mTotalTimeInForeground = totalTimeInForeground;
      return this;
    }

    public UsageStatsBuilder setLastTimeUsed(long lastTimeUsed) {
      usageStats.mLastTimeUsed = lastTimeUsed;
      return this;
    }
  }

  /**
   * Builder for constructing {@link Event} objects. The fields of Event are not part of the Android
   * API.
   */
  public static class EventBuilder {
    private Event event = new Event();

    private EventBuilder() {}

    public static EventBuilder fromEvent(Event event) {
      EventBuilder eventBuilder =
          new EventBuilder()
              .setPackage(event.mPackage)
              .setClass(event.mClass)
              .setTimeStamp(event.mTimeStamp)
              .setEventType(event.mEventType)
              .setConfiguration(event.mConfiguration);
      if (event.mEventType == Event.CONFIGURATION_CHANGE) {
        eventBuilder.setConfiguration(new Configuration());
      }
      return eventBuilder;
    }

    public static EventBuilder buildEvent() {
      return new EventBuilder();
    }

    public Event build() {
      return event;
    }

    public EventBuilder setPackage(String packageName) {
      event.mPackage = packageName;
      return this;
    }

    public EventBuilder setClass(String className) {
      event.mClass = className;
      return this;
    }

    public EventBuilder setTimeStamp(long timeStamp) {
      event.mTimeStamp = timeStamp;
      return this;
    }

    public EventBuilder setEventType(int eventType) {
      event.mEventType = eventType;
      return this;
    }

    public EventBuilder setConfiguration(Configuration configuration) {
      event.mConfiguration = configuration;
      return this;
    }

    public EventBuilder setShortcutId(String shortcutId) {
      event.mShortcutId = shortcutId;
      return this;
    }

    @TargetApi(Build.VERSION_CODES.Q)
    public EventBuilder setInstanceId(int instanceId) {
      event.mInstanceId = instanceId;
      return this;
    }

    @TargetApi(Build.VERSION_CODES.Q)
    public EventBuilder setTaskRootPackage(String taskRootPackage) {
      event.mTaskRootPackage = taskRootPackage;
      return this;
    }

    @TargetApi(Build.VERSION_CODES.Q)
    public EventBuilder setTaskRootClass(String taskRootClass) {
      event.mTaskRootClass = taskRootClass;
      return this;
    }
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.KITKAT;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import java.util.ArrayList;
import java.util.List;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

/** Shadow for {@link BluetoothHeadset} */
@Implements(value = BluetoothHeadset.class)
public class ShadowBluetoothHeadset {
  private final List<BluetoothDevice> connectedDevices = new ArrayList<>();
  private boolean allowsSendVendorSpecificResultCode = true;

  /**
   * Overrides behavior of {@link getConnectedDevices}. Returns list of devices that is set up by
   * call(s) to {@link ShadowBluetoothHeadset#addConnectedDevice}. Returns an empty list by default.
   */
  @Implementation
  protected List<BluetoothDevice> getConnectedDevices() {
    return connectedDevices;
  }

  /** Adds the given BluetoothDevice to the shadow's list of "connected devices" */
  public void addConnectedDevice(BluetoothDevice device) {
    connectedDevices.add(device);
  }

  /**
   * Overrides behavior of {@link getConnectionState}.
   *
   * @return {@code BluetoothProfile.STATE_CONNECTED} if the given device has been previously added
   *     by a call to {@link ShadowBluetoothHeadset#addConnectedDevice}, and {@code
   *     BluetoothProfile.STATE_DISCONNECTED} otherwise.
   */
  @Implementation
  protected int getConnectionState(BluetoothDevice device) {
    return connectedDevices.contains(device)
        ? BluetoothProfile.STATE_CONNECTED
        : BluetoothProfile.STATE_DISCONNECTED;
  }

  /**
   * Overrides behavior of {@link sendVendorSpecificResultCode}.
   *
   * @return 'true' only if the given device has been previously added by a call to {@link
   *     ShadowBluetoothHeadset#addConnectedDevice} and {@link
   *     ShadowBluetoothHeadset#setAllowsSendVendorSpecificResultCode} has not been called with
   *     'false' argument.
   * @throws IllegalArgumentException if 'command' argument is null, per Android API
   */
  @Implementation(minSdk = KITKAT)
  protected boolean sendVendorSpecificResultCode(
      BluetoothDevice device, String command, String arg) {
    if (command == null) {
      throw new IllegalArgumentException("Command cannot be null");
    }
    return allowsSendVendorSpecificResultCode && connectedDevices.contains(device);
  }

  /**
   * Affects the behavior of {@link BluetoothHeadset#sendVendorSpecificResultCode}
   *
   * @param allowsSendVendorSpecificResultCode can be set to 'false' to simulate the situation where
   *     the system is unable to send vendor-specific result codes to a device
   */
  public void setAllowsSendVendorSpecificResultCode(boolean allowsSendVendorSpecificResultCode) {
    this.allowsSendVendorSpecificResultCode = allowsSendVendorSpecificResultCode;
  }
}