/*
 * Copyright (c) 2014, Cloudera, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.common.io;

import java.io.Closeable;
import java.io.IOException;
import java.net.ServerSocket;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * I/O-related utility methods.
 */
public final class IOUtils {

  private static final Logger log = LoggerFactory.getLogger(IOUtils.class);

  private IOUtils() {}

  /**
   * Deletes the given path, and if it is a directory, all files and subdirectories within it.
   *
   * @param rootDir directory to delete
   * @throws IOException if any error occurs while deleting files or directories
   */
  public static void deleteRecursively(Path rootDir) throws IOException {
    if (rootDir == null || !Files.exists(rootDir)) {
      return;
    }
    Files.walkFileTree(rootDir, new SimpleFileVisitor<Path>() {
      @Override
      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
      }
      @Override
      public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
      }
    });
  }

  /**
   * @param dir directory to list
   * @param glob glob pattern for files that should be returned, which can span subdirectories
   *  as in patterns like {@code * /*}
   * @return {@link Path}s, including both files and directories, matching the glob pattern.
   *  No path containing an element whose name starts with "." is returned.
   *  Returned paths are also ordered lexicographically.
   * @throws IOException if an error occurs while accessing the file system
   */
  public static List<Path> listFiles(Path dir, String glob) throws IOException {
    Preconditions.checkArgument(Files.isDirectory(dir), "%s is not a directory", dir);

    List<String> globLevels;
    if (glob == null || glob.isEmpty()) {
      globLevels = Collections.singletonList("*");
    } else {
      globLevels = Arrays.asList(glob.split("/"));
    }
    Preconditions.checkState(!globLevels.isEmpty());

    List<Path> paths = new ArrayList<>();
    paths.add(dir);

    for (String globLevel : globLevels) {
      List<Path> newPaths = new ArrayList<>();
      for (Path existingPath : paths) {
        if (Files.isDirectory(existingPath)) {
          try (DirectoryStream<Path> stream = Files.newDirectoryStream(existingPath, globLevel)) {
            for (Path path : stream) {
              if (!path.getFileName().toString().startsWith(".")) {
                newPaths.add(path);
              }
            }
          }
        }
      }
      paths = newPaths;
    }
    Collections.sort(paths);
    return paths;
  }

  /**
   * Closes a {@link Closeable} and logs the exception, if any. This is only suitable for closing
   * objects where a failure to close does not impact correctness -- for example, an input stream
   * that is already fully read, but not an output stream which may fail when flushing final
   * output.
   *
   * @param closeable thing to close
   */
  public static void closeQuietly(Closeable closeable) {
    if (closeable != null) {
      try {
        closeable.close();
      } catch (IOException e) {
        log.warn("Unable to close", e);
      }
    }
  }

  /**
   * Binds to a free ephemeral port, and then releases it. The returned port is quite likely
   * to be free for use after this, but is not entirely guaranteed to be.
   *
   * @return a (probably) free ephemeral port
   * @throws IOException if an error occurs while binding to a port
   */
  public static int chooseFreePort() throws IOException {
    try (ServerSocket socket = new ServerSocket(0, 0)) {
      return socket.getLocalPort();
    }
  }

}


/*
 * Copyright (c) 2014, Cloudera, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.common.lang;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * {@link Class}-related utility methods.
 */
public final class ClassUtils {

  private static final Class<?>[] NO_TYPES = new Class<?>[0];
  private static final Object[] NO_ARGS = new Object[0];

  private ClassUtils() {}

  /**
   * @param className fully-qualified class name, which should be known to be available
   * @param <T> expected type of returned {@link Class}
   * @return {@link Class} for that named class
   */
  public static <T> Class<T> loadClass(String className) {
    try {
      @SuppressWarnings("unchecked")
      Class<T> theClass = (Class<T>) forName(className);
      return theClass;
    } catch (ClassNotFoundException cnfe) {
      throw new IllegalStateException("No valid " + className + " exists", cnfe);
    }
  }

  /**
   * @param className fully-qualified class name, which should be known to be available
   * @param superClass reference type of returned {@link Class}
   * @param <T> type of returned {@link Class}
   * @return {@link Class} for that named class
   */
  public static <T> Class<? extends T> loadClass(String className, Class<T> superClass) {
    try {
      return forName(className).asSubclass(superClass);
    } catch (ClassNotFoundException cnfe) {
      throw new IllegalStateException("No valid " + superClass + " binding exists", cnfe);
    }
  }

  /**
   * @param implClassName class name to text
   * @return {@code true} if the class exists in the JVM and can be loaded
   */
  public static boolean classExists(String implClassName) {
    try {
      forName(implClassName);
      return true;
    } catch (ClassNotFoundException ignored) {
      return false;
    }
  }

  private static Class<?> forName(String implClassName) throws ClassNotFoundException {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    if (cl == null) {
      cl = ClassUtils.class.getClassLoader();
    }
    return Class.forName(implClassName, true, cl);
  }

  /**
   * Like {@link #loadInstanceOf(String, Class)} where the reference returned is of the same type as
   * the class being loaded -- not any supertype.
   *
   * @param clazz {@link Class} to make an instance of
   * @param <T> type of returned instance
   * @return instance of {@code clazz}
   */
  public static <T> T loadInstanceOf(Class<T> clazz) {
    return loadInstanceOf(clazz.getName(), clazz);
  }

  /**
   * Like {@link #loadInstanceOf(String, Class, Class[], Object[])} for no-arg constructors.
   *
   * @param implClassName implementation class name
   * @param superClass superclass or interface that the implementation extends
   * @param <T> type of returned instance
   * @return instance of {@code implClassName}
   */
  public static <T> T loadInstanceOf(String implClassName, Class<T> superClass) {
    return loadInstanceOf(implClassName, superClass, NO_TYPES, NO_ARGS);
  }

  /**
   * Loads and instantiates a named implementation class, a subclass of a given supertype,
   * whose constructor takes the given arguments.
   *
   * @param implClassName implementation class name
   * @param superClass superclass or interface that the implementation extends
   * @param constructorTypes argument types of constructor to use
   * @param constructorArgs actual constructor arguments
   * @param <T> type of returned instance
   * @return instance of {@code implClassName}
   */
  public static <T> T loadInstanceOf(String implClassName,
                                     Class<T> superClass,
                                     Class<?>[] constructorTypes,
                                     Object[] constructorArgs) {
    try {
      Class<? extends T> configClass = loadClass(implClassName, superClass);
      Constructor<? extends T> constructor = configClass.getConstructor(constructorTypes);
      return constructor.newInstance(constructorArgs);
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) {
      throw new IllegalArgumentException("No valid " + superClass + " binding exists", e);
    } catch (InvocationTargetException ite) {
      throw new IllegalStateException("Could not instantiate " + superClass + " due to exception",
                                      ite.getCause());
    }
  }

}


/*
 * Copyright (c) 2014, Cloudera and Intel, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.app.serving.als;

import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;

import com.cloudera.oryx.api.serving.OryxServingException;
import com.cloudera.oryx.app.als.ALSUtils;
import com.cloudera.oryx.app.serving.als.model.ALSServingModel;
import com.cloudera.oryx.common.collection.Pair;
import com.cloudera.oryx.common.math.Solver;
import com.cloudera.oryx.common.math.VectorMath;

/**
 *
Responds to a GET request to
 * {@code /estimateForAnonymous/[toItemID]/[itemID1(=value1)](/[itemID2(=value2)]/...)}.
 * That is, 1 or more item IDs are supplied, which may each optionally correspond to
 * a value or else default to 1.

 *
 *
Unknown item IDs are ignored.

 *
 *
Outputs the result of the method call as a value on one line.

 */
@Singleton
@Path("/estimateForAnonymous")
public final class EstimateForAnonymous extends AbstractALSResource {

  @GET
  @Path("{toItemID}/{itemID : .+}")
  @Produces({MediaType.TEXT_PLAIN, "text/csv", MediaType.APPLICATION_JSON})
  public Double get(
      @PathParam("toItemID") String toItemID,
      @PathParam("itemID") List<PathSegment> pathSegments) throws OryxServingException {

    ALSServingModel model = getALSServingModel();
    float[] toItemVector = model.getItemVector(toItemID);
    checkExists(toItemVector != null, toItemID);

    float[] anonymousUserFeatures = buildTemporaryUserVector(model, parsePathSegments(pathSegments), null);
    return anonymousUserFeatures == null ? 0.0 : VectorMath.dot(anonymousUserFeatures, toItemVector);
  }

  /**
   * Builds a temporary user feature vector given an (optional) existing user vector and a context
   * of items.
   *
   * @param model current ALS model
   * @param parsedPathSegments request path segments containing item-value pairs
   * @param Xu existing feature vector for the user (may be {@code null})
   * @return user feature vector
   */
  static float[] buildTemporaryUserVector(ALSServingModel model,
                                          List<Pair<String,Double>> parsedPathSegments,
                                          float[] Xu) throws OryxServingException {
    boolean implicit = model.isImplicit();
    Solver solver = model.getYTYSolver();
    check(solver != null, Response.Status.SERVICE_UNAVAILABLE, "No solver available for model yet");
    for (Pair<String,Double> itemValue : parsedPathSegments) {
      float[] Yi = model.getItemVector(itemValue.getFirst());
      // Given value is taken to be the fictitious current value of Qui = Xu * Yi^T
      float[] newXu = ALSUtils.computeUpdatedXu(solver, itemValue.getSecond(), Xu, Yi, implicit);
      if (newXu != null) {
        Xu = newXu;
      }
    }
    return Xu;
  }

