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

Commit 19ee2925 authored by Alison Cichowlas's avatar Alison Cichowlas
Browse files

Sharesheet - App stacking.

When an app offers more than one activity to handle Send intents,
"stack" these into a single target in the All Apps list.

Tapping this target gives a disambiguation dialog to allow the
user to choose the specific action they want.

Long-pressing this app still brings up the app pinning dialog, but
app pinning allows you to pin any of the actions.

Test: atest ChooserActivityTest
Test: manual - press/longpress targets in A-Z list
Change-Id: Ida56344c3451da80d7cbe61bac8ca2117497e59a
parent 7ba8ed19
Loading
Loading
Loading
Loading
+43 −10
Original line number Diff line number Diff line
@@ -111,6 +111,7 @@ import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGett
import com.android.internal.app.ResolverListAdapter.ViewHolder;
import com.android.internal.app.chooser.ChooserTargetInfo;
import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.app.chooser.MultiDisplayResolveInfo;
import com.android.internal.app.chooser.NotSelectableTargetInfo;
import com.android.internal.app.chooser.SelectableTargetInfo;
import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
@@ -1352,17 +1353,31 @@ public class ChooserActivity extends ResolverActivity implements
        return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
    }

    @Override
    public void showTargetDetails(ResolveInfo ri) {
        if (ri == null) {
    void showTargetDetails(TargetInfo ti) {
        if (ti == null) {
            return;
        }

        ComponentName name = ri.activityInfo.getComponentName();
        ComponentName name = ti.getResolveInfo().activityInfo.getComponentName();
        boolean pinned = mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
        ResolverTargetActionsDialogFragment f =
                new ResolverTargetActionsDialogFragment(ri.loadLabel(getPackageManager()),
                        name, pinned);

        ResolverTargetActionsDialogFragment f;

        // For multiple targets, include info on all targets
        if (ti instanceof MultiDisplayResolveInfo) {
            MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) ti;
            List<CharSequence> labels = new ArrayList<>();

            for (TargetInfo innerInfo : mti.getTargets()) {
                labels.add(innerInfo.getResolveInfo().loadLabel(getPackageManager()));
            }
            f = new ResolverTargetActionsDialogFragment(
                    mti.getResolveInfo().loadLabel(getPackageManager()), name, mti.getTargets(),
                    labels);
        } else {
            f = new ResolverTargetActionsDialogFragment(
                    ti.getResolveInfo().loadLabel(getPackageManager()), name, pinned);
        }

        f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
    }

@@ -1416,8 +1431,26 @@ public class ChooserActivity extends ResolverActivity implements
        }

        final long selectionCost = System.currentTimeMillis() - mChooserShownTime;

        // Stacked apps get a disambiguation first
        if (targetInfo instanceof MultiDisplayResolveInfo) {
            MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
            CharSequence[] labels = new CharSequence[mti.getTargets().size()];
            int i = 0;
            for (TargetInfo ti : mti.getTargets()) {
                labels[i++] = ti.getResolveInfo().loadLabel(getPackageManager());
            }
            ChooserStackedAppDialogFragment f = new ChooserStackedAppDialogFragment(
                    targetInfo.getDisplayLabel(),
                    ((MultiDisplayResolveInfo) targetInfo).getTargets(), labels);

            f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
            return;
        }

        super.startSelected(which, always, filtered);


        if (currentListAdapter.getCount() > 0) {
            // Log the index of which type of target the user picked.
            // Lower values mean the ranking was better.
@@ -2363,7 +2396,7 @@ public class ChooserActivity extends ResolverActivity implements
                itemView.setOnLongClickListener(v -> {
                    showTargetDetails(
                            mChooserMultiProfilePagerAdapter.getActiveListAdapter()
                                    .resolveInfoForPosition(mListPosition, /* filtered */ true));
                                    .targetInfoForPosition(mListPosition, /* filtered */ true));
                    return true;
                });
            }
