package org.robolectric.android.controller;

import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
import android.widget.LinearLayout;
import androidx.test.runner.lifecycle.ActivityLifecycleCallback;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import org.robolectric.util.ReflectionHelpers;

/**
 * FragmentController provides low-level APIs to control fragment's lifecycle.
 *
 * @param <F> a class of the fragment which is under control by this class.
 * @deprecated Native Fragments have been deprecated in Android P. Android encourages developers to
 *     use androidx fragments, to test these use FragmentScenario.
 */
@Deprecated
public class FragmentController<F extends Fragment>
    extends ComponentController<FragmentController<F>, F> {
  private F fragment;
  private final ActivityController<? extends Activity> activityController;

  public static <F extends Fragment> FragmentController<F> of(F fragment) {
    return of(fragment, FragmentControllerActivity.class, null, null);
  }

  public static <F extends Fragment> FragmentController<F> of(F fragment, Class<? extends Activity> activityClass) {
    return of(fragment, activityClass, null, null);
  }

  public static <F extends Fragment> FragmentController<F> of(F fragment, Intent intent) {
    return new FragmentController<>(fragment, FragmentControllerActivity.class, intent);
  }

  public static <F extends Fragment> FragmentController<F> of(F fragment, Bundle arguments) {
    return new FragmentController<>(fragment, FragmentControllerActivity.class, arguments);
  }

  public static <F extends Fragment> FragmentController<F> of(F fragment, Intent intent, Bundle arguments) {
    return new FragmentController<>(fragment, FragmentControllerActivity.class, intent,
            arguments);
  }

  public static <F extends Fragment> FragmentController<F> of(F fragment, Class<? extends Activity> activityClass, Intent intent) {
    return new FragmentController<>(fragment, activityClass, intent);
  }

  public static <F extends Fragment> FragmentController<F> of(F fragment, Class<? extends Activity> activityClass, Bundle arguments) {
    return new FragmentController<>(fragment, activityClass, arguments);
  }

  public static <F extends Fragment> FragmentController<F> of(F fragment, Class<? extends Activity> activityClass,
                                                              Intent intent, Bundle arguments) {
    return new FragmentController<>(fragment, activityClass, intent, arguments);
  }

  private FragmentController(F fragment, Class<? extends Activity> activityClass, Intent intent) {
    this(fragment, activityClass, intent, null);
  }

  private FragmentController(F fragment, Class<? extends Activity> activityClass, Bundle arguments) {
    this(fragment, activityClass, null, arguments);
  }

  private FragmentController(F fragment, Class<? extends Activity> activityClass,
                             Intent intent, Bundle arguments) {
    super(fragment, intent);
    this.fragment = fragment;
    if (arguments != null) {
      this.fragment.setArguments(arguments);
    }
    this.activityController = ActivityController.of(ReflectionHelpers.callConstructor(activityClass), intent);
  }

  /**
   * Creates the activity with {@link Bundle} and adds the fragment to the view with ID {@code contentViewId}.
   */
  public FragmentController<F> create(final int contentViewId, final Bundle bundle) {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.create(bundle).get().getFragmentManager().beginTransaction().add(contentViewId, fragment).commit();
      }
    });
    return this;
  }

  /**
   * Creates the activity with {@link Bundle} and adds the fragment to it. Note that the fragment will be added to the view with ID 1.
   */
  public FragmentController<F> create(Bundle bundle) {
    return create(1, bundle);
  }

  @Override
  public FragmentController<F> create() {
    return create(null);
  }

  @Override
  public FragmentController<F> destroy() {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.destroy();
      }
    });
    return this;
  }

  public FragmentController<F> start() {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.start();
      }
    });
    return this;
  }

  public FragmentController<F> resume() {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.resume();
      }
    });
    return this;
  }

  public FragmentController<F> pause() {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.pause();
      }
    });
    return this;
  }

  public FragmentController<F> visible() {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.visible();
      }
    });
    return this;
  }

  public FragmentController<F> stop() {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.stop();
      }
    });
    return this;
  }

  public FragmentController<F> saveInstanceState(final Bundle outState) {
    shadowMainLooper.runPaused(new Runnable() {
      @Override
      public void run() {
        activityController.saveInstanceState(outState);
      }
    });
    return this;
  }

  public FragmentController<F> recreate() {
    return recreate((F) ReflectionHelpers.callConstructor(fragment.getClass()), 1);
  }

  public FragmentController<F> recreate(final F recreatedFragment, final int contentViewId) {
    ActivityLifecycleCallback fragmentCreateCallback =
        new ActivityLifecycleCallback() {
          @Override
          public void onActivityLifecycleChanged(Activity activity, Stage stage) {
            if (Stage.CREATED.equals(stage)) {
              activity
                  .getFragmentManager()
                  .beginTransaction()
                  .add(contentViewId, recreatedFragment)
                  .commit();
              FragmentController.this.fragment = recreatedFragment;
              FragmentController.this.component = recreatedFragment;
            }
          }
        };
    ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(fragmentCreateCallback);

    shadowMainLooper.runPaused(
        new Runnable() {
          @Override
          public void run() {
            activityController.recreate();
          }
        });

    ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(fragmentCreateCallback);
    return this;
  }

  private static class FragmentControllerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      LinearLayout view = new LinearLayout(this);
      view.setId(1);

      setContentView(view);
    }
  }
}


package org.robolectric.shadows;

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

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.os.Build;
import android.os.Parcel;
import android.util.DisplayMetrics;
import java.io.FileDescriptor;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(Bitmap.class)
public class ShadowBitmap {
  /** Number of bytes used internally to represent each pixel (in the {@link #colors} array) */
  private static final int INTERNAL_BYTES_PER_PIXEL = 4;

  @RealObject
  private Bitmap realBitmap;

  int createdFromResId = -1;
  String createdFromPath;
  InputStream createdFromStream;
  FileDescriptor createdFromFileDescriptor;
  byte[] createdFromBytes;
  private Bitmap createdFromBitmap;
  private int createdFromX = -1;
  private int createdFromY = -1;
  private int createdFromWidth = -1;
  private int createdFromHeight = -1;
  private int[] createdFromColors;
  private Matrix createdFromMatrix;
  private boolean createdFromFilter;
  private boolean hasAlpha;

  private int width;
  private int height;
  private int density;
  private int[] colors;
  private Bitmap.Config config;
  private boolean mutable;
  private String description = "";
  private boolean recycled = false;
  private boolean hasMipMap;
  private boolean isPremultiplied;

  /**
   * Returns a textual representation of the appearance of the object.
   *
   * @param bitmap the bitmap to visualize
   * @return Textual representation of the appearance of the object.
   */
  public static String visualize(Bitmap bitmap) {
    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
    return shadowBitmap.getDescription();
  }

  /**
   * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap
   * was not copied from another instance.
   *
   * @return Original Bitmap from which this Bitmap was created.
   */
  public Bitmap getCreatedFromBitmap() {
    return createdFromBitmap;
  }

  /**
   * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created
   * from a resource.
   *
   * @return Resource ID from which this Bitmap was created.
   */
  public int getCreatedFromResId() {
    return createdFromResId;
  }

  /**
   * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a
   * path.
   *
   * @return Path from which this Bitmap was created.
   */
  public String getCreatedFromPath() {
    return createdFromPath;
  }

  /**
   * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not
   * created from a stream.
   *
   * @return InputStream from which this Bitmap was created.
   */
  public InputStream getCreatedFromStream() {
    return createdFromStream;
  }

  /**
   * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from
   * bytes.
   *
   * @return Bytes from which this Bitmap was created.
   */
  public byte[] getCreatedFromBytes() {
    return createdFromBytes;
  }

  /**
   * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
   *
   * @return Horizontal offset within {@link #getCreatedFromBitmap()}.
   */
  public int getCreatedFromX() {
    return createdFromX;
  }

  /**
   * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
   *
   * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
   */
  public int getCreatedFromY() {
    return createdFromY;
  }

  /**
   * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   *
   * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   */
  public int getCreatedFromWidth() {
    return createdFromWidth;
  }

  /**
   * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
   * content, or -1.
   */
  public int getCreatedFromHeight() {
    return createdFromHeight;
  }

  /**
   * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created
   * from a color array.
   * @return Color array from which this Bitmap was created.
   */
  public int[] getCreatedFromColors() {
    return createdFromColors;
  }

  /**
   * Matrix from which this Bitmap's content was transformed, or {@code null}.
   * @return Matrix from which this Bitmap's content was transformed, or {@code null}.
   */
  public Matrix getCreatedFromMatrix() {
    return createdFromMatrix;
  }

