package org.robolectric.shadows;

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

import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.location.Criteria;
import android.location.GpsStatus.Listener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.provider.Settings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;

@Implements(LocationManager.class)
public class ShadowLocationManager {
  @RealObject private LocationManager realLocationManager;

  private final Map<String, LocationProviderEntry> providersEnabled = new LinkedHashMap<>();
  private final Map<String, Location> lastKnownLocations = new HashMap<>();
  private final Map<PendingIntent, Criteria> requestLocationUdpateCriteriaPendingIntents = new HashMap<>();
  private final Map<PendingIntent, String> requestLocationUdpateProviderPendingIntents = new HashMap<>();
  private final ArrayList<LocationListener> removedLocationListeners = new ArrayList<>();

  private final ArrayList<Listener> gpsStatusListeners = new ArrayList<>();
  private Criteria lastBestProviderCriteria;
  private boolean lastBestProviderEnabled;
  private String bestEnabledProvider, bestDisabledProvider;

  /** Location listeners along with metadata on when they should be fired. */
  private static final class ListenerRegistration {
    final long minTime;
    final float minDistance;
    final LocationListener listener;
    final String provider;
    Location lastSeenLocation;
    long lastSeenTime;

    ListenerRegistration(String provider, long minTime, float minDistance, Location locationAtCreation,
               LocationListener listener) {
      this.provider = provider;
      this.minTime = minTime;
      this.minDistance = minDistance;
      this.lastSeenTime = locationAtCreation == null ? 0 : locationAtCreation.getTime();
      this.lastSeenLocation = locationAtCreation;
      this.listener = listener;
    }
  }

  /** Mapped by provider. */
  private final Map<String, List<ListenerRegistration>> locationListeners =
      new HashMap<>();

  @Implementation
  protected boolean isProviderEnabled(String provider) {
    LocationProviderEntry map = providersEnabled.get(provider);
    if (map != null) {
      Boolean isEnabled = map.getKey();
      return isEnabled == null ? true : isEnabled;
    }
    return false;
  }

  @Implementation
  protected List<String> getAllProviders() {
    Set<String> allKnownProviders = new LinkedHashSet<>(providersEnabled.keySet());
    allKnownProviders.add(LocationManager.GPS_PROVIDER);
    allKnownProviders.add(LocationManager.NETWORK_PROVIDER);
    allKnownProviders.add(LocationManager.PASSIVE_PROVIDER);

    return new ArrayList<>(allKnownProviders);
  }

  /**
   * Sets the value to return from {@link #isProviderEnabled(String)} for the given {@code provider}
   *
   * @param provider
   *            name of the provider whose status to set
   * @param isEnabled
   *            whether that provider should appear enabled
   */
  public void setProviderEnabled(String provider, boolean isEnabled) {
    setProviderEnabled(provider, isEnabled, null);
  }

  public void setProviderEnabled(String provider, boolean isEnabled, List<Criteria> criteria) {
    LocationProviderEntry providerEntry = providersEnabled.get(provider);
    if (providerEntry == null) {
      providerEntry = new LocationProviderEntry();
    }
    providerEntry.enabled = isEnabled;
    providerEntry.criteria = criteria;
    providersEnabled.put(provider, providerEntry);
    List<LocationListener> locationUpdateListeners = new ArrayList<>(getRequestLocationUpdateListeners());
    for (LocationListener locationUpdateListener : locationUpdateListeners) {
      if (isEnabled) {
        locationUpdateListener.onProviderEnabled(provider);
      } else {
        locationUpdateListener.onProviderDisabled(provider);
      }
    }
    // Send intent to notify about provider status
    final Intent intent = new Intent();
    intent.putExtra(LocationManager.KEY_PROVIDER_ENABLED, isEnabled);
    getContext().sendBroadcast(intent);
    Set<PendingIntent> requestLocationUdpatePendingIntentSet = requestLocationUdpateCriteriaPendingIntents
        .keySet();
    for (PendingIntent requestLocationUdpatePendingIntent : requestLocationUdpatePendingIntentSet) {
      try {
        requestLocationUdpatePendingIntent.send();
      } catch (CanceledException e) {
        requestLocationUdpateCriteriaPendingIntents
            .remove(requestLocationUdpatePendingIntent);
      }
    }
    // if this provider gets disabled and it was the best active provider, then it's not anymore
    if (provider.equals(bestEnabledProvider) && !isEnabled) {
      bestEnabledProvider = null;
    }
  }

  @Implementation
  protected List<String> getProviders(boolean enabledOnly) {
    ArrayList<String> enabledProviders = new ArrayList<>();
    for (String provider : getAllProviders()) {
      if (!enabledOnly || providersEnabled.get(provider) != null) {
        enabledProviders.add(provider);
      }
    }
    return enabledProviders;
  }

  @Implementation
  protected Location getLastKnownLocation(String provider) {
    return lastKnownLocations.get(provider);
  }

  @Implementation
  protected boolean addGpsStatusListener(Listener listener) {
    if (!gpsStatusListeners.contains(listener)) {
      gpsStatusListeners.add(listener);
    }
    return true;
  }

  @Implementation
  protected void removeGpsStatusListener(Listener listener) {
    gpsStatusListeners.remove(listener);
  }

  @Implementation
  protected String getBestProvider(Criteria criteria, boolean enabled) {
    lastBestProviderCriteria = criteria;
    lastBestProviderEnabled = enabled;

    if (criteria == null) {
      return getBestProviderWithNoCriteria(enabled);
    }

    return getBestProviderWithCriteria(criteria, enabled);
  }

  private String getBestProviderWithCriteria(Criteria criteria, boolean enabled) {
    List<String> providers = getProviders(enabled);
    int powerRequirement = criteria.getPowerRequirement();
    int accuracy = criteria.getAccuracy();
    for (String provider : providers) {
      LocationProviderEntry locationProviderEntry = providersEnabled.get(provider);
      if (locationProviderEntry == null) {
        continue;
      }
      List<Criteria> criteriaList = locationProviderEntry.getValue();
      if (criteriaList == null) {
        continue;
      }
      for (Criteria criteriaListItem : criteriaList) {
        if (criteria.equals(criteriaListItem)) {
          return provider;
        } else if (criteriaListItem.getAccuracy() == accuracy) {
          return provider;
        } else if (criteriaListItem.getPowerRequirement() == powerRequirement) {
          return provider;
        }
      }
    }
    // TODO: these conditions are incomplete
    for (String provider : providers) {
      if (provider.equals(LocationManager.NETWORK_PROVIDER) && (accuracy == Criteria.ACCURACY_COARSE || powerRequirement == Criteria.POWER_LOW)) {
        return provider;
      } else if (provider.equals(LocationManager.GPS_PROVIDER) && accuracy == Criteria.ACCURACY_FINE && powerRequirement != Criteria.POWER_LOW) {
        return provider;
      }
    }

    // No enabled provider found with the desired criteria, then return the the first registered provider(?)
    return providers.isEmpty()? null : providers.get(0);
  }

