package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;

import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpChangedListener;
import android.app.AppOpsManager.OpEntry;
import android.app.AppOpsManager.PackageOps;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.media.AudioAttributes.AttributeUsage;
import android.os.Binder;
import android.os.Build;
import com.android.internal.app.IAppOpsService;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

@Implements(value = AppOpsManager.class)
public class ShadowAppOpsManager {

  // OpEntry fields that the shadow doesn't currently allow the test to configure.
  private static final long OP_TIME = 1400000000L;
  private static final long REJECT_TIME = 0L;
  private static final int DURATION = 10;
  private static final int PROXY_UID = 0;
  private static final String PROXY_PACKAGE = "";

  @RealObject private AppOpsManager realObject;

  // Recorded operations, keyed by "uid|packageName"
  private Multimap<String, Integer> mStoredOps = HashMultimap.create();
  // "uid|packageName|opCode" => opMode
  private Map<String, Integer> appModeMap = new HashMap<>();

  // "packageName|opCode" => listener
  private BiMap<String, OnOpChangedListener> appOpListeners = HashBiMap.create();

  // op | (usage << 8) => ModeAndExcpetion
  private Map<Integer, ModeAndException> audioRestrictions = new HashMap<>();

  private Context context;

  @Implementation(minSdk = KITKAT)
  protected void __constructor__(Context context, IAppOpsService service) {
    this.context = context;
    invokeConstructor(
        AppOpsManager.class,
        realObject,
        ClassParameter.from(Context.class, context),
        ClassParameter.from(IAppOpsService.class, service));
  }