  /**
   * {@code true} if this Bitmap was created with filtering.
   * @return {@code true} if this Bitmap was created with filtering.
   */
  public boolean getCreatedFromFilter() {
    return createdFromFilter;
  }

  @Implementation
  protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
    appendDescription(" compressed as " + format + " with quality " + quality);
    return ImageUtil.writeToStream(realBitmap, format, quality, stream);
  }

  @Implementation
  protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
    return createBitmap((DisplayMetrics) null, width, height, config);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected static Bitmap createBitmap(
      DisplayMetrics displayMetrics,
      int width,
      int height,
      Bitmap.Config config,
      boolean hasAlpha) {
    return createBitmap((DisplayMetrics) null, width, height, config);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected static Bitmap createBitmap(
      DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) {
    if (width <= 0 || height <= 0) {
      throw new IllegalArgumentException("width and height must be > 0");
    }
    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap);
    shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")");

    shadowBitmap.width = width;
    shadowBitmap.height = height;
    shadowBitmap.config = config;
    shadowBitmap.setMutable(true);
    if (displayMetrics != null) {
      shadowBitmap.density = displayMetrics.densityDpi;
    }
    shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, shadowBitmap.getWidth(), 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight());
    return scaledBitmap;
  }

  @Implementation
  protected static Bitmap createBitmap(Bitmap src) {
    ShadowBitmap shadowBitmap = Shadow.extract(src);
    shadowBitmap.appendDescription(" created from Bitmap object");
    return src;
  }

  @Implementation
  protected static Bitmap createScaledBitmap(
      Bitmap src, int dstWidth, int dstHeight, boolean filter) {
    if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) {
      return src; // Return the original.
    }

    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap);

    ShadowBitmap shadowSrcBitmap = Shadow.extract(src);
    shadowBitmap.appendDescription(shadowSrcBitmap.getDescription());
    shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight);
    if (filter) {
      shadowBitmap.appendDescription(" with filter " + filter);
    }

    shadowBitmap.createdFromBitmap = src;
    shadowBitmap.createdFromFilter = filter;
    shadowBitmap.width = dstWidth;
    shadowBitmap.height = dstHeight;
    shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, 0, 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight());
    return scaledBitmap;
  }

  @Implementation
  protected static Bitmap createBitmap(Bitmap src, int x, int y, int width, int height) {
    if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight()) {
      return src; // Return the original.
    }

    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = Shadow.extract(newBitmap);

    ShadowBitmap shadowSrcBitmap = Shadow.extract(src);
    shadowBitmap.appendDescription(shadowSrcBitmap.getDescription());
    shadowBitmap.appendDescription(" at (" + x + "," + y);
    shadowBitmap.appendDescription(" with width " + width + " and height " + height);

    shadowBitmap.createdFromBitmap = src;
    shadowBitmap.createdFromX = x;
    shadowBitmap.createdFromY = y;
    shadowBitmap.createdFromWidth = width;
    shadowBitmap.createdFromHeight = height;
    shadowBitmap.width = width;
    shadowBitmap.height = height;
    return newBitmap;
  }

  @Implementation
  protected void setPixels(
      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
    this.colors = pixels;
  }

  @Implementation
  protected static Bitmap createBitmap(
      Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) {
    if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight() && (matrix == null || matrix.isIdentity())) {
      return src; // Return the original.
    }

    if (x + width > src.getWidth()) {
      throw new IllegalArgumentException("x + width must be <= bitmap.width()");
    }
    if (y + height > src.getHeight()) {
      throw new IllegalArgumentException("y + height must be <= bitmap.height()");
    }

    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowNewBitmap = Shadow.extract(newBitmap);

    ShadowBitmap shadowSrcBitmap = Shadow.extract(src);
    shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription());
    shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")");
    shadowNewBitmap.appendDescription(" with width " + width + " and height " + height);

    shadowNewBitmap.createdFromBitmap = src;
    shadowNewBitmap.createdFromX = x;
    shadowNewBitmap.createdFromY = y;
    shadowNewBitmap.createdFromWidth = width;
    shadowNewBitmap.createdFromHeight = height;
    shadowNewBitmap.createdFromMatrix = matrix;
    shadowNewBitmap.createdFromFilter = filter;

    if (matrix != null) {
      ShadowMatrix shadowMatrix = Shadow.extract(matrix);
      shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription());

      // Adjust width and height by using the matrix.
      RectF mappedRect = new RectF();
      matrix.mapRect(mappedRect, new RectF(0, 0, width, height));
      width = Math.round(mappedRect.width());
      height = Math.round(mappedRect.height());
    }
    if (filter) {
      shadowNewBitmap.appendDescription(" with filter");
    }

    // updated if matrix is non-null
    shadowNewBitmap.width = width;
    shadowNewBitmap.height = height;

    return newBitmap;
  }

  @Implementation
  protected static Bitmap createBitmap(int[] colors, int width, int height, Bitmap.Config config) {
    if (colors.length != width * height) {
      throw new IllegalArgumentException("array length (" + colors.length + ") did not match width * height (" + (width * height) + ")");
    }

    Bitmap newBitmap = Bitmap.createBitmap(width, height, config);
    ShadowBitmap shadowBitmap = Shadow.extract(newBitmap);

    shadowBitmap.setMutable(false);
    shadowBitmap.createdFromColors = colors;
    shadowBitmap.colors = new int[colors.length];
    System.arraycopy(colors, 0, shadowBitmap.colors, 0, colors.length);
    return newBitmap;
  }

  @Implementation
  protected int getPixel(int x, int y) {
    internalCheckPixelAccess(x, y);
    if (colors != null) {
      // Note that getPixel() returns a non-premultiplied ARGB value; if
      // config is RGB_565, our return value will likely be more precise than
      // on a physical device, since it needs to map each color component from
      // 5 or 6 bits to 8 bits.
      return colors[y * getWidth() + x];
    } else {
      return 0;
    }
  }

  @Implementation
  protected void setPixel(int x, int y, int color) {
    if (isRecycled()) {
      throw new IllegalStateException("Can't call setPixel() on a recycled bitmap");
    } else if (!isMutable()) {
      throw new IllegalStateException("Bitmap is immutable");
    }
    internalCheckPixelAccess(x, y);
    if (colors == null) {
      colors = new int[getWidth() * getHeight()];
    }
    colors[y * getWidth() + x] = color;
  }

  /**
   * Note that this method will return a RuntimeException unless: - {@code pixels} has the same
   * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width}
   * and {@code height} height match the current bitmap's dimensions.
   */
  @Implementation
  protected void getPixels(
      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
    if (x != 0 ||
        y != 0 ||
        width != getWidth() ||
        height != getHeight() ||
        pixels.length != colors.length) {
      for (int y0 = 0; y0 < height; y0++) {
        for (int x0 = 0; x0 < width; x0++) {
          pixels[offset + y0 * stride + x0] = colors[(y0 + y) * getWidth() + x0 + x];
        }
      }
    } else {
      System.arraycopy(colors, 0, pixels, 0, colors.length);
    }
  }

  @Implementation
  protected int getRowBytes() {
    return getBytesPerPixel(config) * getWidth();
  }

  @Implementation
  protected int getByteCount() {
    return getRowBytes() * getHeight();
  }

  @Implementation
  protected void recycle() {
    recycled = true;
  }

  @Implementation
  protected final boolean isRecycled() {
    return recycled;
  }

  @Implementation
  protected Bitmap copy(Bitmap.Config config, boolean isMutable) {
    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
    ShadowBitmap shadowBitmap = Shadow.extract(newBitmap);
    shadowBitmap.createdFromBitmap = realBitmap;
    shadowBitmap.config = config;
    shadowBitmap.mutable = isMutable;
    shadowBitmap.height = getHeight();
    shadowBitmap.width = getWidth();
    if (colors != null) {
      shadowBitmap.colors = new int[colors.length];
      System.arraycopy(shadowBitmap.colors, 0, colors, 0, colors.length);
    }
    return newBitmap;
  }

  @Implementation(minSdk = KITKAT)
  protected final int getAllocationByteCount() {
    return getRowBytes() * getHeight();
  }

  @Implementation
  protected final Bitmap.Config getConfig() {
    return config;
  }

  @Implementation(minSdk = KITKAT)
  protected void setConfig(Bitmap.Config config) {
    this.config = config;
  }

  @Implementation
  protected final boolean isMutable() {
    return mutable;
  }

  public void setMutable(boolean mutable) {
    this.mutable = mutable;
  }

  public void appendDescription(String s) {
    description += s;
  }

  public void setDescription(String s) {
    description = s;
  }

  public String getDescription() {
    return description;
  }

  @Implementation
  protected final boolean hasAlpha() {
    return hasAlpha;
  }

  @Implementation
  protected void setHasAlpha(boolean hasAlpha) {
    this.hasAlpha = hasAlpha;
  }

  @Implementation
  protected Bitmap extractAlpha() {
    int[] alphaPixels = new int[colors.length];
    for (int i = 0; i < alphaPixels.length; i++) {
      alphaPixels[i] = Color.alpha(colors[i]);
    }

    return createBitmap(alphaPixels, getWidth(), getHeight(), Bitmap.Config.ALPHA_8);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected final boolean hasMipMap() {
    return hasMipMap;
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected final void setHasMipMap(boolean hasMipMap) {
    this.hasMipMap = hasMipMap;
  }

  @Implementation(minSdk = KITKAT)
  protected void setWidth(int width) {
    this.width = width;
  }

  @Implementation
  protected int getWidth() {
    return width;
  }

  @Implementation(minSdk = KITKAT)
  protected void setHeight(int height) {
    this.height = height;
  }

  @Implementation
  protected int getHeight() {
    return height;
  }

  @Implementation
  protected void setDensity(int density) {
    this.density = density;
  }

  @Implementation
  protected int getDensity() {
    return density;
  }

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

  @Implementation(minSdk = M)
  protected Bitmap createAshmemBitmap() {
    return realBitmap;
  }

  @Implementation
  protected void eraseColor(int color) {
    if (colors != null) {
      Arrays.fill(colors, color);
    }
  }

  @Implementation
  protected void writeToParcel(Parcel p, int flags) {
    p.writeInt(width);
    p.writeInt(height);
    p.writeSerializable(config);
    p.writeIntArray(colors);
  }

  @Implementation
  protected static Bitmap nativeCreateFromParcel(Parcel p) {
    int parceledWidth = p.readInt();
    int parceledHeight = p.readInt();
    Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();

    int[] parceledColors = new int[parceledHeight * parceledWidth];
    p.readIntArray(parceledColors);

    return createBitmap(parceledColors, parceledWidth, parceledHeight, parceledConfig);
  }

  @Implementation
  protected void copyPixelsFromBuffer(Buffer dst) {
    if (isRecycled()) {
      throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap");
    }

    // See the related comment in #copyPixelsToBuffer(Buffer).
    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
      throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL
              + " bytes per pixel are supported");
    }
    if (!(dst instanceof ByteBuffer)) {
      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
    }

    ByteBuffer byteBuffer = (ByteBuffer) dst;
    if (byteBuffer.remaining() < colors.length * INTERNAL_BYTES_PER_PIXEL) {
      throw new RuntimeException("Buffer not large enough for pixels");
    }

    for (int i = 0; i < colors.length; i++) {
      colors[i] = byteBuffer.getInt();
    }
  }

  @Implementation
  protected void copyPixelsToBuffer(Buffer dst) {
    // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels
    // internally. Clients of this API probably expect that the buffer size must be >=
    // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other
    // configs that value would be smaller then the buffer size we actually need.
    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
      throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL
              + " bytes per pixel are supported");
    }

    if (!(dst instanceof ByteBuffer)) {
      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
    }

    ByteBuffer byteBuffer = (ByteBuffer) dst;
    for (int color : colors) {
      byteBuffer.putInt(color);
    }
  }

  @Implementation(minSdk = KITKAT)
  protected void reconfigure(int width, int height, Bitmap.Config config) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) {
      throw new IllegalStateException("native-backed bitmaps may not be reconfigured");
    }

    // This should throw if the resulting allocation size is greater than the initial allocation
    // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to
    // assume that our original dimensions and config are large enough to fit the new dimensions and
    // config
    this.width = width;
    this.height = height;
    this.config = config;
  }

  @Implementation(minSdk = KITKAT)
  protected void setPremultiplied(boolean isPremultiplied) {
    this.isPremultiplied = isPremultiplied;
  }

  @Implementation(minSdk = KITKAT)
  protected boolean isPremultiplied() {
    return isPremultiplied;
  }

  @Implementation
  protected boolean sameAs(Bitmap other) {
    if (other == null) {
      return false;
    }
    ShadowBitmap shadowOtherBitmap = Shadow.extract(other);
    if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) {
      return false;
    }
    if (this.config != null
        && shadowOtherBitmap.config != null
        && this.config != shadowOtherBitmap.config) {
      return false;
    }
    if (!Arrays.equals(colors, shadowOtherBitmap.colors)) {
      return false;
    }
    return true;
  }

  public Bitmap getRealBitmap() {
    return realBitmap;
  }

  public static int getBytesPerPixel(Bitmap.Config config) {
    if (config == null) {
      throw new NullPointerException("Bitmap config was null.");
    }
    switch (config) {
      case RGBA_F16:
        return 8;
      case ARGB_8888:
        return 4;
      case RGB_565:
      case ARGB_4444:
        return 2;
      case ALPHA_8:
        return 1;
      default:
        throw new IllegalArgumentException("Unknown bitmap config: " + config);
    }
  }

  public void setCreatedFromResId(int resId, String description) {
    this.createdFromResId = resId;
    appendDescription(" for resource:" + description);
  }

  private void internalCheckPixelAccess(int x, int y) {
    if (x < 0) {
      throw new IllegalArgumentException("x must be >= 0");
    }
    if (y < 0) {
      throw new IllegalArgumentException("y must be >= 0");
    }
    if (x >= getWidth()) {
      throw new IllegalArgumentException("x must be < bitmap.width()");
    }
    if (y >= getHeight()) {
      throw new IllegalArgumentException("y must be < bitmap.height()");
    }
  }
}


