1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.support.car.lenspicker;
17 
18 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
19 
20 import android.app.Activity;
21 import android.content.ComponentName;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.PatternMatcher;
29 import android.provider.MediaStore;
30 import android.text.TextUtils;
31 import android.util.Log;
32 import android.view.View;
33 import android.view.Window;
34 import android.widget.CheckBox;
35 import android.widget.TextView;
36 
37 import androidx.annotation.StringRes;
38 import androidx.car.util.ColumnCalculator;
39 import androidx.car.widget.PagedListView;
40 
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Set;
44 
45 /**
46  * An activity that is displayed when the system attempts to start an Intent for which there is
47  * more than one matching activity, allowing the user to decide which to go to.
48  *
49  * <p>This activity replaces the default ResolverActivity that Android uses.
50  */
51 public class LensResolverActivity extends Activity implements
52         ResolverListRow.ResolverSelectionHandler {
53     private static final String TAG = "LensResolverActivity";
54     private CheckBox mAlwaysCheckbox;
55 
56     /**
57      * {@code true} if this ResolverActivity is asking to the user to determine the default
58      * launcher.
59      */
60     private boolean mResolvingHome;
61 
62     /**
63      * The Intent to disambiguate.
64      */
65     private Intent mResolveIntent;
66 
67     /**
68      * A set of {@link ComponentName}s that represent the list of activities that the user is
69      * picking from to handle {@link #mResolveIntent}.
70      */
71     private ComponentName[] mComponentSet;
72 
73     @Override
onCreate(Bundle savedInstanceState)74     protected void onCreate(Bundle savedInstanceState) {
75         super.onCreate(savedInstanceState);
76 
77         // It seems that the title bar is added when this Activity is called by the system despite
78         // the theme of this Activity specifying otherwise. As a result, explicitly turn off the
79         // title bar.
80         requestWindowFeature(Window.FEATURE_NO_TITLE);
81 
82         setContentView(R.layout.resolver_list);
83 
84         mResolveIntent = new Intent(getIntent());
85 
86         // Clear the component since it would have been set to this LensResolverActivity.
87         mResolveIntent.setComponent(null);
88 
89         // The resolver activity is set to be hidden from recent tasks. This attribute should not
90         // be propagated to the next activity being launched.  Note that if the original Intent
91         // also had this flag set, we are now losing it.  That should be a very rare case though.
92         mResolveIntent.setFlags(
93                 mResolveIntent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
94 
95         // Check if we are setting the default launcher.
96         Set<String> categories = mResolveIntent.getCategories();
97         if (Intent.ACTION_MAIN.equals(mResolveIntent.getAction()) && categories != null
98                 && categories.size() == 1 && categories.contains(Intent.CATEGORY_HOME)) {
99             mResolvingHome = true;
100         }
101 
102         List<ResolveInfo> infos = getPackageManager().queryIntentActivities(mResolveIntent,
103                 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_RESOLVED_FILTER);
104         buildComponentSet(infos);
105 
106         if (Log.isLoggable(TAG, Log.DEBUG)) {
107             int size = infos == null ? 0 : infos.size();
108             Log.d(TAG, "Found " + size + " matching activities.");
109         }
110 
111         // The title container should match the width of the ColumnCards in the list. Those cards
112         // have their width set depending on the column span, which changes between screen sizes.
113         // As a result, need to set the width of the title container programmatically.
114         int defaultColumnSpan =
115                 getResources().getInteger(R.integer.column_card_default_column_span);
116         int cardWidth = ColumnCalculator.getInstance(this /* context */).getColumnSpanWidth(
117                 defaultColumnSpan);
118         View titleAndCheckboxContainer = findViewById(R.id.title_checkbox_container);
119         titleAndCheckboxContainer.getLayoutParams().width = cardWidth;
120 
121         mAlwaysCheckbox = (CheckBox) findViewById(R.id.always_checkbox);
122 
123         PagedListView pagedListView = (PagedListView) findViewById(R.id.list_view);
124 
125         ResolverAdapter adapter = new ResolverAdapter(this /* context */, infos);
126         adapter.setSelectionHandler(this);
127         pagedListView.setAdapter(adapter);
128 
129         TextView title = (TextView) findViewById(R.id.title);
130         title.setText(getTitleForAction(mResolveIntent.getAction()));
131 
132         findViewById(R.id.dismiss_area).setOnClickListener(v -> finish());
133     }
134 
135     /**
136      * Constructs a set of {@link ComponentName}s that represent the set of activites that the user
137      * was picking from within this list presented by this resolver activity.
138      */
buildComponentSet(List<ResolveInfo> infos)139     private void buildComponentSet(List<ResolveInfo> infos) {
140         int size = infos.size();
141         mComponentSet = new ComponentName[size];
142 
143         for (int i = 0; i < size; i++) {
144             ResolveInfo info = infos.get(i);
145             mComponentSet[i] = new ComponentName(info.activityInfo.packageName,
146                     info.activityInfo.name);
147         }
148     }
149 
150     /**
151      * Returns the title that should be used for the given Intent action.
152      *
153      * @param action One of the actions in Intent, such as {@link Intent#ACTION_VIEW}.
154      */
getTitleForAction(String action)155     private CharSequence getTitleForAction(String action) {
156         ActionTitle title = mResolvingHome ? ActionTitle.HOME : ActionTitle.forAction(action);
157         return getString(title.titleRes);
158     }
159 
160     /**
161      * Opens the activity that is specified by the given {@link ResolveInfo} and
162      * {@link LensPickerItem}. If the {@link #mAlwaysCheckbox} has been checked, then the
163      * activity will be set as the default activity for Intents of a matching format to
164      * {@link #mResolveIntent}.
165      */
166     @Override
onActivitySelected(ResolveInfo info, LensPickerItem item)167     public void onActivitySelected(ResolveInfo info, LensPickerItem item) {
168         ComponentName component = item.getLaunchIntent().getComponent();
169 
170         if (mAlwaysCheckbox.isChecked()) {
171             PackageManager pm = getPackageManager();
172             if (info.handleAllWebDataURI) {
173                 // Set default Browser if needed
174                 int userId = getUserId();
175                 String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
176                 if (TextUtils.isEmpty(packageName)) {
177                     pm.setDefaultBrowserPackageNameAsUser(info.activityInfo.packageName, userId);
178                 }
179             }
180             IntentFilter filter = buildIntentFilterForResolveInfo(info);
181             pm.addPreferredActivity(filter, info.match, mComponentSet, component);
182         }
183 
184         // Now launch the original resolve intent but correctly set the component.
185         Intent launchIntent = new Intent(mResolveIntent);
186         launchIntent.setComponent(component);
187 
188         // It might be necessary to use startActivityAsCaller() instead. The default
189         // ResolverActivity does this. However, that method is unavailable to be called from
190         // classes that are do not have "android" in the package name. As a result, just utilize
191         // a regular startActivity(). If it becomes necessary to utilize this method, then
192         // LensResolverActivity will have to extend ResolverActivity.
193         startActivity(launchIntent);
194         finish();
195     }
196 
197     /**
198      * Returns an {@link IntentFilter} based on the given {@link ResolveInfo} so that the
199      * activity specified by that ResolveInfo will be the default for Intents like
200      * {@link #mResolveIntent}.
201      *
202      * <p>This code is copied from com.android.internal.app.ResolverActivity.
203      */
buildIntentFilterForResolveInfo(ResolveInfo info)204     private IntentFilter buildIntentFilterForResolveInfo(ResolveInfo info) {
205         // Build a reasonable intent filter, based on what matched.
206         IntentFilter filter = new IntentFilter();
207         Intent filterIntent;
208 
209         if (mResolveIntent.getSelector() != null) {
210             filterIntent = mResolveIntent.getSelector();
211         } else {
212             filterIntent = mResolveIntent;
213         }
214 
215         String action = filterIntent.getAction();
216         if (action != null) {
217             filter.addAction(action);
218         }
219         Set<String> categories = filterIntent.getCategories();
220         if (categories != null) {
221             for (String cat : categories) {
222                 filter.addCategory(cat);
223             }
224         }
225         filter.addCategory(Intent.CATEGORY_DEFAULT);
226 
227         int cat = info.match & IntentFilter.MATCH_CATEGORY_MASK;
228         Uri data = filterIntent.getData();
229         if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
230             String mimeType = filterIntent.resolveType(this);
231             if (mimeType != null) {
232                 try {
233                     filter.addDataType(mimeType);
234                 } catch (IntentFilter.MalformedMimeTypeException e) {
235                     Log.e(TAG, "Could not add data type", e);
236                     filter = null;
237                 }
238             }
239         }
240         if (data != null && data.getScheme() != null) {
241             // We need the data specification if there was no type OR if the scheme is not one of
242             // our magical "file:" or "content:" schemes (see IntentFilter for the reason).
243             if (cat != IntentFilter.MATCH_CATEGORY_TYPE
244                     || (!"file".equals(data.getScheme())
245                     && !"content".equals(data.getScheme()))) {
246                 filter.addDataScheme(data.getScheme());
247 
248                 // Look through the resolved filter to determine which part of it matched the
249                 // original Intent.
250                 Iterator<PatternMatcher> pIt = info.filter.schemeSpecificPartsIterator();
251                 if (pIt != null) {
252                     String ssp = data.getSchemeSpecificPart();
253                     while (ssp != null && pIt.hasNext()) {
254                         PatternMatcher p = pIt.next();
255                         if (p.match(ssp)) {
256                             filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
257                             break;
258                         }
259                     }
260                 }
261                 Iterator<IntentFilter.AuthorityEntry> aIt = info.filter.authoritiesIterator();
262                 if (aIt != null) {
263                     while (aIt.hasNext()) {
264                         IntentFilter.AuthorityEntry a = aIt.next();
265                         if (a.match(data) >= 0) {
266                             int port = a.getPort();
267                             filter.addDataAuthority(a.getHost(),
268                                     port >= 0 ? Integer.toString(port) : null);
269                             break;
270                         }
271                     }
272                 }
273                 pIt = info.filter.pathsIterator();
274                 if (pIt != null) {
275                     String path = data.getPath();
276                     while (path != null && pIt.hasNext()) {
277                         PatternMatcher p = pIt.next();
278                         if (p.match(path)) {
279                             filter.addDataPath(p.getPath(), p.getType());
280                             break;
281                         }
282                     }
283                 }
284             }
285         }
286 
287         return filter;
288     }
289 
290     @Override
onStop()291     protected void onStop() {
292         super.onStop();
293 
294         if ((getIntent().getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()) {
295             // This resolver is in the unusual situation where it has been launched at the top of a
296             // new task.  We don't let it be added to the recent tasks shown to the user, and we
297             // need to make sure that each time we are launched we get the correct launching
298             // uid (not re-using the same resolver from an old launching uid), so we will now
299             // finish since being no longer visible, the user probably can't get back to us.
300             if (!isChangingConfigurations()) {
301                 finish();
302             }
303         }
304     }
305 
306     /**
307      * An enum mapping different Intent actions to the strings that should be displayed that
308      * explain to the user what this ResolverActivity is doing.
309      */
310     private enum ActionTitle {
311         VIEW(Intent.ACTION_VIEW,
312                 R.string.whichViewApplication,
313                 R.string.whichViewApplicationNamed,
314                 R.string.whichViewApplicationLabel),
315         EDIT(Intent.ACTION_EDIT,
316                 R.string.whichEditApplication,
317                 R.string.whichEditApplicationNamed,
318                 R.string.whichEditApplicationLabel),
319         SEND(Intent.ACTION_SEND,
320                 R.string.whichSendApplication,
321                 R.string.whichSendApplicationNamed,
322                 R.string.whichSendApplicationLabel),
323         SENDTO(Intent.ACTION_SENDTO,
324                 R.string.whichSendToApplication,
325                 R.string.whichSendToApplicationNamed,
326                 R.string.whichSendToApplicationLabel),
327         SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE,
328                 R.string.whichSendApplication,
329                 R.string.whichSendApplicationNamed,
330                 R.string.whichSendApplicationLabel),
331         CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE,
332                 R.string.whichImageCaptureApplication,
333                 R.string.whichImageCaptureApplicationNamed,
334                 R.string.whichImageCaptureApplicationLabel),
335         DEFAULT(null,
336                 R.string.whichApplication,
337                 R.string.whichApplicationNamed,
338                 R.string.whichApplicationLabel),
339         HOME(Intent.ACTION_MAIN,
340                 R.string.whichHomeApplication,
341                 R.string.whichHomeApplicationNamed,
342                 R.string.whichHomeApplicationLabel);
343 
344         public final String action;
345         public final int titleRes;
346         public final int namedTitleRes;
347 
348         @StringRes
349         public final int labelRes;
350 
ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes)351         ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) {
352             this.action = action;
353             this.titleRes = titleRes;
354             this.namedTitleRes = namedTitleRes;
355             this.labelRes = labelRes;
356         }
357 
358         /**
359          * Returns a set of Strings that should be used for the given Intent action.
360          */
forAction(String action)361         public static ActionTitle forAction(String action) {
362             for (ActionTitle title : values()) {
363                 if (title != HOME && action != null && action.equals(title.action)) {
364                     return title;
365                 }
366             }
367             return DEFAULT;
368         }
369     }
370 }
371