Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cdfcd9a7 authored by Matt Pietal's avatar Matt Pietal
Browse files

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
parent 5d9e667f
Loading
Loading
Loading
Loading
+256 −64
Original line number Diff line number Diff line
@@ -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<DisplayResolveInfo> driList =
                        getDisplayResolveInfos(mChooserListAdapter);
                final List<ShortcutManager.ShareShortcutInfo> 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<Intent> 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 (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<Intent> 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<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
        // Reserve spots for incoming direct share targets by adding placeholders
        private ChooserTargetInfo mPlaceHolderTargetInfo = new PlaceHolderTargetInfo();
        private List<ChooserTargetInfo> mServiceTargets;
        private final List<TargetInfo> 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<ChooserTargetInfo> createPlaceHolders() {
            List<ChooserTargetInfo> 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,55 +2223,78 @@ public class ChooserActivity extends ResolverActivity {
            final int start = getFirstRowPosition(rowPosition);
            final int startType = mChooserListAdapter.getPositionTargetType(start);

            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(
            final 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);

            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--;
            }

            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 (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) {
                final TextView textView = holder.row.findViewById(R.id.chooser_row_text_option);

                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 {
                    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();
            }
        }

@@ -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;
+25 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2019, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
        <shape android:shape="rectangle">
            <stroke android:width="1dp" android:color="@color/chooser_row_divider"/>
        </shape>
    </item>
</layer-list>
+7 −1
Original line number Diff line number Diff line
@@ -23,6 +23,12 @@
              android:gravity="start|top"
              android:paddingStart="@dimen/chooser_grid_padding"
              android:paddingEnd="@dimen/chooser_grid_padding">

  <TextView
      android:id="@+id/chooser_row_text_option"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:gravity="center"
      android:layout_gravity="center"
      android:visibility="gone" />
</LinearLayout>
+2 −0
Original line number Diff line number Diff line
@@ -215,4 +215,6 @@
    <!-- Magnifier -->
    <color name="default_magnifier_color_overlay">#00FFFFFF</color>

    <color name="chooser_row_divider">#1f000000</color>

</resources>
+1 −0
Original line number Diff line number Diff line
@@ -717,6 +717,7 @@

    <!-- chooser (sharesheet) spacing -->
    <dimen name="chooser_corner_radius">8dp</dimen>
    <dimen name="chooser_row_text_option_translate">25dp</dimen>
    <dimen name="chooser_view_spacing">18dp</dimen>
    <dimen name="chooser_edge_margin_thin">16dp</dimen>
    <dimen name="chooser_edge_margin_normal">24dp</dimen>
Loading