package org.robolectric.res.android;

import static org.robolectric.res.android.Asset.AccessMode.ACCESS_BUFFER;
import static org.robolectric.res.android.Errors.NO_ERROR;
import static org.robolectric.res.android.Util.ALOGE;
import static org.robolectric.res.android.Util.ALOGV;
import static org.robolectric.res.android.Util.ALOGW;
import static org.robolectric.res.android.Util.isTruthy;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.robolectric.res.FileTypedResource;
import org.robolectric.res.Fs;

// transliterated from
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/Asset.cpp
// and
// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/include/androidfw/Asset.h
/*
 * Instances of this class provide read-only operations on a byte stream.
 *
 * Access may be optimized for streaming, random, or whole buffer modes.  All
 * operations are supported regardless of how the file was opened, but some
 * things will be less efficient.  [pass that in??]
 *
 * "Asset" is the base class for all types of assets.  The classes below
 * provide most of the implementation.  The AssetManager uses one of the
 * static "create" functions defined here to create a new instance.
 */
@SuppressWarnings("NewApi")
public abstract class Asset {
  public static final Asset EXCLUDED_ASSET = new _FileAsset();

  public Runnable onClose;

  public static Asset newFileAsset(FileTypedResource fileTypedResource) throws IOException {
    _FileAsset fileAsset = new _FileAsset();
    Path path = fileTypedResource.getPath();
    fileAsset.mFileName = Fs.externalize(path);
    fileAsset.mLength = Files.size(path);
    fileAsset.mBuf = Fs.getBytes(path);
    return fileAsset;
  }

  // public:
  // virtual ~Asset(void) = default;

  // static int getGlobalCount();
  // static String8 getAssetAllocations();

  public enum AccessMode {
    ACCESS_UNKNOWN(0),
    /* read chunks, and seek forward and backward */
    ACCESS_RANDOM(1),
    /* read sequentially, with an occasional forward seek */
    ACCESS_STREAMING(2),
    /* caller plans to ask for a read-only buffer with all data */
    ACCESS_BUFFER(3);

    private final int mode;

    AccessMode(int mode) {
      this.mode = mode;
    }

    public int mode() {
      return mode;
    }

    public static AccessMode fromInt(int mode) {
      for (AccessMode enumMode : values()) {
        if (mode == enumMode.mode()) {
          return enumMode;
        }
      }
      throw new IllegalArgumentException("invalid mode " + Integer.toString(mode));
    }
  }

  public static final int SEEK_SET = 0;
  public static final int SEEK_CUR = 1;
  public static final int SEEK_END = 2;

  public final int read(byte[] buf, int count) {
    return read(buf, 0, count);
  }

  /*
   * Read data from the current offset.  Returns the actual number of
   * bytes read, 0 on EOF, or -1 on error.
   *
   * Transliteration note: added bufOffset to translate to: index into buf to start writing at
   */
  public abstract int read(byte[] buf, int bufOffset, int count);

  /*
   * Seek to the specified offset.  "whence" uses the same values as
   * lseek/fseek.  Returns the new position on success, or (long) -1
   * on failure.
   */
  public abstract long seek(long offset, int whence);

    /*
     * Close the asset, freeing all associated resources.
     */
    public abstract void close();

    /*
     * Get a pointer to a buffer with the entire contents of the file.
     */
  public abstract byte[] getBuffer(boolean wordAligned);

  /*
   * Get the total amount of data that can be read.
   */
  public abstract long getLength();

  /*
   * Get the total amount of data that can be read from the current position.
   */
  public abstract long getRemainingLength();

    /*
     * Open a new file descriptor that can be used to read this asset.
     * Returns -1 if you can not use the file descriptor (for example if the
     * asset is compressed).
     */
  public abstract FileDescriptor openFileDescriptor(Ref<Long> outStart, Ref<Long> outLength);

  public abstract File getFile();

  public abstract String getFileName();

  /*
   * Return whether this asset's buffer is allocated in RAM (not mmapped).
   * Note: not virtual so it is safe to call even when being destroyed.
   */
  abstract boolean isAllocated(); // { return false; }

  /*
   * Get a string identifying the asset's source.  This might be a full
   * path, it might be a colon-separated list of identifiers.
   *
   * This is NOT intended to be used for anything except debug output.
   * DO NOT try to parse this or use it to open a file.
   */
  final String getAssetSource() { return mAssetSource.string(); }

  public abstract boolean isNinePatch();

//   protected:
//   /*
//    * Adds this Asset to the global Asset list for debugging and
//    * accounting.
//    * Concrete subclasses must call this in their finalructor.
//    */
//   static void registerAsset(Asset asset);
//
//   /*
//    * Removes this Asset from the global Asset list.
//    * Concrete subclasses must call this in their destructor.
//    */
//   static void unregisterAsset(Asset asset);
//
//   Asset(void);        // finalructor; only invoked indirectly
//
//   /* handle common seek() housekeeping */
//   long handleSeek(long offset, int whence, long curPosn, long maxPosn);

  /* set the asset source string */
  void setAssetSource(final String8 path) { mAssetSource = path; }

  AccessMode getAccessMode() { return mAccessMode; }

//   private:
//   /* these operations are not implemented */
//   Asset(final Asset& src);
//   Asset& operator=(final Asset& src);
//
//     /* AssetManager needs access to our "create" functions */
//   friend class AssetManager;
//
//     /*
//      * Create the asset from a named file on disk.
//      */
//   static Asset createFromFile(final String fileName, AccessMode mode);
//
//     /*
//      * Create the asset from a named, compressed file on disk (e.g. ".gz").
//      */
//   static Asset createFromCompressedFile(final String fileName,
//       AccessMode mode);
//
// #if 0
//     /*
//      * Create the asset from a segment of an open file.  This will fail
//      * if "offset" and "length" don't fit within the bounds of the file.
//      *
//      * The asset takes ownership of the file descriptor.
//      */
//   static Asset createFromFileSegment(int fd, long offset, int length,
//       AccessMode mode);
//
//     /*
//      * Create from compressed data.  "fd" should be seeked to the start of
//      * the compressed data.  This could be inside a gzip file or part of a
//      * Zip archive.
//      *
//      * The asset takes ownership of the file descriptor.
//      *
//      * This may not verify the validity of the compressed data until first
//      * use.
//      */
//   static Asset createFromCompressedData(int fd, long offset,
//       int compressionMethod, int compressedLength,
//       int uncompressedLength, AccessMode mode);
// #endif
//
//     /*
//      * Create the asset from a memory-mapped file segment.
//      *
//      * The asset takes ownership of the FileMap.
//      */
//   static Asset createFromUncompressedMap(FileMap dataMap, AccessMode mode);
//
//     /*
//      * Create the asset from a memory-mapped file segment with compressed
//      * data.
//      *
//      * The asset takes ownership of the FileMap.
//      */
//   static Asset createFromCompressedMap(FileMap dataMap,
//       int uncompressedLen, AccessMode mode);
//
//
//     /*
//      * Create from a reference-counted chunk of shared memory.
//      */
//   // TODO

  AccessMode  mAccessMode;        // how the asset was opened
  String8    mAssetSource;       // debug string

  Asset		mNext;				// linked list.
  Asset		mPrev;

  static final boolean kIsDebug = false;

  final static Object gAssetLock = new Object();
  static int gCount = 0;
  static Asset gHead = null;
  static Asset gTail = null;

  void registerAsset(Asset asset)
  {
  //   AutoMutex _l(gAssetLock);
  //   gCount++;
  //   asset.mNext = asset.mPrev = null;
  //   if (gTail == null) {
  //     gHead = gTail = asset;
  //   } else {
  //     asset.mPrev = gTail;
  //     gTail.mNext = asset;
  //     gTail = asset;
  //   }
  //
  //   if (kIsDebug) {
  //     ALOGI("Creating Asset %s #%d\n", asset, gCount);
  //   }
  }

  void unregisterAsset(Asset asset)
  {
  //   AutoMutex _l(gAssetLock);
  //   gCount--;
  //   if (gHead == asset) {
  //     gHead = asset.mNext;
  //   }
  //   if (gTail == asset) {
  //     gTail = asset.mPrev;
  //   }
  //   if (asset.mNext != null) {
  //     asset.mNext.mPrev = asset.mPrev;
  //   }
  //   if (asset.mPrev != null) {
  //     asset.mPrev.mNext = asset.mNext;
  //   }
  //   asset.mNext = asset.mPrev = null;
  //
  //   if (kIsDebug) {
  //     ALOGI("Destroying Asset in %s #%d\n", asset, gCount);
  //   }
  }

  public static int getGlobalCount()
  {
    // AutoMutex _l(gAssetLock);
    synchronized (gAssetLock) {
      return gCount;
    }
  }

  public static String getAssetAllocations()
  {
    // AutoMutex _l(gAssetLock);
    synchronized (gAssetLock) {
      StringBuilder res = new StringBuilder();
      Asset cur = gHead;
      while (cur != null) {
        if (cur.isAllocated()) {
          res.append("    ");
          res.append(cur.getAssetSource());
          long size = (cur.getLength()+512)/1024;
          String buf = String.format(": %dK\n", (int)size);
          res.append(buf);
        }
        cur = cur.mNext;
      }

      return res.toString();
    }
  }

  Asset() {
    // : mAccessMode(ACCESS_UNKNOWN), mNext(null), mPrev(null)
    mAccessMode = AccessMode.ACCESS_UNKNOWN;
  }

