package org.robolectric.shadows;

import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.GET_ACTIVITIES;
import static android.content.pm.PackageManager.GET_CONFIGURATIONS;
import static android.content.pm.PackageManager.GET_GIDS;
import static android.content.pm.PackageManager.GET_INSTRUMENTATION;
import static android.content.pm.PackageManager.GET_INTENT_FILTERS;
import static android.content.pm.PackageManager.GET_META_DATA;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.GET_PROVIDERS;
import static android.content.pm.PackageManager.GET_RECEIVERS;
import static android.content.pm.PackageManager.GET_RESOLVED_FILTER;
import static android.content.pm.PackageManager.GET_SERVICES;
import static android.content.pm.PackageManager.GET_SHARED_LIBRARY_FILES;
import static android.content.pm.PackageManager.GET_SIGNATURES;
import static android.content.pm.PackageManager.GET_URI_PERMISSION_PATTERNS;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
import static android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
import static android.content.pm.PackageManager.SIGNATURE_FIRST_NOT_SIGNED;
import static android.content.pm.PackageManager.SIGNATURE_MATCH;
import static android.content.pm.PackageManager.SIGNATURE_NEITHER_SIGNED;
import static android.content.pm.PackageManager.SIGNATURE_NO_MATCH;
import static android.content.pm.PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
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.N;
import static java.util.Arrays.asList;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.Manifest;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.IPackageDataObserver;
import android.content.pm.IPackageDeleteObserver;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.Component;
import android.content.pm.PackageParser.IntentInfo;
import android.content.pm.PackageParser.Package;
import android.content.pm.PackageParser.PermissionGroup;
import android.content.pm.PackageStats;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.Signature;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
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;
import org.robolectric.shadows.ShadowPackageParser._PackageParser_;

@SuppressWarnings("NewApi")
@Implements(PackageManager.class)
public class ShadowPackageManager {
  static final String TAG = "PackageManager";

  @RealObject PackageManager realPackageManager;

  static Map<String, Boolean> permissionRationaleMap = new HashMap<>();
  static List<FeatureInfo> systemAvailableFeatures = new ArrayList<>();
  static final List<String> systemSharedLibraryNames = new ArrayList<>();
  static final Map<String, PackageInfo> packageInfos = new LinkedHashMap<>();

  // Those maps contain filter for components. If component exists but doesn't have filters,
  // it will have an entry in the map with an empty list.
  static final SortedMap<ComponentName, List<IntentFilter>> activityFilters = new TreeMap<>();
  static final SortedMap<ComponentName, List<IntentFilter>> serviceFilters = new TreeMap<>();
  static final SortedMap<ComponentName, List<IntentFilter>> providerFilters = new TreeMap<>();
  static final SortedMap<ComponentName, List<IntentFilter>> receiverFilters = new TreeMap<>();

  private static Map<String, PackageInfo> packageArchiveInfo = new HashMap<>();
  static final Map<String, PackageStats> packageStatsMap = new HashMap<>();
  static final Map<String, String> packageInstallerMap = new HashMap<>();
  static final Map<Integer, String[]> packagesForUid = new HashMap<>();
  static final Map<String, Integer> uidForPackage = new HashMap<>();
  static final Map<Integer, String> namesForUid = new HashMap<>();
  static final Map<Integer, Integer> verificationResults = new HashMap<>();
  static final Map<Integer, Long> verificationTimeoutExtension = new HashMap<>();
  static final Map<String, String> currentToCanonicalNames = new HashMap<>();
  static final Map<ComponentName, ComponentState> componentList = new LinkedHashMap<>();
  static final Map<ComponentName, Drawable> drawableList = new LinkedHashMap<>();
  static final Map<String, Drawable> applicationIcons = new HashMap<>();
  static final Map<String, Drawable> unbadgedApplicationIcons = new HashMap<>();
  static final Map<String, Boolean> systemFeatureList = new LinkedHashMap<>();
  static final SortedMap<ComponentName, List<IntentFilter>> preferredActivities = new TreeMap<>();
  static final SortedMap<ComponentName, List<IntentFilter>> persistentPreferredActivities =
      new TreeMap<>();
  static final Map<Pair<String, Integer>, Drawable> drawables = new LinkedHashMap<>();
  static final Map<String, Integer> applicationEnabledSettingMap = new HashMap<>();
  static Map<String, PermissionInfo> extraPermissions = new HashMap<>();
  static Map<String, PermissionGroupInfo> permissionGroups = new HashMap<>();
  public static Map<String, Resources> resources = new HashMap<>();
  static final Map<Intent, List<ResolveInfo>> resolveInfoForIntent =
      new TreeMap<>(new IntentComparator());
  static Set<String> deletedPackages = new HashSet<>();
  static Map<String, IPackageDeleteObserver> pendingDeleteCallbacks = new HashMap<>();
  static Set<String> hiddenPackages = new HashSet<>();
  static Multimap<Integer, String> sequenceNumberChangedPackagesMap = HashMultimap.create();
  static boolean canRequestPackageInstalls = false;
  static boolean safeMode = false;
  boolean shouldShowActivityChooser = false;

  /**
   * Makes sure that given activity exists.
   *
   * If the activity doesn't exist yet, it will be created with {@code applicationInfo} set to an
   * existing application, or if it doesn't exist, a new package will be created.
   *
   * @return existing or newly created activity info.
   */
  public ActivityInfo addActivityIfNotPresent(ComponentName componentName) {
    return addComponent(
        activityFilters,
        p -> p.activities,
        (p, a) -> p.activities = a,
        updateName(componentName, new ActivityInfo()),
        false);
  }

  /**
   * Makes sure that given service exists.
   *
   * If the service doesn't exist yet, it will be created with {@code applicationInfo} set to an
   * existing application, or if it doesn't exist, a new package will be created.
   *
   * @return existing or newly created service info.
   */
  public ServiceInfo addServiceIfNotPresent(ComponentName componentName) {
    return addComponent(
        serviceFilters,
        p -> p.services,
        (p, a) -> p.services = a,
        updateName(componentName, new ServiceInfo()),
        false);
  }

  /**
   * Makes sure that given receiver exists.
   *
   * If the receiver doesn't exist yet, it will be created with {@code applicationInfo} set to an
   * existing application, or if it doesn't exist, a new package will be created.
   *
   * @return existing or newly created receiver info.
   */
  public ActivityInfo addReceiverIfNotPresent(ComponentName componentName) {
    return addComponent(
        receiverFilters,
        p -> p.receivers,
        (p, a) -> p.receivers = a,
        updateName(componentName, new ActivityInfo()),
        false);
  }

  /**
   * Makes sure that given provider exists.
   *
   * If the provider doesn't exist yet, it will be created with {@code applicationInfo} set to an
   * existing application, or if it doesn't exist, a new package will be created.
   *
   * @return existing or newly created provider info.
   */
  public ProviderInfo addProviderIfNotPresent(ComponentName componentName) {
    return addComponent(
        providerFilters,
        p -> p.providers,
        (p, a) -> p.providers = a,
        updateName(componentName, new ProviderInfo()),
        false);
  }

  private <C extends ComponentInfo> C updateName(ComponentName name, C component) {
    component.name = name.getClassName();
    component.packageName = name.getPackageName();
    if (component.applicationInfo != null) {
      component.applicationInfo.packageName = component.packageName;
    }
    return component;
  }

  /**
   * Adds or updates given activity in the system.
   *
   * If activity with the same {@link ComponentInfo#name} and {@code ComponentInfo#packageName}
   * exists it will be updated. Its {@link ComponentInfo#applicationInfo} is always set to {@link
   * ApplicationInfo} already existing in the system, but if no application exists a new one will
   * be created using {@link ComponentInfo#applicationInfo} in this component.
   */
  public void addOrUpdateActivity(ActivityInfo activityInfo) {
    addComponent(
        activityFilters,
        p -> p.activities,
        (p, a) -> p.activities = a,
        new ActivityInfo(activityInfo),
        true);
  }

  /**
   * Adds or updates given service in the system.
   *
   * If service with the same {@link ComponentInfo#name} and {@code ComponentInfo#packageName}
   * exists it will be updated. Its {@link ComponentInfo#applicationInfo} is always set to {@link
   * ApplicationInfo} already existing in the system, but if no application exists a new one will be
   * created using {@link ComponentInfo#applicationInfo} in this component.
   */
  public void addOrUpdateService(ServiceInfo serviceInfo) {
    addComponent(
        serviceFilters,
        p -> p.services,
        (p, a) -> p.services = a,
        new ServiceInfo(serviceInfo),
        true);
  }

  /**
   * Adds or updates given broadcast receiver in the system.
   *
   * If broadcast receiver with the same {@link ComponentInfo#name} and {@code
   * ComponentInfo#packageName} exists it will be updated. Its {@link ComponentInfo#applicationInfo}
   * is always set to {@link ApplicationInfo} already existing in the system, but if no
   * application exists a new one will be created using {@link ComponentInfo#applicationInfo} in
   * this component.
   */
  public void addOrUpdateReceiver(ActivityInfo receiverInfo) {
    addComponent(
        receiverFilters,
        p -> p.receivers,
        (p, a) -> p.receivers = a,
        new ActivityInfo(receiverInfo),
        true);
  }

  /**
   * Adds or updates given content provider in the system.
   *
   * If content provider with the same {@link ComponentInfo#name} and {@code
   * ComponentInfo#packageName} exists it will be updated. Its {@link ComponentInfo#applicationInfo}
   * is always set to {@link ApplicationInfo} already existing in the system, but if no
   * application exists a new one will be created using {@link ComponentInfo#applicationInfo} in
   * this component.
   */
  public void addOrUpdateProvider(ProviderInfo providerInfo) {
    addComponent(
        providerFilters,
        p -> p.providers,
        (p, a) -> p.providers = a,
        new ProviderInfo(providerInfo),
        true);
  }

  /**
   * Removes activity from the package manager.
   *
   * @return the removed component or {@code null} if no such component existed.
   */
  @Nullable
  public ActivityInfo removeActivity(ComponentName componentName) {
    return removeComponent(
        componentName, activityFilters, p -> p.activities, (p, a) -> p.activities = a);
  }

  /**
   * Removes service from the package manager.
   *
   * @return the removed component or {@code null} if no such component existed.
   */
  @Nullable
  public ServiceInfo removeService(ComponentName componentName) {
    return removeComponent(
        componentName, serviceFilters, p -> p.services, (p, a) -> p.services = a);
  }

