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

Commit e5167e3f authored by Hakan Seyalioglu's avatar Hakan Seyalioglu Committed by Android (Google) Code Review
Browse files

Merge "Do the sorting for the ShareSheet asynchronously."

parents 12026857 e1276bfe
Loading
Loading
Loading
Loading
+59 −18
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ResolverActivity.TargetInfo;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -93,6 +94,7 @@ public class ChooserActivity extends ResolverActivity {
    private IntentSender mRefinementIntentSender;
    private RefinementResultReceiver mRefinementResultReceiver;
    private ChooserTarget[] mCallerChooserTargets;
    private ComponentName[] mFilteredComponentNames;

    private Intent mReferrerFillInIntent;

@@ -235,7 +237,7 @@ public class ChooserActivity extends ResolverActivity {
                }
                names[i] = (ComponentName) pa[i];
            }
            setFilteredComponents(names);
            mFilteredComponentNames = names;
        }

        pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
@@ -642,17 +644,65 @@ public class ChooserActivity extends ResolverActivity {
        }
    }

    public class ChooserListController extends ResolverListController {
        public ChooserListController(Context context,
                PackageManager pm,
                Intent targetIntent,
                String referrerPackageName,
                int launchedFromUid) {
            super(context, pm, targetIntent, referrerPackageName, launchedFromUid);
        }

        @Override
        boolean isComponentPinned(ComponentName name) {
            return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
        }

        @Override
        boolean isComponentFiltered(ComponentName name) {
            if (mFilteredComponentNames == null) {
                return false;
            }
            for (ComponentName filteredComponentName : mFilteredComponentNames) {
                if (name.equals(filteredComponentName)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public float getScore(DisplayResolveInfo target) {
            if (target == null) {
                return CALLER_TARGET_SCORE_BOOST;
            }
            float score = super.getScore(target);
            if (target.isPinned()) {
                score += PINNED_TARGET_SCORE_BOOST;
            }
            return score;
        }
    }

    @Override
    public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents,
            Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
            boolean filterLastUsed) {
        final ChooserListAdapter adapter = new ChooserListAdapter(context, payloadIntents,
                initialIntents, rList, launchedFromUid, filterLastUsed);
        if (DEBUG) Log.d(TAG, "Adapter created; querying services");
        queryTargetServices(adapter);
                initialIntents, rList, launchedFromUid, filterLastUsed, createListController());
        return adapter;
    }

    @VisibleForTesting
    protected ResolverListController createListController() {
        return new ChooserListController(
                this,
                mPm,
                getTargetIntent(),
                getReferrerPackageName(),
                mLaunchedFromUid);
    }

    final class ChooserTargetInfo implements TargetInfo {
        private final DisplayResolveInfo mSourceInfo;
        private final ResolveInfo mBackupResolveInfo;
@@ -853,10 +903,11 @@ public class ChooserActivity extends ResolverActivity {

        public ChooserListAdapter(Context context, List<Intent> payloadIntents,
                Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
                boolean filterLastUsed) {
                boolean filterLastUsed, ResolverListController resolverListController) {
            // Don't send the initial intents through the shared ResolverActivity path,
            // we want to separate them into a different section.
            super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed);
            super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed,
                    resolverListController);

            if (initialIntents != null) {
                final PackageManager pm = getPackageManager();
@@ -921,18 +972,6 @@ public class ChooserActivity extends ResolverActivity {
            return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
        }

        @Override
        public float getScore(DisplayResolveInfo target) {
            if (target == null) {
                return CALLER_TARGET_SCORE_BOOST;
            }
            float score = super.getScore(target);
            if (target.isPinned()) {
                score += PINNED_TARGET_SCORE_BOOST;
            }
            return score;
        }

        @Override
        public View onCreateView(ViewGroup parent) {
            return mInflater.inflate(
@@ -944,6 +983,8 @@ public class ChooserActivity extends ResolverActivity {
            if (mServiceTargets != null) {
                pruneServiceTargets();
            }
            if (DEBUG) Log.d(TAG, "List built querying services");
            queryTargetServices(this);
        }

        @Override
+145 −182

File changed.

Preview size limit exceeded, changes collapsed.

+221 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.annotation.WorkerThread;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;

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

/**
 * A helper for the ResolverActivity that exposes methods to retrieve, filter and sort its list of
 * resolvers.
 */
public class ResolverListController {

    private final Context mContext;
    private final PackageManager mpm;
    private final int mLaunchedFromUid;

    // Needed for sorting resolvers.
    private final Intent mTargetIntent;
    private final String mReferrerPackage;

    private static final String TAG = "ResolverListController";
    private static final boolean DEBUG = false;

    private ResolverComparator mResolverComparator;

    public ResolverListController(
            Context context,
            PackageManager pm,
            Intent targetIntent,
            String referrerPackage,
            int launchedFromUid) {
        mContext = context;
        mpm = pm;
        mLaunchedFromUid = launchedFromUid;
        mTargetIntent = targetIntent;
        mReferrerPackage = referrerPackage;
    }

    @VisibleForTesting
    public List<ResolverActivity.ResolvedComponentInfo> getResolversForIntent(
            boolean shouldGetResolvedFilter,
            boolean shouldGetActivityMetadata,
            List<Intent> intents) {
        List<ResolverActivity.ResolvedComponentInfo> resolvedComponents = null;
        for (int i = 0, N = intents.size(); i < N; i++) {
            final Intent intent = intents.get(i);
            final List<ResolveInfo> infos = mpm.queryIntentActivities(intent,
                    PackageManager.MATCH_DEFAULT_ONLY
                            | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
                            | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0));
            if (infos != null) {
                if (resolvedComponents == null) {
                    resolvedComponents = new ArrayList<>();
                }
                addResolveListDedupe(resolvedComponents, intent, infos);
            }
        }
        return resolvedComponents;
    }

    @VisibleForTesting
    public void addResolveListDedupe(List<ResolverActivity.ResolvedComponentInfo> into,
            Intent intent,
            List<ResolveInfo> from) {
        final int fromCount = from.size();
        final int intoCount = into.size();
        for (int i = 0; i < fromCount; i++) {
            final ResolveInfo newInfo = from.get(i);
            boolean found = false;
            // Only loop to the end of into as it was before we started; no dupes in from.
            for (int j = 0; j < intoCount; j++) {
                final ResolverActivity.ResolvedComponentInfo rci = into.get(j);
                if (isSameResolvedComponent(newInfo, rci)) {
                    found = true;
                    rci.add(intent, newInfo);
                    break;
                }
            }
            if (!found) {
                final ComponentName name = new ComponentName(
                        newInfo.activityInfo.packageName, newInfo.activityInfo.name);
                final ResolverActivity.ResolvedComponentInfo rci =
                        new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
                rci.setPinned(isComponentPinned(name));
                into.add(rci);
            }
        }
    }

    // Filter out any activities that the launched uid does not have permission for.
    //
    // Also filter out those that are suspended because they couldn't be started. We don't do this
    // when we have an explicit list of resolved activities, because that only happens when
    // we are being subclassed, so we can safely launch whatever they gave us.
    //
    // To preserve the inputList, optionally will return the original list if any modification has
    // been made.
    @VisibleForTesting
    public ArrayList<ResolverActivity.ResolvedComponentInfo> filterIneligibleActivities(
            List<ResolverActivity.ResolvedComponentInfo> inputList,
            boolean returnCopyOfOriginalListIfModified) {
        ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
        for (int i = inputList.size()-1; i >= 0; i--) {
            ActivityInfo ai = inputList.get(i)
                    .getResolveInfoAt(0).activityInfo;
            int granted = ActivityManager.checkComponentPermission(
                    ai.permission, mLaunchedFromUid,
                    ai.applicationInfo.uid, ai.exported);
            boolean suspended = (ai.applicationInfo.flags
                    & ApplicationInfo.FLAG_SUSPENDED) != 0;
            if (granted != PackageManager.PERMISSION_GRANTED || suspended
                    || isComponentFiltered(ai.getComponentName())) {
                // Access not allowed! We're about to filter an item,
                // so modify the unfiltered version if it hasn't already been modified.
                if (returnCopyOfOriginalListIfModified && listToReturn == null) {
                    listToReturn = new ArrayList<>(inputList);
                }
                inputList.remove(i);
            }
        }
        return listToReturn;
    }

    // Filter out any low priority items.
    //
    // To preserve the inputList, optionally will return the original list if any modification has
    // been made.
    @VisibleForTesting
    public ArrayList<ResolverActivity.ResolvedComponentInfo> filterLowPriority(
            List<ResolverActivity.ResolvedComponentInfo> inputList,
            boolean returnCopyOfOriginalListIfModified) {
        ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
        // Only display the first matches that are either of equal
        // priority or have asked to be default options.
        ResolverActivity.ResolvedComponentInfo rci0 = inputList.get(0);
        ResolveInfo r0 = rci0.getResolveInfoAt(0);
        int N = inputList.size();
        for (int i = 1; i < N; i++) {
            ResolveInfo ri = inputList.get(i).getResolveInfoAt(0);
            if (DEBUG) Log.v(
                    TAG,
                    r0.activityInfo.name + "=" +
                            r0.priority + "/" + r0.isDefault + " vs " +
                            ri.activityInfo.name + "=" +
                            ri.priority + "/" + ri.isDefault);
            if (r0.priority != ri.priority ||
                    r0.isDefault != ri.isDefault) {
                while (i < N) {
                    if (returnCopyOfOriginalListIfModified && listToReturn == null) {
                        listToReturn = new ArrayList<>(inputList);
                    }
                    inputList.remove(i);
                    N--;
                }
            }
        }
        return listToReturn;
    }

    @VisibleForTesting
    @WorkerThread
    public void sort(List<ResolverActivity.ResolvedComponentInfo> inputList) {
        if (mResolverComparator == null) {
            mResolverComparator = new ResolverComparator(mContext, mTargetIntent, mReferrerPackage);
        }
        mResolverComparator.compute(inputList);
        Collections.sort(inputList, mResolverComparator);
    }

    private static boolean isSameResolvedComponent(ResolveInfo a,
            ResolverActivity.ResolvedComponentInfo b) {
        final ActivityInfo ai = a.activityInfo;
        return ai.packageName.equals(b.name.getPackageName())
                && ai.name.equals(b.name.getClassName());
    }

    boolean isComponentPinned(ComponentName name) {
        return false;
    }

    boolean isComponentFiltered(ComponentName componentName) {
        return false;
    }

    @VisibleForTesting
    public float getScore(ResolverActivity.DisplayResolveInfo target) {
        if (mResolverComparator == null) {
            mResolverComparator = new ResolverComparator(mContext, mTargetIntent, mReferrerPackage);
        }
        return mResolverComparator.getScore(target.getResolvedComponentName());
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -1156,6 +1156,7 @@
        </activity>
        <activity android:name="android.app.EmptyActivity">
        </activity>
        <activity android:name="com.android.internal.app.ChooserWrapperActivity"/>

        <receiver android:name="android.app.activity.AbortReceiver">
            <intent-filter android:priority="1">
+178 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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 com.android.internal.R;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

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

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static com.android.internal.app.ChooserWrapperActivity.sOverrides;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.when;

/**
 * Chooser activity instrumentation tests
 */
@RunWith(AndroidJUnit4.class)
public class ChooserActivityTest {
    @Rule
    public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
            new ActivityTestRule<>(ChooserWrapperActivity.class, false,
                    false);

    @Before
    public void cleanOverrideData() {
        sOverrides.reset();
    }

    @Test
    public void customTitle() throws InterruptedException {
        Intent sendIntent = createSendImageIntent();
        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
                Mockito.anyBoolean(),
                Mockito.isA(List.class))).thenReturn(null);
        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test"));
        waitForIdle();
        onView(withId(R.id.title)).check(matches(withText("chooser test")));
    }

    @Test
    public void emptyTitle() throws InterruptedException {
        sOverrides.isVoiceInteraction = false;
        Intent sendIntent = createSendImageIntent();
        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
                Mockito.anyBoolean(),
                Mockito.isA(List.class))).thenReturn(null);
        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
        waitForIdle();
        onView(withId(R.id.title))
                .check(matches(withText(R.string.whichSendApplication)));
    }

    @Test
    public void twoOptionsAndUserSelectsOne() throws InterruptedException {
        Intent sendIntent = createSendImageIntent();
        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);

        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
                Mockito.anyBoolean(),
                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);

        final ChooserWrapperActivity activity = mActivityRule
                .launchActivity(Intent.createChooser(sendIntent, null));
        waitForIdle();

        assertThat(activity.getAdapter().getCount(), is(2));
        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));

        ResolveInfo[] chosen = new ResolveInfo[1];
        sOverrides.onSafelyStartCallback = targetInfo -> {
            chosen[0] = targetInfo.getResolveInfo();
            return true;
        };

        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
        onView(withText(toChoose.activityInfo.name))
                .perform(click());
        waitForIdle();
        assertThat(chosen[0], is(toChoose));
    }

    @Test
    public void noResultsFromPackageManager() {
        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
                Mockito.anyBoolean(),
                Mockito.isA(List.class))).thenReturn(null);
        Intent sendIntent = createSendImageIntent();
        final ChooserWrapperActivity activity = mActivityRule
                .launchActivity(Intent.createChooser(sendIntent, null));
        waitForIdle();
        assertThat(activity.isFinishing(), is(false));

        onView(withId(R.id.empty)).check(matches(isDisplayed()));
        onView(withId(R.id.resolver_list)).check(matches(not(isDisplayed())));
        InstrumentationRegistry.getInstrumentation().runOnMainSync(
                () -> activity.getAdapter().handlePackagesChanged()
        );
        // backward compatibility. looks like we finish when data is empty after package change
        assertThat(activity.isFinishing(), is(true));
    }

    @Test
    public void autoLaunchSingleResult() throws InterruptedException {
        ResolveInfo[] chosen = new ResolveInfo[1];
        sOverrides.onSafelyStartCallback = targetInfo -> {
            chosen[0] = targetInfo.getResolveInfo();
            return true;
        };

        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1);
        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
                Mockito.anyBoolean(),
                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);

        Intent sendIntent = createSendImageIntent();
        final ChooserWrapperActivity activity = mActivityRule
                .launchActivity(Intent.createChooser(sendIntent, null));
        waitForIdle();

        assertThat(chosen[0], is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
        assertThat(activity.isFinishing(), is(true));
    }

    private Intent createSendImageIntent() {
        Intent sendIntent = new Intent();
        sendIntent.setAction(Intent.ACTION_SEND);
        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
        sendIntent.setType("image/jpeg");
        return sendIntent;
    }

    private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) {
        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
        for (int i = 0; i < numberOfResults; i++) {
            infoList.add(ChooserDataProvider.createResolvedComponentInfo(i));
        }
        return infoList;
    }

    private void waitForIdle() {
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    }
}
 No newline at end of file
Loading