  /**
   * Change the operating mode for the given op in the given app package. You must pass in both the
   * uid and name of the application whose mode is being modified; if these do not match, the
   * modification will not be applied.
   *
   *
This method is public for testing {@link #checkOpNoThrow}. If {@link #checkOpNoThrow} is
   * called afterwards with the {@code op}, {@code ui}, and {@code packageName} provided, it will
   * return the {@code mode} set here.
   *
   * @param op The operation to modify. One of the OPSTR_* constants.
   * @param uid The user id of the application whose mode will be changed.
   * @param packageName The name of the application package name whose mode will be changed.
   */
  @Implementation(minSdk = P)
  @HiddenApi
  @SystemApi
  @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES)
  public void setMode(String op, int uid, String packageName, int mode) {
    setMode(AppOpsManager.strOpToOp(op), uid, packageName, mode);
  }

  /**
   * Int version of {@link #setMode(String, int, String, int)}.
   *
   *
This method is public for testing {@link #checkOpNoThrow}. If {@link #checkOpNoThrow} is *
   * called afterwards with the {@code op}, {@code ui}, and {@code packageName} provided, it will *
   * return the {@code mode} set here.
   */
  @Implementation(minSdk = KITKAT)
  @HiddenApi
  @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES)
  public void setMode(int op, int uid, String packageName, int mode) {
    Integer oldMode = appModeMap.put(getOpMapKey(uid, packageName, op), mode);
    OnOpChangedListener listener = appOpListeners.get(getListenerKey(op, packageName));
    if (listener != null && !Objects.equals(oldMode, mode)) {
      String[] sOpToString = ReflectionHelpers.getStaticField(AppOpsManager.class, "sOpToString");
      listener.onOpChanged(sOpToString[op], packageName);
    }
  }

  @Implementation(minSdk = Q)
  public int unsafeCheckOpNoThrow(String op, int uid, String packageName) {
    return checkOpNoThrow(AppOpsManager.strOpToOp(op), uid, packageName);
  }

  @Implementation(minSdk = P)
  @Deprecated // renamed to unsafeCheckOpNoThrow
  protected int checkOpNoThrow(String op, int uid, String packageName) {
    return checkOpNoThrow(AppOpsManager.strOpToOp(op), uid, packageName);
  }

  /**
   * Like {@link AppOpsManager#checkOp} but instead of throwing a {@link SecurityException} it
   * returns {@link AppOpsManager#MODE_ERRORED}.
   *
   *
Made public for testing {@link #setMode} as the method is {@coe @hide}.
   */
  @Implementation(minSdk = KITKAT)
  @HiddenApi
  public int checkOpNoThrow(int op, int uid, String packageName) {
    Integer mode = appModeMap.get(getOpMapKey(uid, packageName, op));
    if (mode == null) {
      return AppOpsManager.MODE_ALLOWED;
    }
    return mode;
  }

  @Implementation(minSdk = KITKAT)
  public int noteOp(int op, int uid, String packageName) {
    mStoredOps.put(getInternalKey(uid, packageName), op);

    // Permission check not currently implemented in this shadow.
    return AppOpsManager.MODE_ALLOWED;
  }

  @Implementation(minSdk = KITKAT)
  protected int noteOpNoThrow(int op, int uid, String packageName) {
    mStoredOps.put(getInternalKey(uid, packageName), op);
    return checkOpNoThrow(op, uid, packageName);
  }

  @Implementation(minSdk = M)
  @HiddenApi
  protected int noteProxyOpNoThrow(int op, String proxiedPackageName) {
    mStoredOps.put(getInternalKey(Binder.getCallingUid(), proxiedPackageName), op);
    return checkOpNoThrow(op, Binder.getCallingUid(), proxiedPackageName);
  }

  @Implementation(minSdk = KITKAT)
  @HiddenApi
  public List<PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) {
    Set<Integer> opFilter = new HashSet<>();
    if (ops != null) {
      for (int op : ops) {
        opFilter.add(op);
      }
    }

    List<OpEntry> opEntries = new ArrayList<>();
    for (Integer op : mStoredOps.get(getInternalKey(uid, packageName))) {
      if (opFilter.isEmpty() || opFilter.contains(op)) {
        opEntries.add(toOpEntry(op));
      }
    }

    return ImmutableList.of(new PackageOps(packageName, uid, opEntries));
  }

  @Implementation(minSdk = KITKAT)
  protected void checkPackage(int uid, String packageName) {
    try {
      // getPackageUid was introduced in API 24, so we call it on the shadow class
      ShadowApplicationPackageManager shadowApplicationPackageManager =
          Shadow.extract(context.getPackageManager());
      int packageUid = shadowApplicationPackageManager.getPackageUid(packageName, 0);
      if (packageUid == uid) {
        return;
      }
      throw new SecurityException("Package " + packageName + " belongs to " + packageUid);
    } catch (NameNotFoundException e) {
      throw new SecurityException("Package " + packageName + " doesn't belong to " + uid, e);
    }
  }

  /**
   * Sets audio restrictions.
   *
   *
This method is public for testing, as the original method is {@code @hide}.
   */
  @Implementation(minSdk = LOLLIPOP)
  @HiddenApi
  public void setRestriction(
      int code, @AttributeUsage int usage, int mode, String[] exceptionPackages) {
    audioRestrictions.put(
        getAudioRestrictionKey(code, usage), new ModeAndException(mode, exceptionPackages));
  }

  @Nullable
  public ModeAndException getRestriction(int code, @AttributeUsage int usage) {
    // this gives us room for 256 op_codes. There are 78 as of P.
    return audioRestrictions.get(getAudioRestrictionKey(code, usage));
  }

  @Implementation(minSdk = KITKAT)
  @HiddenApi
  @RequiresPermission(value = android.Manifest.permission.WATCH_APPOPS)
  protected void startWatchingMode(int op, String packageName, OnOpChangedListener callback) {
    appOpListeners.put(getListenerKey(op, packageName), callback);
  }

  @Implementation(minSdk = KITKAT)
  @RequiresPermission(value = android.Manifest.permission.WATCH_APPOPS)
  protected void stopWatchingMode(OnOpChangedListener callback) {
    appOpListeners.inverse().remove(callback);
  }

  protected OpEntry toOpEntry(Integer op) {
    if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.M) {
      return ReflectionHelpers.callConstructor(
          OpEntry.class,
          ClassParameter.from(int.class, op),
          ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED),
          ClassParameter.from(long.class, OP_TIME),
          ClassParameter.from(long.class, REJECT_TIME),
          ClassParameter.from(int.class, DURATION));
    } else {
      return ReflectionHelpers.callConstructor(
          OpEntry.class,
          ClassParameter.from(int.class, op),
          ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED),
          ClassParameter.from(long.class, OP_TIME),
          ClassParameter.from(long.class, REJECT_TIME),
          ClassParameter.from(int.class, DURATION),
          ClassParameter.from(int.class, PROXY_UID),
          ClassParameter.from(String.class, PROXY_PACKAGE));
    }
  }

  private static String getInternalKey(int uid, String packageName) {
    return uid + "|" + packageName;
  }

  private static String getOpMapKey(int uid, String packageName, int opInt) {
    return String.format("%s|%s|%s", uid, packageName, opInt);
  }

  private static int getAudioRestrictionKey(int code, @AttributeUsage int usage) {
    return code | (usage << 8);
  }

  private static String getListenerKey(int op, String packageName) {
    return String.format("%s|%s", op, packageName);
  }

  /** Class holding usage mode and excpetion packages. */
  public static class ModeAndException {
    public final int mode;
    public final List<String> exceptionPackages;

    public ModeAndException(int mode, String[] exceptionPackages) {
      this.mode = mode;
      this.exceptionPackages =
          exceptionPackages == null
              ? Collections.emptyList()
              : Collections.unmodifiableList(Arrays.asList(exceptionPackages));
    }
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static org.robolectric.RuntimeEnvironment.getApiLevel;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.Scheduler;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

/**
 * The shadow {@link Message} for {@link LooperMode.Mode.LEGACY}.
 *
 *
In {@link LooperMode.Mode.LEGACY}, each Message is associated with a Runnable posted to the
 * {@link Scheduler}.
 *
 * @see ShadowLooper, ShadowLegacyMessageQueue
 */
@Implements(value = Message.class, isInAndroidSdk = false)
public class ShadowLegacyMessage extends ShadowMessage {
  @RealObject
  private Message realMessage;
  private Runnable scheduledRunnable;

  private void unschedule() {
    Handler target = realMessage.getTarget();

    if (target != null && scheduledRunnable != null) {
      shadowOf(target.getLooper()).getScheduler().remove(scheduledRunnable);
      scheduledRunnable = null;
    }
  }

  /**
   * Hook to unscheduled the callback when the message is recycled.
   * Invokes {@link #unschedule()} and then calls through to the
   * package private method {@link Message#recycleUnchecked()}
   * on the real object.
   */
  @HiddenApi
  @Implementation(minSdk = LOLLIPOP)
  public void recycleUnchecked() {
    if (getApiLevel() >= LOLLIPOP) {
      unschedule();
      directlyOn(realMessage, Message.class, "recycleUnchecked");
    } else {
      // provide forward compatibility with SDK 21.
      recycle();
    }
  }

  /**
   * Hook to unscheduled the callback when the message is recycled. Invokes {@link #unschedule()}
   * and then calls through to {@link Message#recycle()} on the real object.
   */
  @Implementation(maxSdk = KITKAT_WATCH)
  protected void recycle() {
    unschedule();
    directlyOn(realMessage, Message.class, "recycle");
  }

  @Override
  public void setScheduledRunnable(Runnable r) {
    scheduledRunnable = r;
  }

  /**
   * Convenience method to provide access to the private {@code Message.isInUse()} method. Note that
   * the definition of "in use" changed with API 21:
   *
   *
In API 19, a message was only considered "in use" during its dispatch. In API 21, the
   * message is considered "in use" from the time it is enqueued until the time that it is freshly
   * obtained via a call to {@link Message#obtain()}. This means that in API 21 messages that are in
   * the recycled pool will still be marked as "in use".
   *
   * @return {@code true} if the message is currently "in use", {@code false} otherwise.
   */
  @Implementation
  protected boolean isInUse() {
    return directlyOn(realMessage, Message.class, "isInUse");
  }

  @Override
  public Message getNext() {
    return reflector(_Message_.class, realMessage).getNext();
  }

  @Override
  public void setNext(Message next) {
    reflector(_Message_.class, realMessage).setNext(next);
  }

  private static ShadowLooper shadowOf(Looper looper) {
    return Shadow.extract(looper);
  }

  /** Accessor interface for {@link Message}'s internals. */
  @ForType(Message.class)
  interface _Message_ {

    void markInUse();

    void recycleUnchecked();

    void recycle();

    @Accessor("next")
    Message getNext();

    @Accessor("next")
    void setNext(Message next);
  }
}


package org.robolectric.shadows;

import static org.robolectric.res.android.Errors.NO_ERROR;
import static org.robolectric.res.android.Util.ATRACE_NAME;
import static org.robolectric.res.android.Util.JNI_TRUE;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.NonNull;
import android.content.res.ApkAssets;
import android.content.res.AssetManager;
import android.os.Build;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Objects;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.res.android.Asset;
import org.robolectric.res.android.CppApkAssets;
import org.robolectric.res.android.Registries;
import org.robolectric.res.android.ResXMLTree;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowApkAssets.Picker;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

// transliterated from
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/core/jni/android_content_res_ApkAssets.cpp

@Implements(value = ApkAssets.class, minSdk = Build.VERSION_CODES.P,
    shadowPicker = Picker.class, isInAndroidSdk = false)
public class ShadowArscApkAssets9 extends ShadowApkAssets {
// #define ATRACE_TAG ATRACE_TAG_RESOURCES
//
// #include "android-base/macros.h"
// #include "android-base/stringprintf.h"
// #include "android-base/unique_fd.h"
// #include "androidfw/ApkAssets.h"
// #include "utils/misc.h"
// #include "utils/Trace.h"
//
// #include "core_jni_helpers.h"
// #include "jni.h"
// #include "nativehelper/ScopedUtfChars.h"
//
// using ::android::base::unique_fd;
//
// namespace android {

  private static final String FRAMEWORK_APK_PATH =
      ReflectionHelpers.getStaticField(AssetManager.class, "FRAMEWORK_APK_PATH");

  private static final HashMap<Key, WeakReference<ApkAssets>> cachedApkAssets =
      new HashMap<>();
  private static final HashMap<Key, Long> cachedNativePtrs = new HashMap<>();

  @RealObject private ApkAssets realApkAssets;

  long getNativePtr() {
    return reflector(_ApkAssets_.class, realApkAssets).getNativePtr();
  }

  /** Accessor interface for {@link ApkAssets}'s internals. */
  @ForType(ApkAssets.class)
  interface _ApkAssets_ {

    @Accessor("mNativePtr")
    long getNativePtr();
  }

  /**
   * Caching key for {@link ApkAssets}.
   */
  private static class Key {
    private final FileDescriptor fd;
    private final String path;
    private final boolean system;
    private final boolean load_as_shared_library;
    private final boolean overlay;

    public Key(FileDescriptor fd, String path, boolean system, boolean load_as_shared_library,
        boolean overlay) {
      this.fd = fd;
      this.path = path;
      this.system = system;
      this.load_as_shared_library = load_as_shared_library;
      this.overlay = overlay;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      Key key = (Key) o;
      return system == key.system &&
          load_as_shared_library == key.load_as_shared_library &&
          overlay == key.overlay &&
          Objects.equals(fd, key.fd) &&
          Objects.equals(path, key.path);
    }

    @Override
    public int hashCode() {
      return Objects.hash(fd, path, system, load_as_shared_library, overlay);
    }
  }

  @FunctionalInterface
  private interface ApkAssetMaker {
    ApkAssets call();
  }

  private static ApkAssets getFromCacheOrLoad(Key key, ApkAssetMaker callable) {
    synchronized (cachedApkAssets) {
      WeakReference<ApkAssets> cachedRef = cachedApkAssets.get(key);
      ApkAssets apkAssets;
      if (cachedRef != null) {
        apkAssets = cachedRef.get();
        if (apkAssets != null) {
          return apkAssets;
        } else {
          cachedApkAssets.remove(key);
          long nativePtr = cachedNativePtrs.remove(key);
          Registries.NATIVE_APK_ASSETS_REGISTRY.unregister(nativePtr);
        }
      }

      apkAssets = callable.call();
      cachedApkAssets.put(key, new WeakReference<>(apkAssets));
      long nativePtr = ((ShadowArscApkAssets9) Shadow.extract(apkAssets)).getNativePtr();
      cachedNativePtrs.put(key, nativePtr);
      return apkAssets;
    }
  }

  @Implementation
  protected static ApkAssets loadFromPath(@NonNull String path) throws IOException {
    return getFromCacheOrLoad(
        new Key(null, path, false, false, false),
        () -> directlyOn(ApkAssets.class, "loadFromPath", ClassParameter.from(String.class, path)));
  }

  /**
   * Necessary to shadow this method because the framework path is hard-coded.
   * Called from AssetManager.createSystemAssetsInZygoteLocked() in P+.
   */
  @Implementation
  protected static ApkAssets loadFromPath(String path, boolean system)
      throws IOException {
    System.out.println(
        "Called loadFromPath("
            + path
            + ", " + system + "); mode="
            + (RuntimeEnvironment.useLegacyResources() ? "legacy" : "binary")
            + " sdk=" + RuntimeEnvironment.getApiLevel());

    if (FRAMEWORK_APK_PATH.equals(path)) {
      path = RuntimeEnvironment.getAndroidFrameworkJarPath().toString();
    }

    String finalPath = path;
    return getFromCacheOrLoad(
        new Key(null, path, system, false, false),
        () -> directlyOn(ApkAssets.class, "loadFromPath",
            ClassParameter.from(String.class, finalPath),
            ClassParameter.from(boolean.class, system)));
  }

  @Implementation
  protected static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system,
      boolean forceSharedLibrary) throws IOException {
    return getFromCacheOrLoad(
        new Key(null, path, system, forceSharedLibrary, false),
        () -> directlyOn(ApkAssets.class, "loadFromPath",
            ClassParameter.from(String.class, path),
            ClassParameter.from(boolean.class, system),
            ClassParameter.from(boolean.class, forceSharedLibrary)));
  }

  @Implementation
  protected static ApkAssets loadFromFd(FileDescriptor fd,
      String friendlyName, boolean system, boolean forceSharedLibrary)
      throws IOException {
    return getFromCacheOrLoad(
        new Key(fd, friendlyName, system, forceSharedLibrary, false),
        () -> directlyOn(ApkAssets.class, "loadFromPath",
            ClassParameter.from(FileDescriptor.class, fd),
            ClassParameter.from(String.class, friendlyName),
            ClassParameter.from(boolean.class, system),
            ClassParameter.from(boolean.class, forceSharedLibrary)));
  }

  @Implementation
  protected static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
      throws IOException {
    throw new UnsupportedOperationException();
    // return getFromCacheOrLoad(
        // new Key(fd, friendlyName, system, forceSharedLibrary, false),
        // () -> directlyOn(ApkAssets.class, "loadFromPath",
        //     ClassParameter.from(FileDescriptor.class, fd),
        //     ClassParameter.from(String.class, friendlyName),
        //     ClassParameter.from(boolean.class, system),
        //     ClassParameter.from(boolean.class, forceSharedLibrary)));
  }

  // static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system,
//                         jboolean force_shared_lib, jboolean overlay) {
  @Implementation
  protected static long nativeLoad(String java_path, boolean system,
      boolean force_shared_lib, boolean overlay) throws IOException {
    String path = java_path;
    if (path == null) {
      return 0;
    }

    ATRACE_NAME(String.format("LoadApkAssets(%s)", path));

    CppApkAssets apk_assets;
    try {
      if (overlay) {
        apk_assets = CppApkAssets.LoadOverlay(path, system);
      } else if (force_shared_lib) {
        apk_assets =
            CppApkAssets.LoadAsSharedLibrary(path, system);
      } else {
        apk_assets = CppApkAssets.Load(path, system);
      }
    } catch (OutOfMemoryError e) {
      OutOfMemoryError outOfMemoryError = new OutOfMemoryError("Failed to load " + path);
      outOfMemoryError.initCause(e);
      throw outOfMemoryError;
    }

    if (apk_assets == null) {
      String error_msg = String.format("Failed to load asset path %s", path);
      throw new IOException(error_msg);
    }
    return Registries.NATIVE_APK_ASSETS_REGISTRY.register(apk_assets);
  }

  // static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor,
//                               jstring friendly_name, jboolean system, jboolean force_shared_lib) {
  @Implementation
  protected static long nativeLoadFromFd(FileDescriptor file_descriptor,
      String friendly_name, boolean system, boolean force_shared_lib) {
    String friendly_name_utf8 = friendly_name;
    if (friendly_name_utf8 == null) {
      return 0;
    }

    throw new UnsupportedOperationException();
    // ATRACE_NAME(String.format("LoadApkAssetsFd(%s)", friendly_name_utf8));
    //
    // int fd = jniGetFDFromFileDescriptor(env, file_descriptor);
    // if (fd < 0) {
    //   throw new IllegalArgumentException("Bad FileDescriptor");
    // }
    //
    // unique_fd dup_fd(.dup(fd));
    // if (dup_fd < 0) {
    //   throw new IOException(errno);
    //   return 0;
    // }
    //
    // ApkAssets apk_assets = ApkAssets.LoadFromFd(std.move(dup_fd),
    //                                                                     friendly_name_utf8,
    //                                                                     system, force_shared_lib);
    // if (apk_assets == null) {
    //   String error_msg = String.format("Failed to load asset path %s from fd %d",
    //                                              friendly_name_utf8, dup_fd.get());
    //   throw new IOException(error_msg);
    //   return 0;
    // }
    // return ShadowArscAssetManager9.NATIVE_APK_ASSETS_REGISTRY.getNativeObjectId(apk_assets);
  }

  // static jstring NativeGetAssetPath(JNIEnv* env, jclass /*clazz*/, jlong ptr) {
  @Implementation
  protected static String nativeGetAssetPath(long ptr) {
    CppApkAssets apk_assets = Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr);
    return apk_assets.GetPath();
  }

  // static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) {
  @Implementation
  protected static long nativeGetStringBlock(long ptr) {
    CppApkAssets apk_assets = Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr);
    return apk_assets.GetLoadedArsc().GetStringPool().getNativePtr();
  }

  // static jboolean NativeIsUpToDate(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) {
  @Implementation
  protected static boolean nativeIsUpToDate(long ptr) {
    CppApkAssets apk_assets = Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr);
    // (void)apk_assets;
    return JNI_TRUE;
  }

  // static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) {
  @Implementation
  protected static long nativeOpenXml(long ptr, String file_name) throws FileNotFoundException {
    String path_utf8 = file_name;
    if (path_utf8 == null) {
      return 0;
    }

    CppApkAssets apk_assets =
        Registries.NATIVE_APK_ASSETS_REGISTRY.getNativeObject(ptr);
    Asset asset = apk_assets.Open(path_utf8,
        Asset.AccessMode.ACCESS_RANDOM);
    if (asset == null) {
      throw new FileNotFoundException(path_utf8);
    }

    // DynamicRefTable is only needed when looking up resource references. Opening an XML file
    // directly from an ApkAssets has no notion of proper resource references.
    ResXMLTree xml_tree = new ResXMLTree(null); // util.make_unique<ResXMLTree>(nullptr /*dynamicRefTable*/);
    int err = xml_tree.setTo(asset.getBuffer(true), (int) asset.getLength(), true);
    // asset.reset();

    if (err != NO_ERROR) {
      throw new FileNotFoundException("Corrupt XML binary file");
    }
    return Registries.NATIVE_RES_XML_TREES.register(xml_tree); // reinterpret_cast<jlong>(xml_tree.release());
  }

