Separate adapter logic from ChooserActivity and ResolverActivity
Test: atest com.android.internal.app.ChooserActivityTest Test: atest com.android.internal.app.ResolverActivityTest Bug: 142537267 Bug: 142538125 Change-Id: Ia4a61e3dc7cbac5c31e0d5b126e033a043c81d90
This commit is contained in:
File diff suppressed because it is too large
Load Diff
38
core/java/com/android/internal/app/ChooserFlags.java
Normal file
38
core/java/com/android/internal/app/ChooserFlags.java
Normal 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;
|
||||
}
|
||||
539
core/java/com/android/internal/app/ChooserListAdapter.java
Normal file
539
core/java/com/android/internal/app/ChooserListAdapter.java
Normal 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
862
core/java/com/android/internal/app/ResolverListAdapter.java
Normal file
862
core/java/com/android/internal/app/ResolverListAdapter.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
128
core/java/com/android/internal/app/chooser/TargetInfo.java
Normal file
128
core/java/com/android/internal/app/chooser/TargetInfo.java
Normal 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();
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user