From cdfcd9a7bae5680f1b6c251e02b207d599a77b6b Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Tue, 5 Mar 2019 08:31:47 -0500 Subject: [PATCH] Sharesheet - Lock direct share row Add single direct share row this is always present, prepare for expanding to 2. Show message about missing direct share targets if none are found in the first XX seconds. Use animations to transition between the states. Add border dividers between sections Bug: 126565347 Test: atest ChooserActivityTest Change-Id: I31963375530433c3d6e9069402b5a20df5766034 --- .../android/internal/app/ChooserActivity.java | 320 ++++++++++++++---- .../res/drawable/chooser_row_layer_list.xml | 25 ++ core/res/res/layout/chooser_row.xml | 8 +- core/res/res/values/colors.xml | 2 + core/res/res/values/dimens.xml | 1 + core/res/res/values/strings.xml | 4 + core/res/res/values/symbols.xml | 4 + .../internal/app/ChooserActivityTest.java | 1 + 8 files changed, 300 insertions(+), 65 deletions(-) create mode 100644 core/res/res/drawable/chooser_row_layer_list.xml diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 67e0446e19e5b..81d70a36159f3 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; }