// Copyright 2015 The Chromium Authors
// 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.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsIntent;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.intents.CustomButtonParams;
import org.chromium.chrome.browser.theme.ThemeUtils;
import org.chromium.components.browser_ui.widget.TintedDrawable;
import org.chromium.ui.util.ColorUtils;
import org.chromium.ui.widget.Toast;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Container for all parameters related to creating a customizable button. */
public class CustomButtonParamsImpl implements CustomButtonParams {
private static final String TAG = "CustomTabs";
private final PendingIntent mPendingIntent;
private int mId;
private Bitmap mIcon;
private String mDescription;
private boolean mShouldTint;
private boolean mIsOnToolbar;
private @ButtonType int mType;
@VisibleForTesting
static final String SHOW_ON_TOOLBAR = "android.support.customtabs.customaction.SHOW_ON_TOOLBAR";
private CustomButtonParamsImpl(
int id,
Bitmap icon,
String description,
@Nullable PendingIntent pendingIntent,
boolean tinted,
boolean onToolbar,
@ButtonType int type) {
mId = id;
mIcon = icon;
mDescription = description;
mPendingIntent = pendingIntent;
mShouldTint = tinted;
mIsOnToolbar = onToolbar;
mType = type;
}
/** Replaces the current icon and description with new ones. */
@Override
public void update(@NonNull Bitmap icon, @NonNull String description) {
mIcon = icon;
mDescription = description;
}
/**
* @return Whether this button should be shown on the toolbar.
*/
@Override
public boolean showOnToolbar() {
return mIsOnToolbar;
}
/**
* @return The id associated with this button. The custom button on the toolbar always uses
* {@link CustomTabsIntent#TOOLBAR_ACTION_BUTTON_ID} as id.
*/
@Override
public int getId() {
return mId;
}
/**
* @return The drawable for the customized button.
*/
@Override
public Drawable getIcon(Context context) {
if (mShouldTint) {
return new TintedDrawable(context, mIcon);
} else {
return new BitmapDrawable(context.getResources(), mIcon);
}
}
/**
* @return The content description for the customized button.
*/
@Override
public String getDescription() {
return mDescription;
}
/**
* @return The {@link PendingIntent} that will be sent when user clicks the customized button.
*/
@Override
public PendingIntent getPendingIntent() {
return mPendingIntent;
}
@Override
public @ButtonType int getType() {
return mType;
}
/**
* Builds an {@link ImageButton} from the data in this params. Generated buttons should be
* placed on the bottom bar. The button's tag will be its id.
*
* @param parent The parent that the inflated {@link ImageButton}.
* @param listener {@link OnClickListener} that should be used with the button.
* @return Parsed list of {@link CustomButtonParams}, which is empty if the input is invalid.
*/
@Override
public ImageButton buildBottomBarButton(
Context context, ViewGroup parent, OnClickListener listener) {
assert !mIsOnToolbar;
ImageButton button =
(ImageButton)
LayoutInflater.from(context)
.inflate(R.layout.custom_tabs_bottombar_item, parent, false);
button.setId(mId);
button.setImageBitmap(mIcon);
button.setContentDescription(mDescription);
if (mPendingIntent == null) {
button.setEnabled(false);
} else {
button.setOnClickListener(listener);
}
button.setOnLongClickListener(
new OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
final int screenWidth = view.getResources().getDisplayMetrics().widthPixels;
final int screenHeight =
view.getResources().getDisplayMetrics().heightPixels;
final int[] screenPos = new int[2];
view.getLocationOnScreen(screenPos);
final int width = view.getWidth();
Toast toast =
Toast.makeText(
view.getContext(),
view.getContentDescription(),
Toast.LENGTH_SHORT);
toast.setGravity(
Gravity.BOTTOM | Gravity.END,
screenWidth - screenPos[0] - width / 2,
screenHeight - screenPos[1]);
toast.show();
return true;
}
});
return button;
}
/**
* Parses a list of {@link CustomButtonParams} from the intent sent by clients.
*
* @param context The context.
* @param intent The intent sent by the client.
* @return A list of parsed {@link CustomButtonParams}. Return an empty list if input is
* invalid.
*/
public static List<CustomButtonParams> fromIntent(Context context, Intent intent) {
List<CustomButtonParams> paramsList = new ArrayList<>(1);
if (intent == null) return paramsList;
Bundle singleBundle =
IntentUtils.safeGetBundleExtra(intent, CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE);
ArrayList<Bundle> bundleList =
IntentUtils.getParcelableArrayListExtra(
intent, CustomTabsIntent.EXTRA_TOOLBAR_ITEMS);
boolean tinted =
IntentUtils.safeGetBooleanExtra(
intent, CustomTabsIntent.EXTRA_TINT_ACTION_BUTTON, false);
if (singleBundle != null) {
CustomButtonParams singleParams = fromBundle(context, singleBundle, tinted, false);
if (singleParams != null) paramsList.add(singleParams);
}
return addToParamListfromBundleList(paramsList, context, bundleList, tinted);
}
/**
* Parses a list of {@link CustomButtonParams} from a bundle list.
*
* @param context The context
* @param bundleList The list of bundles, each expected to contain the data for a single {@link
* CustomButtonParams}.
* @param tinted A flag indicating whether the buttons should be tinted.
* @return A list of parsed {@link CustomButtonParams}. Return an empty list if input is
* invalid.
*/
public static List<CustomButtonParams> fromBundleList(
Context context, List<Bundle> bundleList, boolean tinted) {
return addToParamListfromBundleList(new ArrayList<>(1), context, bundleList, tinted);
}
/**
* Adds {@link CustomButtonParams} objects to an existing list from a bundle list.
*
* <p>This method iterates through a list of bundles, parsing each one into a {@link
* CustomButtonParams} object and adding it to the provided `paramsList`.
*
* @param paramsList The list to which parsed {@link CustomButtonParams} objects will be added.
* @param context The context.
* @param bundleList The list of bundles, each expected to contain the data for a single {@link
* CustomButtonParams}.
* @param tinted A flag indicating whether the buttons should be tinted.
* @return The original `paramsList` with additional parsed {@link CustomButtonParams} objects
* added. If the `bundleList` is null or empty, the `paramsList` is returned unchanged.
*/
private static List<CustomButtonParams> addToParamListfromBundleList(
List<CustomButtonParams> paramsList,
Context context,
List<Bundle> bundleList,
boolean tinted) {
if (bundleList != null) {
Set<Integer> ids = new HashSet<>();
for (Bundle bundle : bundleList) {
CustomButtonParams params = fromBundle(context, bundle, tinted, true);
if (params == null) {
continue;
} else if (ids.contains(params.getId())) {
Log.e(TAG, "Bottom bar items contain duplicate id: " + params.getId());
continue;
}
ids.add(params.getId());
paramsList.add(params);
}
}
return paramsList;
}
/**
* Parses params out of a bundle. Note if a custom button contains a bitmap that does not fit
* into the toolbar, it will be put to the bottom bar.
* @param fromList Whether the bundle is contained in a list or it is the single bundle that
* directly comes from the intent.
*/
private static CustomButtonParams fromBundle(
Context context, Bundle bundle, boolean tinted, boolean fromList) {
if (bundle == null) return null;
if (fromList && !bundle.containsKey(CustomTabsIntent.KEY_ID)) return null;
int id =
IntentUtils.safeGetInt(
bundle, CustomTabsIntent.KEY_ID, CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID);
Bitmap bitmap = parseBitmapFromBundle(bundle);
if (bitmap == null) {
Log.e(TAG, "Invalid action button: bitmap not present in bundle!");
return null;
}
String description = parseDescriptionFromBundle(bundle);
if (TextUtils.isEmpty(description)) {
Log.e(TAG, "Invalid action button: content description not present in bundle!");
removeBitmapFromBundle(bundle);
bitmap.recycle();
return null;
}
boolean onToolbar =
id == CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID
|| IntentUtils.safeGetBoolean(bundle, SHOW_ON_TOOLBAR, false);
if (onToolbar && !doesIconFitToolbar(context, bitmap)) {
onToolbar = false;
Log.w(
TAG,
"Button's icon not suitable for toolbar, putting it to bottom bar instead.See:"
+ " https://developer.android.com/reference/android/support/customtabs/"
+ "CustomTabsIntent.html#KEY_ICON");
}
PendingIntent pendingIntent =
IntentUtils.safeGetParcelable(bundle, CustomTabsIntent.KEY_PENDING_INTENT);
// PendingIntent is a must for buttons on the toolbar, but it's optional for bottom bar.
if (onToolbar && pendingIntent == null) {
Log.w(TAG, "Invalid action button on toolbar: pending intent not present in bundle!");
removeBitmapFromBundle(bundle);
bitmap.recycle();
return null;
}
return new CustomButtonParamsImpl(
id, bitmap, description, pendingIntent, tinted, onToolbar, ButtonType.OTHER);
}
/** Creates and returns a {@link CustomButtonParams} for a share button in the toolbar. */
@VisibleForTesting
public static CustomButtonParams createShareButton(Context context, int backgroundColor) {
int id = CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID;
String description = context.getResources().getString(R.string.share);
Intent shareIntent = new Intent(context, CustomTabsShareBroadcastReceiver.class);
PendingIntent pendingIntent =
PendingIntent.getBroadcast(
context,
0,
shareIntent,
PendingIntent.FLAG_UPDATE_CURRENT
| IntentUtils.getPendingIntentMutabilityFlag(true));
TintedDrawable drawable =
TintedDrawable.constructTintedDrawable(context, R.drawable.ic_share_white_24dp);
boolean useLightTint = ColorUtils.shouldUseLightForegroundOnBackground(backgroundColor);
drawable.setTint(ThemeUtils.getThemedToolbarIconTint(context, useLightTint));
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
return new CustomButtonParamsImpl(
id,
bitmap,
description,
pendingIntent,
/* tinted= */ true,
/* onToolbar= */ true,
ButtonType.CCT_SHARE_BUTTON);
}
@VisibleForTesting
public static CustomButtonParams createOpenInBrowserButton(
Context context, int backgroundColor) {
int id = CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID;
String description =
context.getResources().getString(R.string.menu_open_in_product_default);
TintedDrawable drawable =
TintedDrawable.constructTintedDrawable(
context, R.drawable.ic_open_in_new_white_24dp);
boolean useLightTint = ColorUtils.shouldUseLightForegroundOnBackground(backgroundColor);
drawable.setTint(ThemeUtils.getThemedToolbarIconTint(context, useLightTint));
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
return new CustomButtonParamsImpl(
id,
bitmap,
description,
/* pendingIntent= */ null,
/* tinted= */ true,
/* onToolbar= */ true,
ButtonType.CCT_OPEN_IN_BROWSER_BUTTON);
}
/**
* @return The bitmap contained in the given {@link Bundle}. Will return null if input is
* invalid.
*/
static Bitmap parseBitmapFromBundle(Bundle bundle) {
if (bundle == null) return null;
Bitmap bitmap = IntentUtils.safeGetParcelable(bundle, CustomTabsIntent.KEY_ICON);
if (bitmap == null) return null;
return bitmap;
}
/** Remove the bitmap contained in the given {@link Bundle}. Used when the bitmap is invalid. */
private static void removeBitmapFromBundle(Bundle bundle) {
if (bundle == null) return;
try {
bundle.remove(CustomTabsIntent.KEY_ICON);
} catch (Throwable t) {
Log.e(TAG, "Failed to remove icon extra from the intent");
}
}
/**
* @return The content description contained in the given {@link Bundle}. Will return null if
* input is invalid.
*/
static String parseDescriptionFromBundle(Bundle bundle) {
if (bundle == null) return null;
String description = IntentUtils.safeGetString(bundle, CustomTabsIntent.KEY_DESCRIPTION);
if (TextUtils.isEmpty(description)) return null;
return description;
}
/**
* @return Whether the given icon's size is suitable to put on toolbar.
*/
@Override
public boolean doesIconFitToolbar(Context context) {
return doesIconFitToolbar(context, mIcon);
}
/**
* Updates the visibility of this component on the toolbar.
*
* @param showOnToolbar {@code true} to display the component on the toolbar, {@code false} to
* display the component on the bottomBar.
*/
@Override
public void updateShowOnToolbar(boolean showOnToolbar) {
mIsOnToolbar = showOnToolbar;
}
private static boolean doesIconFitToolbar(Context context, Bitmap bitmap) {
int height = context.getResources().getDimensionPixelSize(R.dimen.toolbar_icon_height);
if (bitmap.getHeight() < height) return false;
int scaledWidth = bitmap.getWidth() / bitmap.getHeight() * height;
if (scaledWidth > 2 * height) return false;
return true;
}
}