  static List<Pair<String, Double>> parsePathSegments(List<PathSegment> pathSegments) {
    return pathSegments.stream().map(segment -> {
      String s = segment.getPath();
      int offset = s.indexOf('=');
      return offset < 0 ?
          new Pair<>(s, 1.0) :
          new Pair<>(s.substring(0, offset), Double.parseDouble(s.substring(offset + 1)));
    }).collect(Collectors.toList());
  }

}


/*
 * Copyright (c) 2014, Cloudera, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.common.lang;

import java.io.Closeable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * JVM-related utility methods.
 */
public final class JVMUtils {

  private static final Logger log = LoggerFactory.getLogger(JVMUtils.class);

  private static final OryxShutdownHook SHUTDOWN_HOOK = new OryxShutdownHook();

  private JVMUtils() {}

  /**
   * Adds a shutdown hook that tries to call {@link Closeable#close()} on the given argument
   * at JVM shutdown.
   *
   * @param closeable thing to close
   */
  public static void closeAtShutdown(Closeable closeable) {
    if (SHUTDOWN_HOOK.addCloseable(closeable)) {
      try {
        Runtime.getRuntime().addShutdownHook(new Thread(SHUTDOWN_HOOK, "OryxShutdownHookThread"));
      } catch (IllegalStateException ise) {
        log.warn("Can't close {} at shutdown since shutdown is in progress", closeable);
      }
    }
  }

  /**
   * @return approximate heap used, in bytes
   */
  public static long getUsedMemory() {
    Runtime runtime = Runtime.getRuntime();
    return runtime.totalMemory() - runtime.freeMemory();
  }

}


/*
 * Copyright (c) 2014, Cloudera and Intel, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.ml;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import com.google.common.base.Preconditions;
import com.typesafe.config.Config;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.rdd.RDD;
import org.dmg.pmml.PMML;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cloudera.oryx.api.TopicProducer;
import com.cloudera.oryx.api.batch.BatchLayerUpdate;
import com.cloudera.oryx.common.collection.Pair;
import com.cloudera.oryx.common.lang.ExecUtils;
import com.cloudera.oryx.common.pmml.PMMLUtils;
import com.cloudera.oryx.common.random.RandomManager;
import com.cloudera.oryx.common.settings.ConfigUtils;
import com.cloudera.oryx.ml.param.HyperParamValues;
import com.cloudera.oryx.ml.param.HyperParams;

/**
 * A specialization of {@link BatchLayerUpdate} for machine learning-oriented
 * update processes. This implementation contains the framework for test/train split
 * for example, parameter optimization, and so on. Subclasses instead implement
 * methods like {@link #buildModel(JavaSparkContext,JavaRDD,List,Path)} to create a PMML model and
 * {@link #evaluate(JavaSparkContext,PMML,Path,JavaRDD,JavaRDD)} to evaluate a model from
 * held-out test data.
 *
 * @param <M> type of message to read from the input topic
 */
public abstract class MLUpdate<M> implements BatchLayerUpdate<Object,M,String> {

  private static final Logger log = LoggerFactory.getLogger(MLUpdate.class);

  public static final String MODEL_FILE_NAME = "model.pmml";

  private final double testFraction;
  private final int candidates;
  private final String hyperParamSearch;
  private final int evalParallelism;
  private final Double threshold;
  private final int maxMessageSize;

  protected MLUpdate(Config config) {
    this.testFraction = config.getDouble("oryx.ml.eval.test-fraction");
    int candidates = config.getInt("oryx.ml.eval.candidates");
    this.evalParallelism = config.getInt("oryx.ml.eval.parallelism");
    this.threshold = ConfigUtils.getOptionalDouble(config, "oryx.ml.eval.threshold");
    this.maxMessageSize = config.getInt("oryx.update-topic.message.max-size");
    Preconditions.checkArgument(testFraction >= 0.0 && testFraction <= 1.0);
    Preconditions.checkArgument(candidates > 0);
    Preconditions.checkArgument(evalParallelism > 0);
    Preconditions.checkArgument(maxMessageSize > 0);
    if (testFraction == 0.0 && candidates > 1) {
      log.info("Eval is disabled (test fraction = 0) so candidates is overridden to 1");
      candidates = 1;
    }
    this.candidates = candidates;
    this.hyperParamSearch = config.getString("oryx.ml.eval.hyperparam-search");
  }

  protected final double getTestFraction() {
    return testFraction;
  }

  /**
   * @return a list of hyperparameter value ranges to try, one {@link HyperParamValues} per
   *  hyperparameter. Different combinations of the values derived from the list will be
   *  passed back into {@link #buildModel(JavaSparkContext,JavaRDD,List,Path)}
   */
  public List<HyperParamValues<?>> getHyperParameterValues() {
    return Collections.emptyList();
  }

  /**
   * @param sparkContext active Spark Context
   * @param trainData training data on which to build a model
   * @param hyperParameters ordered list of hyper parameter values to use in building model
   * @param candidatePath directory where additional model files can be written
   * @return a {@link PMML} representation of a model trained on the given data
   */
  public abstract PMML buildModel(JavaSparkContext sparkContext,
                                  JavaRDD<M> trainData,
                                  List<?> hyperParameters,
                                  Path candidatePath);

  /**
   * @return {@code true} iff additional updates must be published along with the model; if
   *  {@link #publishAdditionalModelData(JavaSparkContext, PMML, JavaRDD, JavaRDD, Path, TopicProducer)} must
   *  be called. This is only applicable for special model types.
   */
  public boolean canPublishAdditionalModelData() {
    return false;
  }

  /**
   * Optionally, publish additional model-related information to the update topic,
   * after the model has been written. This is needed only in specific cases, like the
   * ALS algorithm, where the model serialization in PMML can't contain all of the info.
   *
   * @param sparkContext active Spark Context
   * @param pmml model for which extra data should be written
   * @param newData data that has arrived in current interval
   * @param pastData all previously-known data (may be {@code null})
   * @param modelParentPath directory containing model files, if applicable
   * @param modelUpdateTopic message topic to write to
   */
  public void publishAdditionalModelData(JavaSparkContext sparkContext,
                                         PMML pmml,
                                         JavaRDD<M> newData,
                                         JavaRDD<M> pastData,
                                         Path modelParentPath,
                                         TopicProducer<String, String> modelUpdateTopic) {
    // Do nothing by default
  }

  /**
   * @param sparkContext active Spark Context
   * @param model model to evaluate
   * @param modelParentPath directory containing model files, if applicable
   * @param testData data on which to test the model performance
   * @param trainData data on which model was trained, which can also be useful in evaluating
   *  unsupervised learning problems
   * @return an evaluation of the model on the test data. Higher should mean "better"
   */
  public abstract double evaluate(JavaSparkContext sparkContext,
                                  PMML model,
                                  Path modelParentPath,
                                  JavaRDD<M> testData,
                                  JavaRDD<M> trainData);

  @Override
  public void runUpdate(JavaSparkContext sparkContext,
                        long timestamp,
                        JavaPairRDD<Object,M> newKeyMessageData,
                        JavaPairRDD<Object,M> pastKeyMessageData,
                        String modelDirString,
                        TopicProducer<String,String> modelUpdateTopic)
      throws IOException, InterruptedException {

    Objects.requireNonNull(newKeyMessageData);

    JavaRDD<M> newData = newKeyMessageData.values();
    JavaRDD<M> pastData = pastKeyMessageData == null ? null : pastKeyMessageData.values();

    if (newData != null) {
      newData.cache();
      // This forces caching of the RDD. This shouldn't be necessary but we see some freezes
      // when many workers try to materialize the RDDs at once. Hence the workaround.
      newData.foreachPartition(p -> {});
    }
    if (pastData != null) {
      pastData.cache();
      pastData.foreachPartition(p -> {});
    }

    List<List<?>> hyperParameterCombos = HyperParams.chooseHyperParameterCombos(
        getHyperParameterValues(), hyperParamSearch, candidates);

    Path modelDir = new Path(modelDirString);
    Path tempModelPath = new Path(modelDir, ".temporary");
    Path candidatesPath = new Path(tempModelPath, Long.toString(System.currentTimeMillis()));

    FileSystem fs = FileSystem.get(modelDir.toUri(), sparkContext.hadoopConfiguration());
    fs.mkdirs(candidatesPath);

    Path bestCandidatePath = findBestCandidatePath(
        sparkContext, newData, pastData, hyperParameterCombos, candidatesPath);

    Path finalPath = new Path(modelDir, Long.toString(System.currentTimeMillis()));
    if (bestCandidatePath == null) {
      log.info("Unable to build any model");
    } else {
      // Move best model into place
      fs.rename(bestCandidatePath, finalPath);
    }
    // Then delete everything else
    fs.delete(candidatesPath, true);

    if (modelUpdateTopic == null) {
      log.info("No update topic configured, not publishing models to a topic");
    } else {
      // Push PMML model onto update topic, if it exists
      Path bestModelPath = new Path(finalPath, MODEL_FILE_NAME);
      if (fs.exists(bestModelPath)) {
        FileStatus bestModelPathFS = fs.getFileStatus(bestModelPath);
        PMML bestModel = null;
        boolean modelNeededForUpdates = canPublishAdditionalModelData();
        boolean modelNotTooLarge = bestModelPathFS.getLen() <= maxMessageSize;
        if (modelNeededForUpdates || modelNotTooLarge) {
          // Either the model is required for publishAdditionalModelData, or required because it's going to
          // be serialized to Kafka
          try (InputStream in = fs.open(bestModelPath)) {
            bestModel = PMMLUtils.read(in);
          }
        }

        if (modelNotTooLarge) {
          modelUpdateTopic.send("MODEL", PMMLUtils.toString(bestModel));
        } else {
          modelUpdateTopic.send("MODEL-REF", fs.makeQualified(bestModelPath).toString());
        }

        if (modelNeededForUpdates) {
          publishAdditionalModelData(
              sparkContext, bestModel, newData, pastData, finalPath, modelUpdateTopic);
        }
      }
    }

    if (newData != null) {
      newData.unpersist();
    }
    if (pastData != null) {
      pastData.unpersist();
    }
  }

