blob: 9c268f2d18a8e7de0faf71b2bdd77237574580ef [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 Mak8ab9b182019-03-01 16:44:17 +000026import android.view.textclassifier.intent.LabeledIntent;
27import android.view.textclassifier.intent.TemplateIntentFactory;
Tony Makf99ee172018-11-23 12:14:39 +000028
29import com.android.internal.annotations.VisibleForTesting;
30
31import com.google.android.textclassifier.ActionsSuggestionsModel;
Tony Mak8ab9b182019-03-01 16:44:17 +000032import com.google.android.textclassifier.RemoteActionTemplate;
Tony Makf99ee172018-11-23 12:14:39 +000033
34import java.util.ArrayDeque;
35import java.util.ArrayList;
36import java.util.Deque;
37import java.util.List;
Tony Make94e0782018-12-14 11:57:54 +080038import java.util.Locale;
Tony Makf99ee172018-11-23 12:14:39 +000039import java.util.Map;
Tony Make94e0782018-12-14 11:57:54 +080040import java.util.Objects;
41import java.util.StringJoiner;
Tony Mak82fa8d92018-12-07 17:37:43 +000042import java.util.function.Function;
Tony Makf99ee172018-11-23 12:14:39 +000043import java.util.stream.Collectors;
44
45/**
46 * Helper class for action suggestions.
47 *
48 * @hide
49 */
50@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
51public final class ActionsSuggestionsHelper {
Tony Mak8ab9b182019-03-01 16:44:17 +000052 private static final String TAG = "ActionsSuggestions";
Tony Makf99ee172018-11-23 12:14:39 +000053 private static final int USER_LOCAL = 0;
54 private static final int FIRST_NON_LOCAL_USER = 1;
55
56 private ActionsSuggestionsHelper() {}
57
58 /**
59 * Converts the messages to a list of native messages object that the model can understand.
60 * <p>
61 * User id encoding - local user is represented as 0, Other users are numbered according to
62 * how far before they spoke last time in the conversation. For example, considering this
63 * conversation:
64 * <ul>
65 * <li> User A: xxx
66 * <li> Local user: yyy
67 * <li> User B: zzz
68 * </ul>
69 * User A will be encoded as 2, user B will be encoded as 1 and local user will be encoded as 0.
70 */
Tony Makf99ee172018-11-23 12:14:39 +000071 public static ActionsSuggestionsModel.ConversationMessage[] toNativeMessages(
Tony Mak82fa8d92018-12-07 17:37:43 +000072 List<ConversationActions.Message> messages,
73 Function<CharSequence, String> languageDetector) {
Tony Makf99ee172018-11-23 12:14:39 +000074 List<ConversationActions.Message> messagesWithText =
75 messages.stream()
76 .filter(message -> !TextUtils.isEmpty(message.getText()))
77 .collect(Collectors.toCollection(ArrayList::new));
78 if (messagesWithText.isEmpty()) {
79 return new ActionsSuggestionsModel.ConversationMessage[0];
80 }
Tony Makf99ee172018-11-23 12:14:39 +000081 Deque<ActionsSuggestionsModel.ConversationMessage> nativeMessages = new ArrayDeque<>();
82 PersonEncoder personEncoder = new PersonEncoder();
Tony Mak82fa8d92018-12-07 17:37:43 +000083 int size = messagesWithText.size();
Tony Makf99ee172018-11-23 12:14:39 +000084 for (int i = size - 1; i >= 0; i--) {
85 ConversationActions.Message message = messagesWithText.get(i);
Tony Mak82fa8d92018-12-07 17:37:43 +000086 long referenceTime = message.getReferenceTime() == null
87 ? 0
88 : message.getReferenceTime().toInstant().toEpochMilli();
Tony Mak159f0282019-03-01 14:03:25 +000089 String timeZone = message.getReferenceTime() == null
90 ? null
91 : message.getReferenceTime().getZone().getId();
Tony Makf99ee172018-11-23 12:14:39 +000092 nativeMessages.push(new ActionsSuggestionsModel.ConversationMessage(
93 personEncoder.encode(message.getAuthor()),
Tony Mak159f0282019-03-01 14:03:25 +000094 message.getText().toString(), referenceTime, timeZone,
Tony Mak82fa8d92018-12-07 17:37:43 +000095 languageDetector.apply(message.getText())));
Tony Makf99ee172018-11-23 12:14:39 +000096 }
97 return nativeMessages.toArray(
98 new ActionsSuggestionsModel.ConversationMessage[nativeMessages.size()]);
99 }
100
Tony Make94e0782018-12-14 11:57:54 +0800101 /**
102 * Returns the result id for logging.
103 */
104 public static String createResultId(
105 Context context,
106 List<ConversationActions.Message> messages,
107 int modelVersion,
108 List<Locale> modelLocales) {
109 final StringJoiner localesJoiner = new StringJoiner(",");
110 for (Locale locale : modelLocales) {
111 localesJoiner.add(locale.toLanguageTag());
112 }
113 final String modelName = String.format(
114 Locale.US, "%s_v%d", localesJoiner.toString(), modelVersion);
115 final int hash = Objects.hash(
Tony Mak03a1d032019-01-24 15:12:00 +0000116 messages.stream().mapToInt(ActionsSuggestionsHelper::hashMessage),
117 context.getPackageName(),
118 System.currentTimeMillis());
Tony Make94e0782018-12-14 11:57:54 +0800119 return SelectionSessionLogger.SignatureParser.createSignature(
120 SelectionSessionLogger.CLASSIFIER_ID, modelName, hash);
121 }
122
Tony Makac9b4d82019-02-15 13:57:38 +0000123 /**
Tony Mak8ab9b182019-03-01 16:44:17 +0000124 * Generated labeled intent from an action suggestion and return the resolved result.
125 */
126 @Nullable
127 public static LabeledIntent.Result createLabeledIntentResult(
128 Context context,
129 TemplateIntentFactory templateIntentFactory,
130 ActionsSuggestionsModel.ActionSuggestion nativeSuggestion) {
131 RemoteActionTemplate[] remoteActionTemplates =
132 nativeSuggestion.getRemoteActionTemplates();
133 if (remoteActionTemplates == null) {
134 Log.w(TAG, "createRemoteAction: Missing template for type "
135 + nativeSuggestion.getActionType());
136 return null;
137 }
138 List<LabeledIntent> labeledIntents = templateIntentFactory.create(remoteActionTemplates);
139 if (labeledIntents.isEmpty()) {
140 return null;
141 }
142 // Given that we only support implicit intent here, we should expect there is just one
143 // intent for each action type.
144 LabeledIntent.TitleChooser titleChooser =
145 ActionsSuggestionsHelper.createTitleChooser(nativeSuggestion.getActionType());
Tony Mak72e17972019-03-16 10:28:42 +0000146 return labeledIntents.get(0).resolve(context, titleChooser, null);
Tony Mak8ab9b182019-03-01 16:44:17 +0000147 }
148
149 /**
150 * Returns a {@link LabeledIntent.TitleChooser} for conversation actions use case.
Tony Makac9b4d82019-02-15 13:57:38 +0000151 */
152 @Nullable
153 public static LabeledIntent.TitleChooser createTitleChooser(String actionType) {
154 if (ConversationAction.TYPE_OPEN_URL.equals(actionType)) {
Tony Makc12035e2019-02-26 17:45:34 +0000155 return (labeledIntent, resolveInfo) -> {
156 if (resolveInfo.handleAllWebDataURI) {
157 return labeledIntent.titleWithEntity;
158 }
159 if ("android".equals(resolveInfo.activityInfo.packageName)) {
160 return labeledIntent.titleWithEntity;
161 }
162 return labeledIntent.titleWithoutEntity;
163 };
Tony Makac9b4d82019-02-15 13:57:38 +0000164 }
165 return null;
166 }
167
Tony Makc12035e2019-02-26 17:45:34 +0000168 /**
169 * Returns a list of {@link ConversationAction}s that have 0 duplicates. Two actions are
170 * duplicates if they may look the same to users. This function assumes every
171 * ConversationActions with a non-null RemoteAction also have a non-null intent in the extras.
172 */
173 public static List<ConversationAction> removeActionsWithDuplicates(
174 List<ConversationAction> conversationActions) {
175 // Ideally, we should compare title and icon here, but comparing icon is expensive and thus
176 // we use the component name of the target handler as the heuristic.
177 Map<Pair<String, String>, Integer> counter = new ArrayMap<>();
178 for (ConversationAction conversationAction : conversationActions) {
179 Pair<String, String> representation = getRepresentation(conversationAction);
180 if (representation == null) {
181 continue;
182 }
183 Integer existingCount = counter.getOrDefault(representation, 0);
184 counter.put(representation, existingCount + 1);
185 }
186 List<ConversationAction> result = new ArrayList<>();
187 for (ConversationAction conversationAction : conversationActions) {
188 Pair<String, String> representation = getRepresentation(conversationAction);
189 if (representation == null || counter.getOrDefault(representation, 0) == 1) {
190 result.add(conversationAction);
191 }
192 }
193 return result;
194 }
195
196 @Nullable
197 private static Pair<String, String> getRepresentation(
198 ConversationAction conversationAction) {
199 RemoteAction remoteAction = conversationAction.getAction();
200 if (remoteAction == null) {
201 return null;
202 }
203 return new Pair<>(
204 conversationAction.getAction().getTitle().toString(),
205 ExtrasUtils.getActionIntent(
206 conversationAction.getExtras()).getComponent().getPackageName());
207 }
208
Tony Makf99ee172018-11-23 12:14:39 +0000209 private static final class PersonEncoder {
210 private final Map<Person, Integer> mMapping = new ArrayMap<>();
211 private int mNextUserId = FIRST_NON_LOCAL_USER;
212
213 private int encode(Person person) {
Tony Mak91daa152019-01-24 16:00:28 +0000214 if (ConversationActions.Message.PERSON_USER_SELF.equals(person)) {
Tony Makf99ee172018-11-23 12:14:39 +0000215 return USER_LOCAL;
216 }
217 Integer result = mMapping.get(person);
218 if (result == null) {
219 mMapping.put(person, mNextUserId);
220 result = mNextUserId;
221 mNextUserId++;
222 }
223 return result;
224 }
225 }
Tony Mak03a1d032019-01-24 15:12:00 +0000226
227 private static int hashMessage(ConversationActions.Message message) {
228 return Objects.hash(message.getAuthor(), message.getText(), message.getReferenceTime());
229 }
Tony Makf99ee172018-11-23 12:14:39 +0000230}