package org.robolectric.shadows;

import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;

import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.Scheduler;

/**
 * The shadow Looper for {@link LooperMode.Mode.PAUSED}.
 *
 * This shadow differs from the legacy {@link ShadowLegacyLooper} in the following ways:\
 *   - Has no connection to {@link org.robolectric.util.Scheduler}. Its APIs are standalone
 *   - The main looper is always paused. Posted messages are not executed unless {@link #idle()} is
 *     called.
 *   - Just like in real Android, each looper has its own thread, and posted tasks get executed in
 *   that thread. -
 *   - There is only a single {@link SystemClock} value that all loopers read from. Unlike legacy
 *     behavior where each {@link org.robolectric.util.Scheduler} kept their own clock value.
 *
 * This class should not be used directly; use {@link ShadowLooper} instead.
 */
@Implements(
    value = Looper.class,
    // turn off shadowOf generation.
    isInAndroidSdk = false)
@SuppressWarnings("NewApi")
public final class ShadowPausedLooper extends ShadowLooper {

  // Keep reference to all created Loopers so they can be torn down after test
  private static Set<Looper> loopingLoopers =
      Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<Looper, Boolean>()));

  @RealObject private Looper realLooper;
  private boolean isPaused = false;
  // the Executor that executes looper messages. Must be written to on looper thread
  private Executor looperExecutor;

  @Implementation
  protected void __constructor__(boolean quitAllowed) {
    invokeConstructor(Looper.class, realLooper, from(boolean.class, quitAllowed));

    loopingLoopers.add(realLooper);
    looperExecutor = new HandlerExecutor(new Handler(realLooper));
  }

  @Override
  public void quitUnchecked() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public boolean hasQuit() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void idle() {
    executeOnLooper(new IdlingRunnable());
  }

  @Override
  public void idleFor(long time, TimeUnit timeUnit) {
    long endingTimeMs = SystemClock.uptimeMillis() + timeUnit.toMillis(time);
    long nextScheduledTimeMs = getNextScheduledTaskTime().toMillis();
    while (nextScheduledTimeMs != 0 && nextScheduledTimeMs <= endingTimeMs) {
      SystemClock.setCurrentTimeMillis(nextScheduledTimeMs);
      idle();
      nextScheduledTimeMs = getNextScheduledTaskTime().toMillis();
    }
    SystemClock.setCurrentTimeMillis(endingTimeMs);
  }

  @Override
  public boolean isIdle() {
    if (Thread.currentThread() == realLooper.getThread() || isPaused) {
      return shadowQueue().isIdle();
    } else {
      return shadowQueue().isIdle() && shadowQueue().isPolling();
    }
  }

  @Override
  public void unPause() {
    if (realLooper == Looper.getMainLooper()) {
      throw new UnsupportedOperationException("main looper cannot be unpaused");
    }
    executeOnLooper(new UnPauseRunnable());
  }

  @Override
  public void pause() {
    if (!isPaused()) {
      executeOnLooper(new PausedLooperExecutor());
    }
  }

  @Override
  public boolean isPaused() {
    return isPaused;
  }

  @Override
  public boolean setPaused(boolean shouldPause) {
    if (shouldPause) {
      pause();
    } else {
      unPause();
    }
    return true;
  }

  @Override
  public void resetScheduler() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void reset() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void idleIfPaused() {
    idle();
  }

  @Override
  public void idleConstantly(boolean shouldIdleConstantly) {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void runToEndOfTasks() {
    idleFor(getLastScheduledTaskTime().toMillis() - SystemClock.uptimeMillis(),
        TimeUnit.MILLISECONDS);
  }

  @Override
  public void runToNextTask() {
    idleFor(getNextScheduledTaskTime().toMillis() - SystemClock.uptimeMillis(),
        TimeUnit.MILLISECONDS);
  }

  @Override
  public void runOneTask() {
    executeOnLooper(new RunOneRunnable());
  }

  @Override
  public boolean post(Runnable runnable, long delayMillis) {
    return new Handler(realLooper).postDelayed(runnable, delayMillis);
  }

  @Override
  public boolean postAtFrontOfQueue(Runnable runnable) {
    return new Handler(realLooper).postAtFrontOfQueue(runnable);
  }

  // this API doesn't make sense in LooperMode.PAUSED, but just retain it for backwards
  // compatibility for now
  @Override
  public void runPaused(Runnable runnable) {
    if (isPaused && Thread.currentThread() == realLooper.getThread()) {
      // just run
      runnable.run();
    } else {
      throw new UnsupportedOperationException();
    }
  }

  @Override
  public Duration getNextScheduledTaskTime() {
    return shadowQueue().getNextScheduledTaskTime();
  }

  @Override
  public Duration getLastScheduledTaskTime() {
    return shadowQueue().getLastScheduledTaskTime();
  }

  @Resetter
  public static synchronized void resetLoopers() {
    if (looperMode() != LooperMode.Mode.PAUSED) {
      // ignore if not realistic looper
      return;
    }

    Collection<Looper> loopersCopy = new ArrayList(loopingLoopers);
    for (Looper looper : loopersCopy) {
      ShadowPausedMessageQueue shadowQueue = Shadow.extract(looper.getQueue());
      if (shadowQueue.isQuitAllowed()) {
        looper.quit();
        loopingLoopers.remove(looper);
      } else {
        shadowQueue.reset();
      }
    }
  }

  @Implementation
  protected static void prepareMainLooper() {
    directlyOn(Looper.class, "prepareMainLooper");
    ShadowPausedLooper pausedLooper = Shadow.extract(Looper.getMainLooper());
    pausedLooper.isPaused = true;
  }

  @Implementation
  protected void quit() {
    if (isPaused()) {
      executeOnLooper(new UnPauseRunnable());
    }
    directlyOn(realLooper, Looper.class, "quit");
  }

  @Implementation(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR2)
  protected void quitSafely() {
    if (isPaused()) {
      executeOnLooper(new UnPauseRunnable());
    }
    directlyOn(realLooper, Looper.class, "quitSafely");
  }

  @Override
  public Scheduler getScheduler() {
    throw new UnsupportedOperationException(
        String.format("this action is not supported in %s mode.", looperMode()));
  }

  private static ShadowPausedMessage shadowMsg(Message msg) {
    return Shadow.extract(msg);
  }

  private ShadowPausedMessageQueue shadowQueue() {
    return Shadow.extract(realLooper.getQueue());
  }

  /** Executes the given runnable on the loopers thread, and waits for it to complete. */
  private void executeOnLooper(ControlRunnable runnable) {
    if (Thread.currentThread() == realLooper.getThread()) {
      runnable.run();
    } else {
      if (realLooper.equals(Looper.getMainLooper())) {
        throw new UnsupportedOperationException(
            "main looper can only be controlled from main thread");
      }
      looperExecutor.execute(runnable);
      runnable.waitTillComplete();
    }
  }

  private void setLooperExecutor(Executor executor) {
    looperExecutor = executor;
  }

  /** A runnable that changes looper state, and that must be run from looper's thread */
  private abstract static class ControlRunnable implements Runnable {

    protected final CountDownLatch runLatch = new CountDownLatch(1);

    public void waitTillComplete() {
      try {
        runLatch.await();
      } catch (InterruptedException e) {
        Log.w("ShadowPausedLooper", "wait till idle interrupted");
      }
    }
  }

  private class IdlingRunnable extends ControlRunnable {

    @Override
    public void run() {
      while (!shadowQueue().isIdle()) {
        Message msg = shadowQueue().getNext();
        msg.getTarget().dispatchMessage(msg);
        shadowMsg(msg).recycleUnchecked();
      }
      runLatch.countDown();
    }
  }

  private class RunOneRunnable extends ControlRunnable {

    @Override
    public void run() {
      Message msg = shadowQueue().poll();
      if (msg != null) {
        SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen());
        msg.getTarget().dispatchMessage(msg);
      }
      runLatch.countDown();
    }
  }

  /**
   * A runnable that will block normal looper execution of messages aka will 'pause' the looper.
   *
   * <p>Message execution can be triggered by posting messages to this runnable.
   */
  private class PausedLooperExecutor extends ControlRunnable implements Executor {

    private final LinkedBlockingQueue<Runnable> executionQueue = new LinkedBlockingQueue<>();

    @Override
    public void execute(Runnable runnable) {
      executionQueue.add(runnable);
    }

    @Override
    public void run() {
      setLooperExecutor(this);
      isPaused = true;
      runLatch.countDown();
      while (true) {
        try {
          Runnable runnable = executionQueue.take();
          runnable.run();
          if (runnable instanceof UnPauseRunnable) {
            setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
            return;
          }
        } catch (InterruptedException e) {
          // ignore
        }
      }
    }
  }

  private class UnPauseRunnable extends ControlRunnable {
    @Override
    public void run() {
      isPaused = false;
      runLatch.countDown();
    }
  }

  private static class HandlerExecutor implements Executor {
    private final Handler handler;

    private HandlerExecutor(Handler handler) {
      this.handler = handler;
    }

    @Override
    public void execute(Runnable runnable) {
      handler.post(runnable);
    }
  }
}


package org.robolectric.res.android;

import static org.robolectric.res.android.ApkAssetsCookie.K_INVALID_COOKIE;
import static org.robolectric.res.android.ApkAssetsCookie.kInvalidCookie;
import static org.robolectric.res.android.Util.ALOGI;

import java.util.Arrays;
import org.robolectric.res.android.CppAssetManager2.ResolvedBag;
import org.robolectric.res.android.CppAssetManager2.ResolvedBag.Entry;
import org.robolectric.res.android.CppAssetManager2.Theme;
import org.robolectric.res.android.ResourceTypes.Res_value;

// TODO: update paths to released version.
// transliterated from
// https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_rXX/libs/androidfw/AttributeResolution.cpp and
// https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_rXX/libs/androidfw/include/androidfw/AttributeResolution.h

public class AttributeResolution10 {
    public static final boolean kThrowOnBadId = false;
    private static final boolean kDebugStyles = false;

    // Offsets into the outValues array populated by the methods below. outValues is a uint32_t
    // array, but each logical element takes up 7 uint32_t-sized physical elements.
    // Keep these in sync with android.content.res.TypedArray java class
    public static final int STYLE_NUM_ENTRIES = 7;
    public static final int STYLE_TYPE = 0;
    public static final int STYLE_DATA = 1;
    public static final int STYLE_ASSET_COOKIE = 2;
    public static final int STYLE_RESOURCE_ID = 3;
    public static final int STYLE_CHANGING_CONFIGURATIONS = 4;
    public static final int STYLE_DENSITY = 5;
    public static final int STYLE_SOURCE_STYLE_RESOURCE_ID = 6;