// // JNI registration.
// static const JNINativeMethod gApkAssetsMethods[] = {
//     {"nativeLoad", "(Ljava/lang/String;ZZZ)J", (void*)NativeLoad},
//     {"nativeLoadFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;ZZ)J",
//         (void*)NativeLoadFromFd},
//     {"nativeDestroy", "(J)V", (void*)NativeDestroy},
//     {"nativeGetAssetPath", "(J)Ljava/lang/String;", (void*)NativeGetAssetPath},
//     {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock},
//     {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate},
//     {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml},
// };
//
// int register_android_content_res_ApkAssets(JNIEnv* env) {
//   return RegisterMethodsOrDie(env, "android/content/res/ApkAssets", gApkAssetsMethods,
//                               arraysize(gApkAssetsMethods));
// }
//
// }  // namespace android
}


package org.robolectric.shadows;

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

import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.RecentTaskInfo;
import android.app.IAppTask;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;

@Implements(value = AppTask.class, minSdk = LOLLIPOP)
public class ShadowAppTask {
  private boolean isFinished;
  private RecentTaskInfo recentTaskInfo;
  private boolean hasMovedToFront;
  private boolean isExcludedFromRecents;

  public static AppTask newInstance() {
    return ReflectionHelpers.callConstructor(
        AppTask.class, ReflectionHelpers.ClassParameter.from(IAppTask.class, null));
  }

  /**
   * For tests, marks the task as finished. Task is not finished when created initially.
   *
   * @see #isFinishedAndRemoved()
   */
  @Implementation
  protected void finishAndRemoveTask() {
    this.isFinished = true;
  }

  /**
   * For tests, returns the {@link RecentTaskInfo} set using {@link #setTaskInfo(RecentTaskInfo)}.
   * If nothing is set, it returns null.
   *
   * @see #setTaskInfo(RecentTaskInfo)
   */
  @Implementation
  protected RecentTaskInfo getTaskInfo() {
    return recentTaskInfo;
  }

  /**
   * For tests, marks the task as moved to the front. Task is created and marked as not moved to the
   * front.
   *
   * @see #hasMovedToFront()
   */
  @Implementation
  protected void moveToFront() {
    this.hasMovedToFront = true;
  }

  /**
   * Starts the activity using given context. Started activity can be checked using {@link
   * ShadowContextWrapper#getNextStartedActivity()}
   *
   * @param context Context with which the activity will be start.
   * @param intent Intent of the activity to be started.
   * @param options Extras passed to the activity.
   */
  @Implementation
  protected void startActivity(Context context, Intent intent, Bundle options) {
    context.startActivity(intent, options);
  }

  /**
   * For tests, marks the task as excluded from recents. Current, status can be checked using {@link
   * #isExcludedFromRecents()}.
   *
   * @param exclude Whether to exclude from recents.
   */
  @Implementation
  protected void setExcludeFromRecents(boolean exclude) {
    this.isExcludedFromRecents = exclude;
  }

  /** Returns true if {@link #finishAndRemoveTask()} has been called before. */
  public boolean isFinishedAndRemoved() {
    return isFinished;
  }

  /**
   * Sets the recentTaskInfo for the task. {@link #getTaskInfo()} returns the task info set using
   * this method.
   */
  public void setTaskInfo(RecentTaskInfo recentTaskInfo) {
    this.recentTaskInfo = recentTaskInfo;
  }

  /**
   * Returns true if task has been moved to the front.
   *
   * @see #moveToFront()
   */
  public boolean hasMovedToFront() {
    return hasMovedToFront;
  }

  /**
   * Returns true if task has been excluded from recents.
   *
   * @see #setExcludeFromRecents(boolean)
   */
  public boolean isExcludedFromRecents() {
    return isExcludedFromRecents;
  }
}


/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.robolectric.shadows;

import android.util.TypedValue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Helper class to provide various conversion method used in handling android resources.
 */
public final class ResourceHelper {

