Merge "Separate adapter logic from ChooserActivity and ResolverActivity"

This commit is contained in:
TreeHugger Robot
2019-10-29 16:43:38 +00:00
committed by Android (Google) Code Review
18 changed files with 2542 additions and 1989 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.app;
import android.app.prediction.AppPredictionManager;
/**
* Common flags for {@link ChooserListAdapter} and {@link ChooserActivity}.
*/
public class ChooserFlags {
/**
* If set to true, use ShortcutManager to retrieve the matching direct share targets, instead of
* binding to every ChooserTargetService implementation.
*/
// TODO(b/121287573): Replace with a system flag (setprop?)
public static final boolean USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS = true;
/**
* If {@link ChooserFlags#USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS} and this is set to true,
* {@link AppPredictionManager} will be queried for direct share targets.
*/
// TODO(b/123089490): Replace with system flag
static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true;
}

View File

@@ -0,0 +1,539 @@
/*
* 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 com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
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.LabeledIntent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.AsyncTask;
import android.os.UserManager;
import android.service.chooser.ChooserTarget;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.android.internal.R;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.ChooserTargetInfo;
import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.app.chooser.SelectableTargetInfo;
import com.android.internal.app.chooser.TargetInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ChooserListAdapter extends ResolverListAdapter {
private static final String TAG = "ChooserListAdapter";
private static final boolean DEBUG = false;
public static final int TARGET_BAD = -1;
public static final int TARGET_CALLER = 0;
public static final int TARGET_SERVICE = 1;
public static final int TARGET_STANDARD = 2;
public static final int TARGET_STANDARD_AZ = 3;
private static final int MAX_SUGGESTED_APP_TARGETS = 4;
private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
static final int MAX_SERVICE_TARGETS = 8;
/** {@link #getBaseScore} */
public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
/** {@link #getBaseScore} */
public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
private final int mMaxShortcutTargetsPerApp;
private final ChooserListCommunicator mChooserListCommunicator;
private final SelectableTargetInfo.SelectableTargetInfoCommunicator
mSelectableTargetInfoComunicator;
private int mNumShortcutResults = 0;
// Reserve spots for incoming direct share targets by adding placeholders
private ChooserTargetInfo
mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
private final List<TargetInfo> mCallerTargets = new ArrayList<>();
private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
new ChooserActivity.BaseChooserTargetComparator();
private boolean mListViewDataChanged = false;
// Sorted list of DisplayResolveInfos for the alphabetical app section.
private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
public ChooserListAdapter(Context context, List<Intent> payloadIntents,
Intent[] initialIntents, List<ResolveInfo> rList,
boolean filterLastUsed, ResolverListController resolverListController,
boolean useLayoutForBrowsables,
ChooserListCommunicator chooserListCommunicator,
SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoComunicator) {
// 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, filterLastUsed,
resolverListController, useLayoutForBrowsables,
chooserListCommunicator);
createPlaceHolders();
mMaxShortcutTargetsPerApp =
context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
mChooserListCommunicator = chooserListCommunicator;
mSelectableTargetInfoComunicator = selectableTargetInfoComunicator;
if (initialIntents != null) {
final PackageManager pm = context.getPackageManager();
for (int i = 0; i < initialIntents.length; i++) {
final Intent ii = initialIntents[i];
if (ii == null) {
continue;
}
// We reimplement Intent#resolveActivityInfo here because if we have an
// implicit intent, we want the ResolveInfo returned by PackageManager
// instead of one we reconstruct ourselves. The ResolveInfo returned might
// have extra metadata and resolvePackageName set and we want to respect that.
ResolveInfo ri = null;
ActivityInfo ai = null;
final ComponentName cn = ii.getComponent();
if (cn != null) {
try {
ai = pm.getActivityInfo(ii.getComponent(), 0);
ri = new ResolveInfo();
ri.activityInfo = ai;
} catch (PackageManager.NameNotFoundException ignored) {
// ai will == null below
}
}
if (ai == null) {
ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY);
ai = ri != null ? ri.activityInfo : null;
}
if (ai == null) {
Log.w(TAG, "No activity found for " + ii);
continue;
}
UserManager userManager =
(UserManager) context.getSystemService(Context.USER_SERVICE);
if (ii instanceof LabeledIntent) {
LabeledIntent li = (LabeledIntent) ii;
ri.resolvePackageName = li.getSourcePackage();
ri.labelRes = li.getLabelResource();
ri.nonLocalizedLabel = li.getNonLocalizedLabel();
ri.icon = li.getIconResource();
ri.iconResourceId = ri.icon;
}
if (userManager.isManagedProfile()) {
ri.noResourceId = true;
ri.icon = 0;
}
mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
}
}
}
@Override
public void handlePackagesChanged() {
if (DEBUG) {
Log.d(TAG, "clearing queryTargets on package change");
}
createPlaceHolders();
mChooserListCommunicator.onHandlePackagesChanged();
}
@Override
public void notifyDataSetChanged() {
if (!mListViewDataChanged) {
mChooserListCommunicator.sendListViewUpdateMessage();
mListViewDataChanged = true;
}
}
void refreshListView() {
if (mListViewDataChanged) {
super.notifyDataSetChanged();
}
mListViewDataChanged = false;
}
private void createPlaceHolders() {
mNumShortcutResults = 0;
mServiceTargets.clear();
for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
mServiceTargets.add(mPlaceHolderTargetInfo);
}
}
@Override
public View onCreateView(ViewGroup parent) {
return mInflater.inflate(
com.android.internal.R.layout.resolve_grid_item, parent, false);
}
@Override
protected void onBindView(View view, TargetInfo info) {
super.onBindView(view, info);
// If target is loading, show a special placeholder shape in the label, make unclickable
final ViewHolder holder = (ViewHolder) view.getTag();
if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
final int maxWidth = mContext.getResources().getDimensionPixelSize(
R.dimen.chooser_direct_share_label_placeholder_max_width);
holder.text.setMaxWidth(maxWidth);
holder.text.setBackground(mContext.getResources().getDrawable(
R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
// Prevent rippling by removing background containing ripple
holder.itemView.setBackground(null);
} else {
holder.text.setMaxWidth(Integer.MAX_VALUE);
holder.text.setBackground(null);
holder.itemView.setBackground(holder.defaultItemViewBackground);
}
}
void updateAlphabeticalList() {
mSortedList.clear();
mSortedList.addAll(mDisplayList);
Collections.sort(mSortedList, new ChooserActivity.AzInfoComparator(mContext));
}
@Override
public boolean shouldGetResolvedFilter() {
return true;
}
@Override
public int getCount() {
return getRankedTargetCount() + getAlphaTargetCount()
+ getSelectableServiceTargetCount() + getCallerTargetCount();
}
@Override
public int getUnfilteredCount() {
int appTargets = super.getUnfilteredCount();
if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
}
return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
}
public int getCallerTargetCount() {
return Math.min(mCallerTargets.size(), MAX_SUGGESTED_APP_TARGETS);
}
/**
* Filter out placeholders and non-selectable service targets
*/
public int getSelectableServiceTargetCount() {
int count = 0;
for (ChooserTargetInfo info : mServiceTargets) {
if (info instanceof SelectableTargetInfo) {
count++;
}
}
return count;
}
public int getServiceTargetCount() {
if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
&& !ActivityManager.isLowRamDeviceStatic()) {
return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS);
}
return 0;
}
int getAlphaTargetCount() {
int standardCount = super.getCount();
return standardCount > mChooserListCommunicator.getMaxRankedTargets() ? standardCount : 0;
}
int getRankedTargetCount() {
int spacesAvailable =
mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
return Math.min(spacesAvailable, super.getCount());
}
public int getPositionTargetType(int position) {
int offset = 0;
final int serviceTargetCount = getServiceTargetCount();
if (position < serviceTargetCount) {
return TARGET_SERVICE;
}
offset += serviceTargetCount;
final int callerTargetCount = getCallerTargetCount();
if (position - offset < callerTargetCount) {
return TARGET_CALLER;
}
offset += callerTargetCount;
final int rankedTargetCount = getRankedTargetCount();
if (position - offset < rankedTargetCount) {
return TARGET_STANDARD;
}
offset += rankedTargetCount;
final int standardTargetCount = getAlphaTargetCount();
if (position - offset < standardTargetCount) {
return TARGET_STANDARD_AZ;
}
return TARGET_BAD;
}
@Override
public TargetInfo getItem(int position) {
return targetInfoForPosition(position, true);
}
/**
* Find target info for a given position.
* Since ChooserActivity displays several sections of content, determine which
* section provides this item.
*/
@Override
public TargetInfo targetInfoForPosition(int position, boolean filtered) {
int offset = 0;
// Direct share targets
final int serviceTargetCount = filtered ? getServiceTargetCount() :
getSelectableServiceTargetCount();
if (position < serviceTargetCount) {
return mServiceTargets.get(position);
}
offset += serviceTargetCount;
// Targets provided by calling app
final int callerTargetCount = getCallerTargetCount();
if (position - offset < callerTargetCount) {
return mCallerTargets.get(position - offset);
}
offset += callerTargetCount;
// Ranked standard app targets
final int rankedTargetCount = getRankedTargetCount();
if (position - offset < rankedTargetCount) {
return filtered ? super.getItem(position - offset)
: getDisplayResolveInfo(position - offset);
}
offset += rankedTargetCount;
// Alphabetical complete app target list.
if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
return mSortedList.get(position - offset);
}
return null;
}
/**
* Evaluate targets for inclusion in the direct share area. May not be included
* if score is too low.
*/
public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
@ChooserActivity.ShareTargetType int targetType) {
if (DEBUG) {
Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
+ " targets");
}
if (targets.size() == 0) {
return;
}
final float baseScore = getBaseScore(origTarget, targetType);
Collections.sort(targets, mBaseTargetComparator);
final boolean isShortcutResult =
(targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
|| targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
: MAX_CHOOSER_TARGETS_PER_APP;
float lastScore = 0;
boolean shouldNotify = false;
for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) {
final ChooserTarget target = targets.get(i);
float targetScore = target.getScore();
targetScore *= baseScore;
if (i > 0 && targetScore >= lastScore) {
// Apply a decay so that the top app can't crowd out everything else.
// This incents ChooserTargetServices to define what's truly better.
targetScore = lastScore * 0.95f;
}
boolean isInserted = insertServiceTarget(new SelectableTargetInfo(
mContext, origTarget, target, targetScore, mSelectableTargetInfoComunicator));
if (isInserted && isShortcutResult) {
mNumShortcutResults++;
}
shouldNotify |= isInserted;
if (DEBUG) {
Log.d(TAG, " => " + target.toString() + " score=" + targetScore
+ " base=" + target.getScore()
+ " lastScore=" + lastScore
+ " baseScore=" + baseScore);
}
lastScore = targetScore;
}
if (shouldNotify) {
notifyDataSetChanged();
}
}
int getNumShortcutResults() {
return mNumShortcutResults;
}
/**
* Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
* <ol>
* <li>App-supplied targets
* <li>Shortcuts ranked via App Prediction Manager
* <li>Shortcuts ranked via legacy heuristics
* <li>Legacy direct share targets
* </ol>
*/
public float getBaseScore(
DisplayResolveInfo target,
@ChooserActivity.ShareTargetType int targetType) {
if (target == null) {
return CALLER_TARGET_SCORE_BOOST;
}
if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
return SHORTCUT_TARGET_SCORE_BOOST;
}
float score = super.getScore(target);
if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
return score * SHORTCUT_TARGET_SCORE_BOOST;
}
return score;
}
/**
* Calling this marks service target loading complete, and will attempt to no longer
* update the direct share area.
*/
public void completeServiceTargetLoading() {
mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
if (mServiceTargets.isEmpty()) {
mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
}
notifyDataSetChanged();
}
private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
// Avoid inserting any potentially late results
if (mServiceTargets.size() == 1
&& mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
return false;
}
// Check for duplicates and abort if found
for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
return false;
}
}
int currentSize = mServiceTargets.size();
final float newScore = chooserTargetInfo.getModifiedScore();
for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) {
final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
if (serviceTarget == null) {
mServiceTargets.set(i, chooserTargetInfo);
return true;
} else if (newScore > serviceTarget.getModifiedScore()) {
mServiceTargets.add(i, chooserTargetInfo);
return true;
}
}
if (currentSize < MAX_SERVICE_TARGETS) {
mServiceTargets.add(chooserTargetInfo);
return true;
}
return false;
}
public ChooserTarget getChooserTargetForValue(int value) {
return mServiceTargets.get(value).getChooserTarget();
}
/**
* Rather than fully sorting the input list, this sorting task will put the top k elements
* in the head of input list and fill the tail with other elements in undetermined order.
*/
@Override
AsyncTask<List<ResolvedComponentInfo>,
Void,
List<ResolvedComponentInfo>> createSortingTask() {
return new AsyncTask<List<ResolvedComponentInfo>,
Void,
List<ResolvedComponentInfo>>() {
@Override
protected List<ResolvedComponentInfo> doInBackground(
List<ResolvedComponentInfo>... params) {
mResolverListController.topK(params[0],
mChooserListCommunicator.getMaxRankedTargets());
return params[0];
}
@Override
protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
processSortedList(sortedComponents);
mChooserListCommunicator.updateProfileViewButton();
notifyDataSetChanged();
}
};
}
/**
* Necessary methods to communicate between {@link ChooserListAdapter}
* and {@link ChooserActivity}.
*/
interface ChooserListCommunicator extends ResolverListCommunicator {
int getMaxRankedTargets();
void sendListViewUpdateMessage();
boolean isSendAction(Intent targetIntent);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,862 @@
/*
* 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 android.content.Context.ACTIVITY_SERVICE;
import android.annotation.NonNull;
import android.annotation.Nullable;
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.LabeledIntent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.app.chooser.TargetInfo;
import java.util.ArrayList;
import java.util.List;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
private final List<Intent> mIntents;
private final Intent[] mInitialIntents;
private final List<ResolveInfo> mBaseResolveList;
private final PackageManager mPm;
protected final Context mContext;
private final ColorMatrixColorFilter mSuspendedMatrixColorFilter;
private final boolean mUseLayoutForBrowsables;
private final int mIconDpi;
protected ResolveInfo mLastChosen;
private DisplayResolveInfo mOtherProfile;
ResolverListController mResolverListController;
private int mPlaceholderCount;
private boolean mAllTargetsAreBrowsers = false;
protected final LayoutInflater mInflater;
// This one is the list that the Adapter will actually present.
List<DisplayResolveInfo> mDisplayList;
List<ResolvedComponentInfo> mUnfilteredResolveList;
private int mLastChosenPosition = -1;
private boolean mFilterLastUsed;
private final ResolverListCommunicator mResolverListCommunicator;
private Runnable mPostListReadyRunnable;
public ResolverListAdapter(Context context, List<Intent> payloadIntents,
Intent[] initialIntents, List<ResolveInfo> rList,
boolean filterLastUsed,
ResolverListController resolverListController,
boolean useLayoutForBrowsables,
ResolverListCommunicator resolverListCommunicator) {
mContext = context;
mIntents = payloadIntents;
mInitialIntents = initialIntents;
mBaseResolveList = rList;
mInflater = LayoutInflater.from(context);
mPm = context.getPackageManager();
mDisplayList = new ArrayList<>();
mFilterLastUsed = filterLastUsed;
mResolverListController = resolverListController;
mSuspendedMatrixColorFilter = createSuspendedColorMatrix();
mUseLayoutForBrowsables = useLayoutForBrowsables;
mResolverListCommunicator = resolverListCommunicator;
final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE);
mIconDpi = am.getLauncherLargeIconDensity();
}
public void handlePackagesChanged() {
rebuildList();
mResolverListCommunicator.onHandlePackagesChanged();
}
public void setPlaceholderCount(int count) {
mPlaceholderCount = count;
}
public int getPlaceholderCount() {
return mPlaceholderCount;
}
@Nullable
public DisplayResolveInfo getFilteredItem() {
if (mFilterLastUsed && mLastChosenPosition >= 0) {
// Not using getItem since it offsets to dodge this position for the list
return mDisplayList.get(mLastChosenPosition);
}
return null;
}
public DisplayResolveInfo getOtherProfile() {
return mOtherProfile;
}
public int getFilteredPosition() {
if (mFilterLastUsed && mLastChosenPosition >= 0) {
return mLastChosenPosition;
}
return AbsListView.INVALID_POSITION;
}
public boolean hasFilteredItem() {
return mFilterLastUsed && mLastChosen != null;
}
public float getScore(DisplayResolveInfo target) {
return mResolverListController.getScore(target);
}
public void updateModel(ComponentName componentName) {
mResolverListController.updateModel(componentName);
}
public void updateChooserCounts(String packageName, int userId, String action) {
mResolverListController.updateChooserCounts(packageName, userId, action);
}
/**
* @return true if all items in the display list are defined as browsers by
* ResolveInfo.handleAllWebDataURI
*/
public boolean areAllTargetsBrowsers() {
return mAllTargetsAreBrowsers;
}
/**
* Rebuild the list of resolvers. In some cases some parts will need some asynchronous work
* to complete.
*
* @return Whether or not the list building is completed.
*/
protected boolean rebuildList() {
List<ResolvedComponentInfo> currentResolveList = null;
// Clear the value of mOtherProfile from previous call.
mOtherProfile = null;
mLastChosen = null;
mLastChosenPosition = -1;
mAllTargetsAreBrowsers = false;
mDisplayList.clear();
if (mBaseResolveList != null) {
currentResolveList = mUnfilteredResolveList = new ArrayList<>();
mResolverListController.addResolveListDedupe(currentResolveList,
mResolverListCommunicator.getTargetIntent(),
mBaseResolveList);
} else {
currentResolveList = mUnfilteredResolveList =
mResolverListController.getResolversForIntent(shouldGetResolvedFilter(),
mResolverListCommunicator.shouldGetActivityMetadata(),
mIntents);
if (currentResolveList == null) {
processSortedList(currentResolveList);
return true;
}
List<ResolvedComponentInfo> originalList =
mResolverListController.filterIneligibleActivities(currentResolveList,
true);
if (originalList != null) {
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) {
ResolveInfo resolveInfo = info.getResolveInfoAt(0);
if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
Intent pOrigIntent = mResolverListCommunicator.getReplacementIntent(
resolveInfo.activityInfo,
info.getIntentAt(0));
Intent replacementIntent = mResolverListCommunicator.getReplacementIntent(
resolveInfo.activityInfo,
mResolverListCommunicator.getTargetIntent());
mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0),
resolveInfo,
resolveInfo.loadLabel(mPm),
resolveInfo.loadLabel(mPm),
pOrigIntent != null ? pOrigIntent : replacementIntent,
makePresentationGetter(resolveInfo));
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
// current resolve list are currently the same.
List<ResolvedComponentInfo> originalList =
mResolverListController.filterLowPriority(currentResolveList,
mUnfilteredResolveList == currentResolveList);
if (originalList != null) {
mUnfilteredResolveList = originalList;
}
if (currentResolveList.size() > 1) {
int placeholderCount = currentResolveList.size();
if (mResolverListCommunicator.useLayoutWithDefault()) {
--placeholderCount;
}
setPlaceholderCount(placeholderCount);
createSortingTask().execute(currentResolveList);
postListReadyRunnable();
return false;
} else {
processSortedList(currentResolveList);
return true;
}
} else {
processSortedList(currentResolveList);
return true;
}
}
AsyncTask<List<ResolvedComponentInfo>,
Void,
List<ResolvedComponentInfo>> createSortingTask() {
return new AsyncTask<List<ResolvedComponentInfo>,
Void,
List<ResolvedComponentInfo>>() {
@Override
protected List<ResolvedComponentInfo> doInBackground(
List<ResolvedComponentInfo>... params) {
mResolverListController.sort(params[0]);
return params[0];
}
@Override
protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
processSortedList(sortedComponents);
mResolverListCommunicator.updateProfileViewButton();
notifyDataSetChanged();
}
};
}
protected void processSortedList(List<ResolvedComponentInfo> sortedComponents) {
int n;
if (sortedComponents != null && (n = sortedComponents.size()) != 0) {
mAllTargetsAreBrowsers = mUseLayoutForBrowsables;
// First put the initial items at the top.
if (mInitialIntents != null) {
for (int i = 0; i < mInitialIntents.length; i++) {
Intent ii = mInitialIntents[i];
if (ii == null) {
continue;
}
ActivityInfo ai = ii.resolveActivityInfo(
mPm, 0);
if (ai == null) {
Log.w(TAG, "No activity found for " + ii);
continue;
}
ResolveInfo ri = new ResolveInfo();
ri.activityInfo = ai;
UserManager userManager =
(UserManager) mContext.getSystemService(Context.USER_SERVICE);
if (ii instanceof LabeledIntent) {
LabeledIntent li = (LabeledIntent) ii;
ri.resolvePackageName = li.getSourcePackage();
ri.labelRes = li.getLabelResource();
ri.nonLocalizedLabel = li.getNonLocalizedLabel();
ri.icon = li.getIconResource();
ri.iconResourceId = ri.icon;
}
if (userManager.isManagedProfile()) {
ri.noResourceId = true;
ri.icon = 0;
}
mAllTargetsAreBrowsers &= ri.handleAllWebDataURI;
addResolveInfo(new DisplayResolveInfo(ii, ri,
ri.loadLabel(mPm), null, ii, makePresentationGetter(ri)));
}
}
for (ResolvedComponentInfo rci : sortedComponents) {
final ResolveInfo ri = rci.getResolveInfoAt(0);
if (ri != null) {
mAllTargetsAreBrowsers &= ri.handleAllWebDataURI;
addResolveInfoWithAlternates(rci);
}
}
}
mResolverListCommunicator.sendVoiceChoicesIfNeeded();
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() {
mResolverListCommunicator.onPostListReady();
mPostListReadyRunnable = null;
}
};
mContext.getMainThreadHandler().post(mPostListReadyRunnable);
}
}
public boolean shouldGetResolvedFilter() {
return mFilterLastUsed;
}
private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) {
final int count = rci.getCount();
final Intent intent = rci.getIntentAt(0);
final ResolveInfo add = rci.getResolveInfoAt(0);
final Intent replaceIntent =
mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent);
final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent(
add.activityInfo, mResolverListCommunicator.getTargetIntent());
final DisplayResolveInfo
dri = new DisplayResolveInfo(intent, add,
replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add));
addResolveInfo(dri);
if (replaceIntent == intent) {
// Only add alternates if we didn't get a specific replacement from
// the caller. If we have one it trumps potential alternates.
for (int i = 1, n = count; i < n; i++) {
final Intent altIntent = rci.getIntentAt(i);
dri.addAlternateSourceIntent(altIntent);
}
}
updateLastChosenPosition(add);
}
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)) {
mLastChosenPosition = mDisplayList.size() - 1;
}
}
// 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 != null && dri.getResolveInfo() != null
&& dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) {
// Checks if this info is already listed in display.
for (DisplayResolveInfo existingInfo : mDisplayList) {
if (mResolverListCommunicator
.resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return;
}
}
mDisplayList.add(dri);
}
}
@Nullable
public ResolveInfo resolveInfoForPosition(int position, boolean filtered) {
TargetInfo target = targetInfoForPosition(position, filtered);
if (target != null) {
return target.getResolveInfo();
}
return null;
}
@Nullable
public TargetInfo targetInfoForPosition(int position, boolean filtered) {
if (filtered) {
return getItem(position);
}
if (mDisplayList.size() > position) {
return mDisplayList.get(position);
}
return null;
}
public int getCount() {
int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :
mDisplayList.size();
if (mFilterLastUsed && mLastChosenPosition >= 0) {
totalSize--;
}
return totalSize;
}
public int getUnfilteredCount() {
return mDisplayList.size();
}
@Nullable
public TargetInfo getItem(int position) {
if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) {
position++;
}
if (mDisplayList.size() > position) {
return mDisplayList.get(position);
} else {
return null;
}
}
public long getItemId(int position) {
return position;
}
public int getDisplayResolveInfoCount() {
return mDisplayList.size();
}
public DisplayResolveInfo getDisplayResolveInfo(int index) {
// Used to query services. We only query services for primary targets, not alternates.
return mDisplayList.get(index);
}
public final View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = createView(parent);
}
onBindView(view, getItem(position));
return view;
}
public final View createView(ViewGroup parent) {
final View view = onCreateView(parent);
final ViewHolder holder = new ViewHolder(view);
view.setTag(holder);
return view;
}
public View onCreateView(ViewGroup parent) {
return mInflater.inflate(
com.android.internal.R.layout.resolve_list_item, parent, false);
}
public final void bindView(int position, View view) {
onBindView(view, getItem(position));
}
protected void onBindView(View view, TargetInfo info) {
final ViewHolder holder = (ViewHolder) view.getTag();
if (info == null) {
holder.icon.setImageDrawable(
mContext.getDrawable(R.drawable.resolver_icon_placeholder));
return;
}
if (info instanceof DisplayResolveInfo
&& !((DisplayResolveInfo) info).hasDisplayLabel()) {
getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
} else {
holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo());
}
if (info.isSuspended()) {
holder.icon.setColorFilter(mSuspendedMatrixColorFilter);
} else {
holder.icon.setColorFilter(null);
}
if (info instanceof DisplayResolveInfo
&& !((DisplayResolveInfo) info).hasDisplayIcon()) {
new ResolverListAdapter.LoadIconTask((DisplayResolveInfo) info, holder.icon).execute();
} else {
holder.icon.setImageDrawable(info.getDisplayIcon(mContext));
}
}
protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
return new LoadLabelTask(info, holder);
}
public void onDestroy() {
if (mPostListReadyRunnable != null) {
mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
mPostListReadyRunnable = null;
}
if (mResolverListController != null) {
mResolverListController.destroy();
}
}
private ColorMatrixColorFilter createSuspendedColorMatrix() {
int grayValue = 127;
float scale = 0.5f; // half bright
ColorMatrix tempBrightnessMatrix = new ColorMatrix();
float[] mat = tempBrightnessMatrix.getArray();
mat[0] = scale;
mat[6] = scale;
mat[12] = scale;
mat[4] = grayValue;
mat[9] = grayValue;
mat[14] = grayValue;
ColorMatrix matrix = new ColorMatrix();
matrix.setSaturation(0.0f);
matrix.preConcat(tempBrightnessMatrix);
return new ColorMatrixColorFilter(matrix);
}
ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) {
return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai);
}
ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) {
return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri);
}
Drawable loadIconForResolveInfo(ResolveInfo ri) {
// Load icons based on the current process. If in work profile icons should be badged.
return makePresentationGetter(ri).getIcon(Process.myUserHandle());
}
void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
final DisplayResolveInfo iconInfo = getFilteredItem();
if (iconView != null && iconInfo != null) {
new LoadIconTask(iconInfo, iconView).execute();
}
}
/**
* Necessary methods to communicate between {@link ResolverListAdapter}
* and {@link ResolverActivity}.
*/
interface ResolverListCommunicator {
boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
void onPostListReady();
void sendVoiceChoicesIfNeeded();
void updateProfileViewButton();
boolean useLayoutWithDefault();
boolean shouldGetActivityMetadata();
Intent getTargetIntent();
void onHandlePackagesChanged();
}
static class ViewHolder {
public View itemView;
public Drawable defaultItemViewBackground;
public TextView text;
public TextView text2;
public ImageView icon;
ViewHolder(View view) {
itemView = view;
defaultItemViewBackground = view.getBackground();
text = (TextView) view.findViewById(com.android.internal.R.id.text1);
text2 = (TextView) view.findViewById(com.android.internal.R.id.text2);
icon = (ImageView) view.findViewById(R.id.icon);
}
public void bindLabel(CharSequence label, CharSequence subLabel) {
if (!TextUtils.equals(text.getText(), label)) {
text.setText(label);
}
// Always show a subLabel for visual consistency across list items. Show an empty
// subLabel if the subLabel is the same as the label
if (TextUtils.equals(label, subLabel)) {
subLabel = null;
}
if (!TextUtils.equals(text2.getText(), subLabel)
&& !TextUtils.isEmpty(subLabel)) {
text2.setVisibility(View.VISIBLE);
text2.setText(subLabel);
}
}
}
protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
private final DisplayResolveInfo mDisplayResolveInfo;
private final ViewHolder mHolder;
protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
mDisplayResolveInfo = dri;
mHolder = holder;
}
@Override
protected CharSequence[] doInBackground(Void... voids) {
ResolveInfoPresentationGetter pg =
makePresentationGetter(mDisplayResolveInfo.getResolveInfo());
return new CharSequence[] {
pg.getLabel(),
pg.getSubLabel()
};
}
@Override
protected void onPostExecute(CharSequence[] result) {
mDisplayResolveInfo.setDisplayLabel(result[0]);
mDisplayResolveInfo.setExtendedInfo(result[1]);
mHolder.bindLabel(result[0], result[1]);
}
}
class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
protected final com.android.internal.app.chooser.DisplayResolveInfo mDisplayResolveInfo;
private final ResolveInfo mResolveInfo;
private final ImageView mTargetView;
LoadIconTask(DisplayResolveInfo dri, ImageView target) {
mDisplayResolveInfo = dri;
mResolveInfo = dri.getResolveInfo();
mTargetView = target;
}
@Override
protected Drawable doInBackground(Void... params) {
return loadIconForResolveInfo(mResolveInfo);
}
@Override
protected void onPostExecute(Drawable d) {
if (getOtherProfile() == mDisplayResolveInfo) {
mResolverListCommunicator.updateProfileViewButton();
} else {
mDisplayResolveInfo.setDisplayIcon(d);
mTargetView.setImageDrawable(d);
}
}
}
/**
* Loads the icon and label for the provided ResolveInfo.
*/
@VisibleForTesting
public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
private final ResolveInfo mRi;
public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) {
super(ctx, iconDpi, ri.activityInfo);
mRi = ri;
}
@Override
Drawable getIconSubstituteInternal() {
Drawable dr = null;
try {
// Do not use ResolveInfo#getIconResource() as it defaults to the app
if (mRi.resolvePackageName != null && mRi.icon != 0) {
dr = loadIconFromResource(
mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ "couldn't find resources for package", e);
}
// Fall back to ActivityInfo if no icon is found via ResolveInfo
if (dr == null) dr = super.getIconSubstituteInternal();
return dr;
}
@Override
String getAppSubLabelInternal() {
// Will default to app name if no intent filter or activity label set, make sure to
// check if subLabel matches label before final display
return (String) mRi.loadLabel(mPm);
}
}
/**
* Loads the icon and label for the provided ActivityInfo.
*/
@VisibleForTesting
public static class ActivityInfoPresentationGetter extends
TargetPresentationGetter {
private final ActivityInfo mActivityInfo;
public ActivityInfoPresentationGetter(Context ctx, int iconDpi,
ActivityInfo activityInfo) {
super(ctx, iconDpi, activityInfo.applicationInfo);
mActivityInfo = activityInfo;
}
@Override
Drawable getIconSubstituteInternal() {
Drawable dr = null;
try {
// Do not use ActivityInfo#getIconResource() as it defaults to the app
if (mActivityInfo.icon != 0) {
dr = loadIconFromResource(
mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
mActivityInfo.icon);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ "couldn't find resources for package", e);
}
return dr;
}
@Override
String getAppSubLabelInternal() {
// Will default to app name if no activity label set, make sure to check if subLabel
// matches label before final display
return (String) mActivityInfo.loadLabel(mPm);
}
}
/**
* Loads the icon and label for the provided ApplicationInfo. Defaults to using the application
* icon and label over any IntentFilter or Activity icon to increase user understanding, with an
* exception for applications that hold the right permission. Always attempts to use available
* resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
* Strings to strip creative formatting.
*/
private abstract static class TargetPresentationGetter {
@Nullable abstract Drawable getIconSubstituteInternal();
@Nullable abstract String getAppSubLabelInternal();
private Context mCtx;
private final int mIconDpi;
private final boolean mHasSubstitutePermission;
private final ApplicationInfo mAi;
protected PackageManager mPm;
TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) {
mCtx = ctx;
mPm = ctx.getPackageManager();
mAi = ai;
mIconDpi = iconDpi;
mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
mAi.packageName);
}
public Drawable getIcon(UserHandle userHandle) {
return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle));
}
public Bitmap getIconBitmap(UserHandle userHandle) {
Drawable dr = null;
if (mHasSubstitutePermission) {
dr = getIconSubstituteInternal();
}
if (dr == null) {
try {
if (mAi.icon != 0) {
dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon);
}
} catch (PackageManager.NameNotFoundException ignore) {
}
}
// Fall back to ApplicationInfo#loadIcon if nothing has been loaded
if (dr == null) {
dr = mAi.loadIcon(mPm);
}
SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx);
Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle);
sif.recycle();
return icon;
}
public String getLabel() {
String label = null;
// Apps with the substitute permission will always show the sublabel as their label
if (mHasSubstitutePermission) {
label = getAppSubLabelInternal();
}
if (label == null) {
label = (String) mAi.loadLabel(mPm);
}
return label;
}
public String getSubLabel() {
// Apps with the substitute permission will never have a sublabel
if (mHasSubstitutePermission) return null;
return getAppSubLabelInternal();
}
protected String loadLabelFromResource(Resources res, int resId) {
return res.getString(resId);
}
@Nullable
protected Drawable loadIconFromResource(Resources res, int resId) {
return res.getDrawableForDensity(resId, mIconDpi);
}
}
}