    // Java asset cookies have 0 as an invalid cookie, but TypedArray expects < 0.
    private static int ApkAssetsCookieToJavaCookie(ApkAssetsCookie cookie) {
        return cookie.intValue() != kInvalidCookie ? (cookie.intValue() + 1) : -1;
    }

    public static class XmlAttributeFinder {

        private ResXMLParser xmlParser;

        XmlAttributeFinder(ResXMLParser xmlParser) {
            this.xmlParser = xmlParser;
        }

        public int Find(int curIdent) {
            if (xmlParser == null) {
                return -1;
            }

            int attributeCount = xmlParser.getAttributeCount();
            for (int i = 0; i < attributeCount; i++) {
                if (xmlParser.getAttributeNameResID(i) == curIdent) {
                    return i;
                }
            }
            return -1;
        }
    }

    public static class BagAttributeFinder {
        private final Entry[] bagEntries;

        BagAttributeFinder(ResolvedBag bag) {
            this.bagEntries = bag == null ? null : bag.entries;
        }

        // Robolectric: unoptimized relative to Android impl
        Entry Find(int ident) {
            Entry needle = new Entry();
            needle.key = ident;

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

            int i = Arrays.binarySearch(bagEntries, needle, (o1, o2) -> o1.key - o2.key);
            return i < 0 ? null : bagEntries[i];
        }
    }

    // These are all variations of the same method. They each perform the exact same operation,
    // but on various data sources. I *think* they are re-written to avoid an extra branch
    // in the inner loop, but after one branch miss (some pointer != null), the branch predictor should
    // predict the rest of the iterations' branch correctly.
    // TODO(adamlesinski): Run performance tests against these methods and a new, single method
    // that uses all the sources and branches to the right ones within the inner loop.

    // `out_values` must NOT be nullptr.
    // `out_indices` may be nullptr.
    public static boolean ResolveAttrs(Theme theme, int def_style_attr,
            int def_style_res, int[] src_values,
            int src_values_length, int[] attrs,
            int attrs_length, int[] out_values, int[] out_indices) {
        if (kDebugStyles) {
            ALOGI("APPLY STYLE: theme=0x%p defStyleAttr=0x%x defStyleRes=0x%x", theme,
                    def_style_attr, def_style_res);
        }

        CppAssetManager2 assetmanager = theme.GetAssetManager();
        ResTable_config config = new ResTable_config();
        Res_value value;

        int indicesIdx = 0;

        // Load default style from attribute, if specified...
        final Ref<Integer> def_style_flags = new Ref<>(0);
        if (def_style_attr != 0) {
            final Ref<Res_value> valueRef = new Ref<>(null);
            if (theme.GetAttribute(def_style_attr, valueRef, def_style_flags).intValue() != kInvalidCookie) {
                value = valueRef.get();
                if (value.dataType == Res_value.TYPE_REFERENCE) {
                    def_style_res = value.data;
                }
            }
        }

        // Retrieve the default style bag, if requested.
        ResolvedBag default_style_bag = null;
        if (def_style_res != 0) {
            default_style_bag = assetmanager.GetBag(def_style_res);
            if (default_style_bag != null) {
                def_style_flags.set(def_style_flags.get() | default_style_bag.type_spec_flags);
            }
        }
        BagAttributeFinder def_style_attr_finder = new BagAttributeFinder(default_style_bag);

        // Now iterate through all of the attributes that the client has requested,
        // filling in each with whatever data we can find.
        int destOffset = 0;
        for (int ii=0; ii<attrs_length; ii++) {
            final int cur_ident = attrs[ii];

            if (kDebugStyles) {
                ALOGI("RETRIEVING ATTR 0x%08x...", cur_ident);
            }

            ApkAssetsCookie cookie = K_INVALID_COOKIE;
            int type_set_flags = 0;

            value = Res_value.NULL_VALUE;
            config.density = 0;

            // Try to find a value for this attribute...  we prioritize values
            // coming from, first XML attributes, then XML style, then default
            // style, and finally the theme.

            // Retrieve the current input value if available.
            if (src_values_length > 0 && src_values[ii] != 0) {
                value = new Res_value((byte) Res_value.TYPE_ATTRIBUTE, src_values[ii]);
                if (kDebugStyles) {
                    ALOGI("-> From values: type=0x%x, data=0x%08x", value.dataType, value.data);
                }
            } else {
                final Entry entry = def_style_attr_finder.Find(cur_ident);
                if (entry != null) {
                    cookie = entry.cookie;
                    type_set_flags = def_style_flags.get();
                    value = entry.value;
                    if (kDebugStyles) {
                        ALOGI("-> From def style: type=0x%x, data=0x%08x", value.dataType, value.data);
                    }
                }
            }

            int resid = 0;
            final Ref<Res_value> valueRef = new Ref<>(value);
            final Ref<Integer> residRef = new Ref<>(resid);
            final Ref<Integer> type_set_flagsRef = new Ref<>(type_set_flags);
            final Ref<ResTable_config> configRef = new Ref<>(config);
            if (value.dataType != Res_value.TYPE_NULL) {
                // Take care of resolving the found resource to its final value.
                ApkAssetsCookie new_cookie =
                        theme.ResolveAttributeReference(cookie, valueRef, configRef, type_set_flagsRef, residRef);
                if (new_cookie.intValue() != kInvalidCookie) {
                    cookie = new_cookie;
                }
                if (kDebugStyles) {
                    ALOGI("-> Resolved attr: type=0x%x, data=0x%08x", value.dataType, value.data);
                }
            } else if (value.data != Res_value.DATA_NULL_EMPTY) {
                // If we still don't have a value for this attribute, try to find it in the theme!
                ApkAssetsCookie new_cookie = theme.GetAttribute(cur_ident, valueRef, type_set_flagsRef);
                if (new_cookie.intValue() != kInvalidCookie) {
                    if (kDebugStyles) {
                        ALOGI("-> From theme: type=0x%x, data=0x%08x", value.dataType, value.data);
                    }
                    new_cookie =
                            assetmanager.ResolveReference(new_cookie, valueRef, configRef, type_set_flagsRef, residRef);
                    if (new_cookie.intValue() != kInvalidCookie) {
                        cookie = new_cookie;
                    }
                    if (kDebugStyles) {
                        ALOGI("-> Resolved theme: type=0x%x, data=0x%08x", value.dataType, value.data);
                    }
                }
            }
            value = valueRef.get();
            resid = residRef.get();
            type_set_flags = type_set_flagsRef.get();
            config = configRef.get();

            // Deal with the special @null value -- it turns back to TYPE_NULL.
            if (value.dataType == Res_value.TYPE_REFERENCE && value.data == 0) {
                if (kDebugStyles) {
                    ALOGI("-> Setting to @null!");
                }
                value = Res_value.NULL_VALUE;
                cookie = K_INVALID_COOKIE;
            }

            if (kDebugStyles) {
                ALOGI("Attribute 0x%08x: type=0x%x, data=0x%08x", cur_ident, value.dataType,
                        value.data);
            }

            // Write the final value back to Java.
            out_values[destOffset + STYLE_TYPE] = value.dataType;
            out_values[destOffset + STYLE_DATA] = value.data;
            out_values[destOffset + STYLE_ASSET_COOKIE] = ApkAssetsCookieToJavaCookie(cookie);
            out_values[destOffset + STYLE_RESOURCE_ID] = resid;
            out_values[destOffset + STYLE_CHANGING_CONFIGURATIONS] = type_set_flags;
            out_values[destOffset + STYLE_DENSITY] = config.density;

            if (out_indices != null && value.dataType != Res_value.TYPE_NULL) {
                indicesIdx++;
                out_indices[indicesIdx] = ii;
            }

            destOffset += STYLE_NUM_ENTRIES;
        }

        if (out_indices != null) {
            out_indices[0] = indicesIdx;
        }
        return true;
    }

