blob: 645b939111879eece02309d3bc6a8d6f270e17df [file] [log] [blame]
/*
* Copyright (C) 2022 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.intentresolver;
import android.annotation.Nullable;
import android.app.prediction.AppTarget;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.service.chooser.ChooserTarget;
import android.util.Log;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
class ShortcutSelectionLogic {
private static final String TAG = "ShortcutSelectionLogic";
private static final boolean DEBUG = false;
private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
private final int mMaxShortcutTargetsPerApp;
private final boolean mApplySharingAppLimits;
// Descending order
private final Comparator<ChooserTarget> mBaseTargetComparator =
(lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore());
ShortcutSelectionLogic(
int maxShortcutTargetsPerApp,
boolean applySharingAppLimits) {
mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp;
mApplySharingAppLimits = applySharingAppLimits;
}
/**
* Evaluate targets for inclusion in the direct share area. May not be included
* if score is too low.
*/
public boolean addServiceResults(
@Nullable DisplayResolveInfo origTarget,
float origTargetScore,
List<ChooserTarget> targets,
boolean isShortcutResult,
Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
Map<ChooserTarget, AppTarget> directShareToAppTargets,
Context userContext,
Intent targetIntent,
Intent referrerFillInIntent,
int maxRankedTargets,
List<TargetInfo> serviceTargets) {
if (DEBUG) {
Log.d(TAG, "addServiceResults "
+ (origTarget == null ? null : origTarget.getResolvedComponentName()) + ", "
+ targets.size()
+ " targets");
}
if (targets.size() == 0) {
return false;
}
Collections.sort(targets, mBaseTargetComparator);
final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
: MAX_CHOOSER_TARGETS_PER_APP;
final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
: targets.size();
float lastScore = 0;
boolean shouldNotify = false;
for (int i = 0, count = targetsLimit; i < count; i++) {
final ChooserTarget target = targets.get(i);
float targetScore = target.getScore();
if (mApplySharingAppLimits) {
targetScore *= origTargetScore;
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;
}
}
ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
: null;
if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
}
ResolveInfo backupResolveInfo;
Intent resolvedIntent;
if (origTarget == null) {
resolvedIntent = createResolvedIntentForCallerTarget(target, targetIntent);
backupResolveInfo = userContext.getPackageManager()
.resolveActivity(
resolvedIntent,
PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA));
} else {
resolvedIntent = origTarget.getResolvedIntent();
backupResolveInfo = null;
}
boolean isInserted = insertServiceTarget(
SelectableTargetInfo.newSelectableTargetInfo(
origTarget,
backupResolveInfo,
resolvedIntent,
target,
targetScore,
shortcutInfo,
directShareToAppTargets.get(target),
referrerFillInIntent),
maxRankedTargets,
serviceTargets);
shouldNotify |= isInserted;
if (DEBUG) {
Log.d(TAG, " => " + target + " score=" + targetScore
+ " base=" + target.getScore()
+ " lastScore=" + lastScore
+ " baseScore=" + origTargetScore
+ " applyAppLimit=" + mApplySharingAppLimits);
}
lastScore = targetScore;
}
return shouldNotify;
}
/**
* Creates a resolved intent for a caller-specified target.
* @param target, a caller-specified target.
* @param targetIntent, a target intent for the Chooser (see {@link Intent#EXTRA_INTENT}).
*/
private static Intent createResolvedIntentForCallerTarget(
ChooserTarget target, Intent targetIntent) {
final Intent resolvedIntent = new Intent(targetIntent);
resolvedIntent.setComponent(target.getComponentName());
resolvedIntent.putExtras(target.getIntentExtras());
return resolvedIntent;
}
private boolean insertServiceTarget(
TargetInfo chooserTargetInfo,
int maxRankedTargets,
List<TargetInfo> serviceTargets) {
// Check for duplicates and abort if found
for (TargetInfo otherTargetInfo : serviceTargets) {
if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
return false;
}
}
int currentSize = serviceTargets.size();
final float newScore = chooserTargetInfo.getModifiedScore();
for (int i = 0; i < Math.min(currentSize, maxRankedTargets);
i++) {
final TargetInfo serviceTarget = serviceTargets.get(i);
if (serviceTarget == null) {
serviceTargets.set(i, chooserTargetInfo);
return true;
} else if (newScore > serviceTarget.getModifiedScore()) {
serviceTargets.add(i, chooserTargetInfo);
return true;
}
}
if (currentSize < maxRankedTargets) {
serviceTargets.add(chooserTargetInfo);
return true;
}
return false;
}
}