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