    public static void ApplyStyle(Theme theme, ResXMLParser xml_parser, int def_style_attr,
            int def_style_resid, int[] attrs, int attrs_length,
            int[] out_values, int[] out_indices) {
        if (kDebugStyles) {
            ALOGI("APPLY STYLE: theme=%s defStyleAttr=0x%x defStyleRes=0x%x xml=%s",
                    theme, def_style_attr, def_style_resid, xml_parser);
        }

        CppAssetManager2 assetmanager = theme.GetAssetManager();
        final Ref<ResTable_config> config = new Ref<>(new ResTable_config());
        final Ref<Res_value> value = new Ref<>(new Res_value());

        int indices_idx = 0;

        // Load default style from attribute, if specified...
        final Ref<Integer> def_style_flags = new Ref<>(0);
        if (def_style_attr != 0) {
            if (theme.GetAttribute(def_style_attr, value, def_style_flags).intValue() != kInvalidCookie) {
                if (value.get().dataType == DataType.REFERENCE.code()) {
                    def_style_resid = value.get().data;
                }
            }
        }

        // Retrieve the style resource ID associated with the current XML tag's style attribute.
        int style_resid = 0;
        final Ref<Integer> style_flags = new Ref<>(0);
        if (xml_parser != null) {
            int idx = xml_parser.indexOfStyle();
            if (idx >= 0 && xml_parser.getAttributeValue(idx, value) >= 0) {
                if (value.get().dataType == DataType.ATTRIBUTE.code()) {
                    // Resolve the attribute with out theme.
                    if (theme.GetAttribute(value.get().data, value, style_flags).intValue() == kInvalidCookie) {
                        value.set(value.get().withType(DataType.NULL.code()));
                    }
                }

                if (value.get().dataType == DataType.REFERENCE.code()) {
                    style_resid = value.get().data;
                }
            }
        }

        // Retrieve the default style bag, if requested.
        ResolvedBag default_style_bag = null;
        if (def_style_resid != 0) {
            default_style_bag = assetmanager.GetBag(def_style_resid);
            if (default_style_bag != null) {
                def_style_flags.set(def_style_flags.get() | default_style_bag.type_spec_flags);
            }
        }

        BagAttributeFinder def_style_attr_finder = new BagAttributeFinder(default_style_bag);

        // Retrieve the style class bag, if requested.
        ResolvedBag xml_style_bag = null;
        if (style_resid != 0) {
            xml_style_bag = assetmanager.GetBag(style_resid);
            if (xml_style_bag != null) {
                style_flags.set(style_flags.get() | xml_style_bag.type_spec_flags);
            }
        }

        BagAttributeFinder xml_style_attr_finder = new BagAttributeFinder(xml_style_bag);

        // Retrieve the XML attributes, if requested.
        XmlAttributeFinder xml_attr_finder = new XmlAttributeFinder(xml_parser);

        // Now iterate through all of the attributes that the client has requested,
        // filling in each with whatever data we can find.
        for (int ii = 0; ii < attrs_length; ii++) {
            final int cur_ident = attrs[ii];

            if (kDebugStyles) {
                ALOGI("RETRIEVING ATTR 0x%08x...", cur_ident);
            }

            ApkAssetsCookie cookie = K_INVALID_COOKIE;
            final Ref<Integer> type_set_flags = new Ref<>(0);

            value.set(Res_value.NULL_VALUE);
            config.get().density = 0;
            int source_style_resid = 0;

            // Try to find a value for this attribute...  we prioritize values
            // coming from, first XML attributes, then XML style, then default
            // style, and finally the theme.

            // Walk through the xml attributes looking for the requested attribute.
            int xml_attr_idx = xml_attr_finder.Find(cur_ident);
            if (xml_attr_idx != -1) {
                // We found the attribute we were looking for.
                xml_parser.getAttributeValue(xml_attr_idx, value);
                type_set_flags.set(style_flags.get());
                if (kDebugStyles) {
                    ALOGI("-> From XML: type=0x%x, data=0x%08x", value.get().dataType, value.get().data);
                }
            }

            if (value.get().dataType == DataType.NULL.code() && value.get().data != Res_value.DATA_NULL_EMPTY) {
                // Walk through the style class values looking for the requested attribute.
                Entry entry = xml_style_attr_finder.Find(cur_ident);
                if (entry != null) {
                    // We found the attribute we were looking for.
                    cookie = entry.cookie;
                    type_set_flags.set(style_flags.get());
                    value.set(entry.value);
                    source_style_resid = entry.style;
                    if (kDebugStyles) {
                        ALOGI("-> From style: type=0x%x, data=0x%08x, style=0x%08x", value.get().dataType, value.get().data,
                                entry.style);
                    }
                }
            }

            if (value.get().dataType == DataType.NULL.code() && value.get().data != Res_value.DATA_NULL_EMPTY) {
                // Walk through the default style values looking for the requested attribute.
                Entry entry = def_style_attr_finder.Find(cur_ident);
                if (entry != null) {
                    // We found the attribute we were looking for.
                    cookie = entry.cookie;
                    type_set_flags.set(def_style_flags.get());

                    value.set(entry.value);
                    if (kDebugStyles) {
                        ALOGI("-> From def style: type=0x%x, data=0x%08x, style=0x%08x", value.get().dataType, value.get().data,
                                entry.style);
                    }
                    source_style_resid = entry.style;
                }
            }

            final Ref<Integer> resid = new Ref<>(0);
            if (value.get().dataType != DataType.NULL.code()) {
                // Take care of resolving the found resource to its final value.
                ApkAssetsCookie new_cookie =
                        theme.ResolveAttributeReference(cookie, value, config, type_set_flags, resid);
                if (new_cookie.intValue() != kInvalidCookie) {
                    cookie = new_cookie;
                }

                if (kDebugStyles) {
                    ALOGI("-> Resolved attr: type=0x%x, data=0x%08x", value.get().dataType, value.get().data);
                }
            } else if (value.get().data != Res_value.DATA_NULL_EMPTY) {
                // If we still don't have a value for this attribute, try to find it in the theme!
                ApkAssetsCookie new_cookie = theme.GetAttribute(cur_ident, value, type_set_flags);
                if (new_cookie.intValue() != kInvalidCookie) {
                    if (kDebugStyles) {
                        ALOGI("-> From theme: type=0x%x, data=0x%08x", value.get().dataType, value.get().data);
                    }
                    new_cookie =
                            assetmanager.ResolveReference(new_cookie, value, config, type_set_flags, resid);
                    if (new_cookie.intValue() != kInvalidCookie) {
                        cookie = new_cookie;
                    }

                    if (kDebugStyles) {
                        ALOGI("-> Resolved theme: type=0x%x, data=0x%08x", value.get().dataType, value.get().data);
                    }
                }
            }

            // Deal with the special @null value -- it turns back to TYPE_NULL.
            if (value.get().dataType == DataType.REFERENCE.code() && value.get().data == 0) {
                if (kDebugStyles) {
                    ALOGI(". Setting to @null!");
                }
                value.set(Res_value.NULL_VALUE);
                cookie = K_INVALID_COOKIE;
            }

            if (kDebugStyles) {
                ALOGI("Attribute 0x%08x: type=0x%x, data=0x%08x", cur_ident, value.get().dataType, value.get().data);
            }

            // Write the final value back to Java.
            int destIndex = ii * STYLE_NUM_ENTRIES;
            Res_value res_value = value.get();
            out_values[destIndex + STYLE_TYPE] = res_value.dataType;
            out_values[destIndex + STYLE_DATA] = res_value.data;
            out_values[destIndex + STYLE_ASSET_COOKIE] = ApkAssetsCookieToJavaCookie(cookie);
            out_values[destIndex + STYLE_RESOURCE_ID] = resid.get();
            out_values[destIndex + STYLE_CHANGING_CONFIGURATIONS] = type_set_flags.get();
            out_values[destIndex + STYLE_DENSITY] = config.get().density;
            out_values[destIndex + STYLE_SOURCE_STYLE_RESOURCE_ID] = source_style_resid;

            if (res_value.dataType != DataType.NULL.code() || res_value.data == Res_value.DATA_NULL_EMPTY) {
                indices_idx++;

                // out_indices must NOT be nullptr.
                out_indices[indices_idx] = ii;
            }

            // Robolectric-custom:
            // if (false && res_value.dataType == DataType.ATTRIBUTE.code()) {
            //   final Ref<ResourceName> attrName = new Ref<>(null);
            //   final Ref<ResourceName> attrRefName = new Ref<>(null);
            //   boolean gotName = assetmanager.GetResourceName(cur_ident, attrName);
            //   boolean gotRefName = assetmanager.GetResourceName(res_value.data, attrRefName);
            //   Logger.warn(
            //       "Failed to resolve attribute lookup: %s=\"?%s\"; theme: %s",
            //       gotName ? attrName.get() : "unknown", gotRefName ? attrRefName.get() : "unknown",
            //       theme);
            // }

//      out_values += STYLE_NUM_ENTRIES;
        }

        // out_indices must NOT be nullptr.
        out_indices[0] = indices_idx;
    }

    public static boolean RetrieveAttributes(CppAssetManager2 assetmanager, ResXMLParser xml_parser, int[] attrs,
            int attrs_length, int[] out_values, int[] out_indices) {
        final Ref<ResTable_config> config = new Ref<>(new ResTable_config());
        final Ref<Res_value> value = new Ref<>(null);

        int indices_idx = 0;

        // Retrieve the XML attributes, if requested.
        final int xml_attr_count = xml_parser.getAttributeCount();
        int ix = 0;
        int cur_xml_attr = xml_parser.getAttributeNameResID(ix);

        // Now iterate through all of the attributes that the client has requested,
        // filling in each with whatever data we can find.
        int baseDest = 0;
        for (int ii = 0; ii < attrs_length; ii++) {
            final int cur_ident = attrs[ii];
            ApkAssetsCookie cookie = K_INVALID_COOKIE;
            final Ref<Integer> type_set_flags = new Ref<>(0);

            value.set(Res_value.NULL_VALUE);
            config.get().density = 0;

            // Try to find a value for this attribute...
            // Skip through XML attributes until the end or the next possible match.
            while (ix < xml_attr_count && cur_ident > cur_xml_attr) {
                ix++;
                cur_xml_attr = xml_parser.getAttributeNameResID(ix);
            }
            // Retrieve the current XML attribute if it matches, and step to next.
            if (ix < xml_attr_count && cur_ident == cur_xml_attr) {
                xml_parser.getAttributeValue(ix, value);
                ix++;
                cur_xml_attr = xml_parser.getAttributeNameResID(ix);
            }

            final Ref<Integer> resid = new Ref<>(0);
            if (value.get().dataType != Res_value.TYPE_NULL) {
                // Take care of resolving the found resource to its final value.
                ApkAssetsCookie new_cookie =
                        assetmanager.ResolveReference(cookie, value, config, type_set_flags, resid);
                if (new_cookie.intValue() != kInvalidCookie) {
                    cookie = new_cookie;
                }
            }

            // Deal with the special @null value -- it turns back to TYPE_NULL.
            if (value.get().dataType == Res_value.TYPE_REFERENCE && value.get().data == 0) {
                value.set(Res_value.NULL_VALUE);
                cookie = K_INVALID_COOKIE;
            }

            // Write the final value back to Java.
            out_values[baseDest + STYLE_TYPE] = value.get().dataType;
            out_values[baseDest + STYLE_DATA] = value.get().data;
            out_values[baseDest + STYLE_ASSET_COOKIE] = ApkAssetsCookieToJavaCookie(cookie);
            out_values[baseDest + STYLE_RESOURCE_ID] = resid.get();
            out_values[baseDest + STYLE_CHANGING_CONFIGURATIONS] = type_set_flags.get();
            out_values[baseDest + STYLE_DENSITY] = config.get().density;

            if (out_indices != null &&
                    (value.get().dataType != Res_value.TYPE_NULL
                            || value.get().data == Res_value.DATA_NULL_EMPTY)) {
                indices_idx++;
                out_indices[indices_idx] = ii;
            }

//      out_values += STYLE_NUM_ENTRIES;
            baseDest += STYLE_NUM_ENTRIES;
        }

        if (out_indices != null) {
            out_indices[0] = indices_idx;
        }

        return true;
    }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.P;
import static org.robolectric.RuntimeEnvironment.castNativePtr;

import android.os.BadParcelableException;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;

/**
 * Robolectric's {@link Parcel} pretends to be backed by a byte buffer, closely matching {@link
 * Parcel}'s position, size, and capacity behavior. However, its internal pure-Java representation
 * is strongly typed, to detect non-portable code and common testing mistakes. It may throw {@link
 * IllegalArgumentException} or {@link IllegalStateException} for error-prone behavior normal {@link
 * Parcel} tolerates.
 */
@Implements(Parcel.class)
public class ShadowParcel {
  private static final String TAG = "Parcel";

  @RealObject private Parcel realObject;
  private static final Map<Long, ByteBuffer> NATIVE_PTR_TO_PARCEL = new LinkedHashMap<>();
  private static long nextNativePtr = 1; // this needs to start above 0, which is a magic number to Parcel

  @Implementation(maxSdk = JELLY_BEAN_MR1)
  @SuppressWarnings("TypeParameterUnusedInFormals")
  protected <T extends Parcelable> T readParcelable(ClassLoader loader) {
    // prior to JB MR2, readParcelableCreator() is inlined here.
    Parcelable.Creator<?> creator = readParcelableCreator(loader);
    if (creator == null) {
      return null;
    }

    if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
      Parcelable.ClassLoaderCreator<?> classLoaderCreator =
          (Parcelable.ClassLoaderCreator<?>) creator;
      return (T) classLoaderCreator.createFromParcel(realObject, loader);
    }
    return (T) creator.createFromParcel(realObject);
  }