  private String getBestProviderWithNoCriteria(boolean enabled) {
    List<String> providers = getProviders(enabled);

    if (enabled && bestEnabledProvider != null) {
      return bestEnabledProvider;
    } else if (bestDisabledProvider != null) {
      return bestDisabledProvider;
    } else if (providers.contains(LocationManager.GPS_PROVIDER)) {
      return LocationManager.GPS_PROVIDER;
    } else if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
      return LocationManager.NETWORK_PROVIDER;
    }
    return null;
  }

  public void setLocationEnabled(boolean enabled) {
    setLocationEnabledForUser(enabled, Process.myUserHandle());
  }

  // @SystemApi
  @Implementation(minSdk = P)
  protected void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
    Settings.Secure.putInt(
        getContext().getContentResolver(),
        Settings.Secure.LOCATION_MODE,
        enabled ? Settings.Secure.LOCATION_MODE_HIGH_ACCURACY : Settings.Secure.LOCATION_MODE_OFF);
  }

  // @SystemApi
  @Implementation(minSdk = P)
  protected boolean isLocationEnabledForUser(UserHandle userHandle) {
    return Settings.Secure.getInt(
            getContext().getContentResolver(),
            Settings.Secure.LOCATION_MODE,
            Settings.Secure.LOCATION_MODE_OFF)
        != Settings.Secure.LOCATION_MODE_OFF;
  }

  @Implementation
  protected void requestLocationUpdates(
      String provider, long minTime, float minDistance, LocationListener listener) {
    addLocationListener(provider, listener, minTime, minDistance);
  }

  private void addLocationListener(String provider, LocationListener listener, long minTime, float minDistance) {
    List<ListenerRegistration> providerListeners = locationListeners.get(provider);
    if (providerListeners == null) {
      providerListeners = new ArrayList<>();
      locationListeners.put(provider, providerListeners);
    }
    removeDuplicates(listener, providerListeners);
    providerListeners.add(new ListenerRegistration(provider,
        minTime, minDistance, copyOf(getLastKnownLocation(provider)), listener));

  }

  private void removeDuplicates(LocationListener listener,
      List<ListenerRegistration> providerListeners) {
    final Iterator<ListenerRegistration> iterator = providerListeners.iterator();
    while (iterator.hasNext()) {
      if (iterator.next().listener.equals(listener)) {
        iterator.remove();
      }
    }
  }

  @Implementation
  protected void requestLocationUpdates(
      String provider, long minTime, float minDistance, LocationListener listener, Looper looper) {
    addLocationListener(provider, listener, minTime, minDistance);
  }

  @Implementation
  protected void requestLocationUpdates(
      long minTime, float minDistance, Criteria criteria, PendingIntent pendingIntent) {
    if (pendingIntent == null) {
      throw new IllegalStateException("Intent must not be null");
    }
    if (getBestProvider(criteria, true) == null) {
      throw new IllegalArgumentException("no providers found for criteria");
    }
    requestLocationUdpateCriteriaPendingIntents.put(pendingIntent, criteria);
  }

  @Implementation
  protected void requestLocationUpdates(
      String provider, long minTime, float minDistance, PendingIntent pendingIntent) {
    if (pendingIntent == null) {
      throw new IllegalStateException("Intent must not be null");
    }
    if (!providersEnabled.containsKey(provider)) {
      throw new IllegalArgumentException("no providers found");
    }

    requestLocationUdpateProviderPendingIntents.put(pendingIntent, provider);
  }

  @Implementation
  protected void removeUpdates(LocationListener listener) {
    removedLocationListeners.add(listener);
  }

  private void cleanupRemovedLocationListeners() {
    for (Map.Entry<String, List<ListenerRegistration>> entry : locationListeners.entrySet()) {
      List<ListenerRegistration> listenerRegistrations = entry.getValue();
      for (int i = listenerRegistrations.size() - 1; i >= 0; i--) {
        LocationListener listener = listenerRegistrations.get(i).listener;
        if(removedLocationListeners.contains(listener)) {
          listenerRegistrations.remove(i);
        }
      }
    }
  }

  @Implementation
  protected void removeUpdates(PendingIntent pendingIntent) {
    while (requestLocationUdpateCriteriaPendingIntents.remove(pendingIntent) != null);
    while (requestLocationUdpateProviderPendingIntents.remove(pendingIntent) != null);
  }

  public boolean hasGpsStatusListener(Listener listener) {
    return gpsStatusListeners.contains(listener);
  }

  /**
   * Gets the criteria value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}.
   *
   * @return the criteria used to find the best provider
   */
  public Criteria getLastBestProviderCriteria() {
    return lastBestProviderCriteria;
  }

  /**
   * Gets the enabled value used in the last call to {@link #getBestProvider(android.location.Criteria, boolean)}
   *
   * @return the enabled value used to find the best provider
   */
  public boolean getLastBestProviderEnabledOnly() {
    return lastBestProviderEnabled;
  }

  /**
   * Sets the value to return from {@link #getBestProvider(android.location.Criteria, boolean)} for the given
   * {@code provider}
   *
   * @param provider name of the provider who should be considered best
   * @param enabled Enabled
   * @param criteria List of criteria
   * @throws Exception if provider is not known
   * @return false If provider is not enabled but it is supposed to be set as the best enabled provider don't set it, otherwise true
   */
  public boolean setBestProvider(String provider, boolean enabled, List<Criteria> criteria) throws Exception {
    if (!getAllProviders().contains(provider)) {
      throw new IllegalStateException("Best provider is not a known provider");
    }
    // If provider is not enabled but it is supposed to be set as the best enabled provider don't set it.
    for (String prvdr : providersEnabled.keySet()) {
      if (provider.equals(prvdr) && providersEnabled.get(prvdr).enabled != enabled) {
        return false;
      }
    }

    if (enabled) {
      bestEnabledProvider = provider;
      if (provider.equals(bestDisabledProvider)) {
        bestDisabledProvider = null;
      }
    } else {
      bestDisabledProvider = provider;
      if (provider.equals(bestEnabledProvider)) {
        bestEnabledProvider = null;
      }
    }
    if (criteria == null) {
      return true;
    }
    LocationProviderEntry entry;
    if (!providersEnabled.containsKey(provider)) {
      entry = new LocationProviderEntry();
      entry.enabled = enabled;
      entry.criteria = criteria;
    } else {
      entry = providersEnabled.get(provider);
    }
    providersEnabled.put(provider, entry);

    return true;
  }

  public boolean setBestProvider(String provider, boolean enabled) throws Exception {
    return setBestProvider(provider, enabled, null);
  }

  /**
   * Sets the value to return from {@link #getLastKnownLocation(String)} for the given {@code provider}
   *
   * @param provider
   *            name of the provider whose location to set
   * @param location
   *            the last known location for the provider
   */
  public void setLastKnownLocation(String provider, Location location) {
    lastKnownLocations.put(provider, location);
  }

  /**
   * @return lastRequestedLocationUpdatesLocationListener
   */
  public List<LocationListener> getRequestLocationUpdateListeners() {
    cleanupRemovedLocationListeners();
    List<LocationListener> all = new ArrayList<>();
    for (Map.Entry<String, List<ListenerRegistration>> entry : locationListeners.entrySet()) {
      for (ListenerRegistration reg : entry.getValue()) {
        all.add(reg.listener);
      }
    }

    return all;
  }

  public void simulateLocation(Location location) {
    cleanupRemovedLocationListeners();
    setLastKnownLocation(location.getProvider(), location);

    List<ListenerRegistration> providerListeners = locationListeners.get(
        location.getProvider());
    if (providerListeners == null) return;

    for (ListenerRegistration listenerReg : providerListeners) {
      if(listenerReg.lastSeenLocation != null && location != null) {
        float distanceChange = distanceBetween(location, listenerReg.lastSeenLocation);
        boolean withinMinDistance = distanceChange < listenerReg.minDistance;
        boolean exceededMinTime = location.getTime() - listenerReg.lastSeenTime > listenerReg.minTime;
        if (withinMinDistance || !exceededMinTime) continue;
      }
      listenerReg.lastSeenLocation = copyOf(location);
      listenerReg.lastSeenTime = location == null ? 0 : location.getTime();
      listenerReg.listener.onLocationChanged(copyOf(location));
    }
    cleanupRemovedLocationListeners();
  }

  private Location copyOf(Location location) {
    if (location == null) return null;
    Location copy = new Location(location);
    copy.setAccuracy(location.getAccuracy());
    copy.setAltitude(location.getAltitude());
    copy.setBearing(location.getBearing());
    copy.setExtras(location.getExtras());
    copy.setLatitude(location.getLatitude());
    copy.setLongitude(location.getLongitude());
    copy.setProvider(location.getProvider());
    copy.setSpeed(location.getSpeed());
    copy.setTime(location.getTime());
    return copy;
  }

  /**
   * Returns the distance between the two locations in meters.
   * Adapted from: http://stackoverflow.com/questions/837872/calculate-distance-in-meters-when-you-know-longitude-and-latitude-in-java
   */
  private static float distanceBetween(Location location1, Location location2) {
    double earthRadius = 3958.75;
    double latDifference = Math.toRadians(location2.getLatitude() - location1.getLatitude());
    double lonDifference = Math.toRadians(location2.getLongitude() - location1.getLongitude());
    double a = Math.sin(latDifference/2) * Math.sin(latDifference/2) +
        Math.cos(Math.toRadians(location1.getLatitude())) * Math.cos(Math.toRadians(location2.getLatitude())) *
            Math.sin(lonDifference/2) * Math.sin(lonDifference/2);
    double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    double dist = Math.abs(earthRadius * c);

    int meterConversion = 1609;

    return (float) (dist * meterConversion);
  }

  public Map<PendingIntent, Criteria> getRequestLocationUdpateCriteriaPendingIntents() {
    return requestLocationUdpateCriteriaPendingIntents;
  }

  public Map<PendingIntent, String> getRequestLocationUdpateProviderPendingIntents() {
    return requestLocationUdpateProviderPendingIntents;
  }

  public Collection<String> getProvidersForListener(LocationListener listener) {
    cleanupRemovedLocationListeners();
    Set<String> providers = new HashSet<>();
    for (List<ListenerRegistration> listenerRegistrations : locationListeners.values()) {
      for (ListenerRegistration listenerRegistration : listenerRegistrations) {
        if (listenerRegistration.listener == listener) {
          providers.add(listenerRegistration.provider);
        }
      }
    }
    return providers;
  }

  final private static class LocationProviderEntry implements Map.Entry<Boolean, List<Criteria>> {
    private Boolean enabled;
    private List<Criteria> criteria;

    @Override
    public Boolean getKey() {
      return enabled;
    }

    @Override
    public List<Criteria> getValue() {
      return criteria;
    }

    @Override
    public List<Criteria> setValue(List<Criteria> criteria) {
      List<Criteria> oldCriteria = this.criteria;
      this.criteria = criteria;
      return oldCriteria;
    }
  }

  private Context getContext() {
    return ReflectionHelpers.getField(realLocationManager, "mContext");
  }
}


package org.robolectric.shadows;

import android.graphics.Path;
import android.graphics.PathMeasure;
import java.math.BigDecimal;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;

@Implements(PathMeasure.class)
public class ShadowPathMeasure {

  private CachedPathIteratorFactory mOriginalPathIterator;

  @Implementation
  protected void __constructor__(Path path, boolean forceClosed) {
    if (path != null) {
      ShadowPath shadowPath = (ShadowPath) Shadow.extract(path);
      mOriginalPathIterator =
          new CachedPathIteratorFactory(shadowPath.getJavaShape().getPathIterator(null));
    }
  }

  /**
   * Return the total length of the current contour, or 0 if no path is associated with this measure
   * object.
   */
  @Implementation
  protected float getLength() {
    if (mOriginalPathIterator == null) {
      return 0;
    }

    return mOriginalPathIterator.iterator().getTotalLength();
  }

  /** Note: This is not mathematically correct. */
  @Implementation
  protected boolean getPosTan(float distance, float pos[], float tan[]) {
    if ((pos != null && pos.length < 2) || (tan != null && tan.length < 2)) {
      throw new ArrayIndexOutOfBoundsException();
    }

    // This is not mathematically correct, but the simulation keeps the support library happy.
    if (getLength() > 0) {
      pos[0] = round(distance / getLength(), 4);
      pos[1] = round(distance / getLength(), 4);
    }

    return true;
  }

