/* * 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 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 targets, boolean isShortcutResult, Map directShareToShortcutInfos, Map directShareToAppTargets, Context userContext, Intent targetIntent, Intent referrerFillInIntent, int maxRankedTargets, List 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 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; } }