package org.robolectric.shadows;

import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
import static org.robolectric.shadow.api.Shadow.newInstanceOf;
import static org.robolectric.shadows.ShadowLooper.assertLooperMode;

import android.app.ActivityThread;
import android.app.Application;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.widget.ListPopupWindow;
import android.widget.PopupWindow;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.robolectric.RoboSettings;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Scheduler;

@Implements(Application.class)
public class ShadowApplication extends ShadowContextWrapper {
  @RealObject private Application realApplication;

  private Scheduler backgroundScheduler = RoboSettings.isUseGlobalScheduler()
      ? getForegroundThreadScheduler()
      : new Scheduler();
  private List<android.widget.Toast> shownToasts = new ArrayList<>();
  private PowerManager.WakeLock latestWakeLock;
  private ShadowAlertDialog latestAlertDialog;
  private ShadowDialog latestDialog;
  private ShadowPopupMenu latestPopupMenu;
  private Object bluetoothAdapter = newInstanceOf("android.bluetooth.BluetoothAdapter");
  private PopupWindow latestPopupWindow;
  private ListPopupWindow latestListPopupWindow;

  /**
   * @deprecated Use
   *     `shadowOf({@link androidx.test.core.app.ApplicationProvider#getApplicationContext})`
   *     instead.
   */
  @Deprecated
  public static ShadowApplication getInstance() {
    return RuntimeEnvironment.application == null
        ? null
        : Shadow.extract(RuntimeEnvironment.application);
  }

  /**
   * Runs any background tasks previously queued by {@link android.os.AsyncTask#execute(Object[])}.
   *
   * Note: calling this method does not pause or un-pause the scheduler.
   */
  public static void runBackgroundTasks() {
    getInstance().getBackgroundThreadScheduler().advanceBy(0);
  }

  /**
   * Attaches an application to a base context.
   *
   * @param context The context with which to initialize the application, whose base context will
   *                be attached to the application
   */
  public void callAttach(Context context) {
    ReflectionHelpers.callInstanceMethod(Application.class, realApplication, "attach",
        ReflectionHelpers.ClassParameter.from(Context.class, context));
  }

  public List<Toast> getShownToasts() {
    return shownToasts;
  }

  /**
   * Return the foreground scheduler.
   *
   * @return  Foreground scheduler.
   */
  public Scheduler getForegroundThreadScheduler() {
    return RuntimeEnvironment.getMasterScheduler();
  }

  /**
   * Return the background scheduler.
   *
   * @return  Background scheduler.
   */
  public Scheduler getBackgroundThreadScheduler() {
    assertLooperMode(LEGACY);
    return backgroundScheduler;
  }

