• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.support.annotation.StringRes;
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 com.android.car.stream.ui.ColumnCalculator;
38 import com.android.car.view.PagedListView;
39 
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.Set;
43 
44 /**
45  * An activity that is displayed when the system attempts to start an Intent for which there is
46  * more than one matching activity, allowing the user to decide which to go to.
47  *
48  * <p>This activity replaces the default ResolverActivity that Android uses.
49  */
50 public class LensResolverActivity extends Activity implements
51         ResolverListRow.ResolverSelectionHandler {
52     private static final String TAG = "LensResolverActivity";
53     private CheckBox mAlwaysCheckbox;
54 
55     /**
56      * {@code true} if this ResolverActivity is asking to the user to determine the default
57      * launcher.
58      */
59     private boolean mResolvingHome;
60 
61     /**
62      * The Intent to disambiguate.
63      */
64     private Intent mResolveIntent;
65 
66     /**
67      * A set of {@link ComponentName}s that represent the list of activities that the user is
68      * picking from to handle {@link #mResolveIntent}.
69      */
70     private ComponentName[] mComponentSet;
71 
72     @Override
onCreate(Bundle savedInstanceState)73     protected void onCreate(Bundle savedInstanceState) {
74         super.onCreate(savedInstanceState);
75 
76         // It seems that the title bar is added when this Activity is called by the system despite
77         // the theme of this Activity specifying otherwise. As a result, explicitly turn off the
78         // title bar.
79         requestWindowFeature(Window.FEATURE_NO_TITLE);
80 
81         setContentView(R.layout.resolver_list);
82 
83         mResolveIntent = new Intent(getIntent());
84 
85         // Clear the component since it would have been set to this LensResolverActivity.
86         mResolveIntent.setComponent(null);
87 
88         // The resolver activity is set to be hidden from recent tasks. This attribute should not
89         // be propagated to the next activity being launched.  Note that if the original Intent
90         // also had this flag set, we are now losing it.  That should be a very rare case though.
91         mResolveIntent.setFlags(
92                 mResolveIntent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
93 
94         // Check if we are setting the default launcher.
95         Set<String> categories = mResolveIntent.getCategories();
96         if (Intent.ACTION_MAIN.equals(mResolveIntent.getAction()) && categories != null
97                 && categories.size() == 1 && categories.contains(Intent.CATEGORY_HOME)) {
98             mResolvingHome = true;
99         }
100 
101         List<ResolveInfo> infos = getPackageManager().queryIntentActivities(mResolveIntent,
102                 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_RESOLVED_FILTER);
103         buildComponentSet(infos);
104 
105         if (Log.isLoggable(TAG, Log.DEBUG)) {
106             int size = infos == null ? 0 : infos.size();
107             Log.d(TAG, "Found " + size + " matching activities.");
108         }
109 
110         // The title container should match the width of the StreamCards in the list. Those cards
111         // have their width set depending on the column span, which changes between screen sizes.
112         // As a result, need to set the width of the title container programmatically.
113         int defaultColumnSpan =
114                 getResources().getInteger(R.integer.stream_card_default_column_span);
115         int cardWidth = ColumnCalculator.getInstance(this /* context */).getSizeForColumnSpan(
116                 defaultColumnSpan);
117         View titleAndCheckboxContainer = findViewById(R.id.title_checkbox_container);
118         titleAndCheckboxContainer.getLayoutParams().width = cardWidth;
119 
120         mAlwaysCheckbox = (CheckBox) findViewById(R.id.always_checkbox);
121 
122         PagedListView pagedListView = (PagedListView) findViewById(R.id.list_view);
123         pagedListView.setLightMode();
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             IntentFilter filter = buildIntentFilterForResolveInfo(info);
172             getPackageManager().addPreferredActivity(filter, info.match, mComponentSet, component);
173         }
174 
175         // Now launch the original resolve intent but correctly set the component.
176         Intent launchIntent = new Intent(mResolveIntent);
177         launchIntent.setComponent(component);
178 
179         // It might be necessary to use startActivityAsCaller() instead. The default
180         // ResolverActivity does this. However, that method is unavailable to be called from
181         // classes that are do not have "android" in the package name. As a result, just utilize
182         // a regular startActivity(). If it becomes necessary to utilize this method, then
183         // LensResolverActivity will have to extend ResolverActivity.
184         startActivity(launchIntent);
185         finish();
186     }
187 
188     /**
189      * Returns an {@link IntentFilter} based on the given {@link ResolveInfo} so that the
190      * activity specified by that ResolveInfo will be the default for Intents like
191      * {@link #mResolveIntent}.
192      *
193      * <p>This code is copied from com.android.internal.app.ResolverActivity.
194      */
buildIntentFilterForResolveInfo(ResolveInfo info)195     private IntentFilter buildIntentFilterForResolveInfo(ResolveInfo info) {
196         // Build a reasonable intent filter, based on what matched.
197         IntentFilter filter = new IntentFilter();
198         Intent filterIntent;
199 
200         if (mResolveIntent.getSelector() != null) {
201             filterIntent = mResolveIntent.getSelector();
202         } else {
203             filterIntent = mResolveIntent;
204         }
205 
206         String action = filterIntent.getAction();
207         if (action != null) {
208             filter.addAction(action);
209         }
210         Set<String> categories = filterIntent.getCategories();
211         if (categories != null) {
212             for (String cat : categories) {
213                 filter.addCategory(cat);
214             }
215         }
216         filter.addCategory(Intent.CATEGORY_DEFAULT);
217 
218         int cat = info.match & IntentFilter.MATCH_CATEGORY_MASK;
219         Uri data = filterIntent.getData();
220         if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
221             String mimeType = filterIntent.resolveType(this);
222             if (mimeType != null) {
223                 try {
224                     filter.addDataType(mimeType);
225                 } catch (IntentFilter.MalformedMimeTypeException e) {
226                     Log.e(TAG, "Could not add data type", e);
227                     filter = null;
228                 }
229             }
230         }
231         if (data != null && data.getScheme() != null) {
232             // We need the data specification if there was no type OR if the scheme is not one of
233             // our magical "file:" or "content:" schemes (see IntentFilter for the reason).
234             if (cat != IntentFilter.MATCH_CATEGORY_TYPE
235                     || (!"file".equals(data.getScheme())
236                     && !"content".equals(data.getScheme()))) {
237                 filter.addDataScheme(data.getScheme());
238 
239                 // Look through the resolved filter to determine which part of it matched the
240                 // original Intent.
241                 Iterator<PatternMatcher> pIt = info.filter.schemeSpecificPartsIterator();
242                 if (pIt != null) {
243                     String ssp = data.getSchemeSpecificPart();
244                     while (ssp != null && pIt.hasNext()) {
245                         PatternMatcher p = pIt.next();
246                         if (p.match(ssp)) {
247                             filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
248                             break;
249                         }
250                     }
251                 }
252                 Iterator<IntentFilter.AuthorityEntry> aIt = info.filter.authoritiesIterator();
253                 if (aIt != null) {
254                     while (aIt.hasNext()) {
255                         IntentFilter.AuthorityEntry a = aIt.next();
256                         if (a.match(data) >= 0) {
257                             int port = a.getPort();
258                             filter.addDataAuthority(a.getHost(),
259                                     port >= 0 ? Integer.toString(port) : null);
260                             break;
261                         }
262                     }
263                 }
264                 pIt = info.filter.pathsIterator();
265                 if (pIt != null) {
266                     String path = data.getPath();
267                     while (path != null && pIt.hasNext()) {
268                         PatternMatcher p = pIt.next();
269                         if (p.match(path)) {
270                             filter.addDataPath(p.getPath(), p.getType());
271                             break;
272                         }
273                     }
274                 }
275             }
276         }
277 
278         return filter;
279     }
280 
281     @Override
onStop()282     protected void onStop() {
283         super.onStop();
284 
285         if ((getIntent().getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()) {
286             // This resolver is in the unusual situation where it has been launched at the top of a
287             // new task.  We don't let it be added to the recent tasks shown to the user, and we
288             // need to make sure that each time we are launched we get the correct launching
289             // uid (not re-using the same resolver from an old launching uid), so we will now
290             // finish since being no longer visible, the user probably can't get back to us.
291             if (!isChangingConfigurations()) {
292                 finish();
293             }
294         }
295     }
296 
297     /**
298      * An enum mapping different Intent actions to the strings that should be displayed that
299      * explain to the user what this ResolverActivity is doing.
300      */
301     private enum ActionTitle {
302         VIEW(Intent.ACTION_VIEW,
303                 R.string.whichViewApplication,
304                 R.string.whichViewApplicationNamed,
305                 R.string.whichViewApplicationLabel),
306         EDIT(Intent.ACTION_EDIT,
307                 R.string.whichEditApplication,
308                 R.string.whichEditApplicationNamed,
309                 R.string.whichEditApplicationLabel),
310         SEND(Intent.ACTION_SEND,
311                 R.string.whichSendApplication,
312                 R.string.whichSendApplicationNamed,
313                 R.string.whichSendApplicationLabel),
314         SENDTO(Intent.ACTION_SENDTO,
315                 R.string.whichSendToApplication,
316                 R.string.whichSendToApplicationNamed,
317                 R.string.whichSendToApplicationLabel),
318         SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE,
319                 R.string.whichSendApplication,
320                 R.string.whichSendApplicationNamed,
321                 R.string.whichSendApplicationLabel),
322         CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE,
323                 R.string.whichImageCaptureApplication,
324                 R.string.whichImageCaptureApplicationNamed,
325                 R.string.whichImageCaptureApplicationLabel),
326         DEFAULT(null,
327                 R.string.whichApplication,
328                 R.string.whichApplicationNamed,
329                 R.string.whichApplicationLabel),
330         HOME(Intent.ACTION_MAIN,
331                 R.string.whichHomeApplication,
332                 R.string.whichHomeApplicationNamed,
333                 R.string.whichHomeApplicationLabel);
334 
335         public final String action;
336         public final int titleRes;
337         public final int namedTitleRes;
338 
339         @StringRes
340         public final int labelRes;
341 
ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes)342         ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) {
343             this.action = action;
344             this.titleRes = titleRes;
345             this.namedTitleRes = namedTitleRes;
346             this.labelRes = labelRes;
347         }
348 
349         /**
350          * Returns a set of Strings that should be used for the given Intent action.
351          */
forAction(String action)352         public static ActionTitle forAction(String action) {
353             for (ActionTitle title : values()) {
354                 if (title != HOME && action != null && action.equals(title.action)) {
355                     return title;
356                 }
357             }
358             return DEFAULT;
359         }
360     }
361 }
362