package org.robolectric.android.controller;

import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O_MR1;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.app.Activity;
import android.app.ActivityThread;
import android.app.Application;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.ViewRootImpl;
import android.view.WindowManager;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowActivity;
import org.robolectric.shadows.ShadowContextThemeWrapper;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowViewRootImpl;
import org.robolectric.shadows._Activity_;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;

/**
 * ActivityController provides low-level APIs to control activity's lifecycle.
 *
 * <p>Using ActivityController directly from your tests is strongly discouraged. You have to call
 * all the lifecycle callback methods (create, postCreate, start, ...) in the same manner as the
 * Android framework by yourself otherwise you'll see fidelity issues. Consider using {@link
 * androidx.test.core.app.ActivityScenario} instead, which provides higher-level, streamlined APIs
 * to control the lifecycle and it works with instrumentation tests too.
 *
 * @param <T> a class of the activity which is under control by this class.
 */
@SuppressWarnings("NewApi")
public class ActivityController<T extends Activity>
    extends ComponentController<ActivityController<T>, T> {

  private _Activity_ _component_;

  public static <T extends Activity> ActivityController<T> of(T activity, Intent intent) {
    return new ActivityController<>(activity, intent)
        .attach(/*lastNonConfigurationInstances=*/ null);
  }

  public static <T extends Activity> ActivityController<T> of(T activity) {
    return new ActivityController<>(activity, null).attach(/*lastNonConfigurationInstances=*/ null);
  }

  private ActivityController(T activity, Intent intent) {
    super(activity, intent);

    _component_ = reflector(_Activity_.class, component);
  }

  private ActivityController<T> attach(
      @Nullable @WithType("android.app.Activity$NonConfigurationInstances")
          Object lastNonConfigurationInstances) {
    if (attached) {
      return this;
    }
    // make sure the component is enabled
    Context context = RuntimeEnvironment.application.getBaseContext();
    PackageManager packageManager = context.getPackageManager();
    ComponentName componentName =
        new ComponentName(context.getPackageName(), this.component.getClass().getName());
    ((ShadowPackageManager) extract(packageManager)).addActivityIfNotPresent(componentName);
    packageManager
        .setComponentEnabledSetting(
            componentName,
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
            0);
    ShadowActivity shadowActivity = Shadow.extract(component);
    shadowActivity.callAttach(getIntent(), lastNonConfigurationInstances);
    shadowActivity.attachController(this);
    attached = true;
    return this;
  }

  private ActivityInfo getActivityInfo(Application application) {
    PackageManager packageManager = application.getPackageManager();
    ComponentName componentName =
        new ComponentName(application.getPackageName(), this.component.getClass().getName());
    try {
      return packageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA);
    } catch (PackageManager.NameNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  public ActivityController<T> create(final Bundle bundle) {
    shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnCreate(component, bundle));
    return this;
  }

  @Override public ActivityController<T> create() {
    return create(null);
  }

  public ActivityController<T> restart() {
    if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
      invokeWhilePaused("performRestart");
    } else {
      invokeWhilePaused("performRestart",
          from(boolean.class, true),
          from(String.class, "restart()"));
    }
    return this;
  }

  public ActivityController<T> start() {
    // Start and stop are tricky cases. Unlike other lifecycle methods such as
    // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls
    // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite.
    if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
      invokeWhilePaused("performStart");
    } else {
      invokeWhilePaused("performStart", from(String.class, "start()"));
    }
    return this;
  }

  public ActivityController<T> restoreInstanceState(Bundle bundle) {
    shadowMainLooper.runPaused(
        () -> getInstrumentation().callActivityOnRestoreInstanceState(component, bundle));
    return this;
  }

  public ActivityController<T> postCreate(Bundle bundle) {
    invokeWhilePaused("onPostCreate", from(Bundle.class, bundle));
    return this;
  }

  public ActivityController<T> resume() {
    if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
      invokeWhilePaused("performResume");
    } else {
      invokeWhilePaused("performResume",
          from(boolean.class, true),
          from(String.class, "resume()"));
    }
    return this;
  }

  public ActivityController<T> postResume() {
    invokeWhilePaused("onPostResume");
    return this;
  }

  public ActivityController<T> visible() {
    shadowMainLooper.runPaused(
        () -> {
          // emulate logic of ActivityThread#handleResumeActivity
          component.getWindow().getAttributes().type =
              WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
          _component_.setDecor(component.getWindow().getDecorView());
          ReflectionHelpers.callInstanceMethod(component, "makeVisible");
        });

    shadowMainLooper.idleIfPaused();
    ViewRootImpl root = getViewRoot();
    // root can be null if activity does not have content attached, or if looper is paused.
    // this is unusual but leave the check here for legacy compatibility
    if (root != null) {
      callDispatchResized(root);
      shadowMainLooper.idleIfPaused();
    }
    return this;
  }

  private ViewRootImpl getViewRoot() {
    return component.getWindow().getDecorView().getViewRootImpl();
  }

  private void callDispatchResized(ViewRootImpl root) {
    ((ShadowViewRootImpl) extract(root)).callDispatchResized();
  }

  public ActivityController<T> windowFocusChanged(boolean hasFocus) {
    ViewRootImpl root = getViewRoot();
    if (root == null) {
      // root can be null if looper was paused during visible. Flush the looper and try again
      shadowMainLooper.idle();

      root = checkNotNull(getViewRoot());
      callDispatchResized(root);
    }

    ReflectionHelpers.callInstanceMethod(root, "windowFocusChanged",
        from(boolean.class, hasFocus), /* hasFocus */
        from(boolean.class, false) /* inTouchMode */);
    shadowMainLooper.idleIfPaused();
    return this;
  }

  public ActivityController<T> userLeaving() {
    shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnUserLeaving(component));
    return this;
  }

  public ActivityController<T> pause() {
    shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnPause(component));
    return this;
  }

  public ActivityController<T> saveInstanceState(Bundle outState) {
    shadowMainLooper.runPaused(
        () -> getInstrumentation().callActivityOnSaveInstanceState(component, outState));
    return this;
  }

  public ActivityController<T> stop() {
    // Stop and start are tricky cases. Unlike other lifecycle methods such as
    // Instrumentation#callActivityOnPause calls Activity#performPause, Activity#performStop calls
    // Instrumentation#callActivityOnStop internally so the dependency direction is the opposite.
    if (RuntimeEnvironment.getApiLevel() <= M) {
      invokeWhilePaused("performStop");
    } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
      invokeWhilePaused("performStop", from(boolean.class, true));
    } else {
      invokeWhilePaused("performStop", from(boolean.class, true), from(String.class, "stop()"));
    }
    return this;
  }

  @Override public ActivityController<T> destroy() {
    shadowMainLooper.runPaused(() -> getInstrumentation().callActivityOnDestroy(component));
    return this;
  }

  /**
   * Calls the same lifecycle methods on the Activity called by Android the first time the Activity is created.
   *
   * @return Activity controller instance.
   */
  public ActivityController<T> setup() {
    return create().start().postCreate(null).resume().visible();
  }

  /**
   * Calls the same lifecycle methods on the Activity called by Android when an Activity is restored from previously saved state.
   *
   * @param savedInstanceState Saved instance state.
   * @return Activity controller instance.
   */
  public ActivityController<T> setup(Bundle savedInstanceState) {
    return create(savedInstanceState)
        .start()
        .restoreInstanceState(savedInstanceState)
        .postCreate(savedInstanceState)
        .resume()
        .visible();
  }

  public ActivityController<T> newIntent(Intent intent) {
    invokeWhilePaused("onNewIntent", from(Intent.class, intent));
    return this;
  }

  /**
   * Applies the current system configuration to the Activity.
   *
   * This can be used in conjunction with {@link RuntimeEnvironment#setQualifiers(String)} to
   * simulate configuration changes.
   *
   * If the activity is configured to handle changes without being recreated,
   * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity
   * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html).
   *
   * @return ActivityController instance
   */
  public ActivityController<T> configurationChange() {
    return configurationChange(component.getApplicationContext().getResources().getConfiguration());
  }

  /**
   * Performs a configuration change on the Activity.
   *
   * If the activity is configured to handle changes without being recreated,
   * {@link Activity#onConfigurationChanged(Configuration)} will be called. Otherwise, the activity
   * is recreated as described [here](https://developer.android.com/guide/topics/resources/runtime-changes.html).
   *
   * @param newConfiguration The new configuration to be set.
   * @return ActivityController instance
   */
  public ActivityController<T> configurationChange(final Configuration newConfiguration) {
    final Configuration currentConfig = component.getResources().getConfiguration();
    final int changedBits = currentConfig.diff(newConfiguration);
    currentConfig.setTo(newConfiguration);

    // TODO: throw on changedBits == 0 since it non-intuitively calls onConfigurationChanged

    // Can the activity handle itself ALL configuration changes?
    if ((getActivityInfo(component.getApplication()).configChanges & changedBits) == changedBits) {
      shadowMainLooper.runPaused(() -> component.onConfigurationChanged(newConfiguration));

      return this;
    } else {
      @SuppressWarnings("unchecked")
      final T recreatedActivity = (T) ReflectionHelpers.callConstructor(component.getClass());
      final _Activity_ _recreatedActivity_ = reflector(_Activity_.class, recreatedActivity);

      shadowMainLooper.runPaused(
          new Runnable() {
            @Override
            public void run() {
              // Set flags
              ReflectionHelpers.setField(
                  Activity.class, component, "mChangingConfigurations", true);
              ReflectionHelpers.setField(
                  Activity.class, component, "mConfigChangeFlags", changedBits);

              // Perform activity destruction
              final Bundle outState = new Bundle();

              // The order of onPause/onStop/onSaveInstanceState is undefined, but is usually:
              // onPause -> onSaveInstanceState -> onStop
              _component_.performPause();
              _component_.performSaveInstanceState(outState);
              if (RuntimeEnvironment.getApiLevel() <= M) {
                _component_.performStop();
              } else if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
                _component_.performStop(true);
              } else {
                _component_.performStop(true, "configurationChange");
              }

              // This is the true and complete retained state, including loaders and retained
              // fragments.
              final Object nonConfigInstance = _component_.retainNonConfigurationInstances();
              // This is the activity's "user" state
              final Object activityConfigInstance =
                  nonConfigInstance == null
                      ? null // No framework or user state.
                      : reflector(_NonConfigurationInstances_.class, nonConfigInstance)
                          .getActivity();

              _component_.performDestroy();

              // Restore theme in case it was set in the test manually.
              // This is not technically what happens but is purely to make this easier to use in
              // Robolectric.
              ShadowContextThemeWrapper shadowContextThemeWrapper = Shadow.extract(component);
              int theme = shadowContextThemeWrapper.callGetThemeResId();

              // Setup controller for the new activity
              attached = false;
              component = recreatedActivity;
              _component_ = _recreatedActivity_;

              // TODO: Pass nonConfigurationInstance here instead of setting
              // mLastNonConfigurationInstances directly below. This field must be set before
              // attach. Since current implementation sets it after attach(), initialization is not
              // done correctly. For instance, fragment marked as retained is not retained.
              attach(/*lastNonConfigurationInstances=*/ null);

              if (theme != 0) {
                recreatedActivity.setTheme(theme);
              }

              // Set saved non config instance
              _recreatedActivity_.setLastNonConfigurationInstances(nonConfigInstance);
              ShadowActivity shadowActivity = Shadow.extract(recreatedActivity);
              shadowActivity.setLastNonConfigurationInstance(activityConfigInstance);

              // Create lifecycle
              _recreatedActivity_.performCreate(outState);

              if (RuntimeEnvironment.getApiLevel() <= O_MR1) {

                _recreatedActivity_.performStart();

              } else {
                _recreatedActivity_.performStart("configurationChange");
              }

              _recreatedActivity_.performRestoreInstanceState(outState);
              _recreatedActivity_.onPostCreate(outState);
              if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
                _recreatedActivity_.performResume();
              } else {
                _recreatedActivity_.performResume(true, "configurationChange");
              }
              _recreatedActivity_.onPostResume();
              // TODO: Call visible() too.
            }
          });
    }

    return this;
  }

  /**
   * Recreates activity instance which is controlled by this ActivityController.
   * NonConfigurationInstances and savedInstanceStateBundle are properly passed into a new instance.
   * After the recreation, it brings back its lifecycle state to the original state. The activity
   * should not be destroyed yet.
   */
  @SuppressWarnings("unchecked")
  public ActivityController<T> recreate() {
    Stage originalStage =
        ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(component);

    switch (originalStage) {
      case PRE_ON_CREATE:
        create();
        // fall through
      case CREATED:
      case RESTARTED:
        start();
        postCreate(null);
        // fall through
      case STARTED:
        resume();
        // fall through
      case RESUMED:
        pause();
        // fall through
      case PAUSED:
        stop();
        // fall through
      case STOPPED:
        break;
      default:
        throw new IllegalStateException("Cannot recreate activity since it's destroyed already");
    }

    // Activity#mChangingConfigurations flag should be set prior to Activity recreation process
    // starts. ActivityThread does set it on real device but here we simulate the Activity
    // recreation process on behalf of ActivityThread so set the flag here. Note we don't need to
    // reset the flag to false because this Activity instance is going to be destroyed and disposed.
    // https://android.googlesource.com/platform/frameworks/base/+/55418eada51d4f5e6532ae9517af66c50
    // ea495c4/core/java/android/app/ActivityThread.java#4806
    ReflectionHelpers.setField(component, "mChangingConfigurations", true);

    Bundle outState = new Bundle();
    saveInstanceState(outState);
    Object lastNonConfigurationInstances =
        ReflectionHelpers.callInstanceMethod(component, "retainNonConfigurationInstances");
    destroy();

    component = (T) ReflectionHelpers.callConstructor(component.getClass());
    _component_ = reflector(_Activity_.class, component);
    attached = false;
    attach(lastNonConfigurationInstances);
    create(outState);
    start();
    restoreInstanceState(outState);
    postCreate(outState);
    resume();
    postResume();
    visible();
    windowFocusChanged(true);

    // Move back to the original stage. If the original stage was transient stage, it will bring it
    // to resumed state to match the on device behavior.
    switch (originalStage) {
      case PAUSED:
        pause();
        return this;
      case STOPPED:
        pause();
        stop();
        return this;
      default:
        return this;
    }
  }

  private static Instrumentation getInstrumentation() {
    return ((ActivityThread) RuntimeEnvironment.getActivityThread()).getInstrumentation();
  }

  /** Accessor interface for android.app.Activity.NonConfigurationInstances's internals. */
  @ForType(className = "android.app.Activity$NonConfigurationInstances")
  interface _NonConfigurationInstances_ {

    @Accessor("activity")
    Object getActivity();
  }
}


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