  private Path findBestCandidatePath(JavaSparkContext sparkContext,
                                     JavaRDD<M> newData,
                                     JavaRDD<M> pastData,
                                     List<List<?>> hyperParameterCombos,
                                     Path candidatesPath) throws IOException {
    Map<Path,Double> pathToEval = ExecUtils.collectInParallel(
        candidates,
        Math.min(evalParallelism, candidates),
        true,
        i -> buildAndEval(i, hyperParameterCombos, sparkContext, newData, pastData, candidatesPath),
        Collectors.toMap(Pair::getFirst, Pair::getSecond));

    FileSystem fs = null;
    Path bestCandidatePath = null;
    double bestEval = Double.NEGATIVE_INFINITY;
    for (Map.Entry<Path,Double> pathEval : pathToEval.entrySet()) {
      Path path = pathEval.getKey();
      if (path != null) {
        if (fs == null) {
          fs = FileSystem.get(path.toUri(), sparkContext.hadoopConfiguration());
        }
        if (fs.exists(path)) {
          Double eval = pathEval.getValue();
          if (!Double.isNaN(eval)) {
            // Valid evaluation; if it's the best so far, keep it
            if (eval > bestEval) {
              log.info("Best eval / model path is now {} / {}", eval, path);
              bestEval = eval;
              bestCandidatePath = path;
            }
          } else if (bestCandidatePath == null && testFraction == 0.0) {
            // Normal case when eval is disabled; no eval is possible, but keep the one model
            // that was built
            bestCandidatePath = path;
          }
        } // else can't do anything; no model at all
      }
    }
    if (threshold != null && bestEval < threshold) {
      log.info("Best model at {} had eval {}, but did not exceed threshold {}; discarding model",
               bestCandidatePath, bestEval, threshold);
      bestCandidatePath = null;
    }
    return bestCandidatePath;
  }

  private Pair<Path,Double> buildAndEval(int i,
                                         List<? extends List<?>> hyperParameterCombos,
                                         JavaSparkContext sparkContext,
                                         JavaRDD<M> newData,
                                         JavaRDD<M> pastData,
                                         Path candidatesPath) {
    // % = cycle through combinations if needed
    List<?> hyperParameters = hyperParameterCombos.get(i % hyperParameterCombos.size());
    Path candidatePath = new Path(candidatesPath, Integer.toString(i));
    log.info("Building candidate {} with params {}", i, hyperParameters);

    Pair<JavaRDD<M>,JavaRDD<M>> trainTestData = splitTrainTest(newData, pastData);
    JavaRDD<M> allTrainData = trainTestData.getFirst();
    JavaRDD<M> testData = trainTestData.getSecond();

    double eval = Double.NaN;
    if (empty(allTrainData)) {
      log.info("No train data to build a model");
    } else {
      PMML model = buildModel(sparkContext, allTrainData, hyperParameters, candidatePath);
      if (model == null) {
        log.info("Unable to build a model");
      } else {
        Path modelPath = new Path(candidatePath, MODEL_FILE_NAME);
        log.info("Writing model to {}", modelPath);
        try {
          FileSystem fs = FileSystem.get(candidatePath.toUri(), sparkContext.hadoopConfiguration());
          fs.mkdirs(candidatePath);
          try (OutputStream out = fs.create(modelPath)) {
            PMMLUtils.write(model, out);
          }
        } catch (IOException ioe) {
          throw new IllegalStateException(ioe);
        }
        if (empty(testData)) {
          log.info("No test data available to evaluate model");
        } else {
          log.info("Evaluating model");
          eval = evaluate(sparkContext, model, candidatePath, testData, allTrainData);
        }
      }
    }

    log.info("Model eval for params {}: {} ({})", hyperParameters, eval, candidatePath);
    return new Pair<>(candidatePath, eval);
  }

  private Pair<JavaRDD<M>,JavaRDD<M>> splitTrainTest(JavaRDD<M> newData, JavaRDD<M> pastData) {
    Objects.requireNonNull(newData);
    if (testFraction <= 0.0) {
      return new Pair<>(pastData == null ? newData : newData.union(pastData), null);
    }
    if (testFraction >= 1.0) {
      return new Pair<>(pastData, newData);
    }
    if (empty(newData)) {
      return new Pair<>(pastData, null);
    }
    Pair<JavaRDD<M>,JavaRDD<M>> newTrainTest = splitNewDataToTrainTest(newData);
    JavaRDD<M> newTrainData = newTrainTest.getFirst();
    return new Pair<>(pastData == null ? newTrainData : newTrainData.union(pastData),
                      newTrainTest.getSecond());
  }

  private static boolean empty(JavaRDD<?> rdd) {
    return rdd == null || rdd.isEmpty();
  }

  /**
   * Default implementation which randomly splits new data into train/test sets.
   * This handles the case where {@link #getTestFraction()} is not 0 or 1.
   *
   * @param newData data that has arrived in the current input batch
   * @return a {@link Pair} of train, test {@link RDD}s.
   */
  protected Pair<JavaRDD<M>,JavaRDD<M>> splitNewDataToTrainTest(JavaRDD<M> newData) {
    RDD<M>[] testTrainRDDs = newData.rdd().randomSplit(
        new double[]{1.0 - testFraction, testFraction},
        RandomManager.getRandom().nextLong());
    return new Pair<>(newData.wrapRDD(testTrainRDDs[0]),
                      newData.wrapRDD(testTrainRDDs[1]));
  }

}


/*
 * Copyright (c) 2014, Cloudera, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.common.settings;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;

import com.google.common.base.Preconditions;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigRenderOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utilities for retrieving {@code Config} instances.
 */
public final class ConfigUtils {

  private static final Logger log = LoggerFactory.getLogger(ConfigUtils.class);

  private static final Pattern REDACT_PATTERN =
      Pattern.compile("(\\w*password\\w*\\s*=\\s*).+", Pattern.CASE_INSENSITIVE);
  private static final Config DEFAULT_CONFIG = ConfigFactory.load();
  private static final ConfigRenderOptions RENDER_OPTS =
      ConfigRenderOptions.defaults()
        .setComments(false)
        .setOriginComments(false)
        .setFormatted(true)
        .setJson(false);

  private ConfigUtils() {}

  /**
   * Returns the default {@code Config} object for this app, based on config in the JAR file
   * or otherwise specified to the library.
   *
   * @return default configuration
   */
  public static Config getDefault() {
    return DEFAULT_CONFIG;
  }

  /**
   * @param overlay map of key-value pairs to add to default config. The map is converted
   *  to a string representation, as it were from a config file, and parsed accordingly.
   * @param underlying underlying config to overlay new settings on top of
   * @return default config but with key-value pairs added
   */
  public static Config overlayOn(Map<String,?> overlay, Config underlying) {
    StringBuilder configFileString = new StringBuilder();
    overlay.forEach((k, v) -> configFileString.append(k).append('=').append(v).append('\n'));
    String configFile = configFileString.toString();
    log.debug("Overlaid config: \n{}", configFile);
    return ConfigFactory.parseString(configFile).resolve().withFallback(underlying);
  }

  /**
   * @param config configuration to query for value
   * @param key configuration path key
   * @return value for given key, or {@code null} if none exists
   */
  public static String getOptionalString(Config config, String key) {
    return config.hasPath(key) ? config.getString(key) : null;
  }

  /**
   * @param config configuration to query for value
   * @param key configuration path key
   * @return value for given key, or {@code null} if none exists
   */
  public static List<String> getOptionalStringList(Config config, String key) {
    return config.hasPath(key) ? config.getStringList(key) : null;
  }

  /**
   * @param config configuration to query for value
   * @param key configuration path key
   * @return value for given key, or {@code null} if none exists
   */
  public static Double getOptionalDouble(Config config, String key) {
    return config.hasPath(key) ? config.getDouble(key) : null;
  }

  /**
   * Helper to set a {@link Path} value correctly for use with {@link #overlayOn(Map,Config)}.
   *
   * @param overlay key-value pairs to overlay on a {@link Config}
   * @param key key to set
   * @param path {@link Path} value
   * @throws IOException if {@link Path} can't be made canonical
   */
  public static void set(Map<String,Object> overlay, String key, Path path) throws IOException {
    Path finalPath = Files.exists(path, LinkOption.NOFOLLOW_LINKS) ?
        path.toRealPath(LinkOption.NOFOLLOW_LINKS) :
        path;
    overlay.put(key, "\"" + finalPath.toUri() + "\"");
  }

  /**
   * @param config {@link Config} to serialize to a String
   * @return JSON-like representation of properties in the configuration, excluding those
   *  inherited from the local JVM environment
   */
  public static String serialize(Config config) {
    return config.root().withOnlyKey("oryx").render(ConfigRenderOptions.concise());
  }

  /**
   * @param serialized serialized form of configuration as JSON-like data
   * @return {@link Config} from the serialized config
   */
  public static Config deserialize(String serialized) {
    return ConfigFactory.parseString(serialized).resolve().withFallback(DEFAULT_CONFIG);
  }

  /**
   * @param config {@link Config} to print
   * @return pretty-printed version of config values, excluding those
   *  inherited from the local JVM environment
   */
  public static String prettyPrint(Config config) {
    return redact(config.root().withOnlyKey("oryx").render(RENDER_OPTS));
  }

  static String redact(String s) {
    return REDACT_PATTERN.matcher(s).replaceAll("$1*****");
  }

