blob: 17edf5c209de25715adb31fa219f5875126b90d1 [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 Mak159f0282019-03-01 14:03:25 +000085 String timeZone = message.getReferenceTime() == null
86 ? null
87 : message.getReferenceTime().getZone().getId();
Tony Makf99ee172018-11-23 12:14:39 +000088 nativeMessages.push(new ActionsSuggestionsModel.ConversationMessage(
89 personEncoder.encode(message.getAuthor()),
Tony Mak159f0282019-03-01 14:03:25 +000090 message.getText().toString(), referenceTime, timeZone,
Tony Mak82fa8d92018-12-07 17:37:43 +000091 languageDetector.apply(message.getText())));
Tony Makf99ee172018-11-23 12:14:39 +000092 }
93 return nativeMessages.toArray(
94 new ActionsSuggestionsModel.ConversationMessage[nativeMessages.size()]);
95 }
96
Tony Make94e0782018-12-14 11:57:54 +080097 /**
98 * Returns the result id for logging.
99 */
100 public static String createResultId(
101 Context context,
102 List<ConversationActions.Message> messages,
103 int modelVersion,
104 List<Locale> modelLocales) {
105 final StringJoiner localesJoiner = new StringJoiner(",");
106 for (Locale locale : modelLocales) {
107 localesJoiner.add(locale.toLanguageTag());
108 }
109 final String modelName = String.format(
110 Locale.US, "%s_v%d", localesJoiner.toString(), modelVersion);
111 final int hash = Objects.hash(
Tony Mak03a1d032019-01-24 15:12:00 +0000112 messages.stream().mapToInt(ActionsSuggestionsHelper::hashMessage),
113 context.getPackageName(),
114 System.currentTimeMillis());
Tony Make94e0782018-12-14 11:57:54 +0800115 return SelectionSessionLogger.SignatureParser.createSignature(
116 SelectionSessionLogger.CLASSIFIER_ID, modelName, hash);
117 }
118
Tony Makac9b4d82019-02-15 13:57:38 +0000119 /**
120 * Returns a {@link android.view.textclassifier.LabeledIntent.TitleChooser} for
121 * conversation actions use case.
122 */
123 @Nullable
124 public static LabeledIntent.TitleChooser createTitleChooser(String actionType) {
125 if (ConversationAction.TYPE_OPEN_URL.equals(actionType)) {
Tony Makc12035e2019-02-26 17:45:34 +0000126 return (labeledIntent, resolveInfo) -> {
127 if (resolveInfo.handleAllWebDataURI) {
128 return labeledIntent.titleWithEntity;
129 }
130 if ("android".equals(resolveInfo.activityInfo.packageName)) {
131 return labeledIntent.titleWithEntity;
132 }
133 return labeledIntent.titleWithoutEntity;
134 };
Tony Makac9b4d82019-02-15 13:57:38 +0000135 }
136 return null;
137 }
138
Tony Makc12035e2019-02-26 17:45:34 +0000139 /**
140 * Returns a list of {@link ConversationAction}s that have 0 duplicates. Two actions are
141 * duplicates if they may look the same to users. This function assumes every
142 * ConversationActions with a non-null RemoteAction also have a non-null intent in the extras.
143 */
144 public static List<ConversationAction> removeActionsWithDuplicates(
145 List<ConversationAction> conversationActions) {
146 // Ideally, we should compare title and icon here, but comparing icon is expensive and thus
147 // we use the component name of the target handler as the heuristic.
148 Map<Pair<String, String>, Integer> counter = new ArrayMap<>();
149 for (ConversationAction conversationAction : conversationActions) {
150 Pair<String, String> representation = getRepresentation(conversationAction);
151 if (representation == null) {
152 continue;
153 }
154 Integer existingCount = counter.getOrDefault(representation, 0);
155 counter.put(representation, existingCount + 1);
156 }
157 List<ConversationAction> result = new ArrayList<>();
158 for (ConversationAction conversationAction : conversationActions) {
159 Pair<String, String> representation = getRepresentation(conversationAction);
160 if (representation == null || counter.getOrDefault(representation, 0) == 1) {
161 result.add(conversationAction);
162 }
163 }
164 return result;
165 }
166
167 @Nullable
168 private static Pair<String, String> getRepresentation(
169 ConversationAction conversationAction) {
170 RemoteAction remoteAction = conversationAction.getAction();
171 if (remoteAction == null) {
172 return null;
173 }
174 return new Pair<>(
175 conversationAction.getAction().getTitle().toString(),
176 ExtrasUtils.getActionIntent(
177 conversationAction.getExtras()).getComponent().getPackageName());
178 }
179
Tony Makf99ee172018-11-23 12:14:39 +0000180 private static final class PersonEncoder {
181 private final Map<Person, Integer> mMapping = new ArrayMap<>();
182 private int mNextUserId = FIRST_NON_LOCAL_USER;
183
184 private int encode(Person person) {
Tony Mak91daa152019-01-24 16:00:28 +0000185 if (ConversationActions.Message.PERSON_USER_SELF.equals(person)) {
Tony Makf99ee172018-11-23 12:14:39 +0000186 return USER_LOCAL;
187 }
188 Integer result = mMapping.get(person);
189 if (result == null) {
190 mMapping.put(person, mNextUserId);
191 result = mNextUserId;
192 mNextUserId++;
193 }
194 return result;
195 }
196 }
Tony Mak03a1d032019-01-24 15:12:00 +0000197
198 private static int hashMessage(ConversationActions.Message message) {
199 return Objects.hash(message.getAuthor(), message.getText(), message.getReferenceTime());
200 }
Tony Makf99ee172018-11-23 12:14:39 +0000201}