blob: ddbff7bc915f377ed897997a38b2f868c26bbec5 [file] [log] [blame]
Tony Makf99ee172018-11-23 12:14:39 +00001/*
2 * Copyright (C) 2018 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
17package android.view.textclassifier;
18
Tony Makac9b4d82019-02-15 13:57:38 +000019import android.annotation.Nullable;
Tony Makf99ee172018-11-23 12:14:39 +000020import android.app.Person;
Tony Makc12035e2019-02-26 17:45:34 +000021import android.app.RemoteAction;
Tony Make94e0782018-12-14 11:57:54 +080022import android.content.Context;
Tony Makf99ee172018-11-23 12:14:39 +000023import android.text.TextUtils;
24import android.util.ArrayMap;
Tony Makc12035e2019-02-26 17:45:34 +000025import android.util.Pair;
Tony Makf99ee172018-11-23 12:14:39 +000026
27import com.android.internal.annotations.VisibleForTesting;
28
29import com.google.android.textclassifier.ActionsSuggestionsModel;
30
31import java.util.ArrayDeque;
32import java.util.ArrayList;
33import java.util.Deque;
34import java.util.List;
Tony Make94e0782018-12-14 11:57:54 +080035import java.util.Locale;
Tony Makf99ee172018-11-23 12:14:39 +000036import java.util.Map;
Tony Make94e0782018-12-14 11:57:54 +080037import java.util.Objects;
38import java.util.StringJoiner;
Tony Mak82fa8d92018-12-07 17:37:43 +000039import java.util.function.Function;
Tony Makf99ee172018-11-23 12:14:39 +000040import java.util.stream.Collectors;
41
42/**
43 * Helper class for action suggestions.
44 *
45 * @hide
46 */
47@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
48public final class ActionsSuggestionsHelper {
49 private static final int USER_LOCAL = 0;
50 private static final int FIRST_NON_LOCAL_USER = 1;
51
52 private ActionsSuggestionsHelper() {}
53
54 /**
55 * Converts the messages to a list of native messages object that the model can understand.
56 * <p>
57 * User id encoding - local user is represented as 0, Other users are numbered according to
58 * how far before they spoke last time in the conversation. For example, considering this
59 * conversation:
60 * <ul>
61 * <li> User A: xxx
62 * <li> Local user: yyy
63 * <li> User B: zzz
64 * </ul>
65 * User A will be encoded as 2, user B will be encoded as 1 and local user will be encoded as 0.
66 */
Tony Makf99ee172018-11-23 12:14:39 +000067 public static ActionsSuggestionsModel.ConversationMessage[] toNativeMessages(
Tony Mak82fa8d92018-12-07 17:37:43 +000068 List<ConversationActions.Message> messages,
69 Function<CharSequence, String> languageDetector) {
Tony Makf99ee172018-11-23 12:14:39 +000070 List<ConversationActions.Message> messagesWithText =
71 messages.stream()
72 .filter(message -> !TextUtils.isEmpty(message.getText()))
73 .collect(Collectors.toCollection(ArrayList::new));
74 if (messagesWithText.isEmpty()) {
75 return new ActionsSuggestionsModel.ConversationMessage[0];
76 }
Tony Makf99ee172018-11-23 12:14:39 +000077 Deque<ActionsSuggestionsModel.ConversationMessage> nativeMessages = new ArrayDeque<>();
78 PersonEncoder personEncoder = new PersonEncoder();
Tony Mak82fa8d92018-12-07 17:37:43 +000079 int size = messagesWithText.size();
Tony Makf99ee172018-11-23 12:14:39 +000080 for (int i = size - 1; i >= 0; i--) {
81 ConversationActions.Message message = messagesWithText.get(i);
Tony Mak82fa8d92018-12-07 17:37:43 +000082 long referenceTime = message.getReferenceTime() == null
83 ? 0
84 : message.getReferenceTime().toInstant().toEpochMilli();
Tony Makf99ee172018-11-23 12:14:39 +000085 nativeMessages.push(new ActionsSuggestionsModel.ConversationMessage(
86 personEncoder.encode(message.getAuthor()),
Tony Mak82fa8d92018-12-07 17:37:43 +000087 message.getText().toString(), referenceTime,
88 languageDetector.apply(message.getText())));
Tony Makf99ee172018-11-23 12:14:39 +000089 }
90 return nativeMessages.toArray(
91 new ActionsSuggestionsModel.ConversationMessage[nativeMessages.size()]);
92 }
93
Tony Make94e0782018-12-14 11:57:54 +080094 /**
95 * Returns the result id for logging.
96 */
97 public static String createResultId(
98 Context context,
99 List<ConversationActions.Message> messages,
100 int modelVersion,
101 List<Locale> modelLocales) {
102 final StringJoiner localesJoiner = new StringJoiner(",");
103 for (Locale locale : modelLocales) {
104 localesJoiner.add(locale.toLanguageTag());
105 }
106 final String modelName = String.format(
107 Locale.US, "%s_v%d", localesJoiner.toString(), modelVersion);
108 final int hash = Objects.hash(
Tony Mak03a1d032019-01-24 15:12:00 +0000109 messages.stream().mapToInt(ActionsSuggestionsHelper::hashMessage),
110 context.getPackageName(),
111 System.currentTimeMillis());
Tony Make94e0782018-12-14 11:57:54 +0800112 return SelectionSessionLogger.SignatureParser.createSignature(
113 SelectionSessionLogger.CLASSIFIER_ID, modelName, hash);
114 }
115
Tony Makac9b4d82019-02-15 13:57:38 +0000116 /**
117 * Returns a {@link android.view.textclassifier.LabeledIntent.TitleChooser} for
118 * conversation actions use case.
119 */
120 @Nullable
121 public static LabeledIntent.TitleChooser createTitleChooser(String actionType) {
122 if (ConversationAction.TYPE_OPEN_URL.equals(actionType)) {
Tony Makc12035e2019-02-26 17:45:34 +0000123 return (labeledIntent, resolveInfo) -> {
124 if (resolveInfo.handleAllWebDataURI) {
125 return labeledIntent.titleWithEntity;
126 }
127 if ("android".equals(resolveInfo.activityInfo.packageName)) {
128 return labeledIntent.titleWithEntity;
129 }
130 return labeledIntent.titleWithoutEntity;
131 };
Tony Makac9b4d82019-02-15 13:57:38 +0000132 }
133 return null;
134 }
135
Tony Makc12035e2019-02-26 17:45:34 +0000136 /**
137 * Returns a list of {@link ConversationAction}s that have 0 duplicates. Two actions are
138 * duplicates if they may look the same to users. This function assumes every
139 * ConversationActions with a non-null RemoteAction also have a non-null intent in the extras.
140 */
141 public static List<ConversationAction> removeActionsWithDuplicates(
142 List<ConversationAction> conversationActions) {
143 // Ideally, we should compare title and icon here, but comparing icon is expensive and thus
144 // we use the component name of the target handler as the heuristic.
145 Map<Pair<String, String>, Integer> counter = new ArrayMap<>();
146 for (ConversationAction conversationAction : conversationActions) {
147 Pair<String, String> representation = getRepresentation(conversationAction);
148 if (representation == null) {
149 continue;
150 }
151 Integer existingCount = counter.getOrDefault(representation, 0);
152 counter.put(representation, existingCount + 1);
153 }
154 List<ConversationAction> result = new ArrayList<>();
155 for (ConversationAction conversationAction : conversationActions) {
156 Pair<String, String> representation = getRepresentation(conversationAction);
157 if (representation == null || counter.getOrDefault(representation, 0) == 1) {
158 result.add(conversationAction);
159 }
160 }
161 return result;
162 }
163
164 @Nullable
165 private static Pair<String, String> getRepresentation(
166 ConversationAction conversationAction) {
167 RemoteAction remoteAction = conversationAction.getAction();
168 if (remoteAction == null) {
169 return null;
170 }
171 return new Pair<>(
172 conversationAction.getAction().getTitle().toString(),
173 ExtrasUtils.getActionIntent(
174 conversationAction.getExtras()).getComponent().getPackageName());
175 }
176
Tony Makf99ee172018-11-23 12:14:39 +0000177 private static final class PersonEncoder {
178 private final Map<Person, Integer> mMapping = new ArrayMap<>();
179 private int mNextUserId = FIRST_NON_LOCAL_USER;
180
181 private int encode(Person person) {
Tony Mak91daa152019-01-24 16:00:28 +0000182 if (ConversationActions.Message.PERSON_USER_SELF.equals(person)) {
Tony Makf99ee172018-11-23 12:14:39 +0000183 return USER_LOCAL;
184 }
185 Integer result = mMapping.get(person);
186 if (result == null) {
187 mMapping.put(person, mNextUserId);
188 result = mNextUserId;
189 mNextUserId++;
190 }
191 return result;
192 }
193 }
Tony Mak03a1d032019-01-24 15:12:00 +0000194
195 private static int hashMessage(ConversationActions.Message message) {
196 return Objects.hash(message.getAuthor(), message.getText(), message.getReferenceTime());
197 }
Tony Makf99ee172018-11-23 12:14:39 +0000198}