@@ -2615,7 +2648,7 @@ public class ChooserActivity extends ResolverActivity implements
                    @Override
                    public boolean onLongClick(View v) {
                        showTargetDetails(
                                mChooserListAdapter.resolveInfoForPosition(
                                mChooserListAdapter.targetInfoForPosition(
                                        holder.getItemIndex(column), true));
                        return true;
                    }
+25 −2
Original line number Diff line number Diff line
@@ -38,17 +38,22 @@ import com.android.internal.R;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.ChooserTargetInfo;
import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.app.chooser.MultiDisplayResolveInfo;
import com.android.internal.app.chooser.SelectableTargetInfo;
import com.android.internal.app.chooser.TargetInfo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ChooserListAdapter extends ResolverListAdapter {
    private static final String TAG = "ChooserListAdapter";
    private static final boolean DEBUG = false;

    private boolean mEnableStackedApps = true;

    public static final int NO_POSITION = -1;
    public static final int TARGET_BAD = -1;
    public static final int TARGET_CALLER = 0;
@@ -218,7 +223,25 @@ public class ChooserListAdapter extends ResolverListAdapter {

    void updateAlphabeticalList() {
        mSortedList.clear();
        if (mEnableStackedApps) {
            // Consolidate multiple targets from same app.
            Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
            for (DisplayResolveInfo info : mDisplayList) {
                String packageName = info.getResolvedComponentName().getPackageName();
                if (consolidated.get(packageName) != null) {
                    // create consolidated target
                    MultiDisplayResolveInfo multiDisplayResolveInfo =
                            new MultiDisplayResolveInfo(packageName, info);
                    multiDisplayResolveInfo.addTarget(consolidated.get(packageName));
                    consolidated.put(packageName, multiDisplayResolveInfo);
                } else {
                    consolidated.put(packageName, info);
                }
            }
            mSortedList.addAll(consolidated.values());
        } else {
            mSortedList.addAll(mDisplayList);
        }
        Collections.sort(mSortedList, new ChooserActivity.AzInfoComparator(mContext));
    }

@@ -270,7 +293,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
    }

    int getAlphaTargetCount() {
        int standardCount = super.getCount();
        int standardCount = mSortedList.size();
        return standardCount > mChooserListCommunicator.getMaxRankedTargets() ? standardCount : 0;
    }

+86 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.
 */


package com.android.internal.app;

import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.os.Bundle;

import com.android.internal.app.chooser.DisplayResolveInfo;

import java.util.ArrayList;
import java.util.List;

/**
 * Shows individual actions for a "stacked" app target - such as an app with multiple posting
 * streams represented in the Sharesheet.
 */
public class ChooserStackedAppDialogFragment extends DialogFragment
        implements DialogInterface.OnClickListener {
    private static final String TITLE_KEY = "title";
    private static final String PINNED_KEY = "pinned";

    private List<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
    private CharSequence[] mLabels;

    public ChooserStackedAppDialogFragment() {
    }

    public ChooserStackedAppDialogFragment(CharSequence title) {
        Bundle args = new Bundle();
        args.putCharSequence(TITLE_KEY, title);
        setArguments(args);
    }

    public ChooserStackedAppDialogFragment(CharSequence title,
            List<DisplayResolveInfo> targets, CharSequence[] labels) {
        Bundle args = new Bundle();
        args.putCharSequence(TITLE_KEY, title);
        mTargetInfos = targets;
        mLabels = labels;
        setArguments(args);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final Bundle args = getArguments();
        return new Builder(getContext())
                .setCancelable(true)
                .setItems(mLabels, this)
                .setTitle(args.getCharSequence(TITLE_KEY))
                .create();
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        final Bundle args = getArguments();
        mTargetInfos.get(which).start(getActivity(), null);
        dismiss();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        // Dismiss on config changed (eg: rotation)
        // TODO: Maintain state on config change
        super.onConfigurationChanged(newConfig);
        dismiss();
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -1160,7 +1160,7 @@ public class ResolverActivity extends Activity implements
        return !target.isSuspended();
    }

    public void showTargetDetails(ResolveInfo ri) {
    void showTargetDetails(ResolveInfo ri) {
        Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                .setData(Uri.fromParts("package", ri.activityInfo.packageName, null))
                .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+73 −22
Original line number Diff line number Diff line
@@ -24,14 +24,19 @@ import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;

import com.android.internal.R;
import com.android.internal.app.chooser.DisplayResolveInfo;

import java.util.ArrayList;
import java.util.List;

/**
 * Shows a dialog with actions to take on a chooser target
 * Shows a dialog with actions to take on a chooser target.
 */
public class ResolverTargetActionsDialogFragment extends DialogFragment
        implements DialogInterface.OnClickListener {
@@ -43,6 +48,10 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment
    private static final int TOGGLE_PIN_INDEX = 0;
    private static final int APP_INFO_INDEX = 1;

    private List<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
    private List<CharSequence> mLabels = new ArrayList<>();
    private boolean[] mPinned;

    public ResolverTargetActionsDialogFragment() {
    }

@@ -55,15 +64,43 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment
        setArguments(args);
    }

    public ResolverTargetActionsDialogFragment(CharSequence title, ComponentName name,
            List<DisplayResolveInfo> targets, List<CharSequence> labels) {
        Bundle args = new Bundle();
        args.putCharSequence(TITLE_KEY, title);
        args.putParcelable(NAME_KEY, name);
        mTargetInfos = targets;
        mLabels = labels;
        setArguments(args);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final Bundle args = getArguments();
        final int itemRes = args.getBoolean(PINNED_KEY, false)
                ? R.array.resolver_target_actions_unpin
                : R.array.resolver_target_actions_pin;
        String[] defaultActions = getResources().getStringArray(itemRes);
        CharSequence[] items;

        if (mTargetInfos == null || mTargetInfos.size() < 2) {
            items = defaultActions;
        } else {
            // Pin item for each sub-item
            items = new CharSequence[mTargetInfos.size() + 1];
            for (int i = 0; i < mTargetInfos.size(); i++) {
                items[i] = mTargetInfos.get(i).isPinned()
                         ? getResources().getString(R.string.unpin_specific_target, mLabels.get(i))
                         : getResources().getString(R.string.pin_specific_target, mLabels.get(i));
            }
            // "App info"
            items[mTargetInfos.size()] = defaultActions[1];
        }


        return new Builder(getContext())
                .setCancelable(true)
                .setItems(itemRes, this)
                .setItems(items, this)
                .setTitle(args.getCharSequence(TITLE_KEY))
                .create();
    }
@@ -72,27 +109,41 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment
    public void onClick(DialogInterface dialog, int which) {
        final Bundle args = getArguments();
        ComponentName name = args.getParcelable(NAME_KEY);
        switch (which) {
            case TOGGLE_PIN_INDEX:
                SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext());
                final String key = name.flattenToString();
                boolean currentVal = sp.getBoolean(name.flattenToString(), false);
                if (currentVal) {
                    sp.edit().remove(key).apply();
        if (which == 0 || (mTargetInfos.size() > 0 && which < mTargetInfos.size())) {
            if (mTargetInfos == null || mTargetInfos.size() == 0) {
                pinComponent(name);
            } else {
                    sp.edit().putBoolean(key, true).apply();
                pinComponent(mTargetInfos.get(which).getResolvedComponentName());
            }

            // Force the chooser to requery and resort things
            getActivity().recreate();
                break;
            case APP_INFO_INDEX:
        } else {
            // Last item in dialog is App Info
            Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                    .setData(Uri.fromParts("package", name.getPackageName(), null))
                    .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
            startActivity(in);
                break;
        }
        dismiss();
    }

    private void pinComponent(ComponentName name) {
        SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext());
        final String key = name.flattenToString();
        boolean currentVal = sp.getBoolean(name.flattenToString(), false);
        if (currentVal) {
            sp.edit().remove(key).apply();
        } else {
            sp.edit().putBoolean(key, true).apply();
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        // Dismiss on config changed (eg: rotation)
        // TODO: Maintain state on config change
        super.onConfigurationChanged(newConfig);
        dismiss();
    }

}
Loading