package org.robolectric.shadows;

import android.util.TypedValue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.res.ResName;

/**
 * Helper class to provide various conversion method used in handling android resources.
 */
public final class ResourceHelper2 {

  private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)");
  private final static float[] sFloatOut = new float[1];

  private final static TypedValue mValue = new TypedValue();

  // ------- TypedValue stuff
  // This is taken from //device/libs/utils/ResourceTypes.cpp

  private static final class UnitEntry {
    String name;
    int type;
    int unit;
    float scale;

    UnitEntry(String name, int type, int unit, float scale) {
      this.name = name;
      this.type = type;
      this.unit = unit;
      this.scale = scale;
    }
  }

  private final static UnitEntry[] sUnitNames = new UnitEntry[] {
    new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
    new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
    new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
    new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
    new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
    new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
    new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
    new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
    new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
  };

  /**
   * Returns the raw value from the given attribute float-type value string.
   * This object is only valid until the next call on to {@link ResourceHelper2}.
   *
   * @param attribute Attribute name.
   * @param value Attribute value.
   * @param requireUnit whether the value is expected to contain a unit.
   * @return The typed value.
   */
  public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
    if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
      return mValue;
    }

    return null;
  }

  /**
   * Parse a float attribute and return the parsed value into a given TypedValue.
   * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
   * @param value the string value of the attribute
   * @param outValue the TypedValue to receive the parsed value
   * @param requireUnit whether the value is expected to contain a unit.
   * @return true if success.
   */
  public static boolean parseFloatAttribute(String attribute, String value,
      TypedValue outValue, boolean requireUnit) {
//    assert requireUnit == false || attribute != null;

    // remove the space before and after
    value = value.trim();
    int len = value.length();

    if (len <= 0) {
      return false;
    }

    // check that there's no non ascii characters.
    char[] buf = value.toCharArray();
    for (int i = 0 ; i < len ; i++) {
      if (buf[i] > 255) {
        return false;
      }
    }

    // check the first character
    if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') {
      return false;
    }

    // now look for the string that is after the float...
    Matcher m = sFloatPattern.matcher(value);
    if (m.matches()) {
      String f_str = m.group(1);
      String end = m.group(2);

      float f;
      try {
        f = Float.parseFloat(f_str);
      } catch (NumberFormatException e) {
        // this shouldn't happen with the regexp above.
        return false;
      }

      if (end.length() > 0 && end.charAt(0) != ' ') {
        // Might be a unit...
        if (parseUnit(end, outValue, sFloatOut)) {
          computeTypedValue(outValue, f, sFloatOut[0], end);
          return true;
        }
        return false;
      }

      // make sure it's only spaces at the end.
      end = end.trim();

      if (end.length() == 0) {
        if (outValue != null) {
          outValue.assetCookie = 0;
          outValue.string = null;

          if (requireUnit == false) {
            outValue.type = TypedValue.TYPE_FLOAT;
            outValue.data = Float.floatToIntBits(f);
          } else {
            // no unit when required? Use dp and out an error.
            applyUnit(sUnitNames[1], outValue, sFloatOut);
            computeTypedValue(outValue, f, sFloatOut[0], "dp");

            System.out.println(String.format(
                "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
                    value, attribute == null ? "(unknown)" : attribute));
          }
          return true;
        }
      }
    }

    return false;
  }

  private static void computeTypedValue(TypedValue outValue, float value, float scale, String unit) {
    value *= scale;
    boolean neg = value < 0;
    if (neg) {
      value = -value;
    }
    long bits = (long)(value*(1<<23)+.5f);
    int radix;
    int shift;
    if ((bits&0x7fffff) == 0) {
      // Always use 23p0 if there is no fraction, just to make
      // things easier to read.
      radix = TypedValue.COMPLEX_RADIX_23p0;
      shift = 23;
    } else if ((bits&0xffffffffff800000L) == 0) {
      // Magnitude is zero -- can fit in 0 bits of precision.
      radix = TypedValue.COMPLEX_RADIX_0p23;
      shift = 0;
    } else if ((bits&0xffffffff80000000L) == 0) {
      // Magnitude can fit in 8 bits of precision.
      radix = TypedValue.COMPLEX_RADIX_8p15;
      shift = 8;
    } else if ((bits&0xffffff8000000000L) == 0) {
      // Magnitude can fit in 16 bits of precision.
      radix = TypedValue.COMPLEX_RADIX_16p7;
      shift = 16;
    } else {
      // Magnitude needs entire range, so no fractional part.
      radix = TypedValue.COMPLEX_RADIX_23p0;
      shift = 23;
    }
    int mantissa = (int)(
      (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
    if (neg) {
      mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
    }
    outValue.data |=
      (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
      | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);

    if ("%".equals(unit)) {
      value = value * 100;
    }

    outValue.string = value + unit;
  }

  private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
    str = str.trim();

    for (UnitEntry unit : sUnitNames) {
      if (unit.name.equals(str)) {
        applyUnit(unit, outValue, outScale);
        return true;
      }
    }

    return false;
  }

  private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
    outValue.type = unit.type;
    outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
    outScale[0] = unit.scale;
  }
}


