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