  public void setComponentNameAndServiceForBindService(ComponentName name, IBinder service) {
    getShadowInstrumentation().setComponentNameAndServiceForBindService(name, service);
  }

  public void setComponentNameAndServiceForBindServiceForIntent(
      Intent intent, ComponentName name, IBinder service) {
    getShadowInstrumentation()
        .setComponentNameAndServiceForBindServiceForIntent(intent, name, service);
  }

  public void assertNoBroadcastListenersOfActionRegistered(ContextWrapper context, String action) {
    getShadowInstrumentation().assertNoBroadcastListenersOfActionRegistered(context, action);
  }

  public List<ServiceConnection> getBoundServiceConnections() {
    return getShadowInstrumentation().getBoundServiceConnections();
  }

  public void setUnbindServiceShouldThrowIllegalArgument(boolean flag) {
    getShadowInstrumentation().setUnbindServiceShouldThrowIllegalArgument(flag);
  }

  public List<ServiceConnection> getUnboundServiceConnections() {
    return getShadowInstrumentation().getUnboundServiceConnections();
  }

  /** @deprecated use PackageManager.queryBroadcastReceivers instead */
  @Deprecated
  public boolean hasReceiverForIntent(Intent intent) {
    return getShadowInstrumentation().hasReceiverForIntent(intent);
  }

  /** @deprecated use PackageManager.queryBroadcastReceivers instead */
  @Deprecated
  public List<BroadcastReceiver> getReceiversForIntent(Intent intent) {
    return getShadowInstrumentation().getReceiversForIntent(intent);
  }

  /**
   * @return list of {@link Wrapper}s for registered receivers
   */
  public List<Wrapper> getRegisteredReceivers() {
    return getShadowInstrumentation().getRegisteredReceivers();
  }

  /** Removes all registered receivers. */
  public void clearRegisteredReceivers() {
    getShadowInstrumentation().clearRegisteredReceivers();
  }

  /**
   * @deprecated Please use {@link Context#getSystemService(Context.APPWIDGET_SERVICE)} intstead.
   */
  @Deprecated
  public AppWidgetManager getAppWidgetManager() {
    return (AppWidgetManager) realApplication.getSystemService(Context.APPWIDGET_SERVICE);
  }

  /**
   * @deprecated Use {@link ShadowAlertDialog#getLatestAlertDialog()} instead.
   */
  @Deprecated
  public ShadowAlertDialog getLatestAlertDialog() {
    return latestAlertDialog;
  }

  protected void setLatestAlertDialog(ShadowAlertDialog latestAlertDialog) {
    this.latestAlertDialog = latestAlertDialog;
  }

  /**
   * @deprecated Use {@link ShadowDialog#getLatestDialog()} instead.
   */
  @Deprecated
  public ShadowDialog getLatestDialog() {
    return latestDialog;
  }

  protected void setLatestDialog(ShadowDialog latestDialog) {
    this.latestDialog = latestDialog;
  }

  public Object getBluetoothAdapter() {
    return bluetoothAdapter;
  }

  public void declareActionUnbindable(String action) {
    getShadowInstrumentation().declareActionUnbindable(action);
  }

  public PowerManager.WakeLock getLatestWakeLock() {
    return latestWakeLock;
  }

  public void addWakeLock( PowerManager.WakeLock wl ) {
    latestWakeLock = wl;
  }

  public void clearWakeLocks() {
    latestWakeLock = null;
  }

  private final Map<String, Object> singletons = new HashMap<>();

  public <T> T getSingleton(Class<T> clazz, Provider<T> provider) {
    synchronized (singletons) {
      //noinspection unchecked
      T item = (T) singletons.get(clazz.getName());
      if (item == null) {
        singletons.put(clazz.getName(), item = provider.get());
      }
      return item;
    }
  }

  /**
   * Set to true if you'd like Robolectric to strictly simulate the real Android behavior when
   * calling {@link Context#startActivity(android.content.Intent)}. Real Android throws a
   * {@link android.content.ActivityNotFoundException} if given
   * an {@link Intent} that is not known to the {@link android.content.pm.PackageManager}
   *
   * By default, this behavior is off (false).
   *
   * @param checkActivities True to validate activities.
   */
  public void checkActivities(boolean checkActivities) {
    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    ShadowInstrumentation shadowInstrumentation =
        Shadow.extract(activityThread.getInstrumentation());
    shadowInstrumentation.checkActivities(checkActivities);
  }

  /**
   * @deprecated Use {@link ShadowPopupMenu#getLatestPopupMenu()} instead.
   */
  @Deprecated
  public ShadowPopupMenu getLatestPopupMenu() {
    return latestPopupMenu;
  }

  protected void setLatestPopupMenu(ShadowPopupMenu latestPopupMenu) {
    this.latestPopupMenu = latestPopupMenu;
  }

  public PopupWindow getLatestPopupWindow() {
    return latestPopupWindow;
  }

  protected void setLatestPopupWindow(PopupWindow latestPopupWindow) {
    this.latestPopupWindow = latestPopupWindow;
  }

  public ListPopupWindow getLatestListPopupWindow() {
    return latestListPopupWindow;
  }

  protected void setLatestListPopupWindow(ListPopupWindow latestListPopupWindow) {
    this.latestListPopupWindow = latestListPopupWindow;
  }

  public static class Wrapper {
    public BroadcastReceiver broadcastReceiver;
    public IntentFilter intentFilter;
    public Context context;
    public Throwable exception;
    public String broadcastPermission;
    public Handler scheduler;

    public Wrapper(
        BroadcastReceiver broadcastReceiver,
        IntentFilter intentFilter,
        Context context,
        String broadcastPermission,
        Handler scheduler) {
      this.broadcastReceiver = broadcastReceiver;
      this.intentFilter = intentFilter;
      this.context = context;
      this.broadcastPermission = broadcastPermission;
      this.scheduler = scheduler;
      exception = new Throwable();
    }

    public BroadcastReceiver getBroadcastReceiver() {
      return broadcastReceiver;
    }

    public IntentFilter getIntentFilter() {
      return intentFilter;
    }

    public Context getContext() {
      return context;
    }
  }

  /**
   * @deprecated Do not depend on this method to override services as it will be removed in a future
   * update. The preferered method is use the shadow of the corresponding service.
   */
  @Deprecated
  public void setSystemService(String key, Object service) {
    ShadowContextImpl shadowContext = Shadow.extract(realApplication.getBaseContext());
    shadowContext.setSystemService(key, service);
  }

}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;
import static org.robolectric.RuntimeEnvironment.getApiLevel;

import android.net.ConnectivityManager;
import android.net.ConnectivityManager.OnNetworkActiveListener;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.os.Handler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;

@Implements(ConnectivityManager.class)
public class ShadowConnectivityManager {

  // Package-private for tests.
  static final int NET_ID_WIFI = ConnectivityManager.TYPE_WIFI;
  static final int NET_ID_MOBILE = ConnectivityManager.TYPE_MOBILE;

  private NetworkInfo activeNetworkInfo;
  private boolean backgroundDataSetting;
  private int networkPreference = ConnectivityManager.DEFAULT_NETWORK_PREFERENCE;
  private final Map<Integer, NetworkInfo> networkTypeToNetworkInfo = new HashMap<>();

  private HashSet<ConnectivityManager.NetworkCallback> networkCallbacks = new HashSet<>();
  private final Map<Integer, Network> netIdToNetwork = new HashMap<>();
  private final Map<Integer, NetworkInfo> netIdToNetworkInfo = new HashMap<>();
  private Network processBoundNetwork;
  private boolean defaultNetworkActive;
  private HashSet<ConnectivityManager.OnNetworkActiveListener> onNetworkActiveListeners =
      new HashSet<>();
  private Map<Network, Boolean> reportedNetworkConnectivity = new HashMap<>();
  private Map<Network, NetworkCapabilities> networkCapabilitiesMap = new HashMap<>();
  private String captivePortalServerUrl = "http://10.0.0.2";
  private final Map<Network, LinkProperties> linkPropertiesMap = new HashMap<>();

  public ShadowConnectivityManager() {
    NetworkInfo wifi = ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.DISCONNECTED,
        ConnectivityManager.TYPE_WIFI, 0, true, false);
    networkTypeToNetworkInfo.put(ConnectivityManager.TYPE_WIFI, wifi);

