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