diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index d4baa18f7100e..84c8f7a22962c 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -340,8 +340,7 @@ public class ChooserActivity extends ResolverActivity { } @Override - public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter, - boolean alwaysUseOption) { + public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) { final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; mChooserListAdapter = (ChooserListAdapter) adapter; if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) { @@ -367,6 +366,9 @@ public class ChooserActivity extends ResolverActivity { @Override public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { + // Note that this is only safe because the Intent handled by the ChooserActivity is + // guaranteed to contain no extras unknown to the local ClassLoader. That is why this + // method can not be replaced in the ResolverActivity whole hog. return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, super.shouldAutoLaunchSingleChoice(target)); } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 40142173ece38..3c1a18038d2b7 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -20,22 +20,11 @@ import android.annotation.Nullable; import android.annotation.StringRes; import android.annotation.UiThread; import android.app.Activity; +import android.app.ActivityManager; import android.app.ActivityThread; import android.app.VoiceInteractor.PickOptionRequest; import android.app.VoiceInteractor.PickOptionRequest.Option; import android.app.VoiceInteractor.Prompt; -import android.os.AsyncTask; -import android.os.RemoteException; -import android.provider.MediaStore; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Slog; -import android.widget.AbsListView; -import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.content.PackageMonitor; - -import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -50,16 +39,23 @@ import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.PatternMatcher; +import android.os.RemoteException; import android.os.StrictMode; import android.os.UserHandle; import android.os.UserManager; +import android.provider.MediaStore; +import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; +import android.util.Slog; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; @@ -67,7 +63,9 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; - +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.widget.ResolverDrawerLayout; @@ -92,7 +90,6 @@ public class ResolverActivity extends Activity { protected ResolveListAdapter mAdapter; private boolean mSafeForwardingMode; - private boolean mAlwaysUseOption; private AbsListView mAdapterView; private Button mAlwaysButton; private Button mOnceButton; @@ -108,13 +105,15 @@ public class ResolverActivity extends Activity { private CharSequence mTitle; private int mDefaultTitleResId; + // Whether or not this activity supports choosing a default handler for the intent. + private boolean mSupportsAlwaysUseOption; protected ResolverDrawerLayout mResolverDrawerLayout; protected PackageManager mPm; protected int mLaunchedFromUid; private static final String TAG = "ResolverActivity"; private static final boolean DEBUG = false; - private Runnable mPostListBuildRunnable; + private Runnable mPostListReadyRunnable; private boolean mRegistered; private final PackageMonitor mPackageMonitor = new PackageMonitor() { @@ -230,13 +229,14 @@ public class ResolverActivity extends Activity { */ protected void onCreate(Bundle savedInstanceState, Intent intent, CharSequence title, Intent[] initialIntents, - List rList, boolean alwaysUseOption) { - onCreate(savedInstanceState, intent, title, 0, initialIntents, rList, alwaysUseOption); + List rList, boolean supportsAlwaysUseOption) { + onCreate(savedInstanceState, intent, title, 0, initialIntents, rList, + supportsAlwaysUseOption); } protected void onCreate(Bundle savedInstanceState, Intent intent, CharSequence title, int defaultTitleRes, Intent[] initialIntents, - List rList, boolean alwaysUseOption) { + List rList, boolean supportsAlwaysUseOption) { setTheme(R.style.Theme_DeviceDefault_Resolver); super.onCreate(savedInstanceState); @@ -262,7 +262,7 @@ public class ResolverActivity extends Activity { mPackageMonitor.register(this, getMainLooper(), false); mRegistered = true; mReferrerPackage = getReferrerPackageName(); - mAlwaysUseOption = alwaysUseOption; + mSupportsAlwaysUseOption = supportsAlwaysUseOption; final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); mIconDpi = am.getLauncherLargeIconDensity(); @@ -378,8 +378,11 @@ public class ResolverActivity extends Activity { final DisplayResolveInfo dri = mAdapter.getOtherProfile(); if (dri != null) { mProfileView.setVisibility(View.VISIBLE); - final TextView text = (TextView) mProfileView.findViewById(R.id.profile_button); - text.setText(dri.getDisplayLabel()); + View text = mProfileView.findViewById(R.id.profile_button); + if (!(text instanceof TextView)) { + text = mProfileView.findViewById(R.id.text1); + } + ((TextView) text).setText(dri.getDisplayLabel()); } else { mProfileView.setVisibility(View.GONE); } @@ -421,7 +424,7 @@ public class ResolverActivity extends Activity { final ActionTitle title = mResolvingHome ? ActionTitle.HOME : ActionTitle.forAction(action); // While there may already be a filtered item, we can only use it in the title if the list // is already sorted and all information relevant to it is already in the list. - final boolean named = mAdapter.getFilteredPosition() > 0; + final boolean named = mAdapter.getFilteredPosition() >= 0; if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) { return getString(defaultTitleRes); } else { @@ -512,15 +515,16 @@ public class ResolverActivity extends Activity { if (!isChangingConfigurations() && mPickOptionRequest != null) { mPickOptionRequest.cancel(); } - if (mPostListBuildRunnable != null) { - getMainThreadHandler().removeCallbacks(mPostListBuildRunnable); + if (mPostListReadyRunnable != null) { + getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); + mPostListReadyRunnable = null; } } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); - if (mAlwaysUseOption) { + if (mSupportsAlwaysUseOption) { final int checkedPos = mAdapterView.getCheckedItemPosition(); final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; mLastSelected = checkedPos; @@ -575,17 +579,18 @@ public class ResolverActivity extends Activity { public void onButtonClick(View v) { final int id = v.getId(); - startSelected(mAlwaysUseOption ? - mAdapterView.getCheckedItemPosition() : mAdapter.getFilteredPosition(), + startSelected(mAdapter.hasFilteredItem() ? + mAdapter.getFilteredPosition(): + mAdapterView.getCheckedItemPosition(), id == R.id.button_always, - mAlwaysUseOption); + !mAdapter.hasFilteredItem()); } - public void startSelected(int which, boolean always, boolean filtered) { + public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) { if (isFinishing()) { return; } - ResolveInfo ri = mAdapter.resolveInfoForPosition(which, filtered); + ResolveInfo ri = mAdapter.resolveInfoForPosition(which, hasIndexBeenFiltered); if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) { Toast.makeText(this, String.format(getResources().getString( com.android.internal.R.string.activity_resolver_work_profiles_support), @@ -594,15 +599,15 @@ public class ResolverActivity extends Activity { return; } - TargetInfo target = mAdapter.targetInfoForPosition(which, filtered); + TargetInfo target = mAdapter.targetInfoForPosition(which, hasIndexBeenFiltered); if (target == null) { return; } if (onTargetSelected(target, always)) { - if (always && filtered) { + if (always && mSupportsAlwaysUseOption) { MetricsLogger.action( this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS); - } else if (filtered) { + } else if (mSupportsAlwaysUseOption) { MetricsLogger.action( this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE); } else { @@ -627,7 +632,7 @@ public class ResolverActivity extends Activity { final ResolveInfo ri = target.getResolveInfo(); final Intent intent = target != null ? target.getResolvedIntent() : null; - if (intent != null && (mAlwaysUseOption || mAdapter.hasFilteredItem()) + if (intent != null && (mSupportsAlwaysUseOption || mAdapter.hasFilteredItem()) && mAdapter.mUnfilteredResolveList != null) { // Build a reasonable intent filter, based on what matched. IntentFilter filter = new IntentFilter(); @@ -714,7 +719,18 @@ public class ResolverActivity extends Activity { if (filter != null) { final int N = mAdapter.mUnfilteredResolveList.size(); - ComponentName[] set = new ComponentName[N]; + ComponentName[] set; + // If we don't add back in the component for forwarding the intent to a managed + // profile, the preferred activity may not be updated correctly (as the set of + // components we tell it we knew about will have changed). + final boolean needToAddBackProfileForwardingComponent + = mAdapter.mOtherProfile != null; + if (!needToAddBackProfileForwardingComponent) { + set = new ComponentName[N]; + } else { + set = new ComponentName[N + 1]; + } + int bestMatch = 0; for (int i=0; i bestMatch) bestMatch = r.match; } + + if (needToAddBackProfileForwardingComponent) { + set[N] = mAdapter.mOtherProfile.getResolvedComponentName(); + final int otherProfileMatch = mAdapter.mOtherProfile.getResolveInfo().match; + if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch; + } + if (alwaysCheck) { final int userId = getUserId(); final PackageManager pm = getPackageManager(); @@ -859,15 +882,15 @@ public class ResolverActivity extends Activity { // a more complicated UI that the current voice interaction flow is not able // to handle. mAdapter = createAdapter(this, payloadIntents, initialIntents, rList, - mLaunchedFromUid, mAlwaysUseOption && !isVoiceInteraction()); + mLaunchedFromUid, mSupportsAlwaysUseOption && !isVoiceInteraction()); boolean rebuildCompleted = mAdapter.rebuildList(); - if (mAdapter.hasFilteredItem()) { + if (useLayoutWithDefault()) { mLayoutId = R.layout.resolver_list_with_default; - mAlwaysUseOption = false; } else { mLayoutId = getLayoutResource(); } + setContentView(mLayoutId); int count = mAdapter.getUnfilteredCount(); @@ -887,22 +910,21 @@ public class ResolverActivity extends Activity { } } - setContentView(mLayoutId); + mAdapterView = (AbsListView) findViewById(R.id.resolver_list); if (count == 0 && mAdapter.mPlaceholderCount == 0) { - final TextView empty = (TextView) findViewById(R.id.empty); - empty.setVisibility(View.VISIBLE); + final TextView emptyView = (TextView) findViewById(R.id.empty); + emptyView.setVisibility(View.VISIBLE); mAdapterView.setVisibility(View.GONE); } else { mAdapterView.setVisibility(View.VISIBLE); - onPrepareAdapterView(mAdapterView, mAdapter, mAlwaysUseOption); + onPrepareAdapterView(mAdapterView, mAdapter); } return false; } - public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter, - boolean alwaysUseOption) { + public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) { final boolean useHeader = adapter.hasFilteredItem(); final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; @@ -912,7 +934,7 @@ public class ResolverActivity extends Activity { adapterView.setOnItemClickListener(listener); adapterView.setOnItemLongClickListener(listener); - if (alwaysUseOption) { + if (mSupportsAlwaysUseOption) { listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); } @@ -969,16 +991,19 @@ public class ResolverActivity extends Activity { } public void resetAlwaysOrOnceButtonBar() { - if (mAlwaysUseOption || mAdapter.mLastChosen != null) { + if (mSupportsAlwaysUseOption) { final ViewGroup buttonLayout = (ViewGroup) findViewById(R.id.button_bar); if (buttonLayout != null) { buttonLayout.setVisibility(View.VISIBLE); mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always); mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once); + } else { + Log.e(TAG, "Layout unexpectedly does not have a button bar"); } } - if (mAdapter.getFilteredPosition() >= 0) { + if (useLayoutWithDefault() + && mAdapter.getFilteredPosition() != ListView.INVALID_POSITION) { setAlwaysButtonEnabled(true, mAdapter.getFilteredPosition(), false); mOnceButton.setEnabled(true); return; @@ -992,6 +1017,10 @@ public class ResolverActivity extends Activity { } } + private boolean useLayoutWithDefault() { + return mSupportsAlwaysUseOption && mAdapter.hasFilteredItem(); + } + /** * Check a simple match for the component of two ResolveInfos. */ @@ -1293,6 +1322,7 @@ public class ResolverActivity extends Activity { mPlaceholderCount = count; } + @Nullable public DisplayResolveInfo getFilteredItem() { if (mFilterLastUsed && mLastChosenPosition >= 0) { // Not using getItem since it offsets to dodge this position for the list @@ -1336,14 +1366,10 @@ public class ResolverActivity extends Activity { */ protected boolean rebuildList() { List currentResolveList = null; - try { - mLastChosen = mResolverListController.getLastChosen(); - } catch (RemoteException re) { - Log.d(TAG, "Error calling getLastChosenActivity\n" + re); - } - // Clear the value of mOtherProfile from previous call. mOtherProfile = null; + mLastChosen = null; + mLastChosenPosition = -1; mDisplayList.clear(); if (mBaseResolveList != null) { currentResolveList = mUnfilteredResolveList = new ArrayList<>(); @@ -1366,6 +1392,30 @@ public class ResolverActivity extends Activity { mUnfilteredResolveList = originalList; } } + + // So far we only support a single other profile at a time. + // The first one we see gets special treatment. + for (ResolvedComponentInfo info : currentResolveList) { + if (info.getResolveInfoAt(0).targetUserId != UserHandle.USER_CURRENT) { + mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0), + info.getResolveInfoAt(0), + info.getResolveInfoAt(0).loadLabel(mPm), + info.getResolveInfoAt(0).loadLabel(mPm), + getReplacementIntent(info.getResolveInfoAt(0).activityInfo, + info.getIntentAt(0))); + currentResolveList.remove(info); + break; + } + } + + if (mOtherProfile == null) { + try { + mLastChosen = mResolverListController.getLastChosen(); + } catch (RemoteException re) { + Log.d(TAG, "Error calling getLastChosenActivity\n" + re); + } + } + int N; if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) { // We only care about fixing the unfilteredList if the current resolve list and @@ -1402,19 +1452,7 @@ public class ResolverActivity extends Activity { } }; sortingTask.execute(currentResolveList); - if (mPostListBuildRunnable == null) { - mPostListBuildRunnable = new Runnable() { - @Override - public void run() { - setTitleAndIcon(); - resetAlwaysOrOnceButtonBar(); - onListRebuilt(); - disableLastChosenIfNeeded(); - mPostListBuildRunnable = null; - } - }; - getMainThreadHandler().post(mPostListBuildRunnable); - } + postListReadyRunnable(); return false; } else { processSortedList(currentResolveList); @@ -1426,16 +1464,6 @@ public class ResolverActivity extends Activity { } } - private void disableLastChosenIfNeeded() { - // Layout doesn't handle both profile button and last chosen - // so disable last chosen if profile button is present. - if (mOtherProfile != null && mLastChosenPosition >= 0) { - mLastChosenPosition = -1; - mFilterLastUsed = false; - } - } - - private void processSortedList(List sortedComponents) { int N; if (sortedComponents != null && (N = sortedComponents.size()) != 0) { @@ -1503,10 +1531,27 @@ public class ResolverActivity extends Activity { processGroup(sortedComponents, start, (N - 1), rci0, r0Label); } - setTitleAndIcon(); - resetAlwaysOrOnceButtonBar(); - disableLastChosenIfNeeded(); - onListRebuilt(); + postListReadyRunnable(); + } + + /** + * Some necessary methods for creating the list are initiated in onCreate and will also + * determine the layout known. We therefore can't update the UI inline and post to the + * handler thread to update after the current task is finished. + */ + private void postListReadyRunnable() { + if (mPostListReadyRunnable == null) { + mPostListReadyRunnable = new Runnable() { + @Override + public void run() { + setTitleAndIcon(); + resetAlwaysOrOnceButtonBar(); + onListRebuilt(); + mPostListReadyRunnable = null; + } + }; + getMainThreadHandler().post(mPostListReadyRunnable); + } } public void onListRebuilt() { @@ -1588,6 +1633,11 @@ public class ResolverActivity extends Activity { } private void updateLastChosenPosition(ResolveInfo info) { + // If another profile is present, ignore the last chosen entry. + if (mOtherProfile != null) { + mLastChosenPosition = -1; + return; + } if (mLastChosen != null && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { @@ -1595,12 +1645,10 @@ public class ResolverActivity extends Activity { } } + // We assume that at this point we've already filtered out the only intent for a different + // targetUserId which we're going to use. private void addResolveInfo(DisplayResolveInfo dri) { - if (dri.mResolveInfo.targetUserId != UserHandle.USER_CURRENT && mOtherProfile == null) { - // So far we only support a single other profile at a time. - // The first one we see gets special treatment. - mOtherProfile = dri; - } else { + if (dri.mResolveInfo.targetUserId == UserHandle.USER_CURRENT) { mDisplayList.add(dri); } } @@ -1841,7 +1889,8 @@ public class ResolverActivity extends Activity { final int checkedPos = mAdapterView.getCheckedItemPosition(); final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; - if (mAlwaysUseOption && (!hasValidSelection || mLastSelected != checkedPos)) { + if (!useLayoutWithDefault() + && (!hasValidSelection || mLastSelected != checkedPos)) { setAlwaysButtonEnabled(hasValidSelection, checkedPos, true); mOnceButton.setEnabled(hasValidSelection); if (hasValidSelection) { diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index daebf888f7154..1080a9fcfe715 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -211,6 +211,119 @@ public class ChooserActivityTest { assertThat(activity.isFinishing(), is(true)); } + @Test + public void hasOtherProfileOneOption() throws Exception { + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(ChooserWrapperActivity.sOverrides.resolverListController.getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ChooserWrapperActivity activity = mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserWrapperActivity.sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List stableCopy = + createResolvedComponentsForTestWithOtherProfile(2); + // Check that the "Other Profile" activity is put in the right spot + onView(withId(R.id.profile_button)).check(matches( + withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(ChooserWrapperActivity.sOverrides.resolverListController.getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + when(ChooserWrapperActivity.sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ChooserWrapperActivity activity = mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserWrapperActivity.sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List stableCopy = + createResolvedComponentsForTestWithOtherProfile(3); + // Check that the "Other Profile" activity is put in the right spot + onView(withId(R.id.profile_button)).check(matches( + withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasLastChosenActivityAndOtherProfile() throws Exception { + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(ChooserWrapperActivity.sOverrides.resolverListController.getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ChooserWrapperActivity activity = mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserWrapperActivity.sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List stableCopy = + createResolvedComponentsForTestWithOtherProfile(3); + // Check that the "Other Profile" activity is put in the right spot + onView(withId(R.id.profile_button)).check(matches( + withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -227,6 +340,19 @@ public class ChooserActivityTest { return infoList; } + private List createResolvedComponentsForTestWithOtherProfile( + int numberOfResults) { + List infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + } + return infoList; + } + private void waitForIdle() { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java index 84b844a5b4b6d..2c23018404198 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -43,6 +43,7 @@ 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.isEnabled; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static com.android.internal.app.ResolverWrapperActivity.sOverrides; @@ -102,6 +103,7 @@ public class ResolverActivityTest { public void hasLastChosenActivity() throws Exception { Intent sendIntent = createSendImageIntent(); List resolvedComponentInfos = createResolvedComponentsForTest(2); + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), Mockito.anyBoolean(), @@ -121,14 +123,133 @@ public class ResolverActivityTest { return true; }; - ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); - onView(withId(R.id.title)).perform(click()); + onView(withId(R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileOneOption() throws Exception { + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List stableCopy = + createResolvedComponentsForTestWithOtherProfile(2); + // Check that the "Other Profile" activity is put in the right spot + onView(withId(R.id.profile_button)).check(matches( + withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); onView(withId(R.id.button_once)) .perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); } + @Test + public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List stableCopy = + createResolvedComponentsForTestWithOtherProfile(2); + + // Check that the "Other Profile" activity is put in the right spot + onView(withId(R.id.profile_button)).check(matches( + withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + + @Test + public void hasLastChosenActivityAndOtherProfile() throws Exception { + // In this case we prefer the other profile and don't display anything about the last + // chosen activity. + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List stableCopy = + createResolvedComponentsForTestWithOtherProfile(2); + + // Check that the "Other Profile" activity is put in the right spot + onView(withId(R.id.profile_button)).check(matches( + withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -145,6 +266,19 @@ public class ResolverActivityTest { return infoList; } + private List createResolvedComponentsForTestWithOtherProfile( + int numberOfResults) { + List infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + } + return infoList; + } + private void waitForIdle() { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java b/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java index ae063065afad5..c710b9ad77bfc 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java @@ -32,9 +32,16 @@ import java.util.concurrent.atomic.AtomicInteger; */ class ResolverDataProvider { + static private int USER_SOMEONE_ELSE = 10; + static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfo(int i) { return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), - createResolverIntent(i), createResolveInfo(i)); + createResolverIntent(i), createResolveInfo(i, UserHandle.USER_CURRENT)); + } + + static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) { + return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), + createResolverIntent(i), createResolveInfo(i, USER_SOMEONE_ELSE)); } static ComponentName createComponentName(int i) { @@ -42,10 +49,10 @@ class ResolverDataProvider { return new ComponentName("foo.bar." + name, name); } - static ResolveInfo createResolveInfo(int i) { + static ResolveInfo createResolveInfo(int i, int userId) { final ResolveInfo resolveInfo = new ResolveInfo(); resolveInfo.activityInfo = createActivityInfo(i); - resolveInfo.targetUserId = UserHandle.USER_CURRENT; + resolveInfo.targetUserId = userId; return resolveInfo; } diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java index cd465acb9c7e7..ec5030b8b0282 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java @@ -120,8 +120,7 @@ public class UsbResolverActivity extends ResolverActivity { } CharSequence title = getResources().getText(com.android.internal.R.string.chooseUsbActivity); - super.onCreate(savedInstanceState, target, title, null, rList, - true /* Set alwaysUseOption to true to enable "always use this app" checkbox. */ ); + super.onCreate(savedInstanceState, target, title, null, rList, true); CheckBox alwaysUse = (CheckBox)findViewById(com.android.internal.R.id.alwaysUse); if (alwaysUse != null) {