    NetworkInfo mobile = ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED,
        ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_MOBILE_MMS, true, true);
    networkTypeToNetworkInfo.put(ConnectivityManager.TYPE_MOBILE, mobile);

    this.activeNetworkInfo = mobile;

    if (getApiLevel() >= LOLLIPOP) {
      netIdToNetwork.put(NET_ID_WIFI, ShadowNetwork.newInstance(NET_ID_WIFI));
      netIdToNetwork.put(NET_ID_MOBILE, ShadowNetwork.newInstance(NET_ID_MOBILE));
      netIdToNetworkInfo.put(NET_ID_WIFI, wifi);
      netIdToNetworkInfo.put(NET_ID_MOBILE, mobile);
    }
    defaultNetworkActive = true;
  }

  public Set<ConnectivityManager.NetworkCallback> getNetworkCallbacks() {
    return networkCallbacks;
  }

  /**
   * @return networks and their connectivity status which was reported with {@link
   *     #reportNetworkConnectivity}.
   */
  public Map<Network, Boolean> getReportedNetworkConnectivity() {
    return new HashMap<>(reportedNetworkConnectivity);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void registerNetworkCallback(
      NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
    registerNetworkCallback(request, networkCallback, null);
  }

  @Implementation(minSdk = O)
  protected void registerNetworkCallback(
      NetworkRequest request,
      ConnectivityManager.NetworkCallback networkCallback,
      Handler handler) {
    networkCallbacks.add(networkCallback);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void requestNetwork(
      NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
    registerNetworkCallback(request, networkCallback);
  }

  @Implementation(minSdk = N)
  protected void registerDefaultNetworkCallback(
      ConnectivityManager.NetworkCallback networkCallback) {
    networkCallbacks.add(networkCallback);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void unregisterNetworkCallback(ConnectivityManager.NetworkCallback networkCallback) {
    if (networkCallback == null) {
      throw new IllegalArgumentException("Invalid NetworkCallback");
    }
    if (networkCallbacks.contains(networkCallback)) {
      networkCallbacks.remove(networkCallback);
    }
  }

  @Implementation
  protected NetworkInfo getActiveNetworkInfo() {
    return activeNetworkInfo;
  }

  /**
   * @see #setActiveNetworkInfo(NetworkInfo)
   * @see #setNetworkInfo(int, NetworkInfo)
   */
  @Implementation(minSdk = M)
  protected Network getActiveNetwork() {
    if (defaultNetworkActive) {
      return netIdToNetwork.get(getActiveNetworkInfo().getType());
    }
    return null;
  }

  /**
   * @see #setActiveNetworkInfo(NetworkInfo)
   * @see #setNetworkInfo(int, NetworkInfo)
   */
  @Implementation
  protected NetworkInfo[] getAllNetworkInfo() {
    // todo(xian): is `defaultNetworkActive` really relevant here?
    if (defaultNetworkActive) {
      return networkTypeToNetworkInfo
          .values()
          .toArray(new NetworkInfo[networkTypeToNetworkInfo.size()]);
    }
    return null;
  }

  @Implementation
  protected NetworkInfo getNetworkInfo(int networkType) {
    return networkTypeToNetworkInfo.get(networkType);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected NetworkInfo getNetworkInfo(Network network) {
    if (network == null) {
      return null;
    }
    ShadowNetwork shadowNetwork = Shadow.extract(network);
    return netIdToNetworkInfo.get(shadowNetwork.getNetId());
  }

  @Implementation(minSdk = LOLLIPOP)
  protected Network[] getAllNetworks() {
    return netIdToNetwork.values().toArray(new Network[netIdToNetwork.size()]);
  }

  @Implementation
  protected boolean getBackgroundDataSetting() {
    return backgroundDataSetting;
  }

  @Implementation
  protected void setNetworkPreference(int preference) {
    networkPreference = preference;
  }

  @Implementation
  protected int getNetworkPreference() {
    return networkPreference;
  }

  /**
   * Counts {@link ConnectivityManager#TYPE_MOBILE} networks as metered. Other types will be
   * considered unmetered.
   *
   * @return `true` if the active network is metered, otherwise `false`.
   * @see #setActiveNetworkInfo(NetworkInfo)
   * @see #setDefaultNetworkActive(boolean)
   */
  @Implementation
  protected boolean isActiveNetworkMetered() {
    if (defaultNetworkActive && activeNetworkInfo != null) {
      return activeNetworkInfo.getType() == ConnectivityManager.TYPE_MOBILE;
    } else {
      return false;
    }
  }

  @Implementation(minSdk = M)
  protected boolean bindProcessToNetwork(Network network) {
    processBoundNetwork = network;
    return true;
  }

  @Implementation(minSdk = M)
  protected Network getBoundNetworkForProcess() {
    return processBoundNetwork;
  }

  public void setNetworkInfo(int networkType, NetworkInfo networkInfo) {
    networkTypeToNetworkInfo.put(networkType, networkInfo);
  }

  /**
   * Returns the captive portal URL previously set with {@link #setCaptivePortalServerUrl}.
   */
  @Implementation(minSdk = N)
  protected String getCaptivePortalServerUrl() {
    return captivePortalServerUrl;
  }

  /**
   * Sets the captive portal URL, which will be returned in {@link #getCaptivePortalServerUrl}.
   *
   * @param captivePortalServerUrl the url of captive portal.
   */
  public void setCaptivePortalServerUrl(String captivePortalServerUrl) {
    this.captivePortalServerUrl = captivePortalServerUrl;
  }

  @HiddenApi @Implementation
  public void setBackgroundDataSetting(boolean b) {
    backgroundDataSetting = b;
  }

  public void setActiveNetworkInfo(NetworkInfo info) {
    if (getApiLevel() >= LOLLIPOP) {
      activeNetworkInfo = info;
      if (info != null) {
        networkTypeToNetworkInfo.put(info.getType(), info);
        netIdToNetwork.put(info.getType(), ShadowNetwork.newInstance(info.getType()));
        netIdToNetworkInfo.put(info.getType(), info);
      } else {
        networkTypeToNetworkInfo.clear();
        netIdToNetwork.clear();
      }
    } else {
      activeNetworkInfo = info;
      if (info != null) {
        networkTypeToNetworkInfo.put(info.getType(), info);
      } else {
        networkTypeToNetworkInfo.clear();
      }
    }
  }

  /**
   * Adds new {@code network} to the list of all {@link android.net.Network}s.
   *
   * @param network The network.
   * @param networkInfo The network info paired with the {@link android.net.Network}.
   */
  public void addNetwork(Network network, NetworkInfo networkInfo) {
    ShadowNetwork shadowNetwork = Shadow.extract(network);
    int netId = shadowNetwork.getNetId();
    netIdToNetwork.put(netId, network);
    netIdToNetworkInfo.put(netId, networkInfo);
  }

  /**
   * Removes the {@code network} from the list of all {@link android.net.Network}s.
   * @param network The network.
   */
  public void removeNetwork(Network network) {
    ShadowNetwork shadowNetwork = Shadow.extract(network);
    int netId = shadowNetwork.getNetId();
    netIdToNetwork.remove(netId);
    netIdToNetworkInfo.remove(netId);
  }

  /**
   * Clears the list of all {@link android.net.Network}s.
   */
  public void clearAllNetworks() {
    netIdToNetwork.clear();
    netIdToNetworkInfo.clear();
  }

  /**
   * Sets the active state of the default network.
   *
   * By default this is true and affects the result of {@link
   * ConnectivityManager#isActiveNetworkMetered()}, {@link
   * ConnectivityManager#isDefaultNetworkActive()}, {@link ConnectivityManager#getActiveNetwork()}
   * and {@link ConnectivityManager#getAllNetworkInfo()}.
   *
   * Calling this method with {@code true} after any listeners have been registered with {@link
   * ConnectivityManager#addDefaultNetworkActiveListener(OnNetworkActiveListener)} will result in
   * those listeners being fired.
   *
   * @param isActive The active state of the default network.
   */
  public void setDefaultNetworkActive(boolean isActive) {
    defaultNetworkActive = isActive;
    if (defaultNetworkActive) {
      for (ConnectivityManager.OnNetworkActiveListener l : onNetworkActiveListeners) {
        if (l != null) {
          l.onNetworkActive();
        }
      }
    }
  }

  /**
   * @return `true` by default, or the value specifed via {@link #setDefaultNetworkActive(boolean)}
   * @see #setDefaultNetworkActive(boolean)
   */
  @Implementation(minSdk = LOLLIPOP)
  protected boolean isDefaultNetworkActive() {
    return defaultNetworkActive;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void addDefaultNetworkActiveListener(final ConnectivityManager.OnNetworkActiveListener l) {
    onNetworkActiveListeners.add(l);
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void removeDefaultNetworkActiveListener(ConnectivityManager.OnNetworkActiveListener l) {
    if (l == null) {
      throw new IllegalArgumentException("Invalid OnNetworkActiveListener");
    }
    if (onNetworkActiveListeners.contains(l)) {
      onNetworkActiveListeners.remove(l);
    }
  }

  @Implementation(minSdk = M)
  protected void reportNetworkConnectivity(Network network, boolean hasConnectivity) {
    reportedNetworkConnectivity.put(network, hasConnectivity);
  }

  /**
   * Gets the network capabilities of a given {@link Network}.
   *
   * @param network The {@link Network} object identifying the network in question.
   * @return The {@link android.net.NetworkCapabilities} for the network.
   * @see #setNetworkCapabilities(Network, NetworkCapabilities)
   */
  @Implementation(minSdk = LOLLIPOP)
  protected NetworkCapabilities getNetworkCapabilities(Network network) {
    return networkCapabilitiesMap.get(network);
  }

  /**
   * Sets network capability and affects the result of {@link
   * ConnectivityManager#getNetworkCapabilities(Network)}
   *
   * @param network The {@link Network} object identifying the network in question.
   * @param networkCapabilities The {@link android.net.NetworkCapabilities} for the network.
   */
  public void setNetworkCapabilities(Network network, NetworkCapabilities networkCapabilities) {
    networkCapabilitiesMap.put(network, networkCapabilities);
  }

  /**
   * Sets the value for enabling/disabling airplane mode
   *
   * @param enable new status for airplane mode
   */
  @Implementation(minSdk = KITKAT)
  protected void setAirplaneMode(boolean enable) {
    ShadowSettings.setAirplaneMode(enable);
  }

  /** @see #setLinkProperties(Network, LinkProperties) */
  @Implementation(minSdk = LOLLIPOP)
  protected LinkProperties getLinkProperties(Network network) {
    return linkPropertiesMap.get(network);
  }

  /**
   * Sets the LinkProperties for the given Network.
   *
   * <p>A LinkProperties can be constructed by
   * `org.robolectric.util.ReflectionHelpers.callConstructor` in tests.
   */
  public void setLinkProperties(Network network, LinkProperties linkProperties) {
    linkPropertiesMap.put(network, linkProperties);
  }
}


package org.robolectric.shadows;

import android.content.res.Resources;
import android.util.TypedValue;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.robolectric.res.AttrData;
import org.robolectric.res.ResType;
import org.robolectric.res.TypedResource;
import org.robolectric.util.Util;

@SuppressWarnings("NewApi")
public class Converter<T> {
  private static int nextStringCookie = 0xbaaa5;

  synchronized static int getNextStringCookie() {
    return nextStringCookie++;
  }

  static Converter getConverterFor(AttrData attrData, String type) {
    switch (type) {
      case "enum":
        return new EnumConverter(attrData);
      case "flag":
      case "flags": // because {@link ResourceTable#gFormatFlags} uses "flags"
        return new FlagConverter(attrData);
      case "boolean":
        return new FromBoolean();
      case "color":
        return new FromColor();
      case "dimension":
        return new FromDimen();
      case "float":
        return new FromFloat();
      case "integer":
        return new FromInt();
      case "string":
        return new FromCharSequence();
      case "fraction":
        return new FromFraction();
      default:
        throw new UnsupportedOperationException("Type not supported: " + type);
    }
  }

  // TODO: Handle 'anim' resources
  public static Converter getConverter(ResType resType) {
    switch (resType) {
      case ATTR_DATA:
        return new FromAttrData();
      case BOOLEAN:
        return new FromBoolean();
      case CHAR_SEQUENCE:
        return new FromCharSequence();
      case COLOR:
      case DRAWABLE:
        return new FromColor();
      case COLOR_STATE_LIST:
      case LAYOUT:
        return new FromFilePath();
      case DIMEN:
        return new FromDimen();
      case FILE:
        return new FromFile();
      case FLOAT:
        return new FromFloat();
      case INTEGER:
        return new FromInt();
      case FRACTION:
        return new FromFraction();
      case CHAR_SEQUENCE_ARRAY:
      case INTEGER_ARRAY:
      case TYPED_ARRAY:
        return new FromArray();
      case STYLE:
        return new Converter();
      default:
        throw new UnsupportedOperationException("can't convert from " + resType.name());
    }
  }

  public CharSequence asCharSequence(TypedResource typedResource) {
    return typedResource.asString();
  }

  public int asInt(TypedResource typedResource) {
    throw cantDo("asInt");
  }

  public List<TypedResource> getItems(TypedResource typedResource) {
    return new ArrayList<>();
  }

  public boolean fillTypedValue(T data, TypedValue typedValue) {
    return false;
  }

  private UnsupportedOperationException cantDo(String operation) {
    return new UnsupportedOperationException(getClass().getName() + " doesn't support " + operation);
  }

  public static class FromAttrData extends Converter<AttrData> {
    @Override
    public CharSequence asCharSequence(TypedResource typedResource) {
      return typedResource.asString();
    }

    @Override
    public boolean fillTypedValue(AttrData data, TypedValue typedValue) {
      typedValue.type = TypedValue.TYPE_STRING;
      return false;
    }
  }

  public static class FromCharSequence extends Converter<String> {
    @Override
    public CharSequence asCharSequence(TypedResource typedResource) {
      return typedResource.asString().trim();
    }

    @Override
    public int asInt(TypedResource typedResource) {
      return convertInt(typedResource.asString().trim());
    }

    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      typedValue.type = TypedValue.TYPE_STRING;
      typedValue.data = 0;
      typedValue.assetCookie = getNextStringCookie();
      typedValue.string = data;
      return true;
    }
  }

  public static class FromColor extends Converter<String> {
    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      try {
        typedValue.type =  ResourceHelper.getColorType(data);
        typedValue.data = ResourceHelper.getColor(data);
        typedValue.assetCookie = 0;
        typedValue.string = null;
        return true;
      } catch (NumberFormatException nfe) {
        return false;
      }
    }

    @Override
    public int asInt(TypedResource typedResource) {
      return ResourceHelper.getColor(typedResource.asString().trim());
    }
  }

  public static class FromFilePath extends Converter<String> {
    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      typedValue.type = TypedValue.TYPE_STRING;
      typedValue.data = 0;
      typedValue.string = data;
      typedValue.assetCookie = getNextStringCookie();
      return true;
    }
  }

  public static class FromArray extends Converter {
    @Override
    public List<TypedResource> getItems(TypedResource typedResource) {
      return (List<TypedResource>) typedResource.getData();
    }
  }

  private static class FromInt extends Converter<String> {
    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      try {
        if (data.startsWith("0x")) {
          typedValue.type = data.startsWith("0x") ? TypedValue.TYPE_INT_HEX : TypedValue.TYPE_INT_DEC;
        } else {
          typedValue.type = TypedValue.TYPE_INT_DEC;
        }
        typedValue.data = convertInt(data);
        typedValue.assetCookie = 0;
        typedValue.string = null;
        return true;
      } catch (NumberFormatException nfe) {
        return false;
      }
    }

    @Override
    public int asInt(TypedResource typedResource) {
      return convertInt(typedResource.asString().trim());
    }
  }

  private static class FromFraction extends Converter<String> {
    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      return ResourceHelper.parseFloatAttribute(null, data, typedValue, false);
    }
  }

  private static class FromFile extends Converter<Object> {
    @Override
    public boolean fillTypedValue(Object data, TypedValue typedValue) {
      typedValue.type = TypedValue.TYPE_STRING;
      typedValue.data = 0;
      typedValue.string = data instanceof Path ? data.toString() : (CharSequence) data;
      typedValue.assetCookie = getNextStringCookie();
      return true;
    }
  }

  private static class FromFloat extends Converter<String> {
    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      return ResourceHelper.parseFloatAttribute(null, data, typedValue, false);
    }
  }

  private static class FromBoolean extends Converter<String> {
    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      typedValue.type = TypedValue.TYPE_INT_BOOLEAN;
      typedValue.assetCookie = 0;
      typedValue.string = null;

      if ("true".equalsIgnoreCase(data)) {
        typedValue.data = 1;
        return true;
      } else if ("false".equalsIgnoreCase(data)) {
        typedValue.data = 0;
        return true;
      }
      return false;
    }
  }

  private static class FromDimen extends Converter<String> {
    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      return ResourceHelper.parseFloatAttribute(null, data, typedValue, false);
    }
  }

  private static int convertInt(String rawValue) {
    try {
      // Decode into long, because there are some large hex values in the android resource files
      // (e.g. config_notificationsBatteryLowARGB = 0xFFFF0000 in sdk 14).
      // Integer.decode() does not support large, i.e. negative values in hex numbers.
      // try parsing decimal number
      return (int) Long.parseLong(rawValue);
    } catch (NumberFormatException nfe) {
      // try parsing hex number
      return Long.decode(rawValue).intValue();
    }
  }

  private static class EnumConverter extends EnumOrFlagConverter {
    public EnumConverter(AttrData attrData) {
      super(attrData);
    }

    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      try {
        typedValue.type = TypedValue.TYPE_INT_HEX;
        try {
          typedValue.data = findValueFor(data);
        } catch (Resources.NotFoundException e) {
          typedValue.data = convertInt(data);
        }
        typedValue.assetCookie = 0;
        typedValue.string = null;
        return true;
      } catch (Exception e) {
        return false;
      }
    }
  }

  private static class FlagConverter extends EnumOrFlagConverter {
    public FlagConverter(AttrData attrData) {
      super(attrData);
    }

    @Override
    public boolean fillTypedValue(String data, TypedValue typedValue) {
      int flags = 0;

      try {
        for (String key : data.split("\\|", 0)) {
          flags |= findValueFor(key);
        }
      } catch (Resources.NotFoundException e) {
        try {
          flags = Integer.decode(data);
        } catch (NumberFormatException e1) {
          return false;
        }
      } catch (Exception e) {
        return false;
      }

      typedValue.type = TypedValue.TYPE_INT_HEX;
      typedValue.data = flags;
      typedValue.assetCookie = 0;
      typedValue.string = null;
      return true;
    }
  }

  private static class EnumOrFlagConverter extends Converter<String> {
    private final AttrData attrData;

    public EnumOrFlagConverter(AttrData attrData) {
      this.attrData = attrData;
    }

    protected int findValueFor(String key) {
      key = (key == null) ? null : key.trim();
      String valueFor = attrData.getValueFor(key);
      if (valueFor == null) {
        // Maybe they have passed in the value directly, rather than the name.
        if (attrData.isValue(key)) {
          valueFor = key;
        } else {
          throw new Resources.NotFoundException("no value found for " + key);
        }
      }
      return Util.parseInt(valueFor);
    }
  }
}


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.N;
import static android.os.Build.VERSION_CODES.N_MR1;

