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