/*
 * 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.utils;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

public class PackageManagers {

    /**
     * A safe method to obtain {@link PackageInfo} from {@link android.content.pm.PackageManager}
     * If package is not available, it would just return null.
     *
     * @param context to be used for looking
     * @param packageName to look up against context
     * @return {@link PackageInfo} retrieved from Context.
     */
    @Nullable
    public static PackageInfo getPackageInfo(@NonNull Context context, @NonNull String packageName) {
        try {
            return context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    /**
     * A safe method to check if an app is installed.
     *
     * @param context to be used to check for package
     * @param packageName of the app to be checked.
     * @return true only if app using packageName is installed.
     */
    public static boolean isPackageAvailable(@NonNull Context context, @NonNull String packageName) {
        return (getPackageInfo(context, packageName) != null);
    }
}


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

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
import android.support.annotation.NonNull;
import android.util.Log;

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

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Utility {
    private static final String HASH_ALGORITHM_SHA1 = "SHA-1";

    public static String sha1hash(String key) {
        return hashWithAlgorithm(HASH_ALGORITHM_SHA1, key);
    }

    public static String sha1hash(byte[] bytes) {
        return hashWithAlgorithm(HASH_ALGORITHM_SHA1, bytes);
    }

    /**
     * Detects if the Application is currently in a Debug state
     */
    public static boolean isDebugable(@NonNull Context context) {
        return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo
                .FLAG_DEBUGGABLE ) );

    }

    /**
     * Logs error and when debug is enabled, shows Alert Dialog with debug instructions.
     *
     * @param activity
     * @param logMessage
     * @param alertTitle
     * @param alertMessage
     * @return true if developer error is shown, false otherwise.
     */
    public static boolean logAndShowBlockingDebugUIAlert(@NonNull Activity activity,
            final @NonNull String logMessage,
            final @NonNull String alertTitle,
            final @NonNull String alertMessage,
            final @NonNull RuntimeException exception) {
        Log.e(UberSdk.UBER_SDK_LOG_TAG, logMessage, exception);

        if(Utility.isDebugable(activity)) {
            new AlertDialog.Builder(activity)
                    .setTitle(alertTitle)
                    .setMessage(alertMessage)
                    .setNeutralButton(R.string.ub__alert_dialog_ok, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                        }
                    }).show();
            return true;
        }
        return false;
    }

    private static String hashWithAlgorithm(String algorithm, String key) {
        return hashWithAlgorithm(algorithm, key.getBytes());
    }

    private static String hashWithAlgorithm(String algorithm, byte[] bytes) {
        MessageDigest hash;
        try {
            hash = MessageDigest.getInstance(algorithm);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
        return hashBytes(hash, bytes);
    }

    private static String hashBytes(MessageDigest hash, byte[] bytes) {
        hash.update(bytes);
        byte[] digest = hash.digest();
        StringBuilder builder = new StringBuilder();
        for (int b : digest) {
            builder.append(Integer.toHexString((b >> 4) & 0xf));
            builder.append(Integer.toHexString((b >> 0) & 0xf));
        }
        return builder.toString();
    }
}


/*
 * 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.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.webkit.GeolocationPermissions;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;

import com.uber.sdk.android.core.UberSdk;
import com.uber.sdk.android.core.auth.AccessTokenManager;
import com.uber.sdk.core.auth.AccessToken;
import com.uber.sdk.core.auth.AccessTokenStorage;
import com.uber.sdk.core.client.AccessTokenSession;
import com.uber.sdk.core.client.SessionConfiguration;

import java.util.HashMap;
import java.util.Map;

/**
 * The Uber Ride Request View: an embeddable view that provides the end-to-end Uber experience.
 * The primary way to interact with this view after construction is to call the load() function.
 *
 * @deprecated in favor of directly using mobile web directly.
 * See https://developer.uber.com/docs/riders/ride-requests/tutorials/widget/migration-to-muber
 */
@Deprecated
public class RideRequestView extends LinearLayout {

    private static final String USER_AGENT_RIDE_VIEW = String.format("rides-android-v%s-ride_request_view",
            BuildConfig.VERSION_NAME);
    @Nullable private AccessTokenSession accessTokenSession;
    @NonNull @VisibleForTesting RideParameters rideParameters = new RideParameters.Builder().build();
    @Nullable private RideRequestViewCallback rideRequestViewCallback;
    private WebView webView;

    public RideRequestView(Context context) {
        this(context, null);
    }

    public RideRequestView(Context context, AttributeSet attrs) {
        super(context, attrs, 0);
        init(context);
    }

    /**
     * Stops all current loading and brings up a blank page.
     */
    public void cancelLoad() {
        webView.stopLoading();
        webView.loadUrl("about:blank");
    }

    /**
     * Gets the {@link AccessToken} being used to authorize the {@link RideRequestView}.
     *
     * @return
     */
    @Nullable
    public AccessTokenSession getSession() {
        return accessTokenSession;
    }

    /**
     * Loads the ride widget.
     * Requires that the {@link AccessToken} has been retrieved.
     */
    public void load() {
        final SessionConfiguration config;
        final AccessTokenStorage storage;

        if (accessTokenSession == null && UberSdk.isInitialized()) {
            config = UberSdk.getDefaultSessionConfiguration();
            storage = new AccessTokenManager(getContext());
            accessTokenSession = new AccessTokenSession(config, storage);

        } else if (accessTokenSession != null) {
            config = accessTokenSession.getAuthenticator().getSessionConfiguration();
            storage = accessTokenSession.getAuthenticator().getTokenStorage();
        } else {
            config = null;
            storage = null;
        }

        if (config == null || storage == null || storage.getAccessToken() == null) {
            if (rideRequestViewCallback != null) {
                rideRequestViewCallback.onErrorReceived(RideRequestViewError.NO_ACCESS_TOKEN);
            }
            return;
        }

        webView.loadUrl(buildUrlFromRideParameters(getContext(), rideParameters,
                        config),
                        RideRequestView.getHeaders(storage.getAccessToken()));
    }

    /**
     * Set a custom {@link AccessTokenSession} to use for authenticating into the Ride Widget.
     *
     * @param accessTokenSession the {@link AccessTokenSession} to use for authorization
     */
    public void setSession(@Nullable AccessTokenSession accessTokenSession) {
        this.accessTokenSession = accessTokenSession;
    }

    /**
     * Configure parameters for the Ride Request Control.
     *
     * @param rideParameters the {@link RideParameters} to use for presetting values
     */
    public void setRideParameters(@NonNull RideParameters rideParameters) {
        this.rideParameters = rideParameters;
    }

    /**
     * Sets the callback for events occurring in the Ride Request Control such as errors.
     *
     * @param rideRequestViewCallback the {@link RideRequestViewCallback}
     */
    public void setRideRequestViewCallback(@NonNull RideRequestViewCallback rideRequestViewCallback) {
        this.rideRequestViewCallback = rideRequestViewCallback;
    }

    /**
     * Builds a URL with necessary query parameters to load in the {@link WebView}.
     *
     * @param rideParameters the {@link RideParameters} to build into the query
     * @return the URL {@link String} to load in the {@link WebView}
     */
    @NonNull
    @VisibleForTesting
    static String buildUrlFromRideParameters(@NonNull Context context,
                                             @NonNull RideParameters rideParameters,
                                             @NonNull SessionConfiguration loginConfiguration) {
        final String ENDPOINT = "components";
        final String ENVIRONMENT_KEY = "env";
        final String HTTPS = "https";
        final String PATH = "rides/";
        final String SANDBOX = "sandbox";

        Uri.Builder builder = new Uri.Builder();
        builder.scheme(HTTPS)
                .authority(ENDPOINT + "." + loginConfiguration.getEndpointRegion().getDomain())
                .appendEncodedPath(PATH);

        if (rideParameters.getUserAgent() == null) {
            rideParameters.setUserAgent(USER_AGENT_RIDE_VIEW);
        }

        RideRequestDeeplink deeplink = new RideRequestDeeplink.Builder(context)
                .setSessionConfiguration(loginConfiguration)
                .setRideParameters(rideParameters).build();
        Uri uri = deeplink.getUri();
        builder.encodedQuery(uri.getEncodedQuery());

        if (loginConfiguration.getEnvironment() == SessionConfiguration.Environment.SANDBOX) {
            builder.appendQueryParameter(ENVIRONMENT_KEY, SANDBOX);
        }

        return builder.build().toString();
    }

    /**
     * Creates a {@link Map} of the headers needed to pass to the {@link WebView}.
     *
     * @param accessToken the {@link AccessToken} to use for the Authorization header.
     * @return a {@link Map} containing headers to pass to {@link WebView}.
     */
    @NonNull
    @VisibleForTesting static Map<String, String> getHeaders(@NonNull AccessToken accessToken) {
        final String AUTH_HEADER = "Authorization";
        final String BEARER = "Bearer";

        Map<String, String> headers = new HashMap<String, String>();
        headers.put(AUTH_HEADER, BEARER + " " + accessToken.getToken());
        return headers;
    }

