// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsSessionToken;
import android.text.TextUtils;
import android.util.Pair;
import android.view.View;
import android.widget.RemoteViews;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.util.FeatureUtilities;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.chrome.browser.widget.TintedDrawable;
import java.util.ArrayList;
import java.util.List;
/**
* A model class that parses intent from third-party apps and provides results to
* {@link CustomTabActivity}.
*/
public class CustomTabIntentDataProvider {
private static final String TAG = "CustomTabIntentData";
/**
* Extra used to keep the caller alive. Its value is an Intent.
*/
public static final String EXTRA_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE";
/**
* Herb: Extra that indicates whether or not the Custom Tab is being launched by an Intent fired
* by Chrome itself.
*/
public static final String EXTRA_IS_OPENED_BY_CHROME =
"org.chromium.chrome.browser.customtabs.IS_OPENED_BY_CHROME";
/** Indicates that the Custom Tab should style itself as a media viewer. */
public static final String EXTRA_IS_MEDIA_VIEWER =
"org.chromium.chrome.browser.customtabs.IS_MEDIA_VIEWER";
//TODO(yusufo): Move this to CustomTabsIntent.
/** Signals custom tabs to favor sending initial urls to external handler apps if possible. */
public static final String EXTRA_SEND_TO_EXTERNAL_DEFAULT_HANDLER =
"android.support.customtabs.extra.SEND_TO_EXTERNAL_HANDLER";
private static final int MAX_CUSTOM_MENU_ITEMS = 5;
private static final String ANIMATION_BUNDLE_PREFIX =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? "android:activity." : "android:";
private static final String BUNDLE_PACKAGE_NAME = ANIMATION_BUNDLE_PREFIX + "packageName";
private static final String BUNDLE_ENTER_ANIMATION_RESOURCE =
ANIMATION_BUNDLE_PREFIX + "animEnterRes";
private static final String BUNDLE_EXIT_ANIMATION_RESOURCE =
ANIMATION_BUNDLE_PREFIX + "animExitRes";
private final CustomTabsSessionToken mSession;
private final Intent mKeepAliveServiceIntent;
private final int mTitleVisibilityState;
private final boolean mIsMediaViewer;
private int mToolbarColor;
private int mBottomBarColor;
private boolean mEnableUrlBarHiding;
private List<CustomButtonParams> mCustomButtonParams;
private Drawable mCloseButtonIcon;
private List<Pair<String, PendingIntent>> mMenuEntries = new ArrayList<>();
private Bundle mAnimationBundle;
private boolean mShowShareItem;
private CustomButtonParams mToolbarButton;
private List<CustomButtonParams> mBottombarButtons = new ArrayList<>(2);
private RemoteViews mRemoteViews;
private int[] mClickableViewIds;
private PendingIntent mRemoteViewsPendingIntent;
// OnFinished listener for PendingIntents. Used for testing only.
private PendingIntent.OnFinished mOnFinished;
/** Herb: Whether this CustomTabActivity was explicitly started by another Chrome Activity. */
private boolean mIsOpenedByChrome;
/**
* Constructs a {@link CustomTabIntentDataProvider}.
*/
public CustomTabIntentDataProvider(Intent intent, Context context) {
if (intent == null) assert false;
mSession = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
parseHerbExtras(intent, context);
retrieveCustomButtons(intent, context);
retrieveToolbarColor(intent, context);
retrieveBottomBarColor(intent);
mEnableUrlBarHiding = IntentUtils.safeGetBooleanExtra(
intent, CustomTabsIntent.EXTRA_ENABLE_URLBAR_HIDING, true);
mKeepAliveServiceIntent = IntentUtils.safeGetParcelableExtra(intent, EXTRA_KEEP_ALIVE);
Bitmap bitmap = IntentUtils.safeGetParcelableExtra(intent,
CustomTabsIntent.EXTRA_CLOSE_BUTTON_ICON);
if (bitmap != null && !checkCloseButtonSize(context, bitmap)) {
bitmap.recycle();
bitmap = null;
}
if (bitmap == null) {
mCloseButtonIcon = TintedDrawable.constructTintedDrawable(context.getResources(),
R.drawable.btn_close);
} else {
mCloseButtonIcon = new BitmapDrawable(context.getResources(), bitmap);
}
List<Bundle> menuItems =
IntentUtils.getParcelableArrayListExtra(intent, CustomTabsIntent.EXTRA_MENU_ITEMS);
if (menuItems != null) {
for (int i = 0; i < Math.min(MAX_CUSTOM_MENU_ITEMS, menuItems.size()); i++) {
Bundle bundle = menuItems.get(i);
String title =
IntentUtils.safeGetString(bundle, CustomTabsIntent.KEY_MENU_ITEM_TITLE);
PendingIntent pendingIntent =
IntentUtils.safeGetParcelable(bundle, CustomTabsIntent.KEY_PENDING_INTENT);
if (TextUtils.isEmpty(title) || pendingIntent == null) continue;
mMenuEntries.add(new Pair<String, PendingIntent>(title, pendingIntent));
}
}
mAnimationBundle = IntentUtils.safeGetBundleExtra(
intent, CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE);
mTitleVisibilityState = IntentUtils.safeGetIntExtra(intent,
CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.NO_TITLE);
mShowShareItem = IntentUtils.safeGetBooleanExtra(intent,
CustomTabsIntent.EXTRA_DEFAULT_SHARE_MENU_ITEM, false);
mRemoteViews = IntentUtils.safeGetParcelableExtra(intent,
CustomTabsIntent.EXTRA_REMOTEVIEWS);
mClickableViewIds = IntentUtils.safeGetIntArrayExtra(intent,
CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS);
mRemoteViewsPendingIntent = IntentUtils.safeGetParcelableExtra(intent,
CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT);
mIsMediaViewer = IntentHandler.isIntentChromeOrFirstParty(intent, context)
&& IntentUtils.safeGetBooleanExtra(intent, EXTRA_IS_MEDIA_VIEWER, false);
}
/**
* Gets custom buttons from the intent and updates {@link #mCustomButtonParams},
* {@link #mBottombarButtons} and {@link #mToolbarButton}.
*/
private void retrieveCustomButtons(Intent intent, Context context) {
mCustomButtonParams = CustomButtonParams.fromIntent(context, intent);
if (mCustomButtonParams != null) {
for (CustomButtonParams params : mCustomButtonParams) {
if (params.showOnToolbar()) {
mToolbarButton = params;
} else {
mBottombarButtons.add(params);
}
}
}
}
/**
* Processes the color passed from the client app and updates {@link #mToolbarColor}.
*/
private void retrieveToolbarColor(Intent intent, Context context) {
int defaultColor = ApiCompatibilityUtils.getColor(context.getResources(),
R.color.default_primary_color);
int color = IntentUtils.safeGetIntExtra(intent, CustomTabsIntent.EXTRA_TOOLBAR_COLOR,
defaultColor);
mToolbarColor = removeTransparencyFromColor(color, defaultColor);
}
/**
* Must be called after calling {@link #retrieveToolbarColor(Intent, Context)}.
*/
private void retrieveBottomBarColor(Intent intent) {
int defaultColor = mToolbarColor;
int color = IntentUtils.safeGetIntExtra(intent,
CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR, defaultColor);
mBottomBarColor = removeTransparencyFromColor(color, defaultColor);
}
/**
* Removes the alpha channel of the given color and returns the processed value. If the result
* is blank, returns the fallback value.
*/
private int removeTransparencyFromColor(int color, int fallbackColor) {
color |= 0xFF000000;
if (color == Color.TRANSPARENT) color = fallbackColor;
return color;
}
/**
* @return The session specified in the intent, or null.
*/
public CustomTabsSessionToken getSession() {
return mSession;
}
/**
* @return The keep alive service intent specified in the intent, or null.
*/
public Intent getKeepAliveServiceIntent() {
return mKeepAliveServiceIntent;
}
/**
* @return Whether url bar hiding should be enabled in the custom tab. Default is false.
*/
public boolean shouldEnableUrlBarHiding() {
return mEnableUrlBarHiding;
}
/**
* @return The toolbar color specified in the intent. Will return the color of
* default_primary_color, if not set in the intent.
*/
public int getToolbarColor() {
return mToolbarColor;
}
/**
* @return The drawable of the icon of close button shown in the custom tab toolbar. If the
* client app provides an icon in valid size, use this icon; else return the default
* drawable.
*/
public Drawable getCloseButtonDrawable() {
return mCloseButtonIcon;
}
/**
* @return The title visibility state for the toolbar.
* Default is {@link CustomTabsIntent#NO_TITLE}.
*/
public int getTitleVisibilityState() {
return mTitleVisibilityState;
}
/**
* @return Whether the default share item should be shown in the menu.
*/
public boolean shouldShowShareMenuItem() {
return mShowShareItem;
}
/**
* @return The params for the custom button that shows on the toolbar. If there is no applicable
* buttons, returns null.
*/
public CustomButtonParams getCustomButtonOnToolbar() {
return mToolbarButton;
}
/**
* @return The list of params representing the buttons on the bottombar.
*/
public List<CustomButtonParams> getCustomButtonsOnBottombar() {
return mBottombarButtons;
}
/**
* @return Whether the bottom bar should be shown.
*/
public boolean shouldShowBottomBar() {
return !mBottombarButtons.isEmpty() || mRemoteViews != null;
}
/**
* @return The color of the bottom bar, or {@link #getToolbarColor()} if not specified.
*/
public int getBottomBarColor() {
return mBottomBarColor;
}
/**
* @return The {@link RemoteViews} to show on the bottom bar, or null if the extra is not
* specified.
*/
public RemoteViews getBottomBarRemoteViews() {
return mRemoteViews;
}
/**
* @return A array of {@link View} ids, of which the onClick event is handled by the custom tab.
*/
public int[] getClickableViewIDs() {
return mClickableViewIds.clone();
}
/**
* @return The {@link PendingIntent} that is sent when the user clicks on the remote view.
*/
public PendingIntent getRemoteViewsPendingIntent() {
return mRemoteViewsPendingIntent;
}
/**
* Gets params for all custom buttons, which is the combination of
* {@link #getCustomButtonsOnBottombar()} and {@link #getCustomButtonOnToolbar()}.
*/
public List<CustomButtonParams> getAllCustomButtons() {
return mCustomButtonParams;
}
/**
* @return The {@link CustomButtonParams} having the given id. Returns null if no such params
* can be found.
*/
public CustomButtonParams getButtonParamsForId(int id) {
for (CustomButtonParams params : mCustomButtonParams) {
// A custom button params will always carry an ID. If the client calls updateVisuals()
// without an id, we will assign the toolbar action button id to it.
if (id == params.getId()) return params;
}
return null;
}
/**
* @return Titles of menu items that were passed from client app via intent.
*/
public List<String> getMenuTitles() {
ArrayList<String> list = new ArrayList<>();
for (Pair<String, PendingIntent> pair : mMenuEntries) {
list.add(pair.first);
}
return list;
}
/**
* Triggers the client-defined action when the user clicks a custom menu item.
* @param menuIndex The index that the menu item is shown in the result of
* {@link #getMenuTitles()}
*/
public void clickMenuItemWithUrl(ChromeActivity activity, int menuIndex, String url) {
Intent addedIntent = new Intent();
addedIntent.setData(Uri.parse(url));
try {
// Media viewers pass in PendingIntents that contain CHOOSER Intents. Setting the data
// in these cases prevents the Intent from firing correctly.
PendingIntent pendingIntent = mMenuEntries.get(menuIndex).second;
pendingIntent.send(
activity, 0, isMediaViewer() ? null : addedIntent, mOnFinished, null);
} catch (CanceledException e) {
Log.e(TAG, "Custom tab in Chrome failed to send pending intent.");
}
}
/**
* @return Whether chrome should animate when it finishes. We show animations only if the client
* app has supplied the correct animation resources via intent extra.
*/
public boolean shouldAnimateOnFinish() {
return mAnimationBundle != null && getClientPackageName() != null;
}
/**
* @return The package name of the client app. This is used for a workaround in order to
* retrieve the client's animation resources.
*/
public String getClientPackageName() {
if (mAnimationBundle == null) return null;
return mAnimationBundle.getString(BUNDLE_PACKAGE_NAME);
}
/**
* @return The resource id for enter animation, which is used in
* {@link Activity#overridePendingTransition(int, int)}.
*/
public int getAnimationEnterRes() {
return shouldAnimateOnFinish() ? mAnimationBundle.getInt(BUNDLE_ENTER_ANIMATION_RESOURCE)
: 0;
}
/**
* @return The resource id for exit animation, which is used in
* {@link Activity#overridePendingTransition(int, int)}.
*/
public int getAnimationExitRes() {
return shouldAnimateOnFinish() ? mAnimationBundle.getInt(BUNDLE_EXIT_ANIMATION_RESOURCE)
: 0;
}
/**
* Sends the pending intent for the custom button on toolbar with the given url as data.
* @param context The context to use for sending the {@link PendingIntent}.
* @param url The url to attach as additional data to the {@link PendingIntent}.
*/
public void sendButtonPendingIntentWithUrl(Context context, String url) {
Intent addedIntent = new Intent();
addedIntent.setData(Uri.parse(url));
try {
getCustomButtonOnToolbar().getPendingIntent().send(context, 0, addedIntent, mOnFinished,
null);
} catch (CanceledException e) {
Log.e(TAG, "CanceledException while sending pending intent in custom tab");
}
}
private boolean checkCloseButtonSize(Context context, Bitmap bitmap) {
int size = context.getResources().getDimensionPixelSize(R.dimen.toolbar_icon_height);
if (bitmap.getHeight() == size && bitmap.getWidth() == size) return true;
return false;
}
/**
* Set the callback object for {@link PendingIntent}s that are sent in this class. For testing
* purpose only.
*/
@VisibleForTesting
void setPendingIntentOnFinishedForTesting(PendingIntent.OnFinished onFinished) {
mOnFinished = onFinished;
}
/**
* @return See {@link #EXTRA_IS_OPENED_BY_CHROME}.
*/
boolean isOpenedByChrome() {
return mIsOpenedByChrome;
}
/**
* @return See {@link #EXTRA_IS_MEDIA_VIEWER}.
*/
boolean isMediaViewer() {
return mIsMediaViewer;
}
/**
* Parses out extras specifically added for Herb.
*
* @param intent Intent fired to open the CustomTabActivity.
* @param context Context for the package.
*/
private void parseHerbExtras(Intent intent, Context context) {
String herbFlavor = FeatureUtilities.getHerbFlavor();
if (TextUtils.isEmpty(herbFlavor)
|| TextUtils.equals(ChromeSwitches.HERB_FLAVOR_DISABLED, herbFlavor)) {
return;
}
mIsOpenedByChrome = IntentUtils.safeGetBooleanExtra(
intent, EXTRA_IS_OPENED_BY_CHROME, false);
}
}