/*
* Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.scvngr.levelup.deeplinkauth;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.scvngr.levelup.core.deeplinkauth.R;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collection;
/**
* <p>Deep Link Authorization allows third party apps to request an access token for a LevelUp user
* with {@link Intent}s. The intent launches the LevelUp app and the user will be presented with
* a dialog describing the request for permissions. Upon choosing to accept or reject the request,
* the user will return to the requesting app which will be granted an access token if the request
* is accepted.</p>
*
* <p>A request includes a list of permissions, such as the ability to create orders for the user
* or to access their transaction history. {@link com.scvngr.levelup.core.net.Permissions} has
* definitions for some of
* <a href="http://developer.thelevelup.com/getting-started/permissions-list/">the full list of
* available permissions</a>.</p>
*
* <p>To make an authorization request, first hook up the response handler. In your Activity's
* {@code onActivityResult()}, call {@link #parseActivityResult}:</p>
*
* <p><blockquote><pre>
* protected void onActivityResult(int requestCode, int resultCode, Intent data) {
* // This pulls out your result from the data Intent.
* PermissionsRequestResult result =
* LevelUpDeepLinkIntegrator.parseActivityResult(requestCode, resultCode, data);
*
* if (result != null) {
* if (result.isSuccessful()) {
* // Result will contain your access token.
* } else {
* // The user declined the request or there was as error.
* }
* } else {
* // You can handle your own startActivityForResult results here.
* }
* }
* </pre></blockquote></p>
*
* <p>When you are ready to request permissions, create an instance of {@link
* LevelUpDeepLinkIntegrator} from your Activity and call {@link #requestPermissions} with a list
* of desired permissions:</p>
*
* <pre>
* {@code
* if (LevelUpDeepLinkIntegrator.isLevelUpInstalled()) {
* LevelUpDeepLinkIntegrator integrator =
* new LevelUpDeepLinkIntegrator(yourActivity, yourAppId);
* integrator.requestPermissions(DeepLinkAuthUtil.Permissions.PERMISSION_CREATE_ORDERS);
* } else {
* // If you get here, LevelUp isn't installed or the installed version doesn't support
* // deep link auth. This might be a good place to suggest someone download LevelUp!
* }
* }
* </pre>
*/
@SuppressWarnings("unused")
public final class LevelUpDeepLinkIntegrator {
/**
* The requestCode used to start the permissions request activity.
*/
private static final int REQUEST_CODE = 9635; // LEVELUP on phone keypad, truncated to 16 bits.
/**
* @param context application context.
* @return true if a version of LevelUp that supports deep link authorization is installed and
* available. If false, you can handle the situation however you prefer, however we recommend
* that your app launches an intent that opens up LevelUp in Google Play so your users can
* download it.
*/
public static boolean isLevelUpAvailable(final Context context) {
return DeepLinkAuthUtil.isLevelUpAvailable(context);
}
/**
* Call this from {@link android.app.Activity#onActivityResult}.
*
* @param requestCode the requestCode from onActivityResult
* @param resultCode the resultCode from onActivityResult
* @param data the data from onActivityResult
* @return the interpreted result. If the request code doesn't match, this returns null.
*/
public static PermissionsRequestResult parseActivityResult(final int requestCode,
final int resultCode, final Intent data) {
if (requestCode != REQUEST_CODE) {
return null;
}
String accessToken = null;
Uri requestUri = null;
if (data != null) {
switch (resultCode) {
case Activity.RESULT_OK:
// user accepted request
accessToken = data.getStringExtra(DeepLinkAuthUtil.EXTRA_STRING_ACCESS_TOKEN);
requestUri = data.getData();
break;
case Activity.RESULT_CANCELED:
// errorMessage = data.getStringExtra(name)
// user rejected request
break;
default:
// do nothing
}
}
return new PermissionsRequestResult(accessToken, requestUri, resultCode);
}
private final WeakReference<Activity> mActivityWeakReference;
private final int mAppId;
private volatile OnDismissListener mDismissListener;
/**
* The last launched intent; for testing purposes.
*/
private Intent mLastIntent;
private final OnClickListener mListener = new OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
onPositiveButton(dialog);
break;
case DialogInterface.BUTTON_NEGATIVE:
onNegativeButton(dialog);
break;
default:
throw new UnsupportedOperationException("Unhandled button");
}
}
};
/**
* The install dialog message.
*/
private CharSequence mMessage;
/**
* Allow the acceptable LevelUp signatures to be overridden, for testing purposes.
*/
private String[] mSignatureOverride;
/**
* The install dialog title.
*/
private CharSequence mTitle;
/**
* This can be instantiated in {@link android.app.Activity#onCreate}. This class keeps a weak
* reference to your activity, so you don't need to worry about it being leaked.
*
* @param activity your activity.
* @param appId your app's web service ID.
*/
public LevelUpDeepLinkIntegrator(final Activity activity, final int appId) {
mActivityWeakReference = new WeakReference<Activity>(activity);
mAppId = appId;
}
/**
* Send a request to the LevelUp app to prompt the user to grant your app the requested
* permissions. You must make sure to call {@link #parseActivityResult} from {@link
* android.app.Activity#onActivityResult} in order to get the response from LevelUp.
*
* @param permissionKeys the set of permissions that you wish to request. See {@link
* Permissions}.
* @return the dialog box that was shown to the user with a link to download LevelUp if one was
* shown, otherwise null.
*/
public final AlertDialog requestPermissions(final Collection<String> permissionKeys) {
AlertDialog installDialog = null;
final Activity activity = mActivityWeakReference.get();
if (activity != null) {
try {
final Intent startIntent;
if (mSignatureOverride != null) {
startIntent = DeepLinkAuthUtil
.getRequestPermissionsIntent(activity, mAppId, permissionKeys,
mSignatureOverride);
} else {
startIntent = DeepLinkAuthUtil
.getRequestPermissionsIntent(activity, mAppId, permissionKeys);
}
startActivityForResult(activity, startIntent, REQUEST_CODE);
} catch (final DeepLinkAuthUtil.LevelUpNotInstalledException e) {
installDialog = onLevelUpNotInstalled(activity);
}
}
return installDialog;
}
/**
* Send a request to the LevelUp app to prompt the user to grant your app the requested
* permissions. You must make sure to call {@link #parseActivityResult} from {@link
* android.app.Activity#onActivityResult} in order to get the response from LevelUp.
*
* @param permissionKeys the set of permissions that you wish to request. See {@link
* com.scvngr.levelup.core.net.Permissions}.
* @return the dialog box that was shown to the user with a link to download LevelUp if one was
* shown, otherwise null.
*/
public final AlertDialog requestPermissions(final String... permissionKeys) {
return requestPermissions(Arrays.asList(permissionKeys));
}
/**
* Sets the message that's displayed when the LevelUp app isn't installed on the device.
*
* @param message the body text.
* @see com.scvngr.levelup.core.deeplinkauth.R.string#levelup_deep_link_auth_install_message
*/
public final void setInstallDialogMessage(final CharSequence message) {
mMessage = message;
}
/**
* Sets the title of the dialog box that's displayed when the LevelUp app isn't installed on the device.
*
* @param title the dialog box's title.
* @see com.scvngr.levelup.core.deeplinkauth.R.string#levelup_deep_link_auth_install_title
*/
public final void setInstallDialogTitle(final CharSequence title) {
mTitle = title;
}
/**
* Constructs an {@link AlertDialog} which prompts the user to download LevelUp. Alternative
* text can be set using {@link #setInstallDialogMessage} and {@link #setInstallDialogTitle} or
* by overlaying the associated strings resources.
*
* @param activity the activity that is responsible for the dialog (your activity).
* @return a created, but not shown dialog with two buttons.
*/
protected AlertDialog getInstallDialog(final Activity activity) {
final AlertDialog.Builder ab = new Builder(activity);
ab.setCancelable(true);
if (mTitle != null) {
ab.setTitle(mTitle);
} else {
ab.setTitle(R.string.levelup_deep_link_auth_install_title);
}
if (mMessage != null) {
ab.setMessage(mMessage);
} else {
ab.setMessage(R.string.levelup_deep_link_auth_install_message);
}
ab.setIcon(R.drawable.levelup_ic_levelup);
ab.setPositiveButton(R.string.levelup_deep_link_auth_install_positive_button, mListener);
ab.setNegativeButton(R.string.levelup_deep_link_auth_install_negative_button, mListener);
return ab.create();
}
/**
* Retrieves the Google Play link for the given package name.
*
* @param context application context.
* @param packageName the package to display.
* @return a URL that can be resolved on an Android device to show the given Google Play details
* page.
*/
protected Uri getPlayLink(final Context context, final String packageName) {
return Uri.parse("market://details").buildUpon().appendQueryParameter("id", packageName)
.build();
}
/**
* Called when LevelUp is not installed.
*
* @param activity
* @return the {@link AlertDialog} that was shown to the user.
*/
protected AlertDialog onLevelUpNotInstalled(final Activity activity) {
final AlertDialog installDialog = getInstallDialog(activity);
installDialog.setOnDismissListener(mDismissListener);
installDialog.show();
return installDialog;
}
/**
* Called when the negative action button (Cancel) is pressed on the install dialog.
*
* @param dialog
*/
protected void onNegativeButton(final DialogInterface dialog) {
dialog.cancel();
}
/**
* Called when the positive action button (install LevelUp) is pressed in the install dialog.
*
* @param dialog
*/
protected void onPositiveButton(final DialogInterface dialog) {
Activity activity = mActivityWeakReference.get();
if (activity == null) {
final AlertDialog d2 = (AlertDialog) dialog;
activity = d2.getOwnerActivity();
}
if (activity != null) {
final Uri playLink =
getPlayLink(activity,
activity.getString(R.string.levelup_deep_link_auth_install_package));
startActivity(activity, new Intent(Intent.ACTION_VIEW, playLink));
} else {
Log.d("LevelUpDeepLinkIntegrator", "Lost reference to Activity");
}
dialog.dismiss();
}
/**
* This is intended for testing only.
*
* @return the last intent that this class started with either startActivity or
* startActivityForResult.
*/
/* package */Intent getLastStartedIntent() {
return mLastIntent;
}
/**
* Allows overriding of the allowable signatures for testing.
*
* @param signatureOverride the allowed signatures.
*/
/* package */void setLevelUpAppSignatures(final String[] signatureOverride) {
mSignatureOverride = signatureOverride;
}
/**
* Allows adding an on dismiss listener to the install prompt dialog, for testing.
*
* @param dismissListener
*/
/* package */void setOnDismissListener(final OnDismissListener dismissListener) {
mDismissListener = dismissListener;
}
/**
* Broken out to capture the intent for testing.
*
* @param activity
* @param intent
* @see Activity#startActivity(Intent)
*/
private void startActivity(final Activity activity, final Intent intent) {
mLastIntent = intent;
activity.startActivity(intent);
}
/**
* Broken out to capture the intent for testing.
*
* @param activity
* @param intent
* @param requestCode
* @see Activity#startActivityForResult(Intent, int)
*/
private void startActivityForResult(final Activity activity, final Intent intent,
final int requestCode) {
mLastIntent = intent;
activity.startActivityForResult(intent, requestCode);
}
/**
* The result of a call to {@link #requestPermissions}.
*/
public static final class PermissionsRequestResult {
private final String mAccessToken;
private final Uri mRequestUri;
private final int mResultCode;
/**
* Private constructor, as only the outer class should be instantiating this.
*/
private PermissionsRequestResult(final String accessToken, final Uri requestUri,
final int resultCode) {
mRequestUri = requestUri;
mAccessToken = accessToken;
mResultCode = resultCode;
}
/**
* @return the access token, if the permissions request was successful.
*/
public String getAccessToken() {
return mAccessToken;
}
/**
* @return the original URI that was sent in the request. This is an encoding of your
* request.
*/
public Uri getRequestUri() {
return mRequestUri;
}
/**
* @return true if the permissions request was accepted by the user.
*/
public boolean isSuccessful() {
return mResultCode == Activity.RESULT_OK;
}
}
}