/*
* 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.Scope;
import com.uber.sdk.rides.client.AccessTokenSession;
import com.uber.sdk.rides.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 = "rides-android-v0.6.1-ride_request_widget";
@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 AccessTokenManager accessTokenManager;
@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);
accessTokenManager = 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(accessTokenManager, 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:
accessTokenManager.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) {
accessTokenManager.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 = accessTokenManager.getAccessToken();
if (accessToken != null) {
AccessTokenSession session = new AccessTokenSession(sessionConfiguration, accessTokenManager);
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();
}
}