  private static float round(float d, int decimalPlace) {
    BigDecimal bd = new BigDecimal(d);
    bd = bd.setScale(decimalPlace, BigDecimal.ROUND_HALF_UP);
    return bd.floatValue();
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static org.robolectric.shadow.api.Shadow.directlyOn;

import android.annotation.RequiresPermission;
import android.app.ActivityManager;
import android.app.IActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.os.Build.VERSION_CODES;
import android.os.Process;
import android.os.UserHandle;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;

@Implements(ActivityManager.class)
public class ShadowActivityManager {
  private int memoryClass = 16;
  private String backgroundPackage;
  private ActivityManager.MemoryInfo memoryInfo;
  private final List<ActivityManager.AppTask> appTasks = new CopyOnWriteArrayList<>();
  private final List<ActivityManager.RunningTaskInfo> tasks = new CopyOnWriteArrayList<>();
  private final List<ActivityManager.RunningServiceInfo> services = new CopyOnWriteArrayList<>();
  private static List<ActivityManager.RunningAppProcessInfo> processes =
      new CopyOnWriteArrayList<>();
  @RealObject private ActivityManager realObject;
  private Boolean isLowRamDeviceOverride = null;
  private int lockTaskModeState = ActivityManager.LOCK_TASK_MODE_NONE;
  private boolean isBackgroundRestricted;

  public ShadowActivityManager() {
    ActivityManager.RunningAppProcessInfo processInfo = new ActivityManager.RunningAppProcessInfo();
    fillInProcessInfo(processInfo);
    processInfo.processName = RuntimeEnvironment.application.getPackageName();
    processInfo.pkgList = new String[] {RuntimeEnvironment.application.getPackageName()};
    processes.add(processInfo);
  }

  @Implementation
  protected int getMemoryClass() {
    return memoryClass;
  }

  @Implementation
  protected static boolean isUserAMonkey() {
    return false;
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  @HiddenApi
  @RequiresPermission(
      anyOf = {
        "android.permission.INTERACT_ACROSS_USERS",
        "android.permission.INTERACT_ACROSS_USERS_FULL"
      })
  protected static int getCurrentUser() {
    return UserHandle.myUserId();
  }

  @Implementation
  protected List<ActivityManager.RunningTaskInfo> getRunningTasks(int maxNum) {
    return tasks;
  }

  /**
   * For tests, returns the list of {@link android.app.ActivityManager.AppTask} set using {@link
   * #setAppTasks(List)}. Returns empty list if nothing is set.
   *
   * @see #setAppTasks(List)
   * @return List of current AppTask.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected List<ActivityManager.AppTask> getAppTasks() {
    return appTasks;
  }

  @Implementation
  protected List<ActivityManager.RunningServiceInfo> getRunningServices(int maxNum) {
    return services;
  }

  @Implementation
  protected List<ActivityManager.RunningAppProcessInfo> getRunningAppProcesses() {
    // This method is explicitly documented not to return an empty list
    if (processes.isEmpty()) {
      return null;
    }
    return processes;
  }

  /** Returns information seeded by {@link #setProcesses}. */
  @Implementation
  protected static void getMyMemoryState(ActivityManager.RunningAppProcessInfo inState) {
    fillInProcessInfo(inState);
    for (ActivityManager.RunningAppProcessInfo info : processes) {
      if (info.pid == Process.myPid()) {
        inState.importance = info.importance;
        inState.lru = info.lru;
        inState.importanceReasonCode = info.importanceReasonCode;
        inState.importanceReasonPid = info.importanceReasonPid;
        inState.lastTrimLevel = info.lastTrimLevel;
        inState.pkgList = info.pkgList;
        inState.processName = info.processName;
      }
    }
  }

  private static void fillInProcessInfo(ActivityManager.RunningAppProcessInfo processInfo) {
    processInfo.pid = Process.myPid();
    processInfo.uid = Process.myUid();
  }

  @HiddenApi
  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected boolean switchUser(int userid) {
    ShadowUserManager shadowUserManager =
        Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.USER_SERVICE));
    shadowUserManager.switchUser(userid);
    return true;
  }

  @Implementation(minSdk = android.os.Build.VERSION_CODES.Q)
  protected boolean switchUser(UserHandle userHandle) {
    return switchUser(userHandle.getIdentifier());
  }

  @Implementation
  protected void killBackgroundProcesses(String packageName) {
    backgroundPackage = packageName;
  }

  @Implementation
  protected void getMemoryInfo(ActivityManager.MemoryInfo outInfo) {
    if (memoryInfo != null) {
      outInfo.availMem = memoryInfo.availMem;
      outInfo.lowMemory = memoryInfo.lowMemory;
      outInfo.threshold = memoryInfo.threshold;
      outInfo.totalMem = memoryInfo.totalMem;
    }
  }

  @Implementation
  protected android.content.pm.ConfigurationInfo getDeviceConfigurationInfo() {
    return new ConfigurationInfo();
  }

  /**
   * @param tasks List of running tasks.
   */
  public void setTasks(List<ActivityManager.RunningTaskInfo> tasks) {
    this.tasks.clear();
    this.tasks.addAll(tasks);
  }

  /**
   * Sets the values to be returned by {@link #getAppTasks()}.
   *
   * @see #getAppTasks()
   * @param tasks List of app tasks.
   */
  public void setAppTasks(List<ActivityManager.AppTask> appTasks) {
    this.appTasks.clear();
    this.appTasks.addAll(appTasks);
  }

  /**
   * @param services List of running services.
   */
  public void setServices(List<ActivityManager.RunningServiceInfo> services) {
    this.services.clear();
    this.services.addAll(services);
  }

  /**
   * @param processes List of running processes.
   */
  public void setProcesses(List<ActivityManager.RunningAppProcessInfo> processes) {
    ShadowActivityManager.processes.clear();
    ShadowActivityManager.processes.addAll(processes);
  }

  /**
   * @return Get the package name of the last background processes killed.
   */
  public String getBackgroundPackage() {
    return backgroundPackage;
  }

  /**
   * @param memoryClass Set the application's memory class.
   */
  public void setMemoryClass(int memoryClass) {
    this.memoryClass = memoryClass;
  }

  /**
   * @param memoryInfo Set the application's memory info.
   */
  public void setMemoryInfo(ActivityManager.MemoryInfo memoryInfo) {
    this.memoryInfo = memoryInfo;
  }

  @Implementation(minSdk = O)
  protected static IActivityManager getService() {
    return ReflectionHelpers.createNullProxy(IActivityManager.class);
  }

  @Implementation(minSdk = KITKAT)
  protected boolean isLowRamDevice() {
    if (isLowRamDeviceOverride != null) {
      return isLowRamDeviceOverride;
    }
    return directlyOn(realObject, ActivityManager.class, "isLowRamDevice");
  }

  /**
   * Override the return value of isLowRamDevice().
   */
  public void setIsLowRamDevice(boolean isLowRamDevice) {
    isLowRamDeviceOverride = isLowRamDevice;
  }

  @Implementation(minSdk = VERSION_CODES.M)
  protected int getLockTaskModeState() {
    return lockTaskModeState;
  }

  @Implementation(minSdk = VERSION_CODES.LOLLIPOP)
  protected boolean isInLockTaskMode() {
    return getLockTaskModeState() != ActivityManager.LOCK_TASK_MODE_NONE;
  }

  /**
   * Sets lock task mode state to be reported by {@link ActivityManager#getLockTaskModeState}, but
   * has no effect otherwise.
   */
  public void setLockTaskModeState(int lockTaskModeState) {
    this.lockTaskModeState = lockTaskModeState;
  }

  @Resetter
  public static void reset() {
    processes.clear();
  }

  /**
   * Returns the background restricion state set by {@link #setBackgroundRestricted}.
   */
  @Implementation(minSdk = P)
  protected boolean isBackgroundRestricted() {
    return isBackgroundRestricted;
  }

  /**
   * Sets the background restriction state reported by
   * {@link ActivityManager#isBackgroundRestricted}, but has no effect otherwise.
   */
  public void setBackgroundRestricted(boolean isBackgroundRestricted) {
    this.isBackgroundRestricted = isBackgroundRestricted;
  }
}


package org.robolectric.manifest;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.robolectric.pluginapi.UsesSdk;
import org.robolectric.res.Fs;
import org.robolectric.res.ResourcePath;
import org.robolectric.res.ResourceTable;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A wrapper for an Android App Manifest, which represents information about one's App to an Android
 * system.
 *
 * @see Android
 *     App Manifest
 */
@SuppressWarnings("NewApi")
public class AndroidManifest implements UsesSdk {
  private final Path androidManifestFile;
  private final Path resDirectory;
  private final Path assetsDirectory;
  private final String overridePackageName;
  private final List<AndroidManifest> libraryManifests;
  private final Path apkFile;

  private boolean manifestIsParsed;

  private String applicationName;
  private String applicationLabel;
  private String rClassName;
  private String packageName;
  private String processName;
  private String themeRef;
  private String labelRef;
  private Integer minSdkVersion;
  private Integer targetSdkVersion;
  private Integer maxSdkVersion;
  private int versionCode;
  private String versionName;
  private final Map<String, PermissionItemData> permissions = new HashMap<>();
  private final Map<String, PermissionGroupItemData> permissionGroups = new HashMap<>();
  private final List<ContentProviderData> providers = new ArrayList<>();
  private final List<BroadcastReceiverData> receivers = new ArrayList<>();
  private final Map<String, ServiceData> serviceDatas = new LinkedHashMap<>();
  private final Map<String, ActivityData> activityDatas = new LinkedHashMap<>();
  private final List<String> usedPermissions = new ArrayList<>();
  private final Map<String, String> applicationAttributes = new HashMap<>();
  private MetaData applicationMetaData;

  private Boolean supportsBinaryResourcesMode;

  /**
   * Creates a Robolectric configuration using specified locations.
   *
   * @param androidManifestFile Location of the AndroidManifest.xml file.
   * @param resDirectory Location of the res directory.
   * @param assetsDirectory Location of the assets directory.
   */
  public AndroidManifest(Path androidManifestFile, Path resDirectory, Path assetsDirectory) {
    this(androidManifestFile, resDirectory, assetsDirectory, null);
  }

  /**
   * Creates a Robolectric configuration using specified values.
   *
   * @param androidManifestFile Location of the AndroidManifest.xml file.
   * @param resDirectory Location of the res directory.
   * @param assetsDirectory Location of the assets directory.
   * @param overridePackageName Application package name.
   */
  public AndroidManifest(
      Path androidManifestFile,
      Path resDirectory,
      Path assetsDirectory,
      String overridePackageName) {
    this(androidManifestFile, resDirectory, assetsDirectory, Collections.emptyList(), overridePackageName);
  }

  /**
   * Creates a Robolectric configuration using specified values.
   *
   * @param androidManifestFile Location of the AndroidManifest.xml file.
   * @param resDirectory Location of the res directory.
   * @param assetsDirectory Location of the assets directory.
   * @param libraryManifests List of dependency library manifests.
   * @param overridePackageName Application package name.
   */
  public AndroidManifest(
      Path androidManifestFile,
      Path resDirectory,
      Path assetsDirectory,
      @Nonnull List<AndroidManifest> libraryManifests,
      String overridePackageName) {
    this(
        androidManifestFile,
        resDirectory,
        assetsDirectory,
        libraryManifests,
        overridePackageName,
        null);
  }

  public AndroidManifest(
      Path androidManifestFile,
      Path resDirectory,
      Path assetsDirectory,
      @Nonnull List<AndroidManifest> libraryManifests,
      String overridePackageName,
      Path apkFile) {
    this.androidManifestFile = androidManifestFile;
    this.resDirectory = resDirectory;
    this.assetsDirectory = assetsDirectory;
    this.overridePackageName = overridePackageName;
    this.libraryManifests = libraryManifests;

    this.packageName = overridePackageName;
    this.apkFile = apkFile;
  }

  public String getThemeRef(String activityClassName) {
    ActivityData activityData = getActivityData(activityClassName);
    String themeRef = activityData != null ? activityData.getThemeRef() : null;
    if (themeRef == null) {
      themeRef = getThemeRef();
    }
    return themeRef;
  }

  public String getRClassName() throws Exception {
    parseAndroidManifest();
    return rClassName;
  }

  public Class getRClass() {
    try {
      String rClassName = getRClassName();
      return Class.forName(rClassName);
    } catch (Exception e) {
      return null;
    }
  }