  private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]*(?:\\.[0-9]+)?)(.*)");
  private final static float[] sFloatOut = new float[1];

  private final static TypedValue mValue = new TypedValue();

  private final static Class<?> androidInternalR;

  static {
    try {
      androidInternalR = Class.forName("com.android.internal.R$id");
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns the color value represented by the given string value
   *
   * @param value the color value
   * @return the color as an int
   * @throws NumberFormatException if the conversion failed.
   */
  public static int getColor(String value) {
    if (value != null) {
      if (value.startsWith("#") == false) {
        throw new NumberFormatException(
            String.format("Color value '%s' must start with #", value));
      }

      value = value.substring(1);

      // make sure it's not longer than 32bit
      if (value.length() > 8) {
        throw new NumberFormatException(String.format(
            "Color value '%s' is too long. Format is either" +
            "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
            value));
      }

      if (value.length() == 3) { // RGB format
        char[] color = new char[8];
        color[0] = color[1] = 'F';
        color[2] = color[3] = value.charAt(0);
        color[4] = color[5] = value.charAt(1);
        color[6] = color[7] = value.charAt(2);
        value = new String(color);
      } else if (value.length() == 4) { // ARGB format
        char[] color = new char[8];
        color[0] = color[1] = value.charAt(0);
        color[2] = color[3] = value.charAt(1);
        color[4] = color[5] = value.charAt(2);
        color[6] = color[7] = value.charAt(3);
        value = new String(color);
      } else if (value.length() == 6) {
        value = "FF" + value;
      }

      // this is a RRGGBB or AARRGGBB value

      // Integer.parseInt will fail to inferFromValue strings like "ff191919", so we use
      // a Long, but cast the result back into an int, since we know that we're only
      // dealing with 32 bit values.
      return (int)Long.parseLong(value, 16);
    }

    throw new NumberFormatException();
  }

  /**
   * Returns the TypedValue color type represented by the given string value
   *
   * @param value the color value
   * @return the color as an int. For backwards compatibility, will return a default of ARGB8 if
   *   value format is unrecognized.
   */
  public static int getColorType(String value) {
    if (value != null && value.startsWith("#")) {
      switch (value.length()) {
        case 4:
          return TypedValue.TYPE_INT_COLOR_RGB4;
        case 5:
          return TypedValue.TYPE_INT_COLOR_ARGB4;
        case 7:
          return TypedValue.TYPE_INT_COLOR_RGB8;
        case 9:
          return TypedValue.TYPE_INT_COLOR_ARGB8;
      }
    }
    return TypedValue.TYPE_INT_COLOR_ARGB8;
  }

  public static int getInternalResourceId(String idName) {
    try {
      return (int) androidInternalR.getField(idName).get(null);
    } catch (NoSuchFieldException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  // ------- TypedValue stuff
  // This is taken from //device/libs/utils/ResourceTypes.cpp

  private static final class UnitEntry {
    String name;
    int type;
    int unit;
    float scale;

    UnitEntry(String name, int type, int unit, float scale) {
      this.name = name;
      this.type = type;
      this.unit = unit;
      this.scale = scale;
    }
  }

  private final static UnitEntry[] sUnitNames = new UnitEntry[] {
    new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
    new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
    new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
    new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
    new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
    new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
    new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
    new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
    new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
  };

  /**
   * Returns the raw value from the given attribute float-type value string.
   * This object is only valid until the next call on to {@link ResourceHelper}.
   *
   * @param attribute Attribute name.
   * @param value Attribute value.
   * @param requireUnit whether the value is expected to contain a unit.
   * @return The typed value.
   */
  public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
    if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
      return mValue;
    }

    return null;
  }

  /**
   * Parse a float attribute and return the parsed value into a given TypedValue.
   * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
   * @param value the string value of the attribute
   * @param outValue the TypedValue to receive the parsed value
   * @param requireUnit whether the value is expected to contain a unit.
   * @return true if success.
   */
  public static boolean parseFloatAttribute(String attribute, String value,
      TypedValue outValue, boolean requireUnit) {
    assert requireUnit == false || attribute != null;

    // remove the space before and after
    value = value.trim();
    int len = value.length();

    if (len <= 0) {
      return false;
    }

    // check that there's no non ascii characters.
    char[] buf = value.toCharArray();
    for (int i = 0 ; i < len ; i++) {
      if (buf[i] > 255) {
        return false;
      }
    }

    // check the first character
    if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') {
      return false;
    }

    // now look for the string that is after the float...
    Matcher m = sFloatPattern.matcher(value);
    if (m.matches()) {
      String f_str = m.group(1);
      String end = m.group(2);

      float f;
      try {
        f = Float.parseFloat(f_str);
      } catch (NumberFormatException e) {
        // this shouldn't happen with the regexp above.
        return false;
      }

      if (end.length() > 0 && end.charAt(0) != ' ') {
        // Might be a unit...
        if (parseUnit(end, outValue, sFloatOut)) {
          computeTypedValue(outValue, f, sFloatOut[0]);
          return true;
        }
        return false;
      }

      // make sure it's only spaces at the end.
      end = end.trim();

      if (end.length() == 0) {
        if (outValue != null) {
          outValue.assetCookie = 0;
          outValue.string = null;

          if (requireUnit == false) {
            outValue.type = TypedValue.TYPE_FLOAT;
            outValue.data = Float.floatToIntBits(f);
          } else {
            // no unit when required? Use dp and out an error.
            applyUnit(sUnitNames[1], outValue, sFloatOut);
            computeTypedValue(outValue, f, sFloatOut[0]);

            System.out.println(String.format(
                "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
                    value, attribute));
          }
          return true;
        }
      }
    }

    return false;
  }

  private static void computeTypedValue(TypedValue outValue, float value, float scale) {
    value *= scale;
    boolean neg = value < 0;
    if (neg) {
      value = -value;
    }
    long bits = (long)(value*(1<<23)+.5f);
    int radix;
    int shift;
    if ((bits&0x7fffff) == 0) {
      // Always use 23p0 if there is no fraction, just to make
      // things easier to read.
      radix = TypedValue.COMPLEX_RADIX_23p0;
      shift = 23;
    } else if ((bits&0xffffffffff800000L) == 0) {
      // Magnitude is zero -- can fit in 0 bits of precision.
      radix = TypedValue.COMPLEX_RADIX_0p23;
      shift = 0;
    } else if ((bits&0xffffffff80000000L) == 0) {
      // Magnitude can fit in 8 bits of precision.
      radix = TypedValue.COMPLEX_RADIX_8p15;
      shift = 8;
    } else if ((bits&0xffffff8000000000L) == 0) {
      // Magnitude can fit in 16 bits of precision.
      radix = TypedValue.COMPLEX_RADIX_16p7;
      shift = 16;
    } else {
      // Magnitude needs entire range, so no fractional part.
      radix = TypedValue.COMPLEX_RADIX_23p0;
      shift = 23;
    }
    int mantissa = (int)(
      (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
    if (neg) {
      mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
    }
    outValue.data |=
      (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
      | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);
  }

  private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
    str = str.trim();

    for (UnitEntry unit : sUnitNames) {
      if (unit.name.equals(str)) {
        applyUnit(unit, outValue, outScale);
        return true;
      }
    }

    return false;
  }

  private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
    outValue.type = unit.type;
    outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
    outScale[0] = unit.scale;
  }
}


package org.robolectric.shadows;

import android.content.res.AssetManager;
import android.content.res.AssetManager.AssetInputStream;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ColorSpace;
import android.graphics.ImageDecoder;
import android.graphics.ImageDecoder.DecodeException;
import android.graphics.ImageDecoder.Source;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.util.Size;
import java.io.ByteArrayInputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.res.android.NativeObjRegistry;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

// transliterated from https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/core/jni/android/graphics/ImageDecoder.cpp
@SuppressWarnings({"NewApi", "UnusedDeclaration"})
// ImageDecoder is in fact in SDK, but make it false for now so projects which compile against < P
// still work
@Implements(value = ImageDecoder.class, isInAndroidSdk = false, minSdk = Build.VERSION_CODES.P)
public class ShadowImageDecoder {

  private abstract static class ImgStream {

    private final int width;
    private final int height;
    private final boolean animated = false;
    private final boolean ninePatch;

    ImgStream() {
      InputStream inputStream = getInputStream();
      final Point size = ImageUtil.getImageSizeFromStream(inputStream);
      this.width = size == null ? 10 : size.x;
      this.height = size == null ? 10 : size.y;
      if (inputStream instanceof AssetManager.AssetInputStream) {
        ShadowAssetInputStream sis = Shadow.extract(inputStream);
        this.ninePatch = sis.isNinePatch();
      } else {
        this.ninePatch = false;
      }
    }

    protected abstract InputStream getInputStream();

    int getWidth() {
      return width;
    }

    int getHeight() {
      return height;
    }

    boolean isAnimated() {
      return animated;
    }

    boolean isNinePatch() {
      return ninePatch;
    }
  }

  private static final class CppImageDecoder {

    private final ImgStream imgStream;

    CppImageDecoder(ImgStream imgStream) {
      this.imgStream = imgStream;
    }

  }

  private static final NativeObjRegistry<CppImageDecoder> NATIVE_IMAGE_DECODER_REGISTRY =
      new NativeObjRegistry<>(CppImageDecoder.class);

  @RealObject private ImageDecoder realObject;

  private static ImageDecoder jniCreateDecoder(ImgStream imgStream) {
    CppImageDecoder cppImageDecoder = new CppImageDecoder(imgStream);
    long cppImageDecoderPtr = NATIVE_IMAGE_DECODER_REGISTRY.register(cppImageDecoder);
    return ReflectionHelpers.callConstructor(
        ImageDecoder.class,
        ClassParameter.from(long.class, cppImageDecoderPtr),
        ClassParameter.from(int.class, imgStream.getWidth()),
        ClassParameter.from(int.class, imgStream.getHeight()),
        ClassParameter.from(boolean.class, imgStream.isAnimated()),
        ClassParameter.from(boolean.class, imgStream.isNinePatch()));
  }

  static ImageDecoder ImageDecoder_nCreateFd(
      FileDescriptor fileDescriptor, Source source) {
    throw new UnsupportedOperationException();
    // int descriptor = jniGetFDFromFileDescriptor(fileDescriptor);
    // struct stat fdStat;
    // if (fstat(descriptor, &fdStat) == -1) {
    //   throw_exception(ShadowImageDecoder.Error.kSourceMalformedData,
    //       "broken file descriptor; fstat returned -1", null, source);
    // }
    // int dupDescriptor = dup(descriptor);
    // FILE* file = fdopen(dupDescriptor, "r");
    // if (file == NULL) {
    //   close(dupDescriptor);
    //   throw_exception(ShadowImageDecoder.Error.kSourceMalformedData, "Could not open file",
    //       null, source);
    // }
    // SkFILEStream fileStream(new SkFILEStream(file));
    // return native_create(fileStream, source);
  }

  static ImageDecoder ImageDecoder_nCreateInputStream(
      InputStream is, byte[] storage, Source source) {
    // SkStream stream = CreateJavaInputStreamAdaptor(is, storage, false);
    // if (!isTruthy(stream)) {
    //   throw_exception(ShadowImageDecoder.Error.kSourceMalformedData, "Failed to create a stream",
    //       null, source);
    // }
    // SkStream bufferedStream =
    //     SkFrontBufferedStream.Make(stream,
    //     SkCodec.MinBufferedBytesNeeded()));
    // return native_create(bufferedStream, source);

    return jniCreateDecoder(new ImgStream() {
      @Override
      protected InputStream getInputStream() {
        return is;
      }
    });
  }

  static ImageDecoder ImageDecoder_nCreateAsset(long asset_ptr,
      Source source) throws DecodeException {
    // Asset* asset = reinterpret_cast<Asset*>(assetPtr);
    // SkStream stream = new AssetStreamAdaptor(asset);
    // return jniCreateDecoder(stream, source);
    Resources resources = ReflectionHelpers.getField(source, "mResources");
    AssetInputStream assetInputStream = ShadowAssetInputStream.createAssetInputStream(
        null, asset_ptr, resources.getAssets());
    return jniCreateDecoder(
        new ImgStream() {
          @Override
          protected InputStream getInputStream() {
            return assetInputStream;
          }
        });
  }

  static ImageDecoder ImageDecoder_nCreateByteBuffer(ByteBuffer jbyteBuffer,
      int initialPosition, int limit, Source source) throws DecodeException {
    // SkStream stream = CreateByteBufferStreamAdaptor(jbyteBuffer,
    //     initialPosition, limit);
    // if (!isTruthy(stream)) {
    //   throw_exception(ShadowImageDecoder.Error.kSourceMalformedData, "Failed to read ByteBuffer",
    //       null, source);
    // }
    // return native_create(stream, source);
    return jniCreateDecoder(new ImgStream() {
      @Override
      protected InputStream getInputStream() {
        return new ByteArrayInputStream(jbyteBuffer.array());
      }
    });
  }

  static ImageDecoder ImageDecoder_nCreateByteArray(byte[] byteArray,
      int offset, int length, Source source) {
    // SkStream stream = CreateByteArrayStreamAdaptor(byteArray, offset, length);
    // return native_create(stream, source);
    return jniCreateDecoder(new ImgStream() {
      @Override
      protected InputStream getInputStream() {
        return new ByteArrayInputStream(byteArray);
      }
    });
  }