  /*
   * Create a new Asset from a file on disk.  There is a fair chance that
   * the file doesn't actually exist.
   *
   * We can use "mode" to decide how we want to go about it.
   */
  static Asset createFromFile(final String fileName, AccessMode mode)
  {
    File file = new File(fileName);
    if (!file.exists()) {
      return null;
    }
    throw new UnsupportedOperationException();

    // _FileAsset pAsset;
    // int result;
    // long length;
    // int fd;
    //
    //   fd = open(fileName, O_RDONLY | O_BINARY);
    //   if (fd < 0)
    //     return null;
    //
    //   /*
    //    * Under Linux, the lseek fails if we actually opened a directory.  To
    //    * be correct we should test the file type explicitly, but since we
    //    * always open things read-only it doesn't really matter, so there's
    //    * no value in incurring the extra overhead of an fstat() call.
    //    */
    //   // TODO(kroot): replace this with fstat despite the plea above.
    //   #if 1
    //   length = lseek64(fd, 0, SEEK_END);
    //   if (length < 0) {
    //   ::close(fd);
    //     return null;
    //   }
    //   (void) lseek64(fd, 0, SEEK_SET);
    //   #else
    //   struct stat st;
    //   if (fstat(fd, &st) < 0) {
    //   ::close(fd);
    //   return null;
    // }
    //
    //   if (!S_ISREG(st.st_mode)) {
    //   ::close(fd);
    //     return null;
    //   }
    //   #endif
    //
    //     pAsset = new _FileAsset;
    //   result = pAsset.openChunk(fileName, fd, 0, length);
    //   if (result != NO_ERROR) {
    //     delete pAsset;
    //     return null;
    //   }
    //
    //   pAsset.mAccessMode = mode;
    //   return pAsset;
  }

  /*
   * Create a new Asset from a compressed file on disk.  There is a fair chance
   * that the file doesn't actually exist.
   *
   * We currently support gzip files.  We might want to handle .bz2 someday.
   */
  static Asset createFromCompressedFile(final String fileName,
      AccessMode mode)
  {
    throw new UnsupportedOperationException();
    // _CompressedAsset pAsset;
    // int result;
    // long fileLen;
    // boolean scanResult;
    // long offset;
    // int method;
    // long uncompressedLen, compressedLen;
    // int fd;
    //
    // fd = open(fileName, O_RDONLY | O_BINARY);
    // if (fd < 0)
    //   return null;
    //
    // fileLen = lseek(fd, 0, SEEK_END);
    // if (fileLen < 0) {
    // ::close(fd);
    //   return null;
    // }
    // (void) lseek(fd, 0, SEEK_SET);
    //
    // /* want buffered I/O for the file scan; must dup so fclose() is safe */
    // FILE* fp = fdopen(dup(fd), "rb");
    // if (fp == null) {
    // ::close(fd);
    //   return null;
    // }
    //
    // unsigned long crc32;
    // scanResult = ZipUtils::examineGzip(fp, &method, &uncompressedLen,
    // &compressedLen, &crc32);
    // offset = ftell(fp);
    // fclose(fp);
    // if (!scanResult) {
    //   ALOGD("File '%s' is not in gzip format\n", fileName);
    // ::close(fd);
    //   return null;
    // }
    //
    // pAsset = new _CompressedAsset;
    // result = pAsset.openChunk(fd, offset, method, uncompressedLen,
    //     compressedLen);
    // if (result != NO_ERROR) {
    //   delete pAsset;
    //   return null;
    // }
    //
    // pAsset.mAccessMode = mode;
    // return pAsset;
  }

//     #if 0
// /*
//  * Create a new Asset from part of an open file.
//  */
// /*static*/ Asset createFromFileSegment(int fd, long offset,
//       int length, AccessMode mode)
//   {
//     _FileAsset pAsset;
//     int result;
//
//     pAsset = new _FileAsset;
//     result = pAsset.openChunk(null, fd, offset, length);
//     if (result != NO_ERROR)
//       return null;
//
//     pAsset.mAccessMode = mode;
//     return pAsset;
//   }
//
// /*
//  * Create a new Asset from compressed data in an open file.
//  */
// /*static*/ Asset createFromCompressedData(int fd, long offset,
//       int compressionMethod, int uncompressedLen, int compressedLen,
//       AccessMode mode)
//   {
//     _CompressedAsset pAsset;
//     int result;
//
//     pAsset = new _CompressedAsset;
//     result = pAsset.openChunk(fd, offset, compressionMethod,
//         uncompressedLen, compressedLen);
//     if (result != NO_ERROR)
//       return null;
//
//     pAsset.mAccessMode = mode;
//     return pAsset;
//   }
//     #endif

  /*
   * Create a new Asset from a memory mapping.
   */
  static Asset createFromUncompressedMap(FileMap dataMap,
      AccessMode mode)
  {
    _FileAsset pAsset;
    int result;

    pAsset = new _FileAsset();
    result = pAsset.openChunk(dataMap);
    if (result != NO_ERROR)
      return null;

    pAsset.mAccessMode = mode;
    return pAsset;
  }

  /*
   * Create a new Asset from compressed data in a memory mapping.
   */
static Asset createFromCompressedMap(FileMap dataMap,
      int uncompressedLen, AccessMode mode)
  {
    _CompressedAsset pAsset;
    int result;

    pAsset = new _CompressedAsset();
    result = pAsset.openChunk(dataMap, uncompressedLen);
    if (result != NO_ERROR)
      return null;

    pAsset.mAccessMode = mode;
    return pAsset;
  }

  /*
   * Do generic seek() housekeeping.  Pass in the offset/whence values from
   * the seek request, along with the current chunk offset and the chunk
   * length.
   *
   * Returns the new chunk offset, or -1 if the seek is illegal.
   */
  long handleSeek(long offset, int whence, long curPosn, long maxPosn)
  {
    long newOffset;

    switch (whence) {
      case SEEK_SET:
        newOffset = offset;
        break;
      case SEEK_CUR:
        newOffset = curPosn + offset;
        break;
      case SEEK_END:
        newOffset = maxPosn + offset;
        break;
      default:
        ALOGW("unexpected whence %d\n", whence);
        // this was happening due to an long size mismatch
        assert(false);
        return (long) -1;
    }

    if (newOffset < 0 || newOffset > maxPosn) {
      ALOGW("seek out of range: want %ld, end=%ld\n",
          (long) newOffset, (long) maxPosn);
      return (long) -1;
    }

    return newOffset;
  }

  /*
   * An asset based on an uncompressed file on disk.  It may encompass the
   * entire file or just a piece of it.  Access is through fread/fseek.
   */
  static class _FileAsset extends Asset {

    // public:
//     _FileAsset(void);
//     virtual ~_FileAsset(void);
//
//     /*
//      * Use a piece of an already-open file.
//      *
//      * On success, the object takes ownership of "fd".
//      */
//     int openChunk(final String fileName, int fd, long offset, int length);
//
//     /*
//      * Use a memory-mapped region.
//      *
//      * On success, the object takes ownership of "dataMap".
//      */
//     int openChunk(FileMap dataMap);
//
//     /*
//      * Standard Asset interfaces.
//      */
//     virtual ssize_t read(void* buf, int count);
//     virtual long seek(long offset, int whence);
//     virtual void close(void);
//     virtual final void* getBuffer(boolean wordAligned);

    @Override
    public long getLength() { return mLength; }

    @Override
    public long getRemainingLength() { return mLength-mOffset; }

//     virtual int openFileDescriptor(long* outStart, long* outLength) final;
    @Override
    boolean isAllocated() { return mBuf != null; }

    @Override
    public boolean isNinePatch() {
      String fileName = getFileName();
      if (mMap != null) {
        fileName = mMap.getZipEntry().getName();
      }
      return fileName != null && fileName.toLowerCase().endsWith(".9.png");
    }

    //
// private:
    long mStart;         // absolute file offset of start of chunk
    long mLength;        // length of the chunk
    long mOffset;        // current local offset, 0 == mStart
    // FILE*       mFp;            // for read/seek
    RandomAccessFile mFp;            // for read/seek
    String mFileName;      // for opening

    /*
     * To support getBuffer() we either need to read the entire thing into
     * a buffer or memory-map it.  For small files it's probably best to
     * just read them in.
     */
// enum {
  public static int kReadVsMapThreshold = 4096;
// };

    FileMap mMap;           // for memory map
    byte[] mBuf;        // for read

    // final void* ensureAlignment(FileMap map);
/*
 * ===========================================================================
 *      _FileAsset
 * ===========================================================================
 */

    /*
     * Constructor.
     */
    _FileAsset()
    // : mStart(0), mLength(0), mOffset(0), mFp(null), mFileName(null), mMap(null), mBuf(null)
    {
      // Register the Asset with the global list here after it is fully constructed and its
      // vtable pointer points to this concrete type. b/31113965
      registerAsset(this);
    }

    /*
     * Destructor.  Release resources.
     */
    @Override
    protected void finalize() {
      close();

      // Unregister the Asset from the global list here before it is destructed and while its vtable
      // pointer still points to this concrete type. b/31113965
      unregisterAsset(this);
    }

    /*
     * Operate on a chunk of an uncompressed file.
     *
     * Zero-length chunks are allowed.
     */
    int openChunk(final String fileName, int fd, long offset, int length) {
      throw new UnsupportedOperationException();
      // assert(mFp == null);    // no reopen
      // assert(mMap == null);
      // assert(fd >= 0);
      // assert(offset >= 0);
      //
      // /*
      //  * Seek to end to get file length.
      //  */
      // long fileLength;
      // fileLength = lseek64(fd, 0, SEEK_END);
      // if (fileLength == (long) -1) {
      //   // probably a bad file descriptor
      //   ALOGD("failed lseek (errno=%d)\n", errno);
      //   return UNKNOWN_ERROR;
      // }
      //
      // if ((long) (offset + length) > fileLength) {
      //   ALOGD("start (%ld) + len (%ld) > end (%ld)\n",
      //       (long) offset, (long) length, (long) fileLength);
      //   return BAD_INDEX;
      // }
      //
      // /* after fdopen, the fd will be closed on fclose() */
      // mFp = fdopen(fd, "rb");
      // if (mFp == null)
      //   return UNKNOWN_ERROR;
      //
      // mStart = offset;
      // mLength = length;
      // assert(mOffset == 0);
      //
      // /* seek the FILE* to the start of chunk */
      // if (fseek(mFp, mStart, SEEK_SET) != 0) {
      //   assert(false);
      // }
      //
      // mFileName = fileName != null ? strdup(fileName) : null;
      //
      // return NO_ERROR;
    }