  @SuppressWarnings("CatchAndPrintStackTrace")
  void parseAndroidManifest() {
    if (manifestIsParsed) {
      return;
    }

    if (androidManifestFile != null && Files.exists(androidManifestFile)) {
      try {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

        DocumentBuilder db = dbf.newDocumentBuilder();
        InputStream inputStream = Fs.getInputStream(androidManifestFile);
        Document manifestDocument = db.parse(inputStream);
        inputStream.close();

        if (!packageNameIsOverridden()) {
          packageName = getTagAttributeText(manifestDocument, "manifest", "package");
        }

        versionCode = getTagAttributeIntValue(manifestDocument, "manifest", "android:versionCode", 0);
        versionName = getTagAttributeText(manifestDocument, "manifest", "android:versionName");
        rClassName = packageName + ".R";

        Node applicationNode = findApplicationNode(manifestDocument);
        if (applicationNode != null) {
          NamedNodeMap attributes = applicationNode.getAttributes();
          int attrCount = attributes.getLength();
          for (int i = 0; i < attrCount; i++) {
            Node attr = attributes.item(i);
            applicationAttributes.put(attr.getNodeName(), attr.getTextContent());
          }

          applicationName = applicationAttributes.get("android:name");
          applicationLabel = applicationAttributes.get("android:label");
          processName = applicationAttributes.get("android:process");
          themeRef = applicationAttributes.get("android:theme");
          labelRef = applicationAttributes.get("android:label");

          parseReceivers(applicationNode);
          parseServices(applicationNode);
          parseActivities(applicationNode);
          parseApplicationMetaData(applicationNode);
          parseContentProviders(applicationNode);
        }

        minSdkVersion = getTagAttributeIntValue(manifestDocument, "uses-sdk", "android:minSdkVersion");

        String targetSdkText =
            getTagAttributeText(manifestDocument, "uses-sdk", "android:targetSdkVersion");
        if (targetSdkText != null) {
          // Support Android O Preview. This can be removed once Android O is officially launched.
          targetSdkVersion = targetSdkText.equals("O") ? 26 : Integer.parseInt(targetSdkText);
        }

        maxSdkVersion = getTagAttributeIntValue(manifestDocument, "uses-sdk", "android:maxSdkVersion");
        if (processName == null) {
          processName = packageName;
        }

        parseUsedPermissions(manifestDocument);
        parsePermissions(manifestDocument);
        parsePermissionGroups(manifestDocument);
      } catch (Exception ignored) {
        ignored.printStackTrace();
      }
    } else {
      if (androidManifestFile != null) {
        System.out.println("WARNING: No manifest file found at " + androidManifestFile + ".");
        System.out.println("Falling back to the Android OS resources only.");
        System.out.println("To remove this warning, annotate your test class with @Config(manifest=Config.NONE).");
      }

      if (packageName == null || packageName.equals("")) {
        packageName = "org.robolectric.default";
      }

      rClassName = packageName + ".R";

      if (androidManifestFile != null) {
        System.err.println("No such manifest file: " + androidManifestFile);
      }
    }

    manifestIsParsed = true;
  }

  private boolean packageNameIsOverridden() {
    return overridePackageName != null && !overridePackageName.isEmpty();
  }

  private void parseUsedPermissions(Document manifestDocument) {
    NodeList elementsByTagName = manifestDocument.getElementsByTagName("uses-permission");
    int length = elementsByTagName.getLength();
    for (int i = 0; i < length; i++) {
      Node node = elementsByTagName.item(i).getAttributes().getNamedItem("android:name");
      usedPermissions.add(node.getNodeValue());
    }
  }

  private void parsePermissions(final Document manifestDocument) {
    NodeList elementsByTagName = manifestDocument.getElementsByTagName("permission");

    for (int i = 0; i < elementsByTagName.getLength(); i++) {
      Node permissionNode = elementsByTagName.item(i);
      final MetaData metaData = new MetaData(getChildrenTags(permissionNode, "meta-data"));
      String name = getAttributeValue(permissionNode, "android:name");
      permissions.put(
          name,
          new PermissionItemData(
              name,
              getAttributeValue(permissionNode, "android:label"),
              getAttributeValue(permissionNode, "android:description"),
              getAttributeValue(permissionNode, "android:permissionGroup"),
              getAttributeValue(permissionNode, "android:protectionLevel"),
              metaData));
    }
  }

  private void parsePermissionGroups(final Document manifestDocument) {
    NodeList elementsByTagName = manifestDocument.getElementsByTagName("permission-group");

    for (int i = 0; i < elementsByTagName.getLength(); i++) {
      Node permissionGroupNode = elementsByTagName.item(i);
      final MetaData metaData = new MetaData(getChildrenTags(permissionGroupNode, "meta-data"));
      String name = getAttributeValue(permissionGroupNode, "android:name");
      permissionGroups.put(
          name,
          new PermissionGroupItemData(
              name,
              getAttributeValue(permissionGroupNode, "android:label"),
              getAttributeValue(permissionGroupNode, "android:description"),
              metaData));
    }
  }

  private void parseContentProviders(Node applicationNode) {
    for (Node contentProviderNode : getChildrenTags(applicationNode, "provider")) {
      String name = getAttributeValue(contentProviderNode, "android:name");
      String authorities = getAttributeValue(contentProviderNode, "android:authorities");
      MetaData metaData = new MetaData(getChildrenTags(contentProviderNode, "meta-data"));

      List<PathPermissionData> pathPermissionDatas = new ArrayList<>();
      for (Node node : getChildrenTags(contentProviderNode, "path-permission")) {
        pathPermissionDatas.add(new PathPermissionData(
                getAttributeValue(node, "android:path"),
                getAttributeValue(node, "android:pathPrefix"),
                getAttributeValue(node, "android:pathPattern"),
                getAttributeValue(node, "android:readPermission"),
                getAttributeValue(node, "android:writePermission")
        ));
      }

      providers.add(
          new ContentProviderData(
              resolveClassRef(name),
              metaData,
              authorities,
              parseNodeAttributes(contentProviderNode),
              pathPermissionDatas));
    }
  }

  private @Nullable String getAttributeValue(Node parentNode, String attributeName) {
    Node attributeNode = parentNode.getAttributes().getNamedItem(attributeName);
    return attributeNode == null ? null : attributeNode.getTextContent();
  }

  private static HashMap<String, String> parseNodeAttributes(Node node) {
    final NamedNodeMap attributes = node.getAttributes();
    final int attrCount = attributes.getLength();
    final HashMap<String, String> receiverAttrs = new HashMap<>(attributes.getLength());
    for (int i = 0; i < attrCount; i++) {
      Node attribute = attributes.item(i);
      String value = attribute.getNodeValue();
      if (value != null) {
        receiverAttrs.put(attribute.getNodeName(), value);
      }
    }
    return receiverAttrs;
  }

  private void parseReceivers(Node applicationNode) {
    for (Node receiverNode : getChildrenTags(applicationNode, "receiver")) {
      final HashMap<String, String> receiverAttrs = parseNodeAttributes(receiverNode);

      String receiverName = resolveClassRef(receiverAttrs.get("android:name"));
      receiverAttrs.put("android:name", receiverName);

      MetaData metaData = new MetaData(getChildrenTags(receiverNode, "meta-data"));

      final List<IntentFilterData> intentFilterData = parseIntentFilters(receiverNode);
      BroadcastReceiverData receiver =
          new BroadcastReceiverData(receiverAttrs, metaData, intentFilterData);
      List<Node> intentFilters = getChildrenTags(receiverNode, "intent-filter");
      for (Node intentFilterNode : intentFilters) {
        for (Node actionNode : getChildrenTags(intentFilterNode, "action")) {
          Node nameNode = actionNode.getAttributes().getNamedItem("android:name");
          if (nameNode != null) {
            receiver.addAction(nameNode.getTextContent());
          }
        }
      }

      receivers.add(receiver);
    }
  }

  private void parseServices(Node applicationNode) {
    for (Node serviceNode : getChildrenTags(applicationNode, "service")) {
      final HashMap<String, String> serviceAttrs = parseNodeAttributes(serviceNode);

      String serviceName = resolveClassRef(serviceAttrs.get("android:name"));
      serviceAttrs.put("android:name", serviceName);

      MetaData metaData = new MetaData(getChildrenTags(serviceNode, "meta-data"));

      final List<IntentFilterData> intentFilterData = parseIntentFilters(serviceNode);
      ServiceData service = new ServiceData(serviceAttrs, metaData, intentFilterData);
      List<Node> intentFilters = getChildrenTags(serviceNode, "intent-filter");
      for (Node intentFilterNode : intentFilters) {
        for (Node actionNode : getChildrenTags(intentFilterNode, "action")) {
          Node nameNode = actionNode.getAttributes().getNamedItem("android:name");
          if (nameNode != null) {
            service.addAction(nameNode.getTextContent());
          }
        }
      }

      serviceDatas.put(serviceName, service);
    }
  }

  private void parseActivities(Node applicationNode) {
    for (Node activityNode : getChildrenTags(applicationNode, "activity")) {
      parseActivity(activityNode, false);
    }

    for (Node activityNode : getChildrenTags(applicationNode, "activity-alias")) {
      parseActivity(activityNode, true);
    }
  }

  private Node findApplicationNode(Document manifestDocument) {
    NodeList applicationNodes = manifestDocument.getElementsByTagName("application");
    if (applicationNodes.getLength() > 1) {
      throw new RuntimeException("found " + applicationNodes.getLength() + " application elements");
    }
    return applicationNodes.item(0);
  }

  private void parseActivity(Node activityNode, boolean isAlias) {
    final List<IntentFilterData> intentFilterData = parseIntentFilters(activityNode);
    final MetaData metaData = new MetaData(getChildrenTags(activityNode, "meta-data"));
    final HashMap<String, String> activityAttrs = parseNodeAttributes(activityNode);

    String activityName = resolveClassRef(activityAttrs.get(ActivityData.getNameAttr("android")));
    if (activityName == null) {
      return;
    }
    ActivityData targetActivity = null;
    if (isAlias) {
      String targetName = resolveClassRef(activityAttrs.get(ActivityData.getTargetAttr("android")));
      if (activityName == null) {
        return;
      }
      // The target activity should have been parsed already so if it exists we should find it in
      // activityDatas.
      targetActivity = activityDatas.get(targetName);
      activityAttrs.put(ActivityData.getTargetAttr("android"), targetName);
    }
    activityAttrs.put(ActivityData.getNameAttr("android"), activityName);
    activityDatas.put(activityName, new ActivityData("android", activityAttrs, intentFilterData, targetActivity, metaData));
  }

  private List<IntentFilterData> parseIntentFilters(final Node activityNode) {
    ArrayList<IntentFilterData> intentFilterDatas = new ArrayList<>();
    for (Node n : getChildrenTags(activityNode, "intent-filter")) {
      ArrayList<String> actionNames = new ArrayList<>();
      ArrayList<String> categories = new ArrayList<>();
      //should only be one action.
      for (Node action : getChildrenTags(n, "action")) {
        NamedNodeMap attributes = action.getAttributes();
        Node actionNameNode = attributes.getNamedItem("android:name");
        if (actionNameNode != null) {
          actionNames.add(actionNameNode.getNodeValue());
        }
      }
      for (Node category : getChildrenTags(n, "category")) {
        NamedNodeMap attributes = category.getAttributes();
        Node categoryNameNode = attributes.getNamedItem("android:name");
        if (categoryNameNode != null) {
          categories.add(categoryNameNode.getNodeValue());
        }
      }
      IntentFilterData intentFilterData = new IntentFilterData(actionNames, categories);
      intentFilterData = parseIntentFilterData(n, intentFilterData);
      intentFilterDatas.add(intentFilterData);
    }

    return intentFilterDatas;
  }