View File

@@ -31,6 +31,7 @@ import android.os.RemoteException;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.chooser.DisplayResolveInfo;
import java.util.ArrayList;
import java.util.Collections;
@@ -332,7 +333,7 @@ public class ResolverListController {
}
@VisibleForTesting
public float getScore(ResolverActivity.DisplayResolveInfo target) {
public float getScore(DisplayResolveInfo target) {
return mResolverComparator.getScore(target.getResolvedComponentName());
}

View File

@@ -214,7 +214,7 @@ public class SimpleIconFactory {
* @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
*/
@Deprecated
Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) {
public Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) {
// If no icon is provided use the system default
if (icon == null) {
icon = getFullResDefaultActivityIcon(mFillResIconDpi);

View File

@@ -0,0 +1,53 @@
/*
* 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.chooser;
import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
/**
* A TargetInfo for Direct Share. Includes a {@link ChooserTarget} representing the
* Direct Share deep link into an application.
*/
public interface ChooserTargetInfo extends TargetInfo {
float getModifiedScore();
ChooserTarget getChooserTarget();
/**
* Do not label as 'equals', since this doesn't quite work
* as intended with java 8.
*/
default boolean isSimilar(ChooserTargetInfo other) {
if (other == null) return false;
ChooserTarget ct1 = getChooserTarget();
ChooserTarget ct2 = other.getChooserTarget();
// If either is null, there is not enough info to make an informed decision
// about equality, so just exit
if (ct1 == null || ct2 == null) return false;
if (ct1.getComponentName().equals(ct2.getComponentName())
&& TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
&& TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,182 @@
/*
* 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.chooser;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
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.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import com.android.internal.app.ResolverActivity;
import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
import java.util.ArrayList;
import java.util.List;
/**
* A TargetInfo plus additional information needed to render it (such as icon and label) and
* resolve it to an activity.
*/
public class DisplayResolveInfo implements TargetInfo {
// Temporary flag for new chooser delegate behavior.
private static final boolean ENABLE_CHOOSER_DELEGATE = true;
private final ResolveInfo mResolveInfo;
private CharSequence mDisplayLabel;
private Drawable mDisplayIcon;
private CharSequence mExtendedInfo;
private final Intent mResolvedIntent;
private final List<Intent> mSourceIntents = new ArrayList<>();
private boolean mIsSuspended;
private ResolveInfoPresentationGetter mResolveInfoPresentationGetter;
public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent,
ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent,
resolveInfoPresentationGetter);
}
public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
CharSequence pInfo, @NonNull Intent resolvedIntent,
@Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
mSourceIntents.add(originalIntent);
mResolveInfo = pri;
mDisplayLabel = pLabel;
mExtendedInfo = pInfo;
mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
final Intent intent = new Intent(resolvedIntent);
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
| Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
final ActivityInfo ai = mResolveInfo.activityInfo;
intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
mResolvedIntent = intent;
}
private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags,
ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
mDisplayLabel = other.mDisplayLabel;
mDisplayIcon = other.mDisplayIcon;
mExtendedInfo = other.mExtendedInfo;
mResolvedIntent = new Intent(other.mResolvedIntent);
mResolvedIntent.fillIn(fillInIntent, flags);
mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
}
public ResolveInfo getResolveInfo() {
return mResolveInfo;
}
public CharSequence getDisplayLabel() {
if (mDisplayLabel == null && mResolveInfoPresentationGetter != null) {
mDisplayLabel = mResolveInfoPresentationGetter.getLabel();
mExtendedInfo = mResolveInfoPresentationGetter.getSubLabel();
}
return mDisplayLabel;
}
public boolean hasDisplayLabel() {
return mDisplayLabel != null;
}
public void setDisplayLabel(CharSequence displayLabel) {
mDisplayLabel = displayLabel;
}
public void setExtendedInfo(CharSequence extendedInfo) {
mExtendedInfo = extendedInfo;
}
public Drawable getDisplayIcon(Context context) {
return mDisplayIcon;
}
@Override
public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter);
}
@Override
public List<Intent> getAllSourceIntents() {
return mSourceIntents;
}
public void addAlternateSourceIntent(Intent alt) {
mSourceIntents.add(alt);
}
public void setDisplayIcon(Drawable icon) {
mDisplayIcon = icon;
}
public boolean hasDisplayIcon() {
return mDisplayIcon != null;
}
public CharSequence getExtendedInfo() {
return mExtendedInfo;
}
public Intent getResolvedIntent() {
return mResolvedIntent;
}
@Override
public ComponentName getResolvedComponentName() {
return new ComponentName(mResolveInfo.activityInfo.packageName,
mResolveInfo.activityInfo.name);
}
@Override
public boolean start(Activity activity, Bundle options) {
activity.startActivity(mResolvedIntent, options);
return true;
}
@Override
public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
if (ENABLE_CHOOSER_DELEGATE) {
return activity.startAsCallerImpl(mResolvedIntent, options, false, userId);
} else {
activity.startActivityAsCaller(mResolvedIntent, options, null, false, userId);
return true;
}
}
@Override
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
activity.startActivityAsUser(mResolvedIntent, options, user);
return false;
}
public boolean isSuspended() {
return mIsSuspended;
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.chooser;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.service.chooser.ChooserTarget;
import com.android.internal.app.ResolverActivity;
import java.util.List;
/**
* Distinguish between targets that selectable by the user, vs those that are
* placeholders for the system while information is loading in an async manner.
*/
public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
public Intent getResolvedIntent() {
return null;
}
public ComponentName getResolvedComponentName() {
return null;
}
public boolean start(Activity activity, Bundle options) {
return false;
}
public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
return false;
}
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
return false;
}
public ResolveInfo getResolveInfo() {
return null;
}
public CharSequence getDisplayLabel() {
return null;
}
public CharSequence getExtendedInfo() {
return null;
}
public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
return null;
}
public List<Intent> getAllSourceIntents() {
return null;
}
public float getModifiedScore() {
return -0.1f;
}
public ChooserTarget getChooserTarget() {
return null;
}
public boolean isSuspended() {
return false;
}
}

