diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 7a6f7feb2d751..fd978f5329a5c 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -18,6 +18,11 @@ package com.android.internal.app; import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.annotation.IntDef; import android.app.Activity; import android.app.ActivityManager; @@ -90,6 +95,8 @@ import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.ImageView; @@ -152,11 +159,17 @@ public class ChooserActivity extends ResolverActivity { private static final boolean USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS = true; private static final boolean USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS = true; + /** + * The transition time between placeholders for direct share to a message + * indicating that non are available. + */ + private static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200; + // TODO(b/121287224): Re-evaluate this limit private static final int SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20; private static final int QUERY_TARGET_SERVICE_LIMIT = 5; - private static final int WATCHDOG_TIMEOUT_MILLIS = 2000; + private static final int WATCHDOG_TIMEOUT_MILLIS = 3000; private Bundle mReplacementExtras; private IntentSender mChosenComponentSender; @@ -172,6 +185,8 @@ public class ChooserActivity extends ResolverActivity { private ChooserListAdapter mChooserListAdapter; private ChooserRowAdapter mChooserRowAdapter; + private Drawable mChooserRowLayer; + private int mChooserRowServiceSpacing; private SharedPreferences mPinnedSharedPrefs; private static final float PINNED_TARGET_SCORE_BOOST = 1000.f; @@ -220,7 +235,6 @@ public class ChooserActivity extends ResolverActivity { sri.connection.destroy(); mServiceConnections.remove(sri.connection); if (mServiceConnections.isEmpty()) { - mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); sendVoiceChoicesIfNeeded(); mChooserListAdapter.setShowServiceTargets(true); } @@ -230,8 +244,12 @@ public class ChooserActivity extends ResolverActivity { if (DEBUG) { Log.d(TAG, "CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT; unbinding services"); } + if (isDestroyed()) { + break; + } unbindRemainingServices(); sendVoiceChoicesIfNeeded(); + mChooserListAdapter.completeServiceTargetLoading(); mChooserListAdapter.setShowServiceTargets(true); break; @@ -399,11 +417,17 @@ public class ChooserActivity extends ResolverActivity { .setExtras(extras) .build()); mAppPredictorCallback = resultList -> { + if (isFinishing() || isDestroyed()) { + return; + } final List driList = getDisplayResolveInfos(mChooserListAdapter); final List shareShortcutInfos = new ArrayList<>(); for (AppTarget appTarget : resultList) { + if (appTarget.getShortcutInfo() == null) { + continue; + } shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo( appTarget.getShortcutInfo(), new ComponentName( @@ -414,6 +438,10 @@ public class ChooserActivity extends ResolverActivity { mAppPredictor.registerPredictionUpdates(this.getMainExecutor(), mAppPredictorCallback); } + mChooserRowLayer = getResources().getDrawable(R.drawable.chooser_row_layer_list, null); + mChooserRowServiceSpacing = getResources() + .getDimensionPixelSize(R.dimen.chooser_service_spacing); + if (DEBUG) { Log.d(TAG, "System Time Cost is " + systemCost); } @@ -919,6 +947,10 @@ public class ChooserActivity extends ResolverActivity { @Override protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { + if (target instanceof NotSelectableTargetInfo) { + return false; + } + if (mRefinementIntentSender != null) { final Intent fillIn = new Intent(); final List sourceIntents = target.getAllSourceIntents(); @@ -1064,14 +1096,14 @@ public class ChooserActivity extends ResolverActivity { } } - if (!mServiceConnections.isEmpty()) { - if (DEBUG) { - Log.d(TAG, "queryTargets setting watchdog timer for " - + WATCHDOG_TIMEOUT_MILLIS + "ms"); - } - mChooserHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT, - WATCHDOG_TIMEOUT_MILLIS); - } else { + if (DEBUG) { + Log.d(TAG, "queryTargets setting watchdog timer for " + + WATCHDOG_TIMEOUT_MILLIS + "ms"); + } + mChooserHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT, + WATCHDOG_TIMEOUT_MILLIS); + + if (mServiceConnections.isEmpty()) { sendVoiceChoicesIfNeeded(); } } @@ -1213,7 +1245,6 @@ public class ChooserActivity extends ResolverActivity { conn.destroy(); } mServiceConnections.clear(); - mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); } public void onSetupVoiceInteraction() { @@ -1420,7 +1451,93 @@ public class ChooserActivity extends ResolverActivity { return null; } - final class ChooserTargetInfo implements TargetInfo { + interface ChooserTargetInfo extends TargetInfo { + float getModifiedScore(); + + ChooserTarget getChooserTarget(); + } + + /** + * Distinguish between targets that selectable by the user, vs those that are + * placeholders for the system while information is loading in an async manner. + */ + abstract class NotSelectableTargetInfo implements ChooserTargetInfo { + + public Intent getResolvedIntent() { + return null; + } + + public ComponentName getResolvedComponentName() { + return null; + } + + public boolean start(Activity activity, Bundle options) { + return false; + } + + public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { + return false; + } + + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + return false; + } + + public ResolveInfo getResolveInfo() { + return null; + } + + public CharSequence getDisplayLabel() { + return null; + } + + public CharSequence getExtendedInfo() { + return null; + } + + public Drawable getBadgeIcon() { + return null; + } + + public CharSequence getBadgeContentDescription() { + return null; + } + + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return null; + } + + public List getAllSourceIntents() { + return null; + } + + public boolean isPinned() { + return false; + } + + public float getModifiedScore() { + return 0.1f; + } + + public ChooserTarget getChooserTarget() { + return null; + } + } + + final class PlaceHolderTargetInfo extends NotSelectableTargetInfo { + public Drawable getDisplayIcon() { + return getDrawable(R.drawable.resolver_icon_placeholder); + } + } + + + final class EmptyTargetInfo extends NotSelectableTargetInfo { + public Drawable getDisplayIcon() { + return null; + } + } + + final class SelectableTargetInfo implements ChooserTargetInfo { private final DisplayResolveInfo mSourceInfo; private final ResolveInfo mBackupResolveInfo; private final ChooserTarget mChooserTarget; @@ -1431,7 +1548,7 @@ public class ChooserActivity extends ResolverActivity { private final int mFillInFlags; private final float mModifiedScore; - public ChooserTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, + SelectableTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, float modifiedScore) { mSourceInfo = sourceInfo; mChooserTarget = chooserTarget; @@ -1460,7 +1577,7 @@ public class ChooserActivity extends ResolverActivity { mFillInFlags = 0; } - private ChooserTargetInfo(ChooserTargetInfo other, Intent fillInIntent, int flags) { + private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) { mSourceInfo = other.mSourceInfo; mBackupResolveInfo = other.mBackupResolveInfo; mChooserTarget = other.mChooserTarget; @@ -1616,7 +1733,7 @@ public class ChooserActivity extends ResolverActivity { @Override public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { - return new ChooserTargetInfo(this, fillInIntent, flags); + return new SelectableTargetInfo(this, fillInIntent, flags); } @Override @@ -1644,7 +1761,9 @@ public class ChooserActivity extends ResolverActivity { private static final int MAX_SERVICE_TARGETS = 4; private static final int MAX_TARGETS_PER_SERVICE = 2; - private final List mServiceTargets = new ArrayList<>(); + // Reserve spots for incoming direct share targets by adding placeholders + private ChooserTargetInfo mPlaceHolderTargetInfo = new PlaceHolderTargetInfo(); + private List mServiceTargets; private final List mCallerTargets = new ArrayList<>(); private boolean mShowServiceTargets; @@ -1663,6 +1782,8 @@ public class ChooserActivity extends ResolverActivity { super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed, resolverListController); + mServiceTargets = createPlaceHolders(); + if (initialIntents != null) { final PackageManager pm = getPackageManager(); for (int i = 0; i < initialIntents.length; i++) { @@ -1715,6 +1836,14 @@ public class ChooserActivity extends ResolverActivity { } } + private List createPlaceHolders() { + List list = new ArrayList<>(); + for (int i = 0; i < MAX_SERVICE_TARGETS; i++) { + list.add(mPlaceHolderTargetInfo); + } + return list; + } + @Override public boolean showsExtendedInfo(TargetInfo info) { // We have badges so we don't need this text shown. @@ -1770,22 +1899,33 @@ public class ChooserActivity extends ResolverActivity { @Override public int getCount() { - return super.getCount() + getServiceTargetCount() + getCallerTargetCount(); + return super.getCount() + getSelectableServiceTargetCount() + getCallerTargetCount(); } @Override public int getUnfilteredCount() { - return super.getUnfilteredCount() + getServiceTargetCount() + getCallerTargetCount(); + return super.getUnfilteredCount() + getSelectableServiceTargetCount() + + getCallerTargetCount(); } public int getCallerTargetCount() { return mCallerTargets.size(); } - public int getServiceTargetCount() { - if (!mShowServiceTargets) { - return 0; + /** + * Filter out placeholders and non-selectable service targets + */ + public int getSelectableServiceTargetCount() { + int count = 0; + for (ChooserTargetInfo info : mServiceTargets) { + if (info instanceof SelectableTargetInfo) { + count++; + } } + return count; + } + + public int getServiceTargetCount() { return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS); } @@ -1831,7 +1971,8 @@ public class ChooserActivity extends ResolverActivity { } offset += callerTargetCount; - final int serviceTargetCount = getServiceTargetCount(); + final int serviceTargetCount = filtered ? getServiceTargetCount() : + getSelectableServiceTargetCount(); if (position - offset < serviceTargetCount) { return mServiceTargets.get(position - offset); } @@ -1850,8 +1991,14 @@ public class ChooserActivity extends ResolverActivity { if (mTargetsNeedPruning && targets.size() > 0) { // First proper update since we got an onListRebuilt() with (transient) 0 items. // Clear out the target list and rebuild. - mServiceTargets.clear(); + mServiceTargets = createPlaceHolders(); mTargetsNeedPruning = false; + + // Add back any app-supplied direct share targets that may have been + // wiped by this clear + if (mCallerChooserTargets != null) { + addServiceResults(null, Lists.newArrayList(mCallerChooserTargets)); + } } final float parentScore = getScore(origTarget); @@ -1867,7 +2014,7 @@ public class ChooserActivity extends ResolverActivity { // This incents ChooserTargetServices to define what's truly better. targetScore = lastScore * 0.95f; } - insertServiceTarget(new ChooserTargetInfo(origTarget, target, targetScore)); + insertServiceTarget(new SelectableTargetInfo(origTarget, target, targetScore)); if (DEBUG) { Log.d(TAG, " => " + target.toString() + " score=" + targetScore @@ -1905,11 +2052,33 @@ public class ChooserActivity extends ResolverActivity { } } + /** + * Calling this marks service target loading complete, and will attempt to no longer + * update the direct share area. + */ + public void completeServiceTargetLoading() { + mServiceTargets.removeIf(o -> o instanceof PlaceHolderTargetInfo); + + if (mServiceTargets.isEmpty()) { + mServiceTargets.add(new EmptyTargetInfo()); + } + notifyDataSetChanged(); + } + private void insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { + // Avoid inserting any potentially late results + if (mServiceTargets.size() == 1 + && mServiceTargets.get(0) instanceof EmptyTargetInfo) { + return; + } + final float newScore = chooserTargetInfo.getModifiedScore(); for (int i = 0, N = mServiceTargets.size(); i < N; i++) { final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); - if (newScore > serviceTarget.getModifiedScore()) { + if (serviceTarget == null) { + mServiceTargets.set(i, chooserTargetInfo); + return; + } else if (newScore > serviceTarget.getModifiedScore()) { mServiceTargets.add(i, chooserTargetInfo); return; } @@ -1968,7 +2137,7 @@ public class ChooserActivity extends ResolverActivity { // There can be at most one row of service targets. public int getServiceTargetRowCount() { - return (int) mChooserListAdapter.getServiceTargetCount() == 0 ? 0 : 1; + return 1; } @Override @@ -2054,58 +2223,81 @@ public class ChooserActivity extends ResolverActivity { final int start = getFirstRowPosition(rowPosition); final int startType = mChooserListAdapter.getPositionTargetType(start); + final int lastStartType = mChooserListAdapter.getPositionTargetType( + getFirstRowPosition(rowPosition - 1)); + + if (startType != lastStartType || rowPosition == 0) { + holder.row.setBackground(mChooserRowLayer); + setVertPadding(holder, mChooserRowServiceSpacing, 0); + } else { + holder.row.setBackground(null); + setVertPadding(holder, 0, 0); + } + int end = start + mColumnCount - 1; while (mChooserListAdapter.getPositionTargetType(end) != startType && end >= start) { end--; } - if (startType == ChooserListAdapter.TARGET_SERVICE) { - int nextStartType = mChooserListAdapter.getPositionTargetType( - getFirstRowPosition(rowPosition + 1)); - int serviceSpacing = holder.row.getContext().getResources() - .getDimensionPixelSize(R.dimen.chooser_service_spacing); - if (rowPosition == 0 && nextStartType != ChooserListAdapter.TARGET_SERVICE) { - // if the row is the only row for target service - setVertPadding(holder, 0, 0); - } else { - int top = rowPosition == 0 ? serviceSpacing : 0; - if (nextStartType != ChooserListAdapter.TARGET_SERVICE) { - setVertPadding(holder, top, serviceSpacing); - } else { - setVertPadding(holder, top, 0); - } - } - } else { - holder.row.setBackgroundColor(Color.TRANSPARENT); - int lastStartType = mChooserListAdapter.getPositionTargetType( - getFirstRowPosition(rowPosition - 1)); - if (lastStartType == ChooserListAdapter.TARGET_SERVICE || rowPosition == 0) { - int serviceSpacing = holder.row.getContext().getResources() - .getDimensionPixelSize(R.dimen.chooser_service_spacing); - setVertPadding(holder, serviceSpacing, 0); - } else { - setVertPadding(holder, 0, 0); - } - } + if (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) { + final TextView textView = holder.row.findViewById(R.id.chooser_row_text_option); - final int oldHeight = holder.row.getLayoutParams().height; - holder.row.getLayoutParams().height = Math.max(1, holder.measuredRowHeight); - if (holder.row.getLayoutParams().height != oldHeight) { - holder.row.requestLayout(); + if (textView.getVisibility() != View.VISIBLE) { + textView.setAlpha(0.0f); + textView.setVisibility(View.VISIBLE); + textView.setText(R.string.chooser_no_direct_share_targets); + + ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f); + fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f)); + + float translationInPx = getResources().getDimensionPixelSize( + R.dimen.chooser_row_text_option_translate); + textView.setTranslationY(translationInPx); + ValueAnimator translateAnim = ObjectAnimator.ofFloat(textView, "translationY", + 0.0f); + translateAnim.setInterpolator(new DecelerateInterpolator(1.0f)); + + AnimatorSet animSet = new AnimatorSet(); + animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS); + animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS); + animSet.playTogether(fadeAnim, translateAnim); + animSet.start(); + } } for (int i = 0; i < mColumnCount; i++) { final View v = holder.cells[i]; if (start + i <= end) { - v.setVisibility(View.VISIBLE); + setCellVisibility(holder, i, View.VISIBLE); holder.itemIndices[i] = start + i; mChooserListAdapter.bindView(holder.itemIndices[i], v); } else { - v.setVisibility(View.INVISIBLE); + setCellVisibility(holder, i, View.INVISIBLE); } } } + private void setCellVisibility(RowViewHolder holder, int i, int visibility) { + final View v = holder.cells[i]; + if (visibility == View.VISIBLE) { + holder.cellVisibility[i] = true; + v.setVisibility(visibility); + v.setAlpha(1.0f); + } else if (visibility == View.INVISIBLE && holder.cellVisibility[i]) { + holder.cellVisibility[i] = false; + + ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f); + fadeAnim.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS); + fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f)); + fadeAnim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + v.setVisibility(View.INVISIBLE); + } + }); + fadeAnim.start(); + } + } + private void setVertPadding(RowViewHolder holder, int top, int bottom) { holder.row.setPadding(holder.row.getPaddingLeft(), top, holder.row.getPaddingRight(), bottom); @@ -2132,14 +2324,16 @@ public class ChooserActivity extends ResolverActivity { } static class RowViewHolder { - final View[] cells; - final ViewGroup row; + public final View[] cells; + public final boolean [] cellVisibility; + public final ViewGroup row; int measuredRowHeight; int[] itemIndices; public RowViewHolder(ViewGroup row, int cellCount) { this.row = row; this.cells = new View[cellCount]; + this.cellVisibility = new boolean[cellCount]; this.itemIndices = new int[cellCount]; } @@ -2217,8 +2411,6 @@ public class ChooserActivity extends ResolverActivity { mChooserActivity.unbindService(this); mChooserActivity.mServiceConnections.remove(this); if (mChooserActivity.mServiceConnections.isEmpty()) { - mChooserActivity.mChooserHandler.removeMessages( - CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); mChooserActivity.sendVoiceChoicesIfNeeded(); } mConnectedComponent = null; diff --git a/core/res/res/drawable/chooser_row_layer_list.xml b/core/res/res/drawable/chooser_row_layer_list.xml new file mode 100644 index 0000000000000..0fb26e13fe57a --- /dev/null +++ b/core/res/res/drawable/chooser_row_layer_list.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/core/res/res/layout/chooser_row.xml b/core/res/res/layout/chooser_row.xml index cf812603fb89b..d4585eb1e6fcd 100644 --- a/core/res/res/layout/chooser_row.xml +++ b/core/res/res/layout/chooser_row.xml @@ -23,6 +23,12 @@ android:gravity="start|top" android:paddingStart="@dimen/chooser_grid_padding" android:paddingEnd="@dimen/chooser_grid_padding"> - + diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 02fae4a2c1b37..e9b1bd3af0dc1 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -215,4 +215,6 @@ #00FFFFFF + #1f000000 + diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 39cbd269c75a9..fafd8fe29f8c6 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -717,6 +717,7 @@ 8dp + 25dp 18dp 16dp 24dp diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 4e47b1fb6b2ab..5b658b7ea0d98 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5340,4 +5340,8 @@ %s + %d file %s + %d files + + + Direct share not available + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3d1baaebb3058..e01f4215b7f49 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2752,12 +2752,16 @@ + + + + diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 9fbc1666aed16..3578bc0c911d1 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -740,6 +740,7 @@ public class ChooserActivityTest { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("text/plain"); return sendIntent; }