  /**
   * Removes content provider from the package manager.
   *
   * @return the removed component or {@code null} if no such component existed.
   */
  @Nullable
  public ProviderInfo removeProvider(ComponentName componentName) {
    return removeComponent(
        componentName, providerFilters, p -> p.providers, (p, a) -> p.providers = a);
  }

  /**
   * Removes broadcast receiver from the package manager.
   *
   * @return the removed component or {@code null} if no such component existed.
   */
  @Nullable
  public ActivityInfo removeReceiver(ComponentName componentName) {
    return removeComponent(
        componentName, receiverFilters, p -> p.receivers, (p, a) -> p.receivers = a);
  }

  private <C extends ComponentInfo> C addComponent(
      SortedMap<ComponentName, List<IntentFilter>> filtersMap,
      Function<PackageInfo, C[]> componentArrayInPackage,
      BiConsumer<PackageInfo, C[]> componentsSetter,
      C newComponent,
      boolean updateIfExists) {
    String packageName = newComponent.packageName;
    if (packageName == null && newComponent.applicationInfo != null) {
      packageName = newComponent.applicationInfo.packageName;
    }
    if (packageName == null) {
      throw new IllegalArgumentException("Component needs a package name");
    }
    if (newComponent.name == null) {
      throw new IllegalArgumentException("Component needs a name");
    }
    PackageInfo packageInfo = packageInfos.get(packageName);
    if (packageInfo == null) {
      packageInfo = new PackageInfo();
      packageInfo.packageName = packageName;
      packageInfo.applicationInfo = newComponent.applicationInfo;
      installPackage(packageInfo);
      packageInfo = packageInfos.get(packageName);
    }
    newComponent.applicationInfo = packageInfo.applicationInfo;
    C[] components = componentArrayInPackage.apply(packageInfo);
    if (components == null) {
      @SuppressWarnings("unchecked")
      C[] newComponentArray = (C[]) Array.newInstance(newComponent.getClass(), 0);
      components = newComponentArray;
    } else {
      for (int i = 0; i < components.length; i++) {
        if (newComponent.name.equals(components[i].name)) {
          if (updateIfExists) {
            components[i] = newComponent;
          }
          return components[i];
        }
      }
    }
    components = Arrays.copyOf(components, components.length + 1);
    componentsSetter.accept(packageInfo, components);
    components[components.length - 1] = newComponent;

    filtersMap.put(
        new ComponentName(newComponent.packageName, newComponent.name), new ArrayList<>());
    return newComponent;
  }

  @Nullable
  private <C extends ComponentInfo> C removeComponent(
      ComponentName componentName,
      SortedMap<ComponentName, List<IntentFilter>> filtersMap,
      Function<PackageInfo, C[]> componentArrayInPackage,
      BiConsumer<PackageInfo, C[]> componentsSetter) {
    filtersMap.remove(componentName);
    String packageName = componentName.getPackageName();
    PackageInfo packageInfo = packageInfos.get(packageName);
    if (packageInfo == null) {
      return null;
    }
    C[] components = componentArrayInPackage.apply(packageInfo);
    if (components == null) {
      return null;
    }
    for (int i = 0; i < components.length; i++) {
      C component = components[i];
      if (componentName.getClassName().equals(component.name)) {
        C[] newComponents;
        if (components.length == 1) {
          newComponents = null;
        } else {
          newComponents = Arrays.copyOf(components, components.length - 1);
          System.arraycopy(components, i + 1, newComponents, i, components.length - i - 1);
        }
        componentsSetter.accept(packageInfo, newComponents);
        return component;
      }
    }
    return null;
  }

  /**
   * Settings for a particular package.
   *
   * This class mirrors {@link com.android.server.pm.PackageSetting}, which is used by {@link
   * PackageManager}.
   */
  public static class PackageSetting {

    /** Whether the package is suspended in {@link PackageManager}. */
    private boolean suspended = false;

    /** The message to be displayed to the user when they try to launch the app. */
    private String dialogMessage = null;

    /** An optional {@link PersistableBundle} shared with the app. */
    private PersistableBundle suspendedAppExtras = null;

    /** An optional {@link PersistableBundle} shared with the launcher. */
    private PersistableBundle suspendedLauncherExtras = null;

    public PackageSetting() {}

    public PackageSetting(PackageSetting that) {
      this.suspended = that.suspended;
      this.dialogMessage = that.dialogMessage;
      this.suspendedAppExtras = deepCopyNullablePersistableBundle(that.suspendedAppExtras);
      this.suspendedLauncherExtras =
          deepCopyNullablePersistableBundle(that.suspendedLauncherExtras);
    }

    /**
     * Sets the suspension state of the package.
     *
     * If {@code suspended} is false, {@code dialogMessage}, {@code appExtras}, and {@code
     * launcherExtras} will be ignored.
     */
    void setSuspended(
        boolean suspended,
        String dialogMessage,
        PersistableBundle appExtras,
        PersistableBundle launcherExtras) {
      this.suspended = suspended;
      this.dialogMessage = suspended ? dialogMessage : null;
      this.suspendedAppExtras = suspended ? deepCopyNullablePersistableBundle(appExtras) : null;
      this.suspendedLauncherExtras =
          suspended ? deepCopyNullablePersistableBundle(launcherExtras) : null;
    }

    public boolean isSuspended() {
      return suspended;
    }

    public String getDialogMessage() {
      return dialogMessage;
    }

    public PersistableBundle getSuspendedAppExtras() {
      return suspendedAppExtras;
    }

    public PersistableBundle getSuspendedLauncherExtras() {
      return suspendedLauncherExtras;
    }

    private static PersistableBundle deepCopyNullablePersistableBundle(PersistableBundle bundle) {
      return bundle == null ? null : bundle.deepCopy();
    }
  }

  static final Map<String, PackageSetting> packageSettings = new HashMap<>();

  // From com.android.server.pm.PackageManagerService.compareSignatures().
  static int compareSignature(Signature[] signatures1, Signature[] signatures2) {
    if (signatures1 == null) {
      return (signatures2 == null) ? SIGNATURE_NEITHER_SIGNED : SIGNATURE_FIRST_NOT_SIGNED;
    }
    if (signatures2 == null) {
      return SIGNATURE_SECOND_NOT_SIGNED;
    }
    if (signatures1.length != signatures2.length) {
      return SIGNATURE_NO_MATCH;
    }
    HashSet<Signature> signatures1set = new HashSet<>(asList(signatures1));
    HashSet<Signature> signatures2set = new HashSet<>(asList(signatures2));
    return signatures1set.equals(signatures2set) ? SIGNATURE_MATCH : SIGNATURE_NO_MATCH;
  }

  // TODO(christianw): reconcile with AndroidTestEnvironment.setUpPackageStorage
  private static void setUpPackageStorage(ApplicationInfo applicationInfo) {
    if (applicationInfo.sourceDir == null) {
      applicationInfo.sourceDir = createTempDir(applicationInfo.packageName + "-sourceDir");
    }

    if (applicationInfo.dataDir == null) {
      applicationInfo.dataDir = createTempDir(applicationInfo.packageName + "-dataDir");
    }
    if (applicationInfo.publicSourceDir == null) {
      applicationInfo.publicSourceDir = applicationInfo.sourceDir;
    }
    if (RuntimeEnvironment.getApiLevel() >= N) {
      applicationInfo.credentialProtectedDataDir = createTempDir("userDataDir");
      applicationInfo.deviceProtectedDataDir = createTempDir("deviceDataDir");
    }
  }

  private static String createTempDir(String name) {
    return RuntimeEnvironment.getTempDirectory()
        .createIfNotExists(name)
        .toAbsolutePath()
        .toString();
  }

  /**
   * Sets extra resolve infos for an intent.
   *
   * Those entries are added to whatever might be in the manifest already.
   *
   * Note that all resolve infos will have {@link ResolveInfo#isDefault} field set to {@code
   * true} to allow their resolution for implicit intents. If this is not what you want, then you
   * still have the reference to those ResolveInfos, and you can set the field back to {@code
   * false}.
   *
   * @deprecated see the note on {@link #addResolveInfoForIntent(Intent, ResolveInfo)}.
   */
  @Deprecated
  public void setResolveInfosForIntent(Intent intent, List<ResolveInfo> info) {
    resolveInfoForIntent.remove(intent);
    for (ResolveInfo resolveInfo : info) {
      addResolveInfoForIntent(intent, resolveInfo);
    }
  }

  /** @deprecated see note on {@link #addResolveInfoForIntent(Intent, ResolveInfo)}. */
  @Deprecated
  public void addResolveInfoForIntent(Intent intent, List<ResolveInfo> info) {
    setResolveInfosForIntent(intent, info);
  }

  /**
   * Adds extra resolve info for an intent.
   *
   * Note that this resolve info will have {@link ResolveInfo#isDefault} field set to {@code
   * true} to allow its resolution for implicit intents. If this is not what you want, then please
   * use {@link #addResolveInfoForIntentNoDefaults} instead.
   *
   * @deprecated use {@link #addIntentFilterForComponent} instead and if the component doesn't exist
   *     add it using any of {@link #installPackage}, {@link #addOrUpdateActivity}, {@link
   *     #addActivityIfNotPresent} or their counterparts for other types of components.
   */
  @Deprecated
  public void addResolveInfoForIntent(Intent intent, ResolveInfo info) {
    info.isDefault = true;
    ComponentInfo[] componentInfos =
        new ComponentInfo[] {
          info.activityInfo,
          info.serviceInfo,
          Build.VERSION.SDK_INT >= KITKAT ? info.providerInfo : null
        };
    for (ComponentInfo component : componentInfos) {
      if (component != null && component.applicationInfo != null) {
        component.applicationInfo.flags |= ApplicationInfo.FLAG_INSTALLED;
        if (component.applicationInfo.processName == null) {
          component.applicationInfo.processName = component.applicationInfo.packageName;
        }
      }
    }
    if (info.match == 0) {
      info.match = Integer.MAX_VALUE; // make sure, that this is as good match as possible.
    }
    addResolveInfoForIntentNoDefaults(intent, info);
  }

  /**
   * Adds the {@code info} as {@link ResolveInfo} for the intent but without applying any default
   * values.
   *
   * In particular it will not make the {@link ResolveInfo#isDefault} field {@code true}, that
   * means that this resolve info will not resolve for {@link Intent#resolveActivity} and {@link
   * Context#startActivity}.
   *
   * @deprecated see the note on {@link #addResolveInfoForIntent(Intent, ResolveInfo)}.
   */
  @Deprecated
  public void addResolveInfoForIntentNoDefaults(Intent intent, ResolveInfo info) {
    Preconditions.checkNotNull(info);
    List<ResolveInfo> infoList = resolveInfoForIntent.get(intent);
    if (infoList == null) {
      infoList = new ArrayList<>();
      resolveInfoForIntent.put(intent, infoList);
    }
    infoList.add(info);
  }

