diff options
14 files changed, 266 insertions, 10 deletions
diff --git a/core/java/com/android/internal/app/AbstractResolverComparator.java b/core/java/com/android/internal/app/AbstractResolverComparator.java index 9ac979b54716..bb7e4d5114ba 100644 --- a/core/java/com/android/internal/app/AbstractResolverComparator.java +++ b/core/java/com/android/internal/app/AbstractResolverComparator.java @@ -30,14 +30,17 @@ import android.util.Log; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; +import java.text.Collator; import java.util.ArrayList; import java.util.Comparator; import java.util.List; /** * Used to sort resolved activities in {@link ResolverListController}. + * + * @hide */ -abstract class AbstractResolverComparator implements Comparator<ResolvedComponentInfo> { +public abstract class AbstractResolverComparator implements Comparator<ResolvedComponentInfo> { private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3; private static final boolean DEBUG = false; @@ -62,6 +65,8 @@ abstract class AbstractResolverComparator implements Comparator<ResolvedComponen // predicting ranking scores. private static final int WATCHDOG_TIMEOUT_MILLIS = 500; + private final Comparator<ResolveInfo> mAzComparator; + protected final Handler mHandler = new Handler(Looper.getMainLooper()) { public void handleMessage(Message msg) { switch (msg.what) { @@ -90,7 +95,7 @@ abstract class AbstractResolverComparator implements Comparator<ResolvedComponen } }; - AbstractResolverComparator(Context context, Intent intent) { + public AbstractResolverComparator(Context context, Intent intent) { String scheme = intent.getScheme(); mHttp = "http".equals(scheme) || "https".equals(scheme); mContentType = intent.getType(); @@ -100,6 +105,7 @@ abstract class AbstractResolverComparator implements Comparator<ResolvedComponen mDefaultBrowserPackageName = mHttp ? mPm.getDefaultBrowserPackageNameAsUser(UserHandle.myUserId()) : null; + mAzComparator = new AzInfoComparator(context); } // get annotations of content from intent. @@ -168,6 +174,20 @@ abstract class AbstractResolverComparator implements Comparator<ResolvedComponen return lhsSpecific ? -1 : 1; } } + + final boolean lPinned = lhsp.isPinned(); + final boolean rPinned = rhsp.isPinned(); + + // Pinned items always receive priority. + if (lPinned && !rPinned) { + return -1; + } else if (!lPinned && rPinned) { + return 1; + } else if (lPinned && rPinned) { + // If both items are pinned, resolve the tie alphabetically. + return mAzComparator.compare(lhsp.getResolveInfoAt(0), rhsp.getResolveInfoAt(0)); + } + return compare(lhs, rhs); } @@ -258,4 +278,26 @@ abstract class AbstractResolverComparator implements Comparator<ResolvedComponen } return false; } + + + /** + * Sort intents alphabetically based on package name. + */ + class AzInfoComparator implements Comparator<ResolveInfo> { + Collator mCollator; + AzInfoComparator(Context context) { + mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); + } + + @Override + public int compare(ResolveInfo lhsp, ResolveInfo rhsp) { + if (lhsp == null) { + return -1; + } else if (rhsp == null) { + return 1; + } + return mCollator.compare(lhsp.activityInfo.packageName, rhsp.activityInfo.packageName); + } + } + } diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 6c372e43c1c5..b31bc52a24c9 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -43,6 +43,7 @@ import android.content.IntentFilter; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -64,6 +65,7 @@ import android.metrics.LogMaker; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Message; @@ -73,6 +75,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; import android.os.UserManager; +import android.os.storage.StorageManager; import android.provider.DeviceConfig; import android.provider.DocumentsContract; import android.provider.Downloads; @@ -121,6 +124,7 @@ import com.android.internal.widget.ResolverDrawerLayout; import com.google.android.collect.Lists; +import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -241,6 +245,9 @@ public class ChooserActivity extends ResolverActivity implements private static final int MAX_EXTRA_INITIAL_INTENTS = 2; private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; + private SharedPreferences mPinnedSharedPrefs; + private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings"; + @Retention(SOURCE) @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT}) private @interface ContentPreviewType { @@ -580,6 +587,8 @@ public class ChooserActivity extends ResolverActivity implements Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); setSafeForwardingMode(true); + mPinnedSharedPrefs = getPinnedSharedPrefs(this); + pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS); if (pa != null) { ComponentName[] names = new ComponentName[pa.length]; @@ -710,6 +719,22 @@ public class ChooserActivity extends ResolverActivity implements } } + static SharedPreferences getPinnedSharedPrefs(Context context) { + // The code below is because in the android:ui process, no one can hear you scream. + // The package info in the context isn't initialized in the way it is for normal apps, + // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we + // build the path manually below using the same policy that appears in ContextImpl. + // This fails silently under the hood if there's a problem, so if we find ourselves in + // the case where we don't have access to credential encrypted storage we just won't + // have our pinned target info. + final File prefsFile = new File(new File( + Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL, + context.getUserId(), context.getPackageName()), + "shared_prefs"), + PINNED_SHARED_PREFS_NAME + ".xml"); + return context.getSharedPreferences(prefsFile, MODE_PRIVATE); + } + /** * Returns true if app prediction service is defined and the component exists on device. */ @@ -1272,9 +1297,10 @@ public class ChooserActivity extends ResolverActivity implements } ComponentName name = ri.activityInfo.getComponentName(); + boolean pinned = mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); ResolverTargetActionsDialogFragment f = new ResolverTargetActionsDialogFragment(ri.loadLabel(getPackageManager()), - name); + name, pinned); f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); } @@ -1697,7 +1723,6 @@ public class ChooserActivity extends ResolverActivity implements allAppTargets.get(indexInAllShortcuts)); } } - // Sort ChooserTargets by score in descending order Comparator<ChooserTarget> byScore = (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore()); @@ -1945,6 +1970,11 @@ public class ChooserActivity extends ResolverActivity implements } return false; } + + @Override + public boolean isComponentPinned(ComponentName name) { + return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); + } } @Override diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 0997cf87d592..3257329dc263 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -1263,6 +1263,7 @@ public class ResolverActivity extends Activity implements public final ComponentName name; private final List<Intent> mIntents = new ArrayList<>(); private final List<ResolveInfo> mResolveInfos = new ArrayList<>(); + private boolean mPinned; public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) { this.name = name; @@ -1303,6 +1304,14 @@ public class ResolverActivity extends Activity implements } return -1; } + + public boolean isPinned() { + return mPinned; + } + + public void setPinned(boolean pinned) { + mPinned = pinned; + } } class ItemClickListener implements AdapterView.OnItemClickListener, diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 4570c6f06a28..bb7ca358f815 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -376,6 +376,10 @@ public class ResolverListAdapter extends BaseAdapter { final DisplayResolveInfo dri = new DisplayResolveInfo(intent, add, replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add)); + dri.setPinned(rci.isPinned()); + if (rci.isPinned()) { + Log.i(TAG, "Pinned item: " + rci.name); + } addResolveInfo(dri); if (replaceIntent == intent) { // Only add alternates if we didn't get a specific replacement from diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java index 6cc60b786e55..b456ca00fe2b 100644 --- a/core/java/com/android/internal/app/ResolverListController.java +++ b/core/java/com/android/internal/app/ResolverListController.java @@ -150,11 +150,21 @@ public class ResolverListController { newInfo.activityInfo.packageName, newInfo.activityInfo.name); final ResolverActivity.ResolvedComponentInfo rci = new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo); + rci.setPinned(isComponentPinned(name)); into.add(rci); } } } + + /** + * Whether this component is pinned by the user. Always false for resolver; overridden in + * Chooser. + */ + public boolean isComponentPinned(ComponentName name) { + return false; + } + // Filter out any activities that the launched uid does not have permission for. // To preserve the inputList, optionally will return the original list if any modification has // been made. diff --git a/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java b/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java index a49240cd0019..df91c4a1f88d 100644 --- a/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java +++ b/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java @@ -23,6 +23,7 @@ import android.app.DialogFragment; import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; @@ -36,26 +37,33 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { private static final String NAME_KEY = "componentName"; private static final String TITLE_KEY = "title"; + private static final String PINNED_KEY = "pinned"; // Sync with R.array.resolver_target_actions_* resources - private static final int APP_INFO_INDEX = 0; + private static final int TOGGLE_PIN_INDEX = 0; + private static final int APP_INFO_INDEX = 1; public ResolverTargetActionsDialogFragment() { } - public ResolverTargetActionsDialogFragment(CharSequence title, ComponentName name) { + public ResolverTargetActionsDialogFragment(CharSequence title, ComponentName name, + boolean pinned) { Bundle args = new Bundle(); args.putCharSequence(TITLE_KEY, title); args.putParcelable(NAME_KEY, name); + args.putBoolean(PINNED_KEY, pinned); setArguments(args); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Bundle args = getArguments(); + final int itemRes = args.getBoolean(PINNED_KEY, false) + ? R.array.resolver_target_actions_unpin + : R.array.resolver_target_actions_pin; return new Builder(getContext()) .setCancelable(true) - .setItems(R.array.resolver_target_actions, this) + .setItems(itemRes, this) .setTitle(args.getCharSequence(TITLE_KEY)) .create(); } @@ -65,6 +73,19 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment final Bundle args = getArguments(); ComponentName name = args.getParcelable(NAME_KEY); switch (which) { + case TOGGLE_PIN_INDEX: + SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext()); + final String key = name.flattenToString(); + boolean currentVal = sp.getBoolean(name.flattenToString(), false); + if (currentVal) { + sp.edit().remove(key).apply(); + } else { + sp.edit().putBoolean(key, true).apply(); + } + + // Force the chooser to requery and resort things + getActivity().recreate(); + break; case APP_INFO_INDEX: Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) .setData(Uri.fromParts("package", name.getPackageName(), null)) diff --git a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java index c77444e949ed..f92637c1bf01 100644 --- a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java +++ b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java @@ -51,6 +51,7 @@ public class DisplayResolveInfo implements TargetInfo { private final List<Intent> mSourceIntents = new ArrayList<>(); private boolean mIsSuspended; private ResolveInfoPresentationGetter mResolveInfoPresentationGetter; + private boolean mPinned = false; public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent, ResolveInfoPresentationGetter resolveInfoPresentationGetter) { @@ -179,4 +180,13 @@ public class DisplayResolveInfo implements TargetInfo { public boolean isSuspended() { return mIsSuspended; } + + @Override + public boolean isPinned() { + return mPinned; + } + + public void setPinned(boolean pinned) { + mPinned = pinned; + } } diff --git a/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java index 22cbdaa66267..69d5ce49b552 100644 --- a/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java +++ b/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java @@ -85,4 +85,8 @@ public abstract class NotSelectableTargetInfo implements ChooserTargetInfo { public boolean isSuspended() { return false; } + + public boolean isPinned() { + return false; + } } diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java index 1cc4857b39fe..86a0d10cf67e 100644 --- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java +++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java @@ -301,6 +301,11 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { return results; } + @Override + public boolean isPinned() { + return mSourceInfo != null && mSourceInfo.isPinned(); + } + /** * Necessary methods to communicate between {@link SelectableTargetInfo} * and {@link ResolverActivity} or {@link ChooserActivity}. diff --git a/core/java/com/android/internal/app/chooser/TargetInfo.java b/core/java/com/android/internal/app/chooser/TargetInfo.java index b59def174828..f56ab17cb059 100644 --- a/core/java/com/android/internal/app/chooser/TargetInfo.java +++ b/core/java/com/android/internal/app/chooser/TargetInfo.java @@ -122,7 +122,12 @@ public interface TargetInfo { List<Intent> getAllSourceIntents(); /** - * @return true if this target can be selected by the user + * @return true if this target cannot be selected by the user */ boolean isSuspended(); + + /** + * @return true if this target should be pinned to the front by the request of the user + */ + boolean isPinned(); } diff --git a/core/res/res/values/arrays.xml b/core/res/res/values/arrays.xml index f05898561b8a..8f2d6c3e02f4 100644 --- a/core/res/res/values/arrays.xml +++ b/core/res/res/values/arrays.xml @@ -175,7 +175,13 @@ </array> <!-- Used in ResolverTargetActionsDialogFragment --> - <string-array name="resolver_target_actions"> + <string-array name="resolver_target_actions_pin"> + <item>@string/pin_target</item> + <item>@string/app_info</item> + </string-array> + + <string-array name="resolver_target_actions_unpin"> + <item>@string/unpin_target</item> <item>@string/app_info</item> </string-array> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 0f650e37db2f..0b198a7cc7fd 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4967,6 +4967,10 @@ <string name="usb_mtp_launch_notification_description">Tap to view files</string> <!-- Resolver target actions strings --> + <!-- Pin this app to the top of the Sharesheet app list. [CHAR LIMIT=60]--> + <string name="pin_target">Pin</string> + <!-- Un-pin this app in the Sharesheet, so that it is sorted normally. [CHAR LIMIT=60]--> + <string name="unpin_target">Unpin</string> <!-- View application info for a target. --> <string name="app_info">App info</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 23402c143fa0..69a44479ba5a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2941,7 +2941,8 @@ <java-symbol type="color" name="notification_material_background_color" /> <!-- Resolver target actions --> - <java-symbol type="array" name="resolver_target_actions" /> + <java-symbol type="array" name="resolver_target_actions_pin" /> + <java-symbol type="array" name="resolver_target_actions_unpin" /> <java-symbol type="array" name="non_removable_euicc_slots" /> diff --git a/core/tests/coretests/src/com/android/internal/app/AbstractResolverComparatorTest.java b/core/tests/coretests/src/com/android/internal/app/AbstractResolverComparatorTest.java new file mode 100644 index 000000000000..36dd3e4e72b9 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/AbstractResolverComparatorTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import static junit.framework.Assert.assertEquals; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.os.Message; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Test; + +import java.util.List; + +public class AbstractResolverComparatorTest { + + @Test + public void testPinned() { + ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("package", "class"), new Intent(), new ResolveInfo() + ); + r1.setPinned(true); + + ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("zackage", "zlass"), new Intent(), new ResolveInfo() + ); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context); + + assertEquals("Pinned ranks over unpinned", -1, comparator.compare(r1, r2)); + assertEquals("Unpinned ranks under pinned", 1, comparator.compare(r2, r1)); + } + + + @Test + public void testBothPinned() { + ResolveInfo pmInfo1 = new ResolveInfo(); + pmInfo1.activityInfo = new ActivityInfo(); + pmInfo1.activityInfo.packageName = "aaa"; + + ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("package", "class"), new Intent(), pmInfo1); + r1.setPinned(true); + + ResolveInfo pmInfo2 = new ResolveInfo(); + pmInfo2.activityInfo = new ActivityInfo(); + pmInfo2.activityInfo.packageName = "zzz"; + ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("zackage", "zlass"), new Intent(), pmInfo2); + r2.setPinned(true); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context); + + assertEquals("Both pinned should rank alphabetically", -1, comparator.compare(r1, r2)); + } + + private AbstractResolverComparator getTestComparator(Context context) { + Intent intent = new Intent(); + + AbstractResolverComparator testComparator = + new AbstractResolverComparator(context, intent) { + + @Override + int compare(ResolveInfo lhs, ResolveInfo rhs) { + // Used for testing pinning, so we should never get here --- the overrides should + // determine the result instead. + return 1; + } + + @Override + void doCompute(List<ResolverActivity.ResolvedComponentInfo> targets) {} + + @Override + float getScore(ComponentName name) { + return 0; + } + + @Override + void handleResultMessage(Message message) {} + }; + return testComparator; + } + +} |