  /**
   * @param keyValues a sequence key1, value1, key2, value2, ... where the {@link #toString()}
   *   of successive pairs are interpreted as key-value pairs
   * @return a {@link Properties} containing these key-value pairs
   */
  public static Properties keyValueToProperties(Object... keyValues) {
    Preconditions.checkArgument(keyValues.length % 2 == 0);
    Properties properties = new Properties();
    for (int i = 0; i < keyValues.length - 1; i += 2) { // length must be even
      properties.setProperty(keyValues[i].toString(), keyValues[i+1].toString());
    }
    return properties;
  }

}


/*
 * Copyright (c) 2015, Cloudera and Intel, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.app.batch.mllib.kmeans;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.mllib.linalg.Vector;
import scala.Tuple2;

import com.cloudera.oryx.app.kmeans.ClusterInfo;
import com.cloudera.oryx.app.kmeans.DistanceFn;

final class SilhouetteCoefficient extends AbstractKMeansEvaluation {

  private static final long MAX_SAMPLE_SIZE = 100000;

  SilhouetteCoefficient(Collection<ClusterInfo> clusters) {
    super(clusters);
  }

  @Override
  double evaluate(JavaRDD<Vector> evalData) {
    return silhouetteCoefficient(fetchClusteredPoints(fetchSampleData(evalData)).collectAsMap());
  }

  /**
   * Computes the Silhouette Coefficient for this clustering.
   * See https://en.wikipedia.org/wiki/Silhouette_(clustering)
   *
   * @param clusterPointsByID map of cluster ID to points that map to that cluster,
   *  for whom Silhouette coefficient needs to be computed
   * @return Silhouette Coefficient in range [-1,1]; higher is better
   */
  double silhouetteCoefficient(Map<Integer,Iterable<double[]>> clusterPointsByID) {
    double totalSilhouetteCoefficient = 0.0;
    long sampleCount = 0L;

    for (Map.Entry<Integer, Iterable<double[]>> entry : clusterPointsByID.entrySet()) {
      List<double[]> clusteredPoints = iterableToList(entry.getValue());
      long clusterSize = clusteredPoints.size();
      // Increment the total sample count for computing silhouette coefficient
      sampleCount += clusterSize;
      // if there's only one element in a cluster, then assume the silhouetteCoefficient for
      // the cluster = 0, this is an arbitrary choice per Section 2: Construction of Silhouettes
      // in the referenced paper
      if (clusterSize > 1) {
        for (double[] point : clusteredPoints) {
          double pointIntraClusterDissimilarity = clusterDissimilarityForPoint(point, clusteredPoints, true);
          double pointInterClusterDissimilarity =
              minInterClusterDissimilarityForPoint(entry.getKey(), point, clusterPointsByID);
          totalSilhouetteCoefficient += silhouetteCoefficient(
              pointIntraClusterDissimilarity, pointInterClusterDissimilarity);
        }
      }

    }

    return sampleCount == 0 ? 0.0 : totalSilhouetteCoefficient / sampleCount;
  }

  static JavaRDD<Vector> fetchSampleData(JavaRDD<Vector> evalData) {
    long count = evalData.count();
    if (count > MAX_SAMPLE_SIZE) {
      return evalData.sample(false, (double) MAX_SAMPLE_SIZE / count);
    }
    return evalData;
  }

  private JavaPairRDD<Integer, Iterable<double[]>> fetchClusteredPoints(JavaRDD<? extends Vector> evalData) {
    return evalData.mapToPair(vector -> {
      double closestDist = Double.POSITIVE_INFINITY;
      int minClusterID = Integer.MIN_VALUE;
      double[] vec = vector.toArray();
      DistanceFn<double[]> distanceFn = getDistanceFn();
      Map<Integer,ClusterInfo> clusters = getClustersByID();
      for (ClusterInfo cluster : clusters.values()) {
        double distance = distanceFn.applyAsDouble(cluster.getCenter(), vec);
        if (distance < closestDist) {
          closestDist = distance;
          minClusterID = cluster.getID();
        }
      }
      Preconditions.checkState(!Double.isInfinite(closestDist) && !Double.isNaN(closestDist));
      return new Tuple2<>(minClusterID, vec);
    }).groupByKey();
  }

  private double clusterDissimilarityForPoint(double[] point,
                                              Collection<double[]> clusterPoints,
                                              boolean ownCluster) {
    DistanceFn<double[]> distanceFn = getDistanceFn();
    double totalDissimilarity = 0.0;
    for (double[] clusterPoint : clusterPoints) {
      totalDissimilarity += distanceFn.applyAsDouble(point, clusterPoint);
    }

    if (ownCluster) {
      // (points.size -1) because a point's dissimilarity is being measured with other
      // points in its own cluster, hence there would be (n - 1) dissimilarities computed
      return totalDissimilarity / (clusterPoints.size() - 1);
    } else {
      // point dissimilarity is being measured with all points of one of other clusters
      return totalDissimilarity / clusterPoints.size();
    }
  }

  private double minInterClusterDissimilarityForPoint(
      int otherClusterID,
      double[] point,
      Map<Integer, Iterable<double[]>> clusteredPointsMap) {
    return clusteredPointsMap.entrySet().stream().mapToDouble(entry -> {
      // only compute dissimilarities with other clusters
      if (entry.getKey().equals(otherClusterID)) {
        return Double.POSITIVE_INFINITY;
      }
      return clusterDissimilarityForPoint(point, iterableToList(entry.getValue()), false);
    }).min().orElse(Double.POSITIVE_INFINITY);
  }

  static double silhouetteCoefficient(double ai, double bi) {
    if (ai < bi) {
      return 1.0 - (ai / bi);
    }
    if (ai > bi) {
      return (bi / ai) - 1.0;
    }
    return 0.0;
  }

  private static <T> List<T> iterableToList(Iterable<T> it) {
    return it instanceof List ? (List<T>) it : Lists.newArrayList(it);
  }

}


/*
 * Copyright (c) 2014, Cloudera, Inc. All Rights Reserved.
 *
 * Cloudera, Inc. licenses this file to you 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
 *
 * This software 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 com.cloudera.oryx.app.batch.mllib.als;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import com.google.common.collect.Sets;
import org.apache.commons.math3.random.RandomGenerator;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.PairFlatMapFunction;
import org.apache.spark.broadcast.Broadcast;
import org.apache.spark.mllib.recommendation.MatrixFactorizationModel;
import org.apache.spark.mllib.recommendation.Rating;
import org.apache.spark.rdd.RDD;
import scala.Tuple2;

import com.cloudera.oryx.common.random.RandomManager;

/**
 * Computes AUC (area under the ROC curve) as a recommender evaluation metric.
 * Really, it computes what might be described as "Mean AUC", as it computes AUC per
 * user and averages them.
 */
final class Evaluation {

  private Evaluation() {}

  /**
   * Computes root mean squared error of {@link Rating#rating()} versus predicted value.
   */
  static double rmse(MatrixFactorizationModel mfModel, JavaRDD<Rating> testData) {
    JavaPairRDD<Tuple2<Integer,Integer>,Double> testUserProductValues =
        testData.mapToPair(rating -> new Tuple2<>(new Tuple2<>(rating.user(), rating.product()), rating.rating()));
    @SuppressWarnings("unchecked")
    RDD<Tuple2<Object,Object>> testUserProducts =
        (RDD<Tuple2<Object,Object>>) (RDD<?>) testUserProductValues.keys().rdd();
    JavaRDD<Rating> predictions = testData.wrapRDD(mfModel.predict(testUserProducts));
    double mse = predictions.mapToPair(
        rating -> new Tuple2<>(new Tuple2<>(rating.user(), rating.product()), rating.rating())
    ).join(testUserProductValues).values().mapToDouble(valuePrediction -> {
      double diff = valuePrediction._1() - valuePrediction._2();
      return diff * diff;
    }).mean();
    return Math.sqrt(mse);
  }

  /**
   * Computes AUC (area under the ROC curve) as a recommender evaluation metric.
   * Really, it computes what might be described as "Mean AUC", as it computes AUC per
   * user and averages them.
   */
  static double areaUnderCurve(JavaSparkContext sparkContext,
                               MatrixFactorizationModel mfModel,
                               JavaRDD<Rating> positiveData) {

    // This does not use Spark's BinaryClassificationMetrics.areaUnderROC because it
    // is intended to operate on one large set of (score,label) pairs. The computation
    // here is really many small AUC problems, for which a much faster direct computation
    // is available.

    // Extract all positive (user,product) pairs
    JavaPairRDD<Integer,Integer> positiveUserProducts =
        positiveData.mapToPair(rating -> new Tuple2<>(rating.user(), rating.product()));

    JavaPairRDD<Integer,Iterable<Rating>> positivePredictions =
        predictAll(mfModel, positiveData, positiveUserProducts);

    // All distinct item IDs, to be broadcast
    Broadcast<List<Integer>> allItemIDsBC = sparkContext.broadcast(positiveUserProducts.values().distinct().collect());

    JavaPairRDD<Integer,Integer> negativeUserProducts =
      positiveUserProducts.groupByKey().flatMapToPair(
        new PairFlatMapFunction<Tuple2<Integer,Iterable<Integer>>,Integer,Integer>() {
          private final RandomGenerator random = RandomManager.getRandom();
          @Override
          public Iterator<Tuple2<Integer,Integer>> call(
              Tuple2<Integer,Iterable<Integer>> userIDsAndItemIDs) {
            Integer userID = userIDsAndItemIDs._1();
            Collection<Integer> positiveItemIDs = Sets.newHashSet(userIDsAndItemIDs._2());
            int numPositive = positiveItemIDs.size();
            Collection<Tuple2<Integer,Integer>> negative = new ArrayList<>(numPositive);
            List<Integer> allItemIDs = allItemIDsBC.value();
            int numItems = allItemIDs.size();
            // Sample about as many negative examples as positive
            for (int i = 0; i < numItems && negative.size() < numPositive; i++) {
              Integer itemID = allItemIDs.get(random.nextInt(numItems));
              if (!positiveItemIDs.contains(itemID)) {
                negative.add(new Tuple2<>(userID, itemID));
              }
            }
            return negative.iterator();
          }
        });

    JavaPairRDD<Integer,Iterable<Rating>> negativePredictions =
        predictAll(mfModel, positiveData, negativeUserProducts);

    return positivePredictions.join(negativePredictions).values().mapToDouble(t -> {
        // AUC is also the probability that random positive examples
        // rank higher than random examples at large. Here we compare all random negative
        // examples to all positive examples and report the totals as an alternative
        // computation for AUC
        long correct = 0;
        long total = 0;
        for (Rating positive : t._1()) {
          for (Rating negative : t._2()) {
            if (positive.rating() > negative.rating()) {
              correct++;
            }
            total++;
          }
        }
        if (total == 0) {
          return 0.0;
        }
        return (double) correct / total;
      }).mean();
  }