  /**
   * Removes {@link ResolveInfo}s registered using {@link #addResolveInfoForIntent}.
   *
   * @deprecated see note on {@link #addResolveInfoForIntent(Intent, ResolveInfo)}.
   */
  @Deprecated
  public void removeResolveInfosForIntent(Intent intent, String packageName) {
    List<ResolveInfo> infoList = resolveInfoForIntent.get(intent);
    if (infoList == null) {
      infoList = new ArrayList<>();
      resolveInfoForIntent.put(intent, infoList);
    }

    for (Iterator<ResolveInfo> iterator = infoList.iterator(); iterator.hasNext(); ) {
      ResolveInfo resolveInfo = iterator.next();
      if (getPackageName(resolveInfo).equals(packageName)) {
        iterator.remove();
      }
    }
  }

  private static String getPackageName(ResolveInfo resolveInfo) {
    if (resolveInfo.resolvePackageName != null) {
      return resolveInfo.resolvePackageName;
    } else if (resolveInfo.activityInfo != null) {
      return resolveInfo.activityInfo.packageName;
    } else if (resolveInfo.serviceInfo != null) {
      return resolveInfo.serviceInfo.packageName;
    } else if (resolveInfo.providerInfo != null) {
      return resolveInfo.providerInfo.packageName;
    }
    throw new IllegalStateException(
        "Could not find package name for ResolveInfo " + resolveInfo.toString());
  }

  public void addActivityIcon(ComponentName component, Drawable drawable) {
    drawableList.put(component, drawable);
  }

  public void addActivityIcon(Intent intent, Drawable drawable) {
    drawableList.put(intent.getComponent(), drawable);
  }

  public void setApplicationIcon(String packageName, Drawable drawable) {
    applicationIcons.put(packageName, drawable);
  }

  public void setUnbadgedApplicationIcon(String packageName, Drawable drawable) {
    unbadgedApplicationIcons.put(packageName, drawable);
  }

  /**
   * Return the flags set in call to {@link
   * android.app.ApplicationPackageManager#setComponentEnabledSetting(ComponentName, int, int)}.
   *
   * @param componentName The component name.
   * @return The flags.
   */
  public int getComponentEnabledSettingFlags(ComponentName componentName) {
    ComponentState state = componentList.get(componentName);
    return state != null ? state.flags : 0;
  }

  /**
   * Installs a package with the {@link PackageManager}.
   *
   * In order to create PackageInfo objects in a valid state please use {@link
   * androidx.test.core.content.pm.PackageInfoBuilder}.
   *
   * This method automatically simulates instalation of a package in the system, so it adds a
   * flag {@link ApplicationInfo#FLAG_INSTALLED} to the application info and makes sure it exits. It
   * will update applicationInfo in package components as well.
   *
   * If you don't want the package to be installed, use {@link #addPackageNoDefaults} instead.
   */
  public void installPackage(PackageInfo packageInfo) {
    ApplicationInfo appInfo = packageInfo.applicationInfo;
    if (appInfo == null) {
      appInfo = new ApplicationInfo();
      packageInfo.applicationInfo = appInfo;
    }
    if (appInfo.packageName == null) {
      appInfo.packageName = packageInfo.packageName;
    }
    if (appInfo.processName == null) {
      appInfo.processName = appInfo.packageName;
    }
    appInfo.flags |= ApplicationInfo.FLAG_INSTALLED;
    ComponentInfo[][] componentInfoArrays =
        new ComponentInfo[][] {
          packageInfo.activities,
          packageInfo.services,
          packageInfo.providers,
          packageInfo.receivers,
        };
    int uniqueNameCounter = 0;
    for (ComponentInfo[] componentInfos : componentInfoArrays) {
      if (componentInfos == null) {
        continue;
      }
      for (ComponentInfo componentInfo : componentInfos) {
        if (componentInfo.name == null) {
          componentInfo.name = appInfo.packageName + ".DefaultName" + uniqueNameCounter++;
          componentInfo.packageName = packageInfo.packageName;
        }
        componentInfo.applicationInfo = appInfo;
        componentInfo.packageName = appInfo.packageName;
        if (componentInfo.processName == null) {
          componentInfo.processName = appInfo.processName;
        }
      }
    }
    addPackageNoDefaults(packageInfo);
  }

  /**
   * Adds a package to the {@link PackageManager}, but doesn't set any default values on it.
   *
   * Right now it will not set {@link ApplicationInfo#FLAG_INSTALLED} flag on its application, so
   * if not set explicitly, it will be treated as not installed.
   */
  public void addPackageNoDefaults(PackageInfo packageInfo) {
    PackageStats packageStats = new PackageStats(packageInfo.packageName);
    addPackage(packageInfo, packageStats);
  }

  /**
   * Installs a package with its stats with the {@link PackageManager}.
   *
   * This method doesn't add any defaults to the {@code packageInfo} parameters. You should make
   * sure it is valid (see {@link #installPackage(PackageInfo)}).
   */
  public synchronized void addPackage(PackageInfo packageInfo, PackageStats packageStats) {
    if (packageInfo.applicationInfo != null
        && (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
      Log.w(TAG, "Adding not installed package: " + packageInfo.packageName);
    }
    Preconditions.checkArgument(packageInfo.packageName.equals(packageStats.packageName));

    packageInfos.put(packageInfo.packageName, packageInfo);
    packageStatsMap.put(packageInfo.packageName, packageStats);

    packageSettings.put(packageInfo.packageName, new PackageSetting());

    applicationEnabledSettingMap.put(
        packageInfo.packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT);
    if (packageInfo.applicationInfo != null) {
      namesForUid.put(packageInfo.applicationInfo.uid, packageInfo.packageName);
    }
  }

  /** @deprecated Use {@link #installPackage(PackageInfo)} instead. */
  @Deprecated
  public void addPackage(String packageName) {
    PackageInfo packageInfo = new PackageInfo();
    packageInfo.packageName = packageName;

    ApplicationInfo applicationInfo = new ApplicationInfo();

    applicationInfo.packageName = packageName;
    // TODO: setUpPackageStorage should be in installPackage but we need to fix all tests first
    setUpPackageStorage(applicationInfo);
    packageInfo.applicationInfo = applicationInfo;
    installPackage(packageInfo);
  }

  /** This method is getting renamed to {link {@link #installPackage}. */
  @Deprecated
  public void addPackage(PackageInfo packageInfo) {
    installPackage(packageInfo);
  }

  /**
   * Testing API allowing to retrieve internal package representation.
   *
   * This will allow to modify the package in a way visible to Robolectric, as this is
   * Robolectric's internal full package representation.
   *
   * Note that maybe a better way is to just modify the test manifest to make those modifications
   * in a standard way.
   *
   * Retrieving package info using {@link PackageManager#getPackageInfo} / {@link
   * PackageManager#getApplicationInfo} will return defensive copies that will be stripped out of
   * information according to provided flags. Don't use it to modify Robolectric state.
   */
  public PackageInfo getInternalMutablePackageInfo(String packageName) {
    return packageInfos.get(packageName);
  }

  /** @deprecated Use {@link #getInternalMutablePackageInfo} instead. It has better name. */
  @Deprecated
  public PackageInfo getPackageInfoForTesting(String packageName) {
    return getInternalMutablePackageInfo(packageName);
  }

  public void addPermissionInfo(PermissionInfo permissionInfo) {
    extraPermissions.put(permissionInfo.name, permissionInfo);
  }

  /**
   * Adds {@code packageName} to the list of changed packages for the particular {@code
   * sequenceNumber}.
   *
   * @param sequenceNumber has to be >= 0
   * @param packageName name of the package that was changed
   */
  public void addChangedPackage(int sequenceNumber, String packageName) {
    if (sequenceNumber < 0) {
      return;
    }
    sequenceNumberChangedPackagesMap.put(sequenceNumber, packageName);
  }

  /**
   * Allows overriding or adding permission-group elements. These would be otherwise specified by
   * either (the
   * system)[https://developer.android.com/guide/topics/permissions/requesting.html#perm-groups] or
   * by (the app
   * itself)[https://developer.android.com/guide/topics/manifest/permission-group-element.html], as
   * part of its manifest
   *
   * {@link android.content.pm.PackageParser.PermissionGroup}s added through this method have
   * precedence over those specified with the same name by one of the aforementioned methods.
   *
   * @see PackageManager#getAllPermissionGroups(int)
   * @see PackageManager#getPermissionGroupInfo(String, int)
   */
  public void addPermissionGroupInfo(PermissionGroupInfo permissionGroupInfo) {
    permissionGroups.put(permissionGroupInfo.name, permissionGroupInfo);
  }

  public void removePackage(String packageName) {
    packageInfos.remove(packageName);

    packageSettings.remove(packageName);
  }

  public void setSystemFeature(String name, boolean supported) {
    systemFeatureList.put(name, supported);
  }

  public void addDrawableResolution(String packageName, int resourceId, Drawable drawable) {
    drawables.put(new Pair(packageName, resourceId), drawable);
  }

  public void setNameForUid(int uid, String name) {
    namesForUid.put(uid, name);
  }

  public void setPackagesForCallingUid(String... packagesForCallingUid) {
    packagesForUid.put(Binder.getCallingUid(), packagesForCallingUid);
    for (String packageName : packagesForCallingUid) {
      uidForPackage.put(packageName, Binder.getCallingUid());
    }
  }

  public void setPackagesForUid(int uid, String... packagesForCallingUid) {
    packagesForUid.put(uid, packagesForCallingUid);
    for (String packageName : packagesForCallingUid) {
      uidForPackage.put(packageName, uid);
    }
  }

  @Implementation
  @Nullable
  protected String[] getPackagesForUid(int uid) {
    return packagesForUid.get(uid);
  }

  public void setPackageArchiveInfo(String archiveFilePath, PackageInfo packageInfo) {
    packageArchiveInfo.put(archiveFilePath, packageInfo);
  }

  public int getVerificationResult(int id) {
    Integer result = verificationResults.get(id);
    if (result == null) {
      // 0 isn't a "valid" result, so we can check for the case when verification isn't
      // called, if needed
      return 0;
    }
    return result;
  }

  public long getVerificationExtendedTimeout(int id) {
    Long result = verificationTimeoutExtension.get(id);
    if (result == null) {
      return 0;
    }
    return result;
  }

  public void setShouldShowRequestPermissionRationale(String permission, boolean show) {
    permissionRationaleMap.put(permission, show);
  }

  public void addSystemAvailableFeature(FeatureInfo featureInfo) {
    systemAvailableFeatures.add(featureInfo);
  }

  public void clearSystemAvailableFeatures() {
    systemAvailableFeatures.clear();
  }

  /** Adds a value to be returned by {@link PackageManager#getSystemSharedLibraryNames()}. */
  public void addSystemSharedLibraryName(String name) {
    systemSharedLibraryNames.add(name);
  }

  /** Clears the values returned by {@link PackageManager#getSystemSharedLibraryNames()}. */
  public void clearSystemSharedLibraryNames() {
    systemSharedLibraryNames.clear();
  }

  public void addCurrentToCannonicalName(String currentName, String canonicalName) {
    currentToCanonicalNames.put(currentName, canonicalName);
  }

  /**
   * Sets if the {@link PackageManager} is allowed to request package installs through package
   * installer.
   */
  public void setCanRequestPackageInstalls(boolean canRequestPackageInstalls) {
    ShadowPackageManager.canRequestPackageInstalls = canRequestPackageInstalls;
  }

  @Implementation(minSdk = N)
  protected List<ResolveInfo> queryBroadcastReceiversAsUser(
      Intent intent, int flags, UserHandle userHandle) {
    return null;
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected List<ResolveInfo> queryBroadcastReceivers(
      Intent intent, int flags, @UserIdInt int userId) {
    return null;
  }

  @Implementation
  protected PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) {
    List<PackageInfo> result = new ArrayList<>();
    for (PackageInfo packageInfo : packageInfos.values()) {
      if (applicationEnabledSettingMap.get(packageInfo.packageName)
              != COMPONENT_ENABLED_STATE_DISABLED
          || (flags & MATCH_UNINSTALLED_PACKAGES) == MATCH_UNINSTALLED_PACKAGES) {
        result.add(packageInfo);
      }
    }

    List<PackageInfo> packages = result;
    for (PackageInfo aPackage : packages) {
      ApplicationInfo appInfo = aPackage.applicationInfo;
      if (appInfo != null && archiveFilePath.equals(appInfo.sourceDir)) {
        return aPackage;
      }
    }

    return Shadow.directlyOn(realPackageManager, PackageManager.class).getPackageArchiveInfo(
        archiveFilePath, flags);
  }

  @Implementation
  protected void freeStorageAndNotify(long freeStorageSize, IPackageDataObserver observer) {}

  @Implementation
  protected void freeStorage(long freeStorageSize, IntentSender pi) {}

  /**
   * Uninstalls the package from the system in a way, that will allow its discovery through {@link
   * PackageManager#MATCH_UNINSTALLED_PACKAGES}.
   */
  public void deletePackage(String packageName) {
    deletedPackages.add(packageName);
    packageInfos.remove(packageName);
    mapForPackage(activityFilters, packageName).clear();
    mapForPackage(serviceFilters, packageName).clear();
    mapForPackage(providerFilters, packageName).clear();
    mapForPackage(receiverFilters, packageName).clear();
  }

  protected void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) {
    pendingDeleteCallbacks.put(packageName, observer);
  }