    /*
     * Create the chunk from the map.
     */
    int openChunk(FileMap dataMap) {
      assert(mFp == null);    // no reopen
      assert(mMap == null);
      assert(dataMap != null);

      mMap = dataMap;
      mStart = -1;            // not used
      mLength = dataMap.getDataLength();
      assert(mOffset == 0);

      mBuf = dataMap.getDataPtr();

      return NO_ERROR;
    }

    /*
     * Read a chunk of data.
     */
    @Override
    public int read(byte[] buf, int bufOffset, int count) {
      int maxLen;
      int actual;

      assert(mOffset >= 0 && mOffset <= mLength);

      if (getAccessMode() == ACCESS_BUFFER) {
          /*
           * On first access, read or map the entire file.  The caller has
           * requested buffer access, either because they're going to be
           * using the buffer or because what they're doing has appropriate
           * performance needs and access patterns.
           */
        if (mBuf == null)
          getBuffer(false);
      }

      /* adjust count if we're near EOF */
      maxLen = toIntExact(mLength - mOffset);
      if (count > maxLen)
        count = maxLen;

      if (!isTruthy(count)) {
        return 0;
      }

      if (mMap != null) {
          /* copy from mapped area */
        //printf("map read\n");
        // memcpy(buf, (String)mMap.getDataPtr() + mOffset, count);
        System.arraycopy(mMap.getDataPtr(), toIntExact(mOffset), buf, bufOffset, count);
        actual = count;
      } else if (mBuf != null) {
          /* copy from buffer */
        //printf("buf read\n");
        // memcpy(buf, (String)mBuf + mOffset, count);
        System.arraycopy(mBuf, toIntExact(mOffset), buf, bufOffset, count);
        actual = count;
      } else {
          /* read from the file */
        //printf("file read\n");
        // if (ftell(mFp) != mStart + mOffset) {
        try {
          if (mFp.getFilePointer() != mStart + mOffset) {
            ALOGE("Hosed: %ld != %ld+%ld\n",
                mFp.getFilePointer(), (long) mStart, (long) mOffset);
            assert(false);
          }

          /*
           * This returns 0 on error or eof.  We need to use ferror() or feof()
           * to tell the difference, but we don't currently have those on the
           * device.  However, we know how much data is *supposed* to be in the
           * file, so if we don't read the full amount we know something is
           * hosed.
           */
          actual = mFp.read(buf, 0, count);
          if (actual == 0)        // something failed -- I/O error?
            return -1;

          assert(actual == count);
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }

      mOffset += actual;
      return actual;
    }

    /*
     * Seek to a new position.
     */
    @Override
    public long seek(long offset, int whence) {
      long newPosn;
      long actualOffset;

      // compute new position within chunk
      newPosn = handleSeek(offset, whence, mOffset, mLength);
      if (newPosn == (long) -1)
        return newPosn;

      actualOffset = mStart + newPosn;

      if (mFp != null) {
        throw new UnsupportedOperationException();
        // if (fseek(mFp, (long) actualOffset, SEEK_SET) != 0)
        //   return (long) -1;
      }

      mOffset = actualOffset - mStart;
      return mOffset;
    }

    /*
     * Close the asset.
     */
    @Override
    public void close() {
      throw new UnsupportedOperationException();
      // if (mMap != null) {
      //   delete mMap;
      //   mMap = null;
      // }
      // if (mBuf != null) {
      //   delete[] mBuf;
      //   mBuf = null;
      // }
      //
      // if (mFileName != null) {
      //   free(mFileName);
      //   mFileName = null;
      // }
      //
      // if (mFp != null) {
      //   // can only be null when called from destructor
      //   // (otherwise we would never return this object)
      //   fclose(mFp);
      //   mFp = null;
      // }
    }

    /*
     * Return a read-only pointer to a buffer.
     *
     * We can either read the whole thing in or map the relevant piece of
     * the source file.  Ideally a map would be established at a higher
     * level and we'd be using a different object, but we didn't, so we
     * deal with it here.
     */
    @Override
    public final byte[] getBuffer(boolean wordAligned) {
      /* subsequent requests just use what we did previously */
      if (mBuf != null)
        return mBuf;
      if (mMap != null) {
        // if (!wordAligned) {
          return  mMap.getDataPtr();
        // }
        // return ensureAlignment(mMap);
      }

      // assert(mFp != null);

      if (true /*mLength < kReadVsMapThreshold*/) {
        byte[] buf;
        int allocLen;

          /* zero-length files are allowed; not sure about zero-len allocs */
          /* (works fine with gcc + x86linux) */
        allocLen = toIntExact(mLength);
        if (mLength == 0)
          allocLen = 1;

        buf = new byte[allocLen];
        if (buf == null) {
          ALOGE("alloc of %ld bytes failed\n", (long) allocLen);
          return null;
        }

        ALOGV("Asset %s allocating buffer size %d (smaller than threshold)", this, (int)allocLen);
        if (mLength > 0) {
          try {
            // long oldPosn = ftell(mFp);
            long oldPosn = mFp.getFilePointer();
            // fseek(mFp, mStart, SEEK_SET);
            mFp.seek(mStart);
            // if (fread(buf, 1, mLength, mFp) != (size_t) mLength) {
            if (mFp.read(buf, 0, toIntExact(mLength)) != (int) mLength) {
              ALOGE("failed reading %ld bytes\n", (long) mLength);
              // delete[] buf;
              return null;
            }
            // fseek(mFp, oldPosn, SEEK_SET);
            mFp.seek(oldPosn);
          } catch (IOException e) {
            throw new RuntimeException(e);
          }
        }

        ALOGV(" getBuffer: loaded into buffer\n");

        mBuf = buf;
        return mBuf;
      } else {
        FileMap map;

        map = new FileMap();
        // if (!map.create(null, fileno(mFp), mStart, mLength, true)) {
        if (!map.create(null, -1, mStart, toIntExact(mLength), true)) {
          // delete map;
          return null;
        }

        ALOGV(" getBuffer: mapped\n");

        mMap = map;
        // if (!wordAligned) {
        //   return  mMap.getDataPtr();
        // }
        return ensureAlignment(mMap);
      }
    }

    /**
     * Return the file on disk representing this asset.
     *
     * Non-Android framework method. Based on {@link #openFileDescriptor(Ref, Ref)}.
     */
    @Override
    public File getFile() {
      if (mMap != null) {
        String fname = mMap.getFileName();
        if (fname == null) {
          fname = mFileName;
        }
        if (fname == null) {
          return null;
        }
        // return open(fname, O_RDONLY | O_BINARY);
        return new File(fname);
      }
      if (mFileName == null) {
        return null;
      }
      return new File(mFileName);
    }

    @Override
    public String getFileName() {
      File file = getFile();
      return file == null ? null : file.getName();
    }

    @Override
    public FileDescriptor openFileDescriptor(Ref<Long> outStart, Ref<Long> outLength) {
      if (mMap != null) {
        String fname = mMap.getFileName();
        if (fname == null) {
          fname = mFileName;
        }
        if (fname == null) {
          return null;
        }
        outStart.set(mMap.getDataOffset());
        outLength.set((long) mMap.getDataLength());
        // return open(fname, O_RDONLY | O_BINARY);
        return open(fname);
      }
      if (mFileName == null) {
        return null;
      }
      outStart.set(mStart);
      outLength.set(mLength);
      // return open(mFileName, O_RDONLY | O_BINARY);
      return open(mFileName);
    }

    private static FileDescriptor open(String fname) {
      try {
        return new FileInputStream(new File(fname)).getFD();
      } catch (IOException e) {
        return null;
      }
    }

    final byte[] ensureAlignment(FileMap map) {
      throw new UnsupportedOperationException();
      //   void* data = map.getDataPtr();
      //   if ((((int)data)&0x3) == 0) {
      //     // We can return this directly if it is aligned on a word
      //     // boundary.
      //     ALOGV("Returning aligned FileAsset %s (%s).", this,
      //         getAssetSource());
      //     return data;
      //   }
      //   // If not aligned on a word boundary, then we need to copy it into
      //   // our own buffer.
      //   ALOGV("Copying FileAsset %s (%s) to buffer size %d to make it aligned.", this,
      //       getAssetSource(), (int)mLength);
      //   unsigned String buf = new unsigned char[mLength];
      //   if (buf == null) {
      //     ALOGE("alloc of %ld bytes failed\n", (long) mLength);
      //     return null;
      //   }
      //   memcpy(buf, data, mLength);
      //   mBuf = buf;
      //   return buf;
      // }
    }

    @Override
    public String toString() {
      if (mFileName == null) {
        return "_FileAsset{" +
            "mMap=" + mMap +
            '}';
      } else {
        return "_FileAsset{" +
            "mFileName='" + mFileName + '\'' +
            '}';
      }
    }
  }

  /*
   * An asset based on compressed data in a file.
   */
  static class _CompressedAsset extends Asset {
// public:
//     _CompressedAsset(void);
//     virtual ~_CompressedAsset(void);
//
//     /*
//      * Use a piece of an already-open file.
//      *
//      * On success, the object takes ownership of "fd".
//      */
//     int openChunk(int fd, long offset, int compressionMethod,
//     int uncompressedLen, int compressedLen);
//
//     /*
//      * Use a memory-mapped region.
//      *
//      * On success, the object takes ownership of "fd".
//      */
//     int openChunk(FileMap dataMap, int uncompressedLen);
//
//     /*
//      * Standard Asset interfaces.
//      */
//     virtual ssize_t read(void* buf, int count);
//     virtual long seek(long offset, int whence);
//     virtual void close(void);
//     virtual final void* getBuffer(boolean wordAligned);

    @Override
    public long getLength() { return mUncompressedLen; }

    @Override
    public long getRemainingLength() { return mUncompressedLen-mOffset; }

    @Override
    public File getFile() {
      return null;
    }

    @Override
    public String getFileName() {
      ZipEntry zipEntry = mMap.getZipEntry();
      return zipEntry == null ? null : zipEntry.getName();
    }

    @Override
    public FileDescriptor openFileDescriptor(Ref<Long> outStart, Ref<Long> outLength) { return null; }

    @Override
    boolean isAllocated() { return mBuf != null; }

    @Override
    public boolean isNinePatch() {
      String fileName = getFileName();
      return fileName != null && fileName.toLowerCase().endsWith(".9.png");
    }