View File

@@ -0,0 +1,316 @@
/*
* 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.chooser;
import android.app.Activity;
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.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
import android.service.chooser.ChooserTarget;
import android.text.SpannableStringBuilder;
import android.util.Log;
import com.android.internal.app.ChooserActivity;
import com.android.internal.app.ChooserFlags;
import com.android.internal.app.ResolverActivity;
import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
import com.android.internal.app.SimpleIconFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Live target, currently selectable by the user.
* @see NotSelectableTargetInfo
*/
public final class SelectableTargetInfo implements ChooserTargetInfo {
private static final String TAG = "SelectableTargetInfo";
private final Context mContext;
private final DisplayResolveInfo mSourceInfo;
private final ResolveInfo mBackupResolveInfo;
private final ChooserTarget mChooserTarget;
private final String mDisplayLabel;
private final PackageManager mPm;
private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
private Drawable mBadgeIcon = null;
private CharSequence mBadgeContentDescription;
private Drawable mDisplayIcon;
private final Intent mFillInIntent;
private final int mFillInFlags;
private final float mModifiedScore;
private boolean mIsSuspended = false;
public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
ChooserTarget chooserTarget,
float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator) {
mContext = context;
mSourceInfo = sourceInfo;
mChooserTarget = chooserTarget;
mModifiedScore = modifiedScore;
mPm = mContext.getPackageManager();
mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
if (sourceInfo != null) {
final ResolveInfo ri = sourceInfo.getResolveInfo();
if (ri != null) {
final ActivityInfo ai = ri.activityInfo;
if (ai != null && ai.applicationInfo != null) {
final PackageManager pm = mContext.getPackageManager();
mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
mIsSuspended =
(ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
}
}
}
// TODO(b/121287224): do this in the background thread, and only for selected targets
mDisplayIcon = getChooserTargetIconDrawable(chooserTarget);
if (sourceInfo != null) {
mBackupResolveInfo = null;
} else {
mBackupResolveInfo =
mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0);
}
mFillInIntent = null;
mFillInFlags = 0;
mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
}
private SelectableTargetInfo(SelectableTargetInfo other,
Intent fillInIntent, int flags) {
mContext = other.mContext;
mPm = other.mPm;
mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator;
mSourceInfo = other.mSourceInfo;
mBackupResolveInfo = other.mBackupResolveInfo;
mChooserTarget = other.mChooserTarget;
mBadgeIcon = other.mBadgeIcon;
mBadgeContentDescription = other.mBadgeContentDescription;
mDisplayIcon = other.mDisplayIcon;
mFillInIntent = fillInIntent;
mFillInFlags = flags;
mModifiedScore = other.mModifiedScore;
mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
}
private String sanitizeDisplayLabel(CharSequence label) {
SpannableStringBuilder sb = new SpannableStringBuilder(label);
sb.clearSpans();
return sb.toString();
}
public boolean isSuspended() {
return mIsSuspended;
}
/**
* Since ShortcutInfos are returned by ShortcutManager, we can cache the shortcuts and skip
* the call to LauncherApps#getShortcuts(ShortcutQuery).
*/
// TODO(121287224): Refactor code to apply the suggestion above
private Drawable getChooserTargetIconDrawable(ChooserTarget target) {
Drawable directShareIcon = null;
// First get the target drawable and associated activity info
final Icon icon = target.getIcon();
if (icon != null) {
directShareIcon = icon.loadDrawable(mContext);
} else if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS) {
Bundle extras = target.getIntentExtras();
if (extras != null && extras.containsKey(Intent.EXTRA_SHORTCUT_ID)) {
CharSequence shortcutId = extras.getCharSequence(Intent.EXTRA_SHORTCUT_ID);
LauncherApps launcherApps = (LauncherApps) mContext.getSystemService(
Context.LAUNCHER_APPS_SERVICE);
final LauncherApps.ShortcutQuery q = new LauncherApps.ShortcutQuery();
q.setPackage(target.getComponentName().getPackageName());
q.setShortcutIds(Arrays.asList(shortcutId.toString()));
q.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC);
final List<ShortcutInfo> shortcuts =
launcherApps.getShortcuts(q, mContext.getUser());
if (shortcuts != null && shortcuts.size() > 0) {
directShareIcon = launcherApps.getShortcutIconDrawable(shortcuts.get(0), 0);
}
}
}
if (directShareIcon == null) return null;
ActivityInfo info = null;
try {
info = mPm.getActivityInfo(target.getComponentName(), 0);
} catch (PackageManager.NameNotFoundException error) {
Log.e(TAG, "Could not find activity associated with ChooserTarget");
}
if (info == null) return null;
// Now fetch app icon and raster with no badging even in work profile
Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info)
.getIconBitmap(UserHandle.getUserHandleForUid(UserHandle.myUserId()));
// Raster target drawable with appIcon as a badge
SimpleIconFactory sif = SimpleIconFactory.obtain(mContext);
Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
sif.recycle();
return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon);
}
public float getModifiedScore() {
return mModifiedScore;
}
@Override
public Intent getResolvedIntent() {
if (mSourceInfo != null) {
return mSourceInfo.getResolvedIntent();
}
final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent());
targetIntent.setComponent(mChooserTarget.getComponentName());
targetIntent.putExtras(mChooserTarget.getIntentExtras());
return targetIntent;
}
@Override
public ComponentName getResolvedComponentName() {
if (mSourceInfo != null) {
return mSourceInfo.getResolvedComponentName();
} else if (mBackupResolveInfo != null) {
return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
mBackupResolveInfo.activityInfo.name);
}
return null;
}
private Intent getBaseIntentToSend() {
Intent result = getResolvedIntent();
if (result == null) {
Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
} else {
result = new Intent(result);
if (mFillInIntent != null) {
result.fillIn(mFillInIntent, mFillInFlags);
}
result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0);
}
return result;
}
@Override
public boolean start(Activity activity, Bundle options) {
throw new RuntimeException("ChooserTargets should be started as caller.");
}
@Override
public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
final Intent intent = getBaseIntentToSend();
if (intent == null) {
return false;
}
intent.setComponent(mChooserTarget.getComponentName());
intent.putExtras(mChooserTarget.getIntentExtras());
// Important: we will ignore the target security checks in ActivityManager
// if and only if the ChooserTarget's target package is the same package
// where we got the ChooserTargetService that provided it. This lets a
// ChooserTargetService provide a non-exported or permission-guarded target
// to the chooser for the user to pick.
//
// If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
// so we'll obey the caller's normal security checks.
final boolean ignoreTargetSecurity = mSourceInfo != null
&& mSourceInfo.getResolvedComponentName().getPackageName()
.equals(mChooserTarget.getComponentName().getPackageName());
return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId);
}
@Override
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
throw new RuntimeException("ChooserTargets should be started as caller.");
}
@Override
public ResolveInfo getResolveInfo() {
return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
}
@Override
public CharSequence getDisplayLabel() {
return mDisplayLabel;
}
@Override
public CharSequence getExtendedInfo() {
// ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
return null;
}
@Override
public Drawable getDisplayIcon(Context context) {
return mDisplayIcon;
}
public ChooserTarget getChooserTarget() {
return mChooserTarget;
}
@Override
public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
return new SelectableTargetInfo(this, fillInIntent, flags);
}
@Override
public List<Intent> getAllSourceIntents() {
final List<Intent> results = new ArrayList<>();
if (mSourceInfo != null) {
// We only queried the service for the first one in our sourceinfo.
results.add(mSourceInfo.getAllSourceIntents().get(0));
}
return results;
}
/**
* Necessary methods to communicate between {@link SelectableTargetInfo}
* and {@link ResolverActivity} or {@link ChooserActivity}.
*/
public interface SelectableTargetInfoCommunicator {
ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info);
Intent getTargetIntent();
Intent getReferrerFillInIntent();
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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.chooser;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import com.android.internal.app.ResolverActivity;
import java.util.List;
/**
* A single target as represented in the chooser.
*/
public interface TargetInfo {
/**
* Get the resolved intent that represents this target. Note that this may not be the
* intent that will be launched by calling one of the <code>start</code> methods provided;
* this is the intent that will be credited with the launch.
*
* @return the resolved intent for this target
*/
Intent getResolvedIntent();
/**
* Get the resolved component name that represents this target. Note that this may not
* be the component that will be directly launched by calling one of the <code>start</code>
* methods provided; this is the component that will be credited with the launch.
*
* @return the resolved ComponentName for this target
*/
ComponentName getResolvedComponentName();
/**
* Start the activity referenced by this target.
*
* @param activity calling Activity performing the launch
* @param options ActivityOptions bundle
* @return true if the start completed successfully
*/
boolean start(Activity activity, Bundle options);
/**
* Start the activity referenced by this target as if the ResolverActivity's caller
* was performing the start operation.
*
* @param activity calling Activity (actually) performing the launch
* @param options ActivityOptions bundle
* @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller
* @return true if the start completed successfully
*/
boolean startAsCaller(ResolverActivity activity, Bundle options, int userId);
/**
* Start the activity referenced by this target as a given user.
*
* @param activity calling activity performing the launch
* @param options ActivityOptions bundle
* @param user handle for the user to start the activity as
* @return true if the start completed successfully
*/
boolean startAsUser(Activity activity, Bundle options, UserHandle user);
/**
* Return the ResolveInfo about how and why this target matched the original query
* for available targets.
*
* @return ResolveInfo representing this target's match
*/
ResolveInfo getResolveInfo();
/**
* Return the human-readable text label for this target.
*
* @return user-visible target label
*/
CharSequence getDisplayLabel();
/**
* Return any extended info for this target. This may be used to disambiguate
* otherwise identical targets.
*
* @return human-readable disambig string or null if none present
*/
CharSequence getExtendedInfo();
/**
* @return The drawable that should be used to represent this target including badge
* @param context
*/
Drawable getDisplayIcon(Context context);
/**
* Clone this target with the given fill-in information.
*/
TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
/**
* @return the list of supported source intents deduped against this single target
*/
List<Intent> getAllSourceIntents();
/**
* @return true if this target can be selected by the user
*/
boolean isSuspended();
}