  /**
   * Runs the callbacks pending from calls to {@link PackageManager#deletePackage(String,
   * IPackageDeleteObserver, int)}
   */
  public void doPendingUninstallCallbacks() {
    boolean hasDeletePackagesPermission = false;
    String[] requestedPermissions =
        packageInfos.get(RuntimeEnvironment.application.getPackageName()).requestedPermissions;
    if (requestedPermissions != null) {
      for (String permission : requestedPermissions) {
        if (Manifest.permission.DELETE_PACKAGES.equals(permission)) {
          hasDeletePackagesPermission = true;
          break;
        }
      }
    }

    for (String packageName : pendingDeleteCallbacks.keySet()) {
      int resultCode = PackageManager.DELETE_FAILED_INTERNAL_ERROR;

      PackageInfo removed = packageInfos.get(packageName);
      if (hasDeletePackagesPermission && removed != null) {
        deletePackage(packageName);
        resultCode = PackageManager.DELETE_SUCCEEDED;
      }

      try {
        pendingDeleteCallbacks.get(packageName).packageDeleted(packageName, resultCode);
      } catch (RemoteException e) {
        throw new RuntimeException(e);
      }
    }
    pendingDeleteCallbacks.clear();
  }

  /**
   * Returns package names successfully deleted with {@link PackageManager#deletePackage(String,
   * IPackageDeleteObserver, int)} Note that like real {@link PackageManager} the calling context
   * must have {@link android.Manifest.permission#DELETE_PACKAGES} permission set.
   */
  public Set<String> getDeletedPackages() {
    return deletedPackages;
  }

  protected List<ResolveInfo> queryOverriddenIntents(Intent intent, int flags) {
    List<ResolveInfo> overrides = resolveInfoForIntent.get(intent);
    if (overrides == null) {
      return Collections.emptyList();
    }
    List<ResolveInfo> result = new ArrayList<>(overrides.size());
    for (ResolveInfo resolveInfo : overrides) {
      result.add(ShadowResolveInfo.newResolveInfo(resolveInfo));
    }
    return result;
  }

  /**
   * Internal use only.
   *
   * @param appPackage
   */
  public void addPackageInternal(Package appPackage) {
    int flags =
        GET_ACTIVITIES
            | GET_RECEIVERS
            | GET_SERVICES
            | GET_PROVIDERS
            | GET_INSTRUMENTATION
            | GET_INTENT_FILTERS
            | GET_SIGNATURES
            | GET_RESOLVED_FILTER
            | GET_META_DATA
            | GET_GIDS
            | MATCH_DISABLED_COMPONENTS
            | GET_SHARED_LIBRARY_FILES
            | GET_URI_PERMISSION_PATTERNS
            | GET_PERMISSIONS
            | MATCH_UNINSTALLED_PACKAGES
            | GET_CONFIGURATIONS
            | MATCH_DISABLED_UNTIL_USED_COMPONENTS
            | MATCH_DIRECT_BOOT_UNAWARE
            | MATCH_DIRECT_BOOT_AWARE;

    for (PermissionGroup permissionGroup : appPackage.permissionGroups) {
      PermissionGroupInfo permissionGroupInfo =
          PackageParser.generatePermissionGroupInfo(permissionGroup, flags);
      addPermissionGroupInfo(permissionGroupInfo);
    }
    PackageInfo packageInfo =
        reflector(_PackageParser_.class)
            .generatePackageInfo(appPackage, new int[] {0}, flags, 0, 0);

    packageInfo.applicationInfo.uid = Process.myUid();
    packageInfo.applicationInfo.dataDir = createTempDir(packageInfo.packageName + "-dataDir");
    installPackage(packageInfo);
    addFilters(activityFilters, appPackage.activities);
    addFilters(serviceFilters, appPackage.services);
    addFilters(providerFilters, appPackage.providers);
    addFilters(receiverFilters, appPackage.receivers);
  }

  private void addFilters(
      Map<ComponentName, List<IntentFilter>> componentMap,
      List<? extends PackageParser.Component<?>> components) {
    if (components == null) {
      return;
    }
    for (Component<?> component : components) {
      ComponentName componentName = component.getComponentName();
      List<IntentFilter> registeredFilters = componentMap.get(componentName);
      if (registeredFilters == null) {
        registeredFilters = new ArrayList<>();
        componentMap.put(componentName, registeredFilters);
      }
      for (IntentInfo intentInfo : component.intents) {
        registeredFilters.add(new IntentFilter(intentInfo));
      }
    }
  }

  public static class IntentComparator implements Comparator<Intent> {

    @Override
    public int compare(Intent i1, Intent i2) {
      if (i1 == null && i2 == null) return 0;
      if (i1 == null && i2 != null) return -1;
      if (i1 != null && i2 == null) return 1;
      if (i1.equals(i2)) return 0;
      String action1 = i1.getAction();
      String action2 = i2.getAction();
      if (action1 == null && action2 != null) return -1;
      if (action1 != null && action2 == null) return 1;
      if (action1 != null && action2 != null) {
        if (!action1.equals(action2)) {
          return action1.compareTo(action2);
        }
      }
      Uri data1 = i1.getData();
      Uri data2 = i2.getData();
      if (data1 == null && data2 != null) return -1;
      if (data1 != null && data2 == null) return 1;
      if (data1 != null && data2 != null) {
        if (!data1.equals(data2)) {
          return data1.compareTo(data2);
        }
      }
      ComponentName component1 = i1.getComponent();
      ComponentName component2 = i2.getComponent();
      if (component1 == null && component2 != null) return -1;
      if (component1 != null && component2 == null) return 1;
      if (component1 != null && component2 != null) {
        if (!component1.equals(component2)) {
          return component1.compareTo(component2);
        }
      }
      String package1 = i1.getPackage();
      String package2 = i2.getPackage();
      if (package1 == null && package2 != null) return -1;
      if (package1 != null && package2 == null) return 1;
      if (package1 != null && package2 != null) {
        if (!package1.equals(package2)) {
          return package1.compareTo(package2);
        }
      }
      Set<String> categories1 = i1.getCategories();
      Set<String> categories2 = i2.getCategories();
      if (categories1 == null) return categories2 == null ? 0 : -1;
      if (categories2 == null) return 1;
      if (categories1.size() > categories2.size()) return 1;
      if (categories1.size() < categories2.size()) return -1;
      String[] array1 = categories1.toArray(new String[0]);
      String[] array2 = categories2.toArray(new String[0]);
      Arrays.sort(array1);
      Arrays.sort(array2);
      for (int i = 0; i < array1.length; ++i) {
        int val = array1[i].compareTo(array2[i]);
        if (val != 0) return val;
      }
      return 0;
    }
  }

  /**
   * Compares {@link ResolveInfo}s, ordering better matches before worse ones. This is the order in
   * which resolve infos should be returned to the user.
   */
  static class ResolveInfoComparator implements Comparator<ResolveInfo> {

    @Override
    public int compare(ResolveInfo o1, ResolveInfo o2) {
      if (o1 == null && o2 == null) {
        return 0;
      }
      if (o1 == null) {
        return -1;
      }
      if (o2 == null) {
        return 1;
      }
      if (o1.preferredOrder != o2.preferredOrder) {
        // higher priority is before lower
        return -Integer.compare(o1.preferredOrder, o2.preferredOrder);
      }
      if (o1.priority != o2.priority) {
        // higher priority is before lower
        return -Integer.compare(o1.priority, o2.priority);
      }
      if (o1.match != o2.match) {
        // higher match is before lower
        return -Integer.compare(o1.match, o2.match);
      }
      return 0;
    }
  }