  private static JavaPairRDD<Integer,Iterable<Rating>> predictAll(
      MatrixFactorizationModel mfModel,
      JavaRDD<Rating> data,
      JavaPairRDD<Integer,Integer> userProducts) {
    @SuppressWarnings("unchecked")
    RDD<Tuple2<Object,Object>> userProductsRDD =
        (RDD<Tuple2<Object,Object>>) (RDD<?>) userProducts.rdd();
    return data.wrapRDD(mfModel.predict(userProductsRDD)).groupBy(Rating::user);
  }

}


/*
 * Copyright (c) 2016 Uber Technologies, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.uber.sdk.android.core.auth;

import android.app.Activity;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.uber.sdk.android.core.SupportedAppType;
import com.uber.sdk.android.core.UberSdk;
import com.uber.sdk.android.core.utils.AppProtocol;
import com.uber.sdk.core.auth.AccessToken;
import com.uber.sdk.core.auth.AccessTokenStorage;
import com.uber.sdk.core.auth.Scope;
import com.uber.sdk.core.client.AccessTokenSession;
import com.uber.sdk.core.client.ServerTokenSession;
import com.uber.sdk.core.client.Session;
import com.uber.sdk.core.client.SessionConfiguration;

import java.util.ArrayList;
import java.util.Collection;

import static com.uber.sdk.android.core.utils.Preconditions.checkState;
import static com.uber.sdk.core.client.utils.Preconditions.checkNotNull;

/**
 * Manages user login via OAuth 2.0 Implicit Grant.  Be sure to call
 * {@link LoginManager#onActivityResult(Activity, int, int, Intent)} in your
 * {@link Activity#onActivityResult(int, int, Intent)} to forward along LoginResults.
 */
public class LoginManager {

    /**
     * Used to retrieve the {@link AuthenticationError} from an {@link Intent}.
     */
    static final String EXTRA_ERROR = "ERROR";

    /**
     * Used to retrieve the Access Token from an {@link Intent}.
     */
    static final String EXTRA_ACCESS_TOKEN = "ACCESS_TOKEN";

    /**
     * Used to retrieve the Refresh Token from an {@link Intent}.
     */
    static final String EXTRA_REFRESH_TOKEN = "REFRESH_TOKEN";

    /**
     * Used retrieve the {@link Scope}s granted from from an {@link Intent}.
     */
    static final String EXTRA_SCOPE = "SCOPE";

    /**
     * Used to retrieve the token expiry from an {@link Intent}.
     */
    static final String EXTRA_EXPIRES_IN = "EXPIRES_IN";

    /**
     * Used to retrieve the token type from an {@link Intent}.
     */
    static final String EXTRA_TOKEN_TYPE = "TOKEN_TYPE";

    /**
     * Used to indicate that an authorization code has been received from an {@link Intent}.
     */
    static final String EXTRA_CODE_RECEIVED = "CODE_RECEIVED";

    static final int REQUEST_CODE_LOGIN_DEFAULT = 1001;

    private final AccessTokenStorage accessTokenStorage;
    private final LoginCallback callback;
    private final SessionConfiguration sessionConfiguration;
    private final int requestCode;
    private final LegacyUriRedirectHandler legacyUriRedirectHandler;

    private ArrayList<SupportedAppType> productFlowPriority;
    private boolean authCodeFlowEnabled = false;
    @Deprecated
    private boolean redirectForAuthorizationCode = false;

    /**
     * @param accessTokenStorage to store access token.
     * @param loginCallback      callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)}
     *                           is called.
     */
    public LoginManager(
            @NonNull AccessTokenStorage accessTokenStorage,
            @NonNull LoginCallback loginCallback) {
        this(accessTokenStorage, loginCallback, UberSdk.getDefaultSessionConfiguration());
    }

    /**
     * @param accessTokenStorage to store access token.
     * @param loginCallback      callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called.
     * @param configuration      to provide authentication information
     */
    public LoginManager(
            @NonNull AccessTokenStorage accessTokenStorage,
            @NonNull LoginCallback loginCallback,
            @NonNull SessionConfiguration configuration) {
        this(accessTokenStorage, loginCallback, configuration, REQUEST_CODE_LOGIN_DEFAULT);
    }

    /**
     * @param accessTokenStorage to store access token.
     * @param loginCallback      callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called.
     * @param configuration      to provide authentication information
     * @param requestCode        custom code to use for Activity communication
     */
    public LoginManager(
            @NonNull AccessTokenStorage accessTokenStorage,
            @NonNull LoginCallback loginCallback,
            @NonNull SessionConfiguration configuration,
            int requestCode) {
        this(accessTokenStorage, loginCallback, configuration, requestCode, new LegacyUriRedirectHandler());
    }

    /**
     * @param accessTokenStorage       to store access token.
     * @param loginCallback            callback to be called when {@link LoginManager#onActivityResult(Activity, int, int, Intent)} is called.
     * @param configuration            to provide authentication information
     * @param requestCode              custom code to use for Activity communication
     * @param legacyUriRedirectHandler Used to handle URI Redirect Migration
     */
    LoginManager(
            @NonNull AccessTokenStorage accessTokenStorage,
            @NonNull LoginCallback loginCallback,
            @NonNull SessionConfiguration configuration,
            int requestCode,
            @NonNull LegacyUriRedirectHandler legacyUriRedirectHandler) {
        this.accessTokenStorage = accessTokenStorage;
        this.callback = loginCallback;
        this.productFlowPriority = new ArrayList<>();
        this.sessionConfiguration = configuration;
        this.requestCode = requestCode;
        this.legacyUriRedirectHandler = legacyUriRedirectHandler;
    }

    /**
     * Logs a user in, requesting approval for specified {@link Scope}s.
     *
     * @param activity the activity used to start the {@link LoginActivity}.
     */
    public void login(final @NonNull Activity activity) {
        boolean hasScopes = (sessionConfiguration.getScopes() != null && !sessionConfiguration.getScopes().isEmpty())
                || (sessionConfiguration.getCustomScopes() != null && !sessionConfiguration.getCustomScopes().isEmpty());
        checkState(hasScopes, "Scopes must be set in the Session Configuration.");
        checkNotNull(sessionConfiguration.getRedirectUri(),
                "Redirect URI must be set in Session Configuration.");

        if (!legacyUriRedirectHandler.checkValidState(activity, this)) {
            return;
        }

        SsoDeeplink ssoDeeplink = getSsoDeeplink(activity);

        if (ssoDeeplink.isSupported(SsoDeeplink.FlowVersion.REDIRECT_TO_SDK)) {
            Intent intent = LoginActivity.newIntent(
                    activity,
                    productFlowPriority,
                    sessionConfiguration,
                    ResponseType.TOKEN,
                    false,
                    true,
                    true);
            activity.startActivityForResult(intent, requestCode);
        } else if (ssoDeeplink.isSupported(SsoDeeplink.FlowVersion.DEFAULT)) {
            ssoDeeplink.execute(SsoDeeplink.FlowVersion.DEFAULT);
        } else if (isAuthCodeFlowEnabled()) {
            loginForAuthorizationCode(activity);
        } else {
            loginForImplicitGrantWithFallback(activity);
        }
    }

    /**
     * Login using Implicit Grant (Webview)
     *
     * @param activity to start Activity on.
     */
    @Deprecated
    public void loginForImplicitGrant(@NonNull Activity activity) {

        if (!legacyUriRedirectHandler.checkValidState(activity, this)) {
            return;
        }

        Intent intent = LoginActivity.newIntent(activity, sessionConfiguration,
                ResponseType.TOKEN, legacyUriRedirectHandler.isLegacyMode());
        activity.startActivityForResult(intent, requestCode);
    }

    /**
     * login with Authorization Code mode.
     *
     * @param activity to start Activity on.
     */
    public void loginForAuthorizationCode(@NonNull Activity activity) {
        if (!legacyUriRedirectHandler.checkValidState(activity, this)) {
            return;
        }

        Intent intent = LoginActivity.newIntent(activity, sessionConfiguration,
                ResponseType.CODE, legacyUriRedirectHandler.isLegacyMode());
        activity.startActivityForResult(intent, requestCode);
    }

    /**
     * Login using Implicit Grant, but if requesting privileged scopes, fallback to redirecting the user to the play
     * store to install the app.
     *
     * @param activity to start Activity on.
     */
    private void loginForImplicitGrantWithFallback(@NonNull Activity activity) {
        if (!legacyUriRedirectHandler.checkValidState(activity, this)) {
            return;
        }

        Intent intent = LoginActivity.newIntent(
                activity,
                new ArrayList<SupportedAppType>(),
                sessionConfiguration,
                ResponseType.TOKEN,
                legacyUriRedirectHandler.isLegacyMode(),
                false,
                true);
        activity.startActivityForResult(intent, requestCode);
    }