    // private:
    long mStart;         // offset to start of compressed data
    long mCompressedLen; // length of the compressed data
    long mUncompressedLen; // length of the uncompressed data
    long mOffset;        // current offset, 0 == start of uncomp data

    FileMap mMap;           // for memory-mapped input
    int mFd;            // for file input

// class StreamingZipInflater mZipInflater;  // for streaming large compressed assets

    byte[] mBuf;       // for getBuffer()
/*
 * ===========================================================================
 *      _CompressedAsset
 * ===========================================================================
 */

    /*
     * Constructor.
     */
    _CompressedAsset()
    // : mStart(0), mCompressedLen(0), mUncompressedLen(0), mOffset(0),
    // mMap(null), mFd(-1), mZipInflater(null), mBuf(null)
    {
      mFd = -1;

      // Register the Asset with the global list here after it is fully constructed and its
      // vtable pointer points to this concrete type. b/31113965
      registerAsset(this);
    }

    ZipFile zipFile;
    String entryName;

    // @Override
    // public byte[] getBuffer(boolean wordAligned) {
    //   ZipEntry zipEntry = zipFile.getEntry(entryName);
    //   int size = (int) zipEntry.getSize();
    //   byte[] buf = new byte[size];
    //   try (InputStream in = zipFile.getInputStream(zipEntry)) {
    //     if (in.read(buf) != size) {
    //       throw new IOException(
    //           "Failed to read " + size + " bytes from " + zipFile + "!" + entryName);
    //     }
    //     return buf;
    //   } catch (IOException e) {
    //     throw new RuntimeException(e);
    //   }
    // }

    /*
     * Destructor.  Release resources.
     */
    @Override
    protected void finalize() {
      close();

      // Unregister the Asset from the global list here before it is destructed and while its vtable
      // pointer still points to this concrete type. b/31113965
      unregisterAsset(this);
    }

    /*
     * Open a chunk of compressed data inside a file.
     *
     * This currently just sets up some values and returns.  On the first
     * read, we expand the entire file into a buffer and return data from it.
     */
    int openChunk(int fd, long offset,
        int compressionMethod, int uncompressedLen, int compressedLen) {
      throw new UnsupportedOperationException();
      // assert(mFd < 0);        // no re-open
      // assert(mMap == null);
      // assert(fd >= 0);
      // assert(offset >= 0);
      // assert(compressedLen > 0);
      //
      // if (compressionMethod != ZipFileRO::kCompressDeflated) {
      // assert(false);
      // return UNKNOWN_ERROR;
      // }
      //
      // mStart = offset;
      // mCompressedLen = compressedLen;
      // mUncompressedLen = uncompressedLen;
      // assert(mOffset == 0);
      // mFd = fd;
      // assert(mBuf == null);
      //
      // if (uncompressedLen > StreamingZipInflater::OUTPUT_CHUNK_SIZE) {
      // mZipInflater = new StreamingZipInflater(mFd, offset, uncompressedLen, compressedLen);
      // }
      //
      // return NO_ERROR;
    }

    /*
     * Open a chunk of compressed data in a mapped region.
     *
     * Nothing is expanded until the first read call.
     */
    int openChunk(FileMap dataMap, int uncompressedLen) {
      assert(mFd < 0);        // no re-open
      assert(mMap == null);
      assert(dataMap != null);

      mMap = dataMap;
      mStart = -1;        // not used
      mCompressedLen = dataMap.getDataLength();
      mUncompressedLen = uncompressedLen;
      assert(mOffset == 0);

      // if (uncompressedLen > StreamingZipInflater::OUTPUT_CHUNK_SIZE) {
      // mZipInflater = new StreamingZipInflater(dataMap, uncompressedLen);
      // }
      return NO_ERROR;
    }

    /*
     * Read data from a chunk of compressed data.
     *
     * [For now, that's just copying data out of a buffer.]
     */
    @Override
    public int read(byte[] buf, int bufOffset, int count) {
      int maxLen;
      int actual;

      assert(mOffset >= 0 && mOffset <= mUncompressedLen);

       /* If we're relying on a streaming inflater, go through that */
//       if (mZipInflater) {
//       actual = mZipInflater.read(buf, count);
//       } else {
      if (mBuf == null) {
        if (getBuffer(false) == null)
          return -1;
      }
      assert(mBuf != null);

      /* adjust count if we're near EOF */
      maxLen = toIntExact(mUncompressedLen - mOffset);
      if (count > maxLen)
        count = maxLen;

      if (!isTruthy(count))
        return 0;

      /* copy from buffer */
      //printf("comp buf read\n");
//      memcpy(buf, (String)mBuf + mOffset, count);
      System.arraycopy(mBuf, toIntExact(mOffset), buf, bufOffset, count);
      actual = count;
//       }

      mOffset += actual;
      return actual;
    }

    /*
     * Handle a seek request.
     *
     * If we're working in a streaming mode, this is going to be fairly
     * expensive, because it requires plowing through a bunch of compressed
     * data.
     */
    @Override
    public long seek(long offset, int whence) {
      long newPosn;

      // compute new position within chunk
      newPosn = handleSeek(offset, whence, mOffset, mUncompressedLen);
      if (newPosn == (long) -1)
      return newPosn;

      // if (mZipInflater) {
      //   mZipInflater.seekAbsolute(newPosn);
      // }
      mOffset = newPosn;
      return mOffset;
    }

    /*
     * Close the asset.
     */
    @Override
    public void close() {
       if (mMap != null) {
//       delete mMap;
       mMap = null;
       }

//       delete[] mBuf;
       mBuf = null;

//       delete mZipInflater;
//       mZipInflater = null;

       if (mFd > 0) {
//       ::close(mFd);
       mFd = -1;
       }
    }

    /*
     * Get a pointer to a read-only buffer of data.
     *
     * The first time this is called, we expand the compressed data into a
     * buffer.
     */
    @Override
    public byte[] getBuffer(boolean wordAligned) {
      // return mBuf = mMap.getDataPtr();
      byte[] buf = null;

      if (mBuf != null)
        return mBuf;

      /*
       * Allocate a buffer and read the file into it.
       */
      // buf = new byte[(int) mUncompressedLen];
      // if (buf == null) {
      //   ALOGW("alloc %ld bytes failed\n", (long) mUncompressedLen);
      //   return null;
      // }

      if (mMap != null) {
        buf = mMap.getDataPtr();
        // if (!ZipUtils::inflateToBuffer(mMap.getDataPtr(), buf,
        //     mUncompressedLen, mCompressedLen))
        // return null;
      } else {
        throw new UnsupportedOperationException();
        // assert(mFd >= 0);
        //
        // /*
        //    * Seek to the start of the compressed data.
        //    */
        // if (lseek(mFd, mStart, SEEK_SET) != mStart)
        // goto bail;
        //
        // /*
        //    * Expand the data into it.
        //    */
        // if (!ZipUtils::inflateToBuffer(mFd, buf, mUncompressedLen,
        //     mCompressedLen))
        // goto bail;
      }

      /*
       * Success - now that we have the full asset in RAM we
       * no longer need the streaming inflater
       */
      // delete mZipInflater;
      // mZipInflater = null;

      mBuf = buf;
      // buf = null;

      // bail:
      // delete[] buf;
      return mBuf;
    }

    @Override
    public String toString() {
      return "_CompressedAsset{" +
          "mMap=" + mMap +
          '}';
    }
  }

  // todo: remove when Android supports this
  static int toIntExact(long value) {
    if ((int)value != value) {
      throw new ArithmeticException("integer overflow");
    }
    return (int)value;
  }
}


package org.robolectric.shadows;

import android.os.DropBoxManager;
import android.os.DropBoxManager.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

/** Fake dropbox manager that starts with no entries. */
@Implements(value = DropBoxManager.class)
public class ShadowDropBoxManager {
  private final SortedMap<Long, Entry> entries = new TreeMap<>();

  public ShadowDropBoxManager() {
    reset();
  }

  /**
   * Adds entry to the DropboxManager with the flag indicating data is text.
   *
   *
The existing {@link DropBoxManager#addData}, {@link DropBoxManager#addFile}, and {@link
   * DropBoxManager#addText} methods in DropBoxManager are not shadowed. This method is a
   * convenience for quickly adding multiple historical entries. The entries can be added in any
   * order since this shadow will sort the entries by the specified timestamp.
   *
   *
The flag will be set to {@link DropBoxManager#IS_TEXT} so that {@link
   * DropBoxManager.Entry#getText} can be used.
   *
   * @param tag can be any arbitrary string
   * @param timestamp is an arbitrary timestamp that must be unique from all other entries
   * @param data must not be null
   */
  void addData(String tag, long timestamp, byte[] data) {
    entries.put(timestamp, new DropBoxManager.Entry(tag, timestamp, data, DropBoxManager.IS_TEXT));
  }

  /**
   * Clears all entries.
   */
  public void reset() {
    entries.clear();
  }

  @Implementation
  protected DropBoxManager.Entry getNextEntry(String tag, long millis) {
    for (DropBoxManager.Entry entry : entries.tailMap(millis).values()) {
      if ((tag != null && !entry.getTag().equals(tag)) || entry.getTimeMillis() <= millis) {
        continue;
      }
      return entry;
    }
    return null;
  }
}


package org.robolectric.shadows.support.v4;

import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE;
import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE;

import android.content.ComponentName;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaBrowserCompat.ConnectionCallback;
import android.support.v4.media.MediaBrowserCompat.ItemCallback;
import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.MediaBrowserCompat.SearchCallback;
import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
import android.support.v4.media.MediaBrowserServiceCompat;
import android.support.v4.media.MediaMetadataCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.ClassParameter;

/**
 * This will mimic the connection to a {@link MediaBrowserServiceCompat} by creating and maintaining
 * its own account of {@link MediaItem}s.
 */
@Implements(MediaBrowserCompat.class)
public class ShadowMediaBrowserCompat {

  private final Handler handler = new Handler();
  private @RealObject MediaBrowserCompat mediaBrowser;