import android.content.Context;
import android.media.IAudioService;
import android.media.SoundPool;
import android.media.SoundPool.OnLoadCompleteListener;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;

@Implements(SoundPool.class)
public class ShadowSoundPool {
  @RealObject SoundPool realObject;

  /** Generates sound ids when they are loaded. */
  private final AtomicInteger soundIds = new AtomicInteger();

  /** Tracks mapping between sound id and the paths they refer too. */
  private final SparseArray<String> idToPaths = new SparseArray<>();

  /** Tracks mapping between sound ids and the resource id they refer too. */
  private final SparseIntArray idToRes = new SparseIntArray();

  private final List<Playback> playedSounds = new ArrayList<>();

  private OnLoadCompleteListener listener;

  @Implementation(minSdk = N, maxSdk = N_MR1)
  protected static IAudioService getService() {
    return ReflectionHelpers.createNullProxy(IAudioService.class);
  }

  // Pre api 23, the SoundPool holds an internal delegate rather than directly been used itself.
  // Because of this it's necessary to override the public method, rather than the internal
  // native method.
  @Implementation(maxSdk = LOLLIPOP_MR1)
  protected int play(
      int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate) {
    playedSounds.add(new Playback(soundID, leftVolume, rightVolume, priority, loop, rate));
    return 1;
  }

