diff --git a/core/java/com/android/internal/app/AbstractResolverComparator.java b/core/java/com/android/internal/app/AbstractResolverComparator.java index e0bbc04515e00..28c9464fbf664 100644 --- a/core/java/com/android/internal/app/AbstractResolverComparator.java +++ b/core/java/com/android/internal/app/AbstractResolverComparator.java @@ -43,7 +43,7 @@ import java.util.List; public abstract class AbstractResolverComparator implements Comparator { private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private static final String TAG = "AbstractResolverComp"; protected AfterCompute mAfterCompute; diff --git a/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java b/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java index 986614c0963cf..37a5a63265e8d 100644 --- a/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java +++ b/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java @@ -28,9 +28,11 @@ import android.content.Intent; import android.content.pm.ResolveInfo; import android.os.Message; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.util.Log; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import java.util.ArrayList; import java.util.HashMap; @@ -48,7 +50,6 @@ import java.util.stream.Collectors; class AppPredictionServiceResolverComparator extends AbstractResolverComparator { private static final String TAG = "APSResolverComparator"; - private static final boolean DEBUG = false; private final AppPredictor mAppPredictor; private final Context mContext; @@ -61,6 +62,11 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator // back to using the ResolverRankerService. private ResolverRankerServiceResolverComparator mResolverRankerService; + private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED, + true); + AppPredictionServiceResolverComparator( Context context, Intent intent, @@ -113,9 +119,7 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(), sortedAppTargets -> { if (sortedAppTargets.isEmpty()) { - if (DEBUG) { - Log.d(TAG, "AppPredictionService disabled. Using resolver."); - } + Log.i(TAG, "AppPredictionService disabled. Using resolver."); // APS for chooser is disabled. Fallback to resolver. mResolverRankerService = new ResolverRankerServiceResolverComparator( @@ -123,9 +127,7 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT)); mResolverRankerService.compute(targets); } else { - if (DEBUG) { - Log.d(TAG, "AppPredictionService response received"); - } + Log.i(TAG, "AppPredictionService response received"); Message msg = Message.obtain(mHandler, RANKER_SERVICE_RESULT, sortedAppTargets); msg.sendToTarget(); @@ -145,8 +147,11 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator target.getRank())); } for (int i = 0; i < sortedAppTargets.size(); i++) { - mTargetRanks.put(new ComponentName(sortedAppTargets.get(i).getPackageName(), - sortedAppTargets.get(i).getClassName()), i); + ComponentName componentName = new ComponentName( + sortedAppTargets.get(i).getPackageName(), + sortedAppTargets.get(i).getClassName()); + mTargetRanks.put(componentName, i); + Log.i(TAG, "handleResultMessage, sortedAppTargets #" + i + ": " + componentName); } } else if (msg.obj == null && mResolverRankerService == null) { Log.e(TAG, "Unexpected null result"); @@ -167,7 +172,7 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator if (mResolverRankerService != null) { return mResolverRankerService.getScore(name); } - if (!mTargetScores.isEmpty()) { + if (mAppendDirectShareEnabled && !mTargetScores.isEmpty()) { return mTargetScores.get(name); } Integer rank = mTargetRanks.get(name); diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index a144ffb57a15e..be06913c78e56 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -33,6 +33,7 @@ import android.app.prediction.AppPredictionManager; import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppTargetId; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; import android.content.ClipboardManager; @@ -191,6 +192,8 @@ public class ChooserActivity extends ResolverActivity implements // TODO(b/123088566) Share these in a better way. private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share"; public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; + public static final String CHOOSER_TARGET = "chooser_target"; + private static final String SHORTCUT_TARGET = "shortcut_target"; private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20; public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter"; @@ -247,6 +250,10 @@ public class ChooserActivity extends ResolverActivity implements DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED, true); + private boolean mChooserTargetRankingEnabled = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.CHOOSER_TARGET_RANKING_ENABLED, + false); private Bundle mReplacementExtras; private IntentSender mChosenComponentSender; @@ -430,6 +437,7 @@ public class ChooserActivity extends ResolverActivity implements private static final int SHORTCUT_MANAGER_SHARE_TARGET_RESULT = 4; private static final int SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED = 5; private static final int LIST_VIEW_UPDATE_MESSAGE = 6; + private static final int CHOOSER_TARGET_RANKING_SCORE = 7; private static final int WATCHDOG_TIMEOUT_MAX_MILLIS = 10000; private static final int WATCHDOG_TIMEOUT_MIN_MILLIS = 3000; @@ -448,6 +456,7 @@ public class ChooserActivity extends ResolverActivity implements removeMessages(CHOOSER_TARGET_SERVICE_RESULT); removeMessages(SHORTCUT_MANAGER_SHARE_TARGET_RESULT); removeMessages(SHORTCUT_MANAGER_SHARE_TARGET_RESULT_COMPLETED); + removeMessages(CHOOSER_TARGET_RANKING_SCORE); } private void restartServiceRequestTimer() { @@ -559,6 +568,17 @@ public class ChooserActivity extends ResolverActivity implements getChooserActivityLogger().logSharesheetDirectLoadComplete(); break; + case CHOOSER_TARGET_RANKING_SCORE: + if (DEBUG) Log.d(TAG, "CHOOSER_TARGET_RANKING_SCORE"); + final ChooserTargetRankingInfo scoreInfo = (ChooserTargetRankingInfo) msg.obj; + ChooserListAdapter adapterForUserHandle = + mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle( + scoreInfo.userHandle); + if (adapterForUserHandle != null) { + adapterForUserHandle.addChooserTargetRankingScore(scoreInfo.scores); + } + break; + default: super.handleMessage(msg); } @@ -787,10 +807,26 @@ public class ChooserActivity extends ResolverActivity implements getDisplayResolveInfos(chooserListAdapter); final List shareShortcutInfos = new ArrayList<>(); + + // Separate ChooserTargets ranking scores and ranked Shortcuts. + List shortcutResults = new ArrayList<>(); + List chooserTargetScores = new ArrayList<>(); for (AppTarget appTarget : resultList) { if (appTarget.getShortcutInfo() == null) { continue; } + if (appTarget.getShortcutInfo().getId().equals(CHOOSER_TARGET)) { + chooserTargetScores.add(appTarget); + } else { + shortcutResults.add(appTarget); + } + } + resultList = shortcutResults; + if (mChooserTargetRankingEnabled) { + sendChooserTargetRankingScore(chooserTargetScores, + chooserListAdapter.getUserHandle()); + } + for (AppTarget appTarget : resultList) { shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo( appTarget.getShortcutInfo(), new ComponentName( @@ -1973,6 +2009,14 @@ public class ChooserActivity extends ResolverActivity implements }); } + private void sendChooserTargetRankingScore(List chooserTargetScores, + UserHandle userHandle) { + final Message msg = Message.obtain(); + msg.what = ChooserHandler.CHOOSER_TARGET_RANKING_SCORE; + msg.obj = new ChooserTargetRankingInfo(chooserTargetScores, userHandle); + mChooserHandler.sendMessage(msg); + } + private void sendShareShortcutInfoList( List resultList, List driList, @@ -2170,6 +2214,7 @@ public class ChooserActivity extends ResolverActivity implements ChooserListAdapter currentListAdapter = mChooserMultiProfilePagerAdapter.getActiveListAdapter(); if (currentListAdapter != null) { + sendImpressionToAppPredictor(info, currentListAdapter); currentListAdapter.updateModel(info.getResolvedComponentName()); currentListAdapter.updateChooserCounts(ri.activityInfo.packageName, targetIntent.getAction()); @@ -2185,6 +2230,37 @@ public class ChooserActivity extends ResolverActivity implements mIsSuccessfullySelected = true; } + private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) { + if (!mChooserTargetRankingEnabled) { + return; + } + AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled( + mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); + if (directShareAppPredictor == null) { + return; + } + // Send DS target impression info to AppPredictor, only when user chooses app share. + if (targetInfo instanceof ChooserTargetInfo) { + return; + } + List surfacedTargetInfo = adapter.getSurfacedTargetInfo(); + List targetIds = new ArrayList<>(); + for (ChooserTargetInfo chooserTargetInfo : surfacedTargetInfo) { + ChooserTarget chooserTarget = chooserTargetInfo.getChooserTarget(); + String componentName = chooserTarget.getComponentName().flattenToString(); + if (mDirectShareShortcutInfoCache.containsKey(chooserTarget)) { + String shortcutId = mDirectShareShortcutInfoCache.get(chooserTarget).getId(); + targetIds.add(new AppTargetId( + String.format("%s/%s/%s", shortcutId, componentName, SHORTCUT_TARGET))); + } else { + String titleHash = ChooserUtil.md5(chooserTarget.getTitle().toString()); + targetIds.add(new AppTargetId( + String.format("%s/%s/%s", titleHash, componentName, CHOOSER_TARGET))); + } + } + directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds); + } + private void sendClickToAppPredictor(TargetInfo targetInfo) { AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled( mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); @@ -2199,6 +2275,28 @@ public class ChooserActivity extends ResolverActivity implements if (mDirectShareAppTargetCache != null) { appTarget = mDirectShareAppTargetCache.get(chooserTarget); } + if (mChooserTargetRankingEnabled && appTarget == null) { + // Send ChooserTarget sharing info to AppPredictor. + ComponentName componentName = chooserTarget.getComponentName(); + try { + appTarget = new AppTarget.Builder( + new AppTargetId(componentName.flattenToString()), + new ShortcutInfo.Builder( + createPackageContextAsUser( + componentName.getPackageName(), + 0 /* flags */, + getUser()), + CHOOSER_TARGET) + .setActivity(componentName) + .setShortLabel(ChooserUtil.md5(chooserTarget.getTitle().toString())) + .build()) + .setClassName(componentName.getClassName()) + .build(); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not look up service " + componentName + + "; component name not found"); + } + } // This is a direct share click that was provided by the APS if (appTarget != null) { directShareAppPredictor.notifyAppTargetEvent( @@ -3772,6 +3870,17 @@ public class ChooserActivity extends ResolverActivity implements } } + static class ChooserTargetRankingInfo { + public final List scores; + public final UserHandle userHandle; + + ChooserTargetRankingInfo(List chooserTargetScores, + UserHandle userHandle) { + this.scores = chooserTargetScores; + this.userHandle = userHandle; + } + } + static class RefinementResultReceiver extends ResultReceiver { private ChooserActivity mChooserActivity; private TargetInfo mSelectedTarget; diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index 73ee2950c481f..f426bc0ecbb53 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -21,6 +21,7 @@ import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FRO import android.app.ActivityManager; import android.app.prediction.AppPredictor; +import android.app.prediction.AppTarget; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -98,6 +99,7 @@ public class ChooserListAdapter extends ResolverListAdapter { private int mValidServiceTargetsNum = 0; private final Map, Integer>> mParkingDirectShareTargets = new HashMap<>(); + private final Map> mChooserTargetScores = new HashMap<>(); private Set mPendingChooserTargetService = new HashSet<>(); private Set mShortcutComponents = new HashSet<>(); private final List mServiceTargets = new ArrayList<>(); @@ -409,6 +411,15 @@ public class ChooserListAdapter extends ResolverListAdapter { return null; } + /** + * Fetch surfaced direct share target info + */ + public List getSurfacedTargetInfo() { + int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets(); + return mServiceTargets.subList(0, + Math.min(maxSurfacedTargets, getSelectableServiceTargetCount())); + } + /** * Evaluate targets for inclusion in the direct share area. May not be included @@ -479,6 +490,50 @@ public class ChooserListAdapter extends ResolverListAdapter { } } + /** + * Store ChooserTarget ranking scores info wrapped in {@code targets}. + */ + public void addChooserTargetRankingScore(List targets) { + Log.i(TAG, "addChooserTargetRankingScore " + targets.size() + " targets score."); + for (AppTarget target : targets) { + if (target.getShortcutInfo() == null) { + continue; + } + ShortcutInfo shortcutInfo = target.getShortcutInfo(); + if (!shortcutInfo.getId().equals(ChooserActivity.CHOOSER_TARGET) + || shortcutInfo.getActivity() == null) { + continue; + } + ComponentName componentName = shortcutInfo.getActivity(); + if (!mChooserTargetScores.containsKey(componentName)) { + mChooserTargetScores.put(componentName, new HashMap<>()); + } + mChooserTargetScores.get(componentName).put(shortcutInfo.getShortLabel().toString(), + shortcutInfo.getRank()); + } + mChooserTargetScores.keySet().forEach(key -> rankTargetsWithinComponent(key)); + } + + /** + * Rank chooserTargets of the given {@code componentName} in mParkingDirectShareTargets as per + * available scores stored in mChooserTargetScores. + */ + private void rankTargetsWithinComponent(ComponentName componentName) { + if (!mParkingDirectShareTargets.containsKey(componentName) + || !mChooserTargetScores.containsKey(componentName)) { + return; + } + Map scores = mChooserTargetScores.get(componentName); + Collections.sort(mParkingDirectShareTargets.get(componentName).first, (o1, o2) -> { + // The score has been normalized between 0 and 2, the default is 1. + int score1 = scores.getOrDefault( + ChooserUtil.md5(o1.getChooserTarget().getTitle().toString()), 1); + int score2 = scores.getOrDefault( + ChooserUtil.md5(o2.getChooserTarget().getTitle().toString()), 1); + return score2 - score1; + }); + } + /** * Park {@code targets} into memory for the moment to surface them later when view is refreshed. * Components pending on ChooserTargetService query are also recorded. @@ -517,6 +572,7 @@ public class ChooserListAdapter extends ResolverListAdapter { new Pair<>(new ArrayList<>(), 0)); parkingTargetInfoPair.first.addAll(parkingTargetInfos); mParkingDirectShareTargets.put(origComponentName, parkingTargetInfoPair); + rankTargetsWithinComponent(origComponentName); if (isShortcutResult) { mShortcutComponents.add(origComponentName); } diff --git a/core/java/com/android/internal/app/ChooserUtil.java b/core/java/com/android/internal/app/ChooserUtil.java new file mode 100644 index 0000000000000..3f8788cba9b9e --- /dev/null +++ b/core/java/com/android/internal/app/ChooserUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 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.internal.app; + +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Utility method for common computation operations for Share sheet. + */ +public class ChooserUtil { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + /** + * Hashes the given input based on MD5 algorithm. + * + * @return a string representation of the hash computation. + */ + public static String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(input.getBytes(UTF_8)); + return convertBytesToHexString(md.digest()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + /** Converts byte array input into an hex string. */ + private static String convertBytesToHexString(byte[] input) { + char[] chars = new char[input.length * 2]; + for (int i = 0; i < input.length; i++) { + byte b = input[i]; + chars[i * 2] = Character.forDigit((b >> 4) & 0xF, 16 /* radix */); + chars[i * 2 + 1] = Character.forDigit(b & 0xF, 16 /* radix */); + } + return new String(chars); + } + + private ChooserUtil() {} +} diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index 2b0fd663643de..6f33096a14fda 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -388,6 +388,11 @@ public final class SystemUiDeviceConfigFlags { */ public static final String APPEND_DIRECT_SHARE_ENABLED = "append_direct_share_enabled"; + /** + * (boolean) Whether ChooserTargets ranking on Sharesheet is enabled. + */ + public static final String CHOOSER_TARGET_RANKING_ENABLED = "chooser_target_ranking_enabled"; + /** * (boolean) Whether to enable user-drag resizing for PIP. */