1 // Copyright 2016 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.compositor.bottombar.contextualsearch; 6 7 import android.content.Context; 8 import android.content.Intent; 9 import android.content.pm.ActivityInfo; 10 import android.content.pm.PackageManager; 11 import android.content.pm.ResolveInfo; 12 import android.content.res.Resources; 13 import android.graphics.drawable.Drawable; 14 import android.provider.Browser; 15 import android.text.TextUtils; 16 import android.widget.ImageView; 17 18 import androidx.core.graphics.drawable.DrawableCompat; 19 20 import org.chromium.base.ApiCompatibilityUtils; 21 import org.chromium.base.IntentUtils; 22 import org.chromium.base.PackageManagerUtils; 23 import org.chromium.chrome.R; 24 import org.chromium.chrome.browser.ChromeTabbedActivity; 25 import org.chromium.chrome.browser.ChromeTabbedActivity2; 26 import org.chromium.chrome.browser.IntentHandler; 27 import org.chromium.chrome.browser.contextualsearch.ContextualSearchUma; 28 import org.chromium.chrome.browser.contextualsearch.QuickActionCategory; 29 import org.chromium.chrome.browser.tab.Tab; 30 import org.chromium.chrome.browser.toolbar.ToolbarColors; 31 import org.chromium.content_public.browser.LoadUrlParams; 32 import org.chromium.ui.resources.dynamics.DynamicResourceLoader; 33 import org.chromium.ui.resources.dynamics.ViewResourceInflater; 34 import org.chromium.ui.util.ColorUtils; 35 36 import java.net.URISyntaxException; 37 import java.util.List; 38 39 /** 40 * Stores information related to a Contextual Search "quick action." 41 * Actions can be activated through a tap on the Bar and include intents like calling a phone 42 * number or launching Maps for a street address. 43 */ 44 public class ContextualSearchQuickActionControl extends ViewResourceInflater { 45 private Context mContext; 46 private String mQuickActionUri; 47 private int mQuickActionCategory; 48 private int mToolbarBackgroundColor; 49 private boolean mHasQuickAction; 50 private boolean mOpenQuickActionInChrome; 51 private Intent mIntent; 52 private String mCaption; 53 54 /** 55 * @param context The Android Context used to inflate the View. 56 * @param resourceLoader The resource loader that will handle the snapshot capturing. 57 */ ContextualSearchQuickActionControl(Context context, DynamicResourceLoader resourceLoader)58 public ContextualSearchQuickActionControl(Context context, 59 DynamicResourceLoader resourceLoader) { 60 super(R.layout.contextual_search_quick_action_icon_view, 61 R.id.contextual_search_quick_action_icon_view, 62 context, null, resourceLoader); 63 mContext = context; 64 } 65 66 /** 67 * Gets the resource ID of the icon for the given category. 68 * @param category Which application category the icon should be for. 69 * @return The resource ID or {@code null} if an unsupported category is supplied. 70 */ getIconResId(@uickActionCategory int category)71 private static Integer getIconResId(@QuickActionCategory int category) { 72 switch (category) { 73 case QuickActionCategory.ADDRESS: 74 return R.drawable.ic_place_googblue_36dp; 75 case QuickActionCategory.EMAIL: 76 return R.drawable.ic_email_googblue_36dp; 77 case QuickActionCategory.EVENT: 78 return R.drawable.ic_event_googblue_36dp; 79 case QuickActionCategory.PHONE: 80 return R.drawable.ic_phone_googblue_36dp; 81 case QuickActionCategory.WEBSITE: 82 return R.drawable.ic_link_grey600_36dp; 83 default: 84 return null; 85 } 86 } 87 88 /** 89 * Gets the caption string to show for the default app for the given category. 90 * @param category Which application category the string should be for. 91 * @return A string ID or {@code null} if an unsupported category is supplied. 92 */ getDefaultAppCaptionId(@uickActionCategory int category)93 private static Integer getDefaultAppCaptionId(@QuickActionCategory int category) { 94 switch (category) { 95 case QuickActionCategory.ADDRESS: 96 return R.string.contextual_search_quick_action_caption_open; 97 case QuickActionCategory.EMAIL: 98 return R.string.contextual_search_quick_action_caption_email; 99 case QuickActionCategory.EVENT: 100 return R.string.contextual_search_quick_action_caption_event; 101 case QuickActionCategory.PHONE: 102 return R.string.contextual_search_quick_action_caption_phone; 103 case QuickActionCategory.WEBSITE: 104 return R.string.contextual_search_quick_action_caption_open; 105 default: 106 return null; 107 } 108 } 109 110 /** 111 * Gets the caption string to show for a generic app of the given category. 112 * @param category Which application category the string should be for. 113 * @return A string ID or {@code null} if an unsupported category is supplied. 114 */ getFallbackCaptionId(@uickActionCategory int category)115 private static Integer getFallbackCaptionId(@QuickActionCategory int category) { 116 switch (category) { 117 case QuickActionCategory.ADDRESS: 118 return R.string.contextual_search_quick_action_caption_generic_map; 119 case QuickActionCategory.EMAIL: 120 return R.string.contextual_search_quick_action_caption_generic_email; 121 case QuickActionCategory.EVENT: 122 return R.string.contextual_search_quick_action_caption_generic_event; 123 case QuickActionCategory.PHONE: 124 return R.string.contextual_search_quick_action_caption_phone; 125 case QuickActionCategory.WEBSITE: 126 return R.string.contextual_search_quick_action_caption_generic_website; 127 default: 128 return null; 129 } 130 } 131 132 /** 133 * @param quickActionUri The URI for the intent associated with the quick action. 134 * If the URI is the empty string or cannot be parsed no quick 135 * action will be available. 136 * @param quickActionCategory The {@link QuickActionCategory} for the quick action. 137 * @param toolbarBackgroundColor The current toolbar background color. This may be 138 * used for icon tinting. 139 */ setQuickAction( String quickActionUri, int quickActionCategory, int toolbarBackgroundColor)140 public void setQuickAction( 141 String quickActionUri, int quickActionCategory, int toolbarBackgroundColor) { 142 if (TextUtils.isEmpty(quickActionUri) || quickActionCategory == QuickActionCategory.NONE 143 || quickActionCategory >= QuickActionCategory.BOUNDARY) { 144 reset(); 145 return; 146 } 147 148 mQuickActionUri = quickActionUri; 149 mQuickActionCategory = quickActionCategory; 150 mToolbarBackgroundColor = toolbarBackgroundColor; 151 152 resolveIntent(); 153 } 154 155 /** 156 * Sends the intent associated with the quick action if one is available. 157 * @param tab The current tab, used to load a URL if the quick action should open 158 * inside Chrome. 159 */ sendIntent(Tab tab)160 public void sendIntent(Tab tab) { 161 if (mOpenQuickActionInChrome) { 162 tab.loadUrl(new LoadUrlParams(mQuickActionUri)); 163 return; 164 } 165 166 if (mIntent == null) return; 167 168 // Set the Browser application ID to us in case the user chooses Chrome 169 // as the app from the intent picker. 170 Context context = getContext(); 171 mIntent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); 172 173 mIntent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true); 174 mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 175 176 if (context instanceof ChromeTabbedActivity2) { 177 // Set the window ID so the new tab opens in the correct window. 178 mIntent.putExtra(IntentHandler.EXTRA_WINDOW_ID, 2); 179 } 180 181 IntentUtils.safeStartActivity(mContext, mIntent); 182 } 183 184 /** 185 * @return The caption associated with the quick action or null if no quick action 186 * is available. 187 */ getCaption()188 public String getCaption() { 189 return mCaption; 190 } 191 192 /** 193 * @return The resource id for the icon associated with the quick action or 0 if no 194 * quick action is available. 195 */ getIconResId()196 public int getIconResId() { 197 return mHasQuickAction ? getViewId() : 0; 198 } 199 200 /** 201 * @return Whether there is currently a quick action available. 202 */ hasQuickAction()203 public boolean hasQuickAction() { 204 return mHasQuickAction; 205 } 206 207 /** 208 * Resets quick action data. 209 */ reset()210 public void reset() { 211 mQuickActionUri = ""; 212 mQuickActionCategory = QuickActionCategory.NONE; 213 mHasQuickAction = false; 214 mOpenQuickActionInChrome = false; 215 mIntent = null; 216 mCaption = ""; 217 mToolbarBackgroundColor = 0; 218 } 219 220 @Override shouldAttachView()221 protected boolean shouldAttachView() { 222 return false; 223 } 224 resolveIntent()225 private void resolveIntent() { 226 try { 227 mIntent = Intent.parseUri(mQuickActionUri, 0); 228 } catch (URISyntaxException e) { 229 // If the intent cannot be parsed, there is no quick action available. 230 ContextualSearchUma.logQuickActionIntentResolution(mQuickActionCategory, 0); 231 reset(); 232 return; 233 } 234 235 PackageManager packageManager = mContext.getPackageManager(); 236 237 // If a default is set, PackageManager#resolveActivity() will return the 238 // ResolveInfo for the default activity. 239 ResolveInfo possibleDefaultActivity = PackageManagerUtils.resolveActivity(mIntent, 0); 240 241 // PackageManager#queryIntentActivities() will return a list of activities that 242 // can handle the intent, sorted from best to worst. If there are no matching 243 // activities, an empty list is returned. 244 List<ResolveInfo> resolveInfoList = PackageManagerUtils.queryIntentActivities(mIntent, 0); 245 246 int numMatchingActivities = 0; 247 ResolveInfo defaultActivityResolveInfo = null; 248 for (ResolveInfo resolveInfo : resolveInfoList) { 249 if (resolveInfo.activityInfo != null && resolveInfo.activityInfo.exported) { 250 numMatchingActivities++; 251 252 // Return early if this resolveInfo matches the possibleDefaultActivity. 253 ActivityInfo possibleDefaultActivityInfo = possibleDefaultActivity.activityInfo; 254 if (possibleDefaultActivityInfo == null) continue; 255 256 ActivityInfo resolveActivityInfo = resolveInfo.activityInfo; 257 boolean matchesPossibleDefaultActivity = 258 TextUtils.equals(resolveActivityInfo.name, possibleDefaultActivityInfo.name) 259 && TextUtils.equals(resolveActivityInfo.packageName, 260 possibleDefaultActivityInfo.packageName); 261 262 if (matchesPossibleDefaultActivity) { 263 defaultActivityResolveInfo = resolveInfo; 264 break; 265 } 266 } 267 } 268 269 ContextualSearchUma.logQuickActionIntentResolution( 270 mQuickActionCategory, numMatchingActivities); 271 272 if (numMatchingActivities == 0) { 273 reset(); 274 return; 275 } 276 277 mHasQuickAction = true; 278 Drawable iconDrawable = null; 279 int iconResId = 0; 280 if (defaultActivityResolveInfo != null) { 281 iconDrawable = defaultActivityResolveInfo.loadIcon(mContext.getPackageManager()); 282 283 if (mQuickActionCategory != QuickActionCategory.PHONE) { 284 // Use the default app's name to construct the caption. 285 mCaption = mContext.getResources().getString( 286 getDefaultAppCaptionId(mQuickActionCategory), 287 defaultActivityResolveInfo.loadLabel(packageManager)); 288 } else { 289 // The caption for phone numbers does not use the app's name. 290 mCaption = mContext.getResources().getString( 291 getDefaultAppCaptionId(mQuickActionCategory)); 292 } 293 } else if (mQuickActionCategory == QuickActionCategory.WEBSITE) { 294 // If there is not a default app handler for a URL, open the quick action 295 // inside of Chrome. 296 mOpenQuickActionInChrome = true; 297 298 if (mContext instanceof ChromeTabbedActivity) { 299 // Use the app icon if this is a ChromeTabbedActivity instance. 300 iconResId = R.mipmap.app_icon; 301 } else { 302 // Otherwise use the link icon. 303 iconResId = getIconResId(mQuickActionCategory); 304 305 Resources res = mContext.getResources(); 306 if (mToolbarBackgroundColor != 0 307 && !ToolbarColors.isUsingDefaultToolbarColor( 308 res, false, mToolbarBackgroundColor) 309 && ColorUtils.shouldUseLightForegroundOnBackground( 310 mToolbarBackgroundColor)) { 311 // Tint the link icon to match the custom tab toolbar. 312 iconDrawable = ApiCompatibilityUtils.getDrawable(res, iconResId); 313 iconDrawable.mutate(); 314 DrawableCompat.setTint(iconDrawable, mToolbarBackgroundColor); 315 } 316 } 317 mCaption = 318 mContext.getResources().getString(getFallbackCaptionId(mQuickActionCategory)); 319 } else { 320 iconResId = getIconResId(mQuickActionCategory); 321 mCaption = 322 mContext.getResources().getString(getFallbackCaptionId(mQuickActionCategory)); 323 } 324 325 inflate(); 326 327 if (iconDrawable != null) { 328 ((ImageView) getView()).setImageDrawable(iconDrawable); 329 } else { 330 ((ImageView) getView()).setImageResource(iconResId); 331 } 332 333 invalidate(); 334 } 335 } 336