1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.customtabs;
6 
7 import android.app.PendingIntent;
8 import android.content.Context;
9 import android.content.Intent;
10 import android.graphics.Bitmap;
11 import android.graphics.drawable.BitmapDrawable;
12 import android.graphics.drawable.Drawable;
13 import android.os.Bundle;
14 import android.text.TextUtils;
15 import android.view.Gravity;
16 import android.view.LayoutInflater;
17 import android.view.View;
18 import android.view.View.OnClickListener;
19 import android.view.View.OnLongClickListener;
20 import android.view.ViewGroup;
21 import android.widget.ImageButton;
22 
23 import androidx.annotation.NonNull;
24 import androidx.annotation.Nullable;
25 import androidx.annotation.VisibleForTesting;
26 import androidx.browser.customtabs.CustomTabsIntent;
27 
28 import org.chromium.base.IntentUtils;
29 import org.chromium.base.Log;
30 import org.chromium.chrome.R;
31 import org.chromium.chrome.browser.toolbar.ToolbarColors;
32 import org.chromium.components.browser_ui.widget.TintedDrawable;
33 import org.chromium.ui.util.ColorUtils;
34 import org.chromium.ui.widget.Toast;
35 
36 import java.util.ArrayList;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Set;
40 
41 /**
42  * Container for all parameters related to creating a customizable button.
43  */
44 public class CustomButtonParams {
45     private static final String TAG = "CustomTabs";
46 
47     private final PendingIntent mPendingIntent;
48     private int mId;
49     private Bitmap mIcon;
50     private String mDescription;
51     private boolean mShouldTint;
52     private boolean mIsOnToolbar;
53 
54     @VisibleForTesting
55     static final String SHOW_ON_TOOLBAR = "android.support.customtabs.customaction.SHOW_ON_TOOLBAR";
56 
CustomButtonParams(int id, Bitmap icon, String description, @Nullable PendingIntent pendingIntent, boolean tinted, boolean onToolbar)57     private CustomButtonParams(int id, Bitmap icon, String description,
58             @Nullable PendingIntent pendingIntent, boolean tinted, boolean onToolbar) {
59         mId = id;
60         mIcon = icon;
61         mDescription = description;
62         mPendingIntent = pendingIntent;
63         mShouldTint = tinted;
64         mIsOnToolbar = onToolbar;
65     }
66 
67     /**
68      * Replaces the current icon and description with new ones.
69      */
update(@onNull Bitmap icon, @NonNull String description)70     void update(@NonNull Bitmap icon, @NonNull String description) {
71         mIcon = icon;
72         mDescription = description;
73     }
74 
75     /**
76      * @return Whether this button should be shown on the toolbar.
77      */
showOnToolbar()78     public boolean showOnToolbar() {
79         return mIsOnToolbar;
80     }
81 
82     /**
83      * @return The id associated with this button. The custom button on the toolbar always uses
84      *         {@link CustomTabsIntent#TOOLBAR_ACTION_BUTTON_ID} as id.
85      */
getId()86     public int getId() {
87         return mId;
88     }
89 
90     /**
91      * @return The drawable for the customized button.
92      */
getIcon(Context context)93     public Drawable getIcon(Context context) {
94         if (mShouldTint) {
95             return new TintedDrawable(context, mIcon);
96         } else {
97             return new BitmapDrawable(context.getResources(), mIcon);
98         }
99     }
100 
101     /**
102      * @return The content description for the customized button.
103      */
getDescription()104     public String getDescription() {
105         return mDescription;
106     }
107 
108     /**
109      * @return The {@link PendingIntent} that will be sent when user clicks the customized button.
110      */
getPendingIntent()111     public PendingIntent getPendingIntent() {
112         return mPendingIntent;
113     }
114 
115     /**
116      * Builds an {@link ImageButton} from the data in this params. Generated buttons should be
117      * placed on the bottom bar. The button's tag will be its id.
118      * @param parent The parent that the inflated {@link ImageButton}.
119      * @param listener {@link OnClickListener} that should be used with the button.
120      * @return Parsed list of {@link CustomButtonParams}, which is empty if the input is invalid.
121      */
buildBottomBarButton(Context context, ViewGroup parent, OnClickListener listener)122     ImageButton buildBottomBarButton(Context context, ViewGroup parent, OnClickListener listener) {
123         assert !mIsOnToolbar;
124 
125         ImageButton button = (ImageButton) LayoutInflater.from(context).inflate(
126                 R.layout.custom_tabs_bottombar_item, parent, false);
127         button.setId(mId);
128         button.setImageBitmap(mIcon);
129         button.setContentDescription(mDescription);
130         if (mPendingIntent == null) {
131             button.setEnabled(false);
132         } else {
133             button.setOnClickListener(listener);
134         }
135         button.setOnLongClickListener(new OnLongClickListener() {
136             @Override
137             public boolean onLongClick(View view) {
138                 final int screenWidth = view.getResources().getDisplayMetrics().widthPixels;
139                 final int screenHeight = view.getResources().getDisplayMetrics().heightPixels;
140                 final int[] screenPos = new int[2];
141                 view.getLocationOnScreen(screenPos);
142                 final int width = view.getWidth();
143 
144                 Toast toast = Toast.makeText(
145                         view.getContext(), view.getContentDescription(), Toast.LENGTH_SHORT);
146                 toast.setGravity(Gravity.BOTTOM | Gravity.END,
147                         screenWidth - screenPos[0] - width / 2, screenHeight - screenPos[1]);
148                 toast.show();
149                 return true;
150             }
151         });
152         return button;
153     }
154 
155     /**
156      * Parses a list of {@link CustomButtonParams} from the intent sent by clients.
157      * @param intent The intent sent by the client.
158      * @return A list of parsed {@link CustomButtonParams}. Return an empty list if input is invalid
159      */
fromIntent(Context context, Intent intent)160     public static List<CustomButtonParams> fromIntent(Context context, Intent intent) {
161         List<CustomButtonParams> paramsList = new ArrayList<>(1);
162         if (intent == null) return paramsList;
163 
164         Bundle singleBundle =
165                 IntentUtils.safeGetBundleExtra(intent, CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE);
166         ArrayList<Bundle> bundleList = IntentUtils.getParcelableArrayListExtra(
167                 intent, CustomTabsIntent.EXTRA_TOOLBAR_ITEMS);
168         boolean tinted = IntentUtils.safeGetBooleanExtra(
169                 intent, CustomTabsIntent.EXTRA_TINT_ACTION_BUTTON, false);
170         if (singleBundle != null) {
171             CustomButtonParams singleParams = fromBundle(context, singleBundle, tinted, false);
172             if (singleParams != null) paramsList.add(singleParams);
173         }
174         if (bundleList != null) {
175             Set<Integer> ids = new HashSet<>();
176             for (Bundle bundle : bundleList) {
177                 CustomButtonParams params = fromBundle(context, bundle, tinted, true);
178                 if (params == null) {
179                     continue;
180                 } else if (ids.contains(params.getId())) {
181                     Log.e(TAG, "Bottom bar items contain duplicate id: " + params.getId());
182                     continue;
183                 }
184                 ids.add(params.getId());
185                 paramsList.add(params);
186             }
187         }
188         return paramsList;
189     }
190 
191     /**
192      * Parses params out of a bundle. Note if a custom button contains a bitmap that does not fit
193      * into the toolbar, it will be put to the bottom bar.
194      * @param fromList Whether the bundle is contained in a list or it is the single bundle that
195      *                 directly comes from the intent.
196      */
fromBundle( Context context, Bundle bundle, boolean tinted, boolean fromList)197     private static CustomButtonParams fromBundle(
198             Context context, Bundle bundle, boolean tinted, boolean fromList) {
199         if (bundle == null) return null;
200 
201         if (fromList && !bundle.containsKey(CustomTabsIntent.KEY_ID)) return null;
202         int id = IntentUtils.safeGetInt(
203                 bundle, CustomTabsIntent.KEY_ID, CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID);
204 
205         Bitmap bitmap = parseBitmapFromBundle(bundle);
206         if (bitmap == null) {
207             Log.e(TAG, "Invalid action button: bitmap not present in bundle!");
208             return null;
209         }
210 
211         String description = parseDescriptionFromBundle(bundle);
212         if (TextUtils.isEmpty(description)) {
213             Log.e(TAG, "Invalid action button: content description not present in bundle!");
214             removeBitmapFromBundle(bundle);
215             bitmap.recycle();
216             return null;
217         }
218 
219         boolean onToolbar = id == CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID
220                 || IntentUtils.safeGetBoolean(bundle, SHOW_ON_TOOLBAR, false);
221         if (onToolbar && !doesIconFitToolbar(context, bitmap)) {
222             onToolbar = false;
223             Log.w(TAG,
224                     "Button's icon not suitable for toolbar, putting it to bottom bar instead."
225                             + "See: https://developer.android.com/reference/android/support/customtabs/"
226                             + "CustomTabsIntent.html#KEY_ICON");
227         }
228 
229         PendingIntent pendingIntent =
230                 IntentUtils.safeGetParcelable(bundle, CustomTabsIntent.KEY_PENDING_INTENT);
231         // PendingIntent is a must for buttons on the toolbar, but it's optional for bottom bar.
232         if (onToolbar && pendingIntent == null) {
233             Log.w(TAG, "Invalid action button on toolbar: pending intent not present in bundle!");
234             removeBitmapFromBundle(bundle);
235             bitmap.recycle();
236             return null;
237         }
238 
239         return new CustomButtonParams(id, bitmap, description, pendingIntent, tinted, onToolbar);
240     }
241 
242     /**
243      * Creates and returns a {@link CustomButtonParams} for a share button in the toolbar.
244      */
createShareButton(Context context, int backgroundColor)245     static CustomButtonParams createShareButton(Context context, int backgroundColor) {
246         int id = CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID;
247         String description = context.getResources().getString(R.string.share);
248         Intent shareIntent = new Intent(context, CustomTabsShareBroadcastReceiver.class);
249         PendingIntent pendingIntent = PendingIntent.getBroadcast(
250                 context, 0, shareIntent, PendingIntent.FLAG_UPDATE_CURRENT);
251 
252         TintedDrawable drawable =
253                 TintedDrawable.constructTintedDrawable(context, R.drawable.ic_share_white_24dp);
254         boolean useLightTint = ColorUtils.shouldUseLightForegroundOnBackground(backgroundColor);
255         drawable.setTint(ToolbarColors.getThemedToolbarIconTint(context, useLightTint));
256         Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
257 
258         return new CustomButtonParams(
259                 id, bitmap, description, pendingIntent, /*tinted=*/true, /*onToolbar=*/true);
260     }
261 
262     /**
263      * @return The bitmap contained in the given {@link Bundle}. Will return null if input is
264      *         invalid.
265      */
parseBitmapFromBundle(Bundle bundle)266     static Bitmap parseBitmapFromBundle(Bundle bundle) {
267         if (bundle == null) return null;
268         Bitmap bitmap = IntentUtils.safeGetParcelable(bundle, CustomTabsIntent.KEY_ICON);
269         if (bitmap == null) return null;
270         return bitmap;
271     }
272 
273     /**
274      * Remove the bitmap contained in the given {@link Bundle}. Used when the bitmap is invalid.
275      */
removeBitmapFromBundle(Bundle bundle)276     private static void removeBitmapFromBundle(Bundle bundle) {
277         if (bundle == null) return;
278 
279         try {
280             bundle.remove(CustomTabsIntent.KEY_ICON);
281         } catch (Throwable t) {
282             Log.e(TAG, "Failed to remove icon extra from the intent");
283         }
284     }
285 
286     /**
287      * @return The content description contained in the given {@link Bundle}. Will return null if
288      *         input is invalid.
289      */
parseDescriptionFromBundle(Bundle bundle)290     static String parseDescriptionFromBundle(Bundle bundle) {
291         if (bundle == null) return null;
292         String description = IntentUtils.safeGetString(bundle, CustomTabsIntent.KEY_DESCRIPTION);
293         if (TextUtils.isEmpty(description)) return null;
294         return description;
295     }
296 
297     /**
298      * @return Whether the given icon's size is suitable to put on toolbar.
299      */
doesIconFitToolbar(Context context)300     public boolean doesIconFitToolbar(Context context) {
301         return doesIconFitToolbar(context, mIcon);
302     }
303 
doesIconFitToolbar(Context context, Bitmap bitmap)304     private static boolean doesIconFitToolbar(Context context, Bitmap bitmap) {
305         int height = context.getResources().getDimensionPixelSize(R.dimen.toolbar_icon_height);
306         if (bitmap.getHeight() < height) return false;
307         int scaledWidth = bitmap.getWidth() / bitmap.getHeight() * height;
308         if (scaledWidth > 2 * height) return false;
309         return true;
310     }
311 }
312