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