  protected static Bitmap ImageDecoder_nDecodeBitmap(long nativePtr,
      ImageDecoder decoder,
      boolean doPostProcess,
      int width, int height,
      Rect cropRect, boolean mutable,
      int allocator, boolean unpremulRequired,
      boolean conserveMemory, boolean decodeAsAlphaMask,
      ColorSpace desiredColorSpace)
      throws IOException {
    CppImageDecoder cppImageDecoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);

    final ImgStream imgStream = cppImageDecoder.imgStream;
    final InputStream stream = imgStream.getInputStream();

    if (stream == null) {
      return null;
    }

    Bitmap bitmap = BitmapFactory.decodeStream(stream);

    if (imgStream.isNinePatch() && ReflectionHelpers.getField(bitmap, "mNinePatchChunk") == null) {
      ReflectionHelpers.setField(Bitmap.class, bitmap, "mNinePatchChunk", new byte[0]);
    }
    return bitmap;
  }

  static Size ImageDecoder_nGetSampledSize(long nativePtr,
      int sampleSize) {
    CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
    // SkISize size = decoder.mCodec.getSampledDimensions(sampleSize);
    // return env.NewObject(gSize_class, gSize_constructorMethodID, size.width(), size.height());
    // return new Size(size.width(), size.height());
    throw new UnsupportedOperationException();
  }

  static void ImageDecoder_nGetPadding(long nativePtr,
      Rect outPadding) {
    CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
    // decoder.mPeeker.getPadding(outPadding);
    if (decoder.imgStream.isNinePatch()) {
      outPadding.set(0, 0, 0, 0);
    } else {
      outPadding.set(-1, -1, -1, -1);
    }
  }

  static void ImageDecoder_nClose(long nativePtr) {
    // delete reinterpret_cast<ImageDecoder*>(nativePtr);
    NATIVE_IMAGE_DECODER_REGISTRY.unregister(nativePtr);
  }

  static String ImageDecoder_nGetMimeType(long nativePtr) {
    CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
    // return encodedFormatToString(decoder.mCodec.getEncodedFormat());
    throw new UnsupportedOperationException();
  }

  static ColorSpace ImageDecoder_nGetColorSpace(long nativePtr) {
    CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
    // auto colorType = codec.computeOutputColorType(codec.getInfo().colorType());
    // sk_sp<SkColorSpace> colorSpace = codec.computeOutputColorSpace(colorType);
    // return GraphicsJNI.getColorSpace(colorSpace, colorType);
    throw new UnsupportedOperationException();
  }

  // native method implementations...

  @Implementation
  protected static ImageDecoder nCreate(long asset, Source source) throws IOException {
    return ImageDecoder_nCreateAsset(asset, source);
  }

  @Implementation
  protected static ImageDecoder nCreate(ByteBuffer buffer, int position,
      int limit, Source src) throws IOException {
    return ImageDecoder_nCreateByteBuffer(buffer, position, limit, src);
  }

  @Implementation
  protected static ImageDecoder nCreate(byte[] data, int offset, int length,
      Source src) throws IOException {
    return ImageDecoder_nCreateByteArray(data, offset, length, src);
  }

  @Implementation
  protected static ImageDecoder nCreate(InputStream is, byte[] storage, Source source) {
    return ImageDecoder_nCreateInputStream(is, storage, source);
  }

  // The fd must be seekable.
  @Implementation
  protected static ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException {
    return ImageDecoder_nCreateFd(fd, src);
  }

  @Implementation(maxSdk = Build.VERSION_CODES.P)
  protected static Bitmap nDecodeBitmap(long nativePtr,
      ImageDecoder decoder,
      boolean doPostProcess,
      int width, int height,
      android.graphics.Rect cropRect, boolean mutable,
      int allocator, boolean unpremulRequired,
      boolean conserveMemory, boolean decodeAsAlphaMask,
      android.graphics.ColorSpace desiredColorSpace)
      throws IOException {
    return ImageDecoder_nDecodeBitmap(nativePtr,
        decoder,
        doPostProcess,
        width, height,
        cropRect, mutable,
        allocator, unpremulRequired,
        conserveMemory, decodeAsAlphaMask,
        desiredColorSpace);
  }

  @Implementation(minSdk = Build.VERSION_CODES.Q)
  protected static Bitmap nDecodeBitmap(long nativePtr,
      ImageDecoder decoder,
      boolean doPostProcess,
      int width, int height,
      Rect cropRect, boolean mutable,
      int allocator, boolean unpremulRequired,
      boolean conserveMemory, boolean decodeAsAlphaMask,
      long desiredColorSpace, boolean extended)
      throws IOException {
    return ImageDecoder_nDecodeBitmap(nativePtr,
        decoder,
        doPostProcess,
        width, height,
        cropRect, mutable,
        allocator, unpremulRequired,
        conserveMemory, decodeAsAlphaMask,
        null);
  }

  @Implementation
  protected static Size nGetSampledSize(long nativePtr,
      int sampleSize) {
    return ImageDecoder_nGetSampledSize(nativePtr, sampleSize);
  }

  @Implementation
  protected static void nGetPadding(long nativePtr, Rect outRect) {
    ImageDecoder_nGetPadding(nativePtr, outRect);
  }

  @Implementation
  protected static void nClose(long nativePtr) {
    ImageDecoder_nClose(nativePtr);
  }

  @Implementation
  protected static String nGetMimeType(long nativePtr) {
    return ImageDecoder_nGetMimeType(nativePtr);
  }

  @Implementation
  protected static ColorSpace nGetColorSpace(long nativePtr) {
    return ImageDecoder_nGetColorSpace(nativePtr);
  }
}


package org.robolectric.res;

import java.nio.file.Path;
import java.util.function.Predicate;

@SuppressWarnings({"NewApi", "AndroidJdkLibsChecker"})
class DirBaseNameFilter implements Predicate<Path> {
  private final String prefix;
  private final String prefixDash;

  DirBaseNameFilter(String prefix) {
    this.prefix = prefix;
    this.prefixDash = prefix + "-";
  }

  @Override
  public boolean test(Path file) {
    String fileName = nameWithoutTrailingSeparator(file);
    return fileName.equals(prefix) || fileName.startsWith(prefixDash);
  }

  /**
   * It sure seems like a bug that Path#getFileName() returns "name/" for paths inside a jar, but
   * "name" for paths on a regular filesystem. It's always a normal slash, even on Windows. :-p
   */
  private String nameWithoutTrailingSeparator(Path file) {
    String fileName = file.getFileName().toString();
    int trailingSlash = fileName.indexOf('/');
    if (trailingSlash != -1) {
      fileName = fileName.substring(0, trailingSlash);
    }
    return fileName;
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN;
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 org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.Application;
import android.app.Dialog;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.os.Bundle;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.fakes.RoboMenuItem;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.WithType;

@SuppressWarnings("NewApi")
@Implements(Activity.class)
public class ShadowActivity extends ShadowContextThemeWrapper {

  @RealObject
  protected Activity realActivity;

  private int resultCode;
  private Intent resultIntent;
  private Activity parent;
  private int requestedOrientation = -1;
  private View currentFocus;
  private Integer lastShownDialogId = null;
  private int pendingTransitionEnterAnimResId = -1;
  private int pendingTransitionExitAnimResId = -1;
  private Object lastNonConfigurationInstance;
  private Map<Integer, Dialog> dialogForId = new HashMap<>();
  private ArrayList<Cursor> managedCursors = new ArrayList<>();
  private int mDefaultKeyMode = Activity.DEFAULT_KEYS_DISABLE;
  private SpannableStringBuilder mDefaultKeySsb = null;
  private int streamType = -1;
  private boolean mIsTaskRoot = true;
  private Menu optionsMenu;
  private ComponentName callingActivity;
  private PermissionsRequest lastRequestedPermission;
  private ActivityController controller;
  private boolean inMultiWindowMode = false;
  private IntentSenderRequest lastIntentSenderRequest;
  private boolean throwIntentSenderException;

  public void setApplication(Application application) {
    reflector(_Activity_.class, realActivity).setApplication(application);
  }

  public void callAttach(Intent intent) {
    callAttach(intent, /*lastNonConfigurationInstances=*/ null);
  }

  public void callAttach(
      Intent intent,
      @Nullable @WithType("android.app.Activity$NonConfigurationInstances")
          Object lastNonConfigurationInstances) {
    Application application = RuntimeEnvironment.application;
    Context baseContext = application.getBaseContext();

    ComponentName componentName =
        new ComponentName(application.getPackageName(), realActivity.getClass().getName());
    ActivityInfo activityInfo;
    PackageManager packageManager = application.getPackageManager();
    shadowOf(packageManager).addActivityIfNotPresent(componentName);
    try {
      activityInfo = packageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA);
    } catch (NameNotFoundException e) {
      throw new RuntimeException("Activity is not resolved even if we made sure it exists", e);
    }

    CharSequence activityTitle = activityInfo.loadLabel(baseContext.getPackageManager());

    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    Instrumentation instrumentation = activityThread.getInstrumentation();

    reflector(_Activity_.class, realActivity)
        .callAttach(
            baseContext,
            activityThread,
            instrumentation,
            application,
            intent,
            activityInfo,
            activityTitle,
            lastNonConfigurationInstances);

    int theme = activityInfo.getThemeResource();
    if (theme != 0) {
      realActivity.setTheme(theme);
    }
  }

  public void setCallingActivity(ComponentName activityName) {
    callingActivity = activityName;
  }

  @Implementation
  protected ComponentName getCallingActivity() {
    return callingActivity;
  }

  @Implementation
  protected void setDefaultKeyMode(int keyMode) {
    mDefaultKeyMode = keyMode;

    // Some modes use a SpannableStringBuilder to track & dispatch input events
    // This list must remain in sync with the switch in onKeyDown()
    switch (mDefaultKeyMode) {
      case Activity.DEFAULT_KEYS_DISABLE:
      case Activity.DEFAULT_KEYS_SHORTCUT:
        mDefaultKeySsb = null;      // not used in these modes
        break;
      case Activity.DEFAULT_KEYS_DIALER:
      case Activity.DEFAULT_KEYS_SEARCH_LOCAL:
      case Activity.DEFAULT_KEYS_SEARCH_GLOBAL:
        mDefaultKeySsb = new SpannableStringBuilder();
        Selection.setSelection(mDefaultKeySsb, 0);
        break;
      default:
        throw new IllegalArgumentException();
    }
  }

  public int getDefaultKeymode() {
    return mDefaultKeyMode;
  }

  @Implementation
  protected final void setResult(int resultCode) {
    this.resultCode = resultCode;
  }

  @Implementation
  protected final void setResult(int resultCode, Intent data) {
    this.resultCode = resultCode;
    this.resultIntent = data;
  }

  @Implementation
  protected LayoutInflater getLayoutInflater() {
    return LayoutInflater.from(realActivity);
  }

  @Implementation
  protected MenuInflater getMenuInflater() {
    return new MenuInflater(realActivity);
  }

  /**
   * Checks to ensure that the{@code contentView} has been set
   *
   * @param id ID of the view to find
   * @return the view
   * @throws RuntimeException if the {@code contentView} has not been called first
   */
  @Implementation
  protected View findViewById(int id) {
    return getWindow().findViewById(id);
  }

  @Implementation
  protected final Activity getParent() {
    return parent;
  }

  /**
   * Allow setting of Parent fragmentActivity (for unit testing purposes only)
   *
   * @param parent Parent fragmentActivity to set on this fragmentActivity
   */
  @HiddenApi @Implementation
  public void setParent(Activity parent) {
    this.parent = parent;
  }

  @Implementation
  protected void onBackPressed() {
    finish();
  }

  @Implementation
  protected void finish() {
    // Sets the mFinished field in the real activity so NoDisplay activities can be tested.
    ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", true);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void finishAndRemoveTask() {
    // Sets the mFinished field in the real activity so NoDisplay activities can be tested.
    ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", true);
  }

  @Implementation(minSdk = JELLY_BEAN)
  protected void finishAffinity() {
    // Sets the mFinished field in the real activity so NoDisplay activities can be tested.
    ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", true);
  }

  public void resetIsFinishing() {
    ReflectionHelpers.setField(Activity.class, realActivity, "mFinished", false);
  }

  /**
   * Returns whether {@link #finish()} was called.
   *
   * @deprecated Use {@link Activity#isFinishing()} instead.
   */
  @Deprecated
  public boolean isFinishing() {
    return directlyOn(realActivity, Activity.class).isFinishing();
  }

  /**
   * Constructs a new Window (a {@link com.android.internal.policy.impl.PhoneWindow}) if no window
   * has previously been set.
   *
   * @return the window associated with this Activity
   */
  @Implementation
  protected Window getWindow() {
    Window window = directlyOn(realActivity, Activity.class).getWindow();

    if (window == null) {
      try {
        window = ShadowWindow.create(realActivity);
        setWindow(window);
      } catch (Exception e) {
        throw new RuntimeException("Window creation failed!", e);
      }
    }

    return window;
  }

  public void setWindow(Window window) {
    reflector(_Activity_.class, realActivity).setWindow(window);
  }

  @Implementation
  protected void runOnUiThread(Runnable action) {
    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
      directlyOn(
          realActivity,
          Activity.class,
          "runOnUiThread",
          ClassParameter.from(Runnable.class, action));
    } else {
      ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
    }
  }

