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