  private IntentFilterData parseIntentFilterData(final Node intentFilterNode, IntentFilterData intentFilterData) {
    for (Node n : getChildrenTags(intentFilterNode, "data")) {
      NamedNodeMap attributes = n.getAttributes();
      String host = null;
      String port = null;

      Node schemeNode = attributes.getNamedItem("android:scheme");
      if (schemeNode != null) {
        intentFilterData.addScheme(schemeNode.getNodeValue());
      }

      Node hostNode = attributes.getNamedItem("android:host");
      if (hostNode != null) {
        host = hostNode.getNodeValue();
      }

      Node portNode = attributes.getNamedItem("android:port");
      if (portNode != null) {
        port = portNode.getNodeValue();
      }
      intentFilterData.addAuthority(host, port);

      Node pathNode = attributes.getNamedItem("android:path");
      if (pathNode != null) {
        intentFilterData.addPath(pathNode.getNodeValue());
      }

      Node pathPatternNode = attributes.getNamedItem("android:pathPattern");
      if (pathPatternNode != null) {
        intentFilterData.addPathPattern(pathPatternNode.getNodeValue());
      }

      Node pathPrefixNode = attributes.getNamedItem("android:pathPrefix");
      if (pathPrefixNode != null) {
        intentFilterData.addPathPrefix(pathPrefixNode.getNodeValue());
      }

      Node mimeTypeNode = attributes.getNamedItem("android:mimeType");
      if (mimeTypeNode != null) {
        intentFilterData.addMimeType(mimeTypeNode.getNodeValue());
      }
    }
    return intentFilterData;
  }

  /***
   * Allows ShadowPackageManager to provide
   * a resource index for initialising the resource attributes in all the metadata elements
   * @param resourceTable used for getting resource IDs from string identifiers
   */
  public void initMetaData(ResourceTable resourceTable) throws RoboNotFoundException {
    if (!packageNameIsOverridden()) {
      // packageName needs to be resolved
      parseAndroidManifest();
    }

    if (applicationMetaData != null) {
      applicationMetaData.init(resourceTable, packageName);
    }
    for (PackageItemData receiver : receivers) {
      receiver.getMetaData().init(resourceTable, packageName);
    }
    for (ServiceData service : serviceDatas.values()) {
      service.getMetaData().init(resourceTable, packageName);
    }
    for (ContentProviderData providerData : providers) {
      providerData.getMetaData().init(resourceTable, packageName);
    }
  }

  private void parseApplicationMetaData(Node applicationNode) {
    applicationMetaData = new MetaData(getChildrenTags(applicationNode, "meta-data"));
  }

  private String resolveClassRef(String maybePartialClassName) {
    return (maybePartialClassName.startsWith(".")) ? packageName + maybePartialClassName : maybePartialClassName;
  }

  private List<Node> getChildrenTags(final Node node, final String tagName) {
    List<Node> children = new ArrayList<>();
    for (int i = 0; i < node.getChildNodes().getLength(); i++) {
      Node childNode = node.getChildNodes().item(i);
      if (childNode.getNodeName().equalsIgnoreCase(tagName)) {
        children.add(childNode);
      }
    }
    return children;
  }

  private Integer getTagAttributeIntValue(final Document doc, final String tag, final String attribute) {
    return getTagAttributeIntValue(doc, tag, attribute, null);
  }

  private Integer getTagAttributeIntValue(final Document doc, final String tag, final String attribute, final Integer defaultValue) {
    String valueString = getTagAttributeText(doc, tag, attribute);
    if (valueString != null) {
      return Integer.parseInt(valueString);
    }
    return defaultValue;
  }

  public String getApplicationName() {
    parseAndroidManifest();
    return applicationName;
  }

  public String getActivityLabel(String activityClassName) {
    parseAndroidManifest();
    ActivityData data = getActivityData(activityClassName);
    return (data != null && data.getLabel() != null) ? data.getLabel() : applicationLabel;
  }

  public String getPackageName() {
    parseAndroidManifest();
    return packageName;
  }

  public int getVersionCode() {
    return versionCode;
  }

  public String getVersionName() {
    return versionName;
  }

  public String getLabelRef() {
    return labelRef;
  }

  /**
   * Returns the minimum Android SDK version that this package expects to be runnable on, as
   * specified in the manifest.
   *
   *
Note that if `targetSdkVersion` isn't set, this value changes the behavior of some Android
   * code (notably {@link android.content.SharedPreferences}) to emulate old bugs.
   *
   * @return the minimum SDK version, or Jelly Bean (16) by default
   */
  @Override
  public int getMinSdkVersion() {
    parseAndroidManifest();
    return minSdkVersion == null ? 16 : minSdkVersion;
  }

  /**
   * Returns the Android SDK version that this package prefers to be run on, as specified in the
   * manifest.
   *
   *
Note that this value changes the behavior of some Android code (notably {@link
   * android.content.SharedPreferences}) to emulate old bugs.
   *
   * @return the minimum SDK version, or Jelly Bean (16) by default
   */
  @Override
  public int getTargetSdkVersion() {
    parseAndroidManifest();
    return targetSdkVersion == null ? getMinSdkVersion() : targetSdkVersion;
  }

  @Override
  public Integer getMaxSdkVersion() {
    parseAndroidManifest();
    return maxSdkVersion;
  }

  public Map<String, String> getApplicationAttributes() {
    parseAndroidManifest();
    return applicationAttributes;
  }

  public String getProcessName() {
    parseAndroidManifest();
    return processName;
  }

  public Map<String, Object> getApplicationMetaData() {
    parseAndroidManifest();
    if (applicationMetaData == null) {
      applicationMetaData = new MetaData(Collections.<Node>emptyList());
    }
    return applicationMetaData.getValueMap();
  }

  public ResourcePath getResourcePath() {
    return new ResourcePath(getRClass(), resDirectory, assetsDirectory);
  }

  public List<ResourcePath> getIncludedResourcePaths() {
    Collection<ResourcePath> resourcePaths = new LinkedHashSet<>(); // Needs stable ordering and no duplicates
    resourcePaths.add(getResourcePath());
    for (AndroidManifest libraryManifest : getLibraryManifests()) {
      resourcePaths.addAll(libraryManifest.getIncludedResourcePaths());
    }
    return new ArrayList<>(resourcePaths);
  }

  public List<ContentProviderData> getContentProviders() {
    parseAndroidManifest();
    return providers;
  }

  public List<AndroidManifest> getLibraryManifests() {
    assert(libraryManifests != null);
    return Collections.unmodifiableList(libraryManifests);
  }

  /**
   * Returns all transitively reachable manifests, including this one, in order and without
   * duplicates.
   */
  public List<AndroidManifest> getAllManifests() {
    Set<AndroidManifest> seenManifests = new HashSet<>();
    List<AndroidManifest> uniqueManifests = new ArrayList<>();
    addTransitiveManifests(seenManifests, uniqueManifests);
    return uniqueManifests;
  }

  private void addTransitiveManifests(Set<AndroidManifest> unique, List<AndroidManifest> list) {
    if (unique.add(this)) {
      list.add(this);
      for (AndroidManifest androidManifest : getLibraryManifests()) {
        androidManifest.addTransitiveManifests(unique, list);
      }
    }
  }

  public Path getResDirectory() {
    return resDirectory;
  }

  public Path getAssetsDirectory() {
    return assetsDirectory;
  }

  public Path getAndroidManifestFile() {
    return androidManifestFile;
  }

  public List<BroadcastReceiverData> getBroadcastReceivers() {
    parseAndroidManifest();
    return receivers;
  }

  public List<ServiceData> getServices() {
    parseAndroidManifest();
    return new ArrayList<>(serviceDatas.values());
  }

  public ServiceData getServiceData(String serviceClassName) {
    parseAndroidManifest();
    return serviceDatas.get(serviceClassName);
  }

  private static String getTagAttributeText(final Document doc, final String tag, final String attribute) {
    NodeList elementsByTagName = doc.getElementsByTagName(tag);
    for (int i = 0; i < elementsByTagName.getLength(); ++i) {
      Node item = elementsByTagName.item(i);
      Node namedItem = item.getAttributes().getNamedItem(attribute);
      if (namedItem != null) {
        return namedItem.getTextContent();
      }
    }
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    AndroidManifest that = (AndroidManifest) o;

    if (androidManifestFile != null ? !androidManifestFile.equals(that.androidManifestFile)
        : that.androidManifestFile != null) {
      return false;
    }
    if (resDirectory != null ? !resDirectory.equals(that.resDirectory)
        : that.resDirectory != null) {
      return false;
    }
    if (assetsDirectory != null ? !assetsDirectory.equals(that.assetsDirectory)
        : that.assetsDirectory != null) {
      return false;
    }
    if (overridePackageName != null ? !overridePackageName.equals(that.overridePackageName)
        : that.overridePackageName != null) {
      return false;
    }
    if (libraryManifests != null ? !libraryManifests.equals(that.libraryManifests)
        : that.libraryManifests != null) {
      return false;
    }
    return apkFile != null ? apkFile.equals(that.apkFile) : that.apkFile == null;
  }

  @Override
  public int hashCode() {
    int result = androidManifestFile != null ? androidManifestFile.hashCode() : 0;
    result = 31 * result + (resDirectory != null ? resDirectory.hashCode() : 0);
    result = 31 * result + (assetsDirectory != null ? assetsDirectory.hashCode() : 0);
    result = 31 * result + (overridePackageName != null ? overridePackageName.hashCode() : 0);
    result = 31 * result + (libraryManifests != null ? libraryManifests.hashCode() : 0);
    result = 31 * result + (apkFile != null ? apkFile.hashCode() : 0);
    return result;
  }

  public ActivityData getActivityData(String activityClassName) {
    parseAndroidManifest();
    return activityDatas.get(activityClassName);
  }

  public String getThemeRef() {
    return themeRef;
  }

  public Map<String, ActivityData> getActivityDatas() {
    parseAndroidManifest();
    return activityDatas;
  }

  public List<String> getUsedPermissions() {
    parseAndroidManifest();
    return usedPermissions;
  }

  public Map<String, PermissionItemData> getPermissions() {
    parseAndroidManifest();
    return permissions;
  }

  public Map<String, PermissionGroupItemData> getPermissionGroups() {
    parseAndroidManifest();
    return permissionGroups;
  }

  /**
   * Returns data for the broadcast receiver with the provided name from this manifest. If no
   * receiver with the class name can be found, returns null.
   *
   * @param className the fully resolved class name of the receiver
   * @return data for the receiver or null if it cannot be found
   */
  public @Nullable BroadcastReceiverData getBroadcastReceiver(String className) {
    parseAndroidManifest();
    for (BroadcastReceiverData receiver : receivers) {
      if (receiver.getName().equals(className)) {
        return receiver;
      }
    }
    return null;
  }

  public Path getApkFile() {
    return apkFile;
  }

  /** @deprecated Do not use. */
  @Deprecated
  public boolean supportsLegacyResourcesMode() {
    return true;
  }