  @Implementation(minSdk = M)
  protected int _play(
      int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate) {
    playedSounds.add(new Playback(soundID, leftVolume, rightVolume, priority, loop, rate));
    return 1;
  }

  // It's not possible to override the native _load method as that would only give access to a
  // FileDescriptor which would make it difficult to check if a given sound has been placed.
  @Implementation
  protected int load(String path, int priority) {
    int soundId = soundIds.getAndIncrement();
    idToPaths.put(soundId, path);
    return soundId;
  }

  @Implementation
  protected int load(Context context, int resId, int priority) {
    int soundId = soundIds.getAndIncrement();
    idToRes.put(soundId, resId);
    return soundId;
  }

  @Implementation
  protected void setOnLoadCompleteListener(OnLoadCompleteListener listener) {
    this.listener = listener;
  }

  /** Notify the {@link OnLoadCompleteListener}, if present, that the given path was loaded. */
  public void notifyPathLoaded(String path, boolean success) {
    boolean soundIsKnown = false;
    for (int pathIdx = 0; pathIdx < idToPaths.size(); ++pathIdx) {
      if (idToPaths.valueAt(pathIdx).equals(path)) {
        if (listener != null) {
          listener.onLoadComplete(realObject, idToPaths.keyAt(pathIdx), success ? 0 : 1);
        }
        soundIsKnown = true;
      }
    }
    if (!soundIsKnown) {
      throw new IllegalArgumentException("Unknown sound. You need to call load() first");
    }
  }

  /** Notify the {@link OnLoadCompleteListener}, if present, that the given resource was loaded. */
  public void notifyResourceLoaded(int resId, boolean success) {
    boolean soundIsKnown = false;
    for (int resIdx = 0; resIdx < idToRes.size(); ++resIdx) {
      if (idToRes.valueAt(resIdx) == resId) {
        if (listener != null) {
          listener.onLoadComplete(realObject, idToRes.keyAt(resIdx), success ? 0 : 1);
        }
        soundIsKnown = true;
      }
    }
    if (!soundIsKnown) {
      throw new IllegalArgumentException("Unknown sound. You need to call load() first");
    }
  }

  /** Returns {@code true} if the given path was played. */
  public boolean wasPathPlayed(String path) {
    for (Playback playback : playedSounds) {
      if (idIsForPath(playback.soundId, path)) {
        return true;
      }
    }
    return false;
  }

  /** Returns {@code true} if the given resource was played. */
  public boolean wasResourcePlayed(int resId) {
    for (Playback playback : playedSounds) {
      if (idIsForResource(playback.soundId, resId)) {
        return true;
      }
    }
    return false;
  }

  /** Return a list of calls to {@code play} made for the given path. */
  public List<Playback> getPathPlaybacks(String path) {
    ImmutableList.Builder<Playback> playbacks = ImmutableList.builder();
    for (Playback playback : playedSounds) {
      if (idIsForPath(playback.soundId, path)) {
        playbacks.add(playback);
      }
    }
    return playbacks.build();
  }

  /** Return a list of calls to {@code play} made for the given resource. */
  public List<Playback> getResourcePlaybacks(int resId) {
    ImmutableList.Builder<Playback> playbacks = ImmutableList.builder();
    for (Playback playback : playedSounds) {
      if (idIsForResource(playback.soundId, resId)) {
        playbacks.add(playback);
      }
    }
    return playbacks.build();
  }

  private boolean idIsForPath(int soundId, String path) {
    return idToPaths.indexOfKey(soundId) >= 0 && idToPaths.get(soundId).equals(path);
  }

  private boolean idIsForResource(int soundId, int resId) {
    return idToRes.indexOfKey(soundId) >= 0 && idToRes.get(soundId) == resId;
  }

  /** Clears the sounds played by this SoundPool. */
  public void clearPlayed() {
    playedSounds.clear();
  }

  /** Record of a single call to {@link SoundPool#play }. */
  public static final class Playback {
    public final int soundId;
    public final float leftVolume;
    public final float rightVolume;
    public final int priority;
    public final int loop;
    public final float rate;

    public Playback(
        int soundId, float leftVolume, float rightVolume, int priority, int loop, float rate) {
      this.soundId = soundId;
      this.leftVolume = leftVolume;
      this.rightVolume = rightVolume;
      this.priority = priority;
      this.loop = loop;
      this.rate = rate;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof Playback)) {
        return false;
      }
      Playback that = (Playback) o;
      return this.soundId == that.soundId
          && this.leftVolume == that.leftVolume
          && this.rightVolume == that.rightVolume
          && this.priority == that.priority
          && this.loop == that.loop
          && this.rate == that.rate;
    }

    @Override
    public int hashCode() {
      return Objects.hash(soundId, leftVolume, rightVolume, priority, loop, rate);
    }

    @Override
    public String toString() {
      return Arrays.asList(soundId, leftVolume, rightVolume, priority, loop, rate).toString();
    }
  }
}