  private final Map<String, MediaItem> mediaItems = new LinkedHashMap<>();
  private final Map<MediaItem, List<MediaItem>> mediaItemChildren = new LinkedHashMap<>();

  private boolean isConnected;
  private ConnectionCallback connectionCallback;
  private String rootId = "root_id";

  @Implementation
  protected void __constructor__(
      Context context,
      ComponentName serviceComponent,
      ConnectionCallback callback,
      Bundle rootHints) {
    connectionCallback = callback;
    Shadow.invokeConstructor(
        MediaBrowserCompat.class,
        mediaBrowser,
        ClassParameter.from(Context.class, context),
        ClassParameter.from(ComponentName.class, serviceComponent),
        ClassParameter.from(ConnectionCallback.class, callback),
        ClassParameter.from(Bundle.class, rootHints));
  }

  @Implementation
  protected void connect() {
    handler.post(
        () -> {
          isConnected = true;
          connectionCallback.onConnected();
        });
  }

  @Implementation
  protected void disconnect() {
    handler.post(
        () -> {
          isConnected = false;
        });
  }

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

  @Implementation
  protected String getRoot() {
    if (!isConnected) {
      throw new IllegalStateException("Can't call getRoot() while not connected.");
    }
    return rootId;
  }

  @Implementation
  protected void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
    // mediaItem will be null when there is no MediaItem that matches the given mediaId.
    final MediaItem mediaItem = mediaItems.get(mediaId);

    if (isConnected && mediaItem != null) {
      handler.post(() -> cb.onItemLoaded(mediaItem));
    } else {
      handler.post(() -> cb.onError(mediaId));
    }
  }

  @Implementation
  protected void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
    subscribe(parentId, null, callback);
  }

  @Implementation
  protected void subscribe(
      @NonNull String parentId, @Nullable Bundle options, @NonNull SubscriptionCallback callback) {
    if (isConnected) {
      final MediaItem parentItem = mediaItems.get(parentId);
      List<MediaItem> children =
          mediaItemChildren.get(parentItem) == null
              ? Collections.emptyList()
              : mediaItemChildren.get(parentItem);
      handler.post(
          () -> callback.onChildrenLoaded(parentId, applyOptionsToResults(children, options)));
    } else {
      handler.post(() -> callback.onError(parentId));
    }
  }

  private List<MediaItem> applyOptionsToResults(List<MediaItem> results, final Bundle options) {
    if (results == null || options == null) {
      return results;
    }
    final int resultsSize = results.size();
    final int page = options.getInt(EXTRA_PAGE, -1);
    final int pageSize = options.getInt(EXTRA_PAGE_SIZE, -1);
    if (page == -1 && pageSize == -1) {
      return results;
    }

    final int firstItemIndex = page * pageSize;
    final int lastItemIndex = firstItemIndex + pageSize;
    if (page < 0 || pageSize < 1 || firstItemIndex >= resultsSize) {
      return Collections.emptyList();
    }
    return results.subList(firstItemIndex, Math.min(lastItemIndex, resultsSize));
  }

  /**
   * This differs from real Android search logic. Search results will contain all {@link
   * MediaItem}'s with a title that {@param query} is a substring of.
   */
  @Implementation
  protected void search(
      @NonNull final String query, final Bundle extras, @NonNull SearchCallback callback) {
    if (isConnected) {
      final List<MediaItem> searchResults = new ArrayList<>();
      for (MediaItem item : mediaItems.values()) {
        final String mediaTitle = item.getDescription().getTitle().toString().toLowerCase();
        if (mediaTitle.contains(query.toLowerCase())) {
          searchResults.add(item);
        }
      }
      handler.post(() -> callback.onSearchResult(query, extras, searchResults));
    } else {
      handler.post(() -> callback.onError(query, extras));
    }
  }

  /**
   * Sets the root id. Can be called more than once.
   *
   * @param mediaId the id of the root MediaItem. This MediaItem should already have been created.
   */
  public void setRootId(String mediaId) {
    rootId = mediaId;
  }

  /**
   * Creates a MediaItem and returns it.
   *
   * @param parentId the id of the parent MediaItem. If the MediaItem to be created will be the
   *     root, parentId should be null.
   * @param mediaId the id of the MediaItem to be created.
   * @param title the title of the MediaItem to be created.
   * @param flag says if the MediaItem to be created is browsable and/or playable.
   * @return the newly created MediaItem.
   */
  public MediaItem createMediaItem(String parentId, String mediaId, String title, int flag) {
    final MediaMetadataCompat metadataCompat =
        new MediaMetadataCompat.Builder()
            .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
            .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
            .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, Uri.parse(mediaId).toString())
            .build();
    final MediaItem mediaItem = new MediaItem(metadataCompat.getDescription(), flag);
    mediaItems.put(mediaId, mediaItem);

    // If this MediaItem is the child of a MediaItem that has already been created. This applies to
    // all MediaItems except the root.
    if (parentId != null) {
      final MediaItem parentItem = mediaItems.get(parentId);
      List<MediaItem> children = mediaItemChildren.get(parentItem);
      if (children == null) {
        children = new ArrayList<>();
        mediaItemChildren.put(parentItem, children);
      }
      children.add(mediaItem);
    }

    return mediaItem;
  }

  /** @return a copy of the internal {@link Map} that maps {@link MediaItem}s to their children. */
  public Map<MediaItem, List<MediaItem>> getCopyOfMediaItemChildren() {
    final Map<MediaItem, List<MediaItem>> copyOfMediaItemChildren = new LinkedHashMap<>();
    for (MediaItem parent : mediaItemChildren.keySet()) {
      List<MediaItem> children = new ArrayList<>(mediaItemChildren.get(parent));
      copyOfMediaItemChildren.put(parent, children);
    }
    return copyOfMediaItemChildren;
  }
}


package org.robolectric.internal.bytecode;

import static org.robolectric.internal.bytecode.OldClassInstrumentor.HANDLE_EXCEPTION_METHOD;
import static org.robolectric.internal.bytecode.OldClassInstrumentor.ROBOLECTRIC_INTERNALS_TYPE;
import static org.robolectric.internal.bytecode.OldClassInstrumentor.THROWABLE_TYPE;

import com.google.auto.service.AutoService;
import javax.annotation.Priority;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.robolectric.internal.bytecode.ClassInstrumentor.TryCatch;

/** Decorator which adds Robolectric's shadowing behavior to a class. */
@AutoService(ClassInstrumentor.Decorator.class)
@Priority(Integer.MIN_VALUE)
public class ShadowDecorator implements ClassInstrumentor.Decorator {
  private static final String OBJECT_DESC = Type.getDescriptor(Object.class);
  private static final Type OBJECT_TYPE = Type.getType(Object.class);
  private static final String GET_ROBO_DATA_SIGNATURE = "()Ljava/lang/Object;";

  @Override
  public void decorate(MutableClass mutableClass) {
    mutableClass.addInterface(Type.getInternalName(ShadowedObject.class));

    mutableClass.addField(0, new FieldNode(Opcodes.ACC_PUBLIC,
        ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_DESC, OBJECT_DESC, null));

    addRoboGetDataMethod(mutableClass);
  }

  /**
   * For non-invokedynamic JVMs, generates this code:
   * ```java
   * if (__robo_data__ instanceof ThisClass) {
   *   try {
   *     return __robo_data__.$$robo$$originalMethod(params);
   *   } (Throwable t) {
   *     throw RobolectricInternals.cleanStackTrace(t);
   *   }
   * }
   * ```
   *
   * Note that this method is only called by {@link OldClassInstrumentor}.
   */
  @Override
  public void decorateMethodPreClassHandler(MutableClass mutableClass, MethodNode originalMethod,
      String originalMethodName, RobolectricGeneratorAdapter generator) {
    boolean isNormalInstanceMethod = !generator.isStatic
        && !originalMethodName.equals(ShadowConstants.CONSTRUCTOR_METHOD_NAME);
    // maybe perform direct call...
    if (isNormalInstanceMethod) {
      int exceptionLocalVar = generator.newLocal(THROWABLE_TYPE);
      Label notInstanceOfThis = new Label();

      generator.loadThis();                                         // this
      generator.getField(mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of this.__robo_data__
      generator.instanceOf(mutableClass.classType);                 // __robo_data__, is instance of same class?
      generator.visitJumpInsn(Opcodes.IFEQ, notInstanceOfThis);     // jump if no (is not instance)

      TryCatch tryCatchForProxyCall = generator.tryStart(THROWABLE_TYPE);
      generator.loadThis();                                         // this
      generator.getField(mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of this.__robo_data__
      generator.checkCast(mutableClass.classType);                  // __robo_data__ but cast to my class
      generator.loadArgs();                                         // __robo_data__ instance, [args]

      generator.visitMethodInsn(Opcodes.INVOKESPECIAL, mutableClass.internalClassName, originalMethod.name, originalMethod.desc, false);
      tryCatchForProxyCall.end();

      generator.returnValue();

      // catch(Throwable)
      tryCatchForProxyCall.handler();
      generator.storeLocal(exceptionLocalVar);
      generator.loadLocal(exceptionLocalVar);
      generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD);
      generator.throwException();

      // callClassHandler...
      generator.mark(notInstanceOfThis);
    }
  }

  private void addRoboGetDataMethod(MutableClass mutableClass) {
    MethodNode initMethodNode = new MethodNode(Opcodes.ACC_PUBLIC, ShadowConstants.GET_ROBO_DATA_METHOD_NAME, GET_ROBO_DATA_SIGNATURE, null, null);
    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
    generator.loadThis();                                         // this
    generator.getField(mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);  // contents of __robo_data__
    generator.returnValue();
    generator.endMethod();
    mutableClass.addMethod(initMethodNode);
  }
}


package org.robolectric.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/** Collection of helper methods for calling methods and accessing fields reflectively. */
@SuppressWarnings(value = {"unchecked", "TypeParameterUnusedInFormals", "NewApi"})
public class ReflectionHelpers {

  private static final Map<String, Object> PRIMITIVE_RETURN_VALUES;