    /**
     * @return {@link AccessTokenStorage} that is used.
     * @deprecated Use {@link LoginManager#getAccessTokenStorage()}
     */
    @Deprecated
    @NonNull
    @SuppressWarnings("unchecked")
    public <T extends AccessTokenStorage> T getAccessTokenManager() {
        return (T) accessTokenStorage;
    }

    /**
     * @return {@link AccessTokenStorage} that is used.
     */
    @NonNull
    public AccessTokenStorage getAccessTokenStorage() {
        return accessTokenStorage;
    }

    /**
     * @return {@link LoginCallback} that is used.
     */
    public LoginCallback getLoginCallback() {
        return callback;
    }

    @NonNull
    public SessionConfiguration getSessionConfiguration() {
        return sessionConfiguration;
    }

    /**
     * Gets session based on current {@link SessionConfiguration} and {@link AccessTokenStorage}.
     *
     * @return Session to use with API requests.
     * @throws IllegalStateException when not logged in
     */
    @NonNull
    public Session<?> getSession() {
        if (sessionConfiguration.getServerToken() != null) {
            return new ServerTokenSession(sessionConfiguration);
        } else if (accessTokenStorage.getAccessToken() != null) {
            return new AccessTokenSession(sessionConfiguration, accessTokenStorage);
        } else {
            throw new IllegalStateException("Tried to call getSession but not logged in or server token set.");
        }
    }

    /**
     * Determines if the Login Manager is authenticated based on set {@link SessionConfiguration}
     * and {@link AccessTokenStorage}
     *
     * @return true if authenticated, otherwise false;
     */
    public boolean isAuthenticated() {
        return (sessionConfiguration.getServerToken() != null || accessTokenStorage.getAccessToken() != null);
    }

    /**
     * By default, login will try to authenticate using Uber App, however if Uber app is unavailable and
     * {@link Scope}s requested are of type {@link Scope.ScopeType#PRIVILEGED}. User
     * will be redirected to the install or update the Uber app.
     *

     * Set to true to redirect to be redirected to login Authorization Code instead,
     * see https://developer.uber.com/docs/authentication#section-step-one-authorize.
     *

     *
     * @param redirectForAuthorizationCode true if should redirect, otherwise false
     * @return this instance of {@link LoginManager}
     * @deprecated, See Authorization Migration guide https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above
     */
    @Deprecated
    public LoginManager setRedirectForAuthorizationCode(boolean redirectForAuthorizationCode) {
        this.redirectForAuthorizationCode = redirectForAuthorizationCode;
        setAuthCodeFlowEnabled(true);
        return this;
    }

    /**
     * By default, login will try to authenticate using Uber App, however if Uber app is unavailable and
     * {@link Scope}s requested are of type {@link Scope.ScopeType#PRIVILEGED}. User
     * will be redirected to login Authorization Code,
     * see https://developer.uber.com/docs/authentication#section-step-one-authorize.
     *

     * If your redirect uri does not handle that flow, this flag indicates that the users will be asked to install or
     * update Uber app instead.
     *
     * @return true if redirect by default, otherwise false
     * @deprecated, See Authorization Migration guide https://github.com/uber/rides-android-sdk#authentication-migration-version-08-and-above
     */
    @Deprecated
    public boolean isRedirectForAuthorizationCode() {
        return redirectForAuthorizationCode;
    }

    /**
     * Enable the use of the Authorization Code Flow
     * (See
     * https://developer.uber.com/docs/authentication#section-step-one-authorize) instead of an
     * installation prompt for the Uber app  or Implicit Grant (WebView) as a login fallback mechanism.
     *

     * Requires that the app's backend system is configured to support this flow and the redirect
     * URI is pointed correctly.
     *
     * @param authCodeFlowEnabled true for use of auth code flow, false to fallback to Uber app
     *                            installation
     * @return this instance of {@link LoginManager}.
     */
    public LoginManager setAuthCodeFlowEnabled(boolean authCodeFlowEnabled) {
        this.authCodeFlowEnabled = authCodeFlowEnabled;
        return this;
    }

    /**
     * Dictates the order of which Uber applications should be used for SSO.
     * This can be used to order Eats then Rides or vice-versa.
     * Only specified applications will be used, so specifying only Rides or Eats will ignore other apps if installed.
     * The default behavior (for backward compatibility) is Rides only.
     *
     * @param productFlowPriority A Collection of SupportedAppType indicating priority of SSO applications.
     * @return this instance of {@link LoginManager}.
     */
    public LoginManager setProductFlowPriority(@NonNull Collection<SupportedAppType> productFlowPriority) {
        this.productFlowPriority = new ArrayList<>(productFlowPriority);
        return this;
    }

    /**
     * Indicates the use of the Authorization Code Flow
     * (See
     * https://developer.uber.com/docs/authentication#section-step-one-authorize) instead of an
     * installation prompt for the Uber app  or Implicit Grant (WebView) as a login fallback mechanism.
     *
     * @return true if Auth Code Flow is enabled, otherwise false
     */
    public boolean isAuthCodeFlowEnabled() {
        return authCodeFlowEnabled;
    }

    /**
     * {@link Activity} result handler to be called from starting {@link Activity}. Stores {@link AccessToken} and
     * notifies consumer callback of login result.
     *
     * @param activity    Activity used to start login if Uber app is unavailable.
     * @param requestCode request code originally supplied to {@link Activity#startActivityForResult(Intent, int)}.
     * @param resultCode  result code from returning {@link Activity}.
     * @param data        data from returning {@link Activity}.
     */
    public void onActivityResult(
            @NonNull Activity activity,
            int requestCode,
            int resultCode,
            @Nullable Intent data) {
        if (requestCode != this.requestCode) {
            return;
        }

        if (resultCode == Activity.RESULT_OK) {
            handleResultOk(data);
        } else if (resultCode == Activity.RESULT_CANCELED) {
            handleResultCancelled(activity, data);
        }
    }

    /**
     * Generates the deeplink required to execute the SSO Flow
     *
     * @param activity the activity to execute the deeplink intent
     * @return the object that executes the deeplink
     */
    @VisibleForTesting
    SsoDeeplink getSsoDeeplink(@NonNull Activity activity) {
        return new SsoDeeplink.Builder(activity).clientId(sessionConfiguration.getClientId())
                .scopes(sessionConfiguration.getScopes())
                .customScopes(sessionConfiguration.getCustomScopes())
                .activityRequestCode(requestCode)
                .redirectUri(sessionConfiguration.getRedirectUri())
                .productFlowPriority(productFlowPriority)
                .build();
    }

    private void handleResultCancelled(
            @NonNull Activity activity,
            @Nullable Intent data) {// An error occurred during login

        if (data == null) {
            // User canceled login
            callback.onLoginCancel();
            return;
        }

        final String error = data.getStringExtra(EXTRA_ERROR);
        final AuthenticationError authenticationError
                = (error != null) ? AuthenticationError.fromString(error) : AuthenticationError.UNKNOWN;

        if (authenticationError.equals(AuthenticationError.CANCELLED)) {
            // User canceled login
            callback.onLoginCancel();
            return;
        } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) && isAuthCodeFlowEnabled()) {
            loginForAuthorizationCode(activity);
            return;
        } else if (authenticationError.equals(AuthenticationError.UNAVAILABLE) &&
                !AuthUtils.isPrivilegeScopeRequired(sessionConfiguration.getScopes())) {
            loginForImplicitGrant(activity);
            return;
        } else if (AuthenticationError.INVALID_APP_SIGNATURE.equals(authenticationError)) {
            AppProtocol appProtocol = new AppProtocol();
            String appSignature = appProtocol.getAppSignature(activity);
            if (appSignature == null) {
                Log.e(UberSdk.UBER_SDK_LOG_TAG, "There was an error obtaining your Application Signature. Please check "
                        + "your Application Signature and add it to the developer dashboard at https://developer.uber"
                        + ".com/dashboard");
            } else {
                Log.e(UberSdk.UBER_SDK_LOG_TAG, "Your Application Signature, " + appSignature
                        + ", does not match one of the registered Application Signatures on the developer dashboard. "
                        + "Check your settings at https://developer.uber.com/dashboard");
            }
        }
        callback.onLoginError(authenticationError);
    }

    private void handleResultOk(@Nullable Intent data) {
        if (data == null) {
            // Unknown error, should never occur
            callback.onLoginError(AuthenticationError.UNKNOWN);
            return;
        }

        final String authorizationCode = data.getStringExtra(EXTRA_CODE_RECEIVED);
        if (authorizationCode != null) {
            callback.onAuthorizationCodeReceived(authorizationCode);
        } else {
            AccessToken accessToken = AuthUtils.createAccessToken(data);
            accessTokenStorage.setAccessToken(accessToken);

            callback.onLoginSuccess(accessToken);

        }
    }
}


/*
 * Copyright (c) 2016 Uber Technologies, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.uber.sdk.android.rides;

import android.content.Context;
import android.support.annotation.NonNull;

import com.uber.sdk.android.core.UberSdk;
import com.uber.sdk.core.client.SessionConfiguration;

/**
 * The {@link RideRequestBehavior} to pass to the {@link RideRequestButton} to have it execute a {@link RideRequestDeeplink}.
 */
@Deprecated
public class RequestDeeplinkBehavior implements RideRequestBehavior {

    final SessionConfiguration sessionConfiguration;

    /**
     * Create a {@link RequestDeeplinkBehavior} using the default {@link SessionConfiguration} from {@link UberSdk}
     */
    public RequestDeeplinkBehavior() {
        this.sessionConfiguration = UberSdk.getDefaultSessionConfiguration();
    }

    /**
     * Create a {@link RequestDeeplinkBehavior} with required {@link SessionConfiguration}
     * @param configuration to use for signing with Client Id
     */
    public RequestDeeplinkBehavior(@NonNull SessionConfiguration configuration) {
        this.sessionConfiguration = configuration;
    }