    /**
     * Initialize the layout, properties, and inner web view.
     */
    private void init(@NonNull Context context) {
        inflate(getContext(), R.layout.ub__ride_request_view, this);
        webView = (WebView) findViewById(R.id.ub__ride_request_webview);

        webView.getSettings().setJavaScriptEnabled(true);
        webView.getSettings().setGeolocationEnabled(true);
        webView.getSettings().setAppCacheEnabled(true);
        webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        webView.setWebChromeClient(new RideRequestWebChromeClient());
        webView.setWebViewClient(new RideRequestWebViewClient(new RideRequestWebViewClientCallback() {
            @Override
            public void onErrorParsed(@NonNull RideRequestViewError error) {
                if (rideRequestViewCallback != null) {
                    rideRequestViewCallback.onErrorReceived(error);
                }
            }
        }));
    }

    /**
     * Interface for {@link RideRequestWebViewClient} to communicate with this view.
     */
    @VisibleForTesting
    interface RideRequestWebViewClientCallback {

        /**
         * Called when an error is received in the redirect URL of the {@link WebView}.
         *
         * @param error the {@link RideRequestViewError} that occurred.
         */
        void onErrorParsed(@NonNull RideRequestViewError error);
    }

    /**
     * The {@link WebViewClient} that listens for errors in the URL of the {@link WebView}.
     */
    @VisibleForTesting
    class RideRequestWebViewClient extends WebViewClient {

        private static final String ERROR_KEY = "error";
        private static final String REDIRECT_URL = "uberconnect://oauth";

        @NonNull private RideRequestWebViewClientCallback rideRequestWebViewClientCallback;

        /**
         * Construct the web view client to listen for and report back errors through a callback.
         *
         * @param callback the {@link com.uber.sdk.android.rides.RideRequestView.RideRequestWebViewClientCallback}
         */
        @VisibleForTesting
        RideRequestWebViewClient(@NonNull RideRequestWebViewClientCallback callback) {
            rideRequestWebViewClientCallback = callback;
        }

        /**
         * add deprecated member "onReceivedError" to solve compatibility issue when API level < 23
         * @param view
         * @param errorCode
         * @param description
         * @param failingUrl
         */
        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
                receivedError();
            }
        }

        @TargetApi(23)
        @Override
        public void onReceivedError(WebView view,
                WebResourceRequest request,
                WebResourceError error) {
            receivedError();
        }

        private void receivedError(){
            rideRequestWebViewClientCallback.onErrorParsed(RideRequestViewError.CONNECTIVITY_ISSUE);
        }

        @TargetApi(23)
        @Override
        public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
            // This is a no-op necessary for testing as robolectric only supports up
            // to API 21 and this call was added in API 23
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (url.toLowerCase().startsWith(REDIRECT_URL)) {
                Uri uri = Uri.parse(url);

                Uri fragmentUri = new Uri.Builder().encodedQuery(uri.getFragment()).build();
                String errorValue = fragmentUri.getQueryParameter(ERROR_KEY);
                RideRequestViewError error = RideRequestViewError.UNKNOWN;
                if (errorValue != null) {
                    try {
                        error = RideRequestViewError.valueOf(errorValue.toUpperCase());
                    } catch (IllegalArgumentException e) {
                        error = RideRequestViewError.UNKNOWN;
                    }
                }
                rideRequestWebViewClientCallback.onErrorParsed(error);
                return true;
            } else if (url.startsWith("http:") || url.startsWith("https:")) {
                return false;
            } else {
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                RideRequestView.this.getContext().startActivity(intent);
                return true;
            }
        }
    }

    /**
     * The {@link WebChromeClient} used for overriding the {@link GeolocationPermissions} prompt.
     */
    private class RideRequestWebChromeClient extends WebChromeClient {

        /**
         * The default implementation does nothing, so permission is never obtained and passed to Javascript.
         * Overriding to always gain permission as {@link RideRequestView} assumes the app has already gained
         * location permissions.
         */
        @Override
        public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
            callback.invoke(origin, true, false);
        }
    }
}


/*
 * 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.utils;

import android.support.annotation.NonNull;

import java.util.Collection;

/**
 * Simple static methods to be called at the start of your own methods to verify
 * correct arguments and state.
 */
public class Preconditions {

    /**
     * Ensures the truth of an expression involving the state of the calling
     * instance, but not involving any parameters to the calling method.
     *
     * @param expression a boolean expression
     * @param errorMessage error message thrown if expression is false.
     * @throws IllegalStateException if {@code expression} is false
     */
    public static void checkState(final boolean expression, @NonNull String errorMessage) {
        if (!expression) {
            throw new IllegalStateException(errorMessage);
        }
    }

    /**
     * Ensures the value is not null.
     *
     * @param value value to be validated.
     * @param errorMessage error message thrown if value is null.
     * @param <T> type of value.
     * @return value passed in if not null.
     */
    @NonNull
    public static <T> T checkNotNull(T value, @NonNull String errorMessage) {
        if (value == null) {
            throw new NullPointerException(errorMessage);
        }
        return value;
    }

    /**
     * Ensures a collection is neither null nor empty.
     *
     * @param collection collection to be validated.
     * @param errorMessage error message to be thrown if collection is null or empty.
     * @param <T> type of the collection item.
     * @return collection passed in.
     */
    @NonNull
    public static <T> Collection<T> checkNotEmpty(Collection<T> collection, @NonNull String errorMessage) {
        checkState(collection != null && !collection.isEmpty(), errorMessage);
        return collection;
    }
}


/*
 * 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.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

import com.uber.sdk.android.core.auth.AccessTokenManager;
import com.uber.sdk.android.core.auth.AuthenticationError;
import com.uber.sdk.android.core.auth.LoginCallback;
import com.uber.sdk.android.core.auth.LoginManager;
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.SessionConfiguration;

import java.util.Arrays;

/**
 * The {@link RideRequestActivity} provides the entire flow to have a user authorize with their Uber account
 * and display the {@link RideRequestView} once authorized in one activity.
 */
public class RideRequestActivity extends Activity implements LoginCallback, RideRequestViewCallback {

    /**
     * Key for a {@link AuthenticationError} to be passed back to the calling activity.
     */
    public static final String AUTHENTICATION_ERROR = "authentication_error";

    /**
     * Key for a {@link RideRequestViewError} to be passed back to the calling activity.
     */
    public static final String RIDE_REQUEST_ERROR = "ride_request_error";

    @VisibleForTesting
    static final int LOGIN_REQUEST_CODE = 1112;

    private static final int REQUEST_FINE_LOCATION_PERMISSION_CODE = 1002;
    private static final String USER_AGENT_RIDE_WIDGET = String.format("rides-android-v%s-ride_request_widget",
            BuildConfig.VERSION_NAME);

    @VisibleForTesting static final String RIDE_PARAMETERS = "ride_parameters";
    static final String EXTRA_LOGIN_CONFIGURATION = "login_configuration";
    static final String EXTRA_ACCESS_TOKEN_STORAGE_KEY = "access_token_storage_key";

    @VisibleForTesting AccessTokenStorage accessTokenStorage;
    @Nullable @VisibleForTesting AlertDialog authenticationErrorDialog;
    @Nullable @VisibleForTesting AlertDialog rideRequestErrorDialog;
    @VisibleForTesting RideRequestView rideRequestView;
    @VisibleForTesting LoginManager loginManager;
    SessionConfiguration sessionConfiguration;

    /**
     * Creates a new {@link Intent} to be passed in to this activity with all the required information.
     *
     * @param context the {@link Context} that will be launching this activity
     * @param rideParameters the optional {@link RideParameters} containing information to populate the {@link RideRequestView}
     * @param loginConfiguration required when RideRequestActivity needs to start authentication flow
     * @param accessTokenStorageKey optional key to lookup access token from {@link com.uber.sdk.core.auth.AccessTokenStorage}
     * @return new {@link Intent} with the necessary parameters for this activity
     */
    @NonNull
    public static Intent newIntent(@NonNull Context context,
            @Nullable RideParameters rideParameters,
            @NonNull SessionConfiguration loginConfiguration,
            @Nullable String accessTokenStorageKey) {
        Intent data = new Intent(context, RideRequestActivity.class);

        if (rideParameters == null) {
            rideParameters = new RideParameters.Builder().build();
        }

        data.putExtra(RIDE_PARAMETERS, rideParameters);
        data.putExtra(EXTRA_LOGIN_CONFIGURATION, loginConfiguration);
        data.putExtra(EXTRA_ACCESS_TOKEN_STORAGE_KEY, accessTokenStorageKey);
        return data;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.ub__ride_request_activity);