  @HiddenApi
  @Implementation(minSdk = JELLY_BEAN_MR2)
  public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) {
    //note: calling `readString` will also consume the string, and increment the data-pointer.
    //which is exactly what we need, since we do not call the real `readParcelableCreator`.
    final String name = realObject.readString();
    if (name == null) {
      return null;
    }

    Parcelable.Creator<?> creator;
    try {
      // If loader == null, explicitly emulate Class.forName(String) "caller
      // classloader" behavior.
      ClassLoader parcelableClassLoader =
          (loader == null ? getClass().getClassLoader() : loader);
      // Avoid initializing the Parcelable class until we know it implements
      // Parcelable and has the necessary CREATOR field. http://b/1171613.
      Class<?> parcelableClass = Class.forName(name, false /* initialize */,
          parcelableClassLoader);
      if (!Parcelable.class.isAssignableFrom(parcelableClass)) {
        throw new BadParcelableException("Parcelable protocol requires that the "
            + "class implements Parcelable");
      }
      Field f = parcelableClass.getField("CREATOR");

      // this is a fix for JDK8<->Android VM incompatibility:
      // Apparently, JDK will not allow access to a public field if its
      // class is not visible (private or package-private) from the call-site.
      f.setAccessible(true);

      if ((f.getModifiers() & Modifier.STATIC) == 0) {
        throw new BadParcelableException("Parcelable protocol requires "
            + "the CREATOR object to be static on class " + name);
      }
      Class<?> creatorType = f.getType();
      if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) {
        // Fail before calling Field.get(), not after, to avoid initializing
        // parcelableClass unnecessarily.
        throw new BadParcelableException("Parcelable protocol requires a "
            + "Parcelable.Creator object called "
            + "CREATOR on class " + name);
      }
      creator = (Parcelable.Creator<?>) f.get(null);
    } catch (IllegalAccessException e) {
      Log.e(TAG, "Illegal access when unmarshalling: " + name, e);
      throw new BadParcelableException(
          "IllegalAccessException when unmarshalling: " + name);
    } catch (ClassNotFoundException e) {
      Log.e(TAG, "Class not found when unmarshalling: " + name, e);
      throw new BadParcelableException(
          "ClassNotFoundException when unmarshalling: " + name);
    } catch (NoSuchFieldException e) {
      throw new BadParcelableException("Parcelable protocol requires a "
          + "Parcelable.Creator object called "
          + "CREATOR on class " + name);
    }
    if (creator == null) {
      throw new BadParcelableException("Parcelable protocol requires a "
          + "non-null Parcelable.Creator object called "
          + "CREATOR on class " + name);
    }
    return creator;
  }

  @Implementation
  protected void writeByteArray(byte[] b, int offset, int len) {
    if (b == null) {
      realObject.writeInt(-1);
      return;
    }
    Number nativePtr = ReflectionHelpers.getField(realObject, "mNativePtr");
    nativeWriteByteArray(nativePtr.longValue(), b, offset, len);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeDataSize(int nativePtr) {
    return nativeDataSize((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static int nativeDataSize(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).dataSize();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeDataAvail(int nativePtr) {
    return nativeDataAvail((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static int nativeDataAvail(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).dataAvailable();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeDataPosition(int nativePtr) {
    return nativeDataPosition((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static int nativeDataPosition(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).dataPosition();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeDataCapacity(int nativePtr) {
    return nativeDataCapacity((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static int nativeDataCapacity(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).dataCapacity();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeSetDataSize(int nativePtr, int size) {
    nativeSetDataSize((long) nativePtr, size);
  }

  @Implementation(minSdk = LOLLIPOP)
  @SuppressWarnings("robolectric.ShadowReturnTypeMismatch")
  protected static void nativeSetDataSize(long nativePtr, int size) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).setDataSize(size);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeSetDataPosition(int nativePtr, int pos) {
    nativeSetDataPosition((long) nativePtr, pos);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeSetDataPosition(long nativePtr, int pos) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).setDataPosition(pos);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeSetDataCapacity(int nativePtr, int size) {
    nativeSetDataCapacity((long) nativePtr, size);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeSetDataCapacity(long nativePtr, int size) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).setDataCapacityAtLeast(size);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeWriteByteArray(int nativePtr, byte[] b, int offset, int len) {
    nativeWriteByteArray((long) nativePtr, b, offset, len);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).writeByteArray(b, offset, len);
  }

  // duplicate the writeBlob implementation from latest android, to avoid referencing the
  // non-existent-in-JDK java.util.Arrays.checkOffsetAndCount method.
  @Implementation(minSdk = M)
  protected void writeBlob(byte[] b, int offset, int len) {
    if (b == null) {
      realObject.writeInt(-1);
      return;
    }
    throwsIfOutOfBounds(b.length, offset, len);
    long nativePtr = ReflectionHelpers.getField(realObject, "mNativePtr");
    nativeWriteBlob(nativePtr, b, offset, len);
  }

  private static void throwsIfOutOfBounds(int len, int offset, int count) {
    if (len < 0) {
      throw new ArrayIndexOutOfBoundsException("Negative length: " + len);
    }

    if ((offset | count) < 0 || offset > len - count) {
      throw new ArrayIndexOutOfBoundsException();
    }
  }

  // nativeWriteBlob was introduced in lollipop, thus no need for a int nativePtr variant
  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) {
    nativeWriteByteArray(nativePtr, b, offset, len);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeWriteInt(int nativePtr, int val) {
    nativeWriteInt((long) nativePtr, val);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteInt(long nativePtr, int val) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).writeInt(val);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeWriteLong(int nativePtr, long val) {
    nativeWriteLong((long) nativePtr, val);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteLong(long nativePtr, long val) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).writeLong(val);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeWriteFloat(int nativePtr, float val) {
    nativeWriteFloat((long) nativePtr, val);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteFloat(long nativePtr, float val) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).writeFloat(val);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeWriteDouble(int nativePtr, double val) {
    nativeWriteDouble((long) nativePtr, val);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteDouble(long nativePtr, double val) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).writeDouble(val);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeWriteString(int nativePtr, String val) {
    nativeWriteString((long) nativePtr, val);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteString(long nativePtr, String val) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).writeString(val);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  protected static void nativeWriteStrongBinder(int nativePtr, IBinder val) {
    nativeWriteStrongBinder((long) nativePtr, val);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteStrongBinder(long nativePtr, IBinder val) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).writeStrongBinder(val);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static byte[] nativeCreateByteArray(int nativePtr) {
    return nativeCreateByteArray((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static byte[] nativeCreateByteArray(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).createByteArray();
  }

  // nativeReadBlob was introduced in lollipop, thus no need for a int nativePtr variant
  @Implementation(minSdk = LOLLIPOP)
  protected static byte[] nativeReadBlob(long nativePtr) {
    return nativeCreateByteArray(nativePtr);
  }

  @Implementation(minSdk = O_MR1)
  protected static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).readByteArray(dest, destLen);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static int nativeReadInt(int nativePtr) {
    return nativeReadInt((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static int nativeReadInt(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).readInt();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static long nativeReadLong(int nativePtr) {
    return nativeReadLong((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static long nativeReadLong(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).readLong();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static float nativeReadFloat(int nativePtr) {
    return nativeReadFloat((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static float nativeReadFloat(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).readFloat();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static double nativeReadDouble(int nativePtr) {
    return nativeReadDouble((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static double nativeReadDouble(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).readDouble();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static String nativeReadString(int nativePtr) {
    return nativeReadString((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static String nativeReadString(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).readString();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  protected static IBinder nativeReadStrongBinder(int nativePtr) {
    return nativeReadStrongBinder((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static IBinder nativeReadStrongBinder(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).readStrongBinder();
  }

  @Implementation @HiddenApi
  synchronized public static Number nativeCreate() {
    long nativePtr = nextNativePtr++;
    NATIVE_PTR_TO_PARCEL.put(nativePtr, new ByteBuffer());
    return castNativePtr(nativePtr);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeFreeBuffer(int nativePtr) {
    nativeFreeBuffer((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  @SuppressWarnings("robolectric.ShadowReturnTypeMismatch")
  protected static void nativeFreeBuffer(long nativePtr) {
    NATIVE_PTR_TO_PARCEL.get(nativePtr).clear();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeDestroy(int nativePtr) {
    nativeDestroy((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeDestroy(long nativePtr) {
    NATIVE_PTR_TO_PARCEL.remove(nativePtr);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static byte[] nativeMarshall(int nativePtr) {
    return nativeMarshall((long) nativePtr);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static byte[] nativeMarshall(long nativePtr) {
    return NATIVE_PTR_TO_PARCEL.get(nativePtr).toByteArray();
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeUnmarshall(int nativePtr, byte[] data, int offset, int length) {
    nativeUnmarshall((long) nativePtr, data, offset, length);
  }

  @Implementation(minSdk = LOLLIPOP)
  @SuppressWarnings("robolectric.ShadowReturnTypeMismatch")
  protected static void nativeUnmarshall(long nativePtr, byte[] data, int offset, int length) {
    NATIVE_PTR_TO_PARCEL.put(nativePtr, ByteBuffer.fromByteArray(data, offset, length));
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeAppendFrom(int thisNativePtr, int otherNativePtr, int offset, int length) {
    nativeAppendFrom((long) thisNativePtr, otherNativePtr, offset, length);
  }

  @Implementation(minSdk = LOLLIPOP)
  @SuppressWarnings("robolectric.ShadowReturnTypeMismatch")
  protected static void nativeAppendFrom(
      long thisNativePtr, long otherNativePtr, int offset, int length) {
    ByteBuffer thisByteBuffer = NATIVE_PTR_TO_PARCEL.get(thisNativePtr);
    ByteBuffer otherByteBuffer = NATIVE_PTR_TO_PARCEL.get(otherNativePtr);
    thisByteBuffer.appendFrom(otherByteBuffer, offset, length);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeWriteInterfaceToken(int nativePtr, String interfaceName) {
    nativeWriteInterfaceToken((long) nativePtr, interfaceName);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeWriteInterfaceToken(long nativePtr, String interfaceName) {
    // Write StrictMode.ThreadPolicy bits (assume 0 for test).
    nativeWriteInt(nativePtr, 0);
    nativeWriteString(nativePtr, interfaceName);
  }

  @HiddenApi
  @Implementation(maxSdk = KITKAT_WATCH)
  public static void nativeEnforceInterface(int nativePtr, String interfaceName) {
    nativeEnforceInterface((long) nativePtr, interfaceName);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected static void nativeEnforceInterface(long nativePtr, String interfaceName) {
    // Consume StrictMode.ThreadPolicy bits (don't bother setting in test).
    nativeReadInt(nativePtr);
    String actualInterfaceName = nativeReadString(nativePtr);
    if (!Objects.equals(interfaceName, actualInterfaceName)) {
      throw new SecurityException("Binder invocation to an incorrect interface");
    }
  }

  /**
   * Robolectric-specific error thrown when tests exercise error-prone behavior in Parcel.
   *
   *
Standard Android parcels rarely throw exceptions, but will happily behave in unintended
   * ways. Parcels are not strongly typed, so will happily re-interpret corrupt contents in ways
   * that cause hard-to-diagnose failures, or will cause tests to pass when they should not.
   * ShadowParcel attempts to detect these conditions.
   *
   *
This exception is package-private because production or test code should never catch or rely
   * on this, and may be changed to be an Error (rather than Exception) in the future.
   */
  static class UnreliableBehaviorError extends AssertionError {
    UnreliableBehaviorError(String message) {
      super(message);
    }

    UnreliableBehaviorError(String message, Throwable cause) {
      super(message, cause);
    }

    UnreliableBehaviorError(
        Class<?> clazz, int position, ByteBuffer.FakeEncodedItem item, String extraMessage) {
      super(
          String.format(
              Locale.US,
              "Looking for %s at position %d, found %s [%s] taking %d bytes, %s",
              clazz.getSimpleName(),
              position,
              item.value == null ? "null" : item.value.getClass().getSimpleName(),
              item.value,
              item.sizeBytes,
              extraMessage));
    }
  }

  /**
   * ByteBuffer pretends to be the underlying Parcel implementation.
   *
   *
It faithfully simulates Parcel's handling of position, size, and capacity, but is strongly
   * typed internally. It was debated whether this should instead faithfully represent Android's
   * Parcel bug-for-bug as a true byte array, along with all of its error-tolerant behavior and
   * ability essentially to {@code reinterpret_cast} data. However, the fail-fast behavior here has
   * found several test bugs and avoids reliance on architecture-specific details like Endian-ness.
   *
   *
Quirky behavior this explicitly emulates:
   *
   *

   *
Continuing to read past the end returns zeros/nulls.
   *
{@link setDataCapacity} never decreases buffer size.
   *
It is possible to partially or completely overwrite byte ranges in the buffer.
   *
Zero bytes can be exchanged between primitive data types and empty array/string.
   *

   *
   *
Quirky behavior this forbids:
   *
   *

   *
Reading past the end after writing without calling setDataPosition(0), since there's no
   *       legitimate reason to do this, and is a very common test bug.
   *
Writing one type and reading another; for example, writing a Long and reading two
   *       Integers, or writing a byte array and reading a String. This, effectively like {@code
   *       reinterpret_cast}, may not be portable across architectures.
   *
Similarly, reading from objects that have been truncated or partially overwritten, or
   *       reading from the middle of them.
   *
Using appendFrom to overwrite data, which in Parcel will overwrite the data and
   *       expand data size by the same amount, introducing empty gaps.
   *
Reading from or marshalling buffers with uninitialized gaps (e.g. where data position was
   *       expanded but nothing was written)
   *

   *
   *
Possibly-unwanted divergent behavior:
   *
   *

   *
Reading an object will often return the same instance that was written.
   *
The marshalled form does not at all resemble Parcel's. This is to maintain compatibility
   *       with existing clients that rely on the Java-serialization-based format.
   *
Uses substantially more memory, since each "byte" takes at minimum 4 bytes for a pointer,
   *       and even more for the overhead of allocating a record for each write. But note there is
   *       only at most one allocation for every 4 byte positions.
   *

   */
  private static class ByteBuffer {
    /** Number of bytes in Parcel used by an int, length, or anything smaller. */
    private static final int INT_SIZE_BYTES = 4;
    /** Number of bytes in Parcel used by a long or double. */
    private static final int LONG_OR_DOUBLE_SIZE_BYTES = 8;
    /** Immutable empty byte array. */
    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    /** Representation for an item that has been serialized in a parcel. */
    private static class FakeEncodedItem implements Serializable {
      /** Number of consecutive bytes consumed by this object. */
      final int sizeBytes;
      /** The original typed value stored. */
      final Object value;
      /**
       * Whether this item's byte-encoding is all zero.
       *
       *
This is the one exception to strong typing in ShadowParcel. Since zero can be portably
       * handled by many primitive types as zeros, and strings and arrays as empty. Note that when
       * zeroes are successfully read, the size of this entry may be ignored and the position may
       * progress to the middle of this, which remains safe as long as types that handle zeros are
       * used.
       */
      final boolean isEncodedAsAllZeroBytes;

      FakeEncodedItem(int sizeBytes, Object value) {
        this.sizeBytes = sizeBytes;
        this.value = value;
        this.isEncodedAsAllZeroBytes = isEncodedAsAllZeroBytes(value);
      }
    }

    /**
     * A type-safe simulation of the Parcel's data buffer.
     *
     *
Each index represents a byte of the parcel. Instead of storing raw bytes, this contains
     * records containing both the original data (in its original Java type) as well as the length.
     * Consecutive indices will point to the same FakeEncodedItem instance; for example, an item
     * with sizeBytes of 24 will, in normal cases, have references from 24 consecutive indices.
     *
     *
There are two main fail-fast features in this type-safe buffer. First, objects may only be
     * read from the parcel as the same type they were stored with, enforced by casting. Second,
     * this fails fast when reading incomplete or partially overwritten items.
     *
     *
Even though writing a custom resizable array is a code smell vs ArrayList, arrays' fixed
     * capacity closely models Parcel's dataCapacity (which we emulate anyway), and bulk array
     * utilities are robust compared to ArrayList's bulk operations.
     */
    private FakeEncodedItem[] data;
    /** The read/write pointer. */
    private int dataPosition;
    /** The length of the buffer; the capacity is data.length. */
    private int dataSize;
    /**
     * Whether the next read should fail if it's past the end of the array.
     *
     *
This is set true when modifying the end of the buffer, and cleared if a data position was
     * explicitly set.
     */
    private boolean failNextReadIfPastEnd;

    ByteBuffer() {
      clear();
    }

    /** Removes all elements from the byte buffer */
    public void clear() {
      data = new FakeEncodedItem[0];
      dataPosition = 0;
      dataSize = 0;
      failNextReadIfPastEnd = false;
    }

    /** Reads a byte array from the byte buffer based on the current data position */
    public byte[] createByteArray() {
      // It would be simpler just to store the byte array without a separate length.  However, the
      // "non-native" code in Parcel short-circuits null to -1, so this must consistently write a
      // separate length field in all cases.
      int length = readInt();
      if (length == -1) {
        return null;
      }
      if (length == 0) {
        return EMPTY_BYTE_ARRAY;
      }
      Object current = peek();
      if (current instanceof Byte) {
        // Legacy-encoded byte arrays (created by some tests) encode individual bytes, and do not
        // align to the integer.
        return readLegacyByteArray(length);
      } else if (readZeroes(alignToInt(length))) {
        return new byte[length];
      }
      byte[] result = readValue(EMPTY_BYTE_ARRAY, byte[].class, /* allowNull= */ false);
      if (result.length != length) {
        // Looks like the length doesn't correspond to the array.
        throw new UnreliableBehaviorError(
            String.format(
                Locale.US,
                "Byte array's length prefix is %d but real length is %d",
                length,
                result.length));
      }
      return result;
    }

    /** Reads a byte array encoded the way ShadowParcel previously encoded byte arrays. */
    private byte[] readLegacyByteArray(int length) {
      // Some tests rely on ShadowParcel's previous byte-by-byte encoding.
      byte[] result = new byte[length];
      for (int i = 0; i < length; i++) {
        result[i] = readPrimitive(1, (byte) 0, Byte.class);
      }
      return result;
    }

    /**
     * Reads a byte array from the byte buffer based on the current data position
     */
    public boolean readByteArray(byte[] dest, int destLen) {
      byte[] result = createByteArray();
      if (result == null || destLen != result.length) {
        // Since older versions of Android (pre O MR1) don't call this method at all, let's be more
        // consistent with them and let android.os.Parcel throw RuntimeException, instead of
        // throwing a more helpful exception.
        return false;
      }
      System.arraycopy(result, 0, dest, 0, destLen);
      return true;
    }

    /**
     * Writes a byte array starting at offset for length bytes to the byte buffer at the current
     * data position
     */
    public void writeByteArray(byte[] b, int offset, int length) {
      writeInt(length);
      // Native parcel writes a byte array as length plus the individual bytes.  But we can't write
      // bytes individually because each byte would take up 4 bytes due to Parcel's alignment
      // behavior.  Instead we write the length, and if non-empty, we write the array.
      if (length != 0) {
        writeValue(length, Arrays.copyOfRange(b, offset, length));
      }
    }

    /**
     * Writes an int to the byte buffer at the current data position
     */
    public void writeInt(int i) {
      writeValue(INT_SIZE_BYTES, i);
    }

    /**
     * Reads a int from the byte buffer based on the current data position
     */
    public int readInt() {
      return readPrimitive(INT_SIZE_BYTES, 0, Integer.class);
    }

    /**
     * Writes a long to the byte buffer at the current data position
     */
    public void writeLong(long l) {
      writeValue(LONG_OR_DOUBLE_SIZE_BYTES, l);
    }

    /**
     * Reads a long from the byte buffer based on the current data position
     */
    public long readLong() {
      return readPrimitive(LONG_OR_DOUBLE_SIZE_BYTES, 0L, Long.class);
    }

    /**
     * Writes a float to the byte buffer at the current data position
     */
    public void writeFloat(float f) {
      writeValue(INT_SIZE_BYTES, f);
    }

    /**
     * Reads a float from the byte buffer based on the current data position
     */
    public float readFloat() {
      return readPrimitive(INT_SIZE_BYTES, 0f, Float.class);
    }

    /**
     * Writes a double to the byte buffer at the current data position
     */
    public void writeDouble(double d) {
      writeValue(LONG_OR_DOUBLE_SIZE_BYTES, d);
    }

    /**
     * Reads a double from the byte buffer based on the current data position
     */
    public double readDouble() {
      return readPrimitive(LONG_OR_DOUBLE_SIZE_BYTES, 0d, Double.class);
    }

    /** Writes a String to the byte buffer at the current data position */
    public void writeString(String s) {
      int nullTerminatedChars = (s != null) ? (s.length() + 1) : 0;
      // Android encodes strings as length plus a null-terminated array of 2-byte characters.
      // writeValue will pad to nearest 4 bytes.  Null is encoded as just -1.
      int sizeBytes = INT_SIZE_BYTES + (nullTerminatedChars * 2);
      writeValue(sizeBytes, s);
    }

    /**
     * Reads a String from the byte buffer based on the current data position
     */
    public String readString() {
      if (readZeroes(INT_SIZE_BYTES * 2)) {
        // Empty string is 4 bytes for length of 0, and 4 bytes for null terminator and padding.
        return "";
      }
      return readValue(null, String.class, /* allowNull= */ true);
    }

    /**
     * Writes an IBinder to the byte buffer at the current data position
     */
    public void writeStrongBinder(IBinder b) {
      // Size of struct flat_binder_object in android/binder.h used to encode binders in the real
      // parceling code.
      int length = 5 * INT_SIZE_BYTES;
      writeValue(length, b);
    }

    /**
     * Reads an IBinder from the byte buffer based on the current data position
     */
    public IBinder readStrongBinder() {
      return readValue(null, IBinder.class, /* allowNull= */ true);
    }

    /**
     * Appends the contents of the other byte buffer to this byte buffer starting at offset and
     * ending at length.
     *
     * @param other ByteBuffer to append to this one
     * @param offset number of bytes from beginning of byte buffer to start copy from
     * @param length number of bytes to copy
     */
    public void appendFrom(ByteBuffer other, int offset, int length) {
      int oldSize = dataSize;
      if (dataPosition != dataSize) {
        // Parcel.cpp will always expand the buffer by length even if it is overwriting existing
        // data, yielding extra uninitialized data at the end, in contrast to write methods that
        // won't increase the data length if they are overwriting in place.  This is surprising
        // behavior that production code should avoid.
        throw new UnreliableBehaviorError(
            "Real Android parcels behave unreliably if appendFrom is "
                + "called from any position other than the end");
      }
      setDataSize(oldSize + length);
      // Just blindly copy whatever happens to be in the buffer.  Reads will validate whether any
      // of the objects were only incompletely copied.
      System.arraycopy(other.data, offset, data, dataPosition, length);
      dataPosition += length;
      failNextReadIfPastEnd = true;
    }

    /** Returns whether a data type is encoded as all zeroes. */
    private static boolean isEncodedAsAllZeroBytes(Object value) {
      if (value == null) {
        return false; // Nulls are usually encoded as -1.
      }
      if (value instanceof Number) {
        Number number = (Number) value;
        return number.longValue() == 0 && number.doubleValue() == 0;
      }
      if (value instanceof byte[]) {
        byte[] array = (byte[]) value;
        return isAllZeroes(array, 0, array.length);
      }
      // NOTE: While empty string is all zeros, trying to read an empty string as zeroes is
      // probably unintended; the reverse is supported just so all-zero buffers don't fail.
      return false;
    }

    /** Identifies all zeroes, which can be safely reinterpreted to other types. */
    private static boolean isAllZeroes(byte[] array, int offset, int length) {
      for (int i = offset; i < length; i++) {
        if (array[i] != 0) {
          return false;
        }
      }
      return true;
    }

    /**
     * Creates a Byte buffer from a raw byte array.
     *
     * @param array byte array to read from
     * @param offset starting position in bytes to start reading array at
     * @param length number of bytes to read from array
     */
    public static ByteBuffer fromByteArray(byte[] array, int offset, int length) {
      ByteBuffer byteBuffer = new ByteBuffer();

      if (isAllZeroes(array, offset, length)) {
        // Special case: for all zeroes, it's definitely not an ObjectInputStream, because it has a
        // non-zero mandatory magic.  Zeroes have a portable, unambiguous interpretation.
        byteBuffer.setDataSize(length);
        byteBuffer.writeItem(new FakeEncodedItem(length, new byte[length]));
        return byteBuffer;
      }

      try {
        ByteArrayInputStream bis = new ByteArrayInputStream(array, offset,
            length);
        ObjectInputStream ois = new ObjectInputStream(bis);
        int numElements = ois.readInt();
        for (int i = 0; i < numElements; i++) {
          int sizeOf = ois.readInt();
          Object value = ois.readObject();
          // NOTE: Bypassing writeValue so that this will support ShadowParcels that were
          // marshalled before ShadowParcel simulated alignment.
          byteBuffer.writeItem(new FakeEncodedItem(sizeOf, value));
        }
        // Android leaves the data position at the end in this case.
        return byteBuffer;
      } catch (Exception e) {
        throw new UnreliableBehaviorError("ShadowParcel unable to unmarshall its custom format", e);
      }
    }

    /**
     * Converts a ByteBuffer to a raw byte array. This method should be symmetrical with
     * fromByteArray.
     */
    public byte[] toByteArray() {
      int oldDataPosition = dataPosition;
      try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        // NOTE: Serializing the data array would be simpler, and serialization would actually
        // preserve reference equality between entries.  However, the length-encoded format here
        // preserves the previous format, which some tests appear to rely on.
        List<FakeEncodedItem> entries = new ArrayList<>();
        // NOTE: Use readNextItem to scan so the contents can be proactively validated.
        dataPosition = 0;
        while (dataPosition < dataSize) {
          entries.add(readNextItem(Object.class));
        }
        oos.writeInt(entries.size());
        for (FakeEncodedItem item : entries) {
          oos.writeInt(item.sizeBytes);
          oos.writeObject(item.value);
        }
        oos.flush();
        return bos.toByteArray();
      } catch (IOException e) {
        throw new UnreliableBehaviorError("ErrorProne unable to serialize its custom format", e);
      } finally {
        dataPosition = oldDataPosition;
      }
    }

    /**
     * Number of unused bytes in this byte buffer.
     */
    public int dataAvailable() {
      return dataSize() - dataPosition();
    }

    /**
     * Total buffer size in bytes of byte buffer included unused space.
     */
    public int dataCapacity() {
      return data.length;
    }

    /**
     * Current data position of byte buffer in bytes. Reads / writes are from this position.
     */
    public int dataPosition() {
      return dataPosition;
    }

    /**
     * Current amount of bytes currently written for ByteBuffer.
     */
    public int dataSize() {
      return dataSize;
    }

    /**
     * Sets the current data position.
     *
     * @param pos
     *          Desired position in bytes
     */
    public void setDataPosition(int pos) {
      if (pos > dataSize) {
        // NOTE: Real parcel ignores this until a write occurs.
        throw new UnreliableBehaviorError(pos + " greater than dataSize " + dataSize);
      }
      dataPosition = pos;
      failNextReadIfPastEnd = false;
    }

    public void setDataSize(int size) {
      if (size < dataSize) {
        // Clear all the inaccessible bytes when shrinking, to allow garbage collection, and so
        // they remain cleared if expanded again.  Note this might truncate something mid-object,
        // which would be handled at read time.
        Arrays.fill(data, size, dataSize, null);
      }
      setDataCapacityAtLeast(size);
      dataSize = size;
      if (dataPosition >= dataSize) {
        dataPosition = dataSize;
      }
    }

    public void setDataCapacityAtLeast(int newCapacity) {
      // NOTE: Oddly, Parcel only every increases data capacity, and never decreases it, so this
      // really should have never been named setDataCapacity.
      if (newCapacity > data.length) {
        FakeEncodedItem[] newData = new FakeEncodedItem[newCapacity];
        dataSize = Math.min(dataSize, newCapacity);
        dataPosition = Math.min(dataPosition, dataSize);
        System.arraycopy(data, 0, newData, 0, dataSize);
        data = newData;
      }
    }

    /** Rounds to next 4-byte bounder similar to native Parcel. */
    private int alignToInt(int unpaddedSizeBytes) {
      return ((unpaddedSizeBytes + 3) / 4) * 4;
    }

    /**
     * Ensures that the next sizeBytes are all the initial value we read.
     *
     *
This detects:
     *
     *

     *
Reading an item, but not starting at its start position
     *
Reading items that were truncated by setSize
     *
Reading items that were partially overwritten by another
     *

     */
    private void checkConsistentReadAndIncrementPosition(Class<?> clazz, FakeEncodedItem item) {
      int endPosition = dataPosition + item.sizeBytes;
      for (int i = dataPosition; i < endPosition; i++) {
        FakeEncodedItem foundItemItem = i < dataSize ? data[i] : null;
        if (foundItemItem != item) {
          throw new UnreliableBehaviorError(
              clazz,
              dataPosition,
              item,
              String.format(
                  Locale.US,
                  "but [%s] interrupts it at position %d",
                  foundItemItem == null
                      ? "uninitialized data or the end of the buffer"
                      : foundItemItem.value,
                  i));
        }
      }
      dataPosition = Math.min(dataSize, dataPosition + item.sizeBytes);
    }

    /** Returns the item at the current position, or null if uninitialized or null. */
    private Object peek() {
      return dataPosition < dataSize && data[dataPosition] != null
          ? data[dataPosition].value
          : null;
    }

    /**
     * Reads a complete item in the byte buffer.
     *
     * @param clazz this is the type that is being read, but not checked in this method
     * @return null if the default value should be returned, otherwise the item holding the data
     */
    private <T> FakeEncodedItem readNextItem(Class<T> clazz) {
      FakeEncodedItem item = data[dataPosition];
      if (item == null) {
        // While Parcel will treat these as zeros, in tests, this is almost always an error.
        throw new UnreliableBehaviorError("Reading uninitialized data at position " + dataPosition);
      }
      checkConsistentReadAndIncrementPosition(clazz, item);
      return item;
    }

    /**
     * Reads the next value in the byte buffer of a specified type.
     *
     * @param pastEndValue value to return when reading past the end of the buffer
     * @param clazz this is the type that is being read, but not checked in this method
     * @param allowNull whether null values are permitted
     */
    private <T> T readValue(T pastEndValue, Class<T> clazz, boolean allowNull) {
      if (dataPosition >= dataSize) {
        // Normally, reading past the end is permitted, and returns the default values.  However,
        // writing to a parcel then reading without setting the position back to 0 is an incredibly
        // common error to make in tests, and should never really happen in production code, so
        // this shadow will fail in this condition.
        if (failNextReadIfPastEnd) {
          throw new UnreliableBehaviorError(
              "Did you forget to setDataPosition(0) before reading the parcel?");
        }
        return pastEndValue;
      }
      int startPosition = dataPosition;
      FakeEncodedItem item = readNextItem(clazz);
      if (item == null) {
        return pastEndValue;
      } else if (item.value == null && allowNull) {
        return null;
      } else if (clazz.isInstance(item.value)) {
        return clazz.cast(item.value);
      } else {
        // Numerous existing tests rely on ShadowParcel throwing RuntimeException and catching
        // them.  Many of these tests are trying to test what happens when an invalid Parcel is
        // provided.  However, Android has no concept of an "invalid parcel" because Parcel will
        // happily return garbage if you ask for it.  The only runtime exceptions are thrown on
        // array length mismatches, or higher-level APIs like Parcelable (which has its own safety
        // checks).  Tests trying to test error-handling behavior should instead craft a Parcel
        // that specifically triggers a BadParcelableException.
        throw new RuntimeException(
            new UnreliableBehaviorError(
                clazz, startPosition, item, "and it is non-portable to reinterpret it"));
      }
    }

    /**
     * Determines if there is a sequence of castable zeroes, and consumes them.
     *
     *
This is the only exception for strong typing, because zero bytes are portable and
     * unambiguous. There are a few situations where well-written code can rely on this, so it is
     * worthwhile making a special exception for. This tolerates partially-overwritten and truncated
     * values if all bytes are zero.
     */
    private boolean readZeroes(int bytes) {
      int endPosition = dataPosition + bytes;
      if (endPosition > dataSize) {
        return false;
      }
      for (int i = dataPosition; i < endPosition; i++) {
        if (data[i] == null || !data[i].isEncodedAsAllZeroBytes) {
          return false;
        }
      }
      // Note in this case we short-circuit other verification -- even if we are reading weirdly
      // clobbered zeroes, they're still zeroes.  Future reads might fail, though.
      dataPosition = endPosition;
      return true;
    }

    /**
     * Reads a primitive, which may reinterpret zeros of other types.
     *
     * @param defaultSizeBytes if reinterpreting zeros, the number of bytes to consume
     * @param defaultValue the default value for zeros or reading past the end
     * @param clazz this is the type that is being read, but not checked in this method
     */
    private <T> T readPrimitive(int defaultSizeBytes, T defaultValue, Class<T> clazz) {
      // Check for zeroes first, since partially-overwritten values are not an error for zeroes.
      if (readZeroes(defaultSizeBytes)) {
        return defaultValue;
      }
      return readValue(defaultValue, clazz, /* allowNull= */ false);
    }

    /** Writes an encoded item directly, bypassing alignment, and possibly repeating an item. */
    private void writeItem(FakeEncodedItem item) {
      int endPosition = dataPosition + item.sizeBytes;
      if (endPosition > data.length) {
        // Parcel grows by 3/2 of the new size.
        setDataCapacityAtLeast(endPosition * 3 / 2);
      }
      if (endPosition > dataSize) {
        failNextReadIfPastEnd = true;
        dataSize = endPosition;
      }
      Arrays.fill(data, dataPosition, endPosition, item);
      dataPosition = endPosition;
    }

    /**
     * Writes a value to the next range of bytes.
     *
     *
Writes are aligned to 4-byte regions.
     */
    private void writeValue(int unpaddedSizeBytes, Object o) {
      // Create the item with its final, aligned byte size.
      writeItem(new FakeEncodedItem(alignToInt(unpaddedSizeBytes), o));
    }
  }

  @Implementation(maxSdk = P)
  protected static FileDescriptor openFileDescriptor(String file, int mode) throws IOException {
    RandomAccessFile randomAccessFile =
        new RandomAccessFile(file, mode == ParcelFileDescriptor.MODE_READ_ONLY ? "r" : "rw");
    return randomAccessFile.getFD();
  }
}


package org.robolectric.shadows;

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

import android.app.Activity;
import android.app.KeyguardManager;
import android.app.KeyguardManager.KeyguardDismissCallback;
import android.content.Intent;
import java.util.HashSet;
import java.util.Set;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;

@Implements(KeyguardManager.class)
public class ShadowKeyguardManager {
  @RealObject private KeyguardManager realKeyguardManager;

  private static KeyguardManager.KeyguardLock keyguardLock =
      Shadow.newInstanceOf(KeyguardManager.KeyguardLock.class);

  private static final Set<Integer> deviceLockedForUsers = new HashSet<Integer>();
  private static final Set<Integer> deviceSecureForUsers = new HashSet<Integer>();
  private static boolean inRestrictedInputMode;
  private static boolean isKeyguardLocked;
  private static boolean isDeviceLocked;
  private static boolean isKeyguardSecure;
  private static boolean isDeviceSecure;
  private static Intent confirmFactoryResetCredentialIntent;
  private static KeyguardManager.KeyguardDismissCallback callback;

  /**
   * For tests, returns the value set via {@link #setinRestrictedInputMode(boolean)}, or `false` by
   * default.
   *
   * @see #setInRestrictedInputMode(boolean)
   */
  @Implementation
  protected boolean inKeyguardRestrictedInputMode() {
    return inRestrictedInputMode;
  }

  @Implementation(minSdk = O)
  protected void requestDismissKeyguard(
      Activity activity, KeyguardManager.KeyguardDismissCallback callback) {
    if (isKeyguardLocked) {
      if (this.callback != null) {
        callback.onDismissError();
      }
      this.callback = callback;
    } else {
      callback.onDismissError();
    }
  }

  /**
   * For tests, returns the value set via {@link #setKeyguardLocked(boolean)}, or `false` by
   * default.
   *
   * @see #setKeyguardLocked(boolean)
   */
  @Implementation
  protected boolean isKeyguardLocked() {
    return isKeyguardLocked;
  }

  /**
   * Sets whether the device keyguard is locked or not. This affects the value to be returned by
   * {@link #isKeyguardLocked()} and also invokes callbacks set in
   *  {@link KeyguardManager#requestDismissKeyguard()}.
   *
   *  @param isKeyguardLocked true to lock the keyguard. If a KeyguardDismissCallback is set will
   *  fire {@link KeyguardDismissCallback#onDismissCancelled()} or false to unlock and dismiss the
   *  keyguard firing {@link KeyguardDismissCallback#onDismissSucceeded()} if a
   *  KeyguardDismissCallback is set.
   *  */
  public void setKeyguardLocked(boolean isKeyguardLocked) {
    this.isKeyguardLocked = isKeyguardLocked;
    if (callback != null) {
      if (isKeyguardLocked) {
        callback.onDismissCancelled();
      } else {
        callback.onDismissSucceeded();
      }
      callback = null;
    }
  }

  /**
   * For tests, returns a {@link ShadowKeyguardLock}.
   *
   * @see ShadowKeyguardLock
   */
  @Implementation
  protected KeyguardManager.KeyguardLock newKeyguardLock(String tag) {
    return keyguardLock;
  }

  /**
   * Sets the value to be returned by {@link KeyguardManager#inKeyguardRestrictedInputMode()}.
   *
   * @see KeyguardManager#inKeyguardRestrictedInputMode()
   * @deprecated use {@link #setInRestrictedInputMode(boolean)} instead
   */
  @Deprecated
  public void setinRestrictedInputMode(boolean restricted) {
    inRestrictedInputMode = restricted;
  }

  /**
   * Sets the value to be returned by {@link KeyguardManager#inKeyguardRestrictedInputMode()}.
   *
   * @see KeyguardManager#inKeyguardRestrictedInputMode()
   */
  public void setInRestrictedInputMode(boolean restricted) {
    inRestrictedInputMode = restricted;
  }

  /**
   * For tests, returns the value set by {@link #setIsKeyguardSecure(boolean)}, or `false` by
   * default.
   *
   * @see #setIsKeyguardSecure(boolean)
   */
  @Implementation
  protected boolean isKeyguardSecure() {
    return isKeyguardSecure;
  }

  /**
   * Sets the value to be returned by {@link #isKeyguardSecure()}.
   *
   * @see #isKeyguardSecure()
   */
  public void setIsKeyguardSecure(boolean secure) {
    isKeyguardSecure = secure;
  }

  /**
   * For tests on Android >=M, returns the value set by {@link #setIsDeviceSecure(boolean)}, or
   * `false` by default.
   *
   * @see #setIsDeviceSecure(boolean)
   */
  @Implementation(minSdk = M)
  protected boolean isDeviceSecure() {
    return isDeviceSecure;
  }

  /**
   * For tests on Android >=M, sets the value to be returned by {@link #isDeviceSecure()}.
   *
   * @see #isDeviceSecure()
   */
  public void setIsDeviceSecure(boolean isDeviceSecure) {
    this.isDeviceSecure = isDeviceSecure;
  }

  /**
   * For tests on Android >=M, returns the value set by {@link #setIsDeviceSecure(int, boolean)}, or
   * `false` by default.
   *
   * @see #setIsDeviceSecure(int, boolean)
   */
  @Implementation(minSdk = M)
  protected boolean isDeviceSecure(int userId) {
    return deviceSecureForUsers.contains(userId);
  }

  /**
   * For tests on Android >=M, sets the value to be returned by {@link #isDeviceSecure(int)}.
   *
   * @see #isDeviceSecure(int)
   */
  public void setIsDeviceSecure(int userId, boolean isDeviceSecure) {
    if (isDeviceSecure) {
      deviceSecureForUsers.add(userId);
    } else {
      deviceSecureForUsers.remove(userId);
    }
  }

  /**
   * For tests on Android >=L MR1, sets the value to be returned by {@link #isDeviceLocked()}.
   *
   * @see #isDeviceLocked()
   */
  public void setIsDeviceLocked(boolean isDeviceLocked) {
    this.isDeviceLocked = isDeviceLocked;
  }

  /**
   * @return `false` by default, or the value passed to {@link #setIsDeviceLocked(boolean)}.
   * @see #isDeviceLocked()
   */
  @Implementation(minSdk = LOLLIPOP_MR1)
  protected boolean isDeviceLocked() {
    return isDeviceLocked;
  }

  /**
   * For tests on Android >= L MR1, sets the value to be returned by {@link #isDeviceLocked(int)}.
   *
   * @see #isDeviceLocked(int)
   */
  public void setIsDeviceLocked(int userId, boolean isLocked) {
    if (isLocked) {
      deviceLockedForUsers.add(userId);
    } else {
      deviceLockedForUsers.remove(userId);
    }
  }

  @Implementation(minSdk = LOLLIPOP_MR1)
  protected boolean isDeviceLocked(int userId) {
    return deviceLockedForUsers.contains(userId);
  }

  /**
   * For tests on Android >= O MR1, sets the value to be returned by
   * {@link #createConfirmFactoryResetCredentialIntent(CharSequence,CharSequence,CharSequence)}.
   *
   * @see #createConfirmFactoryResetCredentialIntent(CharSequence,CharSequence,CharSequence)
   */
  public void setConfirmFactoryResetCredentialIntent(Intent intent) {
    confirmFactoryResetCredentialIntent = intent;
  }

  /**
   * Returns the intent set via
   * {@link #setConfirmFactoryResetCredentialIntent(Intent)}, otherwise null.
   */
  @Implementation(minSdk = O_MR1)
  protected Intent createConfirmFactoryResetCredentialIntent(
      CharSequence title, CharSequence description, CharSequence alternateButtonLabel) {
    return confirmFactoryResetCredentialIntent;
  }

  /** An implementation of {@link KeyguardManager#KeyguardLock}, for use in tests. */
  @Implements(KeyguardManager.KeyguardLock.class)
  public static class ShadowKeyguardLock {
    private boolean keyguardEnabled = true;

    /**
     * Sets the value to be returned by {@link #isEnabled()} to false.
     *
     * @see #isEnabled()
     */
    @Implementation
    protected void disableKeyguard() {
      keyguardEnabled = false;
    }

    /**
     * Sets the value to be returned by {@link #isEnabled()} to true.
     *
     * @see #isEnabled()
     */
    @Implementation
    protected void reenableKeyguard() {
      keyguardEnabled = true;
    }

    /**
     * For tests, returns the value set via {@link #disableKeyguard()} or {@link reenableKeyguard},
     * or `true` by default.
     *
     * @see #setKeyguardLocked(boolean)
     */
    public boolean isEnabled() {
      return keyguardEnabled;
    }
  }

  @Resetter
  public static void reset() {
    // Static because the state is Global but Context.getSystemService() returns a new instance
    // on each call.
    keyguardLock = Shadow.newInstanceOf(KeyguardManager.KeyguardLock.class);

    deviceLockedForUsers.clear();
    deviceSecureForUsers.clear();
    inRestrictedInputMode = false;
    isKeyguardLocked = false;
    isDeviceLocked = false;
    isKeyguardSecure = false;
    isDeviceSecure = false;
    callback = null;
  }
}


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

package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.N;
import static org.robolectric.shadows.ShadowVirtualRefBasePtr.get;
import static org.robolectric.shadows.ShadowVirtualRefBasePtr.put;

import android.graphics.drawable.VectorDrawable;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@Implements(value = VectorDrawable.class, minSdk = N)
public class ShadowVectorDrawable extends ShadowDrawable {
  //  private static native long nCreateTree(long rootGroupPtr);
//  private static native long nCreateTreeFromCopy(long treeToCopy, long rootGroupPtr);
//  private static native void nSetRendererViewportSize(long rendererPtr, float viewportWidth,
//                                                      float viewportHeight);
//  private static native boolean nSetRootAlpha(long rendererPtr, float alpha);
//  private static native float nGetRootAlpha(long rendererPtr);
//  private static native void nSetAllowCaching(long rendererPtr, boolean allowCaching);
//
//  private static native int nDraw(long rendererPtr, long canvasWrapperPtr,
//                                  long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache);

  private static final int STROKE_WIDTH_INDEX = 0;
  private static final int STROKE_COLOR_INDEX = 1;
  private static final int STROKE_ALPHA_INDEX = 2;
  private static final int FILL_COLOR_INDEX = 3;
  private static final int FILL_ALPHA_INDEX = 4;
  private static final int TRIM_PATH_START_INDEX = 5;
  private static final int TRIM_PATH_END_INDEX = 6;
  private static final int TRIM_PATH_OFFSET_INDEX = 7;
  private static final int STROKE_LINE_CAP_INDEX = 8;
  private static final int STROKE_LINE_JOIN_INDEX = 9;
  private static final int STROKE_MITER_LIMIT_INDEX = 10;
  private static final int FILL_TYPE_INDEX = 11;
  private static final int TOTAL_PROPERTY_COUNT = 12;

  private static class Path implements Cloneable {
    float strokeWidth;
    int strokeColor;
    float strokeAlpha;
    int fillColor;
    float fillAlpha;
    float trimPathStart;
    float trimPathEnd;
    float trimPathOffset;
    int strokeLineCap;
    int strokeLineJoin;
    float strokeMiterLimit;
    int fillType;

    @Override
    protected Object clone() {
      try {
        return super.clone();
      } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private static Path getPath(long pathPtr) {
    return get(pathPtr, Path.class);
  }

  @Implementation
  protected static long nCreateFullPath() {
    return put(new Path());
  }

  @Implementation
  protected static long nCreateFullPath(long nativeFullPathPtr) {
    return put(getPath(nativeFullPathPtr).clone());
  }

  @Implementation
  protected static boolean nGetFullPathProperties(long pathPtr, byte[] properties, int length) {
    if (length != TOTAL_PROPERTY_COUNT * 4) return false;

    Path path = getPath(pathPtr);
    ByteBuffer propertiesBB = ByteBuffer.wrap(properties);
    propertiesBB.order(ByteOrder.nativeOrder());
    propertiesBB.putFloat(STROKE_WIDTH_INDEX * 4, path.strokeWidth);
    propertiesBB.putInt(STROKE_COLOR_INDEX * 4, path.strokeColor);
    propertiesBB.putFloat(STROKE_ALPHA_INDEX * 4, path.strokeAlpha);
    propertiesBB.putInt(FILL_COLOR_INDEX * 4, path.fillColor);
    propertiesBB.putFloat(FILL_ALPHA_INDEX * 4, path.fillAlpha);
    propertiesBB.putFloat(TRIM_PATH_START_INDEX * 4, path.trimPathStart);
    propertiesBB.putFloat(TRIM_PATH_END_INDEX * 4, path.trimPathEnd);
    propertiesBB.putFloat(TRIM_PATH_OFFSET_INDEX * 4, path.trimPathOffset);
    propertiesBB.putInt(STROKE_LINE_CAP_INDEX * 4, path.strokeLineCap);
    propertiesBB.putInt(STROKE_LINE_JOIN_INDEX * 4, path.strokeLineJoin);
    propertiesBB.putFloat(STROKE_MITER_LIMIT_INDEX * 4, path.strokeMiterLimit);
    propertiesBB.putInt(FILL_TYPE_INDEX * 4, path.fillType);

    return true;
  }

  @Implementation
  protected static void nUpdateFullPathProperties(
      long pathPtr,
      float strokeWidth,
      int strokeColor,
      float strokeAlpha,
      int fillColor,
      float fillAlpha,
      float trimPathStart,
      float trimPathEnd,
      float trimPathOffset,
      float strokeMiterLimit,
      int strokeLineCap,
      int strokeLineJoin,
      int fillType) {
    Path path = getPath(pathPtr);
    path.strokeWidth = strokeWidth;
    path.strokeColor = strokeColor;
    path.strokeAlpha = strokeAlpha;
    path.fillColor = fillColor;
    path.fillAlpha = fillAlpha;
    path.trimPathStart = trimPathStart;
    path.trimPathEnd = trimPathEnd;
    path.trimPathOffset = trimPathOffset;
    path.strokeLineCap = strokeLineCap;
    path.strokeLineJoin = strokeLineJoin;
    path.strokeMiterLimit = strokeMiterLimit;
    path.fillType = fillType;
  }

//  @Implementation
//  public static void nUpdateFullPathFillGradient(long pathPtr, long fillGradientPtr) {
//
//  }
//
//  @Implementation
//  public static void nUpdateFullPathStrokeGradient(long pathPtr, long strokeGradientPtr) {
//
//  }
//
//  private static native long nCreateClipPath();
//  private static native long nCreateClipPath(long clipPathPtr);

  static class Group implements Cloneable {
    float rotation;
    float pivotX;
    float pivotY;
    float scaleX;
    float scaleY;
    float translateX;
    float translateY;

    @Override
    protected Object clone() {
      try {
        return super.clone();
      } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private static Group getGroup(long groupPtr) {
    return get(groupPtr, Group.class);
  }

  @Implementation
  protected static long nCreateGroup() {
    return put(new Group());
  }

  @Implementation
  protected static long nCreateGroup(long groupPtr) {
    return put(getGroup(groupPtr).clone());
  }

  //  public static void nSetName(long nodePtr, String name) {
  //  }

  @Implementation
  protected static boolean nGetGroupProperties(long groupPtr, float[] properties, int length) {
    if (length != 7) return false;
    Group group = getGroup(groupPtr);
    properties[0] = group.rotation;
    properties[1] = group.pivotX;
    properties[2] = group.pivotY;
    properties[3] = group.scaleX;
    properties[4] = group.scaleY;
    properties[5] = group.translateX;
    properties[6] = group.translateY;
    return true;
  }

  @Implementation
  protected static void nUpdateGroupProperties(
      long groupPtr,
      float rotate,
      float pivotX,
      float pivotY,
      float scaleX,
      float scaleY,
      float translateX,
      float translateY) {
    Group group = getGroup(groupPtr);
    group.rotation = rotate;
    group.pivotX = pivotX;
    group.pivotY = pivotY;
    group.scaleX = scaleX;
    group.scaleY = scaleY;
    group.translateX = translateX;
    group.translateY = translateY;
  }

//  private static native void nAddChild(long groupPtr, long nodePtr);
//  private static native void nSetPathString(long pathPtr, String pathString, int length);
//
//  /**
//   * The setters and getters below for paths and groups are here temporarily, and will be
//   * removed once the animation in AVD is replaced with RenderNodeAnimator, in which case the
//   * animation will modify these properties in native. By then no JNI hopping would be necessary
//   * for VD during animation, and these setters and getters will be obsolete.
//   */
//  // Setters and getters during animation.
//  private static native float nGetRotation(long groupPtr);
//  private static native void nSetRotation(long groupPtr, float rotation);
//  private static native float nGetPivotX(long groupPtr);
//  private static native void nSetPivotX(long groupPtr, float pivotX);
//  private static native float nGetPivotY(long groupPtr);
//  private static native void nSetPivotY(long groupPtr, float pivotY);
//  private static native float nGetScaleX(long groupPtr);
//  private static native void nSetScaleX(long groupPtr, float scaleX);
//  private static native float nGetScaleY(long groupPtr);
//  private static native void nSetScaleY(long groupPtr, float scaleY);
//  private static native float nGetTranslateX(long groupPtr);
//  private static native void nSetTranslateX(long groupPtr, float translateX);
//  private static native float nGetTranslateY(long groupPtr);
//  private static native void nSetTranslateY(long groupPtr, float translateY);
//
//  // Setters and getters for VPath during animation.
//  private static native void nSetPathData(long pathPtr, long pathDataPtr);
//  private static native float nGetStrokeWidth(long pathPtr);
//  private static native void nSetStrokeWidth(long pathPtr, float width);
//  private static native int nGetStrokeColor(long pathPtr);
//  private static native void nSetStrokeColor(long pathPtr, int strokeColor);
//  private static native float nGetStrokeAlpha(long pathPtr);
//  private static native void nSetStrokeAlpha(long pathPtr, float alpha);
//  private static native int nGetFillColor(long pathPtr);
//  private static native void nSetFillColor(long pathPtr, int fillColor);
//  private static native float nGetFillAlpha(long pathPtr);
//  private static native void nSetFillAlpha(long pathPtr, float fillAlpha);
//  private static native float nGetTrimPathStart(long pathPtr);
//  private static native void nSetTrimPathStart(long pathPtr, float trimPathStart);
//  private static native float nGetTrimPathEnd(long pathPtr);
//  private static native void nSetTrimPathEnd(long pathPtr, float trimPathEnd);
//  private static native float nGetTrimPathOffset(long pathPtr);
//  private static native void nSetTrimPathOffset(long pathPtr, float trimPathOffset);

}