  /** @deprecated Do not use. */
  @Deprecated
  synchronized public boolean supportsBinaryResourcesMode() {
    if (supportsBinaryResourcesMode == null) {
      supportsBinaryResourcesMode = apkFile != null && Files.exists(apkFile);
    }
    return supportsBinaryResourcesMode;
  }
}


package org.robolectric.shadows;

import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;

import android.app.AutomaticZenRule;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
import android.os.Build;
import android.os.Parcel;
import android.service.notification.StatusBarNotification;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(value = NotificationManager.class, looseSignatures = true)
public class ShadowNotificationManager {
  private boolean mAreNotificationsEnabled = true;
  private boolean isNotificationPolicyAccessGranted = false;
  private Map<Key, Notification> notifications = new HashMap<>();
  private final Map<String, Object> notificationChannels = new HashMap<>();
  private final Map<String, Object> notificationChannelGroups = new HashMap<>();
  private final Map<String, Object> deletedNotificationChannels = new HashMap<>();
  private final Map<String, AutomaticZenRule> automaticZenRules = new HashMap<>();
  private int currentInteruptionFilter = INTERRUPTION_FILTER_ALL;
  private Policy notificationPolicy;

  @Implementation
  protected void notify(int id, Notification notification) {
    notify(null, id, notification);
  }

  @Implementation
  protected void notify(String tag, int id, Notification notification) {
    notifications.put(new Key(tag, id), notification);
  }

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

  @Implementation
  protected void cancel(String tag, int id) {
    Key key = new Key(tag, id);
    if (notifications.containsKey(key)) {
      notifications.remove(key);
    }
  }

  @Implementation
  protected void cancelAll() {
    notifications.clear();
  }

  @Implementation(minSdk = Build.VERSION_CODES.N)
  protected boolean areNotificationsEnabled() {
    return mAreNotificationsEnabled;
  }

  public void setNotificationsEnabled(boolean areNotificationsEnabled) {
    mAreNotificationsEnabled = areNotificationsEnabled;
  }