        String accessTokenStorageKey = getIntent()
                .getExtras()
                .getString(EXTRA_ACCESS_TOKEN_STORAGE_KEY, AccessTokenManager.ACCESS_TOKEN_DEFAULT_KEY);

        rideRequestView = (RideRequestView) findViewById(R.id.ub__ride_request_view);
        accessTokenStorage = new AccessTokenManager(this, accessTokenStorageKey);

        RideParameters rideParameters = getIntent().getParcelableExtra(RIDE_PARAMETERS);
        if (rideParameters == null) {
            rideParameters = new RideParameters.Builder().build();
        }

        if (rideParameters.getUserAgent() == null) {
            rideParameters.setUserAgent(USER_AGENT_RIDE_WIDGET);
        }

        SessionConfiguration loginConfiguration = (SessionConfiguration) getIntent().getSerializableExtra(EXTRA_LOGIN_CONFIGURATION);
        sessionConfiguration = loginConfiguration
                .newBuilder()
                .setScopes(Arrays.asList(Scope.RIDE_WIDGETS))
                .build();

        loginManager = new LoginManager(accessTokenStorage, this, sessionConfiguration, LOGIN_REQUEST_CODE);
        rideRequestView.setRideParameters(rideParameters);
        rideRequestView.setRideRequestViewCallback(this);

        int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION);
        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.ACCESS_FINE_LOCATION},
                    REQUEST_FINE_LOCATION_PERMISSION_CODE);
        } else {
            load();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == LOGIN_REQUEST_CODE) {
            loginManager.onActivityResult(this, requestCode, resultCode, data);
        }
    }

    @Override
    public void onErrorReceived(@NonNull RideRequestViewError error) {
        rideRequestView.cancelLoad();
        Intent data = new Intent();
        data.putExtra(RIDE_REQUEST_ERROR, error);
        switch (error) {
            case CONNECTIVITY_ISSUE:
                rideRequestErrorDialog = buildRetryAlert(
                        R.string.ub__ride_request_activity_widget_error,
                        R.string.ub__ride_error_try_again,
                        android.R.string.cancel,
                        data);
                rideRequestErrorDialog.show();
                break;
            case NO_ACCESS_TOKEN:
            case UNAUTHORIZED:
                accessTokenStorage.removeAccessToken();
                login();
                break;
            default:
                rideRequestErrorDialog = buildErrorAlert(R.string.ub__ride_request_activity_widget_error,
                        android.R.string.ok,
                        data);
                rideRequestErrorDialog.show();
        }
    }

    @Override
    public void onLoginCancel() {
        setResult(RESULT_CANCELED, null);
        finish();
    }

    @Override
    public void onLoginError(@NonNull AuthenticationError error) {
        Intent data = new Intent();
        data.putExtra(AUTHENTICATION_ERROR, error);
        if (AuthenticationError.CONNECTIVITY_ISSUE.equals(error)) {
            authenticationErrorDialog = buildRetryAlert(
                    R.string.ub__ride_request_activity_authentication_error,
                    R.string.ub__ride_error_try_again,
                    android.R.string.cancel,
                    data);
        } else {
            authenticationErrorDialog = buildErrorAlert(
                    R.string.ub__ride_request_activity_authentication_error,
                    android.R.string.ok,
                    data);
        }
        authenticationErrorDialog.show();
    }

    @Override
    public void onLoginSuccess(@NonNull AccessToken accessToken) {
        accessTokenStorage.setAccessToken(accessToken);
        load();
    }

    @Override
    public void onAuthorizationCodeReceived(@NonNull String authorizationCode) {
        //This code should be unreachable, as we request for implicit grant.
        final Intent error = new Intent().putExtra(AUTHENTICATION_ERROR, AuthenticationError.INVALID_FLOW_ERROR);
        authenticationErrorDialog = buildErrorAlert(R.string.ub__ride_request_activity_authentication_error,
                android.R.string.ok, error);
    }

    @Override
    public void onRequestPermissionsResult(
            int requestCode,
            @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        if (requestCode == REQUEST_FINE_LOCATION_PERMISSION_CODE) {
            load();
        }
    }

    /**
     * Loads the appropriate view in the activity based on whether user is successfully authorized or not.
     */
    private void load() {
        AccessToken accessToken = accessTokenStorage.getAccessToken();

        if (accessToken != null) {
            AccessTokenSession session = new AccessTokenSession(sessionConfiguration,
                    accessTokenStorage);
            rideRequestView.setSession(session);

            loadRideRequestView();
        } else {
            login();
        }
    }

    private void login() {
        loginManager.login(this);
    }

    /**
     * Loads the {@link RideRequestView}.
     */
    private void loadRideRequestView() {
        rideRequestView.load();
    }

    /**
     * Builds an {@link AlertDialog} to the user to indicate an error and dismisses activity.
     *
     * @param messageTextId the message content {@link StringRes} text ID
     * @param positiveButtonTextId the positive button {@link StringRes} text ID
     * @param intent the {@link Intent} to pass in the result
     * @return an {@link AlertDialog} to show
     */
    @NonNull
    private AlertDialog buildErrorAlert(
            @StringRes int messageTextId,
            @StringRes int positiveButtonTextId,
            final Intent intent) {
        return new AlertDialog.Builder(this)
                .setMessage(messageTextId)
                .setPositiveButton(positiveButtonTextId, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        setResult(RESULT_CANCELED, intent);
                        finish();
                    }
                })
                .setOnCancelListener(new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        setResult(RESULT_CANCELED, intent);
                        finish();
                    }
                })
                .create();
    }

    /**
     * Builds an {@link AlertDialog} to the user to indicate a {@link RideRequestViewError#CONNECTIVITY_ISSUE}
     * and allow the retry of the button.
     *
     * @param messageTextId the message content {@link StringRes} text ID
     * @param positiveButtonTextId the positive button {@link StringRes} text ID
     * @param negativeButtonTextId the negative button {@link StringRes} text ID
     * @param intent the {@link Intent} to pass in the result
     * @return an {@link AlertDialog} to show
     */
    @NonNull
    private AlertDialog buildRetryAlert(
            @StringRes int messageTextId,
            @StringRes int positiveButtonTextId,
            @StringRes int negativeButtonTextId,
            final Intent intent) {
        return new AlertDialog.Builder(this)
                .setMessage(messageTextId)
                .setPositiveButton(positiveButtonTextId, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        load();
                    }
                })
                .setNegativeButton(negativeButtonTextId, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        setResult(RESULT_CANCELED, intent);
                        finish();
                    }
                })
                .setOnCancelListener(new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialog) {
                        setResult(RESULT_CANCELED, intent);
                        finish();
                    }
                })
                .create();
    }
}


/*
 * 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.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.customtabs.CustomTabsIntent;

import com.uber.sdk.android.core.Deeplink;
import com.uber.sdk.android.core.utils.AppProtocol;
import com.uber.sdk.android.core.utils.CustomTabsHelper;
import com.uber.sdk.core.client.SessionConfiguration;

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

/**
 * A deeplink for requesting rides in the Uber application.
 *
 * @see Uber deeplink documentation
 */
public class RideRequestDeeplink implements Deeplink {

    private static final String USER_AGENT_DEEPLINK = String.format("rides-android-v%s-deeplink",
            BuildConfig.VERSION_NAME);

    @NonNull
    private final Uri uri;
    @NonNull
    private final Context context;
    @NonNull
    private final AppProtocol appProtocol;
    @NonNull
    private final CustomTabsHelper customTabsHelper;

    RideRequestDeeplink(@NonNull Context context,
            @NonNull Uri uri,
            @NonNull AppProtocol appProtocol,
            @NonNull CustomTabsHelper customTabsHelper) {
        this.uri = uri;
        this.context = context;
        this.appProtocol = appProtocol;
        this.customTabsHelper = customTabsHelper;
    }

    /**
     * Executes the deeplink to launch the Uber app.  If the app is not installed redirects to the play store.
     */
    public void execute() {
        final CustomTabsIntent intent = new CustomTabsIntent.Builder().build();
        customTabsHelper.openCustomTab(context, intent, uri, new CustomTabsHelper.BrowserFallback());
    }

    @Override
    public boolean isSupported() {
        return appProtocol.isInstalled(context, UBER);
    }

    /**
     * @return The {@link Uri} for the deeplink.
     */
    @NonNull
    public Uri getUri() {
        return uri;
    }

    /**
     * Builder for {@link RideRequestDeeplink} objects.
     */
    public static class Builder {

