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