  protected static class ComponentState {
    public int newState;
    public int flags;

    public ComponentState(int newState, int flags) {
      this.newState = newState;
      this.flags = flags;
    }
  }

  /**
   * Get list of intent filters defined for given activity.
   *
   * @param componentName Name of the activity whose intent filters are to be retrieved
   * @return the activity's intent filters
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public List<IntentFilter> getIntentFiltersForActivity(ComponentName componentName)
      throws NameNotFoundException {
    return getIntentFiltersForComponent(componentName, activityFilters);
  }

  /**
   * Get list of intent filters defined for given service.
   *
   * @param componentName Name of the service whose intent filters are to be retrieved
   * @return the service's intent filters
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public List<IntentFilter> getIntentFiltersForService(ComponentName componentName)
      throws NameNotFoundException {
    return getIntentFiltersForComponent(componentName, serviceFilters);
  }

  /**
   * Get list of intent filters defined for given receiver.
   *
   * @param componentName Name of the receiver whose intent filters are to be retrieved
   * @return the receiver's intent filters
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public List<IntentFilter> getIntentFiltersForReceiver(ComponentName componentName)
      throws NameNotFoundException {
      return getIntentFiltersForComponent(componentName, receiverFilters);
  }

  /**
   * Get list of intent filters defined for given provider.
   *
   * @param componentName Name of the provider whose intent filters are to be retrieved
   * @return the provider's intent filters
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public List<IntentFilter> getIntentFiltersForProvider(ComponentName componentName)
      throws NameNotFoundException {
    return getIntentFiltersForComponent(componentName, providerFilters);
  }

  /**
   * Add intent filter for given activity.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void addIntentFilterForActivity(ComponentName componentName, IntentFilter filter)
      throws NameNotFoundException {
    addIntentFilterForComponent(componentName, filter, activityFilters);
  }

  /**
   * Add intent filter for given service.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void addIntentFilterForService(ComponentName componentName, IntentFilter filter)
      throws NameNotFoundException {
    addIntentFilterForComponent(componentName, filter, serviceFilters);
  }

  /**
   * Add intent filter for given receiver.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void addIntentFilterForReceiver(ComponentName componentName, IntentFilter filter)
      throws NameNotFoundException {
    addIntentFilterForComponent(componentName, filter, receiverFilters);
  }

  /**
   * Add intent filter for given provider.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void addIntentFilterForProvider(ComponentName componentName, IntentFilter filter)
      throws NameNotFoundException {
    addIntentFilterForComponent(componentName, filter, providerFilters);
  }

  /**
   * Clears intent filters for given activity.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void clearIntentFilterForActivity(ComponentName componentName)
      throws NameNotFoundException {
    clearIntentFilterForComponent(componentName, activityFilters);
  }

  /**
   * Clears intent filters for given service.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void clearIntentFilterForService(ComponentName componentName)
      throws NameNotFoundException {
    clearIntentFilterForComponent(componentName, serviceFilters);
  }

  /**
   * Clears intent filters for given receiver.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void clearIntentFilterForReceiver(ComponentName componentName)
      throws NameNotFoundException {
    clearIntentFilterForComponent(componentName, receiverFilters);
  }

  /**
   * Clears intent filters for given provider.
   *
   * @throws NameNotFoundException if component with given name doesn't exist.
   */
  public void clearIntentFilterForProvider(ComponentName componentName)
      throws NameNotFoundException {
    clearIntentFilterForComponent(componentName, providerFilters);
  }

  private void addIntentFilterForComponent(
      ComponentName componentName,
      IntentFilter filter,
      Map<ComponentName, List<IntentFilter>> filterMap)
      throws NameNotFoundException {
    // Existing components should have an entry in respective filterMap.
    // It is OK to search over all filter maps, as it is impossible to have the same component name
    // being of two comopnent types (like activity and service at the same time).
    List<IntentFilter> filters = filterMap.get(componentName);
    if (filters != null) {
      filters.add(filter);
      return;
    }
    throw new NameNotFoundException(componentName + " doesn't exist");
  }

  private void clearIntentFilterForComponent(
      ComponentName componentName, Map<ComponentName, List<IntentFilter>> filterMap)
      throws NameNotFoundException {
    List<IntentFilter> filters = filterMap.get(componentName);
    if (filters != null) {
      filters.clear();
      return;
    }
    throw new NameNotFoundException(componentName + " doesn't exist");
  }

  private List<IntentFilter> getIntentFiltersForComponent(
      ComponentName componentName, Map<ComponentName, List<IntentFilter>> filterMap)
      throws NameNotFoundException {
    List<IntentFilter> filters = filterMap.get(componentName);
    if (filters != null) {
      return new ArrayList<>(filters);
    }
    throw new NameNotFoundException(componentName + " doesn't exist");
  }

  /**
   * Method to retrieve persistent preferred activities as set by {@link
   * android.app.admin.DevicePolicyManager#addPersistentPreferredActivity}.
   *
   * Works the same way as analogous {@link PackageManager#getPreferredActivities} for regular
   * preferred activities.
   */
  public int getPersistentPreferredActivities(
      List<IntentFilter> outFilters, List<ComponentName> outActivities, String packageName) {
    return getPreferredActivitiesInternal(
        outFilters, outActivities, packageName, persistentPreferredActivities);
  }

  protected static int getPreferredActivitiesInternal(
      List<IntentFilter> outFilters,
      List<ComponentName> outActivities,
      String packageName,
      SortedMap<ComponentName, List<IntentFilter>> preferredActivitiesMap) {
    SortedMap<ComponentName, List<IntentFilter>> preferredMap = preferredActivitiesMap;
    if (packageName != null) {
      preferredMap = mapForPackage(preferredActivitiesMap, packageName);
    }
    int result = 0;
    for (Entry<ComponentName, List<IntentFilter>> entry : preferredMap.entrySet()) {
      int filterCount = entry.getValue().size();
      result += filterCount;
      ComponentName[] componentNames = new ComponentName[filterCount];
      Arrays.fill(componentNames, entry.getKey());
      outActivities.addAll(asList(componentNames));
      outFilters.addAll(entry.getValue());
    }

    return result;
  }

  void clearPackagePersistentPreferredActivities(String packageName) {
    clearPackagePreferredActivitiesInternal(packageName, persistentPreferredActivities);
  }

  protected static void clearPackagePreferredActivitiesInternal(
      String packageName, SortedMap<ComponentName, List<IntentFilter>> preferredActivitiesMap) {
    mapForPackage(preferredActivitiesMap, packageName).clear();
  }

  void addPersistentPreferredActivity(IntentFilter filter, ComponentName activity) {
    addPreferredActivityInternal(filter, activity, persistentPreferredActivities);
  }

  protected static void addPreferredActivityInternal(
      IntentFilter filter,
      ComponentName activity,
      SortedMap<ComponentName, List<IntentFilter>> preferredActivitiesMap) {
    List<IntentFilter> filters = preferredActivitiesMap.get(activity);
    if (filters == null) {
      filters = new ArrayList<>();
      preferredActivitiesMap.put(activity, filters);
    }
    filters.add(filter);
  }

  protected static <V> SortedMap<ComponentName, V> mapForPackage(
      SortedMap<ComponentName, V> input, @Nullable String packageName) {
    if (packageName == null) {
      return input;
    }
    if (packageName == null) {
      return input;
    }
    return input.subMap(
        new ComponentName(packageName, ""), new ComponentName(packageName + " ", ""));
  }

  static boolean isComponentEnabled(@Nullable ComponentInfo componentInfo) {
    if (componentInfo == null) {
      return true;
    }
    if (componentInfo.applicationInfo == null
        || componentInfo.applicationInfo.packageName == null
        || componentInfo.name == null) {
      return componentInfo.enabled;
    }
    ComponentName name =
        new ComponentName(componentInfo.applicationInfo.packageName, componentInfo.name);
    ComponentState componentState = componentList.get(name);
    if (componentState == null
        || componentState.newState == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
      return componentInfo.enabled;
    }
    return componentState.newState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
  }

  /**
   * Returns the current {@link PackageSetting} of {@code packageName}.
   *
   * If {@code packageName} is not present in this {@link ShadowPackageManager}, this method will
   * return null.
   */
  public PackageSetting getPackageSetting(String packageName) {
    PackageSetting setting = packageSettings.get(packageName);
    return setting == null ? null : new PackageSetting(setting);
  }

  /**
   * If this method has been called with `true`, then in cases where many activities match a filter,
   * an activity chooser will be resolved instead of just the first pick.
   */
  public void setShouldShowActivityChooser(boolean shouldShowActivityChooser) {
    this.shouldShowActivityChooser = shouldShowActivityChooser;
  }

  /** Set value to be returned by {@link PackageManager#isSafeMode}. */
  public void setSafeMode(boolean safeMode) {
    ShadowPackageManager.safeMode = safeMode;
  }

  @Resetter
  public static void reset() {
    permissionRationaleMap.clear();
    systemAvailableFeatures.clear();
    systemSharedLibraryNames.clear();
    packageInfos.clear();
    packageArchiveInfo.clear();
    packageStatsMap.clear();
    packageInstallerMap.clear();
    packagesForUid.clear();
    uidForPackage.clear();
    namesForUid.clear();
    verificationResults.clear();
    verificationTimeoutExtension.clear();
    currentToCanonicalNames.clear();
    componentList.clear();
    drawableList.clear();
    applicationIcons.clear();
    unbadgedApplicationIcons.clear();
    systemFeatureList.clear();
    preferredActivities.clear();
    persistentPreferredActivities.clear();
    drawables.clear();
    applicationEnabledSettingMap.clear();
    extraPermissions.clear();
    permissionGroups.clear();
    resources.clear();
    resolveInfoForIntent.clear();
    deletedPackages.clear();
    pendingDeleteCallbacks.clear();
    hiddenPackages.clear();
    sequenceNumberChangedPackagesMap.clear();
    activityFilters.clear();
    serviceFilters.clear();
    providerFilters.clear();
    receiverFilters.clear();
    packageSettings.clear();
    safeMode = false;
  }
}


package org.robolectric.shadows;

import android.os.Build;
import android.os.SharedMemory;
import android.system.ErrnoException;
import android.system.OsConstants;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicReference;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.TempDirectory;

/**
 * A {@link SharedMemory} fake that uses a private temporary disk file for storage and Java's {@link
 * MappedByteBuffer} for the memory mappings.
 */
