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