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