1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver; 18 19 import android.annotation.Nullable; 20 import android.app.prediction.AppTarget; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.content.pm.ResolveInfo; 25 import android.content.pm.ShortcutInfo; 26 import android.service.chooser.ChooserTarget; 27 import android.util.Log; 28 29 import com.android.intentresolver.chooser.DisplayResolveInfo; 30 import com.android.intentresolver.chooser.SelectableTargetInfo; 31 import com.android.intentresolver.chooser.TargetInfo; 32 33 import java.util.Collections; 34 import java.util.Comparator; 35 import java.util.List; 36 import java.util.Map; 37 38 class ShortcutSelectionLogic { 39 private static final String TAG = "ShortcutSelectionLogic"; 40 private static final boolean DEBUG = false; 41 private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f; 42 private static final int MAX_CHOOSER_TARGETS_PER_APP = 2; 43 44 private final int mMaxShortcutTargetsPerApp; 45 private final boolean mApplySharingAppLimits; 46 47 // Descending order 48 private final Comparator<ChooserTarget> mBaseTargetComparator = 49 (lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore()); 50 ShortcutSelectionLogic( int maxShortcutTargetsPerApp, boolean applySharingAppLimits)51 ShortcutSelectionLogic( 52 int maxShortcutTargetsPerApp, 53 boolean applySharingAppLimits) { 54 mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp; 55 mApplySharingAppLimits = applySharingAppLimits; 56 } 57 58 /** 59 * Evaluate targets for inclusion in the direct share area. May not be included 60 * if score is too low. 61 */ addServiceResults( @ullable 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)62 public boolean addServiceResults( 63 @Nullable DisplayResolveInfo origTarget, 64 float origTargetScore, 65 List<ChooserTarget> targets, 66 boolean isShortcutResult, 67 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, 68 Map<ChooserTarget, AppTarget> directShareToAppTargets, 69 Context userContext, 70 Intent targetIntent, 71 Intent referrerFillInIntent, 72 int maxRankedTargets, 73 List<TargetInfo> serviceTargets) { 74 if (DEBUG) { 75 Log.d(TAG, "addServiceResults " 76 + (origTarget == null ? null : origTarget.getResolvedComponentName()) + ", " 77 + targets.size() 78 + " targets"); 79 } 80 if (targets.size() == 0) { 81 return false; 82 } 83 Collections.sort(targets, mBaseTargetComparator); 84 final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp 85 : MAX_CHOOSER_TARGETS_PER_APP; 86 final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets) 87 : targets.size(); 88 float lastScore = 0; 89 boolean shouldNotify = false; 90 for (int i = 0, count = targetsLimit; i < count; i++) { 91 final ChooserTarget target = targets.get(i); 92 float targetScore = target.getScore(); 93 if (mApplySharingAppLimits) { 94 targetScore *= origTargetScore; 95 if (i > 0 && targetScore >= lastScore) { 96 // Apply a decay so that the top app can't crowd out everything else. 97 // This incents ChooserTargetServices to define what's truly better. 98 targetScore = lastScore * 0.95f; 99 } 100 } 101 ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target) 102 : null; 103 if ((shortcutInfo != null) && shortcutInfo.isPinned()) { 104 targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST; 105 } 106 ResolveInfo backupResolveInfo; 107 Intent resolvedIntent; 108 if (origTarget == null) { 109 resolvedIntent = createResolvedIntentForCallerTarget(target, targetIntent); 110 backupResolveInfo = userContext.getPackageManager() 111 .resolveActivity( 112 resolvedIntent, 113 PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA)); 114 } else { 115 resolvedIntent = origTarget.getResolvedIntent(); 116 backupResolveInfo = null; 117 } 118 boolean isInserted = insertServiceTarget( 119 SelectableTargetInfo.newSelectableTargetInfo( 120 origTarget, 121 backupResolveInfo, 122 resolvedIntent, 123 target, 124 targetScore, 125 shortcutInfo, 126 directShareToAppTargets.get(target), 127 referrerFillInIntent), 128 maxRankedTargets, 129 serviceTargets); 130 131 shouldNotify |= isInserted; 132 133 if (DEBUG) { 134 Log.d(TAG, " => " + target + " score=" + targetScore 135 + " base=" + target.getScore() 136 + " lastScore=" + lastScore 137 + " baseScore=" + origTargetScore 138 + " applyAppLimit=" + mApplySharingAppLimits); 139 } 140 141 lastScore = targetScore; 142 } 143 144 return shouldNotify; 145 } 146 147 /** 148 * Creates a resolved intent for a caller-specified target. 149 * @param target, a caller-specified target. 150 * @param targetIntent, a target intent for the Chooser (see {@link Intent#EXTRA_INTENT}). 151 */ createResolvedIntentForCallerTarget( ChooserTarget target, Intent targetIntent)152 private static Intent createResolvedIntentForCallerTarget( 153 ChooserTarget target, Intent targetIntent) { 154 final Intent resolvedIntent = new Intent(targetIntent); 155 resolvedIntent.setComponent(target.getComponentName()); 156 resolvedIntent.putExtras(target.getIntentExtras()); 157 return resolvedIntent; 158 } 159 insertServiceTarget( TargetInfo chooserTargetInfo, int maxRankedTargets, List<TargetInfo> serviceTargets)160 private boolean insertServiceTarget( 161 TargetInfo chooserTargetInfo, 162 int maxRankedTargets, 163 List<TargetInfo> serviceTargets) { 164 165 // Check for duplicates and abort if found 166 for (TargetInfo otherTargetInfo : serviceTargets) { 167 if (chooserTargetInfo.isSimilar(otherTargetInfo)) { 168 return false; 169 } 170 } 171 172 int currentSize = serviceTargets.size(); 173 final float newScore = chooserTargetInfo.getModifiedScore(); 174 for (int i = 0; i < Math.min(currentSize, maxRankedTargets); 175 i++) { 176 final TargetInfo serviceTarget = serviceTargets.get(i); 177 if (serviceTarget == null) { 178 serviceTargets.set(i, chooserTargetInfo); 179 return true; 180 } else if (newScore > serviceTarget.getModifiedScore()) { 181 serviceTargets.add(i, chooserTargetInfo); 182 return true; 183 } 184 } 185 186 if (currentSize < maxRankedTargets) { 187 serviceTargets.add(chooserTargetInfo); 188 return true; 189 } 190 191 return false; 192 } 193 } 194