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 static com.android.intentresolver.Flags.rebuildAdaptersOnTargetPinning; 20 21 import android.app.prediction.AppTarget; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.content.pm.ShortcutInfo; 27 import android.service.chooser.ChooserTarget; 28 import android.util.Log; 29 30 import androidx.annotation.Nullable; 31 32 import com.android.intentresolver.chooser.DisplayResolveInfo; 33 import com.android.intentresolver.chooser.SelectableTargetInfo; 34 import com.android.intentresolver.chooser.TargetInfo; 35 import com.android.intentresolver.ui.AppShortcutLimit; 36 import com.android.intentresolver.ui.EnforceShortcutLimit; 37 38 import java.util.Collections; 39 import java.util.Comparator; 40 import java.util.List; 41 import java.util.Map; 42 43 import javax.inject.Inject; 44 import javax.inject.Singleton; 45 46 @Singleton 47 public class ShortcutSelectionLogic { 48 private static final String TAG = "ShortcutSelectionLogic"; 49 private static final boolean DEBUG = false; 50 private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f; 51 private static final int MAX_CHOOSER_TARGETS_PER_APP = 2; 52 53 private final int mMaxShortcutTargetsPerApp; 54 private final boolean mApplySharingAppLimits; 55 56 // Descending order 57 private final Comparator<ChooserTarget> mBaseTargetComparator = 58 (lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore()); 59 60 @Inject ShortcutSelectionLogic( @ppShortcutLimit int maxShortcutTargetsPerApp, @EnforceShortcutLimit boolean applySharingAppLimits)61 public ShortcutSelectionLogic( 62 @AppShortcutLimit int maxShortcutTargetsPerApp, 63 @EnforceShortcutLimit boolean applySharingAppLimits) { 64 mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp; 65 mApplySharingAppLimits = applySharingAppLimits; 66 } 67 68 /** 69 * Evaluate targets for inclusion in the direct share area. May not be included 70 * if score is too low. 71 */ 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)72 public boolean addServiceResults( 73 @Nullable DisplayResolveInfo origTarget, 74 float origTargetScore, 75 List<ChooserTarget> targets, 76 boolean isShortcutResult, 77 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, 78 Map<ChooserTarget, AppTarget> directShareToAppTargets, 79 Context userContext, 80 Intent targetIntent, 81 Intent referrerFillInIntent, 82 int maxRankedTargets, 83 List<TargetInfo> serviceTargets) { 84 if (DEBUG) { 85 Log.d(TAG, "addServiceResults " 86 + (origTarget == null ? null : origTarget.getResolvedComponentName()) + ", " 87 + targets.size() 88 + " targets"); 89 } 90 if (targets.isEmpty()) { 91 return false; 92 } 93 Collections.sort(targets, mBaseTargetComparator); 94 final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp 95 : MAX_CHOOSER_TARGETS_PER_APP; 96 final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets) 97 : targets.size(); 98 float lastScore = 0; 99 boolean shouldNotify = false; 100 for (int i = 0, count = targetsLimit; i < count; i++) { 101 final ChooserTarget target = targets.get(i); 102 float targetScore = target.getScore(); 103 if (mApplySharingAppLimits) { 104 targetScore *= origTargetScore; 105 if (i > 0 && targetScore >= lastScore) { 106 // Apply a decay so that the top app can't crowd out everything else. 107 // This incents ChooserTargetServices to define what's truly better. 108 targetScore = lastScore * 0.95f; 109 } 110 } 111 ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target) 112 : null; 113 if ((shortcutInfo != null) && shortcutInfo.isPinned()) { 114 targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST; 115 } 116 ResolveInfo backupResolveInfo; 117 Intent resolvedIntent; 118 if (origTarget == null) { 119 resolvedIntent = createResolvedIntentForCallerTarget(target, targetIntent); 120 backupResolveInfo = userContext.getPackageManager() 121 .resolveActivity( 122 resolvedIntent, 123 PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA)); 124 } else { 125 resolvedIntent = origTarget.getResolvedIntent(); 126 backupResolveInfo = null; 127 } 128 boolean isInserted = insertServiceTarget( 129 SelectableTargetInfo.newSelectableTargetInfo( 130 origTarget, 131 backupResolveInfo, 132 resolvedIntent, 133 target, 134 targetScore, 135 shortcutInfo, 136 directShareToAppTargets.get(target), 137 referrerFillInIntent), 138 maxRankedTargets, 139 serviceTargets); 140 141 shouldNotify |= isInserted; 142 143 if (DEBUG) { 144 Log.d(TAG, " => " + target + " score=" + targetScore 145 + " base=" + target.getScore() 146 + " lastScore=" + lastScore 147 + " baseScore=" + origTargetScore 148 + " applyAppLimit=" + mApplySharingAppLimits); 149 } 150 151 lastScore = targetScore; 152 } 153 154 return shouldNotify; 155 } 156 157 /** 158 * Creates a resolved intent for a caller-specified target. 159 * @param target, a caller-specified target. 160 * @param targetIntent, a target intent for the Chooser (see {@link Intent#EXTRA_INTENT}). 161 */ createResolvedIntentForCallerTarget( ChooserTarget target, Intent targetIntent)162 private static Intent createResolvedIntentForCallerTarget( 163 ChooserTarget target, Intent targetIntent) { 164 final Intent resolvedIntent = new Intent(targetIntent); 165 resolvedIntent.setComponent(target.getComponentName()); 166 resolvedIntent.putExtras(target.getIntentExtras()); 167 return resolvedIntent; 168 } 169 insertServiceTarget( TargetInfo chooserTargetInfo, int maxRankedTargets, List<TargetInfo> serviceTargets)170 private boolean insertServiceTarget( 171 TargetInfo chooserTargetInfo, 172 int maxRankedTargets, 173 List<TargetInfo> serviceTargets) { 174 175 // Check for duplicates and abort if found 176 for (int i = 0; i < serviceTargets.size(); i++) { 177 TargetInfo otherTargetInfo = serviceTargets.get(i); 178 if (chooserTargetInfo.isSimilar(otherTargetInfo)) { 179 if (rebuildAdaptersOnTargetPinning() 180 && chooserTargetInfo.isPinned() != otherTargetInfo.isPinned()) { 181 serviceTargets.set(i, chooserTargetInfo); 182 return true; 183 } 184 return false; 185 } 186 } 187 188 int currentSize = serviceTargets.size(); 189 final float newScore = chooserTargetInfo.getModifiedScore(); 190 for (int i = 0; i < Math.min(currentSize, maxRankedTargets); i++) { 191 final TargetInfo serviceTarget = serviceTargets.get(i); 192 if (serviceTarget == null) { 193 serviceTargets.set(i, chooserTargetInfo); 194 return true; 195 } else if (newScore > serviceTarget.getModifiedScore()) { 196 serviceTargets.add(i, chooserTargetInfo); 197 return true; 198 } 199 } 200 201 if (currentSize < maxRankedTargets) { 202 serviceTargets.add(chooserTargetInfo); 203 return true; 204 } 205 206 return false; 207 } 208 } 209