View File

@@ -24,12 +24,12 @@ import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.android.internal.app.ChooserActivity.CALLER_TARGET_SCORE_BOOST;
import static com.android.internal.app.ChooserActivity.SHORTCUT_TARGET_SCORE_BOOST;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_DEFAULT;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
import static com.android.internal.app.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST;
import static com.android.internal.app.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST;
import static com.android.internal.app.ChooserWrapperActivity.sOverrides;
import static org.hamcrest.CoreMatchers.is;
@@ -69,6 +69,7 @@ import androidx.test.rule.ActivityTestRule;
import com.android.internal.R;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -819,17 +820,18 @@ public class ChooserActivityTest {
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
when(sOverrides.resolverListController.getScore(Mockito.isA(
ResolverActivity.DisplayResolveInfo.class))).thenReturn(testBaseScore);
when(sOverrides.resolverListController.getScore(Mockito.isA(DisplayResolveInfo.class)))
.thenReturn(testBaseScore);
final ChooserWrapperActivity activity = mActivityRule
.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
final ResolverActivity.DisplayResolveInfo testDri =
final DisplayResolveInfo testDri =
activity.createTestDisplayResolveInfo(sendIntent,
ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent);
final ChooserActivity.ChooserListAdapter adapter = activity.getAdapter();
ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent,
/* resolveInfoPresentationGetter */ null);
final ChooserListAdapter adapter = activity.getAdapter();
assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST));
assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore));
@@ -970,7 +972,8 @@ public class ChooserActivityTest {
ri,
"testLabel",
"testInfo",
sendIntent),
sendIntent,
/* resolveInfoPresentationGetter */ null),
serviceTargets,
TARGET_TYPE_CHOOSER_TARGET)
);
@@ -1036,7 +1039,8 @@ public class ChooserActivityTest {
ri,
"testLabel",
"testInfo",
sendIntent),
sendIntent,
/* resolveInfoPresentationGetter */ null),
serviceTargets,
TARGET_TYPE_CHOOSER_TARGET)
);
@@ -1097,7 +1101,8 @@ public class ChooserActivityTest {
ri,
"testLabel",
"testInfo",
sendIntent),
sendIntent,
/* resolveInfoPresentationGetter */ null),
serviceTargets,
TARGET_TYPE_CHOOSER_TARGET)
);