@Implements(value = SharedMemory.class,
    minSdk = Build.VERSION_CODES.O_MR1,
    /* not quite true, but this prevents a useless `shadowOf()` accessor showing
     * up, which would break people compiling against API 26 and earlier.
    */
    isInAndroidSdk = false
)
public class ShadowSharedMemory {
  private static final Map<FileDescriptor, File> filesByFd =
      Collections.synchronizedMap(new WeakHashMap<>());

  private static final AtomicReference<ErrnoException> fakeCreateException =
      new AtomicReference<>();

  @RealObject private SharedMemory realObject;

  /**
   * Only works on {@link SharedMemory} instances from {@link SharedMemory#create}.
   *
   * <p>"prot" is ignored -- all mappings are read/write.
   */
  @Implementation
  protected ByteBuffer map(int prot, int offset, int length) throws ErrnoException {
    ReflectionHelpers.callInstanceMethod(realObject, "checkOpen");
    File file = filesByFd.get(getRealFileDescriptor());
    if (file == null) {
      throw new IllegalStateException("SharedMemory from a parcel isn't yet implemented!");
    }

    try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {
      // It would be easy to support a MapMode.READ_ONLY type mapping as well except none of the
      // OsConstants fields are even initialized by robolectric and so "prot" is always zero!
      return randomAccessFile.getChannel().map(MapMode.READ_WRITE, offset, length);
    } catch (IOException e) {
      throw new ErrnoException(e.getMessage(), OsConstants.EIO, e);
    }
  }

  @Implementation
  protected static void unmap(ByteBuffer mappedBuf) throws ErrnoException {
    if (mappedBuf instanceof MappedByteBuffer) {
      // There's no API to clean up a MappedByteBuffer other than GC so we've got nothing to do!
    } else {
      throw new IllegalArgumentException(
          "ByteBuffer wasn't created by #map(int, int, int); can't unmap");
    }
  }

  @Implementation
  protected static FileDescriptor nCreate(String name, int size) throws ErrnoException {
    maybeThrow(fakeCreateException);

    TempDirectory tempDirectory = RuntimeEnvironment.getTempDirectory();

    try {
      // Give each instance its own private file.
      File sharedMemoryFile =
          Files.createTempFile(
                  tempDirectory.createIfNotExists("SharedMemory"), "shmem-" + name, ".tmp")
              .toFile();
      RandomAccessFile randomAccessFile = new RandomAccessFile(sharedMemoryFile, "rw");
      randomAccessFile.setLength(0);
      randomAccessFile.setLength(size);

      filesByFd.put(randomAccessFile.getFD(), sharedMemoryFile);
      return randomAccessFile.getFD();
    } catch (IOException e) {
      throw new RuntimeException("Unable to create file descriptior", e);
    }
  }

  @Implementation
  protected static int nGetSize(FileDescriptor fd) {
    return (int) filesByFd.get(fd).length();
  }

  private FileDescriptor getRealFileDescriptor() {
    return ReflectionHelpers.getField(realObject, "mFileDescriptor");
  }

  /**
   * Causes subsequent calls to {@link SharedMemory#create)} to throw the specified exception, if
   * non-null. Pass null to restore create to normal operation.
   */
  public static void setCreateShouldThrow(ErrnoException e) {
    fakeCreateException.set(e);
  }

  private static void maybeThrow(AtomicReference<ErrnoException> exceptionRef)
      throws ErrnoException {
    ErrnoException exception = exceptionRef.get();
    if (exception != null) {
      throw exception;
    }
  }
}


package org.robolectric.res.android;

import static org.robolectric.res.android.LocaleDataTables.LIKELY_SCRIPTS;
import static org.robolectric.res.android.LocaleDataTables.MAX_PARENT_DEPTH;
import static org.robolectric.res.android.LocaleDataTables.REPRESENTATIVE_LOCALES;
import static org.robolectric.res.android.LocaleDataTables.SCRIPT_CODES;
import static org.robolectric.res.android.LocaleDataTables.SCRIPT_PARENTS;

import java.util.Arrays;
import java.util.Map;

// transliterated from https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/LocaleData.cpp
public class LocaleData {

  private static int packLocale(final byte[] language, final byte[] region) {
    return ((language[0] & 0xff) << 24) | ((language[1] & 0xff) << 16) |
        ((region[0] & 0xff) << 8) | (region[1] & 0xff);
  }

  private static int dropRegion(int packed_locale) {
    return packed_locale & 0xFFFF0000;
  }

  private static boolean hasRegion(int packed_locale) {
    return (packed_locale & 0x0000FFFF) != 0;
  }

  static final int SCRIPT_LENGTH = 4;
  private static final int PACKED_ROOT = 0; // to represent the root locale

  private static int findParent(int packed_locale, final String script) {
    if (hasRegion(packed_locale)) {
      for (Map.Entry<String, Map<Integer, Integer>> entry : SCRIPT_PARENTS.entrySet()) {
        if (script.equals(entry.getKey())) {
          Map<Integer, Integer> map = entry.getValue();
          Integer lookup_result = map.get(packed_locale);
          if (lookup_result != null) {
            return lookup_result;
          }
          break;
        }
      }
      return dropRegion(packed_locale);
    }
    return PACKED_ROOT;
  }

  // Find the ancestors of a locale, and fill 'out' with it (assumes out has enough
  // space). If any of the members of stop_list was seen, write it in the
  // output but stop afterwards.
  //
  // This also outputs the index of the last written ancestor in the stop_list
  // to stop_list_index, which will be -1 if it is not found in the stop_list.
  //
  // Returns the number of ancestors written in the output, which is always
  // at least one.
  //
  // (If 'out' is null, we do everything the same way but we simply don't write
  // any results in 'out'.)
  static int findAncestors(int[] out, Ref<Long> stop_list_index,
      int packed_locale, final String script,
      final int[] stop_list, int stop_set_length) {
    int ancestor = packed_locale;
    int count = 0;
    do {
      if (out != null) {
        out[count] = ancestor;
      }
      count++;
      for (int i = 0; i < stop_set_length; i++) {
        if (stop_list[i] == ancestor) {
          stop_list_index.set((long) i);
          return count;
        }
      }
      ancestor = findParent(ancestor, script);
    } while (ancestor != PACKED_ROOT);
    stop_list_index.set((long) -1);
    return count;
  }

  static int findDistance(int supported,
      final String script,
      final int[] request_ancestors,
      int request_ancestors_count) {
    final Ref<Long> request_ancestors_indexRef = new Ref<>(null);
    final int supported_ancestor_count = findAncestors(
        null, request_ancestors_indexRef,
        supported, script,
        request_ancestors, request_ancestors_count);
    // Since both locales share the same root, there will always be a shared
    // ancestor, so the distance in the parent tree is the sum of the distance
    // of 'supported' to the lowest common ancestor (number of ancestors
    // written for 'supported' minus 1) plus the distance of 'request' to the
    // lowest common ancestor (the index of the ancestor in request_ancestors).
    return (int) (supported_ancestor_count + request_ancestors_indexRef.get() - 1);
  }

  static boolean isRepresentative(int language_and_region, final String script) {
    final long packed_locale = (
        (((long) language_and_region) << 32) |
            (((long) script.charAt(0) & 0xff) << 24) |
            (((long) script.charAt(1) & 0xff) << 16) |
            (((long) script.charAt(2) & 0xff) << 8) |
            ((long) script.charAt(3) & 0xff));
    return (REPRESENTATIVE_LOCALES.contains(packed_locale));
  }

  private static final int US_SPANISH = 0x65735553; // es-US
  private static final int MEXICAN_SPANISH = 0x65734D58; // es-MX
  private static final int LATIN_AMERICAN_SPANISH = 0x6573A424; // es-419

  // The two locales es-US and es-MX are treated as special fallbacks for es-419.
// If there is no es-419, they are considered its equivalent.
  private static boolean isSpecialSpanish(int language_and_region) {
    return (language_and_region == US_SPANISH || language_and_region == MEXICAN_SPANISH);
  }

  static int localeDataCompareRegions(
      final byte[] left_region, final byte[] right_region,
      final byte[] requested_language, final String requested_script,
      final byte[] requested_region) {
    if (left_region[0] == right_region[0] && left_region[1] == right_region[1]) {
      return 0;
    }
    int left = packLocale(requested_language, left_region);
    int right = packLocale(requested_language, right_region);
    final int request = packLocale(requested_language, requested_region);

    // If one and only one of the two locales is a special Spanish locale, we
    // replace it with es-419. We don't do the replacement if the other locale
    // is already es-419, or both locales are special Spanish locales (when
    // es-US is being compared to es-MX).
    final boolean leftIsSpecialSpanish = isSpecialSpanish(left);
    final boolean rightIsSpecialSpanish = isSpecialSpanish(right);
    if (leftIsSpecialSpanish && !rightIsSpecialSpanish && right != LATIN_AMERICAN_SPANISH) {
      left = LATIN_AMERICAN_SPANISH;
    } else if (rightIsSpecialSpanish && !leftIsSpecialSpanish && left != LATIN_AMERICAN_SPANISH) {
      right = LATIN_AMERICAN_SPANISH;
    }

    int[] request_ancestors = new int[MAX_PARENT_DEPTH + 1];
    final Ref<Long> left_right_indexRef = new Ref<Long>(null);
    // Find the parents of the request, but stop as soon as we saw left or right
    final int left_and_right[] = {left, right};
    final int ancestor_count = findAncestors(
        request_ancestors, left_right_indexRef,
        request, requested_script,
        left_and_right, sizeof(left_and_right));
    if (left_right_indexRef.get() == 0) { // We saw left earlier
      return 1;
    }
    if (left_right_indexRef.get() == 1) { // We saw right earlier
      return -1;
    }
    // If we are here, neither left nor right are an ancestor of the
    // request. This means that all the ancestors have been computed and
    // the last ancestor is just the language by itself. We will use the
    // distance in the parent tree for determining the better match.
    final int left_distance = findDistance(
        left, requested_script, request_ancestors, ancestor_count);
    final int right_distance = findDistance(
        right, requested_script, request_ancestors, ancestor_count);
    if (left_distance != right_distance) {
      return (int) right_distance - (int) left_distance; // smaller distance is better
    }
    // If we are here, left and right are equidistant from the request. We will
    // try and see if any of them is a representative locale.
    final boolean left_is_representative = isRepresentative(left, requested_script);
    final boolean right_is_representative = isRepresentative(right, requested_script);
    if (left_is_representative != right_is_representative) {
      return (left_is_representative ? 1 : 0) - (right_is_representative ? 1 : 0);
    }
    // We have no way of figuring out which locale is a better match. For
    // the sake of stability, we consider the locale with the lower region
    // code (in dictionary order) better, with two-letter codes before
    // three-digit codes (since two-letter codes are more specific).
    return right - left;
  }

