1 /* 2 * Copyright (C) 2008 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 17 package com.android.internal.app; 18 19 import static java.lang.annotation.RetentionPolicy.SOURCE; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.AnimatorSet; 24 import android.animation.ObjectAnimator; 25 import android.animation.ValueAnimator; 26 import android.annotation.IntDef; 27 import android.annotation.Nullable; 28 import android.app.Activity; 29 import android.app.ActivityManager; 30 import android.app.prediction.AppPredictionContext; 31 import android.app.prediction.AppPredictionManager; 32 import android.app.prediction.AppPredictor; 33 import android.app.prediction.AppTarget; 34 import android.app.prediction.AppTargetEvent; 35 import android.content.ClipData; 36 import android.content.ClipboardManager; 37 import android.content.ComponentName; 38 import android.content.ContentResolver; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.IntentFilter; 42 import android.content.IntentSender; 43 import android.content.IntentSender.SendIntentException; 44 import android.content.ServiceConnection; 45 import android.content.pm.ActivityInfo; 46 import android.content.pm.ApplicationInfo; 47 import android.content.pm.LabeledIntent; 48 import android.content.pm.LauncherApps; 49 import android.content.pm.PackageManager; 50 import android.content.pm.PackageManager.NameNotFoundException; 51 import android.content.pm.ResolveInfo; 52 import android.content.pm.ShortcutInfo; 53 import android.content.pm.ShortcutManager; 54 import android.content.res.Configuration; 55 import android.database.Cursor; 56 import android.database.DataSetObserver; 57 import android.graphics.Bitmap; 58 import android.graphics.Canvas; 59 import android.graphics.Color; 60 import android.graphics.Paint; 61 import android.graphics.Path; 62 import android.graphics.drawable.AnimatedVectorDrawable; 63 import android.graphics.drawable.BitmapDrawable; 64 import android.graphics.drawable.Drawable; 65 import android.graphics.drawable.Icon; 66 import android.metrics.LogMaker; 67 import android.net.Uri; 68 import android.os.AsyncTask; 69 import android.os.Bundle; 70 import android.os.Handler; 71 import android.os.IBinder; 72 import android.os.Message; 73 import android.os.Parcelable; 74 import android.os.Process; 75 import android.os.RemoteException; 76 import android.os.ResultReceiver; 77 import android.os.UserHandle; 78 import android.os.UserManager; 79 import android.provider.DeviceConfig; 80 import android.provider.DocumentsContract; 81 import android.provider.Downloads; 82 import android.provider.OpenableColumns; 83 import android.service.chooser.ChooserTarget; 84 import android.service.chooser.ChooserTargetService; 85 import android.service.chooser.IChooserTargetResult; 86 import android.service.chooser.IChooserTargetService; 87 import android.text.SpannableStringBuilder; 88 import android.text.TextUtils; 89 import android.util.AttributeSet; 90 import android.util.HashedStringCache; 91 import android.util.Log; 92 import android.util.Size; 93 import android.util.Slog; 94 import android.view.LayoutInflater; 95 import android.view.View; 96 import android.view.View.MeasureSpec; 97 import android.view.View.OnClickListener; 98 import android.view.View.OnLongClickListener; 99 import android.view.ViewGroup; 100 import android.view.ViewGroup.LayoutParams; 101 import android.view.animation.AccelerateInterpolator; 102 import android.view.animation.DecelerateInterpolator; 103 import android.widget.AbsListView; 104 import android.widget.BaseAdapter; 105 import android.widget.ImageView; 106 import android.widget.ListView; 107 import android.widget.TextView; 108 import android.widget.Toast; 109 110 import com.android.internal.R; 111 import com.android.internal.annotations.VisibleForTesting; 112 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 113 import com.android.internal.content.PackageMonitor; 114 import com.android.internal.logging.MetricsLogger; 115 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 116 import com.android.internal.util.ImageUtils; 117 import com.android.internal.widget.ResolverDrawerLayout; 118 119 import com.google.android.collect.Lists; 120 121 import java.io.IOException; 122 import java.lang.annotation.Retention; 123 import java.text.Collator; 124 import java.util.ArrayList; 125 import java.util.Arrays; 126 import java.util.Collections; 127 import java.util.Comparator; 128 import java.util.HashMap; 129 import java.util.HashSet; 130 import java.util.List; 131 import java.util.Map; 132 import java.util.Set; 133 134 /** 135 * The Chooser Activity handles intent resolution specifically for sharing intents - 136 * for example, those generated by @see android.content.Intent#createChooser(Intent, CharSequence). 137 * 138 */ 139 public class ChooserActivity extends ResolverActivity { 140 private static final String TAG = "ChooserActivity"; 141 142 143 /** 144 * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself 145 * in onStop when launched in a new task. If this extra is set to true, we do not finish 146 * ourselves when onStop gets called. 147 */ 148 public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP 149 = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP"; 150 151 private static final String PREF_NUM_SHEET_EXPANSIONS = "pref_num_sheet_expansions"; 152 153 private static final boolean DEBUG = false; 154 155 /** 156 * If {@link #USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS} and this is set to true, 157 * {@link AppPredictionManager} will be queried for direct share targets. 158 */ 159 // TODO(b/123089490): Replace with system flag 160 private static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true; 161 private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true; 162 // TODO(b/123088566) Share these in a better way. 163 private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share"; 164 public static final String LAUNCH_LOCATON_DIRECT_SHARE = "direct_share"; 165 private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20; 166 public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter"; 167 private AppPredictor mAppPredictor; 168 private AppPredictor.Callback mAppPredictorCallback; 169 private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache; 170 171 /** 172 * If set to true, use ShortcutManager to retrieve the matching direct share targets, instead of 173 * binding to every ChooserTargetService implementation. 174 */ 175 // TODO(b/121287573): Replace with a system flag (setprop?) 176 private static final boolean USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS = true; 177 private static final boolean USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS = true; 178 179 /** 180 * The transition time between placeholders for direct share to a message 181 * indicating that non are available. 182 */ 183 private static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200; 184 185 private static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f; 186 187 // TODO(b/121287224): Re-evaluate this limit 188 private static final int SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20; 189 190 private static final int QUERY_TARGET_SERVICE_LIMIT = 5; 191 192 private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7; 193 private int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI, 194 SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS, 195 DEFAULT_SALT_EXPIRATION_DAYS); 196 197 private Bundle mReplacementExtras; 198 private IntentSender mChosenComponentSender; 199 private IntentSender mRefinementIntentSender; 200 private RefinementResultReceiver mRefinementResultReceiver; 201 private ChooserTarget[] mCallerChooserTargets; 202 private ComponentName[] mFilteredComponentNames; 203 204 private Intent mReferrerFillInIntent; 205 206 private long mChooserShownTime; 207 protected boolean mIsSuccessfullySelected; 208 209 private long mQueriedTargetServicesTimeMs; 210 private long mQueriedSharingShortcutsTimeMs; 211 212 private ChooserListAdapter mChooserListAdapter; 213 private ChooserRowAdapter mChooserRowAdapter; 214 private int mChooserRowServiceSpacing; 215 216 private int mCurrAvailableWidth = 0; 217 218 /** {@link ChooserActivity#getBaseScore} */ 219 private static final float CALLER_TARGET_SCORE_BOOST = 900.f; 220 /** {@link ChooserActivity#getBaseScore} */ 221 private static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; 222 private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment"; 223 // TODO: Update to handle landscape instead of using static value 224 private static final int MAX_RANKED_TARGETS = 4; 225 226 private final List<ChooserTargetServiceConnection> mServiceConnections = new ArrayList<>(); 227 private final Set<ComponentName> mServicesRequested = new HashSet<>(); 228 229 private static final int MAX_LOG_RANK_POSITION = 12; 230 231 @VisibleForTesting 232 public static final int LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS = 250; 233 234 private static final int MAX_EXTRA_INITIAL_INTENTS = 2; 235 private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; 236 237 private boolean mListViewDataChanged = false; 238 239 @Retention(SOURCE) 240 @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT}) 241 private @interface ContentPreviewType { 242 } 243 244 // Starting at 1 since 0 is considered "undefined" for some of the database transformations 245 // of tron logs. 246 private static final int CONTENT_PREVIEW_IMAGE = 1; 247 private static final int CONTENT_PREVIEW_FILE = 2; 248 private static final int CONTENT_PREVIEW_TEXT = 3; 249 protected MetricsLogger mMetricsLogger; 250 251 // Sorted list of DisplayResolveInfos for the alphabetical app section. 252 private List<ResolverActivity.DisplayResolveInfo> mSortedList = new ArrayList<>(); 253 254 private ContentPreviewCoordinator mPreviewCoord; 255 256 private class ContentPreviewCoordinator { 257 private static final int IMAGE_FADE_IN_MILLIS = 150; 258 private static final int IMAGE_LOAD_TIMEOUT = 1; 259 private static final int IMAGE_LOAD_INTO_VIEW = 2; 260 261 private final int mImageLoadTimeoutMillis = 262 getResources().getInteger(R.integer.config_shortAnimTime); 263 264 private final View mParentView; 265 private boolean mHideParentOnFail; 266 private boolean mAtLeastOneLoaded = false; 267 268 class LoadUriTask { 269 public final Uri mUri; 270 public final int mImageResourceId; 271 public final int mExtraCount; 272 public final Bitmap mBmp; 273 LoadUriTask(int imageResourceId, Uri uri, int extraCount, Bitmap bmp)274 LoadUriTask(int imageResourceId, Uri uri, int extraCount, Bitmap bmp) { 275 this.mImageResourceId = imageResourceId; 276 this.mUri = uri; 277 this.mExtraCount = extraCount; 278 this.mBmp = bmp; 279 } 280 } 281 282 // If at least one image loads within the timeout period, allow other 283 // loads to continue. Otherwise terminate and optionally hide 284 // the parent area 285 private final Handler mHandler = new Handler() { 286 @Override 287 public void handleMessage(Message msg) { 288 switch (msg.what) { 289 case IMAGE_LOAD_TIMEOUT: 290 maybeHideContentPreview(); 291 break; 292 293 case IMAGE_LOAD_INTO_VIEW: 294 if (isFinishing()) break; 295 296 LoadUriTask task = (LoadUriTask) msg.obj; 297 RoundedRectImageView imageView = mParentView.findViewById( 298 task.mImageResourceId); 299 if (task.mBmp == null) { 300 imageView.setVisibility(View.GONE); 301 maybeHideContentPreview(); 302 return; 303 } 304 305 mAtLeastOneLoaded = true; 306 imageView.setVisibility(View.VISIBLE); 307 imageView.setAlpha(0.0f); 308 imageView.setImageBitmap(task.mBmp); 309 310 ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 311 1.0f); 312 fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f)); 313 fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS); 314 fadeAnim.start(); 315 316 if (task.mExtraCount > 0) { 317 imageView.setExtraImageCount(task.mExtraCount); 318 } 319 } 320 } 321 }; 322 ContentPreviewCoordinator(View parentView, boolean hideParentOnFail)323 ContentPreviewCoordinator(View parentView, boolean hideParentOnFail) { 324 super(); 325 326 this.mParentView = parentView; 327 this.mHideParentOnFail = hideParentOnFail; 328 } 329 loadUriIntoView(final int imageResourceId, final Uri uri, final int extraImages)330 private void loadUriIntoView(final int imageResourceId, final Uri uri, 331 final int extraImages) { 332 mHandler.sendEmptyMessageDelayed(IMAGE_LOAD_TIMEOUT, mImageLoadTimeoutMillis); 333 334 AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { 335 final Bitmap bmp = loadThumbnail(uri, new Size(200, 200)); 336 final Message msg = Message.obtain(); 337 msg.what = IMAGE_LOAD_INTO_VIEW; 338 msg.obj = new LoadUriTask(imageResourceId, uri, extraImages, bmp); 339 mHandler.sendMessage(msg); 340 }); 341 } 342 cancelLoads()343 private void cancelLoads() { 344 mHandler.removeMessages(IMAGE_LOAD_INTO_VIEW); 345 mHandler.removeMessages(IMAGE_LOAD_TIMEOUT); 346 } 347 maybeHideContentPreview()348 private void maybeHideContentPreview() { 349 if (!mAtLeastOneLoaded && mHideParentOnFail) { 350 Log.i(TAG, "Hiding image preview area. Timed out waiting for preview to load" 351 + " within " + mImageLoadTimeoutMillis + "ms."); 352 collapseParentView(); 353 if (mChooserRowAdapter != null) { 354 mChooserRowAdapter.hideContentPreview(); 355 } 356 mHideParentOnFail = false; 357 } 358 } 359 collapseParentView()360 private void collapseParentView() { 361 // This will effectively hide the content preview row by forcing the height 362 // to zero. It is faster than forcing a relayout of the listview 363 final View v = mParentView; 364 int widthSpec = MeasureSpec.makeMeasureSpec(v.getWidth(), MeasureSpec.EXACTLY); 365 int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY); 366 v.measure(widthSpec, heightSpec); 367 v.getLayoutParams().height = 0; 368 v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getTop()); 369 v.invalidate(); 370 } 371 } 372 373 private final ChooserHandler mChooserHandler = new ChooserHandler(); 374 375 private class ChooserHandler extends Handler { 376 private static final int CHOOSER_TARGET_SERVICE_RESULT = 1; 377 private static final int CHOOSER_TARGET_SERVICE_WATCHDOG_MIN_TIMEOUT = 2; 378 private static final int CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT = 3; 379 private static final int SHORTCUT_MANAGER_SHARE_TARGET_RESULT = 4; 380 private static final int SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED = 5; 381 private static final int LIST_VIEW_UPDATE_MESSAGE = 6; 382 383 private static final int WATCHDOG_TIMEOUT_MAX_MILLIS = 10000; 384 private static final int WATCHDOG_TIMEOUT_MIN_MILLIS = 3000; 385 386 private boolean mMinTimeoutPassed = false; 387 removeAllMessages()388 private void removeAllMessages() { 389 removeMessages(LIST_VIEW_UPDATE_MESSAGE); 390 removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_MIN_TIMEOUT); 391 removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT); 392 removeMessages(CHOOSER_TARGET_SERVICE_RESULT); 393 removeMessages(SHORTCUT_MANAGER_SHARE_TARGET_RESULT); 394 removeMessages(SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED); 395 } 396 restartServiceRequestTimer()397 private void restartServiceRequestTimer() { 398 mMinTimeoutPassed = false; 399 removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_MIN_TIMEOUT); 400 removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT); 401 402 if (DEBUG) { 403 Log.d(TAG, "queryTargets setting watchdog timer for " 404 + WATCHDOG_TIMEOUT_MIN_MILLIS + "-" 405 + WATCHDOG_TIMEOUT_MAX_MILLIS + "ms"); 406 } 407 408 sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_MIN_TIMEOUT, 409 WATCHDOG_TIMEOUT_MIN_MILLIS); 410 sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT, 411 WATCHDOG_TIMEOUT_MAX_MILLIS); 412 } 413 maybeStopServiceRequestTimer()414 private void maybeStopServiceRequestTimer() { 415 // Set a minimum timeout threshold, to ensure both apis, sharing shortcuts 416 // and older-style direct share services, have had time to load, otherwise 417 // just checking mServiceConnections could force us to end prematurely 418 if (mMinTimeoutPassed && mServiceConnections.isEmpty()) { 419 logDirectShareTargetReceived( 420 MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_CHOOSER_SERVICE); 421 sendVoiceChoicesIfNeeded(); 422 mChooserListAdapter.completeServiceTargetLoading(); 423 } 424 } 425 426 @Override handleMessage(Message msg)427 public void handleMessage(Message msg) { 428 if (mChooserListAdapter == null || isDestroyed()) { 429 return; 430 } 431 432 switch (msg.what) { 433 case CHOOSER_TARGET_SERVICE_RESULT: 434 if (DEBUG) Log.d(TAG, "CHOOSER_TARGET_SERVICE_RESULT"); 435 final ServiceResultInfo sri = (ServiceResultInfo) msg.obj; 436 if (!mServiceConnections.contains(sri.connection)) { 437 Log.w(TAG, "ChooserTargetServiceConnection " + sri.connection 438 + " returned after being removed from active connections." 439 + " Have you considered returning results faster?"); 440 break; 441 } 442 if (sri.resultTargets != null) { 443 mChooserListAdapter.addServiceResults(sri.originalTarget, 444 sri.resultTargets, false); 445 } 446 unbindService(sri.connection); 447 sri.connection.destroy(); 448 mServiceConnections.remove(sri.connection); 449 maybeStopServiceRequestTimer(); 450 break; 451 452 case CHOOSER_TARGET_SERVICE_WATCHDOG_MIN_TIMEOUT: 453 mMinTimeoutPassed = true; 454 maybeStopServiceRequestTimer(); 455 break; 456 457 case CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT: 458 unbindRemainingServices(); 459 maybeStopServiceRequestTimer(); 460 break; 461 462 case LIST_VIEW_UPDATE_MESSAGE: 463 if (DEBUG) { 464 Log.d(TAG, "LIST_VIEW_UPDATE_MESSAGE; "); 465 } 466 467 mChooserListAdapter.refreshListView(); 468 break; 469 470 case SHORTCUT_MANAGER_SHARE_TARGET_RESULT: 471 if (DEBUG) Log.d(TAG, "SHORTCUT_MANAGER_SHARE_TARGET_RESULT"); 472 final ServiceResultInfo resultInfo = (ServiceResultInfo) msg.obj; 473 if (resultInfo.resultTargets != null) { 474 mChooserListAdapter.addServiceResults(resultInfo.originalTarget, 475 resultInfo.resultTargets, true); 476 } 477 break; 478 479 case SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED: 480 logDirectShareTargetReceived( 481 MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER); 482 sendVoiceChoicesIfNeeded(); 483 break; 484 485 default: 486 super.handleMessage(msg); 487 } 488 } 489 }; 490 491 @Override onCreate(Bundle savedInstanceState)492 protected void onCreate(Bundle savedInstanceState) { 493 final long intentReceivedTime = System.currentTimeMillis(); 494 mIsSuccessfullySelected = false; 495 Intent intent = getIntent(); 496 Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT); 497 if (!(targetParcelable instanceof Intent)) { 498 Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable); 499 finish(); 500 super.onCreate(null); 501 return; 502 } 503 Intent target = (Intent) targetParcelable; 504 if (target != null) { 505 modifyTargetIntent(target); 506 } 507 Parcelable[] targetsParcelable 508 = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS); 509 if (targetsParcelable != null) { 510 final boolean offset = target == null; 511 Intent[] additionalTargets = 512 new Intent[offset ? targetsParcelable.length - 1 : targetsParcelable.length]; 513 for (int i = 0; i < targetsParcelable.length; i++) { 514 if (!(targetsParcelable[i] instanceof Intent)) { 515 Log.w(TAG, "EXTRA_ALTERNATE_INTENTS array entry #" + i + " is not an Intent: " 516 + targetsParcelable[i]); 517 finish(); 518 super.onCreate(null); 519 return; 520 } 521 final Intent additionalTarget = (Intent) targetsParcelable[i]; 522 if (i == 0 && target == null) { 523 target = additionalTarget; 524 modifyTargetIntent(target); 525 } else { 526 additionalTargets[offset ? i - 1 : i] = additionalTarget; 527 modifyTargetIntent(additionalTarget); 528 } 529 } 530 setAdditionalTargets(additionalTargets); 531 } 532 533 mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS); 534 535 // Do not allow the title to be changed when sharing content 536 CharSequence title = null; 537 if (target != null) { 538 if (!isSendAction(target)) { 539 title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE); 540 } else { 541 Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a" 542 + " preview title by using EXTRA_TITLE property of the wrapped" 543 + " EXTRA_INTENT."); 544 } 545 } 546 547 int defaultTitleRes = 0; 548 if (title == null) { 549 defaultTitleRes = com.android.internal.R.string.chooseActivity; 550 } 551 552 Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS); 553 Intent[] initialIntents = null; 554 if (pa != null) { 555 int count = Math.min(pa.length, MAX_EXTRA_INITIAL_INTENTS); 556 initialIntents = new Intent[count]; 557 for (int i = 0; i < count; i++) { 558 if (!(pa[i] instanceof Intent)) { 559 Log.w(TAG, "Initial intent #" + i + " not an Intent: " + pa[i]); 560 finish(); 561 super.onCreate(null); 562 return; 563 } 564 final Intent in = (Intent) pa[i]; 565 modifyTargetIntent(in); 566 initialIntents[i] = in; 567 } 568 } 569 570 mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, getReferrer()); 571 572 mChosenComponentSender = intent.getParcelableExtra( 573 Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER); 574 mRefinementIntentSender = intent.getParcelableExtra( 575 Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); 576 setSafeForwardingMode(true); 577 578 pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS); 579 if (pa != null) { 580 ComponentName[] names = new ComponentName[pa.length]; 581 for (int i = 0; i < pa.length; i++) { 582 if (!(pa[i] instanceof ComponentName)) { 583 Log.w(TAG, "Filtered component #" + i + " not a ComponentName: " + pa[i]); 584 names = null; 585 break; 586 } 587 names[i] = (ComponentName) pa[i]; 588 } 589 mFilteredComponentNames = names; 590 } 591 592 pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS); 593 if (pa != null) { 594 int count = Math.min(pa.length, MAX_EXTRA_CHOOSER_TARGETS); 595 ChooserTarget[] targets = new ChooserTarget[count]; 596 for (int i = 0; i < count; i++) { 597 if (!(pa[i] instanceof ChooserTarget)) { 598 Log.w(TAG, "Chooser target #" + i + " not a ChooserTarget: " + pa[i]); 599 targets = null; 600 break; 601 } 602 targets[i] = (ChooserTarget) pa[i]; 603 } 604 mCallerChooserTargets = targets; 605 } 606 607 setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false)); 608 super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents, 609 null, false); 610 611 mChooserShownTime = System.currentTimeMillis(); 612 final long systemCost = mChooserShownTime - intentReceivedTime; 613 614 getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN) 615 .setSubtype(isWorkProfile() ? MetricsEvent.MANAGED_PROFILE : 616 MetricsEvent.PARENT_PROFILE) 617 .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, target.getType()) 618 .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost)); 619 620 AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(); 621 if (appPredictor != null) { 622 mDirectShareAppTargetCache = new HashMap<>(); 623 mAppPredictorCallback = resultList -> { 624 if (isFinishing() || isDestroyed()) { 625 return; 626 } 627 // May be null if there are no apps to perform share/open action. 628 if (mChooserListAdapter == null) { 629 return; 630 } 631 if (resultList.isEmpty()) { 632 // APS may be disabled, so try querying targets ourselves. 633 queryDirectShareTargets(mChooserListAdapter, true); 634 return; 635 } 636 final List<DisplayResolveInfo> driList = 637 getDisplayResolveInfos(mChooserListAdapter); 638 final List<ShortcutManager.ShareShortcutInfo> shareShortcutInfos = 639 new ArrayList<>(); 640 for (AppTarget appTarget : resultList) { 641 if (appTarget.getShortcutInfo() == null) { 642 continue; 643 } 644 shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo( 645 appTarget.getShortcutInfo(), 646 new ComponentName( 647 appTarget.getPackageName(), appTarget.getClassName()))); 648 } 649 sendShareShortcutInfoList(shareShortcutInfos, driList, resultList); 650 }; 651 appPredictor 652 .registerPredictionUpdates(this.getMainExecutor(), mAppPredictorCallback); 653 } 654 655 mChooserRowServiceSpacing = getResources() 656 .getDimensionPixelSize(R.dimen.chooser_service_spacing); 657 658 if (mResolverDrawerLayout != null) { 659 mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); 660 661 // expand/shrink direct share 4 -> 8 viewgroup 662 if (isSendAction(target)) { 663 mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll); 664 } 665 666 final View chooserHeader = mResolverDrawerLayout.findViewById(R.id.chooser_header); 667 final float defaultElevation = chooserHeader.getElevation(); 668 final float chooserHeaderScrollElevation = 669 getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation); 670 671 mAdapterView.setOnScrollListener(new AbsListView.OnScrollListener() { 672 public void onScrollStateChanged(AbsListView view, int scrollState) { 673 } 674 675 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 676 int totalItemCount) { 677 if (view.getChildCount() > 0) { 678 if (firstVisibleItem > 0 || view.getChildAt(0).getTop() < 0) { 679 chooserHeader.setElevation(chooserHeaderScrollElevation); 680 return; 681 } 682 } 683 684 chooserHeader.setElevation(defaultElevation); 685 } 686 }); 687 688 mResolverDrawerLayout.setOnCollapsedChangedListener( 689 new ResolverDrawerLayout.OnCollapsedChangedListener() { 690 691 // Only consider one expansion per activity creation 692 private boolean mWrittenOnce = false; 693 694 @Override 695 public void onCollapsedChanged(boolean isCollapsed) { 696 if (!isCollapsed && !mWrittenOnce) { 697 incrementNumSheetExpansions(); 698 mWrittenOnce = true; 699 } 700 } 701 }); 702 } 703 704 if (DEBUG) { 705 Log.d(TAG, "System Time Cost is " + systemCost); 706 } 707 } 708 709 /** 710 * Check if the profile currently used is a work profile. 711 * @return true if it is work profile, false if it is parent profile (or no work profile is 712 * set up) 713 */ isWorkProfile()714 protected boolean isWorkProfile() { 715 return ((UserManager) getSystemService(Context.USER_SERVICE)) 716 .getUserInfo(UserHandle.myUserId()).isManagedProfile(); 717 } 718 719 @Override createPackageMonitor()720 protected PackageMonitor createPackageMonitor() { 721 return new PackageMonitor() { 722 @Override 723 public void onSomePackagesChanged() { 724 mAdapter.handlePackagesChanged(); 725 bindProfileView(); 726 } 727 }; 728 } 729 730 private void onCopyButtonClicked(View v) { 731 Intent targetIntent = getTargetIntent(); 732 if (targetIntent == null) { 733 finish(); 734 } else { 735 final String action = targetIntent.getAction(); 736 737 ClipData clipData = null; 738 if (Intent.ACTION_SEND.equals(action)) { 739 String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT); 740 Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); 741 742 if (extraText != null) { 743 clipData = ClipData.newPlainText(null, extraText); 744 } else if (extraStream != null) { 745 clipData = ClipData.newUri(getContentResolver(), null, extraStream); 746 } else { 747 Log.w(TAG, "No data available to copy to clipboard"); 748 return; 749 } 750 } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { 751 final ArrayList<Uri> streams = targetIntent.getParcelableArrayListExtra( 752 Intent.EXTRA_STREAM); 753 clipData = ClipData.newUri(getContentResolver(), null, streams.get(0)); 754 for (int i = 1; i < streams.size(); i++) { 755 clipData.addItem(getContentResolver(), new ClipData.Item(streams.get(i))); 756 } 757 } else { 758 // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE 759 // so warn about unexpected action 760 Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard"); 761 return; 762 } 763 764 ClipboardManager clipboardManager = (ClipboardManager) getSystemService( 765 Context.CLIPBOARD_SERVICE); 766 clipboardManager.setPrimaryClip(clipData); 767 Toast.makeText(getApplicationContext(), R.string.copied, Toast.LENGTH_SHORT).show(); 768 769 finish(); 770 } 771 } 772 773 @Override 774 public void onConfigurationChanged(Configuration newConfig) { 775 super.onConfigurationChanged(newConfig); 776 777 adjustPreviewWidth(newConfig.orientation, null); 778 } 779 780 private boolean shouldDisplayLandscape(int orientation) { 781 // Sharesheet fixes the # of items per row and therefore can not correctly lay out 782 // when in the restricted size of multi-window mode. In the future, would be nice 783 // to use minimum dp size requirements instead 784 return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode(); 785 } 786 787 private void adjustPreviewWidth(int orientation, View parent) { 788 int width = -1; 789 if (shouldDisplayLandscape(orientation)) { 790 width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width); 791 } 792 793 parent = parent == null ? getWindow().getDecorView() : parent; 794 795 updateLayoutWidth(R.id.content_preview_text_layout, width, parent); 796 updateLayoutWidth(R.id.content_preview_title_layout, width, parent); 797 updateLayoutWidth(R.id.content_preview_file_layout, width, parent); 798 } 799 800 private void updateLayoutWidth(int layoutResourceId, int width, View parent) { 801 View view = parent.findViewById(layoutResourceId); 802 if (view != null && view.getLayoutParams() != null) { 803 LayoutParams params = view.getLayoutParams(); 804 params.width = width; 805 view.setLayoutParams(params); 806 } 807 } 808 809 private ViewGroup displayContentPreview(@ContentPreviewType int previewType, 810 Intent targetIntent, LayoutInflater layoutInflater, ViewGroup convertView, 811 ViewGroup parent) { 812 if (convertView != null) return convertView; 813 814 ViewGroup layout = null; 815 816 switch (previewType) { 817 case CONTENT_PREVIEW_TEXT: 818 layout = displayTextContentPreview(targetIntent, layoutInflater, parent); 819 break; 820 case CONTENT_PREVIEW_IMAGE: 821 layout = displayImageContentPreview(targetIntent, layoutInflater, parent); 822 break; 823 case CONTENT_PREVIEW_FILE: 824 layout = displayFileContentPreview(targetIntent, layoutInflater, parent); 825 break; 826 default: 827 Log.e(TAG, "Unexpected content preview type: " + previewType); 828 } 829 830 if (layout != null) { 831 adjustPreviewWidth(getResources().getConfiguration().orientation, layout); 832 } 833 834 return layout; 835 } 836 837 private ViewGroup displayTextContentPreview(Intent targetIntent, LayoutInflater layoutInflater, 838 ViewGroup parent) { 839 ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( 840 R.layout.chooser_grid_preview_text, parent, false); 841 842 contentPreviewLayout.findViewById(R.id.copy_button).setOnClickListener( 843 this::onCopyButtonClicked); 844 845 CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); 846 if (sharingText == null) { 847 contentPreviewLayout.findViewById(R.id.content_preview_text_layout).setVisibility( 848 View.GONE); 849 } else { 850 TextView textView = contentPreviewLayout.findViewById(R.id.content_preview_text); 851 textView.setText(sharingText); 852 } 853 854 String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); 855 if (TextUtils.isEmpty(previewTitle)) { 856 contentPreviewLayout.findViewById(R.id.content_preview_title_layout).setVisibility( 857 View.GONE); 858 } else { 859 TextView previewTitleView = contentPreviewLayout.findViewById( 860 R.id.content_preview_title); 861 previewTitleView.setText(previewTitle); 862 863 ClipData previewData = targetIntent.getClipData(); 864 Uri previewThumbnail = null; 865 if (previewData != null) { 866 if (previewData.getItemCount() > 0) { 867 ClipData.Item previewDataItem = previewData.getItemAt(0); 868 previewThumbnail = previewDataItem.getUri(); 869 } 870 } 871 872 ImageView previewThumbnailView = contentPreviewLayout.findViewById( 873 R.id.content_preview_thumbnail); 874 if (previewThumbnail == null) { 875 previewThumbnailView.setVisibility(View.GONE); 876 } else { 877 mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false); 878 mPreviewCoord.loadUriIntoView(R.id.content_preview_thumbnail, previewThumbnail, 0); 879 } 880 } 881 882 return contentPreviewLayout; 883 } 884 885 private ViewGroup displayImageContentPreview(Intent targetIntent, LayoutInflater layoutInflater, 886 ViewGroup parent) { 887 ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( 888 R.layout.chooser_grid_preview_image, parent, false); 889 mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, true); 890 891 String action = targetIntent.getAction(); 892 if (Intent.ACTION_SEND.equals(action)) { 893 Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); 894 mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, uri, 0); 895 } else { 896 ContentResolver resolver = getContentResolver(); 897 898 List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); 899 List<Uri> imageUris = new ArrayList<>(); 900 for (Uri uri : uris) { 901 if (isImageType(resolver.getType(uri))) { 902 imageUris.add(uri); 903 } 904 } 905 906 if (imageUris.size() == 0) { 907 Log.i(TAG, "Attempted to display image preview area with zero" 908 + " available images detected in EXTRA_STREAM list"); 909 contentPreviewLayout.setVisibility(View.GONE); 910 return contentPreviewLayout; 911 } 912 913 mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, imageUris.get(0), 0); 914 915 if (imageUris.size() == 2) { 916 mPreviewCoord.loadUriIntoView(R.id.content_preview_image_2_large, 917 imageUris.get(1), 0); 918 } else if (imageUris.size() > 2) { 919 mPreviewCoord.loadUriIntoView(R.id.content_preview_image_2_small, 920 imageUris.get(1), 0); 921 mPreviewCoord.loadUriIntoView(R.id.content_preview_image_3_small, 922 imageUris.get(2), imageUris.size() - 3); 923 } 924 } 925 926 return contentPreviewLayout; 927 } 928 929 private static class FileInfo { 930 public final String name; 931 public final boolean hasThumbnail; 932 933 FileInfo(String name, boolean hasThumbnail) { 934 this.name = name; 935 this.hasThumbnail = hasThumbnail; 936 } 937 } 938 939 /** 940 * Wrapping the ContentResolver call to expose for easier mocking, 941 * and to avoid mocking Android core classes. 942 */ 943 @VisibleForTesting 944 public Cursor queryResolver(ContentResolver resolver, Uri uri) { 945 return resolver.query(uri, null, null, null, null); 946 } 947 948 private FileInfo extractFileInfo(Uri uri, ContentResolver resolver) { 949 String fileName = null; 950 boolean hasThumbnail = false; 951 952 try (Cursor cursor = queryResolver(resolver, uri)) { 953 if (cursor != null && cursor.getCount() > 0) { 954 int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); 955 int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE); 956 int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS); 957 958 cursor.moveToFirst(); 959 if (nameIndex != -1) { 960 fileName = cursor.getString(nameIndex); 961 } else if (titleIndex != -1) { 962 fileName = cursor.getString(titleIndex); 963 } 964 965 if (flagsIndex != -1) { 966 hasThumbnail = (cursor.getInt(flagsIndex) 967 & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0; 968 } 969 } 970 } catch (SecurityException | NullPointerException e) { 971 logContentPreviewWarning(uri); 972 } 973 974 if (TextUtils.isEmpty(fileName)) { 975 fileName = uri.getPath(); 976 int index = fileName.lastIndexOf('/'); 977 if (index != -1) { 978 fileName = fileName.substring(index + 1); 979 } 980 } 981 982 return new FileInfo(fileName, hasThumbnail); 983 } 984 985 private void logContentPreviewWarning(Uri uri) { 986 // The ContentResolver already logs the exception. Log something more informative. 987 Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If " 988 + "desired, consider using Intent#createChooser to launch the ChooserActivity, " 989 + "and set your Intent's clipData and flags in accordance with that method's " 990 + "documentation"); 991 } 992 993 private ViewGroup displayFileContentPreview(Intent targetIntent, LayoutInflater layoutInflater, 994 ViewGroup parent) { 995 996 ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( 997 R.layout.chooser_grid_preview_file, parent, false); 998 999 // TODO(b/120417119): Disable file copy until after moving to sysui, 1000 // due to permissions issues 1001 contentPreviewLayout.findViewById(R.id.file_copy_button).setVisibility(View.GONE); 1002 1003 String action = targetIntent.getAction(); 1004 if (Intent.ACTION_SEND.equals(action)) { 1005 Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); 1006 loadFileUriIntoView(uri, contentPreviewLayout); 1007 } else { 1008 List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); 1009 int uriCount = uris.size(); 1010 1011 if (uriCount == 0) { 1012 contentPreviewLayout.setVisibility(View.GONE); 1013 Log.i(TAG, 1014 "Appears to be no uris available in EXTRA_STREAM, removing " 1015 + "preview area"); 1016 return contentPreviewLayout; 1017 } else if (uriCount == 1) { 1018 loadFileUriIntoView(uris.get(0), contentPreviewLayout); 1019 } else { 1020 FileInfo fileInfo = extractFileInfo(uris.get(0), getContentResolver()); 1021 int remUriCount = uriCount - 1; 1022 String fileName = getResources().getQuantityString(R.plurals.file_count, 1023 remUriCount, fileInfo.name, remUriCount); 1024 1025 TextView fileNameView = contentPreviewLayout.findViewById( 1026 R.id.content_preview_filename); 1027 fileNameView.setText(fileName); 1028 1029 View thumbnailView = contentPreviewLayout.findViewById( 1030 R.id.content_preview_file_thumbnail); 1031 thumbnailView.setVisibility(View.GONE); 1032 1033 ImageView fileIconView = contentPreviewLayout.findViewById( 1034 R.id.content_preview_file_icon); 1035 fileIconView.setVisibility(View.VISIBLE); 1036 fileIconView.setImageResource(R.drawable.ic_file_copy); 1037 } 1038 } 1039 1040 return contentPreviewLayout; 1041 } 1042 1043 private void loadFileUriIntoView(final Uri uri, final View parent) { 1044 FileInfo fileInfo = extractFileInfo(uri, getContentResolver()); 1045 1046 TextView fileNameView = parent.findViewById(R.id.content_preview_filename); 1047 fileNameView.setText(fileInfo.name); 1048 1049 if (fileInfo.hasThumbnail) { 1050 mPreviewCoord = new ContentPreviewCoordinator(parent, false); 1051 mPreviewCoord.loadUriIntoView(R.id.content_preview_file_thumbnail, uri, 0); 1052 } else { 1053 View thumbnailView = parent.findViewById(R.id.content_preview_file_thumbnail); 1054 thumbnailView.setVisibility(View.GONE); 1055 1056 ImageView fileIconView = parent.findViewById(R.id.content_preview_file_icon); 1057 fileIconView.setVisibility(View.VISIBLE); 1058 fileIconView.setImageResource(R.drawable.chooser_file_generic); 1059 } 1060 } 1061 1062 @VisibleForTesting 1063 protected boolean isImageType(String mimeType) { 1064 return mimeType != null && mimeType.startsWith("image/"); 1065 } 1066 1067 @ContentPreviewType 1068 private int findPreferredContentPreview(Uri uri, ContentResolver resolver) { 1069 if (uri == null) { 1070 return CONTENT_PREVIEW_TEXT; 1071 } 1072 1073 String mimeType = resolver.getType(uri); 1074 return isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE; 1075 } 1076 1077 /** 1078 * In {@link android.content.Intent#getType}, the app may specify a very general 1079 * mime-type that broadly covers all data being shared, such as {@literal *}/* 1080 * when sending an image and text. We therefore should inspect each item for the 1081 * the preferred type, in order of IMAGE, FILE, TEXT. 1082 */ 1083 @ContentPreviewType 1084 private int findPreferredContentPreview(Intent targetIntent, ContentResolver resolver) { 1085 String action = targetIntent.getAction(); 1086 if (Intent.ACTION_SEND.equals(action)) { 1087 Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); 1088 return findPreferredContentPreview(uri, resolver); 1089 } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { 1090 List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); 1091 if (uris == null || uris.isEmpty()) { 1092 return CONTENT_PREVIEW_TEXT; 1093 } 1094 1095 for (Uri uri : uris) { 1096 // Defaulting to file preview when there are mixed image/file types is 1097 // preferable, as it shows the user the correct number of items being shared 1098 if (findPreferredContentPreview(uri, resolver) == CONTENT_PREVIEW_FILE) { 1099 return CONTENT_PREVIEW_FILE; 1100 } 1101 } 1102 1103 return CONTENT_PREVIEW_IMAGE; 1104 } 1105 1106 return CONTENT_PREVIEW_TEXT; 1107 } 1108 1109 private int getNumSheetExpansions() { 1110 return getPreferences(Context.MODE_PRIVATE).getInt(PREF_NUM_SHEET_EXPANSIONS, 0); 1111 } 1112 1113 private void incrementNumSheetExpansions() { 1114 getPreferences(Context.MODE_PRIVATE).edit().putInt(PREF_NUM_SHEET_EXPANSIONS, 1115 getNumSheetExpansions() + 1).apply(); 1116 } 1117 1118 @Override 1119 protected void onDestroy() { 1120 super.onDestroy(); 1121 if (mRefinementResultReceiver != null) { 1122 mRefinementResultReceiver.destroy(); 1123 mRefinementResultReceiver = null; 1124 } 1125 unbindRemainingServices(); 1126 mChooserHandler.removeAllMessages(); 1127 1128 if (mPreviewCoord != null) mPreviewCoord.cancelLoads(); 1129 1130 if (mAppPredictor != null) { 1131 mAppPredictor.unregisterPredictionUpdates(mAppPredictorCallback); 1132 mAppPredictor.destroy(); 1133 } 1134 } 1135 1136 @Override 1137 public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { 1138 Intent result = defIntent; 1139 if (mReplacementExtras != null) { 1140 final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName); 1141 if (replExtras != null) { 1142 result = new Intent(defIntent); 1143 result.putExtras(replExtras); 1144 } 1145 } 1146 if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT) 1147 || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) { 1148 result = Intent.createChooser(result, 1149 getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE)); 1150 1151 // Don't auto-launch single intents if the intent is being forwarded. This is done 1152 // because automatically launching a resolving application as a response to the user 1153 // action of switching accounts is pretty unexpected. 1154 result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); 1155 } 1156 return result; 1157 } 1158 1159 @Override 1160 public void onActivityStarted(TargetInfo cti) { 1161 if (mChosenComponentSender != null) { 1162 final ComponentName target = cti.getResolvedComponentName(); 1163 if (target != null) { 1164 final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target); 1165 try { 1166 mChosenComponentSender.sendIntent(this, Activity.RESULT_OK, fillIn, null, null); 1167 } catch (IntentSender.SendIntentException e) { 1168 Slog.e(TAG, "Unable to launch supplied IntentSender to report " 1169 + "the chosen component: " + e); 1170 } 1171 } 1172 } 1173 } 1174 1175 @Override 1176 public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) { 1177 final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; 1178 mChooserListAdapter = (ChooserListAdapter) adapter; 1179 if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) { 1180 mChooserListAdapter.addServiceResults(null, Lists.newArrayList(mCallerChooserTargets), 1181 false); 1182 } 1183 mChooserRowAdapter = new ChooserRowAdapter(mChooserListAdapter); 1184 if (listView != null) { 1185 listView.setItemsCanFocus(true); 1186 } 1187 } 1188 1189 @Override 1190 public int getLayoutResource() { 1191 return R.layout.chooser_grid; 1192 } 1193 1194 @Override 1195 public boolean shouldGetActivityMetadata() { 1196 return true; 1197 } 1198 1199 @Override 1200 public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { 1201 // Note that this is only safe because the Intent handled by the ChooserActivity is 1202 // guaranteed to contain no extras unknown to the local ClassLoader. That is why this 1203 // method can not be replaced in the ResolverActivity whole hog. 1204 if (!super.shouldAutoLaunchSingleChoice(target)) { 1205 return false; 1206 } 1207 1208 return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); 1209 } 1210 1211 @Override 1212 public void showTargetDetails(ResolveInfo ri) { 1213 if (ri == null) { 1214 return; 1215 } 1216 1217 ComponentName name = ri.activityInfo.getComponentName(); 1218 ResolverTargetActionsDialogFragment f = 1219 new ResolverTargetActionsDialogFragment(ri.loadLabel(getPackageManager()), 1220 name); 1221 f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); 1222 } 1223 1224 private void modifyTargetIntent(Intent in) { 1225 if (isSendAction(in)) { 1226 in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | 1227 Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 1228 } 1229 } 1230 1231 @Override 1232 protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { 1233 if (mRefinementIntentSender != null) { 1234 final Intent fillIn = new Intent(); 1235 final List<Intent> sourceIntents = target.getAllSourceIntents(); 1236 if (!sourceIntents.isEmpty()) { 1237 fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0)); 1238 if (sourceIntents.size() > 1) { 1239 final Intent[] alts = new Intent[sourceIntents.size() - 1]; 1240 for (int i = 1, N = sourceIntents.size(); i < N; i++) { 1241 alts[i - 1] = sourceIntents.get(i); 1242 } 1243 fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts); 1244 } 1245 if (mRefinementResultReceiver != null) { 1246 mRefinementResultReceiver.destroy(); 1247 } 1248 mRefinementResultReceiver = new RefinementResultReceiver(this, target, null); 1249 fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, 1250 mRefinementResultReceiver); 1251 try { 1252 mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null); 1253 return false; 1254 } catch (SendIntentException e) { 1255 Log.e(TAG, "Refinement IntentSender failed to send", e); 1256 } 1257 } 1258 } 1259 updateModelAndChooserCounts(target); 1260 return super.onTargetSelected(target, alwaysCheck); 1261 } 1262 1263 @Override 1264 public void startSelected(int which, boolean always, boolean filtered) { 1265 TargetInfo targetInfo = mChooserListAdapter.targetInfoForPosition(which, filtered); 1266 if (targetInfo != null && targetInfo instanceof NotSelectableTargetInfo) { 1267 return; 1268 } 1269 1270 final long selectionCost = System.currentTimeMillis() - mChooserShownTime; 1271 super.startSelected(which, always, filtered); 1272 1273 if (mChooserListAdapter != null) { 1274 // Log the index of which type of target the user picked. 1275 // Lower values mean the ranking was better. 1276 int cat = 0; 1277 int value = which; 1278 int directTargetAlsoRanked = -1; 1279 int numCallerProvided = 0; 1280 HashedStringCache.HashResult directTargetHashed = null; 1281 switch (mChooserListAdapter.getPositionTargetType(which)) { 1282 case ChooserListAdapter.TARGET_SERVICE: 1283 cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET; 1284 // Log the package name + target name to answer the question if most users 1285 // share to mostly the same person or to a bunch of different people. 1286 ChooserTarget target = 1287 mChooserListAdapter.mServiceTargets.get(value).getChooserTarget(); 1288 directTargetHashed = HashedStringCache.getInstance().hashString( 1289 this, 1290 TAG, 1291 target.getComponentName().getPackageName() 1292 + target.getTitle().toString(), 1293 mMaxHashSaltDays); 1294 directTargetAlsoRanked = getRankedPosition((SelectableTargetInfo) targetInfo); 1295 1296 if (mCallerChooserTargets != null) { 1297 numCallerProvided = mCallerChooserTargets.length; 1298 } 1299 break; 1300 case ChooserListAdapter.TARGET_CALLER: 1301 case ChooserListAdapter.TARGET_STANDARD: 1302 cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET; 1303 value -= mChooserListAdapter.getSelectableServiceTargetCount(); 1304 numCallerProvided = mChooserListAdapter.getCallerTargetCount(); 1305 break; 1306 case ChooserListAdapter.TARGET_STANDARD_AZ: 1307 // A-Z targets are unranked standard targets; we use -1 to mark that they 1308 // are from the alphabetical pool. 1309 value = -1; 1310 cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET; 1311 break; 1312 } 1313 1314 if (cat != 0) { 1315 LogMaker targetLogMaker = new LogMaker(cat).setSubtype(value); 1316 if (directTargetHashed != null) { 1317 targetLogMaker.addTaggedData( 1318 MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString); 1319 targetLogMaker.addTaggedData( 1320 MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN, 1321 directTargetHashed.saltGeneration); 1322 targetLogMaker.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION, 1323 directTargetAlsoRanked); 1324 } 1325 targetLogMaker.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, 1326 numCallerProvided); 1327 getMetricsLogger().write(targetLogMaker); 1328 } 1329 1330 if (mIsSuccessfullySelected) { 1331 if (DEBUG) { 1332 Log.d(TAG, "User Selection Time Cost is " + selectionCost); 1333 Log.d(TAG, "position of selected app/service/caller is " + 1334 Integer.toString(value)); 1335 } 1336 MetricsLogger.histogram(null, "user_selection_cost_for_smart_sharing", 1337 (int) selectionCost); 1338 MetricsLogger.histogram(null, "app_position_for_smart_sharing", value); 1339 } 1340 } 1341 } 1342 1343 private int getRankedPosition(SelectableTargetInfo targetInfo) { 1344 String targetPackageName = 1345 targetInfo.getChooserTarget().getComponentName().getPackageName(); 1346 int maxRankedResults = Math.min(mChooserListAdapter.mDisplayList.size(), 1347 MAX_LOG_RANK_POSITION); 1348 1349 for (int i = 0; i < maxRankedResults; i++) { 1350 if (mChooserListAdapter.mDisplayList.get(i) 1351 .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) { 1352 return i; 1353 } 1354 } 1355 return -1; 1356 } 1357 1358 void queryTargetServices(ChooserListAdapter adapter) { 1359 mQueriedTargetServicesTimeMs = System.currentTimeMillis(); 1360 1361 final PackageManager pm = getPackageManager(); 1362 ShortcutManager sm = (ShortcutManager) getSystemService(ShortcutManager.class); 1363 int targetsToQuery = 0; 1364 1365 for (int i = 0, N = adapter.getDisplayResolveInfoCount(); i < N; i++) { 1366 final DisplayResolveInfo dri = adapter.getDisplayResolveInfo(i); 1367 if (adapter.getScore(dri) == 0) { 1368 // A score of 0 means the app hasn't been used in some time; 1369 // don't query it as it's not likely to be relevant. 1370 continue; 1371 } 1372 final ActivityInfo ai = dri.getResolveInfo().activityInfo; 1373 if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS 1374 && sm.hasShareTargets(ai.packageName)) { 1375 // Share targets will be queried from ShortcutManager 1376 continue; 1377 } 1378 final Bundle md = ai.metaData; 1379 final String serviceName = md != null ? convertServiceName(ai.packageName, 1380 md.getString(ChooserTargetService.META_DATA_NAME)) : null; 1381 if (serviceName != null) { 1382 final ComponentName serviceComponent = new ComponentName( 1383 ai.packageName, serviceName); 1384 1385 if (mServicesRequested.contains(serviceComponent)) { 1386 continue; 1387 } 1388 mServicesRequested.add(serviceComponent); 1389 1390 final Intent serviceIntent = new Intent(ChooserTargetService.SERVICE_INTERFACE) 1391 .setComponent(serviceComponent); 1392 1393 if (DEBUG) { 1394 Log.d(TAG, "queryTargets found target with service " + serviceComponent); 1395 } 1396 1397 try { 1398 final String perm = pm.getServiceInfo(serviceComponent, 0).permission; 1399 if (!ChooserTargetService.BIND_PERMISSION.equals(perm)) { 1400 Log.w(TAG, "ChooserTargetService " + serviceComponent + " does not require" 1401 + " permission " + ChooserTargetService.BIND_PERMISSION 1402 + " - this service will not be queried for ChooserTargets." 1403 + " add android:permission=\"" 1404 + ChooserTargetService.BIND_PERMISSION + "\"" 1405 + " to the <service> tag for " + serviceComponent 1406 + " in the manifest."); 1407 continue; 1408 } 1409 } catch (NameNotFoundException e) { 1410 Log.e(TAG, "Could not look up service " + serviceComponent 1411 + "; component name not found"); 1412 continue; 1413 } 1414 1415 final ChooserTargetServiceConnection conn = 1416 new ChooserTargetServiceConnection(this, dri); 1417 1418 // Explicitly specify Process.myUserHandle instead of calling bindService 1419 // to avoid the warning from calling from the system process without an explicit 1420 // user handle 1421 if (bindServiceAsUser(serviceIntent, conn, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND, 1422 Process.myUserHandle())) { 1423 if (DEBUG) { 1424 Log.d(TAG, "Binding service connection for target " + dri 1425 + " intent " + serviceIntent); 1426 } 1427 mServiceConnections.add(conn); 1428 targetsToQuery++; 1429 } 1430 } 1431 if (targetsToQuery >= QUERY_TARGET_SERVICE_LIMIT) { 1432 if (DEBUG) { 1433 Log.d(TAG, "queryTargets hit query target limit " 1434 + QUERY_TARGET_SERVICE_LIMIT); 1435 } 1436 break; 1437 } 1438 } 1439 1440 mChooserHandler.restartServiceRequestTimer(); 1441 } 1442 1443 private IntentFilter getTargetIntentFilter() { 1444 try { 1445 final Intent intent = getTargetIntent(); 1446 String dataString = intent.getDataString(); 1447 if (TextUtils.isEmpty(dataString)) { 1448 dataString = intent.getType(); 1449 } 1450 return new IntentFilter(intent.getAction(), dataString); 1451 } catch (Exception e) { 1452 Log.e(TAG, "failed to get target intent filter " + e); 1453 return null; 1454 } 1455 } 1456 1457 private List<DisplayResolveInfo> getDisplayResolveInfos(ChooserListAdapter adapter) { 1458 // Need to keep the original DisplayResolveInfos to be able to reconstruct ServiceResultInfo 1459 // and use the old code path. This Ugliness should go away when Sharesheet is refactored. 1460 List<DisplayResolveInfo> driList = new ArrayList<>(); 1461 int targetsToQuery = 0; 1462 for (int i = 0, n = adapter.getDisplayResolveInfoCount(); i < n; i++) { 1463 final DisplayResolveInfo dri = adapter.getDisplayResolveInfo(i); 1464 if (adapter.getScore(dri) == 0) { 1465 // A score of 0 means the app hasn't been used in some time; 1466 // don't query it as it's not likely to be relevant. 1467 continue; 1468 } 1469 driList.add(dri); 1470 targetsToQuery++; 1471 // TODO(b/121287224): Do we need this here? (similar to queryTargetServices) 1472 if (targetsToQuery >= SHARE_TARGET_QUERY_PACKAGE_LIMIT) { 1473 if (DEBUG) { 1474 Log.d(TAG, "queryTargets hit query target limit " 1475 + SHARE_TARGET_QUERY_PACKAGE_LIMIT); 1476 } 1477 break; 1478 } 1479 } 1480 return driList; 1481 } 1482 1483 private void queryDirectShareTargets( 1484 ChooserListAdapter adapter, boolean skipAppPredictionService) { 1485 mQueriedSharingShortcutsTimeMs = System.currentTimeMillis(); 1486 if (!skipAppPredictionService) { 1487 AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(); 1488 if (appPredictor != null) { 1489 appPredictor.requestPredictionUpdate(); 1490 return; 1491 } 1492 } 1493 // Default to just querying ShortcutManager if AppPredictor not present. 1494 final IntentFilter filter = getTargetIntentFilter(); 1495 if (filter == null) { 1496 return; 1497 } 1498 final List<DisplayResolveInfo> driList = getDisplayResolveInfos(adapter); 1499 1500 AsyncTask.execute(() -> { 1501 ShortcutManager sm = (ShortcutManager) getSystemService(Context.SHORTCUT_SERVICE); 1502 List<ShortcutManager.ShareShortcutInfo> resultList = sm.getShareTargets(filter); 1503 sendShareShortcutInfoList(resultList, driList, null); 1504 }); 1505 } 1506 1507 private void sendShareShortcutInfoList( 1508 List<ShortcutManager.ShareShortcutInfo> resultList, 1509 List<DisplayResolveInfo> driList, 1510 @Nullable List<AppTarget> appTargets) { 1511 if (appTargets != null && appTargets.size() != resultList.size()) { 1512 throw new RuntimeException("resultList and appTargets must have the same size." 1513 + " resultList.size()=" + resultList.size() 1514 + " appTargets.size()=" + appTargets.size()); 1515 } 1516 1517 for (int i = resultList.size() - 1; i >= 0; i--) { 1518 final String packageName = resultList.get(i).getTargetComponent().getPackageName(); 1519 if (!isPackageEnabled(packageName)) { 1520 resultList.remove(i); 1521 if (appTargets != null) { 1522 appTargets.remove(i); 1523 } 1524 } 1525 } 1526 1527 // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path 1528 // for direct share targets. After ShareSheet is refactored we should use the 1529 // ShareShortcutInfos directly. 1530 boolean resultMessageSent = false; 1531 for (int i = 0; i < driList.size(); i++) { 1532 List<ChooserTarget> chooserTargets = new ArrayList<>(); 1533 for (int j = 0; j < resultList.size(); j++) { 1534 if (driList.get(i).getResolvedComponentName().equals( 1535 resultList.get(j).getTargetComponent())) { 1536 ShortcutManager.ShareShortcutInfo shareShortcutInfo = resultList.get(j); 1537 // Incoming results are ordered but without a score. Create a score 1538 // based on the index in order to be sorted appropriately when joined 1539 // with legacy direct share api results. 1540 float score = Math.max(1.0f - (0.05f * j), 0.0f); 1541 ChooserTarget chooserTarget = convertToChooserTarget(shareShortcutInfo, score); 1542 chooserTargets.add(chooserTarget); 1543 if (mDirectShareAppTargetCache != null && appTargets != null) { 1544 mDirectShareAppTargetCache.put(chooserTarget, appTargets.get(j)); 1545 } 1546 } 1547 } 1548 if (chooserTargets.isEmpty()) { 1549 continue; 1550 } 1551 final Message msg = Message.obtain(); 1552 msg.what = ChooserHandler.SHORTCUT_MANAGER_SHARE_TARGET_RESULT; 1553 msg.obj = new ServiceResultInfo(driList.get(i), chooserTargets, null); 1554 mChooserHandler.sendMessage(msg); 1555 resultMessageSent = true; 1556 } 1557 1558 if (resultMessageSent) { 1559 sendShortcutManagerShareTargetResultCompleted(); 1560 } 1561 } 1562 1563 private void sendShortcutManagerShareTargetResultCompleted() { 1564 final Message msg = Message.obtain(); 1565 msg.what = ChooserHandler.SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED; 1566 mChooserHandler.sendMessage(msg); 1567 } 1568 1569 private boolean isPackageEnabled(String packageName) { 1570 if (TextUtils.isEmpty(packageName)) { 1571 return false; 1572 } 1573 ApplicationInfo appInfo; 1574 try { 1575 appInfo = getPackageManager().getApplicationInfo(packageName, 0); 1576 } catch (NameNotFoundException e) { 1577 return false; 1578 } 1579 1580 if (appInfo != null && appInfo.enabled 1581 && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) { 1582 return true; 1583 } 1584 return false; 1585 } 1586 1587 private ChooserTarget convertToChooserTarget(ShortcutManager.ShareShortcutInfo shareShortcut, 1588 float score) { 1589 ShortcutInfo shortcutInfo = shareShortcut.getShortcutInfo(); 1590 Bundle extras = new Bundle(); 1591 extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId()); 1592 return new ChooserTarget( 1593 // The name of this target. 1594 shortcutInfo.getShortLabel(), 1595 // Don't load the icon until it is selected to be shown 1596 null, 1597 // The ranking score for this target (0.0-1.0); the system will omit items with low 1598 // scores when there are too many Direct Share items. 1599 score, 1600 // The name of the component to be launched if this target is chosen. 1601 shareShortcut.getTargetComponent().clone(), 1602 // The extra values here will be merged into the Intent when this target is chosen. 1603 extras); 1604 } 1605 1606 private String convertServiceName(String packageName, String serviceName) { 1607 if (TextUtils.isEmpty(serviceName)) { 1608 return null; 1609 } 1610 1611 final String fullName; 1612 if (serviceName.startsWith(".")) { 1613 // Relative to the app package. Prepend the app package name. 1614 fullName = packageName + serviceName; 1615 } else if (serviceName.indexOf('.') >= 0) { 1616 // Fully qualified package name. 1617 fullName = serviceName; 1618 } else { 1619 fullName = null; 1620 } 1621 return fullName; 1622 } 1623 1624 void unbindRemainingServices() { 1625 if (DEBUG) { 1626 Log.d(TAG, "unbindRemainingServices, " + mServiceConnections.size() + " left"); 1627 } 1628 for (int i = 0, N = mServiceConnections.size(); i < N; i++) { 1629 final ChooserTargetServiceConnection conn = mServiceConnections.get(i); 1630 if (DEBUG) Log.d(TAG, "unbinding " + conn); 1631 unbindService(conn); 1632 conn.destroy(); 1633 } 1634 mServicesRequested.clear(); 1635 mServiceConnections.clear(); 1636 } 1637 1638 public void onSetupVoiceInteraction() { 1639 // Do nothing. We'll send the voice stuff ourselves. 1640 } 1641 1642 private void logDirectShareTargetReceived(int logCategory) { 1643 final long queryTime = 1644 logCategory == MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER 1645 ? mQueriedSharingShortcutsTimeMs : mQueriedTargetServicesTimeMs; 1646 final int apiLatency = (int) (System.currentTimeMillis() - queryTime); 1647 getMetricsLogger().write(new LogMaker(logCategory).setSubtype(apiLatency)); 1648 } 1649 1650 void updateModelAndChooserCounts(TargetInfo info) { 1651 if (info != null) { 1652 sendClickToAppPredictor(info); 1653 final ResolveInfo ri = info.getResolveInfo(); 1654 Intent targetIntent = getTargetIntent(); 1655 if (ri != null && ri.activityInfo != null && targetIntent != null) { 1656 if (mAdapter != null) { 1657 mAdapter.updateModel(info.getResolvedComponentName()); 1658 mAdapter.updateChooserCounts(ri.activityInfo.packageName, getUserId(), 1659 targetIntent.getAction()); 1660 } 1661 if (DEBUG) { 1662 Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName); 1663 Log.d(TAG, "Action to be updated is " + targetIntent.getAction()); 1664 } 1665 } else if (DEBUG) { 1666 Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo"); 1667 } 1668 } 1669 mIsSuccessfullySelected = true; 1670 } 1671 1672 private void sendClickToAppPredictor(TargetInfo targetInfo) { 1673 AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(); 1674 if (directShareAppPredictor == null) { 1675 return; 1676 } 1677 if (!(targetInfo instanceof ChooserTargetInfo)) { 1678 return; 1679 } 1680 ChooserTarget chooserTarget = ((ChooserTargetInfo) targetInfo).getChooserTarget(); 1681 AppTarget appTarget = null; 1682 if (mDirectShareAppTargetCache != null) { 1683 appTarget = mDirectShareAppTargetCache.get(chooserTarget); 1684 } 1685 // This is a direct share click that was provided by the APS 1686 if (appTarget != null) { 1687 directShareAppPredictor.notifyAppTargetEvent( 1688 new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) 1689 .setLaunchLocation(LAUNCH_LOCATON_DIRECT_SHARE) 1690 .build()); 1691 } 1692 } 1693 1694 @Nullable 1695 private AppPredictor getAppPredictor() { 1696 if (mAppPredictor == null 1697 && getPackageManager().getAppPredictionServicePackageName() != null) { 1698 final IntentFilter filter = getTargetIntentFilter(); 1699 Bundle extras = new Bundle(); 1700 extras.putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, filter); 1701 AppPredictionContext appPredictionContext = new AppPredictionContext.Builder(this) 1702 .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE) 1703 .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT) 1704 .setExtras(extras) 1705 .build(); 1706 AppPredictionManager appPredictionManager 1707 = getSystemService(AppPredictionManager.class); 1708 mAppPredictor = appPredictionManager.createAppPredictionSession(appPredictionContext); 1709 } 1710 return mAppPredictor; 1711 } 1712 1713 /** 1714 * This will return an app predictor if it is enabled for direct share sorting 1715 * and if one exists. Otherwise, it returns null. 1716 */ 1717 @Nullable 1718 private AppPredictor getAppPredictorForDirectShareIfEnabled() { 1719 return USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS && !ActivityManager.isLowRamDeviceStatic() 1720 ? getAppPredictor() : null; 1721 } 1722 1723 /** 1724 * This will return an app predictor if it is enabled for share activity sorting 1725 * and if one exists. Otherwise, it returns null. 1726 */ 1727 @Nullable 1728 private AppPredictor getAppPredictorForShareActivitesIfEnabled() { 1729 return USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES ? getAppPredictor() : null; 1730 } 1731 1732 void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) { 1733 if (mRefinementResultReceiver != null) { 1734 mRefinementResultReceiver.destroy(); 1735 mRefinementResultReceiver = null; 1736 } 1737 if (selectedTarget == null) { 1738 Log.e(TAG, "Refinement result intent did not match any known targets; canceling"); 1739 } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) { 1740 Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget 1741 + " cannot match refined source intent " + matchingIntent); 1742 } else { 1743 TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0); 1744 if (super.onTargetSelected(clonedTarget, false)) { 1745 updateModelAndChooserCounts(clonedTarget); 1746 finish(); 1747 return; 1748 } 1749 } 1750 onRefinementCanceled(); 1751 } 1752 1753 void onRefinementCanceled() { 1754 if (mRefinementResultReceiver != null) { 1755 mRefinementResultReceiver.destroy(); 1756 mRefinementResultReceiver = null; 1757 } 1758 finish(); 1759 } 1760 1761 boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) { 1762 final List<Intent> targetIntents = target.getAllSourceIntents(); 1763 for (int i = 0, N = targetIntents.size(); i < N; i++) { 1764 final Intent targetIntent = targetIntents.get(i); 1765 if (targetIntent.filterEquals(matchingIntent)) { 1766 return true; 1767 } 1768 } 1769 return false; 1770 } 1771 1772 void filterServiceTargets(String packageName, List<ChooserTarget> targets) { 1773 if (targets == null) { 1774 return; 1775 } 1776 1777 final PackageManager pm = getPackageManager(); 1778 for (int i = targets.size() - 1; i >= 0; i--) { 1779 final ChooserTarget target = targets.get(i); 1780 final ComponentName targetName = target.getComponentName(); 1781 if (packageName != null && packageName.equals(targetName.getPackageName())) { 1782 // Anything from the original target's package is fine. 1783 continue; 1784 } 1785 1786 boolean remove; 1787 try { 1788 final ActivityInfo ai = pm.getActivityInfo(targetName, 0); 1789 remove = !ai.exported || ai.permission != null; 1790 } catch (NameNotFoundException e) { 1791 Log.e(TAG, "Target " + target + " returned by " + packageName 1792 + " component not found"); 1793 remove = true; 1794 } 1795 1796 if (remove) { 1797 targets.remove(i); 1798 } 1799 } 1800 } 1801 1802 private void updateAlphabeticalList() { 1803 mSortedList.clear(); 1804 mSortedList.addAll(getDisplayList()); 1805 Collections.sort(mSortedList, new AzInfoComparator(ChooserActivity.this)); 1806 } 1807 1808 /** 1809 * Sort intents alphabetically based on display label. 1810 */ 1811 class AzInfoComparator implements Comparator<ResolverActivity.DisplayResolveInfo> { 1812 Collator mCollator; 1813 AzInfoComparator(Context context) { 1814 mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); 1815 } 1816 1817 @Override 1818 public int compare(ResolverActivity.DisplayResolveInfo lhsp, 1819 ResolverActivity.DisplayResolveInfo rhsp) { 1820 return mCollator.compare(lhsp.getDisplayLabel(), rhsp.getDisplayLabel()); 1821 } 1822 } 1823 1824 protected MetricsLogger getMetricsLogger() { 1825 if (mMetricsLogger == null) { 1826 mMetricsLogger = new MetricsLogger(); 1827 } 1828 return mMetricsLogger; 1829 } 1830 1831 public class ChooserListController extends ResolverListController { 1832 public ChooserListController(Context context, 1833 PackageManager pm, 1834 Intent targetIntent, 1835 String referrerPackageName, 1836 int launchedFromUid, 1837 AbstractResolverComparator resolverComparator) { 1838 super(context, pm, targetIntent, referrerPackageName, launchedFromUid, 1839 resolverComparator); 1840 } 1841 1842 @Override 1843 boolean isComponentFiltered(ComponentName name) { 1844 if (mFilteredComponentNames == null) { 1845 return false; 1846 } 1847 for (ComponentName filteredComponentName : mFilteredComponentNames) { 1848 if (name.equals(filteredComponentName)) { 1849 return true; 1850 } 1851 } 1852 return false; 1853 } 1854 } 1855 1856 @Override 1857 public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents, 1858 Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid, 1859 boolean filterLastUsed) { 1860 final ChooserListAdapter adapter = new ChooserListAdapter(context, payloadIntents, 1861 initialIntents, rList, launchedFromUid, filterLastUsed, createListController()); 1862 return adapter; 1863 } 1864 1865 @VisibleForTesting 1866 protected ResolverListController createListController() { 1867 AppPredictor appPredictor = getAppPredictorForShareActivitesIfEnabled(); 1868 AbstractResolverComparator resolverComparator; 1869 if (appPredictor != null) { 1870 resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(), 1871 getReferrerPackageName(), appPredictor, getUser()); 1872 } else { 1873 resolverComparator = 1874 new ResolverRankerServiceResolverComparator(this, getTargetIntent(), 1875 getReferrerPackageName(), null); 1876 } 1877 1878 return new ChooserListController( 1879 this, 1880 mPm, 1881 getTargetIntent(), 1882 getReferrerPackageName(), 1883 mLaunchedFromUid, 1884 resolverComparator); 1885 } 1886 1887 @VisibleForTesting 1888 protected Bitmap loadThumbnail(Uri uri, Size size) { 1889 if (uri == null || size == null) { 1890 return null; 1891 } 1892 1893 try { 1894 return ImageUtils.loadThumbnail(getContentResolver(), uri, size); 1895 } catch (IOException | NullPointerException | SecurityException ex) { 1896 logContentPreviewWarning(uri); 1897 } 1898 return null; 1899 } 1900 1901 interface ChooserTargetInfo extends TargetInfo { 1902 float getModifiedScore(); 1903 1904 ChooserTarget getChooserTarget(); 1905 1906 /** 1907 * Do not label as 'equals', since this doesn't quite work 1908 * as intended with java 8. 1909 */ 1910 default boolean isSimilar(ChooserTargetInfo other) { 1911 if (other == null) return false; 1912 1913 ChooserTarget ct1 = getChooserTarget(); 1914 ChooserTarget ct2 = other.getChooserTarget(); 1915 1916 // If either is null, there is not enough info to make an informed decision 1917 // about equality, so just exit 1918 if (ct1 == null || ct2 == null) return false; 1919 1920 if (ct1.getComponentName().equals(ct2.getComponentName()) 1921 && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel()) 1922 && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) { 1923 return true; 1924 } 1925 1926 return false; 1927 } 1928 } 1929 1930 /** 1931 * Distinguish between targets that selectable by the user, vs those that are 1932 * placeholders for the system while information is loading in an async manner. 1933 */ 1934 abstract class NotSelectableTargetInfo implements ChooserTargetInfo { 1935 1936 public Intent getResolvedIntent() { 1937 return null; 1938 } 1939 1940 public ComponentName getResolvedComponentName() { 1941 return null; 1942 } 1943 1944 public boolean start(Activity activity, Bundle options) { 1945 return false; 1946 } 1947 1948 public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { 1949 return false; 1950 } 1951 1952 public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { 1953 return false; 1954 } 1955 1956 public ResolveInfo getResolveInfo() { 1957 return null; 1958 } 1959 1960 public CharSequence getDisplayLabel() { 1961 return null; 1962 } 1963 1964 public CharSequence getExtendedInfo() { 1965 return null; 1966 } 1967 1968 public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { 1969 return null; 1970 } 1971 1972 public List<Intent> getAllSourceIntents() { 1973 return null; 1974 } 1975 1976 public float getModifiedScore() { 1977 return -0.1f; 1978 } 1979 1980 public ChooserTarget getChooserTarget() { 1981 return null; 1982 } 1983 1984 public boolean isSuspended() { 1985 return false; 1986 } 1987 } 1988 1989 final class PlaceHolderTargetInfo extends NotSelectableTargetInfo { 1990 public Drawable getDisplayIcon() { 1991 AnimatedVectorDrawable avd = (AnimatedVectorDrawable) 1992 getDrawable(R.drawable.chooser_direct_share_icon_placeholder); 1993 avd.start(); // Start animation after generation 1994 return avd; 1995 } 1996 } 1997 1998 1999 final class EmptyTargetInfo extends NotSelectableTargetInfo { 2000 public Drawable getDisplayIcon() { 2001 return null; 2002 } 2003 } 2004 2005 final class SelectableTargetInfo implements ChooserTargetInfo { 2006 private final DisplayResolveInfo mSourceInfo; 2007 private final ResolveInfo mBackupResolveInfo; 2008 private final ChooserTarget mChooserTarget; 2009 private final String mDisplayLabel; 2010 private Drawable mBadgeIcon = null; 2011 private CharSequence mBadgeContentDescription; 2012 private Drawable mDisplayIcon; 2013 private final Intent mFillInIntent; 2014 private final int mFillInFlags; 2015 private final float mModifiedScore; 2016 private boolean mIsSuspended = false; 2017 2018 SelectableTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, 2019 float modifiedScore) { 2020 mSourceInfo = sourceInfo; 2021 mChooserTarget = chooserTarget; 2022 mModifiedScore = modifiedScore; 2023 if (sourceInfo != null) { 2024 final ResolveInfo ri = sourceInfo.getResolveInfo(); 2025 if (ri != null) { 2026 final ActivityInfo ai = ri.activityInfo; 2027 if (ai != null && ai.applicationInfo != null) { 2028 final PackageManager pm = getPackageManager(); 2029 mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo); 2030 mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo); 2031 mIsSuspended = 2032 (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; 2033 } 2034 } 2035 } 2036 // TODO(b/121287224): do this in the background thread, and only for selected targets 2037 mDisplayIcon = getChooserTargetIconDrawable(chooserTarget); 2038 2039 if (sourceInfo != null) { 2040 mBackupResolveInfo = null; 2041 } else { 2042 mBackupResolveInfo = getPackageManager().resolveActivity(getResolvedIntent(), 0); 2043 } 2044 2045 mFillInIntent = null; 2046 mFillInFlags = 0; 2047 2048 mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle()); 2049 } 2050 2051 private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) { 2052 mSourceInfo = other.mSourceInfo; 2053 mBackupResolveInfo = other.mBackupResolveInfo; 2054 mChooserTarget = other.mChooserTarget; 2055 mBadgeIcon = other.mBadgeIcon; 2056 mBadgeContentDescription = other.mBadgeContentDescription; 2057 mDisplayIcon = other.mDisplayIcon; 2058 mFillInIntent = fillInIntent; 2059 mFillInFlags = flags; 2060 mModifiedScore = other.mModifiedScore; 2061 2062 mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle()); 2063 } 2064 2065 private String sanitizeDisplayLabel(CharSequence label) { 2066 SpannableStringBuilder sb = new SpannableStringBuilder(label); 2067 sb.clearSpans(); 2068 return sb.toString(); 2069 } 2070 2071 public boolean isSuspended() { 2072 return mIsSuspended; 2073 } 2074 2075 /** 2076 * Since ShortcutInfos are returned by ShortcutManager, we can cache the shortcuts and skip 2077 * the call to LauncherApps#getShortcuts(ShortcutQuery). 2078 */ 2079 // TODO(121287224): Refactor code to apply the suggestion above 2080 private Drawable getChooserTargetIconDrawable(ChooserTarget target) { 2081 Drawable directShareIcon = null; 2082 2083 // First get the target drawable and associated activity info 2084 final Icon icon = target.getIcon(); 2085 if (icon != null) { 2086 directShareIcon = icon.loadDrawable(ChooserActivity.this); 2087 } else if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS) { 2088 Bundle extras = target.getIntentExtras(); 2089 if (extras != null && extras.containsKey(Intent.EXTRA_SHORTCUT_ID)) { 2090 CharSequence shortcutId = extras.getCharSequence(Intent.EXTRA_SHORTCUT_ID); 2091 LauncherApps launcherApps = (LauncherApps) getSystemService( 2092 Context.LAUNCHER_APPS_SERVICE); 2093 final LauncherApps.ShortcutQuery q = new LauncherApps.ShortcutQuery(); 2094 q.setPackage(target.getComponentName().getPackageName()); 2095 q.setShortcutIds(Arrays.asList(shortcutId.toString())); 2096 q.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC); 2097 final List<ShortcutInfo> shortcuts = launcherApps.getShortcuts(q, getUser()); 2098 if (shortcuts != null && shortcuts.size() > 0) { 2099 directShareIcon = launcherApps.getShortcutIconDrawable(shortcuts.get(0), 0); 2100 } 2101 } 2102 } 2103 2104 if (directShareIcon == null) return null; 2105 2106 ActivityInfo info = null; 2107 try { 2108 info = mPm.getActivityInfo(target.getComponentName(), 0); 2109 } catch (NameNotFoundException error) { 2110 Log.e(TAG, "Could not find activity associated with ChooserTarget"); 2111 } 2112 2113 if (info == null) return null; 2114 2115 // Now fetch app icon and raster with no badging even in work profile 2116 Bitmap appIcon = makePresentationGetter(info).getIconBitmap( 2117 UserHandle.getUserHandleForUid(UserHandle.myUserId())); 2118 2119 // Raster target drawable with appIcon as a badge 2120 SimpleIconFactory sif = SimpleIconFactory.obtain(ChooserActivity.this); 2121 Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon); 2122 sif.recycle(); 2123 2124 return new BitmapDrawable(getResources(), directShareBadgedIcon); 2125 } 2126 2127 public float getModifiedScore() { 2128 return mModifiedScore; 2129 } 2130 2131 @Override 2132 public Intent getResolvedIntent() { 2133 if (mSourceInfo != null) { 2134 return mSourceInfo.getResolvedIntent(); 2135 } 2136 2137 final Intent targetIntent = new Intent(getTargetIntent()); 2138 targetIntent.setComponent(mChooserTarget.getComponentName()); 2139 targetIntent.putExtras(mChooserTarget.getIntentExtras()); 2140 return targetIntent; 2141 } 2142 2143 @Override 2144 public ComponentName getResolvedComponentName() { 2145 if (mSourceInfo != null) { 2146 return mSourceInfo.getResolvedComponentName(); 2147 } else if (mBackupResolveInfo != null) { 2148 return new ComponentName(mBackupResolveInfo.activityInfo.packageName, 2149 mBackupResolveInfo.activityInfo.name); 2150 } 2151 return null; 2152 } 2153 2154 private Intent getBaseIntentToSend() { 2155 Intent result = getResolvedIntent(); 2156 if (result == null) { 2157 Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); 2158 } else { 2159 result = new Intent(result); 2160 if (mFillInIntent != null) { 2161 result.fillIn(mFillInIntent, mFillInFlags); 2162 } 2163 result.fillIn(mReferrerFillInIntent, 0); 2164 } 2165 return result; 2166 } 2167 2168 @Override 2169 public boolean start(Activity activity, Bundle options) { 2170 throw new RuntimeException("ChooserTargets should be started as caller."); 2171 } 2172 2173 @Override 2174 public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { 2175 final Intent intent = getBaseIntentToSend(); 2176 if (intent == null) { 2177 return false; 2178 } 2179 intent.setComponent(mChooserTarget.getComponentName()); 2180 intent.putExtras(mChooserTarget.getIntentExtras()); 2181 2182 // Important: we will ignore the target security checks in ActivityManager 2183 // if and only if the ChooserTarget's target package is the same package 2184 // where we got the ChooserTargetService that provided it. This lets a 2185 // ChooserTargetService provide a non-exported or permission-guarded target 2186 // to the chooser for the user to pick. 2187 // 2188 // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere 2189 // so we'll obey the caller's normal security checks. 2190 final boolean ignoreTargetSecurity = mSourceInfo != null 2191 && mSourceInfo.getResolvedComponentName().getPackageName() 2192 .equals(mChooserTarget.getComponentName().getPackageName()); 2193 return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId); 2194 } 2195 2196 @Override 2197 public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { 2198 throw new RuntimeException("ChooserTargets should be started as caller."); 2199 } 2200 2201 @Override 2202 public ResolveInfo getResolveInfo() { 2203 return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; 2204 } 2205 2206 @Override 2207 public CharSequence getDisplayLabel() { 2208 return mDisplayLabel; 2209 } 2210 2211 @Override 2212 public CharSequence getExtendedInfo() { 2213 // ChooserTargets have badge icons, so we won't show the extended info to disambiguate. 2214 return null; 2215 } 2216 2217 @Override 2218 public Drawable getDisplayIcon() { 2219 return mDisplayIcon; 2220 } 2221 2222 public ChooserTarget getChooserTarget() { 2223 return mChooserTarget; 2224 } 2225 2226 @Override 2227 public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { 2228 return new SelectableTargetInfo(this, fillInIntent, flags); 2229 } 2230 2231 @Override 2232 public List<Intent> getAllSourceIntents() { 2233 final List<Intent> results = new ArrayList<>(); 2234 if (mSourceInfo != null) { 2235 // We only queried the service for the first one in our sourceinfo. 2236 results.add(mSourceInfo.getAllSourceIntents().get(0)); 2237 } 2238 return results; 2239 } 2240 } 2241 2242 private void handleScroll(View view, int x, int y, int oldx, int oldy) { 2243 if (mChooserRowAdapter != null) { 2244 mChooserRowAdapter.handleScroll(view, y, oldy); 2245 } 2246 } 2247 2248 /* 2249 * Need to dynamically adjust how many icons can fit per row before we add them, 2250 * which also means setting the correct offset to initially show the content 2251 * preview area + 2 rows of targets 2252 */ 2253 private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 2254 int oldTop, int oldRight, int oldBottom) { 2255 if (mChooserRowAdapter == null || mAdapterView == null) { 2256 return; 2257 } 2258 2259 final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight(); 2260 if (mChooserRowAdapter.consumeLayoutRequest() 2261 || mChooserRowAdapter.calculateChooserTargetWidth(availableWidth) 2262 || mAdapterView.getAdapter() == null 2263 || availableWidth != mCurrAvailableWidth) { 2264 mCurrAvailableWidth = availableWidth; 2265 mAdapterView.setAdapter(mChooserRowAdapter); 2266 2267 getMainThreadHandler().post(() -> { 2268 if (mResolverDrawerLayout == null || mChooserRowAdapter == null) { 2269 return; 2270 } 2271 2272 final int bottomInset = mSystemWindowInsets != null 2273 ? mSystemWindowInsets.bottom : 0; 2274 int offset = bottomInset; 2275 int rowsToShow = mChooserRowAdapter.getContentPreviewRowCount() 2276 + mChooserRowAdapter.getProfileRowCount() 2277 + mChooserRowAdapter.getServiceTargetRowCount() 2278 + mChooserRowAdapter.getCallerAndRankedTargetRowCount(); 2279 2280 // then this is most likely not a SEND_* action, so check 2281 // the app target count 2282 if (rowsToShow == 0) { 2283 rowsToShow = mChooserRowAdapter.getCount(); 2284 } 2285 2286 // still zero? then use a default height and leave, which 2287 // can happen when there are no targets to show 2288 if (rowsToShow == 0) { 2289 offset += getResources().getDimensionPixelSize( 2290 R.dimen.chooser_max_collapsed_height); 2291 mResolverDrawerLayout.setCollapsibleHeightReserved(offset); 2292 return; 2293 } 2294 2295 int directShareHeight = 0; 2296 rowsToShow = Math.min(4, rowsToShow); 2297 for (int i = 0; i < Math.min(rowsToShow, mAdapterView.getChildCount()); i++) { 2298 View child = mAdapterView.getChildAt(i); 2299 int height = child.getHeight(); 2300 offset += height; 2301 2302 if (child.getTag() != null 2303 && (child.getTag() instanceof DirectShareViewHolder)) { 2304 directShareHeight = height; 2305 } 2306 } 2307 2308 boolean isExpandable = getResources().getConfiguration().orientation 2309 == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode(); 2310 if (directShareHeight != 0 && isSendAction(getTargetIntent()) && isExpandable) { 2311 // make sure to leave room for direct share 4->8 expansion 2312 int requiredExpansionHeight = 2313 (int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE); 2314 int topInset = mSystemWindowInsets != null ? mSystemWindowInsets.top : 0; 2315 int minHeight = bottom - top - mResolverDrawerLayout.getAlwaysShowHeight() 2316 - requiredExpansionHeight - topInset - bottomInset; 2317 2318 offset = Math.min(offset, minHeight); 2319 } 2320 2321 mResolverDrawerLayout.setCollapsibleHeightReserved(Math.min(offset, bottom - top)); 2322 }); 2323 } 2324 } 2325 2326 public class ChooserListAdapter extends ResolveListAdapter { 2327 public static final int TARGET_BAD = -1; 2328 public static final int TARGET_CALLER = 0; 2329 public static final int TARGET_SERVICE = 1; 2330 public static final int TARGET_STANDARD = 2; 2331 public static final int TARGET_STANDARD_AZ = 3; 2332 2333 private static final int MAX_SUGGESTED_APP_TARGETS = 4; 2334 private static final int MAX_CHOOSER_TARGETS_PER_APP = 2; 2335 2336 private static final int MAX_SERVICE_TARGETS = 8; 2337 2338 private final int mMaxShortcutTargetsPerApp = 2339 getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp); 2340 2341 private int mNumShortcutResults = 0; 2342 2343 // Reserve spots for incoming direct share targets by adding placeholders 2344 private ChooserTargetInfo mPlaceHolderTargetInfo = new PlaceHolderTargetInfo(); 2345 private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>(); 2346 private final List<TargetInfo> mCallerTargets = new ArrayList<>(); 2347 2348 private final BaseChooserTargetComparator mBaseTargetComparator 2349 = new BaseChooserTargetComparator(); 2350 2351 public ChooserListAdapter(Context context, List<Intent> payloadIntents, 2352 Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid, 2353 boolean filterLastUsed, ResolverListController resolverListController) { 2354 // Don't send the initial intents through the shared ResolverActivity path, 2355 // we want to separate them into a different section. 2356 super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed, 2357 resolverListController); 2358 2359 createPlaceHolders(); 2360 2361 if (initialIntents != null) { 2362 final PackageManager pm = getPackageManager(); 2363 for (int i = 0; i < initialIntents.length; i++) { 2364 final Intent ii = initialIntents[i]; 2365 if (ii == null) { 2366 continue; 2367 } 2368 2369 // We reimplement Intent#resolveActivityInfo here because if we have an 2370 // implicit intent, we want the ResolveInfo returned by PackageManager 2371 // instead of one we reconstruct ourselves. The ResolveInfo returned might 2372 // have extra metadata and resolvePackageName set and we want to respect that. 2373 ResolveInfo ri = null; 2374 ActivityInfo ai = null; 2375 final ComponentName cn = ii.getComponent(); 2376 if (cn != null) { 2377 try { 2378 ai = pm.getActivityInfo(ii.getComponent(), 0); 2379 ri = new ResolveInfo(); 2380 ri.activityInfo = ai; 2381 } catch (PackageManager.NameNotFoundException ignored) { 2382 // ai will == null below 2383 } 2384 } 2385 if (ai == null) { 2386 ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY); 2387 ai = ri != null ? ri.activityInfo : null; 2388 } 2389 if (ai == null) { 2390 Log.w(TAG, "No activity found for " + ii); 2391 continue; 2392 } 2393 UserManager userManager = 2394 (UserManager) getSystemService(Context.USER_SERVICE); 2395 if (ii instanceof LabeledIntent) { 2396 LabeledIntent li = (LabeledIntent) ii; 2397 ri.resolvePackageName = li.getSourcePackage(); 2398 ri.labelRes = li.getLabelResource(); 2399 ri.nonLocalizedLabel = li.getNonLocalizedLabel(); 2400 ri.icon = li.getIconResource(); 2401 ri.iconResourceId = ri.icon; 2402 } 2403 if (userManager.isManagedProfile()) { 2404 ri.noResourceId = true; 2405 ri.icon = 0; 2406 } 2407 ResolveInfoPresentationGetter getter = makePresentationGetter(ri); 2408 mCallerTargets.add(new DisplayResolveInfo(ii, ri, 2409 getter.getLabel(), getter.getSubLabel(), ii)); 2410 } 2411 } 2412 } 2413 2414 @Override 2415 public void handlePackagesChanged() { 2416 if (DEBUG) { 2417 Log.d(TAG, "clearing queryTargets on package change"); 2418 } 2419 createPlaceHolders(); 2420 mServicesRequested.clear(); 2421 notifyDataSetChanged(); 2422 2423 super.handlePackagesChanged(); 2424 } 2425 2426 @Override 2427 public void notifyDataSetChanged() { 2428 if (!mListViewDataChanged) { 2429 mChooserHandler.sendEmptyMessageDelayed(ChooserHandler.LIST_VIEW_UPDATE_MESSAGE, 2430 LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS); 2431 mListViewDataChanged = true; 2432 } 2433 } 2434 2435 private void refreshListView() { 2436 if (mListViewDataChanged) { 2437 super.notifyDataSetChanged(); 2438 } 2439 mListViewDataChanged = false; 2440 } 2441 2442 2443 private void createPlaceHolders() { 2444 mNumShortcutResults = 0; 2445 mServiceTargets.clear(); 2446 for (int i = 0; i < MAX_SERVICE_TARGETS; i++) { 2447 mServiceTargets.add(mPlaceHolderTargetInfo); 2448 } 2449 } 2450 2451 @Override 2452 public View onCreateView(ViewGroup parent) { 2453 return mInflater.inflate( 2454 com.android.internal.R.layout.resolve_grid_item, parent, false); 2455 } 2456 2457 @Override 2458 protected void onBindView(View view, TargetInfo info) { 2459 super.onBindView(view, info); 2460 2461 // If target is loading, show a special placeholder shape in the label, make unclickable 2462 final ViewHolder holder = (ViewHolder) view.getTag(); 2463 if (info instanceof PlaceHolderTargetInfo) { 2464 final int maxWidth = getResources().getDimensionPixelSize( 2465 R.dimen.chooser_direct_share_label_placeholder_max_width); 2466 holder.text.setMaxWidth(maxWidth); 2467 holder.text.setBackground(getResources().getDrawable( 2468 R.drawable.chooser_direct_share_label_placeholder, getTheme())); 2469 // Prevent rippling by removing background containing ripple 2470 holder.itemView.setBackground(null); 2471 } else { 2472 holder.text.setMaxWidth(Integer.MAX_VALUE); 2473 holder.text.setBackground(null); 2474 holder.itemView.setBackground(holder.defaultItemViewBackground); 2475 } 2476 } 2477 2478 @Override 2479 public void onListRebuilt() { 2480 updateAlphabeticalList(); 2481 2482 // don't support direct share on low ram devices 2483 if (ActivityManager.isLowRamDeviceStatic()) { 2484 return; 2485 } 2486 2487 if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS 2488 || USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) { 2489 if (DEBUG) { 2490 Log.d(TAG, "querying direct share targets from ShortcutManager"); 2491 } 2492 2493 queryDirectShareTargets(this, false); 2494 } 2495 if (USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS) { 2496 if (DEBUG) { 2497 Log.d(TAG, "List built querying services"); 2498 } 2499 2500 queryTargetServices(this); 2501 } 2502 } 2503 2504 @Override 2505 public boolean shouldGetResolvedFilter() { 2506 return true; 2507 } 2508 2509 @Override 2510 public int getCount() { 2511 return getRankedTargetCount() + getAlphaTargetCount() 2512 + getSelectableServiceTargetCount() + getCallerTargetCount(); 2513 } 2514 2515 @Override 2516 public int getUnfilteredCount() { 2517 int appTargets = super.getUnfilteredCount(); 2518 if (appTargets > getMaxRankedTargets()) { 2519 appTargets = appTargets + getMaxRankedTargets(); 2520 } 2521 return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount(); 2522 } 2523 2524 2525 public int getCallerTargetCount() { 2526 return Math.min(mCallerTargets.size(), MAX_SUGGESTED_APP_TARGETS); 2527 } 2528 2529 /** 2530 * Filter out placeholders and non-selectable service targets 2531 */ 2532 public int getSelectableServiceTargetCount() { 2533 int count = 0; 2534 for (ChooserTargetInfo info : mServiceTargets) { 2535 if (info instanceof SelectableTargetInfo) { 2536 count++; 2537 } 2538 } 2539 return count; 2540 } 2541 2542 public int getServiceTargetCount() { 2543 if (isSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) { 2544 return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS); 2545 } 2546 2547 return 0; 2548 } 2549 2550 int getAlphaTargetCount() { 2551 int standardCount = super.getCount(); 2552 return standardCount > getMaxRankedTargets() ? standardCount : 0; 2553 } 2554 2555 int getRankedTargetCount() { 2556 int spacesAvailable = getMaxRankedTargets() - getCallerTargetCount(); 2557 return Math.min(spacesAvailable, super.getCount()); 2558 } 2559 2560 private int getMaxRankedTargets() { 2561 return mChooserRowAdapter == null ? 4 : mChooserRowAdapter.getMaxTargetsPerRow(); 2562 } 2563 2564 public int getPositionTargetType(int position) { 2565 int offset = 0; 2566 2567 final int serviceTargetCount = getServiceTargetCount(); 2568 if (position < serviceTargetCount) { 2569 return TARGET_SERVICE; 2570 } 2571 offset += serviceTargetCount; 2572 2573 final int callerTargetCount = getCallerTargetCount(); 2574 if (position - offset < callerTargetCount) { 2575 return TARGET_CALLER; 2576 } 2577 offset += callerTargetCount; 2578 2579 final int rankedTargetCount = getRankedTargetCount(); 2580 if (position - offset < rankedTargetCount) { 2581 return TARGET_STANDARD; 2582 } 2583 offset += rankedTargetCount; 2584 2585 final int standardTargetCount = getAlphaTargetCount(); 2586 if (position - offset < standardTargetCount) { 2587 return TARGET_STANDARD_AZ; 2588 } 2589 2590 return TARGET_BAD; 2591 } 2592 2593 @Override 2594 public TargetInfo getItem(int position) { 2595 return targetInfoForPosition(position, true); 2596 } 2597 2598 2599 /** 2600 * Find target info for a given position. 2601 * Since ChooserActivity displays several sections of content, determine which 2602 * section provides this item. 2603 */ 2604 @Override 2605 public TargetInfo targetInfoForPosition(int position, boolean filtered) { 2606 int offset = 0; 2607 2608 // Direct share targets 2609 final int serviceTargetCount = filtered ? getServiceTargetCount() : 2610 getSelectableServiceTargetCount(); 2611 if (position < serviceTargetCount) { 2612 return mServiceTargets.get(position); 2613 } 2614 offset += serviceTargetCount; 2615 2616 // Targets provided by calling app 2617 final int callerTargetCount = getCallerTargetCount(); 2618 if (position - offset < callerTargetCount) { 2619 return mCallerTargets.get(position - offset); 2620 } 2621 offset += callerTargetCount; 2622 2623 // Ranked standard app targets 2624 final int rankedTargetCount = getRankedTargetCount(); 2625 if (position - offset < rankedTargetCount) { 2626 return filtered ? super.getItem(position - offset) 2627 : getDisplayResolveInfo(position - offset); 2628 } 2629 offset += rankedTargetCount; 2630 2631 // Alphabetical complete app target list. 2632 if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) { 2633 return mSortedList.get(position - offset); 2634 } 2635 2636 return null; 2637 } 2638 2639 2640 /** 2641 * Evaluate targets for inclusion in the direct share area. May not be included 2642 * if score is too low. 2643 */ 2644 public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets, 2645 boolean isShortcutResult) { 2646 if (DEBUG) { 2647 Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size() 2648 + " targets"); 2649 } 2650 2651 if (targets.size() == 0) { 2652 return; 2653 } 2654 2655 final float baseScore = getBaseScore(origTarget, isShortcutResult); 2656 Collections.sort(targets, mBaseTargetComparator); 2657 2658 final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp 2659 : MAX_CHOOSER_TARGETS_PER_APP; 2660 float lastScore = 0; 2661 boolean shouldNotify = false; 2662 for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) { 2663 final ChooserTarget target = targets.get(i); 2664 float targetScore = target.getScore(); 2665 targetScore *= baseScore; 2666 if (i > 0 && targetScore >= lastScore) { 2667 // Apply a decay so that the top app can't crowd out everything else. 2668 // This incents ChooserTargetServices to define what's truly better. 2669 targetScore = lastScore * 0.95f; 2670 } 2671 boolean isInserted = insertServiceTarget( 2672 new SelectableTargetInfo(origTarget, target, targetScore)); 2673 2674 if (isInserted && isShortcutResult) { 2675 mNumShortcutResults++; 2676 } 2677 2678 shouldNotify |= isInserted; 2679 2680 if (DEBUG) { 2681 Log.d(TAG, " => " + target.toString() + " score=" + targetScore 2682 + " base=" + target.getScore() 2683 + " lastScore=" + lastScore 2684 + " baseScore=" + baseScore); 2685 } 2686 2687 lastScore = targetScore; 2688 } 2689 2690 if (shouldNotify) { 2691 notifyDataSetChanged(); 2692 } 2693 } 2694 2695 private int getNumShortcutResults() { 2696 return mNumShortcutResults; 2697 } 2698 2699 /** 2700 * Use the scoring system along with artificial boosts to create up to 4 distinct buckets: 2701 * <ol> 2702 * <li>App-supplied targets 2703 * <li>Shortcuts ranked via App Prediction Manager 2704 * <li>Shortcuts ranked via legacy heuristics 2705 * <li>Legacy direct share targets 2706 * </ol> 2707 */ 2708 private float getBaseScore(DisplayResolveInfo target, boolean isShortcutResult) { 2709 if (target == null) { 2710 return CALLER_TARGET_SCORE_BOOST; 2711 } 2712 2713 if (isShortcutResult && getAppPredictorForDirectShareIfEnabled() != null) { 2714 return SHORTCUT_TARGET_SCORE_BOOST; 2715 } 2716 2717 float score = super.getScore(target); 2718 if (isShortcutResult) { 2719 return score * SHORTCUT_TARGET_SCORE_BOOST; 2720 } 2721 2722 return score; 2723 } 2724 2725 /** 2726 * Calling this marks service target loading complete, and will attempt to no longer 2727 * update the direct share area. 2728 */ 2729 public void completeServiceTargetLoading() { 2730 mServiceTargets.removeIf(o -> o instanceof PlaceHolderTargetInfo); 2731 2732 if (mServiceTargets.isEmpty()) { 2733 mServiceTargets.add(new EmptyTargetInfo()); 2734 } 2735 notifyDataSetChanged(); 2736 } 2737 2738 private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { 2739 // Avoid inserting any potentially late results 2740 if (mServiceTargets.size() == 1 2741 && mServiceTargets.get(0) instanceof EmptyTargetInfo) { 2742 return false; 2743 } 2744 2745 // Check for duplicates and abort if found 2746 for (ChooserTargetInfo otherTargetInfo : mServiceTargets) { 2747 if (chooserTargetInfo.isSimilar(otherTargetInfo)) { 2748 return false; 2749 } 2750 } 2751 2752 int currentSize = mServiceTargets.size(); 2753 final float newScore = chooserTargetInfo.getModifiedScore(); 2754 for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) { 2755 final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); 2756 if (serviceTarget == null) { 2757 mServiceTargets.set(i, chooserTargetInfo); 2758 return true; 2759 } else if (newScore > serviceTarget.getModifiedScore()) { 2760 mServiceTargets.add(i, chooserTargetInfo); 2761 return true; 2762 } 2763 } 2764 2765 if (currentSize < MAX_SERVICE_TARGETS) { 2766 mServiceTargets.add(chooserTargetInfo); 2767 return true; 2768 } 2769 2770 return false; 2771 } 2772 } 2773 2774 static class BaseChooserTargetComparator implements Comparator<ChooserTarget> { 2775 @Override 2776 public int compare(ChooserTarget lhs, ChooserTarget rhs) { 2777 // Descending order 2778 return (int) Math.signum(rhs.getScore() - lhs.getScore()); 2779 } 2780 } 2781 2782 2783 private boolean isSendAction(Intent targetIntent) { 2784 if (targetIntent == null) { 2785 return false; 2786 } 2787 2788 String action = targetIntent.getAction(); 2789 if (action == null) { 2790 return false; 2791 } 2792 2793 if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { 2794 return true; 2795 } 2796 2797 return false; 2798 } 2799 2800 class ChooserRowAdapter extends BaseAdapter { 2801 private ChooserListAdapter mChooserListAdapter; 2802 private final LayoutInflater mLayoutInflater; 2803 2804 private DirectShareViewHolder mDirectShareViewHolder; 2805 private int mChooserTargetWidth = 0; 2806 private boolean mShowAzLabelIfPoss; 2807 2808 private boolean mHideContentPreview = false; 2809 private boolean mLayoutRequested = false; 2810 2811 private static final int VIEW_TYPE_DIRECT_SHARE = 0; 2812 private static final int VIEW_TYPE_NORMAL = 1; 2813 private static final int VIEW_TYPE_CONTENT_PREVIEW = 2; 2814 private static final int VIEW_TYPE_PROFILE = 3; 2815 private static final int VIEW_TYPE_AZ_LABEL = 4; 2816 2817 private static final int MAX_TARGETS_PER_ROW_PORTRAIT = 4; 2818 private static final int MAX_TARGETS_PER_ROW_LANDSCAPE = 8; 2819 2820 private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20; 2821 2822 public ChooserRowAdapter(ChooserListAdapter wrappedAdapter) { 2823 mChooserListAdapter = wrappedAdapter; 2824 mLayoutInflater = LayoutInflater.from(ChooserActivity.this); 2825 2826 mShowAzLabelIfPoss = getNumSheetExpansions() < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL; 2827 2828 wrappedAdapter.registerDataSetObserver(new DataSetObserver() { 2829 @Override 2830 public void onChanged() { 2831 super.onChanged(); 2832 notifyDataSetChanged(); 2833 } 2834 2835 @Override 2836 public void onInvalidated() { 2837 super.onInvalidated(); 2838 notifyDataSetInvalidated(); 2839 } 2840 }); 2841 } 2842 2843 /** 2844 * Calculate the chooser target width to maximize space per item 2845 * 2846 * @param width The new row width to use for recalculation 2847 * @return true if the view width has changed 2848 */ 2849 public boolean calculateChooserTargetWidth(int width) { 2850 if (width == 0) { 2851 return false; 2852 } 2853 2854 int newWidth = width / getMaxTargetsPerRow(); 2855 if (newWidth != mChooserTargetWidth) { 2856 mChooserTargetWidth = newWidth; 2857 return true; 2858 } 2859 2860 return false; 2861 } 2862 2863 private int getMaxTargetsPerRow() { 2864 int maxTargets = MAX_TARGETS_PER_ROW_PORTRAIT; 2865 if (shouldDisplayLandscape(getResources().getConfiguration().orientation)) { 2866 maxTargets = MAX_TARGETS_PER_ROW_LANDSCAPE; 2867 } 2868 2869 return maxTargets; 2870 } 2871 2872 public void hideContentPreview() { 2873 mHideContentPreview = true; 2874 mLayoutRequested = true; 2875 notifyDataSetChanged(); 2876 } 2877 2878 public boolean consumeLayoutRequest() { 2879 boolean oldValue = mLayoutRequested; 2880 mLayoutRequested = false; 2881 return oldValue; 2882 } 2883 2884 @Override 2885 public boolean areAllItemsEnabled() { 2886 return false; 2887 } 2888 2889 @Override 2890 public boolean isEnabled(int position) { 2891 int viewType = getItemViewType(position); 2892 if (viewType == VIEW_TYPE_CONTENT_PREVIEW || viewType == VIEW_TYPE_AZ_LABEL) { 2893 return false; 2894 } 2895 return true; 2896 } 2897 2898 @Override 2899 public int getCount() { 2900 return (int) ( 2901 getContentPreviewRowCount() 2902 + getProfileRowCount() 2903 + getServiceTargetRowCount() 2904 + getCallerAndRankedTargetRowCount() 2905 + getAzLabelRowCount() 2906 + Math.ceil( 2907 (float) mChooserListAdapter.getAlphaTargetCount() 2908 / getMaxTargetsPerRow()) 2909 ); 2910 } 2911 2912 public int getContentPreviewRowCount() { 2913 if (!isSendAction(getTargetIntent())) { 2914 return 0; 2915 } 2916 2917 if (mHideContentPreview || mChooserListAdapter == null 2918 || mChooserListAdapter.getCount() == 0) { 2919 return 0; 2920 } 2921 2922 return 1; 2923 } 2924 2925 public int getProfileRowCount() { 2926 return mChooserListAdapter.getOtherProfile() == null ? 0 : 1; 2927 } 2928 2929 public int getCallerAndRankedTargetRowCount() { 2930 return (int) Math.ceil( 2931 ((float) mChooserListAdapter.getCallerTargetCount() 2932 + mChooserListAdapter.getRankedTargetCount()) / getMaxTargetsPerRow()); 2933 } 2934 2935 // There can be at most one row in the listview, that is internally 2936 // a ViewGroup with 2 rows 2937 public int getServiceTargetRowCount() { 2938 if (isSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) { 2939 return 1; 2940 } 2941 return 0; 2942 } 2943 2944 public int getAzLabelRowCount() { 2945 // Only show a label if the a-z list is showing 2946 return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0; 2947 } 2948 2949 @Override 2950 public Object getItem(int position) { 2951 // We have nothing useful to return here. 2952 return position; 2953 } 2954 2955 @Override 2956 public long getItemId(int position) { 2957 return position; 2958 } 2959 2960 @Override 2961 public View getView(int position, View convertView, ViewGroup parent) { 2962 final RowViewHolder holder; 2963 int viewType = getItemViewType(position); 2964 2965 if (viewType == VIEW_TYPE_CONTENT_PREVIEW) { 2966 return createContentPreviewView(convertView, parent); 2967 } 2968 2969 if (viewType == VIEW_TYPE_PROFILE) { 2970 return createProfileView(convertView, parent); 2971 } 2972 2973 if (viewType == VIEW_TYPE_AZ_LABEL) { 2974 return createAzLabelView(parent); 2975 } 2976 2977 if (convertView == null) { 2978 holder = createViewHolder(viewType, parent); 2979 } else { 2980 holder = (RowViewHolder) convertView.getTag(); 2981 } 2982 2983 bindViewHolder(position, holder); 2984 2985 return holder.getViewGroup(); 2986 } 2987 2988 @Override 2989 public int getItemViewType(int position) { 2990 int count; 2991 2992 int countSum = (count = getContentPreviewRowCount()); 2993 if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW; 2994 2995 countSum += (count = getProfileRowCount()); 2996 if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE; 2997 2998 countSum += (count = getServiceTargetRowCount()); 2999 if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE; 3000 3001 countSum += (count = getCallerAndRankedTargetRowCount()); 3002 if (count > 0 && position < countSum) return VIEW_TYPE_NORMAL; 3003 3004 countSum += (count = getAzLabelRowCount()); 3005 if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL; 3006 3007 return VIEW_TYPE_NORMAL; 3008 } 3009 3010 @Override 3011 public int getViewTypeCount() { 3012 return 5; 3013 } 3014 3015 private ViewGroup createContentPreviewView(View convertView, ViewGroup parent) { 3016 Intent targetIntent = getTargetIntent(); 3017 int previewType = findPreferredContentPreview(targetIntent, getContentResolver()); 3018 3019 if (convertView == null) { 3020 getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW) 3021 .setSubtype(previewType)); 3022 } 3023 3024 return displayContentPreview(previewType, targetIntent, mLayoutInflater, 3025 (ViewGroup) convertView, parent); 3026 } 3027 3028 private View createProfileView(View convertView, ViewGroup parent) { 3029 View profileRow = convertView != null ? convertView : mLayoutInflater.inflate( 3030 R.layout.chooser_profile_row, parent, false); 3031 profileRow.setBackground( 3032 getResources().getDrawable(R.drawable.chooser_row_layer_list, null)); 3033 mProfileView = profileRow.findViewById(R.id.profile_button); 3034 mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); 3035 bindProfileView(); 3036 return profileRow; 3037 } 3038 3039 private View createAzLabelView(ViewGroup parent) { 3040 return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false); 3041 } 3042 3043 private RowViewHolder loadViewsIntoRow(RowViewHolder holder) { 3044 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 3045 final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth, 3046 MeasureSpec.EXACTLY); 3047 int columnCount = holder.getColumnCount(); 3048 3049 final boolean isDirectShare = holder instanceof DirectShareViewHolder; 3050 3051 for (int i = 0; i < columnCount; i++) { 3052 final View v = mChooserListAdapter.createView(holder.getRowByIndex(i)); 3053 final int column = i; 3054 v.setOnClickListener(new OnClickListener() { 3055 @Override 3056 public void onClick(View v) { 3057 startSelected(holder.getItemIndex(column), false, true); 3058 } 3059 }); 3060 v.setOnLongClickListener(new OnLongClickListener() { 3061 @Override 3062 public boolean onLongClick(View v) { 3063 showTargetDetails( 3064 mChooserListAdapter.resolveInfoForPosition( 3065 holder.getItemIndex(column), true)); 3066 return true; 3067 } 3068 }); 3069 ViewGroup row = holder.addView(i, v); 3070 3071 // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll = 3072 // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be 3073 // done before measuring. 3074 if (isDirectShare) { 3075 final ViewHolder vh = (ViewHolder) v.getTag(); 3076 vh.text.setLines(2); 3077 vh.text.setHorizontallyScrolling(false); 3078 vh.text2.setVisibility(View.GONE); 3079 } 3080 3081 // Force height to be a given so we don't have visual disruption during scaling. 3082 v.measure(exactSpec, spec); 3083 setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight()); 3084 } 3085 3086 final ViewGroup viewGroup = holder.getViewGroup(); 3087 3088 // Pre-measure and fix height so we can scale later. 3089 holder.measure(); 3090 setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight()); 3091 3092 if (isDirectShare) { 3093 DirectShareViewHolder dsvh = (DirectShareViewHolder) holder; 3094 setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight()); 3095 setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight()); 3096 } 3097 3098 viewGroup.setTag(holder); 3099 3100 return holder; 3101 } 3102 3103 private void setViewBounds(View view, int widthPx, int heightPx) { 3104 LayoutParams lp = view.getLayoutParams(); 3105 if (lp == null) { 3106 lp = new LayoutParams(widthPx, heightPx); 3107 view.setLayoutParams(lp); 3108 } else { 3109 lp.height = heightPx; 3110 lp.width = widthPx; 3111 } 3112 } 3113 3114 RowViewHolder createViewHolder(int viewType, ViewGroup parent) { 3115 if (viewType == VIEW_TYPE_DIRECT_SHARE) { 3116 ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate( 3117 R.layout.chooser_row_direct_share, parent, false); 3118 ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, 3119 parentGroup, false); 3120 ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, 3121 parentGroup, false); 3122 parentGroup.addView(row1); 3123 parentGroup.addView(row2); 3124 3125 mDirectShareViewHolder = new DirectShareViewHolder(parentGroup, 3126 Lists.newArrayList(row1, row2), getMaxTargetsPerRow()); 3127 loadViewsIntoRow(mDirectShareViewHolder); 3128 3129 return mDirectShareViewHolder; 3130 } else { 3131 ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parent, 3132 false); 3133 RowViewHolder holder = new SingleRowViewHolder(row, getMaxTargetsPerRow()); 3134 loadViewsIntoRow(holder); 3135 3136 return holder; 3137 } 3138 } 3139 3140 /** 3141 * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from 3142 * showing on top of the AZ list if the AZ label is visible. All other types are placed into 3143 * their own row as determined by their target type, and dividers are added in the list to 3144 * separate each type. 3145 */ 3146 int getRowType(int rowPosition) { 3147 // Merge caller and ranked standard into a single row 3148 int positionType = mChooserListAdapter.getPositionTargetType(rowPosition); 3149 if (positionType == ChooserListAdapter.TARGET_CALLER) { 3150 return ChooserListAdapter.TARGET_STANDARD; 3151 } 3152 3153 // If an the A-Z label is shown, prevent a separator from appearing by making the A-Z 3154 // row type the same as the suggestion row type 3155 if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) { 3156 return ChooserListAdapter.TARGET_STANDARD; 3157 } 3158 3159 return positionType; 3160 } 3161 3162 void bindViewHolder(int rowPosition, RowViewHolder holder) { 3163 final int start = getFirstRowPosition(rowPosition); 3164 final int startType = getRowType(start); 3165 final int lastStartType = getRowType(getFirstRowPosition(rowPosition - 1)); 3166 3167 final ViewGroup row = holder.getViewGroup(); 3168 3169 if (startType != lastStartType 3170 || rowPosition == getContentPreviewRowCount() + getProfileRowCount()) { 3171 row.setForeground( 3172 getResources().getDrawable(R.drawable.chooser_row_layer_list, null)); 3173 } else { 3174 row.setForeground(null); 3175 } 3176 3177 int columnCount = holder.getColumnCount(); 3178 int end = start + columnCount - 1; 3179 while (getRowType(end) != startType && end >= start) { 3180 end--; 3181 } 3182 3183 if (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) { 3184 final TextView textView = row.findViewById(R.id.chooser_row_text_option); 3185 3186 if (textView.getVisibility() != View.VISIBLE) { 3187 textView.setAlpha(0.0f); 3188 textView.setVisibility(View.VISIBLE); 3189 textView.setText(R.string.chooser_no_direct_share_targets); 3190 3191 ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f); 3192 fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f)); 3193 3194 float translationInPx = getResources().getDimensionPixelSize( 3195 R.dimen.chooser_row_text_option_translate); 3196 textView.setTranslationY(translationInPx); 3197 ValueAnimator translateAnim = ObjectAnimator.ofFloat(textView, "translationY", 3198 0.0f); 3199 translateAnim.setInterpolator(new DecelerateInterpolator(1.0f)); 3200 3201 AnimatorSet animSet = new AnimatorSet(); 3202 animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS); 3203 animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS); 3204 animSet.playTogether(fadeAnim, translateAnim); 3205 animSet.start(); 3206 } 3207 } 3208 3209 for (int i = 0; i < columnCount; i++) { 3210 final View v = holder.getView(i); 3211 if (start + i <= end) { 3212 holder.setViewVisibility(i, View.VISIBLE); 3213 holder.setItemIndex(i, start + i); 3214 mChooserListAdapter.bindView(holder.getItemIndex(i), v); 3215 } else { 3216 holder.setViewVisibility(i, View.INVISIBLE); 3217 } 3218 } 3219 } 3220 3221 int getFirstRowPosition(int row) { 3222 row -= getContentPreviewRowCount() + getProfileRowCount(); 3223 3224 final int serviceCount = mChooserListAdapter.getServiceTargetCount(); 3225 final int serviceRows = (int) Math.ceil((float) serviceCount 3226 / ChooserListAdapter.MAX_SERVICE_TARGETS); 3227 if (row < serviceRows) { 3228 return row * getMaxTargetsPerRow(); 3229 } 3230 3231 final int callerAndRankedCount = mChooserListAdapter.getCallerTargetCount() 3232 + mChooserListAdapter.getRankedTargetCount(); 3233 final int callerAndRankedRows = getCallerAndRankedTargetRowCount(); 3234 if (row < callerAndRankedRows + serviceRows) { 3235 return serviceCount + (row - serviceRows) * getMaxTargetsPerRow(); 3236 } 3237 3238 row -= getAzLabelRowCount(); 3239 3240 return callerAndRankedCount + serviceCount 3241 + (row - callerAndRankedRows - serviceRows) * getMaxTargetsPerRow(); 3242 } 3243 3244 public void handleScroll(View v, int y, int oldy) { 3245 // Only expand direct share area if there is a minimum number of shortcuts, 3246 // which will help reduce the amount of visible shuffling due to older-style 3247 // direct share targets. 3248 int orientation = getResources().getConfiguration().orientation; 3249 boolean canExpandDirectShare = 3250 mChooserListAdapter.getNumShortcutResults() > getMaxTargetsPerRow() 3251 && orientation == Configuration.ORIENTATION_PORTRAIT 3252 && !isInMultiWindowMode(); 3253 3254 if (mDirectShareViewHolder != null && canExpandDirectShare) { 3255 mDirectShareViewHolder.handleScroll(mAdapterView, y, oldy, getMaxTargetsPerRow()); 3256 } 3257 } 3258 } 3259 3260 abstract class RowViewHolder { 3261 protected int mMeasuredRowHeight; 3262 private int[] mItemIndices; 3263 protected final View[] mCells; 3264 private final int mColumnCount; 3265 3266 RowViewHolder(int cellCount) { 3267 this.mCells = new View[cellCount]; 3268 this.mItemIndices = new int[cellCount]; 3269 this.mColumnCount = cellCount; 3270 } 3271 3272 abstract ViewGroup addView(int index, View v); 3273 3274 abstract ViewGroup getViewGroup(); 3275 3276 abstract ViewGroup getRowByIndex(int index); 3277 3278 abstract ViewGroup getRow(int rowNumber); 3279 3280 abstract void setViewVisibility(int i, int visibility); 3281 3282 public int getColumnCount() { 3283 return mColumnCount; 3284 } 3285 3286 public void measure() { 3287 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 3288 getViewGroup().measure(spec, spec); 3289 mMeasuredRowHeight = getViewGroup().getMeasuredHeight(); 3290 } 3291 3292 public int getMeasuredRowHeight() { 3293 return mMeasuredRowHeight; 3294 } 3295 3296 public void setItemIndex(int itemIndex, int listIndex) { 3297 mItemIndices[itemIndex] = listIndex; 3298 } 3299 3300 public int getItemIndex(int itemIndex) { 3301 return mItemIndices[itemIndex]; 3302 } 3303 3304 public View getView(int index) { 3305 return mCells[index]; 3306 } 3307 } 3308 3309 class SingleRowViewHolder extends RowViewHolder { 3310 private final ViewGroup mRow; 3311 3312 SingleRowViewHolder(ViewGroup row, int cellCount) { 3313 super(cellCount); 3314 3315 this.mRow = row; 3316 } 3317 3318 public ViewGroup getViewGroup() { 3319 return mRow; 3320 } 3321 3322 public ViewGroup getRowByIndex(int index) { 3323 return mRow; 3324 } 3325 3326 public ViewGroup getRow(int rowNumber) { 3327 if (rowNumber == 0) return mRow; 3328 return null; 3329 } 3330 3331 public ViewGroup addView(int index, View v) { 3332 mRow.addView(v); 3333 mCells[index] = v; 3334 3335 return mRow; 3336 } 3337 3338 public void setViewVisibility(int i, int visibility) { 3339 getView(i).setVisibility(visibility); 3340 } 3341 } 3342 3343 class DirectShareViewHolder extends RowViewHolder { 3344 private final ViewGroup mParent; 3345 private final List<ViewGroup> mRows; 3346 private int mCellCountPerRow; 3347 3348 private boolean mHideDirectShareExpansion = false; 3349 private int mDirectShareMinHeight = 0; 3350 private int mDirectShareCurrHeight = 0; 3351 private int mDirectShareMaxHeight = 0; 3352 3353 private final boolean[] mCellVisibility; 3354 3355 DirectShareViewHolder(ViewGroup parent, List<ViewGroup> rows, int cellCountPerRow) { 3356 super(rows.size() * cellCountPerRow); 3357 3358 this.mParent = parent; 3359 this.mRows = rows; 3360 this.mCellCountPerRow = cellCountPerRow; 3361 this.mCellVisibility = new boolean[rows.size() * cellCountPerRow]; 3362 } 3363 3364 public ViewGroup addView(int index, View v) { 3365 ViewGroup row = getRowByIndex(index); 3366 row.addView(v); 3367 mCells[index] = v; 3368 3369 return row; 3370 } 3371 3372 public ViewGroup getViewGroup() { 3373 return mParent; 3374 } 3375 3376 public ViewGroup getRowByIndex(int index) { 3377 return mRows.get(index / mCellCountPerRow); 3378 } 3379 3380 public ViewGroup getRow(int rowNumber) { 3381 return mRows.get(rowNumber); 3382 } 3383 3384 public void measure() { 3385 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 3386 getRow(0).measure(spec, spec); 3387 getRow(1).measure(spec, spec); 3388 3389 mDirectShareMinHeight = getRow(0).getMeasuredHeight(); 3390 mDirectShareCurrHeight = mDirectShareCurrHeight > 0 3391 ? mDirectShareCurrHeight : mDirectShareMinHeight; 3392 mDirectShareMaxHeight = 2 * mDirectShareMinHeight; 3393 } 3394 3395 public int getMeasuredRowHeight() { 3396 return mDirectShareCurrHeight; 3397 } 3398 3399 public int getMinRowHeight() { 3400 return mDirectShareMinHeight; 3401 } 3402 3403 public void setViewVisibility(int i, int visibility) { 3404 final View v = getView(i); 3405 if (visibility == View.VISIBLE) { 3406 mCellVisibility[i] = true; 3407 v.setVisibility(visibility); 3408 v.setAlpha(1.0f); 3409 } else if (visibility == View.INVISIBLE && mCellVisibility[i]) { 3410 mCellVisibility[i] = false; 3411 3412 ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f); 3413 fadeAnim.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS); 3414 fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f)); 3415 fadeAnim.addListener(new AnimatorListenerAdapter() { 3416 public void onAnimationEnd(Animator animation) { 3417 v.setVisibility(View.INVISIBLE); 3418 } 3419 }); 3420 fadeAnim.start(); 3421 } 3422 } 3423 3424 public void handleScroll(AbsListView view, int y, int oldy, int maxTargetsPerRow) { 3425 // only exit early if fully collapsed, otherwise onListRebuilt() with shifting 3426 // targets can lock us into an expanded mode 3427 boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight; 3428 if (notExpanded) { 3429 if (mHideDirectShareExpansion) { 3430 return; 3431 } 3432 3433 // only expand if we have more than maxTargetsPerRow, and delay that decision 3434 // until they start to scroll 3435 if (mChooserListAdapter.getSelectableServiceTargetCount() <= maxTargetsPerRow) { 3436 mHideDirectShareExpansion = true; 3437 return; 3438 } 3439 } 3440 3441 int yDiff = (int) ((oldy - y) * DIRECT_SHARE_EXPANSION_RATE); 3442 3443 int prevHeight = mDirectShareCurrHeight; 3444 int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight); 3445 newHeight = Math.max(newHeight, mDirectShareMinHeight); 3446 yDiff = newHeight - prevHeight; 3447 3448 if (view == null || view.getChildCount() == 0 || yDiff == 0) { 3449 return; 3450 } 3451 3452 // locate the item to expand, and offset the rows below that one 3453 boolean foundExpansion = false; 3454 for (int i = 0; i < view.getChildCount(); i++) { 3455 View child = view.getChildAt(i); 3456 3457 if (foundExpansion) { 3458 child.offsetTopAndBottom(yDiff); 3459 } else { 3460 if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) { 3461 int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(), 3462 MeasureSpec.EXACTLY); 3463 int heightSpec = MeasureSpec.makeMeasureSpec(newHeight, 3464 MeasureSpec.EXACTLY); 3465 child.measure(widthSpec, heightSpec); 3466 child.getLayoutParams().height = child.getMeasuredHeight(); 3467 child.layout(child.getLeft(), child.getTop(), child.getRight(), 3468 child.getTop() + child.getMeasuredHeight()); 3469 3470 foundExpansion = true; 3471 } 3472 } 3473 } 3474 3475 if (foundExpansion) { 3476 mDirectShareCurrHeight = newHeight; 3477 } 3478 } 3479 } 3480 3481 static class ChooserTargetServiceConnection implements ServiceConnection { 3482 private DisplayResolveInfo mOriginalTarget; 3483 private ComponentName mConnectedComponent; 3484 private ChooserActivity mChooserActivity; 3485 private final Object mLock = new Object(); 3486 3487 private final IChooserTargetResult mChooserTargetResult = new IChooserTargetResult.Stub() { 3488 @Override 3489 public void sendResult(List<ChooserTarget> targets) throws RemoteException { 3490 synchronized (mLock) { 3491 if (mChooserActivity == null) { 3492 Log.e(TAG, "destroyed ChooserTargetServiceConnection received result from " 3493 + mConnectedComponent + "; ignoring..."); 3494 return; 3495 } 3496 mChooserActivity.filterServiceTargets( 3497 mOriginalTarget.getResolveInfo().activityInfo.packageName, targets); 3498 final Message msg = Message.obtain(); 3499 msg.what = ChooserHandler.CHOOSER_TARGET_SERVICE_RESULT; 3500 msg.obj = new ServiceResultInfo(mOriginalTarget, targets, 3501 ChooserTargetServiceConnection.this); 3502 mChooserActivity.mChooserHandler.sendMessage(msg); 3503 } 3504 } 3505 }; 3506 3507 public ChooserTargetServiceConnection(ChooserActivity chooserActivity, 3508 DisplayResolveInfo dri) { 3509 mChooserActivity = chooserActivity; 3510 mOriginalTarget = dri; 3511 } 3512 3513 @Override 3514 public void onServiceConnected(ComponentName name, IBinder service) { 3515 if (DEBUG) Log.d(TAG, "onServiceConnected: " + name); 3516 synchronized (mLock) { 3517 if (mChooserActivity == null) { 3518 Log.e(TAG, "destroyed ChooserTargetServiceConnection got onServiceConnected"); 3519 return; 3520 } 3521 3522 final IChooserTargetService icts = IChooserTargetService.Stub.asInterface(service); 3523 try { 3524 icts.getChooserTargets(mOriginalTarget.getResolvedComponentName(), 3525 mOriginalTarget.getResolveInfo().filter, mChooserTargetResult); 3526 } catch (RemoteException e) { 3527 Log.e(TAG, "Querying ChooserTargetService " + name + " failed.", e); 3528 mChooserActivity.unbindService(this); 3529 mChooserActivity.mServiceConnections.remove(this); 3530 destroy(); 3531 } 3532 } 3533 } 3534 3535 @Override 3536 public void onServiceDisconnected(ComponentName name) { 3537 if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name); 3538 synchronized (mLock) { 3539 if (mChooserActivity == null) { 3540 Log.e(TAG, 3541 "destroyed ChooserTargetServiceConnection got onServiceDisconnected"); 3542 return; 3543 } 3544 3545 mChooserActivity.unbindService(this); 3546 mChooserActivity.mServiceConnections.remove(this); 3547 if (mChooserActivity.mServiceConnections.isEmpty()) { 3548 mChooserActivity.sendVoiceChoicesIfNeeded(); 3549 } 3550 mConnectedComponent = null; 3551 destroy(); 3552 } 3553 } 3554 3555 public void destroy() { 3556 synchronized (mLock) { 3557 mChooserActivity = null; 3558 mOriginalTarget = null; 3559 } 3560 } 3561 3562 @Override 3563 public String toString() { 3564 return "ChooserTargetServiceConnection{service=" 3565 + mConnectedComponent + ", activity=" 3566 + (mOriginalTarget != null 3567 ? mOriginalTarget.getResolveInfo().activityInfo.toString() 3568 : "<connection destroyed>") + "}"; 3569 } 3570 } 3571 3572 static class ServiceResultInfo { 3573 public final DisplayResolveInfo originalTarget; 3574 public final List<ChooserTarget> resultTargets; 3575 public final ChooserTargetServiceConnection connection; 3576 3577 public ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt, 3578 ChooserTargetServiceConnection c) { 3579 originalTarget = ot; 3580 resultTargets = rt; 3581 connection = c; 3582 } 3583 } 3584 3585 static class RefinementResultReceiver extends ResultReceiver { 3586 private ChooserActivity mChooserActivity; 3587 private TargetInfo mSelectedTarget; 3588 3589 public RefinementResultReceiver(ChooserActivity host, TargetInfo target, 3590 Handler handler) { 3591 super(handler); 3592 mChooserActivity = host; 3593 mSelectedTarget = target; 3594 } 3595 3596 @Override 3597 protected void onReceiveResult(int resultCode, Bundle resultData) { 3598 if (mChooserActivity == null) { 3599 Log.e(TAG, "Destroyed RefinementResultReceiver received a result"); 3600 return; 3601 } 3602 if (resultData == null) { 3603 Log.e(TAG, "RefinementResultReceiver received null resultData"); 3604 return; 3605 } 3606 3607 switch (resultCode) { 3608 case RESULT_CANCELED: 3609 mChooserActivity.onRefinementCanceled(); 3610 break; 3611 case RESULT_OK: 3612 Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT); 3613 if (intentParcelable instanceof Intent) { 3614 mChooserActivity.onRefinementResult(mSelectedTarget, 3615 (Intent) intentParcelable); 3616 } else { 3617 Log.e(TAG, "RefinementResultReceiver received RESULT_OK but no Intent" 3618 + " in resultData with key Intent.EXTRA_INTENT"); 3619 } 3620 break; 3621 default: 3622 Log.w(TAG, "Unknown result code " + resultCode 3623 + " sent to RefinementResultReceiver"); 3624 break; 3625 } 3626 } 3627 3628 public void destroy() { 3629 mChooserActivity = null; 3630 mSelectedTarget = null; 3631 } 3632 } 3633 3634 /** 3635 * Used internally to round image corners while obeying view padding. 3636 */ 3637 public static class RoundedRectImageView extends ImageView { 3638 private int mRadius = 0; 3639 private Path mPath = new Path(); 3640 private Paint mOverlayPaint = new Paint(0); 3641 private Paint mRoundRectPaint = new Paint(0); 3642 private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 3643 private String mExtraImageCount = null; 3644 3645 public RoundedRectImageView(Context context) { 3646 super(context); 3647 } 3648 3649 public RoundedRectImageView(Context context, AttributeSet attrs) { 3650 this(context, attrs, 0); 3651 } 3652 3653 public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr) { 3654 this(context, attrs, defStyleAttr, 0); 3655 } 3656 3657 public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr, 3658 int defStyleRes) { 3659 super(context, attrs, defStyleAttr, defStyleRes); 3660 mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius); 3661 3662 mOverlayPaint.setColor(0x99000000); 3663 mOverlayPaint.setStyle(Paint.Style.FILL); 3664 3665 mRoundRectPaint.setColor(context.getResources().getColor(R.color.chooser_row_divider)); 3666 mRoundRectPaint.setStyle(Paint.Style.STROKE); 3667 mRoundRectPaint.setStrokeWidth(context.getResources() 3668 .getDimensionPixelSize(R.dimen.chooser_preview_image_border)); 3669 3670 mTextPaint.setColor(Color.WHITE); 3671 mTextPaint.setTextSize(context.getResources() 3672 .getDimensionPixelSize(R.dimen.chooser_preview_image_font_size)); 3673 mTextPaint.setTextAlign(Paint.Align.CENTER); 3674 } 3675 3676 private void updatePath(int width, int height) { 3677 mPath.reset(); 3678 3679 int imageWidth = width - getPaddingRight() - getPaddingLeft(); 3680 int imageHeight = height - getPaddingBottom() - getPaddingTop(); 3681 mPath.addRoundRect(getPaddingLeft(), getPaddingTop(), imageWidth, imageHeight, mRadius, 3682 mRadius, Path.Direction.CW); 3683 } 3684 3685 /** 3686 * Sets the corner radius on all corners 3687 * 3688 * param radius 0 for no radius, > 0 for a visible corner radius 3689 */ 3690 public void setRadius(int radius) { 3691 mRadius = radius; 3692 updatePath(getWidth(), getHeight()); 3693 } 3694 3695 /** 3696 * Display an overlay with extra image count on 3rd image 3697 */ 3698 public void setExtraImageCount(int count) { 3699 if (count > 0) { 3700 this.mExtraImageCount = "+" + count; 3701 } else { 3702 this.mExtraImageCount = null; 3703 } 3704 } 3705 3706 @Override 3707 protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { 3708 super.onSizeChanged(width, height, oldWidth, oldHeight); 3709 updatePath(width, height); 3710 } 3711 3712 @Override 3713 protected void onDraw(Canvas canvas) { 3714 if (mRadius != 0) { 3715 canvas.clipPath(mPath); 3716 } 3717 3718 super.onDraw(canvas); 3719 3720 int x = getPaddingLeft(); 3721 int y = getPaddingRight(); 3722 int width = getWidth() - getPaddingRight() - getPaddingLeft(); 3723 int height = getHeight() - getPaddingBottom() - getPaddingTop(); 3724 if (mExtraImageCount != null) { 3725 canvas.drawRect(x, y, width, height, mOverlayPaint); 3726 3727 int xPos = canvas.getWidth() / 2; 3728 int yPos = (int) ((canvas.getHeight() / 2.0f) 3729 - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f)); 3730 3731 canvas.drawText(mExtraImageCount, xPos, yPos, mTextPaint); 3732 } 3733 3734 canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint); 3735 } 3736 } 3737 } 3738