// 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.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.customtabs.CustomTabsIntent; 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 org.chromium.base.Log; import org.chromium.chrome.R; import org.chromium.chrome.browser.util.IntentUtils; import org.chromium.chrome.browser.widget.TintedDrawable; import org.chromium.ui.widget.Toast; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; /** * Container for all parameters related to creating a customizable button. */ class 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 CustomButtonParams(int id, Bitmap icon, String description, @Nullable PendingIntent pendingIntent, boolean tinted, boolean onToolbar) { mId = id; mIcon = icon; mDescription = description; mPendingIntent = pendingIntent; mShouldTint = tinted; mIsOnToolbar = onToolbar; } /** * Replaces the current icon and description with new ones. */ void update(@NonNull Bitmap icon, @NonNull String description) { mIcon = icon; mDescription = description; } /** * @return Whether this button should be shown on the toolbar. */ 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. */ int getId() { return mId; } /** * @return The drawable for the customized button. */ Drawable getIcon(Resources res) { if (mShouldTint) { return TintedDrawable.constructTintedDrawable(res, mIcon); } else { return new BitmapDrawable(res, mIcon); } } /** * @return The content description for the customized button. */ String getDescription() { return mDescription; } /** * @return The {@link PendingIntent} that will be sent when user clicks the customized button. */ PendingIntent getPendingIntent() { return mPendingIntent; } /** * 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. */ ImageButton buildBottomBarButton(Context context, ViewGroup parent, OnClickListener listener) { if (mIsOnToolbar) return null; 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 intent The intent sent by the client. * @return A list of parsed {@link CustomButtonParams}. Return an empty list if input is invalid */ 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); } 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!"); bitmap.recycle(); return null; } boolean onToolbar = id == CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID; 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!"); bitmap.recycle(); return null; } return new CustomButtonParams(id, bitmap, description, pendingIntent, tinted, onToolbar); } /** * @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; } /** * @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. */ 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; } }