  static void localeDataComputeScript(byte[] out, final byte[] language, final byte[] region) {
    if (language[0] == '\0') {
//      memset(out, '\0', SCRIPT_LENGTH);
      Arrays.fill(out, (byte) 0);
      return;
    }
    int lookup_key = packLocale(language, region);
    Byte lookup_result = LIKELY_SCRIPTS.get(lookup_key);
    if (lookup_result == null) {
      // We couldn't find the locale. Let's try without the region
      if (region[0] != '\0') {
        lookup_key = dropRegion(lookup_key);
        lookup_result = LIKELY_SCRIPTS.get(lookup_key);
        if (lookup_result != null) {
//          memcpy(out, SCRIPT_CODES[lookup_result.second], SCRIPT_LENGTH);
          System.arraycopy(SCRIPT_CODES[lookup_result], 0, out, 0, SCRIPT_LENGTH);
          return;
        }
      }
      // We don't know anything about the locale
//      memset(out, '\0', SCRIPT_LENGTH);
      Arrays.fill(out, (byte) 0);
      return;
    } else {
      // We found the locale.
//      memcpy(out, SCRIPT_CODES[lookup_result.second], SCRIPT_LENGTH);
      System.arraycopy(SCRIPT_CODES[lookup_result], 0, out, 0, SCRIPT_LENGTH);
    }
  }

  static final int[] ENGLISH_STOP_LIST = {
      0x656E0000, // en
      0x656E8400, // en-001
  };

  static final byte[] ENGLISH_CHARS = {'e', 'n'};

  static final String LATIN_CHARS = "Latn";

  static boolean localeDataIsCloseToUsEnglish(final byte[] region) {
    final int locale = packLocale(ENGLISH_CHARS, region);
    final Ref<Long> stop_list_indexRef = new Ref<>(null);
    findAncestors(null, stop_list_indexRef, locale, LATIN_CHARS, ENGLISH_STOP_LIST, 2);
    // A locale is like US English if we see "en" before "en-001" in its ancestor list.
    return stop_list_indexRef.get() == 0; // 'en' is first in ENGLISH_STOP_LIST
  }

  private static int sizeof(int[] array) {
    return array.length;
  }

}


package org.robolectric.internal.dependency;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

public class LocalDependencyResolver implements DependencyResolver {
  private File offlineJarDir;

  public LocalDependencyResolver(File offlineJarDir) {
    super();
    this.offlineJarDir = offlineJarDir;
  }

  @Override
  public URL getLocalArtifactUrl(DependencyJar dependency) {
    StringBuilder filenameBuilder = new StringBuilder();
    filenameBuilder.append(dependency.getArtifactId())
        .append("-")
        .append(dependency.getVersion());

    if (dependency.getClassifier() != null) {
      filenameBuilder.append("-")
          .append(dependency.getClassifier());
    }

    filenameBuilder.append(".")
        .append(dependency.getType());

    return fileToUrl(validateFile(new File(offlineJarDir, filenameBuilder.toString())));
  }

  /**
   * Validates {@code file} is an existing file that is readable.
   *
   * @param file the File to test
   * @return the provided file, if all validation passes
   * @throws IllegalArgumentException if validation fails
   */
  private static File validateFile(File file) throws IllegalArgumentException {
    if (!file.isFile()) {
      throw new IllegalArgumentException("Path is not a file: " + file);
    }
    if (!file.canRead()) {
      throw new IllegalArgumentException("Unable to read file: " + file);
    }
    return file;
  }

  /** Returns the given file as a {@link URL}. */
  private static URL fileToUrl(File file) {
    try {
      return file.toURI().toURL();
    } catch (MalformedURLException e) {
      throw new IllegalArgumentException(
          String.format("File \"%s\" cannot be represented as a URL: %s", file, e));
    }
  }
}


package org.robolectric.shadows;

import android.annotation.Nullable;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraCharacteristics.Key;
import android.os.Build.VERSION_CODES;
import com.google.common.base.Preconditions;
import java.util.HashMap;
import java.util.Map;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;

@Implements(value = CameraCharacteristics.class, minSdk = VERSION_CODES.LOLLIPOP)
public class ShadowCameraCharacteristics {

  private final Map<Key<?>, Object> charactersKeyToValue = new HashMap<>();

  /** Convenience method which returns a new instance of {@link CameraCharacteristics}. */
  public static CameraCharacteristics newCameraCharacteristics() {
    return ReflectionHelpers.callConstructor(CameraCharacteristics.class);
  }

  @Implementation
  @Nullable
  protected <T> T get(Key<T> key) {
    return (T) charactersKeyToValue.get(key);
  }

  /**
   * Sets the value for a given key.
   *
   * @throws IllegalArgumentException if there's an existing value for the key.
   */
  public <T> void set(Key<T> key, Object value) {
    Preconditions.checkArgument(!charactersKeyToValue.containsKey(key));
    charactersKeyToValue.put(key, value);
  }
}


package org.robolectric.internal.bytecode;

import static org.objectweb.asm.Type.ARRAY;
import static org.objectweb.asm.Type.OBJECT;
import static org.objectweb.asm.Type.VOID;

import java.util.ListIterator;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;

public class OldClassInstrumentor extends ClassInstrumentor {
  private static final Type PLAN_TYPE = Type.getType(ClassHandler.Plan.class);
  static final Type THROWABLE_TYPE = Type.getType(Throwable.class);
  static final Type ROBOLECTRIC_INTERNALS_TYPE = Type.getType(RobolectricInternals.class);
  private static final Method INITIALIZING_METHOD = new Method("initializing", "(Ljava/lang/Object;)Ljava/lang/Object;");
  private static final Method METHOD_INVOKED_METHOD = new Method("methodInvoked", "(Ljava/lang/String;ZLjava/lang/Class;)L" + PLAN_TYPE.getInternalName() + ";");
  private static final Method PLAN_RUN_METHOD = new Method("run", OBJECT_TYPE, new Type[]{OBJECT_TYPE, Type.getType(Object[].class)});
  static final Method HANDLE_EXCEPTION_METHOD = new Method("cleanStackTrace", THROWABLE_TYPE, new Type[]{THROWABLE_TYPE});
  private static final String DIRECT_OBJECT_MARKER_TYPE_DESC = Type.getObjectType(DirectObjectMarker.class.getName().replace('.', '/')).getDescriptor();

  public OldClassInstrumentor(ClassInstrumentor.Decorator decorator) {
    super(decorator);
  }

  /**
   * Generates code like this:
   * ```java
   * public ThisClass(DirectObjectMarker dom, ThisClass domInstance) {
   *   super(dom, domInstance);
   *   __robo_data__ = domInstance;
   * }
   * ```
   */
  @Override
  protected void addDirectCallConstructor(MutableClass mutableClass) {
    MethodNode directCallConstructor = new MethodNode(Opcodes.ACC_PUBLIC,
        "<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + mutableClass.classType.getDescriptor() + ")V", null, null);
    RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(directCallConstructor);
    generator.loadThis();
    String superName = mutableClass.classNode.superName;
    if (superName.equals("java/lang/Object")) {
      generator.visitMethodInsn(Opcodes.INVOKESPECIAL, superName, "<init>", "()V", false);
    } else {
      generator.loadArgs();
      generator.visitMethodInsn(Opcodes.INVOKESPECIAL, superName,
          "<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + "L" + superName + ";)V", false);
    }
    generator.loadThis();
    generator.loadArg(1);
    generator.putField(mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);
    generator.returnValue();
    mutableClass.addMethod(directCallConstructor);
  }