  @Implementation
  protected void setRequestedOrientation(int requestedOrientation) {
    if (getParent() != null) {
      getParent().setRequestedOrientation(requestedOrientation);
    } else {
      this.requestedOrientation = requestedOrientation;
    }
  }

  @Implementation
  protected int getRequestedOrientation() {
    if (getParent() != null) {
      return getParent().getRequestedOrientation();
    } else {
      return this.requestedOrientation;
    }
  }

  @Implementation
  protected int getTaskId() {
    return 0;
  }

  @Implementation
  public void startIntentSenderForResult(
      IntentSender intentSender,
      int requestCode,
      @Nullable Intent fillInIntent,
      int flagsMask,
      int flagsValues,
      int extraFlags,
      Bundle options)
      throws IntentSender.SendIntentException {
    if (throwIntentSenderException) {
      throw new IntentSender.SendIntentException("PendingIntent was canceled");
    }
    lastIntentSenderRequest =
        new IntentSenderRequest(
            intentSender, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options);
  }

  /**
   * @return the {@code contentView} set by one of the {@code setContentView()} methods
   */
  public View getContentView() {
    return ((ViewGroup) getWindow().findViewById(android.R.id.content)).getChildAt(0);
  }

  /**
   * @return the {@code resultCode} set by one of the {@code setResult()} methods
   */
  public int getResultCode() {
    return resultCode;
  }

  /**
   * @return the {@code Intent} set by {@link #setResult(int, android.content.Intent)}
   */
  public Intent getResultIntent() {
    return resultIntent;
  }

  /**
   * Consumes and returns the next {@code Intent} on the
   * started activities for results stack.
   *
   * @return the next started {@code Intent} for an activity, wrapped in
   *         an {@link ShadowActivity.IntentForResult} object
   */
  public IntentForResult getNextStartedActivityForResult() {
    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    ShadowInstrumentation shadowInstrumentation = Shadow.extract(activityThread.getInstrumentation());
    return shadowInstrumentation.getNextStartedActivityForResult();
  }

  /**
   * Returns the most recent {@code Intent} started by
   * {@link Activity#startActivityForResult(Intent, int)} without consuming it.
   *
   * @return the most recently started {@code Intent}, wrapped in
   *         an {@link ShadowActivity.IntentForResult} object
   */
  public IntentForResult peekNextStartedActivityForResult() {
    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    ShadowInstrumentation shadowInstrumentation = Shadow.extract(activityThread.getInstrumentation());
    return shadowInstrumentation.peekNextStartedActivityForResult();
  }

  @Implementation
  protected Object getLastNonConfigurationInstance() {
    if (lastNonConfigurationInstance != null) {
      return lastNonConfigurationInstance;
    }
    return directlyOn(realActivity, Activity.class).getLastNonConfigurationInstance();
  }

  /** @deprecated use {@link ActivityController#recreate()}. */
  @Deprecated
  public void setLastNonConfigurationInstance(Object lastNonConfigurationInstance) {
    this.lastNonConfigurationInstance = lastNonConfigurationInstance;
  }

  /**
   * @param view View to focus.
   */
  public void setCurrentFocus(View view) {
    currentFocus = view;
  }

  @Implementation
  protected View getCurrentFocus() {
    return currentFocus;
  }

  public int getPendingTransitionEnterAnimationResourceId() {
    return pendingTransitionEnterAnimResId;
  }

  public int getPendingTransitionExitAnimationResourceId() {
    return pendingTransitionExitAnimResId;
  }

  @Implementation
  protected boolean onCreateOptionsMenu(Menu menu) {
    optionsMenu = menu;
    return directlyOn(realActivity, Activity.class).onCreateOptionsMenu(menu);
  }

  /**
   * Return the options menu.
   *
   * @return  Options menu.
   */
  public Menu getOptionsMenu() {
    return optionsMenu;
  }