        public static final String AUTHORITY = "riderequest";
        public static final String ACTION = "action";
        public static final String SET_PICKUP = "setPickup";
        public static final String CLIENT_ID = "client_id";
        public static final String PRODUCT_ID = "product_id";
        public static final String MY_LOCATION = "my_location";
        public static final String LATITUDE = "[latitude]";
        public static final String LONGITUDE = "[longitude]";
        public static final String NICKNAME = "[nickname]";
        public static final String FORMATTED_ADDRESS = "[formatted_address]";
        public static final String USER_AGENT = "user-agent";

        private RideParameters rideParameters;
        private SessionConfiguration sessionConfiguration;
        private Fallback fallback = Fallback.APP_INSTALL;
        private final Context context;
        private AppProtocol appProtocol;
        private CustomTabsHelper customTabsHelper;

        /**
         * @param context to execute the deeplink.
         */
        public Builder(Context context) {
            this.context = context;
        }

        /**
         * Sets the {@link RideParameters} for the deeplink.
         *
         * @return this instance of {@link Builder}
         */
        public RideRequestDeeplink.Builder setRideParameters(@NonNull RideParameters rideParameters) {
            this.rideParameters = rideParameters;
            return this;
        }

        /**
         * Sets the client Id
         *
         * @return this instance of {@link Builder}
         */
        public RideRequestDeeplink.Builder setSessionConfiguration(@NonNull SessionConfiguration
                sessionConfiguration) {
            this.sessionConfiguration = sessionConfiguration;
            return this;
        }

        /**
         * Sets the fallback to use when the Uber app isn't installed.
         *
         * @return this instance of {@link Builder}
         */
        public RideRequestDeeplink.Builder setFallback(@NonNull  Fallback fallback) {
            this.fallback = fallback;
            return this;
        }

        @VisibleForTesting
        RideRequestDeeplink.Builder setCustomTabsHelper(@NonNull CustomTabsHelper customTabsHelper) {
            this.customTabsHelper = customTabsHelper;
            return this;
        }

        @VisibleForTesting
        RideRequestDeeplink.Builder setAppProtocol(@NonNull AppProtocol appProtocol){
            this.appProtocol = appProtocol;
            return this;
        }

        /**
         * Builds an {@link RideRequestDeeplink} object.
         *
         * @return {@link RideRequestDeeplink} generated from parameters
         */
        @NonNull
        public RideRequestDeeplink build() {
            checkNotNull(rideParameters, "Must supply ride parameters.");
            checkNotNull(sessionConfiguration, "Must supply a Session Configuration");
            checkNotNull(sessionConfiguration.getClientId(), "Must supply client Id on Login Configuration");

            if (appProtocol == null) {
                appProtocol = new AppProtocol();
            }
            if (customTabsHelper == null) {
                customTabsHelper = new CustomTabsHelper();
            }

            final Uri.Builder builder = getUriBuilder(context, fallback);

            builder.appendQueryParameter(ACTION, SET_PICKUP);
            builder.appendQueryParameter(CLIENT_ID, sessionConfiguration.getClientId());
            if (rideParameters.getProductId() != null) {
                builder.appendQueryParameter(PRODUCT_ID, rideParameters.getProductId());
            }
            if (rideParameters.getPickupLatitude() != null && rideParameters.getPickupLongitude() != null) {
                addLocation(LocationType.PICKUP, Double.toString(rideParameters.getPickupLatitude()),
                        Double.toString(rideParameters.getPickupLongitude()), rideParameters.getPickupNickname(),
                        rideParameters.getPickupAddress(), builder);
            }
            if (rideParameters.isPickupMyLocation()) {
                builder.appendQueryParameter(LocationType.PICKUP.getUriQueryKey(), MY_LOCATION);
            }
            if (rideParameters.getDropoffLatitude() != null && rideParameters.getDropoffLongitude() != null) {
                addLocation(LocationType.DROPOFF, Double.toString(rideParameters.getDropoffLatitude()),
                        Double.toString(rideParameters.getDropoffLongitude()), rideParameters.getDropoffNickname(),
                        rideParameters.getDropoffAddress(), builder);
            }

            String userAgent = rideParameters.getUserAgent();
            if (userAgent == null) {
                userAgent = USER_AGENT_DEEPLINK;
            }
            builder.appendQueryParameter(USER_AGENT, userAgent);

            return new RideRequestDeeplink(context, builder.build(), appProtocol, customTabsHelper);
        }

        private void addLocation(
                @NonNull LocationType locationType, @NonNull String latitude,
                @NonNull String longitude, @Nullable String nickname, @Nullable String address, Uri.Builder builder) {
            String typeQueryKey = locationType.getUriQueryKey();
            builder.appendQueryParameter(typeQueryKey + LATITUDE, latitude);
            builder.appendQueryParameter(typeQueryKey + LONGITUDE, longitude);
            if (nickname != null) {
                builder.appendQueryParameter(typeQueryKey + NICKNAME, nickname);
            }
            if (address != null) {
                builder.appendQueryParameter(typeQueryKey + FORMATTED_ADDRESS, address);
            }
        }

        Uri.Builder getUriBuilder(@NonNull Context context, @NonNull Deeplink.Fallback
                fallback) {
            final Uri.Builder builder;
            if (appProtocol.isInstalled(context, UBER)) {
                if (appProtocol.isAppLinkSupported()) {
                    builder = Uri.parse(Deeplink.APP_LINK_URI).buildUpon();
                } else {
                    builder = new Uri.Builder()
                            .scheme(Deeplink.DEEPLINK_SCHEME);
                }
            } else {
                if (fallback == Deeplink.Fallback.MOBILE_WEB) {
                    builder = Uri.parse(Deeplink.MOBILE_WEB_URI).buildUpon();
                } else {
                    builder = Uri.parse(Deeplink.APP_LINK_URI).buildUpon();
                }
            }
            return builder;
        }
    }

    private enum LocationType {
        PICKUP,
        DROPOFF;

        private String getUriQueryKey() {
            return name().toLowerCase();
        }
    }
}


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

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Base64;
import android.util.Pair;
import com.uber.sdk.android.core.SupportedAppType;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import static com.uber.sdk.android.core.SupportedAppType.UBER;
import static com.uber.sdk.android.core.SupportedAppType.UBER_EATS;

public class AppProtocol {
    @Deprecated
    public static final String[] UBER_PACKAGE_NAMES =
            {"com.ubercab", "com.ubercab.presidio.app", "com.ubercab.presidio.exo", "com.ubercab.presidio.development"};

    @VisibleForTesting
    static final String[] RIDER_PACKAGE_NAMES =
            {"com.ubercab.presidio.development", "com.ubercab.presidio.exo", "com.ubercab.presidio.app", "com.ubercab"};
    @VisibleForTesting
    static final String[] EATS_PACKAGE_NAMES =
            {"com.ubercab.eats.debug", "com.ubercab.eats.exo", "com.ubercab.eats.nightly", "com.ubercab.eats"};
    public static final String PLATFORM = "android";

    private static final String UBER_RIDER_HASH = "411c40b31f6d01dac68d711df99b6eafeec8e73b";
    private static final String UBER_EATS_HASH = "ae0b86995f174533b423067837beba13d922fbb0";
    private static final String HASH_ALGORITHM_SHA1 = "SHA-1";
    private static final int DEFAULT_MIN_VERSION = 0;

    private static final HashSet<String> validAppSignatureHashes = buildAppSignatureHashes();

    @NonNull
    private static HashSet<String> buildAppSignatureHashes() {
        HashSet<String> set = new HashSet<>();
        set.add(UBER_RIDER_HASH);
        set.add(UBER_EATS_HASH);
        return set;
    }

    /**
     * @return Map of SupportedAppType and their respective package names.
     */
    @NonNull
    private static Map<SupportedAppType, List<String>> getSupportedPackageNames() {
        Map<SupportedAppType, List<String>> packageNames = new HashMap<>();
        packageNames.put(UBER, Arrays.asList(RIDER_PACKAGE_NAMES));
        packageNames.put(UBER_EATS, Arrays.asList(EATS_PACKAGE_NAMES));
        return packageNames;
    }

    /**
     * Validates minimum version of app required or returns true if in debug.
     */
    public boolean validateMinimumVersion(
            @NonNull Context context, @NonNull PackageInfo packageInfo, int minimumVersion) {
        if (isDebug(context)) {
            return true;
        }

        return packageInfo.versionCode >= minimumVersion;
    }

    /**
     * @deprecated Use {@link #isInstalled(Context, SupportedAppType)}.
     */
    @Deprecated
    public boolean isUberInstalled(@NonNull Context context) {
        return isInstalled(context, UBER);
    }