  @Override
  protected void writeCallToInitializing(MutableClass mutableClass,
      RobolectricGeneratorAdapter generator) {
    generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, INITIALIZING_METHOD);
  }

  @Override
  protected void generateClassHandlerCall(MutableClass mutableClass, MethodNode originalMethod,
      String originalMethodName, RobolectricGeneratorAdapter generator) {
    generateCallToClassHandler(mutableClass, originalMethod, originalMethodName, generator);
  }

  /**
   * Generates codelike this:
   * ```java
   * // decorator-specific code...
   *
   * Plan plan = RobolectricInternals.methodInvoked(
   *     "pkg/ThisClass/thisMethod(Ljava/lang/String;Z)V", isStatic, ThisClass.class);
   * if (plan != null) {
   *   try {
   *     return plan.run(this, args);
   *   } catch (Throwable t) {
   *     throw RobolectricInternals.cleanStackTrace(t);
   *   }
   * } else {
   *   return $$robo$$thisMethod(*args);
   * }
   * ```
   */
  private void generateCallToClassHandler(MutableClass mutableClass, MethodNode originalMethod,
      String originalMethodName, RobolectricGeneratorAdapter generator) {
    decorator.decorateMethodPreClassHandler(mutableClass, originalMethod, originalMethodName, generator);

    int planLocalVar = generator.newLocal(PLAN_TYPE);
    int exceptionLocalVar = generator.newLocal(THROWABLE_TYPE);
    Label directCall = new Label();
    Label doReturn = new Label();

    // prepare for call to classHandler.methodInvoked(String signature, boolean isStatic)
    generator.push(mutableClass.classType.getInternalName() + "/" + originalMethodName + originalMethod.desc);
    generator.push(generator.isStatic());
    generator.push(mutableClass.classType);                                         // my class
    generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, METHOD_INVOKED_METHOD);
    generator.storeLocal(planLocalVar);

    generator.loadLocal(planLocalVar); // plan
    generator.ifNull(directCall);

    // prepare for call to plan.run(Object instance, Object[] params)
    TryCatch tryCatchForHandler = generator.tryStart(THROWABLE_TYPE);
    generator.loadLocal(planLocalVar); // plan
    generator.loadThisOrNull();        // instance
    generator.loadArgArray();          // params
    generator.invokeInterface(PLAN_TYPE, PLAN_RUN_METHOD);

    Type returnType = generator.getReturnType();
    int sort = returnType.getSort();
    switch (sort) {
      case VOID:
        generator.pop();
        break;
      case OBJECT:
        /* falls through */
      case ARRAY:
        generator.checkCast(returnType);
        break;
      default:
        int unboxLocalVar = generator.newLocal(OBJECT_TYPE);
        generator.storeLocal(unboxLocalVar);
        generator.loadLocal(unboxLocalVar);
        Label notNull = generator.newLabel();
        Label afterward = generator.newLabel();
        generator.ifNonNull(notNull);
        generator.pushDefaultReturnValueToStack(returnType); // return zero, false, whatever
        generator.goTo(afterward);

        generator.mark(notNull);
        generator.loadLocal(unboxLocalVar);
        generator.unbox(returnType);
        generator.mark(afterward);
        break;
    }
    tryCatchForHandler.end();
    generator.goTo(doReturn);

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

    if (!originalMethod.name.equals("<init>")) {
      generator.mark(directCall);
      TryCatch tryCatchForDirect = generator.tryStart(THROWABLE_TYPE);
      generator.invokeMethod(mutableClass.classType.getInternalName(), originalMethod.name, originalMethod.desc);
      tryCatchForDirect.end();
      generator.returnValue();

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

    generator.mark(doReturn);
    generator.returnValue();
  }

  /**
   * Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL
   * Opcode, depending if the invokedynamic bytecode instruction is available (Java 7+).
   */
  @Override
  protected void interceptInvokeVirtualMethod(MutableClass mutableClass,
      ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
    interceptInvokeVirtualMethodWithoutInvokeDynamic(mutableClass, instructions, targetMethod);
  }

  /**
   * Intercepts the method without using the invokedynamic bytecode instruction.
   * Should be called through interceptInvokeVirtualMethod, not directly.
   */
  private void interceptInvokeVirtualMethodWithoutInvokeDynamic(MutableClass mutableClass,
      ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
    boolean isStatic = targetMethod.getOpcode() == Opcodes.INVOKESTATIC;

    instructions.remove(); // remove the method invocation

    Type[] argumentTypes = Type.getArgumentTypes(targetMethod.desc);

    instructions.add(new LdcInsnNode(argumentTypes.length));
    instructions.add(new TypeInsnNode(Opcodes.ANEWARRAY, "java/lang/Object"));

    // first, move any arguments into an Object[] in reverse order
    for (int i = argumentTypes.length - 1; i >= 0; i--) {
      Type type = argumentTypes[i];
      int argWidth = type.getSize();

      if (argWidth == 1) {                               // A B C []
        instructions.add(new InsnNode(Opcodes.DUP_X1));  // A B [] C []
        instructions.add(new InsnNode(Opcodes.SWAP));    // A B [] [] C
        instructions.add(new LdcInsnNode(i));            // A B [] [] C 2
        instructions.add(new InsnNode(Opcodes.SWAP));    // A B [] [] 2 C
        box(type, instructions);                         // A B [] [] 2 (C)
        instructions.add(new InsnNode(Opcodes.AASTORE)); // A B [(C)]
      } else if (argWidth == 2) {                        // A B _C_ []
        instructions.add(new InsnNode(Opcodes.DUP_X2));  // A B [] _C_ []
        instructions.add(new InsnNode(Opcodes.DUP_X2));  // A B [] [] _C_ []
        instructions.add(new InsnNode(Opcodes.POP));     // A B [] [] _C_
        box(type, instructions);                         // A B [] [] (C)
        instructions.add(new LdcInsnNode(i));            // A B [] [] (C) 2
        instructions.add(new InsnNode(Opcodes.SWAP));    // A B [] [] 2 (C)
        instructions.add(new InsnNode(Opcodes.AASTORE)); // A B [(C)]
      }
    }

    if (isStatic) { // []
      instructions.add(new InsnNode(Opcodes.ACONST_NULL)); // [] null
      instructions.add(new InsnNode(Opcodes.SWAP));        // null []
    }

    // instance []
    instructions.add(new LdcInsnNode(targetMethod.owner + "/" + targetMethod.name + targetMethod.desc)); // target method signature
    // instance [] signature
    instructions.add(new InsnNode(Opcodes.DUP_X2));       // signature instance [] signature
    instructions.add(new InsnNode(Opcodes.POP));          // signature instance []

    instructions.add(new LdcInsnNode(mutableClass.classType)); // signature instance [] class
    instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
        Type.getType(RobolectricInternals.class).getInternalName(), "intercept",
        "(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;",
        false));

    final Type returnType = Type.getReturnType(targetMethod.desc);
    switch (returnType.getSort()) {
      case ARRAY:
        /* falls through */
      case OBJECT:
        String remappedType = mutableClass.config.mappedTypeName(returnType.getInternalName());
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, remappedType));
        break;
      case VOID:
        instructions.add(new InsnNode(Opcodes.POP));
        break;
      case Type.LONG:
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(Long.class)));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Long.class), "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false));
        break;
      case Type.FLOAT:
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(Float.class)));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Float.class), "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false));
        break;
      case Type.DOUBLE:
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(Double.class)));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Double.class), "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false));
        break;
      case Type.BOOLEAN:
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(Boolean.class)));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Boolean.class), "booleanValue", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false));
        break;
      case Type.INT:
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(Integer.class)));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Integer.class), "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false));
        break;
      case Type.SHORT:
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(Short.class)));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Short.class), "shortValue", Type.getMethodDescriptor(Type.SHORT_TYPE), false));
        break;
      case Type.BYTE:
        instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(Byte.class)));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Byte.class), "byteValue", Type.getMethodDescriptor(Type.BYTE_TYPE), false));
        break;
      default:
        throw new RuntimeException("Not implemented: " + getClass().getName() + " cannot intercept methods with return type " + returnType.getClassName());
    }
  }

  static void box(final Type type, ListIterator<AbstractInsnNode> instructions) {
    if (type.getSort() == OBJECT || type.getSort() == ARRAY) {
      return;
    }

    if (Type.VOID_TYPE.equals(type)) {
      instructions.add(new InsnNode(Opcodes.ACONST_NULL));
    } else {
      Type boxed = getBoxedType(type);
      instructions.add(new TypeInsnNode(Opcodes.NEW, boxed.getInternalName()));
      if (type.getSize() == 2) {
        // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
        instructions.add(new InsnNode(Opcodes.DUP_X2));
        instructions.add(new InsnNode(Opcodes.DUP_X2));
        instructions.add(new InsnNode(Opcodes.POP));
      } else {
        // p -> po -> opo -> oop -> o
        instructions.add(new InsnNode(Opcodes.DUP_X1));
        instructions.add(new InsnNode(Opcodes.SWAP));
      }
      instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, boxed.getInternalName(),
          "<init>", "(" + type.getDescriptor() + ")V", false));
    }
  }

  private static Type getBoxedType(final Type type) {
    switch (type.getSort()) {
      case Type.BYTE:
        return Type.getObjectType("java/lang/Byte");
      case Type.BOOLEAN:
        return Type.getObjectType("java/lang/Boolean");
      case Type.SHORT:
        return Type.getObjectType("java/lang/Short");
      case Type.CHAR:
        return Type.getObjectType("java/lang/Character");
      case Type.INT:
        return Type.getObjectType("java/lang/Integer");
      case Type.FLOAT:
        return Type.getObjectType("java/lang/Float");
      case Type.LONG:
        return Type.getObjectType("java/lang/Long");
      case Type.DOUBLE:
        return Type.getObjectType("java/lang/Double");
      default:
        // no boxing required
        return type;
    }
  }
}


package org.robolectric.shadows;

import android.net.NetworkInfo;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;

@Implements(NetworkInfo.class)
public class ShadowNetworkInfo {
  private boolean isAvailable;
  private NetworkInfo.State state;
  private int connectionType;
  private int connectionSubType;
  private NetworkInfo.DetailedState detailedState;

  @Implementation
  protected static void __staticInitializer__() {}

  /**
   * @deprecated use {@link #newInstance(NetworkInfo.DetailedState, int, int, boolean,
   *     NetworkInfo.State)} instead
   */
  @Deprecated
  public static NetworkInfo newInstance(
      NetworkInfo.DetailedState detailedState,
      int type,
      int subType,
      boolean isAvailable,
      boolean isConnected) {
    return newInstance(
        detailedState,
        type,
        subType,
        isAvailable,
        isConnected ? NetworkInfo.State.CONNECTED : NetworkInfo.State.DISCONNECTED);
  }

  /** Allows developers to create a {@link NetworkInfo} instance for testing. */
  public static NetworkInfo newInstance(
      NetworkInfo.DetailedState detailedState,
      int type,
      int subType,
      boolean isAvailable,
      NetworkInfo.State state) {
    NetworkInfo networkInfo = Shadow.newInstanceOf(NetworkInfo.class);
    final ShadowNetworkInfo info = Shadow.extract(networkInfo);
    info.setConnectionType(type);
    info.setSubType(subType);
    info.setDetailedState(detailedState);
    info.setAvailableStatus(isAvailable);
    info.setConnectionStatus(state);
    return networkInfo;
  }

  @Implementation
  protected boolean isConnected() {
    return state == NetworkInfo.State.CONNECTED;
  }

  @Implementation
  protected boolean isConnectedOrConnecting() {
    return isConnected() || state == NetworkInfo.State.CONNECTING;
  }

  @Implementation
  protected NetworkInfo.State getState() {
    return state;
  }

  @Implementation
  protected NetworkInfo.DetailedState getDetailedState() {
    return detailedState;
  }

  @Implementation
  protected int getType() {
    return connectionType;
  }

  @Implementation
  protected int getSubtype() {
    return connectionSubType;
  }

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

  /**
   * Sets up the return value of {@link #isAvailable()}.
   *
   * @param isAvailable the value that {@link #isAvailable()} will return.
   */
  public void setAvailableStatus(boolean isAvailable) {
    this.isAvailable = isAvailable;
  }

  /**
   * Sets up the return value of {@link #isConnectedOrConnecting()}, {@link #isConnected()}, and
   * {@link #getState()}. If the input is true, state will be {@link NetworkInfo.State#CONNECTED},
   * else it will be {@link NetworkInfo.State#DISCONNECTED}.
   *
   * @param isConnected the value that {@link #isConnectedOrConnecting()} and {@link #isConnected()}
   *     will return.
   * @deprecated use {@link #setConnectionStatus(NetworkInfo.State)} instead
   */
  @Deprecated
  public void setConnectionStatus(boolean isConnected) {
    setConnectionStatus(isConnected ? NetworkInfo.State.CONNECTED : NetworkInfo.State.DISCONNECTED);
  }

  /**
   * Sets up the return value of {@link #getState()}.
   *
   * @param state the value that {@link #getState()} will return.
   */
  public void setConnectionStatus(NetworkInfo.State state) {
    this.state = state;
  }

  /**
   * Sets up the return value of {@link #getType()}.
   *
   * @param connectionType the value that {@link #getType()} will return.
   */
  public void setConnectionType(int connectionType) {
    this.connectionType = connectionType;
  }

  public void setSubType(int subType) {
    this.connectionSubType = subType;
  }

  public void setDetailedState(NetworkInfo.DetailedState detailedState) {
    this.detailedState = detailedState;
  }
}