View File

@@ -18,6 +18,7 @@ package com.android.internal.app;
import static org.mockito.Mockito.mock;
import android.annotation.Nullable;
import android.app.usage.UsageStatsManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -30,6 +31,9 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Size;
import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.app.chooser.TargetInfo;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -64,7 +68,7 @@ public class ChooserWrapperActivity extends ChooserActivity {
}
@Override
public void safelyStartActivity(TargetInfo cti) {
public void safelyStartActivity(com.android.internal.app.chooser.TargetInfo cti) {
if (sOverrides.onSafelyStartCallback != null &&
sOverrides.onSafelyStartCallback.apply(cti)) {
return;
@@ -133,8 +137,10 @@ public class ChooserWrapperActivity extends ChooserActivity {
}
public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
CharSequence pLabel, CharSequence pInfo, Intent pOrigIntent) {
return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, pOrigIntent);
CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
@Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, replacementIntent,
resolveInfoPresentationGetter);
}
/**

View File

@@ -43,10 +43,10 @@ import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R;
import com.android.internal.app.ResolverActivity.ActivityInfoPresentationGetter;
import com.android.internal.app.ResolverActivity.ResolveInfoPresentationGetter;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.ResolverDataProvider.PackageManagerMockedInfo;
import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
import com.android.internal.widget.ResolverDrawerLayout;
import org.junit.Before;
@@ -83,7 +83,7 @@ public class ResolverActivityTest {
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
Espresso.registerIdlingResources(activity.getLabelIdlingResource());
Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
assertThat(activity.getAdapter().getCount(), is(2));
@@ -216,7 +216,7 @@ public class ResolverActivityTest {
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
Espresso.registerIdlingResources(activity.getLabelIdlingResource());
Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
// The other entry is filtered to the last used slot
@@ -254,7 +254,7 @@ public class ResolverActivityTest {
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
Espresso.registerIdlingResources(activity.getLabelIdlingResource());
Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
// The other entry is filtered to the other profile slot
@@ -300,7 +300,7 @@ public class ResolverActivityTest {
.thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
Espresso.registerIdlingResources(activity.getLabelIdlingResource());
Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
// The other entry is filtered to the other profile slot

View File

@@ -19,10 +19,14 @@ package com.android.internal.app;
import static org.mockito.Mockito.mock;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import androidx.test.espresso.idling.CountingIdlingResource;
import com.android.internal.app.chooser.TargetInfo;
import java.util.List;
import java.util.function.Function;
/*
@@ -31,15 +35,17 @@ import java.util.function.Function;
public class ResolverWrapperActivity extends ResolverActivity {
static final OverrideData sOverrides = new OverrideData();
private UsageStatsManager mUsm;
private CountingIdlingResource mLabelIdlingResource =
new CountingIdlingResource("LoadLabelTask");
public CountingIdlingResource getLabelIdlingResource() {
return mLabelIdlingResource;
@Override
public ResolverListAdapter createAdapter(Context context, List<Intent> payloadIntents,
Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed,
boolean useLayoutForBrowsables) {
return new ResolverWrapperAdapter(context, payloadIntents, initialIntents, rList,
filterLastUsed, createListController(), useLayoutForBrowsables, this);
}
ResolveListAdapter getAdapter() {
return mAdapter;
ResolverWrapperAdapter getAdapter() {
return (ResolverWrapperAdapter) mAdapter;
}
@Override
@@ -72,11 +78,6 @@ public class ResolverWrapperActivity extends ResolverActivity {
return super.getPackageManager();
}
@Override
protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
return new LoadLabelWrapperTask(info, holder);
}
/**
* We cannot directly mock the activity created since instrumentation creates it.
* <p>
@@ -96,22 +97,4 @@ public class ResolverWrapperActivity extends ResolverActivity {
resolverListController = mock(ResolverListController.class);
}
}
class LoadLabelWrapperTask extends LoadLabelTask {
protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
super(dri, holder);
}
@Override
protected void onPreExecute() {
mLabelIdlingResource.increment();
}
@Override
protected void onPostExecute(CharSequence[] result) {
super.onPostExecute(result);
mLabelIdlingResource.decrement();
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.app;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import androidx.test.espresso.idling.CountingIdlingResource;
import com.android.internal.app.chooser.DisplayResolveInfo;
import java.util.List;
public class ResolverWrapperAdapter extends ResolverListAdapter {
private CountingIdlingResource mLabelIdlingResource =
new CountingIdlingResource("LoadLabelTask");
public ResolverWrapperAdapter(Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
List<ResolveInfo> rList, boolean filterLastUsed,
ResolverListController resolverListController, boolean useLayoutForBrowsables,
ResolverListCommunicator resolverListCommunicator) {
super(context, payloadIntents, initialIntents, rList, filterLastUsed,
resolverListController,
useLayoutForBrowsables, resolverListCommunicator);
}
public CountingIdlingResource getLabelIdlingResource() {
return mLabelIdlingResource;
}
@Override
protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
return new LoadLabelWrapperTask(info, holder);
}
class LoadLabelWrapperTask extends LoadLabelTask {
protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
super(dri, holder);
}
@Override
protected void onPreExecute() {
mLabelIdlingResource.increment();
}
@Override
protected void onPostExecute(CharSequence[] result) {
super.onPostExecute(result);
mLabelIdlingResource.decrement();
}
}
}

View File

@@ -35,6 +35,7 @@ import android.util.Log;
import android.widget.CheckBox;
import com.android.internal.app.ResolverActivity;
import com.android.internal.app.chooser.TargetInfo;
import com.android.systemui.R;
import java.util.ArrayList;