    /**
     * Verify if any version of the app has been installed on this device.
     *
     * @param context      A {@link Context}.
     * @param supportedApp The {@link SupportedAppType}.
     * @return {@code true} if any version of the app is installed (min version of 0).
     */
    public boolean isInstalled(@NonNull Context context, @NonNull SupportedAppType supportedApp) {
        return isInstalled(context, supportedApp, DEFAULT_MIN_VERSION);
    }

    /**
     * Check if the minimum version of {@link SupportedAppType} is installed.
     *
     * @param context        A {@link Context}.
     * @param supportedApp   The {@link SupportedAppType}.
     * @param minimumVersion The minimum version of {@link SupportedAppType} that must be installed.
     * @return {@code true} if any valid package for the app is installed.
     */
    public boolean isInstalled(@NonNull Context context, @NonNull SupportedAppType supportedApp, int minimumVersion) {
        return !getInstalledPackages(context, supportedApp, minimumVersion).isEmpty();

    }

    /**
     * Find the installed and validated packages for a {@link SupportedAppType}.
     * <p>
     * This will validate the signature and minimum version of the installed package or exclude it from returned list.
     *
     * @param context         A {@link Context}.
     * @param supportedApp   The {@link SupportedAppType}.
     * @param minimumVersion The minimum version of {@link SupportedAppType} that must be installed.
     * @return A list of {@link PackageInfo} which is installed and has been validated.
     */
    @NonNull
    public List<PackageInfo> getInstalledPackages(
            @NonNull Context context, @NonNull SupportedAppType supportedApp, int minimumVersion) {
        List<PackageInfo> packageInfos = new ArrayList<>();

        List<Pair<SupportedAppType, PackageInfo>> installedApps = getInstalledPackagesByApp(context, supportedApp);

        for (Pair<SupportedAppType, PackageInfo> installedApp : installedApps) {
            PackageInfo packageInfo = installedApp.second;
            if (packageInfo != null
                    && validateSignature(context, packageInfo.packageName)
                    && validateMinimumVersion(context, packageInfo, minimumVersion)) {
                packageInfos.add(packageInfo);
            }
        }
        return packageInfos;
    }

    @NonNull
    private List<Pair<SupportedAppType, PackageInfo>> getInstalledPackagesByApp(
            @NonNull Context context, @NonNull SupportedAppType selectedApp) {
        List<Pair<SupportedAppType, PackageInfo>> installedPackages = new ArrayList<>();

        for (String installedPackage : getSupportedPackageNames().get(selectedApp)) {
            if (PackageManagers.isPackageAvailable(context, installedPackage)) {
                installedPackages.add(
                        Pair.create(selectedApp, PackageManagers.getPackageInfo(context, installedPackage)));
            }
        }
        return installedPackages;
    }

    /**
     * @return true if the device supports App Links
     */
    public boolean isAppLinkSupported() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
    }

    /**
     * Validates the app signature required or returns true if in debug.
     */
    public boolean validateSignature(@NonNull Context context, @NonNull String packageName) {
        if (isDebug(context)) {
            return true;
        }

        PackageInfo packageInfo;
        try {
            packageInfo = context.getPackageManager().getPackageInfo(packageName,
                    PackageManager.GET_SIGNATURES);
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }

        for (Signature signature : packageInfo.signatures) {
            String hashedSignature = Utility.sha1hash(signature.toByteArray());
            if (!validAppSignatureHashes.contains(hashedSignature)) {
                return false;
            }
        }
        return true;
    }

    /**
     * @return the Application Signature of the associated {@link Context} or null if it cannot be fetched.
     */
    @Nullable
    public String getAppSignature(@NonNull Context context) {
        final PackageInfo packageInfo;
        try {
            packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
                    PackageManager.GET_SIGNATURES);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }

        if (packageInfo == null || packageInfo.signatures.length == 0) {
            return null;
        }

        final MessageDigest messageDigest;
        try {
            messageDigest = getSha1MessageDigest();
        } catch (NoSuchAlgorithmException e) {
            return null;
        }

        messageDigest.update(packageInfo.signatures[0].toByteArray());
        return Base64.encodeToString(messageDigest.digest(), Base64.NO_WRAP);
    }

    /**
     * Gets an instance of {@link MessageDigest} with the SHA-1 Algorithm.
     *
     * @return the message digest.
     * @throws NoSuchAlgorithmException thrown by {@link MessageDigest#getInstance(String)}.
     */
    @NonNull
    MessageDigest getSha1MessageDigest() throws NoSuchAlgorithmException {
        return MessageDigest.getInstance(HASH_ALGORITHM_SHA1);
    }

    private boolean isDebug(@NonNull Context context) {
        String brand = Build.BRAND;
        int applicationFlags = context.getApplicationInfo().flags;
        // We are debugging on an emulator, don't validate package signature.
        return (brand.startsWith("Android") || brand.startsWith("generic")) &&
                (applicationFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
    }
}


/*
 * 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.samples;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.uber.sdk.android.core.auth.AccessTokenManager;
import com.uber.sdk.android.core.auth.AuthenticationError;
import com.uber.sdk.android.core.auth.LoginButton;
import com.uber.sdk.android.core.auth.LoginCallback;
import com.uber.sdk.android.core.auth.LoginManager;
import com.uber.sdk.android.rides.samples.BuildConfig;
import com.uber.sdk.android.rides.samples.R;
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.Session;
import com.uber.sdk.core.client.SessionConfiguration;
import com.uber.sdk.rides.client.UberRidesApi;
import com.uber.sdk.rides.client.error.ApiError;
import com.uber.sdk.rides.client.error.ErrorParser;
import com.uber.sdk.rides.client.model.UserProfile;
import com.uber.sdk.rides.client.services.RidesService;

import java.util.Arrays;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

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

/**
 * Activity that demonstrates how to use a {@link com.uber.sdk.android.core.auth.LoginManager}.
 */
public class LoginSampleActivity extends AppCompatActivity {
    //Please update CLIENT_ID and REDIRECT_URI below with your app's values.

    public static final String CLIENT_ID = BuildConfig.CLIENT_ID;
    public static final String REDIRECT_URI = BuildConfig.REDIRECT_URI;

    private static final String LOG_TAG = "LoginSampleActivity";

    private static final int LOGIN_BUTTON_CUSTOM_REQUEST_CODE = 1112;
    private static final int CUSTOM_BUTTON_REQUEST_CODE = 1113;