  @Implementation(minSdk = M)
  public StatusBarNotification[] getActiveNotifications() {
    StatusBarNotification[] statusBarNotifications =
        new StatusBarNotification[notifications.size()];
    int i = 0;
    for (Map.Entry<Key, Notification> entry : notifications.entrySet()) {
      statusBarNotifications[i++] = new StatusBarNotification(
	  RuntimeEnvironment.application.getPackageName(),
	  null /* opPkg */,
	  entry.getKey().id,
	  entry.getKey().tag,
	  android.os.Process.myUid() /* uid */,
	  android.os.Process.myPid() /* initialPid */,
	  0 /* score */,
	  entry.getValue(),
	  android.os.Process.myUserHandle(),
	  0 /* postTime */);
    }
    return statusBarNotifications;
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected Object /*NotificationChannel*/ getNotificationChannel(String channelId) {
    return notificationChannels.get(channelId);
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected void createNotificationChannelGroup(Object /*NotificationChannelGroup*/ group) {
    String id = ReflectionHelpers.callInstanceMethod(group, "getId");
    notificationChannelGroups.put(id, group);
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected void createNotificationChannelGroups(
      List<Object /*NotificationChannelGroup*/> groupList) {
    for (Object group : groupList) {
      createNotificationChannelGroup(group);
    }
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected List<Object /*NotificationChannelGroup*/> getNotificationChannelGroups() {
    return ImmutableList.copyOf(notificationChannelGroups.values());
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected void createNotificationChannel(Object /*NotificationChannel*/ channel) {
    String id = ReflectionHelpers.callInstanceMethod(channel, "getId");
    // Per documentation, recreating a deleted channel should have the same settings as the old
    // deleted channel. See
    // https://developer.android.com/reference/android/app/NotificationManager.html#deleteNotificationChannel%28java.lang.String%29
    // for more info.
    if (deletedNotificationChannels.containsKey(id)) {
      notificationChannels.put(id, deletedNotificationChannels.remove(id));
    } else {
      notificationChannels.put(id, channel);
    }
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected void createNotificationChannels(List<Object /*NotificationChannel*/> channelList) {
    for (Object channel : channelList) {
      createNotificationChannel(channel);
    }
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  public List<Object /*NotificationChannel*/> getNotificationChannels() {
    return ImmutableList.copyOf(notificationChannels.values());
  }

  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected void deleteNotificationChannel(String channelId) {
    if (getNotificationChannel(channelId) != null) {
      Object /*NotificationChannel*/ channel = notificationChannels.remove(channelId);
      deletedNotificationChannels.put(channelId, channel);
    }
  }

  /**
   * Delete a notification channel group and all notification channels associated with the group.
   * This method will not notify any NotificationListenerService of resulting changes to
   * notification channel groups nor to notification channels.
   */
  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected void deleteNotificationChannelGroup(String channelGroupId) {
    if (getNotificationChannelGroup(channelGroupId) != null) {
      // Deleting a channel group also deleted all associated channels. See
      // https://developer.android.com/reference/android/app/NotificationManager.html#deleteNotificationChannelGroup%28java.lang.String%29
      // for more info.
      for (/* NotificationChannel */ Object channel : getNotificationChannels()) {
        String groupId = ReflectionHelpers.callInstanceMethod(channel, "getGroup");
        if (channelGroupId.equals(groupId)) {
          String channelId = ReflectionHelpers.callInstanceMethod(channel, "getId");
          deleteNotificationChannel(channelId);
        }
      }
      notificationChannelGroups.remove(channelGroupId);
    }
  }

  /**
   * @return {@link NotificationManager#INTERRUPTION_FILTER_ALL} by default, or the value specified
   *         via {@link #setInterruptionFilter(int)}
   */
  @Implementation(minSdk = M)
  protected final int getCurrentInterruptionFilter() {
    return currentInteruptionFilter;
  }

  /**
   * Currently does not support checking for granted policy access.
   *
   * @see NotificationManager#getCurrentInterruptionFilter()
   */
  @Implementation(minSdk = M)
  protected final void setInterruptionFilter(int interruptionFilter) {
    currentInteruptionFilter = interruptionFilter;
  }

  /**
   * @return the value specified via {@link #setNotificationPolicy(Policy)}
   */
  @Implementation(minSdk = M)
  protected final Policy getNotificationPolicy() {
    return notificationPolicy;
  }

  /**
   * @return the value specified via {@link #setNotificationPolicyAccessGranted(boolean)}
   */
  @Implementation(minSdk = M)
  protected final boolean isNotificationPolicyAccessGranted() {
    return isNotificationPolicyAccessGranted;
  }

  /**
   * Currently does not support checking for granted policy access.
   *
   * @see NotificationManager#getNotificationPolicy()
   */
  @Implementation(minSdk = M)
  protected final void setNotificationPolicy(Policy policy) {
    notificationPolicy = policy;
  }

  /**
   * Sets the value returned by {@link NotificationManager#isNotificationPolicyAccessGranted()}. If
   * {@code granted} is false, this also deletes all {@link AutomaticZenRule}s.
   *
   * @see NotificationManager#isNotificationPolicyAccessGranted()
   */
  public void setNotificationPolicyAccessGranted(boolean granted) {
    isNotificationPolicyAccessGranted = granted;
    if (!granted) {
      automaticZenRules.clear();
    }
  }

  @Implementation(minSdk = N)
  protected AutomaticZenRule getAutomaticZenRule(String id) {
    Preconditions.checkNotNull(id);
    enforcePolicyAccess();

    return automaticZenRules.get(id);
  }

  @Implementation(minSdk = N)
  protected Map<String, AutomaticZenRule> getAutomaticZenRules() {
    enforcePolicyAccess();

    ImmutableMap.Builder<String, AutomaticZenRule> rules = new ImmutableMap.Builder();
    for (Map.Entry<String, AutomaticZenRule> entry : automaticZenRules.entrySet()) {
      rules.put(entry.getKey(), copyAutomaticZenRule(entry.getValue()));
    }
    return rules.build();
  }

  @Implementation(minSdk = N)
  protected String addAutomaticZenRule(AutomaticZenRule automaticZenRule) {
    Preconditions.checkNotNull(automaticZenRule);
    Preconditions.checkNotNull(automaticZenRule.getName());
    Preconditions.checkNotNull(automaticZenRule.getOwner());
    Preconditions.checkNotNull(automaticZenRule.getConditionId());
    enforcePolicyAccess();

    String id = UUID.randomUUID().toString().replace("-", "");
    automaticZenRules.put(id, copyAutomaticZenRule(automaticZenRule));
    return id;
  }

  @Implementation(minSdk = N)
  protected boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) {
    // NotificationManagerService doesn't check that id is non-null.
    Preconditions.checkNotNull(automaticZenRule);
    Preconditions.checkNotNull(automaticZenRule.getName());
    Preconditions.checkNotNull(automaticZenRule.getOwner());
    Preconditions.checkNotNull(automaticZenRule.getConditionId());
    enforcePolicyAccess();

    // ZenModeHelper throws slightly cryptic exceptions.
    if (id == null) {
      throw new IllegalArgumentException("Rule doesn't exist");
    } else if (!automaticZenRules.containsKey(id)) {
      throw new SecurityException("Cannot update rules not owned by your condition provider");
    }

    automaticZenRules.put(id, copyAutomaticZenRule(automaticZenRule));
    return true;
  }

  @Implementation(minSdk = N)
  protected boolean removeAutomaticZenRule(String id) {
    Preconditions.checkNotNull(id);
    enforcePolicyAccess();
    return automaticZenRules.remove(id) != null;
  }

  /**
   * Enforces that the caller has notification policy access.
   *
   * @see NotificationManager#isNotificationPolicyAccessGranted()
   * @throws SecurityException if the caller doesn't have notification policy access
   */
  private void enforcePolicyAccess() {
    if (!isNotificationPolicyAccessGranted) {
      throw new SecurityException("Notification policy access denied");
    }
  }

  /** Returns a copy of {@code automaticZenRule}. */
  private AutomaticZenRule copyAutomaticZenRule(AutomaticZenRule automaticZenRule) {
    Parcel parcel = Parcel.obtain();
    try {
      automaticZenRule.writeToParcel(parcel, /* flags= */ 0);
      parcel.setDataPosition(0);
      return new AutomaticZenRule(parcel);
    } finally {
      parcel.recycle();
    }
  }

  /**
   * Checks whether a channel is considered a "deleted" channel by Android. This is a channel that
   * was created but later deleted. If a channel is created that was deleted before, it recreates
   * the channel with the old settings.
   */
  public boolean isChannelDeleted(String channelId) {
    return deletedNotificationChannels.containsKey(channelId);
  }

  public Object /*NotificationChannelGroup*/ getNotificationChannelGroup(String id) {
    return notificationChannelGroups.get(id);
  }

  public int size() {
    return notifications.size();
  }

  public Notification getNotification(int id) {
    return notifications.get(new Key(null, id));
  }

  public Notification getNotification(String tag, int id) {
    return notifications.get(new Key(tag, id));
  }

  public List<Notification> getAllNotifications() {
    return new ArrayList<>(notifications.values());
  }

  private static final class Key {
    public final String tag;
    public final int id;

    private Key(String tag, int id) {
      this.tag = tag;
      this.id = id;
    }

    @Override
    public int hashCode() {
      int hashCode = 17;
      hashCode = 37 * hashCode + (tag == null ? 0 : tag.hashCode());
      hashCode = 37 * hashCode + id;
      return hashCode;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof Key)) return false;
      Key other = (Key) o;
      return (this.tag == null ? other.tag == null : this.tag.equals(other.tag)) && this.id == other.id;
    }
  }
}


package org.robolectric.res.android;

import java.io.File;
import java.nio.file.Path;
import org.robolectric.res.Fs;

// transliterated from
// https://android.googlesource.com/platform/system/core/+/android-9.0.0_r12/libutils/String8.cpp
// and
// https://android.googlesource.com/platform/system/core/+/android-9.0.0_r12/include/utils/String8.h
@SuppressWarnings("NewApi")
public class String8 {

  private StringBuilder mString;

  public String8() {
    this("");
  }

  public String8(Path value) {
    this(Fs.externalize(value));
  }

  public String8(String value) {
    mString = new StringBuilder(value);
  }

  public String8(String8 path) {
    this(path.string());
  }

  public String8(String value, int len) {
    this(value.substring(0, len));
  }

  int length() {
    return mString.length();
  }
//String8 String8::format(const char* fmt, ...)
//{
//    va_list args;
//    va_start(args, fmt);
//    String8 result(formatV(fmt, args));
//    va_end(args);
//    return result;
//}
//String8 String8::formatV(const char* fmt, va_list args)
//{
//    String8 result;
//    result.appendFormatV(fmt, args);
//    return result;
//}
//void String8::clear() {
//    SharedBuffer::bufferFromData(mString)->release();
//    mString = getEmptyString();
//}
//void String8::setTo(const String8& other)
//{
//    SharedBuffer::bufferFromData(other.mString)->acquire();
//    SharedBuffer::bufferFromData(mString)->release();
//    mString = other.mString;
//}
//status_t String8::setTo(const char* other)
//{
//    const char *newString = allocFromUTF8(other, strlen(other));
//    SharedBuffer::bufferFromData(mString)->release();
//    mString = newString;
//    if (mString) return NO_ERROR;
//    mString = getEmptyString();
//    return NO_MEMORY;
//}
//status_t String8::setTo(const char* other, size_t len)
//{
//    const char *newString = allocFromUTF8(other, len);
//    SharedBuffer::bufferFromData(mString)->release();
//    mString = newString;
//    if (mString) return NO_ERROR;
//    mString = getEmptyString();
//    return NO_MEMORY;
//}
//status_t String8::setTo(const char16_t* other, size_t len)
//{
//    const char *newString = allocFromUTF16(other, len);
//    SharedBuffer::bufferFromData(mString)->release();
//    mString = newString;
//    if (mString) return NO_ERROR;
//    mString = getEmptyString();
//    return NO_MEMORY;
//}
//status_t String8::setTo(const char32_t* other, size_t len)
//{
//    const char *newString = allocFromUTF32(other, len);
//    SharedBuffer::bufferFromData(mString)->release();
//    mString = newString;
//    if (mString) return NO_ERROR;
//    mString = getEmptyString();
//    return NO_MEMORY;
//}
//status_t String8::append(const String8& other)
//{
//    const size_t otherLen = other.bytes();
//    if (bytes() == 0) {
//        setTo(other);
//        return NO_ERROR;
//    } else if (otherLen == 0) {
//        return NO_ERROR;
//    }
//    return real_append(other.string(), otherLen);
//}
public String8 append(final String other) {
  mString.append(other);
    return this;
}
//status_t String8::append(const char* other, size_t otherLen)
//{
//    if (bytes() == 0) {
//        return setTo(other, otherLen);
//    } else if (otherLen == 0) {
//        return NO_ERROR;
//    }
//    return real_append(other, otherLen);
//}
//status_t String8::appendFormat(const char* fmt, ...)
//{
//    va_list args;
//    va_start(args, fmt);
//    status_t result = appendFormatV(fmt, args);
//    va_end(args);
//    return result;
//}
//status_t String8::appendFormatV(const char* fmt, va_list args)
//{
//    int n, result = NO_ERROR;
//    va_list tmp_args;
//    /* args is undefined after vsnprintf.
//     * So we need a copy here to avoid the
//     * second vsnprintf access undefined args.
//     */
//    va_copy(tmp_args, args);
//    n = vsnprintf(NULL, 0, fmt, tmp_args);
//    va_end(tmp_args);
//    if (n != 0) {
//        size_t oldLength = length();
//        char* buf = lockBuffer(oldLength + n);
//        if (buf) {
//            vsnprintf(buf + oldLength, n + 1, fmt, args);
//        } else {
//            result = NO_MEMORY;
//        }
//    }
//    return result;
//}
//status_t String8::real_append(const char* other, size_t otherLen)
//{
//    const size_t myLen = bytes();
//
//    SharedBuffer* buf = SharedBuffer::bufferFromData(mString)
//        ->editResize(myLen+otherLen+1);
//    if (buf) {
//        char* str = (char*)buf->data();
//        mString = str;
//        str += myLen;
//        memcpy(str, other, otherLen);
//        str[otherLen] = '\0';
//        return NO_ERROR;
//    }
//    return NO_MEMORY;
//}
//char* String8::lockBuffer(size_t size)
//{
//    SharedBuffer* buf = SharedBuffer::bufferFromData(mString)
//        ->editResize(size+1);
//    if (buf) {
//        char* str = (char*)buf->data();
//        mString = str;
//        return str;
//    }
//    return NULL;
//}
//void String8::unlockBuffer()
//{
//    unlockBuffer(strlen(mString));
//}
//status_t String8::unlockBuffer(size_t size)
//{
//    if (size != this->size()) {
//        SharedBuffer* buf = SharedBuffer::bufferFromData(mString)
//            ->editResize(size+1);
//        if (! buf) {
//            return NO_MEMORY;
//        }
//        char* str = (char*)buf->data();
//        str[size] = 0;
//        mString = str;
//    }
//    return NO_ERROR;
//}
//ssize_t String8::find(const char* other, size_t start) const
//{
//    size_t len = size();
//    if (start >= len) {
//        return -1;
//    }
//    const char* s = mString+start;
//    const char* p = strstr(s, other);
//    return p ? p-mString : -1;
//}
//bool String8::removeAll(const char* other) {
//    ssize_t index = find(other);
//    if (index < 0) return false;
//    char* buf = lockBuffer(size());
//    if (!buf) return false; // out of memory
//    size_t skip = strlen(other);
//    size_t len = size();
//    size_t tail = index;
//    while (size_t(index) < len) {
//        ssize_t next = find(other, index + skip);
//        if (next < 0) {
//            next = len;
//        }
//        memmove(buf + tail, buf + index + skip, next - index - skip);
//        tail += next - index - skip;
//        index = next;
//    }
//    unlockBuffer(tail);
//    return true;
//}
//void String8::toLower()
//{
//    toLower(0, size());
//}
//void String8::toLower(size_t start, size_t length)
//{
//    const size_t len = size();
//    if (start >= len) {
//        return;
//    }
//    if (start+length > len) {
//        length = len-start;
//    }
//    char* buf = lockBuffer(len);
//    buf += start;
//    while (length > 0) {
//        *buf = tolower(*buf);
//        buf++;
//        length--;
//    }
//    unlockBuffer(len);
//}
//void String8::toUpper()
//{
//    toUpper(0, size());
//}
//void String8::toUpper(size_t start, size_t length)
//{
//    const size_t len = size();
//    if (start >= len) {
//        return;
//    }
//    if (start+length > len) {
//        length = len-start;
//    }
//    char* buf = lockBuffer(len);
//    buf += start;
//    while (length > 0) {
//        *buf = toupper(*buf);
//        buf++;
//        length--;
//    }
//    unlockBuffer(len);
//}
//size_t String8::getUtf32Length() const
//{
//    return utf8_to_utf32_length(mString, length());
//}
//int32_t String8::getUtf32At(size_t index, size_t *next_index) const
//{
//    return utf32_from_utf8_at(mString, length(), index, next_index);
//}
//void String8::getUtf32(char32_t* dst) const
//{
//    utf8_to_utf32(mString, length(), dst);
//}
//// ---------------------------------------------------------------------------
//// Path functions
//void String8::setPathName(const char* name)
//{
//    setPathName(name, strlen(name));
//}
//void String8::setPathName(const char* name, size_t len)
//{
//    char* buf = lockBuffer(len);
//    memcpy(buf, name, len);
//    // remove trailing path separator, if present
//    if (len > 0 && buf[len-1] == OS_PATH_SEPARATOR)
//        len--;
//    buf[len] = '\0';
//    unlockBuffer(len);
//}
String8 getPathLeaf() {
  final int cp;
  final String buf = mString.toString();
  cp = buf.lastIndexOf('/');
  if (cp == -1) {
    return new String8(this);
  } else {
    return new String8(buf.substring(cp + 1));
  }
}
//String8 String8::getPathDir(void) const
//{
//    const char* cp;
//    const char*const str = mString;
//    cp = strrchr(str, OS_PATH_SEPARATOR);
//    if (cp == NULL)
//        return String8("");
//    else
//        return String8(str, cp - str);
//}
//String8 String8::walkPath(String8* outRemains) const
//{
//    const char* cp;
//    const char*const str = mString;
//    const char* buf = str;
//    cp = strchr(buf, OS_PATH_SEPARATOR);
//    if (cp == buf) {
//        // don't include a leading '/'.
//        buf = buf+1;
//        cp = strchr(buf, OS_PATH_SEPARATOR);
//    }
//    if (cp == NULL) {
//        String8 res = buf != str ? String8(buf) : *this;
//        if (outRemains) *outRemains = String8("");
//        return res;
//    }
//    String8 res(buf, cp-buf);
//    if (outRemains) *outRemains = String8(cp+1);
//    return res;
//}

/*
 * Helper function for finding the start of an extension in a pathname.
 *
 * Returns a index inside mString, or -1 if no extension was found.
 */
private int find_extension()
{
    int lastSlashIndex;

    final StringBuilder str = mString;
    // only look at the filename
    lastSlashIndex = str.lastIndexOf(File.pathSeparator);
    if (lastSlashIndex == -1) {
      lastSlashIndex = 0;
    } else {
      lastSlashIndex++;
    }
    // find the last dot
    return str.lastIndexOf(".", lastSlashIndex);
}

public String getPathExtension()
{
    int extIndex;
    extIndex = find_extension();
    if (extIndex != -1) {
      return mString.substring(extIndex);
    }
    else {
      return "";
    }
}

  String8 getBasePath() {
    int extIndex;
    extIndex = find_extension();
    if (extIndex == -1) {
      return new String8(this);
    } else {
      return new String8(mString.substring(extIndex));
    }
  }

  public String8 appendPath(String name) {
    if (name.length() == 0) {
      // nothing to do
      return this;
    }
    if (name.charAt(0) != '/') {
      mString.append('/');
    }
    mString.append(name);
    return this;
}

//String8& String8::convertToResPath()
//{
//#if OS_PATH_SEPARATOR != RES_PATH_SEPARATOR
//    size_t len = length();
//    if (len > 0) {
//        char * buf = lockBuffer(len);
//        for (char * end = buf + len; buf < end; ++buf) {
//            if (*buf == OS_PATH_SEPARATOR)
//                *buf = RES_PATH_SEPARATOR;
//        }
//        unlockBuffer(len);
//    }
//#endif
//    return *this;
//}
//}; // namespace android

  public final String string() {
    return mString.toString();
  }

  @Override
  public String toString() {
    return mString.toString();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    String8 string8 = (String8) o;

    return mString != null ? mString.toString().equals(string8.mString.toString()) : string8.mString == null;
  }

  @Override
  public int hashCode() {
    return mString != null ? mString.hashCode() : 0;
  }

}


package org.robolectric.internal.bytecode;

import com.google.common.collect.ImmutableMap;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.robolectric.annotation.Implements;
import org.robolectric.internal.ShadowProvider;
import org.robolectric.shadow.api.ShadowPicker;

/**
 * Maps from instrumented class to shadow class.
 *
 * We deal with class names rather than actual classes here, since a ShadowMap is built outside of
 * any sandboxes, but instrumented and shadowed classes must be loaded through a
 * {@link SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
 *
 * Once constructed, instances are immutable.
 */
@SuppressWarnings("NewApi")
public class ShadowMap {

  static final ShadowMap EMPTY = new ShadowMap(ImmutableMap.of(), ImmutableMap.of());

  private final ImmutableMap<String, String> defaultShadows;
  private final ImmutableMap<String, ShadowInfo> overriddenShadows;
  private final ImmutableMap<String, String> shadowPickers;

  @SuppressWarnings("AndroidJdkLibsChecker")
  public static ShadowMap createFromShadowProviders(List<ShadowProvider> sortedProviders) {
    final Map<String, String> shadowMap = new HashMap<>();
    final Map<String, String> shadowPickerMap = new HashMap<>();

    for (ShadowProvider provider : sortedProviders) {
       shadowMap.putAll(provider.getShadowMap());
       shadowPickerMap.putAll(provider.getShadowPickerMap());
    }
    return new ShadowMap(ImmutableMap.copyOf(shadowMap), Collections.emptyMap(),
        ImmutableMap.copyOf(shadowPickerMap));
  }

  ShadowMap(
      ImmutableMap<String, String> defaultShadows,
      Map<String, ShadowInfo> overriddenShadows) {
    this(defaultShadows, overriddenShadows, Collections.emptyMap());
  }

  private ShadowMap(ImmutableMap<String, String> defaultShadows,
      Map<String, ShadowInfo> overriddenShadows,
      Map<String, String> shadowPickers) {
    this.defaultShadows = defaultShadows;
    this.overriddenShadows = ImmutableMap.copyOf(overriddenShadows);
    this.shadowPickers = ImmutableMap.copyOf(shadowPickers);
  }

  public ShadowInfo getShadowInfo(Class<?> clazz, int apiLevel) {
    String instrumentedClassName = clazz.getName();

    ShadowInfo shadowInfo = overriddenShadows.get(instrumentedClassName);
    if (shadowInfo == null) {
      shadowInfo = checkShadowPickers(instrumentedClassName, clazz);
    } else if (shadowInfo.hasShadowPicker()) {
      shadowInfo = pickShadow(instrumentedClassName, clazz, shadowInfo);
    }

    if (shadowInfo == null && clazz.getClassLoader() != null) {
      try {
        final String shadowName = defaultShadows.get(clazz.getCanonicalName());
        if (shadowName != null) {
          Class<?> shadowClass = clazz.getClassLoader().loadClass(shadowName);
          shadowInfo = obtainShadowInfo(shadowClass);
          if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
            // somehow we got the wrong shadow class?
            shadowInfo = null;
          }
        }
      } catch (ClassNotFoundException | IncompatibleClassChangeError e) {
        return null;
      }
    }

    if (shadowInfo != null && !shadowInfo.supportsSdk(apiLevel)) {
      return null;
    }

    return shadowInfo;
  }

  // todo: some caching would probably be nice here...
  private ShadowInfo checkShadowPickers(String instrumentedClassName, Class<?> clazz) {
    String shadowPickerClassName = shadowPickers.get(instrumentedClassName);
    if (shadowPickerClassName == null) {
      return null;
    }

    return pickShadow(instrumentedClassName, clazz, shadowPickerClassName);
  }

  private ShadowInfo pickShadow(String instrumentedClassName, Class<?> clazz,
      String shadowPickerClassName) {
    ClassLoader sandboxClassLoader = clazz.getClassLoader();
    try {
      Class<? extends ShadowPicker<?>> shadowPickerClass =
          (Class<? extends ShadowPicker<?>>) sandboxClassLoader.loadClass(shadowPickerClassName);
      ShadowPicker<?> shadowPicker = shadowPickerClass.getDeclaredConstructor().newInstance();
      Class<?> selectedShadowClass = shadowPicker.pickShadowClass();
      if (selectedShadowClass == null) {
        return obtainShadowInfo(Object.class, true);
      }
      ShadowInfo shadowInfo = obtainShadowInfo(selectedShadowClass);

      if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
        throw new IllegalArgumentException("Implemented class for "
            + selectedShadowClass.getName() + " (" + shadowInfo.shadowedClassName + ") != "
            + instrumentedClassName);
      }

      return shadowInfo;
    } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
        | IllegalAccessException | InstantiationException e) {
      throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName,
          e);
    }
  }

