From 5aab2d3fbe3b484fe6b80d98c07179665a2151c6 Mon Sep 17 00:00:00 2001 From: Song Hu Date: Thu, 30 Apr 2020 10:07:15 -0700 Subject: [PATCH] Pass ChooserTarget sharing and impression info to AppPredictor. Rank ChooserTargets of the same component as per available scores returned by AppPredictor. These two features are guarded by flag "mChooserTargetRankingEnabled" Add relevant logging to make it easy to figure out root cause from bug report. Bug: 151112858 Test: atest CtsSharesheetTestCases:android.sharesheet.cts.CtsSharesheetDeviceTest Change-Id: Id8c4b373ba9e2e1ae29281791e49dde8722ba9d1 --- .../app/AbstractResolverComparator.java | 2 +- ...ppPredictionServiceResolverComparator.java | 25 ++-- .../android/internal/app/ChooserActivity.java | 109 ++++++++++++++++++ .../internal/app/ChooserListAdapter.java | 56 +++++++++ .../com/android/internal/app/ChooserUtil.java | 57 +++++++++ .../sysui/SystemUiDeviceConfigFlags.java | 5 + 6 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 core/java/com/android/internal/app/ChooserUtil.java 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 d851a099d0e17..061022cb78521 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( @@ -3723,6 +3821,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 69a4927c9ab1d..1f83075898acf 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 837cc466a895c..a801632a09a03 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. */