    private LoginButton blackButton;
    private LoginButton whiteButton;
    private Button customButton;
    private AccessTokenStorage accessTokenStorage;
    private LoginManager loginManager;
    private SessionConfiguration configuration;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample);

        configuration = new SessionConfiguration.Builder()
                .setClientId(CLIENT_ID)
                .setRedirectUri(REDIRECT_URI)
                .setScopes(Arrays.asList(Scope.PROFILE, Scope.RIDE_WIDGETS))
                .build();

        validateConfiguration(configuration);

        accessTokenStorage = new AccessTokenManager(this);

        //Create a button with a custom request code
        whiteButton = (LoginButton) findViewById(R.id.uber_button_white);
        whiteButton.setCallback(new SampleLoginCallback())
                    .setSessionConfiguration(configuration);

        //Create a button using a custom AccessTokenStorage
        //Custom Scopes are set using XML for this button as well in R.layout.activity_sample
        blackButton = (LoginButton) findViewById(R.id.uber_button_black);
        blackButton.setAccessTokenStorage(accessTokenStorage)
                    .setCallback(new SampleLoginCallback())
                    .setSessionConfiguration(configuration)
                    .setRequestCode(LOGIN_BUTTON_CUSTOM_REQUEST_CODE);

        //Use a custom button with an onClickListener to call the LoginManager directly
        loginManager = new LoginManager(accessTokenStorage,
                new SampleLoginCallback(),
                configuration,
                CUSTOM_BUTTON_REQUEST_CODE);

        customButton = (Button) findViewById(R.id.custom_uber_button);
        customButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loginManager.login(LoginSampleActivity.this);
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (loginManager.isAuthenticated()) {
            loadProfileInfo();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.i(LOG_TAG, String.format("onActivityResult requestCode:[%s] resultCode [%s]",
                requestCode, resultCode));

        //Allow each a chance to catch it.
        whiteButton.onActivityResult(requestCode, resultCode, data);

        blackButton.onActivityResult(requestCode, resultCode, data);

        loginManager.onActivityResult(this, requestCode, resultCode, data);
    }

    private class SampleLoginCallback implements LoginCallback {

        @Override
        public void onLoginCancel() {
            Toast.makeText(LoginSampleActivity.this, R.string.user_cancels_message, Toast.LENGTH_LONG).show();
        }

        @Override
        public void onLoginError(@NonNull AuthenticationError error) {
            Toast.makeText(LoginSampleActivity.this,
                    getString(R.string.login_error_message, error.name()), Toast.LENGTH_LONG)
                    .show();
        }

        @Override
        public void onLoginSuccess(@NonNull AccessToken accessToken) {
            loadProfileInfo();
        }

        @Override
        public void onAuthorizationCodeReceived(@NonNull String authorizationCode) {
            Toast.makeText(LoginSampleActivity.this, getString(R.string.authorization_code_message, authorizationCode),
                    Toast.LENGTH_LONG)
                    .show();
        }
    }

    private void loadProfileInfo() {
        Session session = loginManager.getSession();
        RidesService service = UberRidesApi.with(session).build().createService();

        service.getUserProfile()
                .enqueue(new Callback<UserProfile>() {
            @Override
            public void onResponse(Call<UserProfile> call, Response<UserProfile> response) {
                if (response.isSuccessful()) {
                    Toast.makeText(LoginSampleActivity.this, getString(R.string.greeting, response.body().getFirstName()), Toast.LENGTH_LONG).show();
                } else {
                    ApiError error = ErrorParser.parseError(response);
                    Toast.makeText(LoginSampleActivity.this, error.getClientErrors().get(0).getTitle(), Toast.LENGTH_LONG).show();
                }
            }

            @Override
            public void onFailure(Call<UserProfile> call, Throwable t) {

            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        accessTokenStorage = new AccessTokenManager(this);

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_clear) {
            accessTokenStorage.removeAccessToken();
            Toast.makeText(this, "AccessToken cleared", Toast.LENGTH_SHORT).show();
            return true;
        } else if (id == R.id.action_copy) {
            AccessToken accessToken = accessTokenStorage.getAccessToken();

            String message = accessToken == null ? "No AccessToken stored" : "AccessToken copied to clipboard";
            if (accessToken != null) {
                ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
                ClipData clip = ClipData.newPlainText("UberSampleAccessToken", accessToken.getToken());
                clipboard.setPrimaryClip(clip);
            }
            Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * Validates the local variables needed by the Uber SDK used in the sample project
     * @param configuration
     */
    private void validateConfiguration(SessionConfiguration configuration) {
        String nullError = "%s must not be null";
        String sampleError = "Please update your %s in the gradle.properties of the project before " +
                "using the Uber SDK Sample app. For a more secure storage location, " +
                "please investigate storing in your user home gradle.properties ";

        checkNotNull(configuration, String.format(nullError, "SessionConfiguration"));
        checkNotNull(configuration.getClientId(), String.format(nullError, "Client ID"));
        checkNotNull(configuration.getRedirectUri(), String.format(nullError, "Redirect URI"));
        checkState(!configuration.getClientId().equals("insert_your_client_id_here"),
                String.format(sampleError, "Client ID"));
        checkState(!configuration.getRedirectUri().equals("insert_your_redirect_uri_here"),
                String.format(sampleError, "Redirect 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.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.uber.sdk.android.core.BuildConfig;
import com.uber.sdk.android.core.Deeplink;
import com.uber.sdk.android.core.SupportedAppType;
import com.uber.sdk.android.core.utils.AppProtocol;
import com.uber.sdk.core.auth.Scope;

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

import static com.uber.sdk.android.core.SupportedAppType.UBER;
import static com.uber.sdk.android.core.SupportedAppType.UBER_EATS;
import static com.uber.sdk.android.core.UberSdk.UBER_SDK_LOG_TAG;
import static com.uber.sdk.android.core.utils.Preconditions.checkNotNull;
import static com.uber.sdk.android.core.utils.Preconditions.checkState;

/**
 * Provides deep link to login into the installed Uber app. For a simpler integration see
 * {@link LoginButton} or {@link LoginManager#login(Activity)}.
 */
public class SsoDeeplink implements Deeplink {

    public static final int DEFAULT_REQUEST_CODE = LoginManager.REQUEST_CODE_LOGIN_DEFAULT;

    @VisibleForTesting
    static final int MIN_UBER_RIDES_VERSION_SUPPORTED = 31302;
    @VisibleForTesting
    static final int MIN_UBER_EATS_VERSION_SUPPORTED = 2488;

    @VisibleForTesting
    static final int MIN_UBER_RIDES_VERSION_REDIRECT_FLOW_SUPPORTED = 35757;

    private static final String URI_QUERY_CLIENT_ID = "client_id";
    private static final String URI_QUERY_SCOPE = "scope";
    private static final String URI_QUERY_PLATFORM = "sdk";
    private static final String URI_QUERY_SDK_VERSION = "sdk_version";
    private static final String URI_QUERY_FLOW_TYPE = "flow_type";
    private static final String URI_QUERY_REDIRECT = "redirect_uri";
    private static final String URI_HOST = "connect";

    private final Activity activity;
    private final AppProtocol appProtocol;
    private final String clientId;
    private final String redirectUri;
    private final Collection<Scope> requestedScopes;
    private final Collection<String> requestedCustomScopes;
    private final Collection<SupportedAppType> productFlowPriority;
    private final int requestCode;

    private SsoDeeplink(
            @NonNull Activity activity,
            @NonNull AppProtocol appProtocol,
            @NonNull String clientId,
            @NonNull String redirectUri,
            @NonNull Collection<Scope> requestedScopes,
            @NonNull Collection<String> requestedCustomScopes,
            @NonNull Collection<SupportedAppType> productFlowPriority,
            int requestCode) {
        this.activity = activity;
        this.appProtocol = appProtocol;
        this.clientId = clientId;
        this.redirectUri = redirectUri;
        this.requestCode = requestCode;
        this.requestedScopes = requestedScopes;
        this.requestedCustomScopes = requestedCustomScopes;
        this.productFlowPriority = productFlowPriority;
    }

    /**
     * {@code flowVersion} defaults to {@link FlowVersion#DEFAULT}
     *
     * @see #execute(FlowVersion)
     */
    @Override
    public void execute() {
        execute(FlowVersion.DEFAULT);
    }

    /**
     * Starts the deeplink with the configuration options specified by this object. Use {@link Builder} to construct
     * an instance.
     *
     * @param flowVersion specifies which client implementation to use for handling the response to the SSO request
     * @throws IllegalStateException if compatible Uber app is not installed or the deeplink is incorrectly configured.
     *                               Use {@link #isSupported()} to check.
     */
    void execute(@NonNull FlowVersion flowVersion) {
        checkState(isSupported(flowVersion), "Single sign on is not supported on the device. " +
                "Please install or update to the latest version of Uber app.");

        Intent intent = new Intent(Intent.ACTION_VIEW);
        final Uri deepLinkUri = createSsoUri(flowVersion);
        intent.setData(deepLinkUri);

        List<PackageInfo> validatedPackages = new ArrayList<>();
        if (productFlowPriority.isEmpty()) {
            validatedPackages.addAll(
                    appProtocol.getInstalledPackages(activity, UBER, getSupportedAppVersion(UBER, flowVersion)));
        } else {
            for (SupportedAppType supportedAppType : productFlowPriority) {
                validatedPackages.addAll(appProtocol.getInstalledPackages(
                        activity, supportedAppType, getSupportedAppVersion(supportedAppType, flowVersion)));
            }
        }

        if (!validatedPackages.isEmpty()) {
            intent.setPackage(validatedPackages.get(0).packageName);
        }
        if (flowVersion == FlowVersion.DEFAULT) {
            activity.startActivityForResult(intent, requestCode);
        } else {
            activity.startActivity(intent);
        }
    }

    private Uri createSsoUri(@NonNull FlowVersion flowVersion) {
        String scopes = AuthUtils.scopeCollectionToString(requestedScopes);
        if (!requestedCustomScopes.isEmpty()) {
            scopes = AuthUtils.mergeScopeStrings(scopes,
                    AuthUtils.customScopeCollectionToString(requestedCustomScopes));
        }
        Uri.Builder uriBuilder = new Uri.Builder().scheme(Deeplink.DEEPLINK_SCHEME)
                .authority(URI_HOST)
                .appendQueryParameter(URI_QUERY_CLIENT_ID, clientId)
                .appendQueryParameter(URI_QUERY_SCOPE, scopes)
                .appendQueryParameter(URI_QUERY_PLATFORM, AppProtocol.PLATFORM)
                .appendQueryParameter(URI_QUERY_FLOW_TYPE, flowVersion.name())
                .appendQueryParameter(URI_QUERY_REDIRECT, redirectUri)
                .appendQueryParameter(URI_QUERY_SDK_VERSION, BuildConfig.VERSION_NAME);
        return uriBuilder.build();
    }

    /**
     * {@code flowVersion} defaults to {@link FlowVersion#DEFAULT}
     *
     * @see #isSupported(FlowVersion)
     */
    @Override
    public boolean isSupported() {
        return isSupported(FlowVersion.DEFAULT);
    }

    /**
     * Check if SSO deep linking is supported in this device.
     *
     * @param flowVersion specifies which client implementation to validate
     * @return true if package name, minimum version, and redirect uri requirements for flowVersion are met
     */
    boolean isSupported(@NonNull FlowVersion flowVersion) {
        if (flowVersion == FlowVersion.REDIRECT_TO_SDK) {
            Intent redirectIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(redirectUri));
            redirectIntent.setPackage(activity.getPackageName());
            List<ResolveInfo> resolveInfoList = activity.getPackageManager().queryIntentActivities(
                    redirectIntent, PackageManager.MATCH_DEFAULT_ONLY);
            if (resolveInfoList.isEmpty()) {
                return false;
            }
        }

        if (productFlowPriority.isEmpty()) {
            return appProtocol.isInstalled(activity, UBER, getSupportedAppVersion(UBER, flowVersion));
        } else {
            for (SupportedAppType supportedAppType : productFlowPriority) {
                if (appProtocol.isInstalled(
                        activity,
                        supportedAppType,
                        getSupportedAppVersion(supportedAppType, flowVersion))) {
                    return true;
                }
            }
            return false;
        }
    }

    private static int getSupportedAppVersion(SupportedAppType supportedAppType, FlowVersion flowVersion) {
        if (UBER == supportedAppType) {
            return flowVersion == FlowVersion.REDIRECT_TO_SDK
                    ? MIN_UBER_RIDES_VERSION_REDIRECT_FLOW_SUPPORTED
                    : MIN_UBER_RIDES_VERSION_SUPPORTED;
        } else if (UBER_EATS == supportedAppType) {
            return MIN_UBER_EATS_VERSION_SUPPORTED;
        }
        return Integer.MAX_VALUE;
    }

    public static class Builder {

        private final Activity activity;
        private AppProtocol appProtocol;
        private String clientId;
        private String redirectUri;
        private Collection<Scope> requestedScopes;
        private Collection<String> requestedCustomScopes;
        private Collection<SupportedAppType> productFlowPriority;
        private int requestCode = DEFAULT_REQUEST_CODE;

        public Builder(@NonNull Activity activity) {
            this.activity = activity;
        }

        public Builder clientId(@NonNull String clientId) {
            this.clientId = clientId;
            return this;
        }

        public Builder scopes(@NonNull Scope... scopes) {
            this.requestedScopes = Arrays.asList(scopes);
            return this;
        }

        public Builder scopes(@NonNull Collection<Scope> scopes) {
            this.requestedScopes = scopes;
            return this;
        }

        public Builder customScopes(@NonNull Collection<String> customScopes) {
            this.requestedCustomScopes = customScopes;
            return this;
        }

        public Builder productFlowPriority(@NonNull Collection<SupportedAppType> productFlowPriority) {
            this.productFlowPriority = productFlowPriority;
            return this;
        }

        public Builder activityRequestCode(int requestCode) {
            this.requestCode = requestCode;
            return this;
        }

        @VisibleForTesting
        Builder appProtocol(@NonNull AppProtocol appProtocol) {
            this.appProtocol = appProtocol;
            return this;
        }

        Builder redirectUri(@NonNull String redirectUri) {
            this.redirectUri = redirectUri;
            return this;
        }

        public SsoDeeplink build() {
            checkNotNull(clientId, "Client Id must be set");

            boolean hasScopes = (requestedScopes != null && !requestedScopes.isEmpty())
                    || (requestedCustomScopes != null && !requestedCustomScopes.isEmpty());
            checkState(hasScopes, "Scopes must be set.");

            if (requestedCustomScopes == null) {
                requestedCustomScopes = new ArrayList<>();
            }

            if (requestCode == DEFAULT_REQUEST_CODE) {
                Log.i(UBER_SDK_LOG_TAG, "Request code is not set, using default request code");
            }

            if (appProtocol == null) {
                appProtocol = new AppProtocol();
            }

            if (productFlowPriority == null) {
                productFlowPriority = new ArrayList<>();
            }

            if (redirectUri == null) {
                redirectUri = activity.getPackageName().concat(".uberauth://redirect");
            }

            return new SsoDeeplink(activity,
                    appProtocol,
                    clientId,
                    redirectUri,
                    requestedScopes,
                    requestedCustomScopes,
                    productFlowPriority,
                    requestCode);
        }
    }

    /**
     * Defines which client implementation of the SSO flow to use. This determines how the response from the SSO
     * request is returned to the calling application.
     */
    enum FlowVersion {
        DEFAULT, REDIRECT_TO_SDK
    }
}


/*
 * 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.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.customtabs.CustomTabsIntent;
import android.text.TextUtils;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.uber.sdk.android.core.BuildConfig;
import com.uber.sdk.android.core.R;
import com.uber.sdk.android.core.SupportedAppType;
import com.uber.sdk.android.core.install.SignupDeeplink;
import com.uber.sdk.android.core.utils.CustomTabsHelper;
import com.uber.sdk.core.client.SessionConfiguration;

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

/**
 * {@link android.app.Activity} that shows web view for Uber user authentication and authorization.
 */
public class LoginActivity extends Activity {
    @VisibleForTesting
    static final String USER_AGENT = String.format("core-android-v%s-login_manager", BuildConfig.VERSION_NAME);

    static final String EXTRA_PRODUCT_PRIORITY = "PRODUCT_PRIORITY";
    static final String EXTRA_RESPONSE_TYPE = "RESPONSE_TYPE";
    static final String EXTRA_SESSION_CONFIGURATION = "SESSION_CONFIGURATION";
    static final String EXTRA_FORCE_WEBVIEW = "FORCE_WEBVIEW";
    static final String EXTRA_SSO_ENABLED = "SSO_ENABLED";
    static final String EXTRA_REDIRECT_TO_PLAY_STORE_ENABLED = "REDIRECT_TO_PLAY_STORE_ENABLED";

    static final String ERROR = "error";

    private ArrayList<SupportedAppType> productPriority;
    private ResponseType responseType;
    private SessionConfiguration sessionConfiguration;
    private boolean authStarted;

    @VisibleForTesting
    WebView webView;

    @VisibleForTesting
    SsoDeeplinkFactory ssoDeeplinkFactory = new SsoDeeplinkFactory();

    @VisibleForTesting
    CustomTabsHelper customTabsHelper = new CustomTabsHelper();

    /**
     * Create an {@link Intent} to pass to this activity
     *
     * @param context the {@link Context} for the intent
     * @param sessionConfiguration to be used for gather clientId
     * @param responseType that is expected
     * @return an intent that can be passed to this activity
     */
    @NonNull
    public static Intent newIntent(
            @NonNull Context context,
            @NonNull SessionConfiguration sessionConfiguration,
            @NonNull ResponseType responseType) {

        return newIntent(context, sessionConfiguration, responseType, false);
    }

    /**
     * Create an {@link Intent} to pass to this activity
     *
     * @param context the {@link Context} for the intent
     * @param sessionConfiguration to be used for gather clientId
     * @param responseType that is expected
     * @param forceWebview Forced to use old webview instead of chrometabs
     * @return an intent that can be passed to this activity
     */
    @NonNull
    public static Intent newIntent(
            @NonNull Context context,
            @NonNull SessionConfiguration sessionConfiguration,
            @NonNull ResponseType responseType,
            boolean forceWebview) {

        return newIntent(context, new ArrayList<SupportedAppType>(), sessionConfiguration, responseType, forceWebview, false, false);
    }

    /**
     * Create an {@link Intent} to pass to this activity
     *
     * @param context the {@link Context} for the intent
     * @param productPriority dictates the order of which Uber applications should be used for SSO.
     * @param sessionConfiguration to be used for gather clientId
     * @param responseType that is expected
     * @param forceWebview Forced to use old webview instead of chrometabs
     * @param isSsoEnabled specifies whether to attempt login with SSO
     * @return an intent that can be passed to this activity
     */
    @NonNull
    static Intent newIntent(
            @NonNull Context context,
            @NonNull ArrayList<SupportedAppType> productPriority,
            @NonNull SessionConfiguration sessionConfiguration,
            @NonNull ResponseType responseType,
            boolean forceWebview,
            boolean isSsoEnabled,
            boolean isRedirectToPlayStoreEnabled) {

        final Intent data = new Intent(context, LoginActivity.class)
                .putExtra(EXTRA_PRODUCT_PRIORITY, productPriority)
                .putExtra(EXTRA_SESSION_CONFIGURATION, sessionConfiguration)
                .putExtra(EXTRA_RESPONSE_TYPE, responseType)
                .putExtra(EXTRA_FORCE_WEBVIEW, forceWebview)
                .putExtra(EXTRA_SSO_ENABLED, isSsoEnabled)
                .putExtra(EXTRA_REDIRECT_TO_PLAY_STORE_ENABLED, isRedirectToPlayStoreEnabled);

        return data;
    }

    /**
     * Used to handle Redirect URI response from customtab or browser
     *
     * @param context
     * @param responseUri
     * @return
     */
    public static Intent newResponseIntent(Context context, Uri responseUri) {
        Intent intent = new Intent(context, LoginActivity.class);
        intent.setData(responseUri);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
        return intent;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        init();
    }

    @Override
    protected void onResume() {
        super.onResume();

        if(webView == null) {
            if(!authStarted) {
                authStarted = true;
                return;
            }

            finish();
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        authStarted = false;
        setIntent(intent);
        init();
    }

    protected void init() {
        if(getIntent().getData() != null) {
            handleResponse(getIntent().getData());
        } else {
            loadUrl();
        }
    }

    protected void loadUrl() {
        Intent intent = getIntent();

        sessionConfiguration = (SessionConfiguration) intent.getSerializableExtra(EXTRA_SESSION_CONFIGURATION);
        responseType = (ResponseType) intent.getSerializableExtra(EXTRA_RESPONSE_TYPE);
        productPriority = (ArrayList<SupportedAppType>) intent.getSerializableExtra(EXTRA_PRODUCT_PRIORITY);

        if (!validateRequestParams()) {
            return;
        }

        String redirectUri = sessionConfiguration.getRedirectUri() != null ? sessionConfiguration
                .getRedirectUri() : getApplicationContext().getPackageName().concat(".uberauth://redirect");

        if (intent.getBooleanExtra(EXTRA_SSO_ENABLED, false)) {
            SsoDeeplink ssoDeeplink = ssoDeeplinkFactory.getSsoDeeplink(this, productPriority, sessionConfiguration);

            if (ssoDeeplink.isSupported(SsoDeeplink.FlowVersion.REDIRECT_TO_SDK)) {
                ssoDeeplink.execute(SsoDeeplink.FlowVersion.REDIRECT_TO_SDK);
            } else {
                onError(AuthenticationError.INVALID_REDIRECT_URI);
            }
            return;
        }

        boolean forceWebview = intent.getBooleanExtra(EXTRA_FORCE_WEBVIEW, false);
        boolean isRedirectToPlayStoreEnabled = intent.getBooleanExtra(EXTRA_REDIRECT_TO_PLAY_STORE_ENABLED, false);
        if (responseType == ResponseType.CODE) {
            loadWebPage(redirectUri, ResponseType.CODE, sessionConfiguration, forceWebview);
        } else if (responseType == ResponseType.TOKEN && !(AuthUtils.isPrivilegeScopeRequired(sessionConfiguration.getScopes())
                && isRedirectToPlayStoreEnabled)) {
            loadWebPage(redirectUri, ResponseType.TOKEN, sessionConfiguration, forceWebview);
        } else {
            redirectToInstallApp(this);
        }
    }

    protected void loadWebPage(String redirectUri, ResponseType responseType, SessionConfiguration sessionConfiguration, boolean forceWebview) {
        String url = AuthUtils.buildUrl(redirectUri, responseType, sessionConfiguration);
        if (forceWebview) {
            loadWebview(url, redirectUri);
        } else {
            loadChrometab(url);
        }
    }

    /**
     * Handler for both AccessToken and AuthorizationCode redirects.
     *
     * @param uri The redirect Uri.
     */
    private void handleResponse(@NonNull Uri uri) {
        if (AuthUtils.isAuthorizationCodePresent(uri)) {
            onCodeReceived(uri);
        } else {
            handleAccessTokenResponse(uri);
        }
    }

    /**
     * Process the callback for AccessToken.
     *
     * @param uri Redirect URI containing AccessToken values.
     */
    private void handleAccessTokenResponse(@NonNull Uri uri) {
        final String fragment = uri.getFragment();
        if (TextUtils.isEmpty(fragment)) {
            onError(AuthenticationError.INVALID_RESPONSE);
            return;
        }

        final Uri fragmentUri = new Uri.Builder().encodedQuery(fragment).build();

        // In case fragment contains error, we want to handle that too.
        final String error = fragmentUri.getQueryParameter(ERROR);
        if (!TextUtils.isEmpty(error)) {
            onError(AuthenticationError.fromString(error));
        } else {
            onTokenReceived(fragmentUri);
        }
    }

    protected void loadWebview(String url, String redirectUri) {
        setContentView(R.layout.ub__login_activity);
        webView = (WebView) findViewById(R.id.ub__login_webview);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.getSettings().setAppCacheEnabled(true);
        webView.getSettings().setDomStorageEnabled(true);
        webView.setWebViewClient(createOAuthClient(redirectUri));
        webView.loadUrl(url);
    }

    protected void loadChrometab(String url) {
        final CustomTabsIntent intent = new CustomTabsIntent.Builder().build();
        customTabsHelper.openCustomTab(this, intent, Uri.parse(url), new CustomTabsHelper
                .BrowserFallback());
    }

    protected OAuthWebViewClient createOAuthClient(String redirectUri) {
        if (responseType == ResponseType.TOKEN) {
            return new AccessTokenClient(redirectUri);
        } else {
            return new AuthorizationCodeClient(redirectUri);
        }
    }

    void onError(@NonNull AuthenticationError error) {
        Intent data = new Intent();
        data.putExtra(LoginManager.EXTRA_ERROR, error.toStandardString());
        setResult(RESULT_CANCELED, data);
        finish();
    }

    void onTokenReceived(@NonNull Uri uri) {
        try {
            Intent data = AuthUtils.parseTokenUriToIntent(uri);

            setResult(RESULT_OK, data);
            finish();
        } catch (LoginAuthenticationException loginException) {
            onError(loginException.getAuthenticationError());
            return;
        }
    }

    void onCodeReceived(Uri uri) {
        try {
            String code = AuthUtils.parseAuthorizationCode(uri);

            setResult(RESULT_OK, new Intent().putExtra(LoginManager.EXTRA_CODE_RECEIVED, code));
            finish();
        } catch (LoginAuthenticationException loginException) {
            onError(loginException.getAuthenticationError());
            return;
        }
    }

    private boolean validateRequestParams() {
        if (sessionConfiguration == null) {
            onError(AuthenticationError.INVALID_PARAMETERS);
            return false;
        }

        if ((sessionConfiguration.getScopes() == null || sessionConfiguration.getScopes().isEmpty())
                && (sessionConfiguration.getCustomScopes() == null  || sessionConfiguration.getCustomScopes().isEmpty())) {
            onError(AuthenticationError.INVALID_SCOPE);
            return false;
        }

        if (responseType == null) {
            onError(AuthenticationError.INVALID_RESPONSE_TYPE);
            return false;
        }

        return true;
    }

    private void redirectToInstallApp(@NonNull Activity activity) {
        new SignupDeeplink(activity, sessionConfiguration.getClientId(), USER_AGENT).execute();
    }

    /**
     * Custom {@link WebViewClient} for authorization.
     */
    @VisibleForTesting
    abstract class OAuthWebViewClient extends WebViewClient {

        @NonNull
        protected final String redirectUri;

        /**
         * Initialize the {@link WebView} client.
         *
         * @param redirectUri the redirect URI {@link String} that will contain the access token information
         */
        public OAuthWebViewClient(@NonNull String redirectUri) {
            this.redirectUri = redirectUri;
        }

        /**
         * add deprecated member "onReceivedError" to solve compatibility issue when API level < 23
         * @param view
         * @param errorCode
         * @param description
         * @param failingUrl
         */
        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                receivedError();
            }
        }

        @TargetApi(23)
        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            receivedError();
        }

        @Override
        public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
            receivedError();
        }

        private void receivedError(){
            onError(AuthenticationError.CONNECTIVITY_ISSUE);
        }
    }

    class AuthorizationCodeClient extends OAuthWebViewClient {

        public AuthorizationCodeClient(@NonNull String redirectUri) {
            super(redirectUri);
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            if (url.startsWith(redirectUri)) {
                onCodeReceived(Uri.parse(url));
            }
        }
    }

    class AccessTokenClient extends OAuthWebViewClient {

        public AccessTokenClient(@NonNull String redirectUri) {
            super(redirectUri);
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            // First thing to do is check for error, because the url argument here can be the
            // redirect URL or in the case of user passing in the wrong redirect URL, url will be
            // our Uber URL.
            final Uri uri = Uri.parse(url);
            final String errorParam = uri.getQueryParameter(ERROR);
            if (!TextUtils.isEmpty(errorParam)) {
                onError(AuthenticationError.fromString(errorParam));
                return true;
            }

            if (url.startsWith(redirectUri)) {
                handleAccessTokenResponse(uri);
                return true;
            }

            return super.shouldOverrideUrlLoading(view, url);
        }
    }
}