  private ShadowInfo pickShadow(
      String instrumentedClassName, Class<?> clazz, ShadowInfo shadowInfo) {
    return pickShadow(instrumentedClassName, clazz, shadowInfo.getShadowPickerClass().getName());
  }

  public static ShadowInfo obtainShadowInfo(Class<?> clazz) {
    return obtainShadowInfo(clazz, false);
  }

  static ShadowInfo obtainShadowInfo(Class<?> clazz, boolean mayBeNonShadow) {
    Implements annotation = clazz.getAnnotation(Implements.class);
    if (annotation == null) {
      if (mayBeNonShadow) {
        return null;
      } else {
        throw new IllegalArgumentException(clazz + " is not annotated with @Implements");
      }
    }

    String className = annotation.className();
    if (className.isEmpty()) {
      className = annotation.value().getName();
    }
    return new ShadowInfo(className, clazz.getName(), annotation);
  }

  @SuppressWarnings("ReferenceEquality")
  public Set<String> getInvalidatedClasses(ShadowMap previous) {
    if (this == previous && shadowPickers.isEmpty()) return Collections.emptySet();

    Map<String, ShadowInfo> invalidated = new HashMap<>(overriddenShadows);

    for (Map.Entry<String, ShadowInfo> entry : previous.overriddenShadows.entrySet()) {
      String className = entry.getKey();
      ShadowInfo previousConfig = entry.getValue();
      ShadowInfo currentConfig = invalidated.get(className);
      if (currentConfig == null) {
        invalidated.put(className, previousConfig);
      } else if (previousConfig.equals(currentConfig)) {
        invalidated.remove(className);
      }
    }

    return invalidated.keySet();
  }

  /**
   * @deprecated do not use
   */
  @Deprecated
  public static String convertToShadowName(String className) {
    String shadowClassName =
        "org.robolectric.shadows.Shadow" + className.substring(className.lastIndexOf(".") + 1);
    shadowClassName = shadowClassName.replaceAll("\\$", "\\$Shadow");
    return shadowClassName;
  }

  public Builder newBuilder() {
    return new Builder(this);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof ShadowMap)) return false;

    ShadowMap shadowMap = (ShadowMap) o;

    if (!overriddenShadows.equals(shadowMap.overriddenShadows)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    return overriddenShadows.hashCode();
  }

  public static class Builder {
    private final ImmutableMap<String, String> defaultShadows;
    private final Map<String, ShadowInfo> overriddenShadows;
    private final Map<String, String> shadowPickers;

    public Builder () {
      defaultShadows = ImmutableMap.of();
      overriddenShadows = new HashMap<>();
      shadowPickers = new HashMap<>();
    }

    public Builder(ShadowMap shadowMap) {
      this.defaultShadows = shadowMap.defaultShadows;
      this.overriddenShadows = new HashMap<>(shadowMap.overriddenShadows);
      this.shadowPickers = new HashMap<>(shadowMap.shadowPickers);
    }

    public Builder addShadowClasses(Class<?>... shadowClasses) {
      for (Class<?> shadowClass : shadowClasses) {
        addShadowClass(shadowClass);
      }
      return this;
    }

    Builder addShadowClass(Class<?> shadowClass) {
      addShadowInfo(obtainShadowInfo(shadowClass));
      return this;
    }

    Builder addShadowClass(
        String realClassName,
        String shadowClassName,
        boolean callThroughByDefault,
        boolean looseSignatures) {
      addShadowInfo(
          new ShadowInfo(
              realClassName, shadowClassName, callThroughByDefault, looseSignatures, -1, -1, null));
      return this;
    }

    private void addShadowInfo(ShadowInfo shadowInfo) {
      overriddenShadows.put(shadowInfo.shadowedClassName, shadowInfo);
      if (shadowInfo.hasShadowPicker()) {
        shadowPickers
            .put(shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
      }
    }

    public ShadowMap build() {
      return new ShadowMap(defaultShadows, overriddenShadows, shadowPickers);
    }
  }
}


package org.robolectric.shadows;

import android.annotation.NonNull;
import android.util.ArraySet;
import android.view.accessibility.CaptioningManager;
import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

/** Shadow of {@link android.view.accessibility.CaptioningManager}. */
@Implements(CaptioningManager.class)
public class ShadowCaptioningManager {
  private float fontScale = 1;
  private boolean isEnabled = false;

  private final ArraySet<CaptioningChangeListener> listeners = new ArraySet<>();

  /** Returns 1.0 as default or the most recent value passed to {@link #setFontScale()} */
  @Implementation(minSdk = 19)
  protected float getFontScale() {
    return fontScale;
  }

  /** Sets the value to be returned by {@link CaptioningManager#getFontScale()} */
  public void setFontScale(float fontScale) {
    this.fontScale = fontScale;

    for (CaptioningChangeListener captioningChangeListener : listeners) {
      captioningChangeListener.onFontScaleChanged(fontScale);
    }
  }

  /** Returns false or the most recent value passed to {@link #setEnabled(boolean)} */
  @Implementation(minSdk = 19)
  protected boolean isEnabled() {
    return isEnabled;
  }

  /** Sets the value to be returned by {@link CaptioningManager#isEnabled()} */
  public void setEnabled(boolean isEnabled) {
    this.isEnabled = isEnabled;
  }

  @Implementation(minSdk = 19)
  protected void addCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
    listeners.add(listener);
  }

  @Implementation(minSdk = 19)
  protected void removeCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
    listeners.remove(listener);
  }
}