package org.robolectric.shadows;

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

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RemoteViews;
import com.android.internal.appwidget.IAppWidgetService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(AppWidgetManager.class)
public class ShadowAppWidgetManager {

  @RealObject
  private AppWidgetManager realAppWidgetManager;

  private Context context;
  private final Map<Integer, WidgetInfo> widgetInfos = new HashMap<>();
  private int nextWidgetId = 1;
  private boolean alwaysRecreateViewsDuringUpdate = false;
  private boolean allowedToBindWidgets;
  private boolean validWidgetProviderComponentName = true;
  private final ArrayList<AppWidgetProviderInfo> installedProviders = new ArrayList<>();

  @Implementation(maxSdk = KITKAT)
  protected void __constructor__(Context context) {
    this.context = context;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected void __constructor__(Context context, IAppWidgetService service) {
    this.context = context;
  }

  @Implementation
  protected void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
    for (int appWidgetId : appWidgetIds) {
      updateAppWidget(appWidgetId, views);
    }
  }

  /**
   * Simulates updating an {@code AppWidget} with a new set of views
   *
   * @param appWidgetId id of widget
   * @param views views to update
   */
  @Implementation
  protected void updateAppWidget(int appWidgetId, RemoteViews views) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    int layoutId = views.getLayoutId();
    if (widgetInfo.layoutId != layoutId || alwaysRecreateViewsDuringUpdate) {
      widgetInfo.view = createWidgetView(layoutId);
      widgetInfo.layoutId = layoutId;
    }
    widgetInfo.lastRemoteViews = views;
    views.reapply(context, widgetInfo.view);
  }

  @Implementation
  protected int[] getAppWidgetIds(ComponentName provider) {
    List<Integer> idList = new ArrayList<>();
    for (int id : widgetInfos.keySet()) {
      WidgetInfo widgetInfo = widgetInfos.get(id);
      if (provider.equals(widgetInfo.providerComponent)) {
        idList.add(id);
      }
    }
    int ids[] = new int[idList.size()];
    for (int i = 0; i < idList.size(); i++) {
      ids[i] = idList.get(i);
    }
    return ids;
  }

  @Implementation
  protected List<AppWidgetProviderInfo> getInstalledProviders() {
    return new ArrayList<>(installedProviders);
  }

  public void addInstalledProvider(AppWidgetProviderInfo appWidgetProviderInfo) {
    installedProviders.add(appWidgetProviderInfo);
  }

  public void addBoundWidget(int appWidgetId, AppWidgetProviderInfo providerInfo) {
    addInstalledProvider(providerInfo);
    bindAppWidgetId(appWidgetId, providerInfo.provider);
    widgetInfos.get(appWidgetId).info = providerInfo;
  }

  @Deprecated
  public void putWidgetInfo(int appWidgetId, AppWidgetProviderInfo expectedWidgetInfo) {
    addBoundWidget(appWidgetId, expectedWidgetInfo);
  }

  @Implementation
  protected AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    if (widgetInfo == null) return null;
    return widgetInfo.info;
  }

  @HiddenApi @Implementation
  public void bindAppWidgetId(int appWidgetId, ComponentName provider) {
    WidgetInfo widgetInfo = new WidgetInfo(provider);
    widgetInfos.put(appWidgetId, widgetInfo);
    for (AppWidgetProviderInfo appWidgetProviderInfo : installedProviders) {
      if (provider != null && provider.equals(appWidgetProviderInfo.provider)) {
        widgetInfo.info = appWidgetProviderInfo;
      }
    }
  }

  @Implementation
  protected boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider) {
    if (validWidgetProviderComponentName) {
      bindAppWidgetId(appWidgetId, provider);
      return allowedToBindWidgets;
    } else {
      throw new IllegalArgumentException("not an appwidget provider");
    }
  }

  /**
   * Triggers a reapplication of the most recent set of actions against the widget, which is what happens when the
   * phone is rotated. Does not attempt to simulate a change in screen geometry.
   *
   * @param appWidgetId the ID of the widget to be affected
   */
  public void reconstructWidgetViewAsIfPhoneWasRotated(int appWidgetId) {
    WidgetInfo widgetInfo = widgetInfos.get(appWidgetId);
    widgetInfo.view = createWidgetView(widgetInfo.layoutId);
    widgetInfo.lastRemoteViews.reapply(context, widgetInfo.view);
  }

  /**
   * Creates a widget by inflating its layout.
   *
   * @param appWidgetProviderClass the app widget provider class
   * @param widgetLayoutId         id of the layout to inflate
   * @return the ID of the new widget
   */
  public int createWidget(Class<? extends AppWidgetProvider> appWidgetProviderClass, int widgetLayoutId) {
    return createWidgets(appWidgetProviderClass, widgetLayoutId, 1)[0];
  }

  /**
   * Creates a bunch of widgets by inflating the same layout multiple times.
   *
   * @param appWidgetProviderClass the app widget provider class
   * @param widgetLayoutId         id of the layout to inflate
   * @param howManyToCreate        number of new widgets to create
   * @return the IDs of the new widgets
   */
  public int[] createWidgets(Class<? extends AppWidgetProvider> appWidgetProviderClass, int widgetLayoutId, int howManyToCreate) {
    AppWidgetProvider appWidgetProvider = ReflectionHelpers.callConstructor(appWidgetProviderClass);

    int[] newWidgetIds = new int[howManyToCreate];
    for (int i = 0; i < howManyToCreate; i++) {
      View widgetView = createWidgetView(widgetLayoutId);

      int myWidgetId = nextWidgetId++;
      widgetInfos.put(myWidgetId, new WidgetInfo(widgetView, widgetLayoutId, appWidgetProvider));
      newWidgetIds[i] = myWidgetId;
    }

    appWidgetProvider.onUpdate(context, realAppWidgetManager, newWidgetIds);
    return newWidgetIds;
  }

  private View createWidgetView(int widgetLayoutId) {
    return LayoutInflater.from(RuntimeEnvironment.application).inflate(widgetLayoutId, null);
  }

  /**
   * @param widgetId id of the desired widget
   * @return the widget associated with {@code widgetId}
   */
  public View getViewFor(int widgetId) {
    return widgetInfos.get(widgetId).view;
  }

  /**
   * @param widgetId id of the widget whose provider is to be returned
   * @return the {@code AppWidgetProvider} associated with {@code widgetId}
   */
  public AppWidgetProvider getAppWidgetProviderFor(int widgetId) {
    return widgetInfos.get(widgetId).appWidgetProvider;
  }

  /**
   * Enables testing of widget behavior when all of the views are recreated on every
   * update. This is useful for ensuring that your widget will behave correctly even
   * if it is restarted by the OS between events.
   *
   * @param alwaysRecreate whether or not to always recreate the views
   */
  public void setAlwaysRecreateViewsDuringUpdate(boolean alwaysRecreate) {
    alwaysRecreateViewsDuringUpdate = alwaysRecreate;
  }

  /**
   * @return the state of the{@code alwaysRecreateViewsDuringUpdate} flag
   */
  public boolean getAlwaysRecreateViewsDuringUpdate() {
    return alwaysRecreateViewsDuringUpdate;
  }

  public void setAllowedToBindAppWidgets(boolean allowed) {
    allowedToBindWidgets = allowed;
  }

  public void setValidWidgetProviderComponentName(boolean validWidgetProviderComponentName) {
    this.validWidgetProviderComponentName = validWidgetProviderComponentName;
  }

  private static class WidgetInfo {
    View view;
    int layoutId;
    final AppWidgetProvider appWidgetProvider;
    RemoteViews lastRemoteViews;
    final ComponentName providerComponent;
    AppWidgetProviderInfo info;

    public WidgetInfo(View view, int layoutId, AppWidgetProvider appWidgetProvider) {
      this.view = view;
      this.layoutId = layoutId;
      this.appWidgetProvider = appWidgetProvider;
      String packageName = appWidgetProvider.getClass().getPackage().getName();
      String className = appWidgetProvider.getClass().getName();
      providerComponent = new ComponentName(packageName, className);
    }

    public WidgetInfo(ComponentName providerComponent) {
      this.providerComponent = providerComponent;
      this.appWidgetProvider = null;
    }
  }
}