  static {
    HashMap<String, Object> map = new HashMap<>();
    map.put("boolean", Boolean.FALSE);
    map.put("int", 0);
    map.put("long", (long) 0);
    map.put("float", (float) 0);
    map.put("double", (double) 0);
    map.put("short", (short) 0);
    map.put("byte", (byte) 0);
    PRIMITIVE_RETURN_VALUES = Collections.unmodifiableMap(map);
  }

  public static <T> T createNullProxy(Class<T> clazz) {
    return (T)
        Proxy.newProxyInstance(
            clazz.getClassLoader(),
            new Class<?>[] {clazz},
            (proxy, method, args) -> PRIMITIVE_RETURN_VALUES.get(method.getReturnType().getName()));
  }

  /**
   * Create a proxy for the given class which returns other deep proxies from all it's methods.
   *
   *
The returned object will be an instance of the given class, but all methods will return
   * either the "default" value for primitives, or another deep proxy for non-primitive types.
   *
   *
This should be used rarely, for cases where we need to create deep proxies in order not
   * to crash. The inner proxies are impossible to configure, so there is no way to create
   * meaningful behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions.
   * @param clazz the class to provide a proxy instance of.
   * @return a new "Deep Proxy" instance of the given class.
   */
  public static <T> T createDeepProxy(Class<T> clazz) {
    return (T)
        Proxy.newProxyInstance(
            clazz.getClassLoader(),
            new Class[] {clazz},
            new InvocationHandler() {
              @Override
              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (PRIMITIVE_RETURN_VALUES.containsKey(method.getReturnType().getName())) {
                  return PRIMITIVE_RETURN_VALUES.get(method.getReturnType().getName());
                } else if (method.getReturnType().isInterface()) {
                  return createDeepProxy(method.getReturnType());
                } else {
                  return null;
                }
              }
            });
  }