    /**
     * Requests a ride using a {@link RideRequestDeeplink} that is constructed using the provided {@link RideParameters}.
     *
     * @param context {@link Context} to pass to launch the {@link RideRequestDeeplink} from
     * @param params the {@link RideParameters} to use for building and executing the {@link RideRequestDeeplink}
     */
    @Override
    public void requestRide(@NonNull Context context, @NonNull RideParameters params) {
        RideRequestDeeplink deeplink = new RideRequestDeeplink.Builder(context)
                .setSessionConfiguration(sessionConfiguration)
                .setRideParameters(params).build();
        deeplink.execute();
    }
}


/*
 * Copyright (c) 2016 Uber Technologies, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.uber.sdk.android.core.auth;

import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.webkit.CookieManager;

import com.uber.sdk.core.auth.AccessToken;
import com.uber.sdk.core.auth.AccessTokenStorage;
import com.uber.sdk.core.auth.Scope;

import java.util.Collection;
import java.util.Set;

/**
 * Manages the storage of {@link AccessToken}s.
 */
public class AccessTokenManager implements AccessTokenStorage {

    public static final String ACCESS_TOKEN_DEFAULT_KEY = "defaultAccessToken";
    private static final String ACCESS_TOKEN_SHARED_PREFERENCES = "uberSdkAccessTokenConfig";
    private static final String DATE_KEY_SUFFIX = "_date";
    private static final String EXPIRED_EMPTY_LOGGED_IN_COOKIE = "logged_in=;expires=Thu, 01 Jan 1970 00:00:01 GMT";
    private static final String EXPIRED_EMPTY_SESSION_COOKIE = "session=;expires=Thu, 01 Jan 1970 00:00:01 GMT";
    private static final String LOGIN_COOKIE_URL = "https://.login.uber.com";
    private static final String TOKEN_KEY_SUFFIX = "_token";
    private static final String REFRESH_TOKEN_KEY_SUFFIX = "_refresh_token";
    private static final String TOKEN_TYPE_KEY_SUFFIX = "_token_type";
    private static final String SCOPES_KEY_SUFFIX = "_scopes";
    private static final String UBER_COOKIE_URL = ".uber.com";

    @NonNull
    private final SharedPreferences sharedPreferences;

    @NonNull
    private final CookieUtils cookieUtils;

    @NonNull
    private final String accessTokenKey;

    /**
     *
     * @param context for access {@link SharedPreferences} to save {@link AccessToken}
     */
    public AccessTokenManager(@NonNull Context context) {
        this(context, ACCESS_TOKEN_DEFAULT_KEY);
    }

    /**
     * Instantiate {@link AccessTokenManager} and override accessTokenKey.
     * Use one instance per token.
     *
     * @param context
     * @param accessTokenKey key for storage
     */
    public AccessTokenManager(@NonNull Context context, @NonNull String accessTokenKey) {
        this(context, new CookieUtils(), accessTokenKey);
    }

    @VisibleForTesting
    AccessTokenManager(@NonNull Context context,
                       @NonNull CookieUtils cookieManagerUtil) {
        this(context, cookieManagerUtil, ACCESS_TOKEN_DEFAULT_KEY);
    }

    AccessTokenManager(@NonNull Context context,
                               @NonNull CookieUtils cookieManagerUtil,
                               @NonNull String accessTokenKey) {
        sharedPreferences = context.getApplicationContext()
                .getSharedPreferences(ACCESS_TOKEN_SHARED_PREFERENCES, Context.MODE_PRIVATE);
        cookieUtils = cookieManagerUtil;
        this.accessTokenKey = accessTokenKey;
    }

    /**
     * Gets an {@link AccessToken} stored.
     */
    @Override
    @Nullable
    public AccessToken getAccessToken() {

        long expiresIn;
        String token;
        Set<String> scopesString;
        String refreshToken;
        String tokenType;

        try {
            expiresIn = sharedPreferences.getLong(accessTokenKey + DATE_KEY_SUFFIX, -1);
            token = sharedPreferences.getString(accessTokenKey + TOKEN_KEY_SUFFIX, null);
            scopesString = sharedPreferences.getStringSet(accessTokenKey + SCOPES_KEY_SUFFIX, null);
            refreshToken = sharedPreferences.getString(accessTokenKey + REFRESH_TOKEN_KEY_SUFFIX, null);
            tokenType = sharedPreferences.getString(accessTokenKey + TOKEN_TYPE_KEY_SUFFIX, null);
        } catch (ClassCastException ignored) {
            return null;
        }

        if (expiresIn == -1 || token == null || scopesString == null) {
            // Return null, if we can't parse it this token is considered unsaved.
            return null;
        }

        Collection<Scope> scopes;
        try {
            scopes = AuthUtils.stringCollectionToScopeCollection(scopesString);
        } catch (IllegalArgumentException ignored) {
            return null;
        }

        return new AccessToken(expiresIn, scopes, token, refreshToken, tokenType);
    }

    /**
     * Removes the {@link AccessToken} stored.
     **/
    @Override
    public void removeAccessToken() {
        cookieUtils.clearUberCookies();

        sharedPreferences.edit().remove(accessTokenKey + DATE_KEY_SUFFIX).apply();
        sharedPreferences.edit().remove(accessTokenKey + TOKEN_KEY_SUFFIX).apply();
        sharedPreferences.edit().remove(accessTokenKey + SCOPES_KEY_SUFFIX).apply();
        sharedPreferences.edit().remove(accessTokenKey + REFRESH_TOKEN_KEY_SUFFIX).apply();
        sharedPreferences.edit().remove(accessTokenKey + TOKEN_TYPE_KEY_SUFFIX).apply();
    }

    /**
     * Stores the {@link AccessToken}.
     *
     */
    @Override
    public void setAccessToken(@NonNull AccessToken accessToken) {
        sharedPreferences.edit().putLong(accessTokenKey + DATE_KEY_SUFFIX, accessToken.getExpiresIn()).apply();
        sharedPreferences.edit().putString(accessTokenKey + TOKEN_KEY_SUFFIX, accessToken.getToken()).apply();
        sharedPreferences.edit().putStringSet(accessTokenKey + SCOPES_KEY_SUFFIX,
                AuthUtils.scopeCollectionToStringSet(accessToken.getScopes())).apply();
        sharedPreferences.edit().putString(accessTokenKey + REFRESH_TOKEN_KEY_SUFFIX, accessToken.getRefreshToken()).apply();
        sharedPreferences.edit().putString(accessTokenKey + TOKEN_TYPE_KEY_SUFFIX, accessToken.getTokenType()).apply();
    }

    @VisibleForTesting
    static class CookieUtils {

        /**
         * Clears Uber logged_in and session cookies.
         */
        void clearUberCookies() {
            CookieManager cookieManager = CookieManager.getInstance();

            cookieManager.setCookie(UBER_COOKIE_URL, EXPIRED_EMPTY_LOGGED_IN_COOKIE);
            cookieManager.setCookie(LOGIN_COOKIE_URL, EXPIRED_EMPTY_SESSION_COOKIE);

            cookieManager.removeExpiredCookie();
        }
    }
}


// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 com.uber.sdk.android.core.utils;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsClient;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsServiceConnection;
import android.text.TextUtils;
import android.util.Log;

import com.uber.sdk.android.core.UberSdk;

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

/**
 * Helper class for Custom Tabs.
 */
public class CustomTabsHelper {
    private static final String TAG = "CustomTabsHelper";
    static final String STABLE_PACKAGE = "com.android.chrome";
    static final String BETA_PACKAGE = "com.chrome.beta";
    static final String DEV_PACKAGE = "com.chrome.dev";
    static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
    private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
            "android.support.customtabs.extra.KEEP_ALIVE";
    private static final String ACTION_CUSTOM_TABS_CONNECTION =
            "android.support.customtabs.action.CustomTabsService";

    private static String packageNameToUse;

    public CustomTabsHelper() {}