package org.robolectric;

import static com.google.common.collect.Lists.reverse;

import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.robolectric.annotation.Config;
import org.robolectric.util.Join;

/**
 * Computes the effective Robolectric configuration for a given test method.
 *
 * This class is no longer used directly by Robolectric, but is left for convenience during
 * migration.
 *
 * @deprecated Provide an implementation of {@link javax.inject.Provider<Config>} instead. See
 *     [Migration Notes](http://robolectric.org/migrating/#migrating-to-40) for details. This
 *     class will be removed in Robolectric 4.3.
 */
@Deprecated
public class ConfigMerger {
  private final Map<String, Config> packageConfigCache = new LinkedHashMap<String, Config>() {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
      return size() > 10;
    }
  };

  /**
   * Calculate the {@link Config} for the given test.
   *
   * @param testClass the class containing the test
   * @param method the test method
   * @param globalConfig global configuration values
   * @return the effective configuration
   * @since 3.2
   */
  public Config getConfig(Class<?> testClass, Method method, Config globalConfig) {
    Config config = Config.Builder.defaults().build();
    config = override(config, globalConfig);

    for (String packageName : reverse(packageHierarchyOf(testClass))) {
      Config packageConfig = cachedPackageConfig(packageName);
      config = override(config, packageConfig);
    }

    for (Class clazz : reverse(parentClassesFor(testClass))) {
      Config classConfig = (Config) clazz.getAnnotation(Config.class);
      config = override(config, classConfig);
    }

    Config methodConfig = method.getAnnotation(Config.class);
    config = override(config, methodConfig);

    return config;
  }

  /**
   * Generate {@link Config} for the specified package.
   *
   * More specific packages, test classes, and test method configurations
   * will override values provided here.
   *
   * The default implementation uses properties provided by {@link #getConfigProperties(String)}.
   *
   * The returned object is likely to be reused for many tests.
   *
   * @param packageName the name of the package, or empty string ({@code ""}) for the top level package
   * @return {@link Config} object for the specified package
   * @since 3.2
   */
  @Nullable
  private Config buildPackageConfig(String packageName) {
    return Config.Implementation.fromProperties(getConfigProperties(packageName));
  }

  /**
   * Return a {@link Properties} file for the given package name, or {@code null} if none is available.
   *
   * @since 3.2
   */
  protected Properties getConfigProperties(String packageName) {
    List<String> packageParts = new ArrayList<>(Arrays.asList(packageName.split("\\.")));
    packageParts.add(RobolectricTestRunner.CONFIG_PROPERTIES);
    final String resourceName = Join.join("/", packageParts);
    try (InputStream resourceAsStream = getResourceAsStream(resourceName)) {
      if (resourceAsStream == null) return null;
      Properties properties = new Properties();
      properties.load(resourceAsStream);
      return properties;
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Nonnull @VisibleForTesting
  List<String> packageHierarchyOf(Class<?> javaClass) {
    Package aPackage = javaClass.getPackage();
    String testPackageName = aPackage == null ? "" : aPackage.getName();
    List<String> packageHierarchy = new ArrayList<>();
    while (!testPackageName.isEmpty()) {
      packageHierarchy.add(testPackageName);
      int lastDot = testPackageName.lastIndexOf('.');
      testPackageName = lastDot > 1 ? testPackageName.substring(0, lastDot) : "";
    }
    packageHierarchy.add("");
    return packageHierarchy;
  }

  @Nonnull
  private List<Class> parentClassesFor(Class testClass) {
    List<Class> testClassHierarchy = new ArrayList<>();
    while (testClass != null && !testClass.equals(Object.class)) {
      testClassHierarchy.add(testClass);
      testClass = testClass.getSuperclass();
    }
    return testClassHierarchy;
  }

  private Config override(Config config, Config classConfig) {
    return classConfig != null ? new Config.Builder(config).overlay(classConfig).build() : config;
  }

  @Nullable
  private Config cachedPackageConfig(String packageName) {
    synchronized (packageConfigCache) {
      Config config = packageConfigCache.get(packageName);
      if (config == null && !packageConfigCache.containsKey(packageName)) {
        config = buildPackageConfig(packageName);
        packageConfigCache.put(packageName, config);
      }
      return config;
    }
  }

  // visible for testing
  @SuppressWarnings("WeakerAccess")
  InputStream getResourceAsStream(String resourceName) {
    return getClass().getClassLoader().getResourceAsStream(resourceName);
  }
}


package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static android.telephony.PhoneStateListener.LISTEN_CALL_STATE;
import static android.telephony.PhoneStateListener.LISTEN_CELL_INFO;
import static android.telephony.PhoneStateListener.LISTEN_CELL_LOCATION;
import static android.telephony.PhoneStateListener.LISTEN_NONE;
import static android.telephony.TelephonyManager.CALL_STATE_IDLE;
import static android.telephony.TelephonyManager.CALL_STATE_RINGING;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.PersistableBundle;
import android.os.SystemProperties;
import android.telecom.PhoneAccountHandle;
import android.telephony.CellInfo;
import android.telephony.CellLocation;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;

@Implements(value = TelephonyManager.class, looseSignatures = true)
public class ShadowTelephonyManager {

  @RealObject
  protected TelephonyManager realTelephonyManager;

  private final Map<PhoneStateListener, Integer> phoneStateRegistrations = new HashMap<>();
  private final Map<Integer, String> slotIndexToDeviceId = new HashMap<>();
  private final Map<Integer, String> slotIndexToImei = new HashMap<>();
  private final Map<Integer, String> slotIndexToMeid = new HashMap<>();
  private final Map<PhoneAccountHandle, Boolean> voicemailVibrationEnabledMap = new HashMap<>();
  private final Map<PhoneAccountHandle, Uri> voicemailRingtoneUriMap = new HashMap<>();
  private final Map<PhoneAccountHandle, TelephonyManager> phoneAccountToTelephonyManagers =
      new HashMap<>();

  private PhoneStateListener lastListener;
  private int lastEventFlags;

  private String deviceId;
  private String imei;
  private String meid;
  private String groupIdLevel1;
  private String networkOperatorName = "";
  private String networkCountryIso;
  private String networkOperator = "";
  private String simOperator;
  private String simOperatorName;
  private String simSerialNumber;
  private boolean readPhoneStatePermission = true;
  private int phoneType = TelephonyManager.PHONE_TYPE_GSM;
  private String line1Number;
  private int networkType;
  private int voiceNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
  private List<CellInfo> allCellInfo = Collections.emptyList();
  private CellLocation cellLocation = null;
  private int callState = CALL_STATE_IDLE;
  private String incomingPhoneNumber = null;
  private boolean isSmsCapable = true;
  private String voiceMailNumber;
  private String voiceMailAlphaTag;
  private int phoneCount = 1;
  private Map<Integer, TelephonyManager> subscriptionIdsToTelephonyManagers = new HashMap<>();
  private PersistableBundle carrierConfig;
  private ServiceState serviceState;
  private boolean isNetworkRoaming;
  private final SparseIntArray simStates = new SparseIntArray();
  private final SparseIntArray currentPhoneTypes = new SparseIntArray();
  private final SparseArray<List<String>> carrierPackageNames = new SparseArray<>();
  private final Map<Integer, String> simCountryIsoMap = new HashMap<>();
  private int simCarrierId;
  private String subscriberId;
  private /*UiccSlotInfo[]*/ Object uiccSlotInfos;
  private String visualVoicemailPackageName = null;
  private SignalStrength signalStrength;
  private boolean dataEnabled = false;

  {
    resetSimStates();
    resetSimCountryIsos();
  }

  @Implementation
  protected void listen(PhoneStateListener listener, int flags) {
    lastListener = listener;
    lastEventFlags = flags;

    if (flags == LISTEN_NONE) {
      phoneStateRegistrations.remove(listener);
    } else {
      initListener(listener, flags);
      phoneStateRegistrations.put(listener, flags);
    }
  }

  /**
   * Returns the most recent listener passed to #listen().
   *
   * @return Phone state listener.
   * @deprecated Avoid using.
   */
  @Deprecated
  public PhoneStateListener getListener() {
    return lastListener;
  }

  /**
   * Returns the most recent flags passed to #listen().
   *
   * @return Event flags.
   * @deprecated Avoid using.
   */
  @Deprecated
  public int getEventFlags() {
    return lastEventFlags;
  }

  /** Call state may be specified via {@link #setCallState(int)}. */
  @Implementation
  protected int getCallState() {
    return callState;
  }

  /** Sets the current call state to the desired state and updates any listeners. */
  public void setCallState(int callState) {
    setCallState(callState, null);
  }

  /**
   * Sets the current call state with the option to specify an incoming phone number for the
   * CALL_STATE_RINGING state. The incoming phone number will be ignored for all other cases.
   */
  public void setCallState(int callState, String incomingPhoneNumber) {
    if (callState != CALL_STATE_RINGING) {
      incomingPhoneNumber = null;
    }

    this.callState = callState;
    this.incomingPhoneNumber = incomingPhoneNumber;

    for (PhoneStateListener listener : getListenersForFlags(LISTEN_CALL_STATE)) {
      listener.onCallStateChanged(callState, incomingPhoneNumber);
    }
  }

  @Implementation
  protected String getDeviceId() {
    checkReadPhoneStatePermission();
    return deviceId;
  }

  public void setDeviceId(String newDeviceId) {
    deviceId = newDeviceId;
  }

  public void setNetworkOperatorName(String networkOperatorName) {
    this.networkOperatorName = networkOperatorName;
  }

  @Implementation(minSdk = LOLLIPOP)
  protected String getImei() {
    checkReadPhoneStatePermission();
    return imei;
  }

  @Implementation(minSdk = O)
  protected String getImei(int slotIndex) {
    checkReadPhoneStatePermission();
    return slotIndexToImei.get(slotIndex);
  }

  /** Set the IMEI returned by getImei(). */
  public void setImei(String imei) {
    this.imei = imei;
  }

  /** Set the IMEI returned by {@link #getImei(int)}. */
  public void setImei(int slotIndex, String imei) {
    slotIndexToImei.put(slotIndex, imei);
  }

  @Implementation(minSdk = O)
  protected String getMeid() {
    checkReadPhoneStatePermission();
    return meid;
  }

  @Implementation(minSdk = O)
  protected String getMeid(int slotIndex) {
    checkReadPhoneStatePermission();
    return slotIndexToMeid.get(slotIndex);
  }

  /** Set the MEID returned by getMeid(). */
  public void setMeid(String meid) {
    this.meid = meid;
  }

  /** Set the MEID returned by {@link #getMeid(int)}. */
  public void setMeid(int slotIndex, String meid) {
    slotIndexToMeid.put(slotIndex, meid);
  }

  @Implementation
  protected String getNetworkOperatorName() {
    return networkOperatorName;
  }

  public void setNetworkCountryIso(String networkCountryIso) {
    this.networkCountryIso = networkCountryIso;
  }

  @Implementation
  protected String getNetworkCountryIso() {
    return networkCountryIso;
  }

  public void setNetworkOperator(String networkOperator) {
    this.networkOperator = networkOperator;
  }

  @Implementation
  protected String getNetworkOperator() {
    return networkOperator;
  }

  @Implementation
  protected String getSimOperator() {
    return simOperator;
  }

  public void setSimOperator(String simOperator) {
    this.simOperator = simOperator;
  }

  @Implementation
  protected String getSimOperatorName() {
    return simOperatorName;
  }

  public void setSimOperatorName(String simOperatorName) {
    this.simOperatorName = simOperatorName;
  }

  @Implementation
  protected String getSimSerialNumber() {
    checkReadPhoneStatePermission();
    return this.simSerialNumber;
  }

  /** sets the serial number that will be returned by {@link #getSimSerialNumber}. */
  public void setSimSerialNumber(String simSerialNumber) {
    this.simSerialNumber = simSerialNumber;
  }

  @Implementation
  protected String getSimCountryIso() {
    return simCountryIsoMap.get(/* subId= */ 0);
  }

  @Implementation(minSdk = N)
  @HiddenApi
  protected String getSimCountryIso(int subId) {
    return simCountryIsoMap.get(subId);
  }

  public void setSimCountryIso(String simCountryIso) {
    setSimCountryIso(/* subId= */ 0, simCountryIso);
  }

  /** Sets the {@code simCountryIso} for the given {@code subId}. */
  public void setSimCountryIso(int subId, String simCountryIso) {
    simCountryIsoMap.put(subId, simCountryIso);
  }

  /** Clears {@code subId} to simCountryIso mapping and resets to default state. */
  public void resetSimCountryIsos() {
    simCountryIsoMap.clear();
    simCountryIsoMap.put(0, "");
  }

  @Implementation
  protected int getSimState() {
    return getSimState(/* slotIndex= */ 0);
  }

  /** Sets the sim state of slot 0. */
  public void setSimState(int simState) {
    setSimState(/* slotIndex= */ 0, simState);
  }

  /** Set the sim state for the given {@code slotIndex}. */
  public void setSimState(int slotIndex, int state) {
    simStates.put(slotIndex, state);
  }

  @Implementation(minSdk = O)
  protected int getSimState(int slotIndex) {
    return simStates.get(slotIndex, TelephonyManager.SIM_STATE_UNKNOWN);
  }

  /** Sets the UICC slots information returned by {@link #getUiccSlotsInfo()}. */
  public void setUiccSlotsInfo(/*UiccSlotInfo[]*/ Object uiccSlotsInfos) {
    this.uiccSlotInfos = uiccSlotsInfos;
  }

  /** Returns the UICC slots information set by {@link #setUiccSlotsInfo}. */
  @Implementation(minSdk = P)
  @HiddenApi
  protected /*UiccSlotInfo[]*/ Object getUiccSlotsInfo() {
    return uiccSlotInfos;
  }

  /** Clears {@code slotIndex} to state mapping and resets to default state. */
  public void resetSimStates() {
    simStates.clear();
    simStates.put(0, TelephonyManager.SIM_STATE_READY);
  }

  public void setReadPhoneStatePermission(boolean readPhoneStatePermission) {
    this.readPhoneStatePermission = readPhoneStatePermission;
  }

  private void checkReadPhoneStatePermission() {
    if (!readPhoneStatePermission) {
      throw new SecurityException();
    }
  }

  @Implementation
  protected int getPhoneType() {
    return phoneType;
  }

  public void setPhoneType(int phoneType) {
    this.phoneType = phoneType;
  }

  @Implementation
  protected String getLine1Number() {
    return line1Number;
  }

  public void setLine1Number(String line1Number) {
    this.line1Number = line1Number;
  }

  @Implementation
  protected int getNetworkType() {
    return networkType;
  }

  public void setNetworkType(int networkType) {
    this.networkType = networkType;
  }

  /**
   * Returns whatever value was set by the last call to {@link #setVoiceNetworkType}, defaulting to
   * {@link TelephonyManager#NETWORK_TYPE_UNKNOWN} if it was never called.
   */
  @Implementation(minSdk = N)
  protected int getVoiceNetworkType() {
    return voiceNetworkType;
  }

  /**
   * Sets the value to be returned by calls to {@link getVoiceNetworkType}. This <b>should</b>
   * correspond to one of the {@code NETWORK_TYPE_*} constants defined on {@link TelephonyManager},
   * but this is not enforced.
   */
  public void setVoiceNetworkType(int voiceNetworkType) {
    this.voiceNetworkType = voiceNetworkType;
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected List<CellInfo> getAllCellInfo() {
    return allCellInfo;
  }

  public void setAllCellInfo(List<CellInfo> allCellInfo) {
    this.allCellInfo = allCellInfo;

    if (VERSION.SDK_INT >= JELLY_BEAN_MR1) {
      for (PhoneStateListener listener : getListenersForFlags(LISTEN_CELL_INFO)) {
        listener.onCellInfoChanged(allCellInfo);
      }
    }
  }

  @Implementation
  protected CellLocation getCellLocation() {
    return this.cellLocation;
  }

  public void setCellLocation(CellLocation cellLocation) {
    this.cellLocation = cellLocation;

    for (PhoneStateListener listener : getListenersForFlags(LISTEN_CELL_LOCATION)) {
      listener.onCellLocationChanged(cellLocation);
    }
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected String getGroupIdLevel1() {
    return this.groupIdLevel1;
  }

  public void setGroupIdLevel1(String groupIdLevel1) {
    this.groupIdLevel1 = groupIdLevel1;
  }

  private void initListener(PhoneStateListener listener, int flags) {
    if ((flags & LISTEN_CALL_STATE) != 0) {
      listener.onCallStateChanged(callState, incomingPhoneNumber);
    }
    if ((flags & LISTEN_CELL_INFO) != 0) {
      if (VERSION.SDK_INT >= JELLY_BEAN_MR1) {
        listener.onCellInfoChanged(allCellInfo);
      }
    }
    if ((flags & LISTEN_CELL_LOCATION) != 0) {
      listener.onCellLocationChanged(cellLocation);
    }
  }

  private Iterable<PhoneStateListener> getListenersForFlags(int flags) {
    return Iterables.filter(
        phoneStateRegistrations.keySet(),
        new Predicate<PhoneStateListener>() {
          @Override
          public boolean apply(PhoneStateListener input) {
            // only select PhoneStateListeners with matching flags
            return (phoneStateRegistrations.get(input) & flags) != 0;
          }
        });
  }

  /** @return `true` by default, or the value specified via {@link #setIsSmsCapable(boolean)} */
  @Implementation
  protected boolean isSmsCapable() {
    return isSmsCapable;
  }

  /** Sets the value returned by {@link TelephonyManager#isSmsCapable()}. */
  public void setIsSmsCapable(boolean isSmsCapable) {
    this.isSmsCapable = isSmsCapable;
  }

  /**
   * Returns a new empty {@link PersistableBundle} by default, or the value specified via {@link
   * #setCarrierConfig(PersistableBundle)}.
   */
  @Implementation(minSdk = O)
  protected PersistableBundle getCarrierConfig() {
    return carrierConfig != null ? carrierConfig : new PersistableBundle();
  }

  /**
   * Sets the value returned by {@link TelephonyManager#getCarrierConfig()}.
   *
   * @param carrierConfig
   */
  public void setCarrierConfig(PersistableBundle carrierConfig) {
    this.carrierConfig = carrierConfig;
  }

  /**
   * Returns {@code null} by default, or the value specified via {@link
   * #setVoiceMailNumber(String)}.
   */
  @Implementation
  protected String getVoiceMailNumber() {
    return voiceMailNumber;
  }

  /** Sets the value returned by {@link TelephonyManager#getVoiceMailNumber()}. */
  public void setVoiceMailNumber(String voiceMailNumber) {
    this.voiceMailNumber = voiceMailNumber;
  }

  /**
   * Returns {@code null} by default or the value specified via {@link
   * #setVoiceMailAlphaTag(String)}.
   */
  @Implementation
  protected String getVoiceMailAlphaTag() {
    return voiceMailAlphaTag;
  }

  /** Sets the value returned by {@link TelephonyManager#getVoiceMailAlphaTag()}. */
  public void setVoiceMailAlphaTag(String voiceMailAlphaTag) {
    this.voiceMailAlphaTag = voiceMailAlphaTag;
  }

  /** Returns 1 by default or the value specified via {@link #setPhoneCount(int)}. */
  @Implementation(minSdk = M)
  protected int getPhoneCount() {
    return phoneCount;
  }

  /** Sets the value returned by {@link TelephonyManager#getPhoneCount()}. */
  public void setPhoneCount(int phoneCount) {
    this.phoneCount = phoneCount;
  }

  /**
   * Returns {@code null} by default or the value specified via {@link #setDeviceId(int, String)}.
   */
  @Implementation(minSdk = M)
  protected String getDeviceId(int slot) {
    return slotIndexToDeviceId.get(slot);
  }

  /** Sets the value returned by {@link TelephonyManager#getDeviceId(int)}. */
  public void setDeviceId(int slot, String deviceId) {
    slotIndexToDeviceId.put(slot, deviceId);
  }

  /**
   * Returns {@code null} by default or the value specified via {@link
   * #setVoicemailVibrationEnabled(PhoneAccountHandle, boolean)}.
   */
  @Implementation(minSdk = N)
  protected boolean isVoicemailVibrationEnabled(PhoneAccountHandle handle) {
    Boolean result = voicemailVibrationEnabledMap.get(handle);
    return result != null ? result : false;
  }

  /**
   * Sets the value returned by {@link
   * TelephonyManager#isVoicemailVibrationEnabled(PhoneAccountHandle)}.
   */
  @Implementation(minSdk = O)
  protected void setVoicemailVibrationEnabled(PhoneAccountHandle handle, boolean isEnabled) {
    voicemailVibrationEnabledMap.put(handle, isEnabled);
  }

  /**
   * Returns {@code null} by default or the value specified via {@link
   * #setVoicemailRingtoneUri(PhoneAccountHandle, Uri)}.
   */
  @Implementation(minSdk = N)
  protected Uri getVoicemailRingtoneUri(PhoneAccountHandle handle) {
    return voicemailRingtoneUriMap.get(handle);
  }

  /**
   * Sets the value returned by {@link
   * TelephonyManager#getVoicemailRingtoneUri(PhoneAccountHandle)}.
   */
  @Implementation(minSdk = O)
  protected void setVoicemailRingtoneUri(PhoneAccountHandle handle, Uri uri) {
    voicemailRingtoneUriMap.put(handle, uri);
  }

  /**
   * Returns {@code null} by default or the value specified via {@link
   * #setTelephonyManagerForHandle(PhoneAccountHandle, TelephonyManager)}.
   */
  @Implementation(minSdk = O)
  protected TelephonyManager createForPhoneAccountHandle(PhoneAccountHandle handle) {
    return phoneAccountToTelephonyManagers.get(handle);
  }

  /**
   * Sets the value returned by {@link
   * TelephonyManager#createForPhoneAccountHandle(PhoneAccountHandle)}.
   */
  public void setTelephonyManagerForHandle(
      PhoneAccountHandle handle, TelephonyManager telephonyManager) {
    phoneAccountToTelephonyManagers.put(handle, telephonyManager);
  }

  /**
   * Returns {@code null} by default or the value specified via {@link
   * #setTelephonyManagerForSubscriptionId(int, TelephonyManager)}
   */
  @Implementation(minSdk = N)
  protected TelephonyManager createForSubscriptionId(int subId) {
    return subscriptionIdsToTelephonyManagers.get(subId);
  }

  /** Sets the value returned by {@link TelephonyManager#createForSubscriptionId(int)}. */
  public void setTelephonyManagerForSubscriptionId(
      int subscriptionId, TelephonyManager telephonyManager) {
    subscriptionIdsToTelephonyManagers.put(subscriptionId, telephonyManager);
  }

  /**
   * Returns {@code null} by default or the value specified via {@link
   * #setServiceState(ServiceState)}
   */
  @Implementation(minSdk = O)
  protected ServiceState getServiceState() {
    return serviceState;
  }

  /** Sets the value returned by {@link TelephonyManager#getServiceState()}. */
  public void setServiceState(ServiceState serviceState) {
    this.serviceState = serviceState;
  }

  /**
   * Returns {@code false} by default or the value specified via {@link
   * #setIsNetworkRoaming(boolean)}
   */
  @Implementation
  protected boolean isNetworkRoaming() {
    return isNetworkRoaming;
  }

  /** Sets the value returned by {@link TelephonyManager#isNetworkRoaming()}. */
  public void setIsNetworkRoaming(boolean isNetworkRoaming) {
    this.isNetworkRoaming = isNetworkRoaming;
  }

  @Implementation(minSdk = M)
  @HiddenApi
  protected int getCurrentPhoneType(int subId) {
    return currentPhoneTypes.get(subId, TelephonyManager.PHONE_TYPE_NONE);
  }

  /** Sets the phone type for the given {@code subId}. */
  public void setCurrentPhoneType(int subId, int phoneType) {
    currentPhoneTypes.put(subId, phoneType);
  }

  /** Removes all {@code subId} to {@code phoneType} mappings. */
  public void clearPhoneTypes() {
    currentPhoneTypes.clear();
  }

  @Implementation(minSdk = M)
  @HiddenApi
  protected List<String> getCarrierPackageNamesForIntentAndPhone(Intent intent, int phoneId) {
    return carrierPackageNames.get(phoneId);
  }

  @Implementation(minSdk = LOLLIPOP)
  @HiddenApi
  protected List<String> getCarrierPackageNamesForIntent(Intent intent) {
    return carrierPackageNames.get(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
  }

  /** Sets the {@code packages} for the given {@code phoneId}. */
  public void setCarrierPackageNamesForPhone(int phoneId, List<String> packages) {
    carrierPackageNames.put(phoneId, packages);
  }

  @Implementation(minSdk = P)
  protected int getSimCarrierId() {
    return simCarrierId;
  }

  /** Sets the value to be returned by {@link #getSimCarrierId()}. */
  public void setSimCarrierId(int simCarrierId) {
    this.simCarrierId = simCarrierId;
  }

  @Implementation
  protected String getSubscriberId() {
    return subscriberId;
  }

  /** Sets the value to be returned by {@link #getSubscriberId()}. */
  public void setSubscriberId(String subscriberId) {
    this.subscriberId = subscriberId;
  }

  /** Returns the value set by {@link #setVisualVoicemailPackageName(String)}. */
  @Implementation(minSdk = O)
  protected String getVisualVoicemailPackageName() {
    return visualVoicemailPackageName;
  }

  /** Sets the value to be returned by {@link #getVisualVoicemailPackageName()}. */
  public void setVisualVoicemailPackageName(String visualVoicemailPackageName) {
    this.visualVoicemailPackageName = visualVoicemailPackageName;
  }

  @Implementation(minSdk = P)
  protected SignalStrength getSignalStrength() {
    return signalStrength;
  }

  /** Sets the value to be returned by {@link #getSignalStrength()} */
  public void setSignalStrength(SignalStrength signalStrength) {
    this.signalStrength = signalStrength;
    for (PhoneStateListener listener :
        getListenersForFlags(PhoneStateListener.LISTEN_SIGNAL_STRENGTHS)) {
      listener.onSignalStrengthsChanged(signalStrength);
    }
  }

  /**
   * Cribbed from {@link android.telephony.PhoneNumberUtils#isEmergencyNumberInternal}.
   *
   * TODO(b/122324733) need better implementation
   */
  @Implementation(minSdk = Build.VERSION_CODES.Q)
  protected boolean isCurrentEmergencyNumber(String number) {

    if (number == null) {
      return false;
    }

    Context context = ReflectionHelpers.getField(realTelephonyManager, "mContext");
    Locale locale = context == null ? null : context.getResources().getConfiguration().locale;
    String defaultCountryIso = locale == null ? null : locale.getCountry();

    int slotId = -1;
    boolean useExactMatch = true;

    // retrieve the list of emergency numbers
    // check read-write ecclist property first
    String ecclist = (slotId <= 0) ? "ril.ecclist" : ("ril.ecclist" + slotId);

    String emergencyNumbers = SystemProperties.get(ecclist, "");

    if (TextUtils.isEmpty(emergencyNumbers)) {
      // then read-only ecclist property since old RIL only uses this
      emergencyNumbers = SystemProperties.get("ro.ril.ecclist");
    }

    if (!TextUtils.isEmpty(emergencyNumbers)) {
      // searches through the comma-separated list for a match,
      // return true if one is found.
      for (String emergencyNum : emergencyNumbers.split(",")) {
        // It is not possible to append additional digits to an emergency number to dial
        // the number in Brazil - it won't connect.
        if (useExactMatch || "BR".equalsIgnoreCase(defaultCountryIso)) {
          if (number.equals(emergencyNum)) {
            return true;
          }
        } else {
          if (number.startsWith(emergencyNum)) {
            return true;
          }
        }
      }
      // no matches found against the list!
      return false;
    }

    emergencyNumbers = ((slotId < 0) ? "112,911,000,08,110,118,119,999" : "112,911");
    for (String emergencyNum : emergencyNumbers.split(",")) {
      if (useExactMatch) {
        if (number.equals(emergencyNum)) {
          return true;
        }
      } else {
        if (number.startsWith(emergencyNum)) {
          return true;
        }
      }
    }

    return false;
  }

  @Implementation(minSdk = Build.VERSION_CODES.Q)
  protected boolean isCurrentPotentialEmergencyNumber(String number) {
    return isCurrentEmergencyNumber(number);
  }

  /**
   * Implementation for {@link TelephonyManager#isDataEnabled}.
   * @return False by default, unless set with {@link TelephonyManager#setDataEnabled}.
   */
  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected boolean isDataEnabled() {
    return dataEnabled;
  }

  /** Implementation for {@link TelephonyManager#setDataEnabled}. */
  @Implementation(minSdk = Build.VERSION_CODES.O)
  protected void setDataEnabled(boolean enabled) {
    dataEnabled = enabled;
  }
}


package org.robolectric.junit.rules;

import android.util.Log;
import com.google.common.collect.ImmutableSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowLog.LogItem;

/**
 * Allows tests to assert about the presence of log messages, and turns logged errors that are not
 * explicitly expected into test failures.
 */
public final class ExpectedLogMessagesRule implements TestRule {
  /** Tags that apps can't prevent. We whitelist them globally. */
  private static final ImmutableSet<String> UNPREVENTABLE_TAGS =
      ImmutableSet.of("Typeface", "RingtoneManager");

  private final Set<LogItem> expectedLogs = new HashSet<>();
  private final Set<LogItem> observedLogs = new HashSet<>();
  private final Set<String> expectedTags = new HashSet<>();
  private final Set<String> observedTags = new HashSet<>();

  private boolean shouldIgnoreMissingLoggedTags = false;

  @Override
  public Statement apply(final Statement base, Description description) {
    return new Statement() {
      @Override
      public void evaluate() throws Throwable {
        base.evaluate();
        List<LogItem> logs = ShadowLog.getLogs();
        for (LogItem log : logs) {
          LogItem throwLessLogItem = new LogItem(log.type, log.tag, log.msg, null);
          if (expectedLogs.contains(throwLessLogItem)) {
            observedLogs.add(throwLessLogItem);
            continue;
          }
          if (log.type >= Log.ERROR) {
            if (UNPREVENTABLE_TAGS.contains(log.tag)) {
              continue;
            }
            if (expectedTags.contains(log.tag)) {
              observedTags.add(log.tag);
              continue;
            }
            if (log.throwable != null) {
              throw new AssertionError(
                  "Unexpected error log message: " + log.tag + ": " + log.msg, log.throwable);
            } else {
              throw new AssertionError("Unexpected error log message: " + log.tag + ": " + log.msg);
            }
          }
        }
        if (!expectedLogs.equals(observedLogs)) {
          throw new AssertionError(
              "Some expected logs were not printed."
                  + "\nExpected: "
                  + expectedLogs
                  + "\nObserved: "
                  + observedLogs);
        }
        if (!expectedTags.equals(observedTags) && !shouldIgnoreMissingLoggedTags) {
          throw new AssertionError(
              "Some expected tags were not printed. "
                  + "Expected tags should not be used to suppress errors, only expect them."
                  + "\nExpected: "
                  + expectedTags
                  + "\nObserved: "
                  + observedTags);
        }
      }
    };
  }

  /**
   * Adds an expected log statement. If this log is not printed during test execution, the test case
   * will fail. Do not use this to suppress failures. Use this to test that expected error cases in
   * your code cause log messages to be printed.
   */
  public void expectLogMessage(int level, String tag, String message) {
    checkTag(tag);
    expectedLogs.add(new LogItem(level, tag, message, null));
  }

  /**
   * Blanket suppress test failures due to errors from a tag. If this tag is not printed at
   * Log.ERROR during test execution, the test case will fail (unless {@link
   * #ignoreMissingLoggedTags()} is used).
   *
   *
Avoid using this method when possible. Prefer to assert on the presence of a specific
   * message using {@link #expectLogMessage} in test cases that *intentionally* trigger an error.
   */
  public void expectErrorsForTag(String tag) {
    checkTag(tag);
    if (UNPREVENTABLE_TAGS.contains(tag)) {
      throw new AssertionError("Tag `" + tag + "` is already suppressed.");
    }
    expectedTags.add(tag);
  }

  /**
   * If set true, tests that call {@link #expectErrorsForTag()} but do not log errors for the given
   * tag will not fail. By default this is false.
   *
   *
Avoid using this method when possible. Prefer tests that print (or do not print) log
   * messages deterministically.
   */
  public void ignoreMissingLoggedTags(boolean shouldIgnore) {
    shouldIgnoreMissingLoggedTags = shouldIgnore;
  }

  private void checkTag(String tag) {
    if (tag.length() > 23) {
      throw new IllegalArgumentException("Tag length cannot exceed 23 characters: " + tag);
    }
  }
}


package org.robolectric.shadows;

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

import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
import android.util.SparseArray;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@Implements(value = CarrierConfigManager.class, minSdk = M)
public class ShadowCarrierConfigManager {

  private SparseArray<PersistableBundle> bundles = new SparseArray<>();

  /**
   * Returns {@link android.os.PersistableBundle} previously set by {@link #setConfigForSubId(int)},
   * or default values for an invalid {@code subId}.
   */
  @Implementation
  protected PersistableBundle getConfigForSubId(int subId) {
    PersistableBundle persistableBundle = bundles.get(subId);
    if (persistableBundle == null) {
      return new PersistableBundle();
    }
    return persistableBundle;
  }

  /**
   * Sets that the {@code config} PersistableBundle for a particular {@code subId}; controls the
   * return value of {@link CarrierConfigManager#getConfigForSubId()}.
   */
  public void setConfigForSubId(int subId, PersistableBundle config) {
    bundles.put(subId, config);
  }
}


package org.robolectric.manifest;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Holds parsed service data from manifest.
 */
public class ServiceData extends PackageItemData {

  private static final String EXPORTED = "android:exported";
  private static final String NAME = "android:name";
  private static final String PERMISSION = "android:permission";
  private static final String ENABLED = "android:enabled";

  private final Map<String, String> attributes;
  private final List<String> actions;
  private List<IntentFilterData> intentFilters;

  public ServiceData(
      Map<String, String> attributes, MetaData metaData, List<IntentFilterData> intentFilters) {
    super(attributes.get(NAME), metaData);
    this.attributes = attributes;
    this.actions = new ArrayList<>();
    this.intentFilters = new ArrayList<>(intentFilters);
  }

  public List<String> getActions() {
    return actions;
  }

  public void addAction(String action) {
    this.actions.add(action);
  }

  public void setPermission(final String permission) {
    attributes.put(PERMISSION, permission);
  }

  public String getPermission() {
    return attributes.get(PERMISSION);
  }

  /**
   * Get the intent filters defined for the service.
   *
   * @return A list of intent filters.
   */
  public List<IntentFilterData> getIntentFilters() {
    return intentFilters;
  }

  /**
   * Get the map for all attributes defined for the service.
   *
   * @return map of attributes names to values from the manifest.
   */
  public Map<String, String> getAllAttributes() {
    return attributes;
  }

  /**
   * Returns whether this service is exported by checking the XML attribute.
   *
   * @return true if the service is exported
   */
  public boolean isExported() {
    boolean defaultValue = !intentFilters.isEmpty();
    return (attributes.containsKey(EXPORTED)
        ? Boolean.parseBoolean(attributes.get(EXPORTED))
        : defaultValue);
  }

  public boolean isEnabled() {
    return attributes.containsKey(ENABLED) ? Boolean.parseBoolean(attributes.get(ENABLED)) : true;
  }
}


package org.robolectric.android.controller;

import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowContentResolver;
import org.robolectric.util.Logger;

public class ContentProviderController<T extends ContentProvider>  {
  private T contentProvider;

  private ContentProviderController(T contentProvider) {
    this.contentProvider = contentProvider;
  }

  public static <T extends ContentProvider> ContentProviderController<T> of(T contentProvider) {
    return new ContentProviderController<>(contentProvider);
  }

  /**
   * Create and register {@link ContentProvider} using {@link ProviderInfo} found from manifest.
   */
  public ContentProviderController<T> create() {
    Context baseContext = RuntimeEnvironment.application.getBaseContext();

    ComponentName componentName = createRelative(baseContext.getPackageName(), contentProvider.getClass().getName());

    ProviderInfo providerInfo = null;
    try {
      providerInfo =
          baseContext
              .getPackageManager()
              .getProviderInfo(componentName, PackageManager.MATCH_DISABLED_COMPONENTS);
    } catch (PackageManager.NameNotFoundException e) {
      Logger.strict("Unable to find provider info for " + componentName, e);
    }

    return create(providerInfo);
  }

  /**
   * Create and register {@link ContentProvider} using {@link ProviderInfo} found from manifest.
   *
   * @param authority the authority to use
   * @return this {@link ContentProviderController}
   */
  public ContentProviderController<T> create(String authority) {
    ProviderInfo providerInfo = new ProviderInfo();
    providerInfo.authority = authority;
    return create(providerInfo);
  }

  /**
   * Create and register {@link ContentProvider} using the given {@link ProviderInfo}.
   *
   * @param providerInfo the {@link ProviderInfo} to use
   * @return this {@link ContentProviderController}
   */
  public ContentProviderController<T> create(ProviderInfo providerInfo) {
    Context baseContext = RuntimeEnvironment.application.getBaseContext();
    // make sure the component is enabled
    ComponentName componentName =
        createRelative(baseContext.getPackageName(), contentProvider.getClass().getName());
    baseContext
        .getPackageManager()
        .setComponentEnabledSetting(
            componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
    contentProvider.attachInfo(baseContext, providerInfo);

    if (providerInfo != null) {
      ShadowContentResolver.registerProviderInternal(providerInfo.authority, contentProvider);
    }

    return this;
  }

  public T get() {
    return contentProvider;
  }

  public ContentProviderController<T> shutdown() {
    contentProvider.shutdown();
    return this;
  }

  private static ComponentName createRelative(String pkg, String cls) {
    final String fullName;
    if (cls.charAt(0) == '.') {
      // Relative to the package. Prepend the package name.
      fullName = pkg + cls;
    } else {
      // Fully qualified package name.
      fullName = cls;
    }
    return new ComponentName(pkg, fullName);
  }
}


package org.robolectric.shadows;

import java.awt.geom.CubicCurve2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
import java.util.ArrayList;

/**
 * Class that returns iterators for a given path. These iterators are lightweight and can be reused
 * multiple times to iterate over the path.
 *
 * <p>copied from
 * https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java
 */
public class CachedPathIteratorFactory {
  /*
   * A few conventions used in the code:
   * Coordinates or coords arrays store segment coordinates. They use the same format as
   * PathIterator#currentSegment coordinates array.
   * float arrays store always points where the first element is X and the second is Y.
   */

  // This governs how accurate the approximation of the Path is.
  private static final float PRECISION = 0.002f;

  private final int mWindingRule;
  private final int[] mTypes;
  private final float[][] mCoordinates;
  private final float[] mSegmentsLength;
  private final float mTotalLength;

  public CachedPathIteratorFactory(PathIterator iterator) {
    mWindingRule = iterator.getWindingRule();

    ArrayList<Integer> typesArray = new ArrayList<>();
    ArrayList<float[]> pointsArray = new ArrayList<>();
    float[] points = new float[6];
    while (!iterator.isDone()) {
      int type = iterator.currentSegment(points);
      int nPoints = getNumberOfPoints(type) * 2; // 2 coordinates per point

      typesArray.add(type);
      float[] itemPoints = new float[nPoints];
      System.arraycopy(points, 0, itemPoints, 0, nPoints);
      pointsArray.add(itemPoints);
      iterator.next();
    }

    mTypes = new int[typesArray.size()];
    mCoordinates = new float[mTypes.length][];
    for (int i = 0; i < typesArray.size(); i++) {
      mTypes[i] = typesArray.get(i);
      mCoordinates[i] = pointsArray.get(i);
    }

    // Do measurement
    mSegmentsLength = new float[mTypes.length];

    // Curves that we can reuse to estimate segments length
    CubicCurve2D.Float cubicCurve = new CubicCurve2D.Float();
    QuadCurve2D.Float quadCurve = new QuadCurve2D.Float();
    float lastX = 0;
    float lastY = 0;
    float totalLength = 0;
    for (int i = 0; i < mTypes.length; i++) {
      switch (mTypes[i]) {
        case PathIterator.SEG_CUBICTO:
          cubicCurve.setCurve(
              lastX,
              lastY,
              mCoordinates[i][0],
              mCoordinates[i][1],
              mCoordinates[i][2],
              mCoordinates[i][3],
              lastX = mCoordinates[i][4],
              lastY = mCoordinates[i][5]);
          mSegmentsLength[i] = getFlatPathLength(cubicCurve.getPathIterator(null, PRECISION));
          break;
        case PathIterator.SEG_QUADTO:
          quadCurve.setCurve(
              lastX,
              lastY,
              mCoordinates[i][0],
              mCoordinates[i][1],
              lastX = mCoordinates[i][2],
              lastY = mCoordinates[i][3]);
          mSegmentsLength[i] = getFlatPathLength(quadCurve.getPathIterator(null, PRECISION));
          break;
        case PathIterator.SEG_CLOSE:
          mSegmentsLength[i] =
              (float)
                  Point2D.distance(
                      lastX, lastY, lastX = mCoordinates[0][0], lastY = mCoordinates[0][1]);
          mCoordinates[i] = new float[2];
          // We convert a SEG_CLOSE segment to a SEG_LINETO so we do not have to worry
          // about this special case in the rest of the code.
          mTypes[i] = PathIterator.SEG_LINETO;
          mCoordinates[i][0] = mCoordinates[0][0];
          mCoordinates[i][1] = mCoordinates[0][1];
          break;
        case PathIterator.SEG_MOVETO:
          mSegmentsLength[i] = 0;
          lastX = mCoordinates[i][0];
          lastY = mCoordinates[i][1];
          break;
        case PathIterator.SEG_LINETO:
          mSegmentsLength[i] =
              (float) Point2D.distance(lastX, lastY, mCoordinates[i][0], mCoordinates[i][1]);
          lastX = mCoordinates[i][0];
          lastY = mCoordinates[i][1];
          break;
        default:
      }
      totalLength += mSegmentsLength[i];
    }

    mTotalLength = totalLength;
  }

  private static void quadCurveSegment(float[] coords, float t0, float t1) {
    // Calculate X and Y at 0.5 (We'll use this to reconstruct the control point later)
    float mt = t0 + (t1 - t0) / 2;
    float mu = 1 - mt;
    float mx = mu * mu * coords[0] + 2 * mu * mt * coords[2] + mt * mt * coords[4];
    float my = mu * mu * coords[1] + 2 * mu * mt * coords[3] + mt * mt * coords[5];

    float u0 = 1 - t0;
    float u1 = 1 - t1;

    // coords at t0
    coords[0] = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0;
    coords[1] = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0;

    // coords at t1
    coords[4] = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1;
    coords[5] = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1;

    // estimated control point at t'=0.5
    coords[2] = 2 * mx - coords[0] / 2 - coords[4] / 2;
    coords[3] = 2 * my - coords[1] / 2 - coords[5] / 2;
  }

  private static void cubicCurveSegment(float[] coords, float t0, float t1) {
    // http://stackoverflow.com/questions/11703283/cubic-bezier-curve-segment
    float u0 = 1 - t0;
    float u1 = 1 - t1;

    // Calculate the points at t0 and t1 for the quadratic curves formed by (P0, P1, P2) and
    // (P1, P2, P3)
    float qxa = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0;
    float qxb = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1;
    float qxc = coords[2] * u0 * u0 + coords[4] * 2 * t0 * u0 + coords[6] * t0 * t0;
    float qxd = coords[2] * u1 * u1 + coords[4] * 2 * t1 * u1 + coords[6] * t1 * t1;

    float qya = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0;
    float qyb = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1;
    float qyc = coords[3] * u0 * u0 + coords[5] * 2 * t0 * u0 + coords[7] * t0 * t0;
    float qyd = coords[3] * u1 * u1 + coords[5] * 2 * t1 * u1 + coords[7] * t1 * t1;

    // Linear interpolation
    coords[0] = qxa * u0 + qxc * t0;
    coords[1] = qya * u0 + qyc * t0;

    coords[2] = qxa * u1 + qxc * t1;
    coords[3] = qya * u1 + qyc * t1;

    coords[4] = qxb * u0 + qxd * t0;
    coords[5] = qyb * u0 + qyd * t0;

    coords[6] = qxb * u1 + qxd * t1;
    coords[7] = qyb * u1 + qyd * t1;
  }

  /**
   * Returns the end point of a given segment
   *
   * @param type the segment type
   * @param coords the segment coordinates array
   * @param point the return array where the point will be stored
   */
  private static void getShapeEndPoint(int type, float[] coords, float[] point) {
    // start index of the end point for the segment type
    int pointIndex = (getNumberOfPoints(type) - 1) * 2;
    point[0] = coords[pointIndex];
    point[1] = coords[pointIndex + 1];
  }

  /** Returns the number of points stored in a coordinates array for the given segment type. */
  private static int getNumberOfPoints(int segmentType) {
    switch (segmentType) {
      case PathIterator.SEG_QUADTO:
        return 2;
      case PathIterator.SEG_CUBICTO:
        return 3;
      case PathIterator.SEG_CLOSE:
        return 0;
      default:
        return 1;
    }
  }

  /**
   * Returns the estimated length of a flat path. If the passed path is not flat (i.e. contains a
   * segment that is not {@link PathIterator#SEG_CLOSE}, {@link PathIterator#SEG_MOVETO} or {@link
   * PathIterator#SEG_LINETO} this method will fail.
   */
  private static float getFlatPathLength(PathIterator iterator) {
    float segment[] = new float[6];
    float totalLength = 0;
    float[] previousPoint = new float[2];
    boolean isFirstPoint = true;

    while (!iterator.isDone()) {
      int type = iterator.currentSegment(segment);
      assert type == PathIterator.SEG_LINETO
          || type == PathIterator.SEG_CLOSE
          || type == PathIterator.SEG_MOVETO;

      // MoveTo shouldn't affect the length
      if (!isFirstPoint && type != PathIterator.SEG_MOVETO) {
        totalLength +=
            (float) Point2D.distance(previousPoint[0], previousPoint[1], segment[0], segment[1]);
      } else {
        isFirstPoint = false;
      }
      previousPoint[0] = segment[0];
      previousPoint[1] = segment[1];
      iterator.next();
    }

    return totalLength;
  }

  /** Returns the estimated position along a path of the given length. */
  private void getPointAtLength(
      int type, float[] coords, float lastX, float lastY, float t, float[] point) {
    if (type == PathIterator.SEG_LINETO) {
      point[0] = lastX + (coords[0] - lastX) * t;
      point[1] = lastY + (coords[1] - lastY) * t;
      // Return here, since we do not need a shape to estimate
      return;
    }

    float[] curve = new float[8];
    int lastPointIndex = (getNumberOfPoints(type) - 1) * 2;

    System.arraycopy(coords, 0, curve, 2, coords.length);
    curve[0] = lastX;
    curve[1] = lastY;
    if (type == PathIterator.SEG_CUBICTO) {
      cubicCurveSegment(curve, 0f, t);
    } else {
      quadCurveSegment(curve, 0f, t);
    }

    point[0] = curve[2 + lastPointIndex];
    point[1] = curve[2 + lastPointIndex + 1];
  }

  public CachedPathIterator iterator() {
    return new CachedPathIterator();
  }

  /** Class that allows us to iterate over a path multiple times */
  public class CachedPathIterator implements PathIterator {
    private int mNextIndex;

    /**
     * Current segment type.
     *
     * @see PathIterator
     */
    private int mCurrentType;

    /**
     * Stores the coordinates array of the current segment. The number of points stored depends on
     * the segment type.
     *
     * @see PathIterator
     */
    private float[] mCurrentCoords = new float[6];

    private float mCurrentSegmentLength;

    /**
     * Current segment length offset. When asking for the length of the current segment, the length
     * will be reduced by this amount. This is useful when we are only using portions of the
     * segment.
     *
     * @see #jumpToSegment(float)
     */
    private float mOffsetLength;

    /** Point where the current segment started */
    private float[] mLastPoint = new float[2];

    private boolean isIteratorDone;

    private CachedPathIterator() {
      next();
    }

    public float getCurrentSegmentLength() {
      return mCurrentSegmentLength;
    }

    @Override
    public int getWindingRule() {
      return mWindingRule;
    }

    @Override
    public boolean isDone() {
      return isIteratorDone;
    }

    @Override
    public void next() {
      if (mNextIndex >= mTypes.length) {
        isIteratorDone = true;
        return;
      }

      if (mNextIndex >= 1) {
        // We've already called next() once so there is a previous segment in this path.
        // We want to get the coordinates where the path ends.
        getShapeEndPoint(mCurrentType, mCurrentCoords, mLastPoint);
      } else {
        // This is the first segment, no previous point so initialize to 0, 0
        mLastPoint[0] = mLastPoint[1] = 0f;
      }
      mCurrentType = mTypes[mNextIndex];
      mCurrentSegmentLength = mSegmentsLength[mNextIndex] - mOffsetLength;

      if (mOffsetLength > 0f && (mCurrentType == SEG_CUBICTO || mCurrentType == SEG_QUADTO)) {
        // We need to skip part of the start of the current segment (because
        // mOffsetLength > 0)
        float[] points = new float[8];

        if (mNextIndex < 1) {
          points[0] = points[1] = 0f;
        } else {
          getShapeEndPoint(mTypes[mNextIndex - 1], mCoordinates[mNextIndex - 1], points);
        }

        System.arraycopy(mCoordinates[mNextIndex], 0, points, 2, mCoordinates[mNextIndex].length);
        float t0 =
            (mSegmentsLength[mNextIndex] - mCurrentSegmentLength) / mSegmentsLength[mNextIndex];
        if (mCurrentType == SEG_CUBICTO) {
          cubicCurveSegment(points, t0, 1f);
        } else {
          quadCurveSegment(points, t0, 1f);
        }
        System.arraycopy(points, 2, mCurrentCoords, 0, mCoordinates[mNextIndex].length);
      } else {
        System.arraycopy(
            mCoordinates[mNextIndex], 0, mCurrentCoords, 0, mCoordinates[mNextIndex].length);
      }

      mOffsetLength = 0f;
      mNextIndex++;
    }

    @Override
    public int currentSegment(float[] coords) {
      System.arraycopy(mCurrentCoords, 0, coords, 0, getNumberOfPoints(mCurrentType) * 2);
      return mCurrentType;
    }

    @Override
    public int currentSegment(double[] coords) {
      throw new UnsupportedOperationException();
    }

    /** Returns the point where the current segment ends */
    public void getCurrentSegmentEnd(float[] point) {
      point[0] = mLastPoint[0];
      point[1] = mLastPoint[1];
    }

    /** Restarts the iterator and jumps all the segments of this path up to the length value. */
    public void jumpToSegment(float length) {
      isIteratorDone = false;
      if (length <= 0f) {
        mNextIndex = 0;
        return;
      }

      float accLength = 0;
      float lastPoint[] = new float[2];
      for (mNextIndex = 0; mNextIndex < mTypes.length; mNextIndex++) {
        float segmentLength = mSegmentsLength[mNextIndex];
        if (accLength + segmentLength >= length && mTypes[mNextIndex] != SEG_MOVETO) {
          float[] estimatedPoint = new float[2];
          getPointAtLength(
              mTypes[mNextIndex],
              mCoordinates[mNextIndex],
              lastPoint[0],
              lastPoint[1],
              (length - accLength) / segmentLength,
              estimatedPoint);

          // This segment makes us go further than length so we go back one step,
          // set a moveto and offset the length of the next segment by the length
          // of this segment that we've already used.
          mCurrentType = PathIterator.SEG_MOVETO;
          mCurrentCoords[0] = estimatedPoint[0];
          mCurrentCoords[1] = estimatedPoint[1];
          mCurrentSegmentLength = 0;

          // We need to offset next path length to account for the segment we've just
          // skipped.
          mOffsetLength = length - accLength;
          return;
        }
        accLength += segmentLength;
        getShapeEndPoint(mTypes[mNextIndex], mCoordinates[mNextIndex], lastPoint);
      }
    }

    /**
     * Returns the current segment up to certain length. If the current segment is shorter than
     * length, then the whole segment is returned. The segment coordinates are copied into the
     * coords array.
     *
     * @return the segment type
     */
    public int currentSegment(float[] coords, float length) {
      int type = currentSegment(coords);
      // If the length is greater than the current segment length, no need to find
      // the cut point. Same if this is a SEG_MOVETO.
      if (mCurrentSegmentLength <= length || type == SEG_MOVETO) {
        return type;
      }

      float t = length / getCurrentSegmentLength();

      // We find at which offset the end point is located within the coords array and set
      // a new end point to cut the segment short
      switch (type) {
        case SEG_CUBICTO:
        case SEG_QUADTO:
          float[] curve = new float[8];
          curve[0] = mLastPoint[0];
          curve[1] = mLastPoint[1];
          System.arraycopy(coords, 0, curve, 2, coords.length);
          if (type == SEG_CUBICTO) {
            cubicCurveSegment(curve, 0f, t);
          } else {
            quadCurveSegment(curve, 0f, t);
          }
          System.arraycopy(curve, 2, coords, 0, coords.length);
          break;
        default:
          float[] point = new float[2];
          getPointAtLength(type, coords, mLastPoint[0], mLastPoint[1], t, point);
          coords[0] = point[0];
          coords[1] = point[1];
      }

      return type;
    }

    /** Returns the total length of the path */
    public float getTotalLength() {
      return mTotalLength;
    }
  }
}