  public static <T> T createDelegatingProxy(Class<T> clazz, final Object delegate) {
    final Class delegateClass = delegate.getClass();
    return (T) Proxy.newProxyInstance(clazz.getClassLoader(),
        new Class[]{clazz}, new InvocationHandler() {
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
              Method delegateMethod = delegateClass.getMethod(method.getName(), method.getParameterTypes());
              delegateMethod.setAccessible(true);
              return delegateMethod.invoke(delegate, args);
            } catch (NoSuchMethodException e) {
              return PRIMITIVE_RETURN_VALUES.get(method.getReturnType().getName());
            }
          }
        });
  }

  public static  A defaultsFor(Class annotation) {
    return annotation.cast(
        Proxy.newProxyInstance(annotation.getClassLoader(), new Class[] { annotation },
            new InvocationHandler() {
              @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return method.getDefaultValue();
              }
            }));
  }

  /**
   * Reflectively get the value of a field.
   *
   * @param object Target object.
   * @param fieldName The field name.
   * @param <R> The return type.
   * @return Value of the field on the object.
   */
  @SuppressWarnings("unchecked")
  public static <R> R getField(final Object object, final String fieldName) {
    try {
      return traverseClassHierarchy(object.getClass(), NoSuchFieldException.class, new InsideTraversal<R>() {
        @Override
        public R run(Class<?> traversalClass) throws Exception {
          Field field = traversalClass.getDeclaredField(fieldName);
          field.setAccessible(true);
          return (R) field.get(object);
        }
      });
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively set the value of a field.
   *
   * @param object Target object.
   * @param fieldName The field name.
   * @param fieldNewValue New value.
   */
  public static void setField(final Object object, final String fieldName, final Object fieldNewValue) {
    try {
      traverseClassHierarchy(object.getClass(), NoSuchFieldException.class, new InsideTraversal<Void>() {
        @Override
        public Void run(Class<?> traversalClass) throws Exception {
          Field field = traversalClass.getDeclaredField(fieldName);
          field.setAccessible(true);
          field.set(object, fieldNewValue);
          return null;
        }
      });
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively set the value of a field.
   *
   * @param type Target type.
   * @param object Target object.
   * @param fieldName The field name.
   * @param fieldNewValue New value.
   */
  public static void setField(Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) {
    try {
      Field field = type.getDeclaredField(fieldName);
      field.setAccessible(true);
      field.set(object, fieldNewValue);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively get the value of a static field.
   *
   * @param field Field object.
   * @param <R> The return type.
   * @return Value of the field.
   */
  @SuppressWarnings("unchecked")
  public static <R> R getStaticField(Field field) {
    try {
      makeFieldVeryAccessible(field);
      return (R) field.get(null);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively get the value of a static field.
   *
   * @param clazz Target class.
   * @param fieldName The field name.
   * @param <R> The return type.
   * @return Value of the field.
   */
  public static <R> R getStaticField(Class<?> clazz, String fieldName) {
    try {
      return getStaticField(clazz.getDeclaredField(fieldName));
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively set the value of a static field.
   *
   * @param field Field object.
   * @param fieldNewValue The new value.
   */
  public static void setStaticField(Field field, Object fieldNewValue) {
    try {
      makeFieldVeryAccessible(field);
      field.set(null, fieldNewValue);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively set the value of a static field.
   *
   * @param clazz Target class.
   * @param fieldName The field name.
   * @param fieldNewValue The new value.
   */
  public static void setStaticField(Class<?> clazz, String fieldName, Object fieldNewValue) {
    try {
      setStaticField(clazz.getDeclaredField(fieldName), fieldNewValue);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively call an instance method on an object.
   *
   * @param instance Target object.
   * @param methodName The method name to call.
   * @param classParameters Array of parameter types and values.
   * @param <R> The return type.
   * @return The return value of the method.
   */
  public static <R> R callInstanceMethod(final Object instance, final String methodName, ClassParameter<?>... classParameters) {
    try {
      final Class<?>[] classes = ClassParameter.getClasses(classParameters);
      final Object[] values = ClassParameter.getValues(classParameters);

      return traverseClassHierarchy(instance.getClass(), NoSuchMethodException.class, new InsideTraversal<R>() {
        @Override
        @SuppressWarnings("unchecked")
        public R run(Class<?> traversalClass) throws Exception {
          Method declaredMethod = traversalClass.getDeclaredMethod(methodName, classes);
          declaredMethod.setAccessible(true);
          return (R) declaredMethod.invoke(instance, values);
        }
      });
    } catch (InvocationTargetException e) {
      if (e.getTargetException() instanceof RuntimeException) {
        throw (RuntimeException) e.getTargetException();
      }
      if (e.getTargetException() instanceof Error) {
        throw (Error) e.getTargetException();
      }
      throw new RuntimeException(e.getTargetException());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively call an instance method on an object on a specific class.
   *
   * @param cl The class.
   * @param instance Target object.
   * @param methodName The method name to call.
   * @param classParameters Array of parameter types and values.
   * @param <R> The return type.
   * @return The return value of the method.
   */
  public static <R> R callInstanceMethod(Class<?> cl, final Object instance, final String methodName, ClassParameter<?>... classParameters) {
    try {
      final Class<?>[] classes = ClassParameter.getClasses(classParameters);
      final Object[] values = ClassParameter.getValues(classParameters);

      Method method = cl.getDeclaredMethod(methodName, classes);
      method.setAccessible(true);
      if (Modifier.isStatic(method.getModifiers())) {
        throw new IllegalArgumentException(method + " is static");
      }
      return (R) method.invoke(instance, values);
    } catch (InvocationTargetException e) {
      if (e.getTargetException() instanceof RuntimeException) {
        throw (RuntimeException) e.getTargetException();
      }
      if (e.getTargetException() instanceof Error) {
        throw (Error) e.getTargetException();
      }
      throw new RuntimeException(e.getTargetException());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Helper method for calling a static method using a class from a custom class loader
   *
   * @param classLoader
   * @param fullyQualifiedClassName
   * @param methodName
   * @param classParameters
   * @param <R>
   * @return
   */
  public static <R> R callStaticMethod(
      ClassLoader classLoader,
      String fullyQualifiedClassName,
      String methodName,
      ClassParameter<?>... classParameters) {
    Class<?> clazz = loadClass(classLoader, fullyQualifiedClassName);
    return callStaticMethod(clazz, methodName, classParameters);
  }

  /**
   * Reflectively call a static method on a class.
   *
   * @param clazz Target class.
   * @param methodName The method name to call.
   * @param classParameters Array of parameter types and values.
   * @param <R> The return type.
   * @return The return value of the method.
   */
  @SuppressWarnings("unchecked")
  public static <R> R callStaticMethod(Class<?> clazz, String methodName, ClassParameter<?>... classParameters) {
    try {
      Class<?>[] classes = ClassParameter.getClasses(classParameters);
      Object[] values = ClassParameter.getValues(classParameters);

      Method method = clazz.getDeclaredMethod(methodName, classes);
      method.setAccessible(true);
      if (!Modifier.isStatic(method.getModifiers())) {
        throw new IllegalArgumentException(method + " is not static");
      }
      return (R) method.invoke(null, values);
    } catch (InvocationTargetException e) {
      if (e.getTargetException() instanceof RuntimeException) {
        throw (RuntimeException) e.getTargetException();
      }
      if (e.getTargetException() instanceof Error) {
        throw (Error) e.getTargetException();
      }
      throw new RuntimeException(e.getTargetException());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("no such method " + clazz + "." + methodName, e);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Load a class.
   *
   * @param classLoader The class loader.
   * @param fullyQualifiedClassName The fully qualified class name.
   * @return The class object.
   */
  public static Class<?> loadClass(ClassLoader classLoader, String fullyQualifiedClassName) {
    try {
      return classLoader.loadClass(fullyQualifiedClassName);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Create a new instance of a class
   *
   * @param cl The class object.
   * @param <T> The class type.
   * @return New class instance.
   */
  public static <T> T newInstance(Class<T> cl) {
    try {
      return cl.getDeclaredConstructor().newInstance();
    } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
        | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Reflectively call the constructor of an object.
   *
   * @param clazz Target class.
   * @param classParameters Array of parameter types and values.
   * @param <R> The return type.
   * @return The return value of the method.
   */
  public static <R> R callConstructor(Class<? extends R> clazz, ClassParameter<?>... classParameters) {
    try {
      final Class<?>[] classes = ClassParameter.getClasses(classParameters);
      final Object[] values = ClassParameter.getValues(classParameters);

      Constructor<? extends R> constructor = clazz.getDeclaredConstructor(classes);
      constructor.setAccessible(true);
      return constructor.newInstance(values);
    } catch (InstantiationException e) {
      throw new RuntimeException("error instantiating " + clazz.getName(), e);
    } catch (InvocationTargetException e) {
      if (e.getTargetException() instanceof RuntimeException) {
        throw (RuntimeException) e.getTargetException();
      }
      if (e.getTargetException() instanceof Error) {
        throw (Error) e.getTargetException();
      }
      throw new RuntimeException(e.getTargetException());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private static <R, E extends Exception> R traverseClassHierarchy(Class<?> targetClass, Class<? extends E> exceptionClass, InsideTraversal<R> insideTraversal) throws Exception {
    Class<?> hierarchyTraversalClass = targetClass;
    while (true) {
      try {
        return insideTraversal.run(hierarchyTraversalClass);
      } catch (Exception e) {
        if (!exceptionClass.isInstance(e)) {
          throw e;
        }
        hierarchyTraversalClass = hierarchyTraversalClass.getSuperclass();
        if (hierarchyTraversalClass == null) {
          throw new RuntimeException(e);
        }
      }
    }
  }

  private static void makeFieldVeryAccessible(Field field) {
    field.setAccessible(true);

    try {
      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      try {
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    } catch (NoSuchFieldException e) {
      // ignore missing fields
    }
  }

  public static Object defaultValueForType(String returnType) {
    return PRIMITIVE_RETURN_VALUES.get(returnType);
  }

  private interface InsideTraversal<R> {
    R run(Class<?> traversalClass) throws Exception;
  }

  /**
   * Typed parameter used with reflective method calls.
   *
   * @param <V> The value of the method parameter.
   */
  public static class ClassParameter<V> {
    public final Class<? extends V> clazz;
    public final V val;

    public ClassParameter(Class<? extends V> clazz, V val) {
      this.clazz = clazz;
      this.val = val;
    }

    public static <V> ClassParameter<V> from(Class<? extends V> clazz, V val) {
      return new ClassParameter<>(clazz, val);
    }

    public static ClassParameter<?>[] fromComponentLists(Class<?>[] classes, Object[] values) {
      ClassParameter<?>[] classParameters = new ClassParameter[classes.length];
      for (int i = 0; i < classes.length; i++) {
        classParameters[i] = ClassParameter.from(classes[i], values[i]);
      }
      return classParameters;
    }

    public static Class<?>[] getClasses(ClassParameter<?>... classParameters) {
      Class<?>[] classes = new Class[classParameters.length];
      for (int i = 0; i < classParameters.length; i++) {
        Class<?> paramClass = classParameters[i].clazz;
        classes[i] = paramClass;
      }
      return classes;
    }

    public static Object[] getValues(ClassParameter<?>... classParameters) {
      Object[] values = new Object[classParameters.length];
      for (int i = 0; i < classParameters.length; i++) {
        Object paramValue = classParameters[i].val;
        values[i] = paramValue;
      }
      return values;
    }
  }

  /**
   * String parameter used with reflective method calls.
   *
   * @param <V> The value of the method parameter.
   */
  public static class StringParameter<V> {
    public final String className;
    public final V val;

    public StringParameter(String className, V val) {
      this.className = className;
      this.val = val;
    }

    public static <V> StringParameter<V> from(String className, V val) {
      return new StringParameter<>(className, val);
    }
  }
}


package org.robolectric.shadows;

import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.os.Build.VERSION_CODES.P;
import static com.google.common.base.Preconditions.checkNotNull;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.CrossProfileApps;
import android.content.pm.ICrossProfileApps;
import android.content.pm.PackageManager;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

/** Robolectric implementation of {@link CrossProfileApps}. */
@Implements(value = CrossProfileApps.class, minSdk = P)
public class ShadowCrossProfileApps {

  private final Set<UserHandle> targetUserProfiles = new LinkedHashSet<>();
  private final List<StartedMainActivity> startedMainActivities = new ArrayList<>();

  private PackageManager packageManager;

  @Implementation
  protected void __constructor__(Context context, ICrossProfileApps service) {
    this.packageManager = context.getPackageManager();
  }

  /**
   * Returns a list of {@link UserHandle}s currently accessible. This list is populated from calls
   * to {@link #addTargetUserProfile(UserHandle)}.
   */
  @Implementation
  protected List<UserHandle> getTargetUserProfiles() {
    return ImmutableList.copyOf(targetUserProfiles);
  }

  /**
   * Returns a {@link Drawable} that can be shown for profile switching, which is guaranteed to
   * always be the same for a particular user and to be distinct between users.
   */
  @Implementation
  protected Drawable getProfileSwitchingIconDrawable(UserHandle userHandle) {
    verifyCanAccessUser(userHandle);
    return new ColorDrawable(userHandle.getIdentifier());
  }

  /**
   * Returns a {@link CharSequence} that can be shown as a label for profile switching, which is
   * guaranteed to always be the same for a particular user and to be distinct between users.
   */
  @Implementation
  protected CharSequence getProfileSwitchingLabel(UserHandle userHandle) {
    verifyCanAccessUser(userHandle);
    return "Switch to " + userHandle;
  }

  /**
   * Simulates starting the main activity specified in the specified profile, performing the same
   * security checks done by the real {@link CrossProfileApps}.
   *
   * <p>The most recent main activity started can be queried by {@link
   * #peekNextStartedMainActivity()}.
   */
  @Implementation
  protected void startMainActivity(ComponentName componentName, UserHandle targetUser) {
    verifyCanAccessUser(targetUser);
    verifyMainActivityInManifest(componentName);
    startedMainActivities.add(new StartedMainActivity(componentName, targetUser));
  }

  /** Adds {@code userHandle} to the list of accessible handles. */
  public void addTargetUserProfile(UserHandle userHandle) {
    if (userHandle.equals(Process.myUserHandle())) {
      throw new IllegalArgumentException("Cannot target current user");
    }
    targetUserProfiles.add(userHandle);
  }

  /** Removes {@code userHandle} from the list of accessible handles, if present. */
  public void removeTargetUserProfile(UserHandle userHandle) {
    if (userHandle.equals(Process.myUserHandle())) {
      throw new IllegalArgumentException("Cannot target current user");
    }
    targetUserProfiles.remove(userHandle);
  }

  /** Clears the list of accessible handles. */
  public void clearTargetUserProfiles() {
    targetUserProfiles.clear();
  }

  /**
   * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link
   * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, wrapped in {@link
   * StartedMainActivity}.
   */
  public StartedMainActivity peekNextStartedMainActivity() {
    if (startedMainActivities.isEmpty()) {
      return null;
    } else {
      return Iterables.getLast(startedMainActivities);
    }
  }

  private void verifyCanAccessUser(UserHandle userHandle) {
    if (!targetUserProfiles.contains(userHandle)) {
      throw new SecurityException(
          "Not allowed to access "
              + userHandle
              + " (did you forget to call addTargetUserProfile?)");
    }
  }

  /**
   * Ensures that {@code component} is present in the manifest as an exported and enabled launcher
   * activity. This check and the error thrown are the same as the check done by the real {@link
   * CrossProfileApps}.
   */
  private void verifyMainActivityInManifest(ComponentName component) {
    Intent launchIntent =
        new Intent(Intent.ACTION_MAIN)
            .addCategory(Intent.CATEGORY_LAUNCHER)
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
            .setPackage(component.getPackageName());

    boolean existsMatchingActivity =
        Iterables.any(
            packageManager.queryIntentActivities(
                launchIntent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE),
            resolveInfo -> {
              ActivityInfo activityInfo = resolveInfo.activityInfo;
              return TextUtils.equals(activityInfo.packageName, component.getPackageName())
                  && TextUtils.equals(activityInfo.name, component.getClassName())
                  && activityInfo.exported;
            });
    if (!existsMatchingActivity) {
      throw new SecurityException(
          "Attempt to launch activity without "
              + " category Intent.CATEGORY_LAUNCHER or activity is not exported"
              + component);
    }
  }

  /**
   * Container object to hold parameters passed to {@link #startMainActivity(ComponentName,
   * UserHandle)}.
   */
  public static class StartedMainActivity {

    private final ComponentName componentName;
    private final UserHandle userHandle;

    public StartedMainActivity(ComponentName componentName, UserHandle userHandle) {
      this.componentName = checkNotNull(componentName);
      this.userHandle = checkNotNull(userHandle);
    }

    public ComponentName getComponentName() {
      return componentName;
    }

    public UserHandle getUserHandle() {
      return userHandle;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      StartedMainActivity that = (StartedMainActivity) o;
      return Objects.equals(componentName, that.componentName)
          && Objects.equals(userHandle, that.userHandle);
    }

    @Override
    public int hashCode() {
      return Objects.hash(componentName, userHandle);
    }
  }
}


package org.robolectric.shadows;

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

import android.net.wifi.rtt.RangingRequest;
import android.net.wifi.rtt.RangingResult;
import android.net.wifi.rtt.RangingResultCallback;
import android.net.wifi.rtt.WifiRttManager;
import java.util.List;
import java.util.concurrent.Executor;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

/** Shadow for {@link android.net.wifi.rtt.WifiRttManager}. */
@Config(minSdk = P)
@Implements(WifiRttManager.class)
public class ShadowWifiRttManager {
  private List<RangingResult> rangingResults;

  /**
   * If there are RangingResults set by the setRangeResults method of this shadow class, this method
   * will call the onRangingResults method of the callback on the executor thread and pass the list
   * of RangingResults. If there are no ranging results set, it will pass
   * RangingResultCallback.STATUS_CODE_FAIL to the onRangingFailure method of the callback, also
   * called on the executor thread.
   */
  @Implementation(minSdk = P)
  protected void startRanging(
      RangingRequest request, Executor executor, RangingResultCallback callback) {
    if (!rangingResults.isEmpty()) {
      executor.execute(() -> callback.onRangingResults(this.rangingResults));
    } else {
      executor.execute(() -> callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL));
    }
  }

  /**
   * This method sets the RangingResults that are passed to the RangingResultCallback when the
   * shadow startRanging method is called.
   */
  public void setRangeResults(List<RangingResult> rangingResults) {
    this.rangingResults = rangingResults;
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.content.ComponentName;
import android.content.IntentSender;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.LauncherApps.ShortcutQuery;
import android.content.pm.PackageInstaller.SessionCallback;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.Pair;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

/** Shadow of {@link android.content.pm.LauncherApps}. */
@Implements(value = LauncherApps.class, minSdk = LOLLIPOP)
public class ShadowLauncherApps {
  private List<ShortcutInfo> shortcuts = new ArrayList<>();

  private final List<Pair<LauncherApps.Callback, Handler>> callbacks = new ArrayList<>();

  /**
   * Adds a dynamic shortcut to be returned by {@link #getShortcuts(ShortcutQuery, UserHandle)}.
   *
   * @param shortcutInfo the shortcut to add.
   */
  public void addDynamicShortcut(ShortcutInfo shortcutInfo) {
    shortcuts.add(shortcutInfo);
    shortcutsChanged(shortcutInfo.getPackage(), Lists.newArrayList(shortcutInfo));
  }

  private void shortcutsChanged(String packageName, List<ShortcutInfo> shortcuts) {
    for (Pair<LauncherApps.Callback, Handler> callbackPair : callbacks) {
      callbackPair.second.post(
          () ->
              callbackPair.first.onShortcutsChanged(
                  packageName, shortcuts, Process.myUserHandle()));
    }
  }

  /**
   * Fires {@link LauncherApps.Callback#onPackageAdded(String, UserHandle)} on all of the registered
   * callbacks, with the provided packageName.
   *
   * @param packageName the package the was added.
   */
  public void notifyPackageAdded(String packageName) {
    for (Pair<LauncherApps.Callback, Handler> callbackPair : callbacks) {
      callbackPair.second.post(
          () -> callbackPair.first.onPackageAdded(packageName, Process.myUserHandle()));
    }
  }

  /**
   * Fires {@link LauncherApps.Callback#onPackageRemoved(String, UserHandle)} on all of the
   * registered callbacks, with the provided packageName.
   *
   * @param packageName the package the was removed.
   */
  public void notifyPackageRemoved(String packageName) {
    for (Pair<LauncherApps.Callback, Handler> callbackPair : callbacks) {
      callbackPair.second.post(
          () -> callbackPair.first.onPackageRemoved(packageName, Process.myUserHandle()));
    }
  }

  @Implementation(minSdk = Q)
  protected void startPackageInstallerSessionDetailsActivity(
      @NonNull SessionInfo sessionInfo, @Nullable Rect sourceBounds, @Nullable Bundle opts) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation
  protected void startAppDetailsActivity(
      ComponentName component, UserHandle user, Rect sourceBounds, Bundle opts) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = O)
  protected List<LauncherActivityInfo> getShortcutConfigActivityList(
      @Nullable String packageName, @NonNull UserHandle user) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = O)
  @Nullable
  protected IntentSender getShortcutConfigActivityIntent(@NonNull LauncherActivityInfo info) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation
  protected boolean isPackageEnabled(String packageName, UserHandle user) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = P)
  @Nullable
  protected Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = Q)
  protected boolean shouldHideFromSuggestions(
      @NonNull String packageName, @NonNull UserHandle user) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation
  protected boolean isActivityEnabled(ComponentName component, UserHandle user) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = N)
  protected boolean hasShortcutHostPermission() {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  /**
   * This method is an incomplete implementation of this API that only supports querying for pinned
   * dynamic shortcuts. It also doesn't not support {@link ShortcutQuery#setChangedSince(long)}.
   */
  @Implementation(minSdk = N_MR1)
  @Nullable
  protected List<ShortcutInfo> getShortcuts(
      @NonNull ShortcutQuery query, @NonNull UserHandle user) {
    if (reflector(ReflectorShortcutQuery.class, query).getChangedSince() != 0) {
      throw new UnsupportedOperationException(
          "Robolectric does not currently support ShortcutQueries that filter on time since"
              + " change.");
    }
    int flags = reflector(ReflectorShortcutQuery.class, query).getQueryFlags();
    if ((flags & ShortcutQuery.FLAG_MATCH_PINNED) == 0
        || (flags & ShortcutQuery.FLAG_MATCH_DYNAMIC) == 0) {
      throw new UnsupportedOperationException(
          "Robolectric does not currently support ShortcutQueries that match non-dynamic"
              + " Shortcuts.");
    }
    Iterable<ShortcutInfo> shortcutsItr = shortcuts;

    List<String> ids = reflector(ReflectorShortcutQuery.class, query).getShortcutIds();
    if (ids != null) {
      shortcutsItr = Iterables.filter(shortcutsItr, shortcut -> ids.contains(shortcut.getId()));
    }
    ComponentName activity = reflector(ReflectorShortcutQuery.class, query).getActivity();
    if (activity != null) {
      shortcutsItr =
          Iterables.filter(shortcutsItr, shortcut -> shortcut.getActivity().equals(activity));
    }
    String packageName = reflector(ReflectorShortcutQuery.class, query).getPackage();
    if (packageName != null && !packageName.isEmpty()) {
      shortcutsItr =
          Iterables.filter(shortcutsItr, shortcut -> shortcut.getPackage().equals(packageName));
    }
    return Lists.newArrayList(shortcutsItr);
  }

  @Implementation(minSdk = N_MR1)
  protected void pinShortcuts(
      @NonNull String packageName, @NonNull List<String> shortcutIds, @NonNull UserHandle user) {
    Iterable<ShortcutInfo> changed =
        Iterables.filter(shortcuts, shortcut -> !shortcutIds.contains(shortcut.getId()));
    List<ShortcutInfo> ret = Lists.newArrayList(changed);
    shortcuts =
        Lists.newArrayList(
            Iterables.filter(shortcuts, shortcut -> shortcutIds.contains(shortcut.getId())));

    shortcutsChanged(packageName, ret);
  }

  @Implementation(minSdk = N_MR1)
  protected void startShortcut(
      @NonNull String packageName,
      @NonNull String shortcutId,
      @Nullable Rect sourceBounds,
      @Nullable Bundle startActivityOptions,
      @NonNull UserHandle user) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = N_MR1)
  protected void startShortcut(
      @NonNull ShortcutInfo shortcut,
      @Nullable Rect sourceBounds,
      @Nullable Bundle startActivityOptions) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation
  protected void registerCallback(LauncherApps.Callback callback) {
    registerCallback(callback, null);
  }

  @Implementation
  protected void registerCallback(LauncherApps.Callback callback, Handler handler) {
    callbacks.add(
        Pair.create(callback, handler != null ? handler : new Handler(Looper.myLooper())));
  }

  @Implementation
  protected void unregisterCallback(LauncherApps.Callback callback) {
    int index = Iterables.indexOf(this.callbacks, pair -> pair.first == callback);
    if (index != -1) {
      this.callbacks.remove(index);
    }
  }

  @Implementation(minSdk = Q)
  protected void registerPackageInstallerSessionCallback(
      @NonNull Executor executor, @NonNull SessionCallback callback) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = Q)
  protected void unregisterPackageInstallerSessionCallback(@NonNull SessionCallback callback) {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @Implementation(minSdk = Q)
  @NonNull
  protected List<SessionInfo> getAllPackageInstallerSessions() {
    throw new UnsupportedOperationException(
        "This method is not currently supported in Robolectric.");
  }

  @ForType(ShortcutQuery.class)
  private interface ReflectorShortcutQuery {
    @Accessor("mChangedSince")
    long getChangedSince();

    @Accessor("mQueryFlags")
    int getQueryFlags();

    @Accessor("mShortcutIds")
    List<String> getShortcutIds();

    @Accessor("mActivity")
    ComponentName getActivity();

    @Accessor("mPackage")
    String getPackage();
  }
}


package org.robolectric.shadows;

import android.content.Intent;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(Intent.class)
public class ShadowIntent {
  @RealObject private Intent realIntent;

  /**
   * Returns the {@code Class} object set by
   * {@link Intent#setClass(android.content.Context, Class)}
   *
   * @return the {@code Class} object set by
   *         {@link Intent#setClass(android.content.Context, Class)}
   */
  public Class<?> getIntentClass() {
    try {
      return Class.forName(realIntent.getComponent().getClassName());
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }
}


package org.robolectric.shadows;

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

import android.graphics.Typeface;
import android.provider.FontRequest;
import android.provider.FontsContract;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@Implements(value = FontsContract.class, minSdk = O)
public class ShadowFontsContract {

  /** Returns a stub typeface immediately. */
  @Implementation
  public static Typeface getFontSync(FontRequest request) {
    return Typeface.create(request.getQuery(), Typeface.NORMAL);
  }
}


package org.robolectric;

import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.PackageResourceTable;
import org.robolectric.res.ResourceMerger;
import org.robolectric.res.ResourcePath;
import org.robolectric.res.ResourceTableFactory;

/**
 * Mediates loading of "APKs" in legacy mode.
 */
public class ApkLoader {

  private final Map<AndroidManifest, PackageResourceTable> appResourceTableCache = new HashMap<>();
  private PackageResourceTable compiletimeSdkResourceTable;

  synchronized public PackageResourceTable getAppResourceTable(final AndroidManifest appManifest) {
    PackageResourceTable resourceTable = appResourceTableCache.get(appManifest);
    if (resourceTable == null) {
      resourceTable = new ResourceMerger().buildResourceTable(appManifest);

      appResourceTableCache.put(appManifest, resourceTable);
    }
    return resourceTable;
  }

  /**
   * Returns the ResourceTable for the compile time SDK.
   */
  @Nonnull
  synchronized public PackageResourceTable getCompileTimeSdkResourceTable() {
    if (compiletimeSdkResourceTable == null) {
      ResourceTableFactory resourceTableFactory = new ResourceTableFactory();
      compiletimeSdkResourceTable = resourceTableFactory
          .newFrameworkResourceTable(new ResourcePath(android.R.class, null, null));
    }
    return compiletimeSdkResourceTable;
  }
}