  /**
   * Perform a click on a menu item.
   *
   * @param menuItemResId Menu item resource ID.
   * @return True if the click was handled, false otherwise.
   */
  public boolean clickMenuItem(int menuItemResId) {
    final RoboMenuItem item = new RoboMenuItem(menuItemResId);
    return realActivity.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, item);
  }

  /** For internal use only. Not for public use. */
  public void callOnActivityResult(int requestCode, int resultCode, Intent resultData) {
    final ActivityInvoker invoker = new ActivityInvoker();
    invoker
        .call("onActivityResult", Integer.TYPE, Integer.TYPE, Intent.class)
        .with(requestCode, resultCode, resultData);
  }

  /** For internal use only. Not for public use. */
  public <T extends Activity> void attachController(ActivityController controller) {
    this.controller = controller;
  }

  /** Sets if startIntentSenderForRequestCode will throw an IntentSender.SendIntentException. */
  public void setThrowIntentSenderException(boolean throwIntentSenderException) {
    this.throwIntentSenderException = throwIntentSenderException;
  }

  /**
   * Container object to hold an Intent, together with the requestCode used
   * in a call to {@code Activity.startActivityForResult(Intent, int)}
   */
  public static class IntentForResult {
    public Intent intent;
    public int requestCode;
    public Bundle options;

    public IntentForResult(Intent intent, int requestCode) {
      this.intent = intent;
      this.requestCode = requestCode;
      this.options = null;
    }

    public IntentForResult(Intent intent, int requestCode, Bundle options) {
      this.intent = intent;
      this.requestCode = requestCode;
      this.options = options;
    }
  }

  public void receiveResult(Intent requestIntent, int resultCode, Intent resultIntent) {
    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    ShadowInstrumentation shadowInstrumentation = Shadow.extract(activityThread.getInstrumentation());
    int requestCode = shadowInstrumentation.getRequestCodeForIntent(requestIntent);

    callOnActivityResult(requestCode, resultCode, resultIntent);
  }

  @Implementation
  protected final void showDialog(int id) {
    showDialog(id, null);
  }

  @Implementation
  protected final void dismissDialog(int id) {
    final Dialog dialog = dialogForId.get(id);
    if (dialog == null) {
      throw new IllegalArgumentException();
    }

    dialog.dismiss();
  }

  @Implementation
  protected final void removeDialog(int id) {
    dialogForId.remove(id);
  }

  @Implementation
  protected final boolean showDialog(int id, Bundle bundle) {
    this.lastShownDialogId = id;
    Dialog dialog = dialogForId.get(id);

    if (dialog == null) {
      final ActivityInvoker invoker = new ActivityInvoker();
      dialog = (Dialog) invoker.call("onCreateDialog", Integer.TYPE).with(id);
      if (dialog == null) {
        return false;
      }
      if (bundle == null) {
        invoker.call("onPrepareDialog", Integer.TYPE, Dialog.class).with(id, dialog);
      } else {
        invoker.call("onPrepareDialog", Integer.TYPE, Dialog.class, Bundle.class).with(id, dialog, bundle);
      }

      dialogForId.put(id, dialog);
    }

    dialog.show();
    return true;
  }

  public void setIsTaskRoot(boolean isRoot) {
    mIsTaskRoot = isRoot;
  }

  @Implementation
  protected final boolean isTaskRoot() {
    return mIsTaskRoot;
  }

  /**
   * @return the dialog resource id passed into
   *         {@code Activity.showDialog(int, Bundle)} or {@code Activity.showDialog(int)}
   */
  public Integer getLastShownDialogId() {
    return lastShownDialogId;
  }

  public boolean hasCancelledPendingTransitions() {
    return pendingTransitionEnterAnimResId == 0 && pendingTransitionExitAnimResId == 0;
  }

  @Implementation
  protected void overridePendingTransition(int enterAnim, int exitAnim) {
    pendingTransitionEnterAnimResId = enterAnim;
    pendingTransitionExitAnimResId = exitAnim;
  }

  public Dialog getDialogById(int dialogId) {
    return dialogForId.get(dialogId);
  }

  @Implementation
  protected void recreate() {
    if (controller != null) {
      controller.recreate();
    } else {
      throw new IllegalStateException(
          "Cannot use an Activity that is not managed by an ActivityController");
    }
  }

  @Implementation
  protected void startManagingCursor(Cursor c) {
    managedCursors.add(c);
  }

  @Implementation
  protected void stopManagingCursor(Cursor c) {
    managedCursors.remove(c);
  }

  public List<Cursor> getManagedCursors() {
    return managedCursors;
  }

  @Implementation
  protected final void setVolumeControlStream(int streamType) {
    this.streamType = streamType;
  }

  @Implementation
  protected final int getVolumeControlStream() {
    return streamType;
  }

  @Implementation(minSdk = M)
  protected final void requestPermissions(String[] permissions, int requestCode) {
    lastRequestedPermission = new PermissionsRequest(permissions, requestCode);
  }

  /**
   * Starts a lock task.
   *
   *
The status of the lock task can be verified using {@link #isLockTask} method. Otherwise this
   * implementation has no effect.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected void startLockTask() {
    Shadow.<ShadowActivityManager>extract(getActivityManager())
        .setLockTaskModeState(ActivityManager.LOCK_TASK_MODE_LOCKED);
  }

  /**
   * Stops a lock task.
   *
   *
The status of the lock task can be verified using {@link #isLockTask} method. Otherwise this
   * implementation has no effect.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected void stopLockTask() {
    Shadow.<ShadowActivityManager>extract(getActivityManager())
        .setLockTaskModeState(ActivityManager.LOCK_TASK_MODE_NONE);
  }

  /**
   * Returns if the activity is in the lock task mode.
   *
   * @deprecated Use {@link ActivityManager#getLockTaskModeState} instead.
   */
  @Deprecated
  public boolean isLockTask() {
    return getActivityManager().isInLockTaskMode();
  }

  private ActivityManager getActivityManager() {
    return (ActivityManager) realActivity.getSystemService(Context.ACTIVITY_SERVICE);
  }

  /**
   * Changes state of {@link #isInMultiWindowMode} method.
   */
  public void setInMultiWindowMode(boolean value) {
    inMultiWindowMode = value;
  }

  @Implementation(minSdk = N)
  protected boolean isInMultiWindowMode() {
    return inMultiWindowMode;
  }

  /**
   * Gets the last startIntentSenderForResult request made to this activity.
   *
   * @return The IntentSender request details.
   */
  public IntentSenderRequest getLastIntentSenderRequest() {
    return lastIntentSenderRequest;
  }

  /**
   * Gets the last permission request submitted to this activity.
   *
   * @return The permission request details.
   */
  public PermissionsRequest getLastRequestedPermission() {
    return lastRequestedPermission;
  }

  private final class ActivityInvoker {
    private Method method;

    public ActivityInvoker call(final String methodName, final Class... argumentClasses) {
      try {
        method = Activity.class.getDeclaredMethod(methodName, argumentClasses);
        method.setAccessible(true);
        return this;
      } catch (NoSuchMethodException e) {
        throw new RuntimeException(e);
      }
    }

    public Object withNothing() {
      return with();
    }

    public Object with(final Object... parameters) {
      try {
        return method.invoke(realActivity, parameters);
      } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        throw new RuntimeException(e);
      }
    }
  }

  /** Class to hold a permissions request, including its request code. */
  public static class PermissionsRequest {
    public final int requestCode;
    public final String[] requestedPermissions;

    public PermissionsRequest(String[] requestedPermissions, int requestCode) {
      this.requestedPermissions = requestedPermissions;
      this.requestCode = requestCode;
    }
  }

  /** Class to holds details of a startIntentSenderForResult request. */
  public static class IntentSenderRequest {
    public final IntentSender intentSender;
    public final int requestCode;
    @Nullable public final Intent fillInIntent;
    public final int flagsMask;
    public final int flagsValues;
    public final int extraFlags;
    public final Bundle options;

    public IntentSenderRequest(
        IntentSender intentSender,
        int requestCode,
        @Nullable Intent fillInIntent,
        int flagsMask,
        int flagsValues,
        int extraFlags,
        Bundle options) {
      this.intentSender = intentSender;
      this.requestCode = requestCode;
      this.fillInIntent = fillInIntent;
      this.flagsMask = flagsMask;
      this.flagsValues = flagsValues;
      this.extraFlags = extraFlags;
      this.options = options;
    }
  }

  private ShadowPackageManager shadowOf(PackageManager packageManager) {
    return Shadow.extract(packageManager);
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/** Shadow of TimeZoneFinder for Android O and P. */
@Implements(
    className = "libcore.util.TimeZoneFinder",
    minSdk = O,
    maxSdk = P,
    isInAndroidSdk = false,
    looseSignatures = true)
public class ShadowTimeZoneFinder {

  private static final String TZLOOKUP_PATH = "/usr/share/zoneinfo/tzlookup.xml";

  @Implementation
  protected static Object getInstance() {
    try {
      return ReflectionHelpers.callStaticMethod(
          Class.forName("libcore.util.TimeZoneFinder"),
          "createInstanceForTests",
          ClassParameter.from(String.class, readTzlookup()));
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reads tzlookup.xml from the files bundled inside android-all JARs. We need to read the file
   * instead of passing in the path because the real implementation uses {@link java.nio.file.Paths}
   * which doesn't support reading from JARs.
   */
  public static String readTzlookup() {
    StringBuilder stringBuilder = new StringBuilder();
    InputStream is = null;
    try {
      try {
        is = ShadowTimeZoneFinder.class.getResourceAsStream(TZLOOKUP_PATH);
        BufferedReader reader = new BufferedReader(new InputStreamReader(is, UTF_8));
        for (String line; (line = reader.readLine()) != null; ) {
          stringBuilder.append(line);
        }
      } finally {
        if (is != null) {
          is.close();
        }
      }
    } catch (IOException e) {
      // ignore
    }

    return stringBuilder.toString();
  }
}


package org.robolectric.android;

import static org.robolectric.res.android.ResourceTypes.ANDROID_NS;
import static org.robolectric.res.android.ResourceTypes.AUTO_NS;
import static org.robolectric.res.android.ResourceTypes.RES_XML_END_ELEMENT_TYPE;
import static org.robolectric.res.android.ResourceTypes.RES_XML_RESOURCE_MAP_TYPE;
import static org.robolectric.res.android.ResourceTypes.RES_XML_START_ELEMENT_TYPE;
import static org.robolectric.res.android.ResourceTypes.ResTable_map.ATTR_TYPE;
import static org.robolectric.shadows.ShadowLegacyAssetManager.ATTRIBUTE_TYPE_PRECIDENCE;

import android.content.Context;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import com.google.common.collect.ImmutableMap;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.TreeSet;
import org.robolectric.res.AttrData;
import org.robolectric.res.AttrData.Pair;
import org.robolectric.res.AttributeResource;
import org.robolectric.res.ResName;
import org.robolectric.res.ResType;
import org.robolectric.res.ResourceTable;
import org.robolectric.res.TypedResource;
import org.robolectric.res.android.DataType;
import org.robolectric.res.android.ResTable;
import org.robolectric.res.android.ResTable.ResourceName;
import org.robolectric.res.android.ResourceTable.flag_entry;
import org.robolectric.res.android.ResourceTypes.ResChunk_header;
import org.robolectric.res.android.ResourceTypes.ResStringPool_header.Writer;
import org.robolectric.res.android.ResourceTypes.ResXMLTree_attrExt;
import org.robolectric.res.android.ResourceTypes.ResXMLTree_endElementExt;
import org.robolectric.res.android.ResourceTypes.ResXMLTree_header;
import org.robolectric.res.android.ResourceTypes.ResXMLTree_node;
import org.robolectric.res.android.ResourceTypes.Res_value;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.Converter;
import org.robolectric.shadows.Converter2;
import org.robolectric.shadows.ShadowArscAssetManager;
import org.robolectric.shadows.ShadowAssetManager;
import org.robolectric.shadows.ShadowLegacyAssetManager;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

public class AttributeSetBuilderImpl implements AttributeSetBuilder {
  private static final int STYLE_RES_ID = Integer.MAX_VALUE - 2;
  private static final int CLASS_RES_ID = Integer.MAX_VALUE - 1;
  private static final int ID_RES_ID = Integer.MAX_VALUE;

  private static final ImmutableMap<Integer, String> MAGIC_ATTRS = ImmutableMap.of(
      STYLE_RES_ID, "style",
      CLASS_RES_ID, "class",
      ID_RES_ID, "id"
  );

  private final ResourceResolver resourceResolver;
  private final Map<Integer, String> attrToValue = new TreeMap<>();

  public interface ResourceResolver {

    String getPackageName();

    String getResourceName(Integer attrId);

    Integer getIdentifier(String name, String type, String packageName);

    void parseValue(Integer attrId, ResName attrResName, AttributeResource attribute,
        TypedValue outValue);
  }

  public static class ArscResourceResolver implements ResourceResolver {

    private final Context context;
    private final ResTable resTable;

    public ArscResourceResolver(Context context) {
      this.context = context;

      ShadowAssetManager.ArscBase shadowArscAssetManager = Shadow.extract(context.getAssets());
      this.resTable = shadowArscAssetManager.getCompileTimeResTable();
    }

    @Override
    public String getPackageName() {
      return context.getPackageName();
    }

    @Override
    public String getResourceName(Integer attrId) {
      ResourceName name = new ResourceName();
      if (!resTable.getResourceName(attrId, true, name)) {
        return null;
      }

      StringBuilder str = new StringBuilder();
      if (name.packageName != null) {
        str.append(name.packageName.trim());
      }
      if (name.type != null) {
        if (str.length() > 0) {
          char div = ':';
          str.append(div);
        }
        str.append(name.type);
      }
      if (name.name != null) {
        if (str.length() > 0) {
          char div = '/';
          str.append(div);
        }
        str.append(name.name);
      }
      return str.toString();
    }

    @Override
    public Integer getIdentifier(String name, String type, String packageName) {
      return resTable.identifierForName(name, type, packageName);
    }

    @Override
    public void parseValue(Integer attrId, ResName attrResName, AttributeResource attribute,
        TypedValue outValue) {
      arscParse(attrId, attrResName, attribute, outValue);
    }

    private void arscParse(Integer attrId, ResName attrResName, AttributeResource attribute,
        TypedValue outValue) {
      String format = ShadowArscAssetManager.getResourceBagValue(attrId, ATTR_TYPE, resTable);
      Map<String, Integer> map = ShadowArscAssetManager.getResourceBagValues(attrId, resTable);
      ArrayList<Pair> pairs = new ArrayList<>();
      for (Entry<String, Integer> e : map.entrySet()) {
        pairs.add(new Pair(e.getKey(), Integer.toString(e.getValue())));
      }

      int formatFlags = Integer.parseInt(format);
      TreeSet<flag_entry> sortedFlags = new TreeSet<>(
          (a, b) -> ATTRIBUTE_TYPE_PRECIDENCE.compare(a.name, b.name));
      Collections.addAll(sortedFlags,
          org.robolectric.res.android.ResourceTable.gFormatFlags);

      for (flag_entry flag : sortedFlags) {
        if ((formatFlags & flag.value) != 0) {
          if ("reference".equals(flag.name)) {
            continue;
          }

          AttrData attrData = new AttrData(attrResName.getFullyQualifiedName(), flag.name, pairs);
          Converter2 converter = Converter2.getConverterFor(attrData, flag.name);
          if (converter.fillTypedValue(attribute.value, outValue, true)) {
            break;
          }
        }
      }
    }
  }

  public static class LegacyResourceResolver implements ResourceResolver {

    private final Context context;
    private final ResourceTable resourceTable;

    public LegacyResourceResolver(Context context, ResourceTable compileTimeResourceTable) {
      this.context = context;
      resourceTable = compileTimeResourceTable;
    }

    @Override
    public String getPackageName() {
      return context.getPackageName();
    }

    @Override
    public String getResourceName(Integer attrId) {
      return resourceTable.getResName(attrId).getFullyQualifiedName();
    }

    @Override
    public Integer getIdentifier(String name, String type, String packageName) {
      Integer resourceId = resourceTable.getResourceId(new ResName(packageName, type, name));
      if (resourceId == 0) {
        resourceId = resourceTable.getResourceId(
            new ResName(packageName, type, name.replace('.', '_')));
      }
      return resourceId;
    }

    @Override
    public void parseValue(Integer attrId, ResName attrResName, AttributeResource attribute,
        TypedValue outValue) {
      ShadowLegacyAssetManager shadowAssetManager = Shadow
          .extract(context.getResources().getAssets());
      TypedResource attrTypeData = shadowAssetManager.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
          Converter2 converter = Converter2.getConverterFor(attrData, type);

          if (converter != null) {
            if (converter.fillTypedValue(attribute.value, outValue, true)) {
              break;
            }
          }

        }
        // throw new IllegalArgumentException("wha? " + format);
      } 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);
      }
    }
  }

  protected AttributeSetBuilderImpl(ResourceResolver resourceResolver) {
    this.resourceResolver = resourceResolver;
  }

  // todo rename to setAttribute(), or just set()?
  @Override
  public AttributeSetBuilder addAttribute(int resId, String value) {
    attrToValue.put(resId, value);
    return this;
  }

  // todo rename to setStyle()?
  @Override
  public AttributeSetBuilder setStyleAttribute(String value) {
    attrToValue.put(STYLE_RES_ID, value);
    return this;
  }

  // todo rename to setClass()?
  @Override
  public AttributeSetBuilder setClassAttribute(String value) {
    attrToValue.put(CLASS_RES_ID, value);
    return this;
  }

  // todo rename to setId()?
  @Override
  public AttributeSetBuilder setIdAttribute(String value) {
    attrToValue.put(ID_RES_ID, value);
    return this;
  }

  @Override
  public AttributeSet build() {
    Class<?> xmlBlockClass = ReflectionHelpers
        .loadClass(this.getClass().getClassLoader(), "android.content.res.XmlBlock");

    ByteBuffer buf = ByteBuffer.allocate(16 * 1024).order(ByteOrder.LITTLE_ENDIAN);
    Writer resStringPoolWriter = new Writer();

    final SparseArray<Integer> resIds = new SparseArray<>();
    final int[] maxAttrNameIndex = new int[] { 0 };

    ResXMLTree_attrExt.Writer dummyStart = new ResXMLTree_attrExt.Writer(buf, resStringPoolWriter,
        null, "dummy") {
      {
        String packageName = resourceResolver.getPackageName();

        for (Entry<Integer, String> entry : attrToValue.entrySet()) {
          Integer attrId = entry.getKey();
          String attrNs = "";
          String attrName;
          ResName attrResName = null;

          String magicAttr = MAGIC_ATTRS.get(attrId);
          if (magicAttr != null) {
            attrId = null;
            attrName = magicAttr;
          } else {
            String attrNameStr = resourceResolver.getResourceName(attrId);
            attrResName = ResName.qualifyResName(attrNameStr, packageName, "attr");
            attrNs = (attrResName.packageName.equals("android")) ? ANDROID_NS : AUTO_NS;
            attrName = attrResName.name;
          }

          String value = entry.getValue();
          DataType type;
          int valueInt;

          if (value == null || AttributeResource.isNull(value)) {
            type = DataType.NULL;
            valueInt = TypedValue.DATA_NULL_EMPTY;
          } else if (AttributeResource.isResourceReference(value)) {
            ResName resRef = AttributeResource.getResourceReference(value, packageName, null);
            Integer valueResId = resourceResolver.getIdentifier(resRef.name, resRef.type, resRef.packageName);
            if (valueResId == 0) {
              throw new IllegalArgumentException("no such resource " + value
                  + " while resolving value for "
                  + (attrResName == null ? attrName : attrResName.getFullyQualifiedName()));
            }
            type = DataType.REFERENCE;
            if (attrResName != null) {
              value = "@" +  valueResId;
            }
            valueInt = valueResId;
          } else if (AttributeResource.isStyleReference(value)) {
            ResName resRef = AttributeResource.getStyleReference(value, packageName, "attr");
            Integer valueResId = resourceResolver.getIdentifier(resRef.name, resRef.type, resRef.packageName);
            if (valueResId == 0) {
              throw new IllegalArgumentException("no such attr " + value
                  + " while resolving value for "
                  + (attrResName == null ? attrName : attrResName.getFullyQualifiedName()));
            }
            type = DataType.ATTRIBUTE;
            valueInt = valueResId;
          } else if (attrResName == null) { // class, id, or style
            type = DataType.STRING;
            valueInt = resStringPoolWriter.string(value);
          } else {
            TypedValue outValue = parse(attrId, attrResName, value, packageName);
            type = DataType.fromCode(outValue.type);
            value = (String) outValue.string;
            if (type == DataType.STRING && outValue.data == 0) {
              valueInt = resStringPoolWriter.string(value);
            } else {
              valueInt = outValue.data;
            }
          }

          Res_value resValue = new Res_value(type.code(), valueInt);

          int attrNameIndex = resStringPoolWriter.uniqueString(attrName);
          attr(resStringPoolWriter.string(attrNs), attrNameIndex,
              resStringPoolWriter.string(value), resValue, attrNs + ":" + attrName);
          if (attrId != null) {
            resIds.put(attrNameIndex, attrId);
          }
          maxAttrNameIndex[0] = Math.max(maxAttrNameIndex[0], attrNameIndex);
        }
      }
    };

    ResXMLTree_endElementExt.Writer dummyEnd =
        new ResXMLTree_endElementExt.Writer(buf, resStringPoolWriter, null, "dummy");

    int finalMaxAttrNameIndex = maxAttrNameIndex[0];
    ResXMLTree_header.write(buf, resStringPoolWriter, () -> {
      if (finalMaxAttrNameIndex > 0) {
        ResChunk_header.write(buf, (short) RES_XML_RESOURCE_MAP_TYPE, () -> {}, () -> {
          // not particularly compact, but no big deal for our purposes...
          for (int i = 0; i <= finalMaxAttrNameIndex; i++) {
            Integer value = resIds.get(i);
            buf.putInt(value == null ? 0 : value);
          }
        });
      }

      ResXMLTree_node.write(buf, RES_XML_START_ELEMENT_TYPE, dummyStart::write);
      ResXMLTree_node.write(buf, RES_XML_END_ELEMENT_TYPE, dummyEnd::write);
    });

    int size = buf.position();
    byte[] bytes = new byte[size];
    buf.position(0);
    buf.get(bytes, 0, size);

    Object xmlBlockInstance = ReflectionHelpers
        .callConstructor(xmlBlockClass, ClassParameter.from(byte[].class, bytes));

    AttributeSet parser = ReflectionHelpers.callInstanceMethod(xmlBlockClass, xmlBlockInstance,
        "newParser");
    ReflectionHelpers.callInstanceMethod(parser, "next");
    ReflectionHelpers.callInstanceMethod(parser, "next");

    return parser;
  }

  private TypedValue parse(Integer attrId, ResName attrResName, String value,
      String packageName) {
    AttributeResource attribute =
        new AttributeResource(attrResName, value, packageName);
    TypedValue outValue = new TypedValue();

    if (attribute.isResourceReference()) {
      ResName resourceReference = attribute.getResourceReference();
      int id = resourceResolver.getIdentifier(resourceReference.name, resourceReference.type,
          resourceReference.packageName);
      if (id == 0) {
        throw new IllegalArgumentException("couldn't resolve " + attribute);
      }

      outValue.type = Res_value.TYPE_REFERENCE;
      outValue.data = id;
      outValue.resourceId = id;
      outValue.string = "@" + id;
    } else {
      resourceResolver.parseValue(attrId, attrResName, attribute, outValue);
    }
    return outValue;
  }
}