    /**
     * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
     *
     * @param context The host context.
     * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
     * @param uri the Uri to be opened.
     * @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
     */
    public void openCustomTab(
            final Context context,
            final CustomTabsIntent customTabsIntent,
            final Uri uri,
            CustomTabFallback fallback) {
        final String packageName = getPackageNameToUse(context);

        if (packageName != null) {
            final CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {
                @Override
                public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) {
                    client.warmup(0L); // This prevents backgrounding after redirection

                    customTabsIntent.intent.setPackage(packageName);
                    customTabsIntent.intent.setData(uri);
                    customTabsIntent.launchUrl(context, uri);
                }
                @Override
                public void onServiceDisconnected(ComponentName name) {}
            };
            CustomTabsClient.bindCustomTabsService(context, packageName, connection);
        } else if (fallback != null) {
            fallback.openUri(context, uri);
        } else {
            Log.e(UberSdk.UBER_SDK_LOG_TAG,
                    "Use of openCustomTab without Customtab support or a fallback set");
        }
    }

    /**
     * Goes through all apps that handle VIEW intents and have a warmup service. Picks
     * the one chosen by the user if there is one, otherwise makes a best effort to return a
     * valid package name.
     *
     * This is not threadsafe.
     *
     * @param context {@link Context} to use for accessing {@link PackageManager}.
     * @return The package name recommended to use for connecting to custom tabs related components.
     */
    @Nullable
    public String getPackageNameToUse(Context context) {
        if (packageNameToUse != null) return packageNameToUse;

        PackageManager pm = context.getPackageManager();
        // Get default VIEW intent handler.
        Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
        ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
        String defaultViewHandlerPackageName = null;
        if (defaultViewHandlerInfo != null) {
            defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
        }

        // Get all apps that can handle VIEW intents.
        List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
        List<String> packagesSupportingCustomTabs = new ArrayList<>();
        for (ResolveInfo info : resolvedActivityList) {
            Intent serviceIntent = new Intent();
            serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
            serviceIntent.setPackage(info.activityInfo.packageName);
            if (pm.resolveService(serviceIntent, 0) != null) {
                packagesSupportingCustomTabs.add(info.activityInfo.packageName);
            }
        }

        // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
        // and service calls.
        if (packagesSupportingCustomTabs.isEmpty()) {
            packageNameToUse = null;
        } else if (packagesSupportingCustomTabs.size() == 1) {
            packageNameToUse = packagesSupportingCustomTabs.get(0);
        } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
                && !hasSpecializedHandlerIntents(context, activityIntent)
                && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
            packageNameToUse = defaultViewHandlerPackageName;
        } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
            packageNameToUse = STABLE_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
            packageNameToUse = BETA_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
            packageNameToUse = DEV_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
            packageNameToUse = LOCAL_PACKAGE;
        }
        return packageNameToUse;
    }

    /**
     * Used to check whether there is a specialized handler for a given intent.
     * @param intent The intent to check with.
     * @return Whether there is a specialized handler for the given intent.
     */
    private boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
        try {
            PackageManager pm = context.getPackageManager();
            List<ResolveInfo> handlers = pm.queryIntentActivities(
                    intent,
                    PackageManager.GET_RESOLVED_FILTER);
            if (handlers == null || handlers.size() == 0) {
                return false;
            }
            for (ResolveInfo resolveInfo : handlers) {
                IntentFilter filter = resolveInfo.filter;
                if (filter == null) continue;
                if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
                if (resolveInfo.activityInfo == null) continue;
                return true;
            }
        } catch (RuntimeException e) {
            Log.e(TAG, "Runtime exception while getting specialized handlers");
        }
        return false;
    }

    /**
     * @return All possible chrome package names that provide custom tabs feature.
     */
    public String[] getPackages() {
        return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
    }

    /**
     * Fallback that uses browser
     */
    public static class BrowserFallback implements CustomTabFallback {
        @Override
        public void openUri(@NonNull Context context, Uri uri) {
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
        }
    }

    /**
     * To be used as a fallback to open the Uri when Custom Tabs is not available.
     */
    public interface CustomTabFallback {
        /**
         *
         * @param context The Context that wants to open the Uri.
         * @param uri The uri to be opened by the fallback.
         */
        void openUri(@NonNull Context context, Uri uri);
    }
}


/*
 * Copyright (c) 2016 Uber Technologies, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.uber.sdk.android.core.auth;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.StyleRes;
import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.view.View;

import com.uber.sdk.android.core.R;
import com.uber.sdk.android.core.UberButton;
import com.uber.sdk.android.core.UberSdk;
import com.uber.sdk.android.core.UberStyle;
import com.uber.sdk.core.auth.AccessTokenStorage;
import com.uber.sdk.core.auth.Scope;
import com.uber.sdk.core.client.SessionConfiguration;

import java.util.Collection;

import static com.uber.sdk.android.core.utils.Preconditions.checkNotNull;

/**
 * The {@link LoginButton} is used to initiate the Uber SDK Login flow.
 */
public class LoginButton extends UberButton {
    private static final
    @StyleRes
    int[] STYLES = {R.style.UberButton_Login,
            R.style.UberButton_Login_White};

    private AccessTokenStorage accessTokenStorage;
    private SessionConfiguration sessionConfiguration;
    private LoginManager loginManager;
    private LoginCallback callback;
    private Collection<Scope> scopes;
    private int requestCode = LoginManager.REQUEST_CODE_LOGIN_DEFAULT;

    public LoginButton(Context context) {
        super(context);
    }

    public LoginButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LoginButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public LoginButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void init(
            @NonNull Context context,
            @StringRes int defaultText,
            @Nullable AttributeSet attrs,
            int defStyleAttr,
            @NonNull UberStyle uberStyle) {
        setAllCaps(true);

        int defStyleRes = STYLES[uberStyle.getValue()];

        applyStyle(context, R.string.ub__sign_in, attrs, defStyleAttr, defStyleRes);

        final TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,
                R.styleable.LoginButton, 0, 0);

        setRequestCodeFromXml(typedArray);
        setScopesFromXml(typedArray);

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                login();
            }
        });
    }

    @VisibleForTesting
    void login() {
        final Activity activity = getActivity();

        checkNotNull(callback, "Callback has not been set, call setCallback to assign value.");
        if ((scopes == null || scopes.isEmpty()) && (sessionConfiguration.getScopes() == null ||
                sessionConfiguration.getScopes().isEmpty())) {
            throw new IllegalStateException("Scopes are not yet set.");
        }

        getOrCreateLoginManager().login(activity);
    }

    /**
     * A {@link SessionConfiguration} is required to identify the app being authenticated.
     *
     * @param sessionConfiguration to be identified.
     * @return this instance of {@link LoginButton}
     */
    public LoginButton setSessionConfiguration(@NonNull SessionConfiguration sessionConfiguration) {
        this.sessionConfiguration = sessionConfiguration;
        return this;
    }

    /**
     * Provide {@link LoginCallback}
     *
     * @param callback to be used to notify Success or Failure
     * @return this instance of {@link LoginButton}
     */
    public LoginButton setCallback(@NonNull LoginCallback callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Provide {@link Scope}s requested, not providing this will result in a {@link IllegalStateException} during login.
     *
     * @param scopes
     * @return this instance of {@link LoginButton}
     */
    public LoginButton setScopes(@NonNull Collection<Scope> scopes) {
        this.scopes = scopes;
        return this;
    }

    /**
     * Optionally provide a request code to be used when {@link Activity#onActivityResult(int, int, Intent)} is called,
     * otherwise {@link LoginManager#REQUEST_CODE_LOGIN_DEFAULT} will be used.
     *
     * @param requestCode
     * @return this instance of {@link LoginButton}
     */
    public LoginButton setRequestCode(int requestCode) {
        this.requestCode = requestCode;
        return this;
    }

    /**
     * Optionally provide {@link AccessTokenStorage}. If none provided, one will be created.
     *
     * @param accessTokenStorage
     * @return this instance of {@link LoginButton}
     */
    public LoginButton setAccessTokenStorage(AccessTokenStorage accessTokenStorage) {
        this.accessTokenStorage = accessTokenStorage;
        return this;
    }

    /**
     * Optionally provide {@link AccessTokenStorage}. If none provided, one will be created.
     *
     * @param accessTokenStorage
     * @return this instance of {@link LoginButton}
     * @deprecated use {@link LoginButton#setAccessTokenStorage(AccessTokenStorage)}
     */
    @Deprecated
    public LoginButton setAccessTokenManager(AccessTokenStorage accessTokenStorage) {
        this.accessTokenStorage = accessTokenStorage;
        return this;
    }

    /**
     * @return currently assigned {@link LoginManager}.
     * If none has been set, one will be created when login is invoked.
     */
    @Nullable
    public LoginManager getLoginManager() {
        return loginManager;
    }

    /**
     * @return currently assigned {@link LoginCallback}.
     */
    @Nullable
    public LoginCallback getCallback() {
        return callback;
    }

    /**
     * @return currently assigned {@link Scope}s
     */
    @Nullable
    public Collection<Scope> getScopes() {
        return scopes;
    }

    /**
     * @return request code to be used when {@link Activity#onActivityResult(int, int, Intent)} is called. Default
     * value is {@link LoginManager#REQUEST_CODE_LOGIN_DEFAULT}.
     */
    public int getRequestCode() {
        return requestCode;
    }

    /**
     * {@link Activity} result handler to be called from starting {@link Activity}. Stores
     * {@link com.uber.sdk.core.auth.AccessToken} and notifies consumer callback of login result.
     *
     * @param requestCode request code originally supplied to {@link Activity#startActivityForResult(Intent, int)}.
     * @param resultCode  result code from returning {@link Activity}.
     * @param data        data from returning {@link Activity}.
     */
    public void onActivityResult(
            int requestCode,
            int resultCode,
            @Nullable Intent data) {
        if (requestCode == this.requestCode) {
            getOrCreateLoginManager().onActivityResult(getActivity(), requestCode, resultCode, data);
        }
    }

    @NonNull
    @VisibleForTesting
    protected synchronized LoginManager getOrCreateLoginManager() {
        if (loginManager == null) {
            loginManager = new LoginManager(getOrCreateAccessTokenStorage(),
                    callback,
                    getOrCreateSessionConfiguration(),
                    requestCode);
        }
        return loginManager;
    }

    @NonNull
    protected synchronized AccessTokenStorage getOrCreateAccessTokenStorage() {
        if (accessTokenStorage == null) {
            accessTokenStorage = new AccessTokenManager(getContext());
        }
        return accessTokenStorage;
    }

    @NonNull
    protected synchronized SessionConfiguration getOrCreateSessionConfiguration() {
        if (sessionConfiguration == null) {
            sessionConfiguration = UberSdk.getDefaultSessionConfiguration();
        }

        if (scopes != null) {
            sessionConfiguration = sessionConfiguration.newBuilder()
                    .setScopes(scopes)
                    .build();
        }

        return sessionConfiguration;
    }

    private void setRequestCodeFromXml(TypedArray typedArray) {
        final int requestCode = typedArray.getInt(R.styleable.LoginButton_ub__request_code,
                LoginManager.REQUEST_CODE_LOGIN_DEFAULT);

        this.requestCode = requestCode;
    }

    private void setScopesFromXml(TypedArray typedArray) {
        final int scopes = typedArray.getInt(R.styleable.LoginButton_ub__scopes, 0);
        if (scopes > 0) {
            this.scopes = Scope.parseScopes(scopes);
        }
    }

}