Loading core/java/com/android/internal/app/ChooserActivity.java +59 −18 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -93,6 +94,7 @@ public class ChooserActivity extends ResolverActivity { private IntentSender mRefinementIntentSender; private RefinementResultReceiver mRefinementResultReceiver; private ChooserTarget[] mCallerChooserTargets; private ComponentName[] mFilteredComponentNames; private Intent mReferrerFillInIntent; Loading Loading @@ -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); Loading Loading @@ -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; Loading Loading @@ -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(); Loading Loading @@ -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( Loading @@ -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 Loading core/java/com/android/internal/app/ResolverActivity.java +145 −182 File changed.Preview size limit exceeded, changes collapsed. Show changes core/java/com/android/internal/app/ResolverListController.java 0 → 100644 +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()); } } core/tests/coretests/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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"> Loading core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java 0 → 100644 +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
core/java/com/android/internal/app/ChooserActivity.java +59 −18 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -93,6 +94,7 @@ public class ChooserActivity extends ResolverActivity { private IntentSender mRefinementIntentSender; private RefinementResultReceiver mRefinementResultReceiver; private ChooserTarget[] mCallerChooserTargets; private ComponentName[] mFilteredComponentNames; private Intent mReferrerFillInIntent; Loading Loading @@ -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); Loading Loading @@ -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; Loading Loading @@ -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(); Loading Loading @@ -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( Loading @@ -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 Loading
core/java/com/android/internal/app/ResolverActivity.java +145 −182 File changed.Preview size limit exceeded, changes collapsed. Show changes
core/java/com/android/internal/app/ResolverListController.java 0 → 100644 +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()); } }
core/tests/coretests/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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"> Loading
core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java 0 → 100644 +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