blob: 2cf265d58b74361bf42b83065792d4a477c585c7 [file] [log] [blame]
Abodunrinwa Toki43e03502017-01-13 13:46:33 -08001/*
2 * Copyright (C) 2017 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
Jan Althausa1652cf2018-03-29 17:51:57 +020019import static java.time.temporal.ChronoUnit.MILLIS;
20
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080021import android.annotation.NonNull;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080022import android.annotation.Nullable;
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +000023import android.annotation.WorkerThread;
Abodunrinwa Toki253827f2018-04-24 19:19:48 +010024import android.app.PendingIntent;
Jan Althaus20d346e2018-03-23 14:03:52 +010025import android.app.RemoteAction;
Jan Althaus705b9e92018-01-22 18:22:29 +010026import android.app.SearchManager;
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +000027import android.content.ComponentName;
Jan Althaus705b9e92018-01-22 18:22:29 +010028import android.content.ContentUris;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080029import android.content.Context;
30import android.content.Intent;
31import android.content.pm.PackageManager;
32import android.content.pm.ResolveInfo;
Jan Althaus20d346e2018-03-23 14:03:52 +010033import android.graphics.drawable.Icon;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080034import android.net.Uri;
Jan Althaus05e00512018-02-02 15:28:34 +010035import android.os.Bundle;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000036import android.os.LocaleList;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080037import android.os.ParcelFileDescriptor;
Jan Althaus05e00512018-02-02 15:28:34 +010038import android.os.UserManager;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000039import android.provider.Browser;
Jan Althaus705b9e92018-01-22 18:22:29 +010040import android.provider.CalendarContract;
Jan Althaus92d76832017-09-27 18:14:35 +020041import android.provider.ContactsContract;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080042
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010043import com.android.internal.annotations.GuardedBy;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080044import com.android.internal.util.Preconditions;
45
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010046import java.io.File;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080047import java.io.FileNotFoundException;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010048import java.io.IOException;
Jan Althauseaff57e2018-02-12 12:47:27 +010049import java.io.UnsupportedEncodingException;
Jan Althauseaff57e2018-02-12 12:47:27 +010050import java.net.URLEncoder;
Jan Althausa1652cf2018-03-29 17:51:57 +020051import java.time.Instant;
52import java.time.ZonedDateTime;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080053import java.util.ArrayList;
Richard Ledleydb18a572017-11-30 17:33:51 +000054import java.util.Arrays;
55import java.util.Collection;
56import java.util.Collections;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010057import java.util.HashMap;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080058import java.util.List;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000059import java.util.Locale;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080060import java.util.Map;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010061import java.util.Objects;
Jan Althausef0156d2018-01-29 19:28:41 +010062import java.util.StringJoiner;
Jan Althaus705b9e92018-01-22 18:22:29 +010063import java.util.concurrent.TimeUnit;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010064import java.util.regex.Matcher;
65import java.util.regex.Pattern;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080066
67/**
68 * Default implementation of the {@link TextClassifier} interface.
69 *
70 * <p>This class uses machine learning to recognize entities in text.
71 * Unless otherwise stated, methods of this class are blocking operations and should most
72 * likely not be called on the UI thread.
73 *
74 * @hide
75 */
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080076public final class TextClassifierImpl implements TextClassifier {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080077
Abodunrinwa Toki692b1962017-08-15 15:05:11 +010078 private static final String LOG_TAG = DEFAULT_LOG_TAG;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010079 private static final String MODEL_DIR = "/etc/textclassifier/";
Jan Althausabb4fc82018-01-31 11:51:34 +010080 private static final String MODEL_FILE_REGEX = "textclassifier\\.(.*)\\.model";
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010081 private static final String UPDATED_MODEL_FILE_PATH =
Jan Althaus67d234d2018-01-26 17:53:31 +010082 "/data/misc/textclassifier/textclassifier.model";
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +000083
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080084 private final Context mContext;
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080085 private final TextClassifier mFallback;
Jan Althaus31efdc32018-02-19 22:23:13 +010086 private final GenerateLinksLogger mGenerateLinksLogger;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010087
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080088 private final Object mLock = new Object();
89 @GuardedBy("mLock") // Do not access outside this lock.
Jan Althausef0156d2018-01-29 19:28:41 +010090 private List<ModelFile> mAllModelFiles;
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080091 @GuardedBy("mLock") // Do not access outside this lock.
Jan Althausef0156d2018-01-29 19:28:41 +010092 private ModelFile mModel;
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080093 @GuardedBy("mLock") // Do not access outside this lock.
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +010094 private TextClassifierImplNative mNative;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080095
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +000096 private final Object mLoggerLock = new Object();
97 @GuardedBy("mLoggerLock") // Do not access outside this lock.
Jan Althaus5a030942018-04-04 19:40:38 +020098 private SelectionSessionLogger mSessionLogger;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +000099
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000100 private final TextClassificationConstants mSettings;
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100101
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100102 public TextClassifierImpl(
103 Context context, TextClassificationConstants settings, TextClassifier fallback) {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800104 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100105 mFallback = Preconditions.checkNotNull(fallback);
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000106 mSettings = Preconditions.checkNotNull(settings);
107 mGenerateLinksLogger = new GenerateLinksLogger(mSettings.getGenerateLinksLogSampleRate());
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800108 }
109
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100110 public TextClassifierImpl(Context context, TextClassificationConstants settings) {
111 this(context, settings, TextClassifier.NO_OP);
112 }
113
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800114 /** @inheritDoc */
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800115 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000116 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100117 public TextSelection suggestSelection(TextSelection.Request request) {
118 Preconditions.checkNotNull(request);
119 Utils.checkMainThread();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800120 try {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100121 final int rangeLength = request.getEndIndex() - request.getStartIndex();
122 final String string = request.getText().toString();
123 if (string.length() > 0
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000124 && rangeLength <= mSettings.getSuggestSelectionMaxRangeLength()) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100125 final String localesString = concatenateLocales(request.getDefaultLocales());
Jan Althausa1652cf2018-03-29 17:51:57 +0200126 final ZonedDateTime refTime = ZonedDateTime.now();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100127 final TextClassifierImplNative nativeImpl = getNative(request.getDefaultLocales());
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100128 final int start;
129 final int end;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100130 if (mSettings.isModelDarkLaunchEnabled() && !request.isDarkLaunchAllowed()) {
131 start = request.getStartIndex();
132 end = request.getEndIndex();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100133 } else {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100134 final int[] startEnd = nativeImpl.suggestSelection(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100135 string, request.getStartIndex(), request.getEndIndex(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100136 new TextClassifierImplNative.SelectionOptions(localesString));
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100137 start = startEnd[0];
138 end = startEnd[1];
139 }
Jan Althausb7f7d362018-01-26 13:51:31 +0100140 if (start < end
Abodunrinwa Tokib4162972017-05-05 18:07:17 +0100141 && start >= 0 && end <= string.length()
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100142 && start <= request.getStartIndex() && end >= request.getEndIndex()) {
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100143 final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100144 final TextClassifierImplNative.ClassificationResult[] results =
145 nativeImpl.classifyText(
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000146 string, start, end,
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100147 new TextClassifierImplNative.ClassificationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200148 refTime.toInstant().toEpochMilli(),
149 refTime.getZone().getId(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100150 localesString));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000151 final int size = results.length;
152 for (int i = 0; i < size; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100153 tsBuilder.setEntityType(results[i].getCollection(), results[i].getScore());
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000154 }
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100155 return tsBuilder.setId(createId(
156 string, request.getStartIndex(), request.getEndIndex()))
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100157 .build();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800158 } else {
159 // We can not trust the result. Log the issue and ignore the result.
160 Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
161 }
162 }
163 } catch (Throwable t) {
164 // Avoid throwing from this method. Log the error.
165 Log.e(LOG_TAG,
166 "Error suggesting selection for text. No changes to selection suggested.",
167 t);
168 }
169 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100170 return mFallback.suggestSelection(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100171 }
172
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800173 /** @inheritDoc */
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100174 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000175 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100176 public TextClassification classifyText(TextClassification.Request request) {
177 Preconditions.checkNotNull(request);
178 Utils.checkMainThread();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800179 try {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100180 final int rangeLength = request.getEndIndex() - request.getStartIndex();
181 final String string = request.getText().toString();
182 if (string.length() > 0 && rangeLength <= mSettings.getClassifyTextMaxRangeLength()) {
183 final String localesString = concatenateLocales(request.getDefaultLocales());
184 final ZonedDateTime refTime = request.getReferenceTime() != null
185 ? request.getReferenceTime() : ZonedDateTime.now();
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100186 final TextClassifierImplNative.ClassificationResult[] results =
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100187 getNative(request.getDefaultLocales())
188 .classifyText(
189 string, request.getStartIndex(), request.getEndIndex(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100190 new TextClassifierImplNative.ClassificationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200191 refTime.toInstant().toEpochMilli(),
192 refTime.getZone().getId(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100193 localesString));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000194 if (results.length > 0) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100195 return createClassificationResult(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100196 results, string,
197 request.getStartIndex(), request.getEndIndex(), refTime.toInstant());
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800198 }
199 }
200 } catch (Throwable t) {
201 // Avoid throwing from this method. Log the error.
Abodunrinwa Toki78618852017-10-17 15:31:39 +0100202 Log.e(LOG_TAG, "Error getting text classification info.", t);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800203 }
204 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100205 return mFallback.classifyText(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100206 }
207
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800208 /** @inheritDoc */
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100209 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000210 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100211 public TextLinks generateLinks(@NonNull TextLinks.Request request) {
212 Preconditions.checkNotNull(request);
213 Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength());
214 Utils.checkMainThread();
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000215
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100216 if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
217 return Utils.generateLegacyLinks(request);
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000218 }
219
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100220 final String textString = request.getText().toString();
Richard Ledley68d94522017-10-05 10:52:19 +0100221 final TextLinks.Builder builder = new TextLinks.Builder(textString);
Richard Ledley9cfa6062018-01-15 13:13:29 +0000222
Richard Ledley68d94522017-10-05 10:52:19 +0100223 try {
Jan Althaus31efdc32018-02-19 22:23:13 +0100224 final long startTimeMs = System.currentTimeMillis();
Jan Althausa1652cf2018-03-29 17:51:57 +0200225 final ZonedDateTime refTime = ZonedDateTime.now();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100226 final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
227 ? request.getEntityConfig().resolveEntityListModifications(
228 getEntitiesForHints(request.getEntityConfig().getHints()))
229 : mSettings.getEntityListDefault();
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100230 final TextClassifierImplNative nativeImpl =
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100231 getNative(request.getDefaultLocales());
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100232 final TextClassifierImplNative.AnnotatedSpan[] annotations =
233 nativeImpl.annotate(
234 textString,
235 new TextClassifierImplNative.AnnotationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200236 refTime.toInstant().toEpochMilli(),
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100237 refTime.getZone().getId(),
238 concatenateLocales(request.getDefaultLocales())));
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100239 for (TextClassifierImplNative.AnnotatedSpan span : annotations) {
240 final TextClassifierImplNative.ClassificationResult[] results =
241 span.getClassification();
242 if (results.length == 0
243 || !entitiesToIdentify.contains(results[0].getCollection())) {
Richard Ledleydb18a572017-11-30 17:33:51 +0000244 continue;
245 }
246 final Map<String, Float> entityScores = new HashMap<>();
Richard Ledley68d94522017-10-05 10:52:19 +0100247 for (int i = 0; i < results.length; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100248 entityScores.put(results[i].getCollection(), results[i].getScore());
Richard Ledley68d94522017-10-05 10:52:19 +0100249 }
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000250 builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores);
Richard Ledley68d94522017-10-05 10:52:19 +0100251 }
Jan Althaus31efdc32018-02-19 22:23:13 +0100252 final TextLinks links = builder.build();
253 final long endTimeMs = System.currentTimeMillis();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100254 final String callingPackageName = request.getCallingPackageName() == null
255 ? mContext.getPackageName() // local (in process) TC.
256 : request.getCallingPackageName();
Jan Althaus31efdc32018-02-19 22:23:13 +0100257 mGenerateLinksLogger.logGenerateLinks(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100258 request.getText(), links, callingPackageName, endTimeMs - startTimeMs);
Jan Althaus31efdc32018-02-19 22:23:13 +0100259 return links;
Richard Ledley68d94522017-10-05 10:52:19 +0100260 } catch (Throwable t) {
261 // Avoid throwing from this method. Log the error.
262 Log.e(LOG_TAG, "Error getting links info.", t);
263 }
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100264 return mFallback.generateLinks(request);
Richard Ledley68d94522017-10-05 10:52:19 +0100265 }
266
Jan Althaus108aad32018-01-30 15:26:55 +0100267 /** @inheritDoc */
268 @Override
269 public int getMaxGenerateLinksTextLength() {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000270 return mSettings.getGenerateLinksMaxTextLength();
Jan Althaus108aad32018-01-30 15:26:55 +0100271 }
272
Richard Ledley1fc998b2018-02-16 15:45:06 +0000273 private Collection<String> getEntitiesForHints(Collection<String> hints) {
Jan Althaus0aacdb62018-02-19 11:44:37 +0100274 final boolean editable = hints.contains(HINT_TEXT_IS_EDITABLE);
275 final boolean notEditable = hints.contains(HINT_TEXT_IS_NOT_EDITABLE);
276
277 // Use the default if there is no hint, or conflicting ones.
278 final boolean useDefault = editable == notEditable;
279 if (useDefault) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000280 return mSettings.getEntityListDefault();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100281 } else if (editable) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000282 return mSettings.getEntityListEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100283 } else { // notEditable
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000284 return mSettings.getEntityListNotEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100285 }
Richard Ledleydb18a572017-11-30 17:33:51 +0000286 }
287
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000288 @Override
289 public void onSelectionEvent(SelectionEvent event) {
290 Preconditions.checkNotNull(event);
291 synchronized (mLoggerLock) {
Jan Althaus5a030942018-04-04 19:40:38 +0200292 if (mSessionLogger == null) {
293 mSessionLogger = new SelectionSessionLogger();
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000294 }
Jan Althaus5a030942018-04-04 19:40:38 +0200295 mSessionLogger.writeEvent(event);
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000296 }
297 }
298
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100299 private TextClassifierImplNative getNative(LocaleList localeList)
300 throws FileNotFoundException {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800301 synchronized (mLock) {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100302 localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
Jan Althausef0156d2018-01-29 19:28:41 +0100303 final ModelFile bestModel = findBestModelLocked(localeList);
304 if (bestModel == null) {
305 throw new FileNotFoundException("No model for " + localeList.toLanguageTags());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100306 }
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100307 if (mNative == null || !Objects.equals(mModel, bestModel)) {
Jan Althausef0156d2018-01-29 19:28:41 +0100308 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100309 destroyNativeIfExistsLocked();
Jan Althausef0156d2018-01-29 19:28:41 +0100310 final ParcelFileDescriptor fd = ParcelFileDescriptor.open(
311 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100312 mNative = new TextClassifierImplNative(fd.getFd());
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100313 closeAndLogError(fd);
Jan Althausef0156d2018-01-29 19:28:41 +0100314 mModel = bestModel;
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000315 }
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100316 return mNative;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800317 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800318 }
319
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100320 private String createId(String text, int start, int end) {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800321 synchronized (mLock) {
Jan Althaus5a030942018-04-04 19:40:38 +0200322 return SelectionSessionLogger.createId(text, start, end, mContext, mModel.getVersion(),
Jan Althausef0156d2018-01-29 19:28:41 +0100323 mModel.getSupportedLocales());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100324 }
325 }
326
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800327 @GuardedBy("mLock") // Do not call outside this lock.
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100328 private void destroyNativeIfExistsLocked() {
329 if (mNative != null) {
330 mNative.close();
331 mNative = null;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100332 }
333 }
334
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100335 private static String concatenateLocales(@Nullable LocaleList locales) {
336 return (locales == null) ? "" : locales.toLanguageTags();
337 }
338
Jan Althausef0156d2018-01-29 19:28:41 +0100339 /**
340 * Finds the most appropriate model to use for the given target locale list.
341 *
342 * The basic logic is: we ignore all models that don't support any of the target locales. For
343 * the remaining candidates, we take the update model unless its version number is lower than
344 * the factory version. It's assumed that factory models do not have overlapping locale ranges
345 * and conflict resolution between these models hence doesn't matter.
346 */
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800347 @GuardedBy("mLock") // Do not call outside this lock.
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100348 @Nullable
Jan Althausef0156d2018-01-29 19:28:41 +0100349 private ModelFile findBestModelLocked(LocaleList localeList) {
Abodunrinwa Tokia2df6e52017-04-13 09:56:52 +0100350 // Specified localeList takes priority over the system default, so it is listed first.
351 final String languages = localeList.isEmpty()
352 ? LocaleList.getDefault().toLanguageTags()
353 : localeList.toLanguageTags() + "," + LocaleList.getDefault().toLanguageTags();
354 final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100355
Jan Althausef0156d2018-01-29 19:28:41 +0100356 ModelFile bestModel = null;
Jan Althausef0156d2018-01-29 19:28:41 +0100357 for (ModelFile model : listAllModelsLocked()) {
358 if (model.isAnyLanguageSupported(languageRangeList)) {
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100359 if (model.isPreferredTo(bestModel)) {
Jan Althausef0156d2018-01-29 19:28:41 +0100360 bestModel = model;
Jan Althausef0156d2018-01-29 19:28:41 +0100361 }
362 }
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100363 }
Jan Althausef0156d2018-01-29 19:28:41 +0100364 return bestModel;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100365 }
366
Jan Althausef0156d2018-01-29 19:28:41 +0100367 /** Returns a list of all model files available, in order of precedence. */
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800368 @GuardedBy("mLock") // Do not call outside this lock.
Jan Althausef0156d2018-01-29 19:28:41 +0100369 private List<ModelFile> listAllModelsLocked() {
370 if (mAllModelFiles == null) {
371 final List<ModelFile> allModels = new ArrayList<>();
372 // The update model has the highest precedence.
373 if (new File(UPDATED_MODEL_FILE_PATH).exists()) {
374 final ModelFile updatedModel = ModelFile.fromPath(UPDATED_MODEL_FILE_PATH);
375 if (updatedModel != null) {
376 allModels.add(updatedModel);
377 }
378 }
379 // Factory models should never have overlapping locales, so the order doesn't matter.
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100380 final File modelsDir = new File(MODEL_DIR);
381 if (modelsDir.exists() && modelsDir.isDirectory()) {
Jan Althausef0156d2018-01-29 19:28:41 +0100382 final File[] modelFiles = modelsDir.listFiles();
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100383 final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX);
Jan Althausef0156d2018-01-29 19:28:41 +0100384 for (File modelFile : modelFiles) {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100385 final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName());
386 if (matcher.matches() && modelFile.isFile()) {
Jan Althausef0156d2018-01-29 19:28:41 +0100387 final ModelFile model = ModelFile.fromPath(modelFile.getAbsolutePath());
388 if (model != null) {
389 allModels.add(model);
390 }
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100391 }
392 }
393 }
Jan Althausef0156d2018-01-29 19:28:41 +0100394 mAllModelFiles = allModels;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100395 }
Jan Althausef0156d2018-01-29 19:28:41 +0100396 return mAllModelFiles;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100397 }
398
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100399 private TextClassification createClassificationResult(
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100400 TextClassifierImplNative.ClassificationResult[] classifications,
Jan Althausa1652cf2018-03-29 17:51:57 +0200401 String text, int start, int end, @Nullable Instant referenceTime) {
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000402 final String classifiedText = text.substring(start, end);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100403 final TextClassification.Builder builder = new TextClassification.Builder()
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000404 .setText(classifiedText);
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +0000405
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000406 final int size = classifications.length;
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100407 TextClassifierImplNative.ClassificationResult highestScoringResult = null;
Jan Althaus705b9e92018-01-22 18:22:29 +0100408 float highestScore = Float.MIN_VALUE;
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000409 for (int i = 0; i < size; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100410 builder.setEntityType(classifications[i].getCollection(),
411 classifications[i].getScore());
412 if (classifications[i].getScore() > highestScore) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100413 highestScoringResult = classifications[i];
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100414 highestScore = classifications[i].getScore();
Jan Althaus705b9e92018-01-22 18:22:29 +0100415 }
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000416 }
417
Jan Althaus20d346e2018-03-23 14:03:52 +0100418 boolean isPrimaryAction = true;
419 for (LabeledIntent labeledIntent : IntentFactory.create(
420 mContext, referenceTime, highestScoringResult, classifiedText)) {
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100421 final RemoteAction action = labeledIntent.asRemoteAction(mContext);
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100422 if (action == null) {
423 continue;
424 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100425 if (isPrimaryAction) {
426 // For O backwards compatibility, the first RemoteAction is also written to the
427 // legacy API fields.
428 builder.setIcon(action.getIcon().loadDrawable(mContext));
429 builder.setLabel(action.getTitle().toString());
430 builder.setIntent(labeledIntent.getIntent());
431 builder.setOnClickListener(TextClassification.createIntentOnClickListener(
432 TextClassification.createPendingIntent(mContext,
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100433 labeledIntent.getIntent(), labeledIntent.getRequestCode())));
Jan Althaus20d346e2018-03-23 14:03:52 +0100434 isPrimaryAction = false;
435 }
436 builder.addAction(action);
437 }
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100438
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100439 return builder.setId(createId(text, start, end)).build();
Jan Althaus92d76832017-09-27 18:14:35 +0200440 }
441
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800442 /**
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100443 * Closes the ParcelFileDescriptor and logs any errors that occur.
444 */
445 private static void closeAndLogError(ParcelFileDescriptor fd) {
446 try {
447 fd.close();
448 } catch (IOException e) {
449 Log.e(LOG_TAG, "Error closing file.", e);
450 }
451 }
452
453 /**
Jan Althausef0156d2018-01-29 19:28:41 +0100454 * Describes TextClassifier model files on disk.
455 */
456 private static final class ModelFile {
457
458 private final String mPath;
459 private final String mName;
460 private final int mVersion;
461 private final List<Locale> mSupportedLocales;
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100462 private final boolean mLanguageIndependent;
Jan Althausef0156d2018-01-29 19:28:41 +0100463
464 /** Returns null if the path did not point to a compatible model. */
465 static @Nullable ModelFile fromPath(String path) {
466 final File file = new File(path);
467 try {
468 final ParcelFileDescriptor modelFd = ParcelFileDescriptor.open(
469 file, ParcelFileDescriptor.MODE_READ_ONLY);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100470 final int version = TextClassifierImplNative.getVersion(modelFd.getFd());
471 final String supportedLocalesStr =
472 TextClassifierImplNative.getLocales(modelFd.getFd());
Jan Althausef0156d2018-01-29 19:28:41 +0100473 if (supportedLocalesStr.isEmpty()) {
474 Log.d(DEFAULT_LOG_TAG, "Ignoring " + file.getAbsolutePath());
475 return null;
476 }
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100477 final boolean languageIndependent = supportedLocalesStr.equals("*");
Jan Althausef0156d2018-01-29 19:28:41 +0100478 final List<Locale> supportedLocales = new ArrayList<>();
479 for (String langTag : supportedLocalesStr.split(",")) {
480 supportedLocales.add(Locale.forLanguageTag(langTag));
481 }
482 closeAndLogError(modelFd);
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100483 return new ModelFile(path, file.getName(), version, supportedLocales,
484 languageIndependent);
Jan Althausef0156d2018-01-29 19:28:41 +0100485 } catch (FileNotFoundException e) {
486 Log.e(DEFAULT_LOG_TAG, "Failed to peek " + file.getAbsolutePath(), e);
487 return null;
488 }
489 }
490
491 /** The absolute path to the model file. */
492 String getPath() {
493 return mPath;
494 }
495
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100496 /** A name to use for id generation. Effectively the name of the model file. */
Jan Althausef0156d2018-01-29 19:28:41 +0100497 String getName() {
498 return mName;
499 }
500
501 /** Returns the version tag in the model's metadata. */
502 int getVersion() {
503 return mVersion;
504 }
505
506 /** Returns whether the language supports any language in the given ranges. */
507 boolean isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges) {
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100508 return mLanguageIndependent || Locale.lookup(languageRanges, mSupportedLocales) != null;
Jan Althausef0156d2018-01-29 19:28:41 +0100509 }
510
511 /** All locales supported by the model. */
512 List<Locale> getSupportedLocales() {
513 return Collections.unmodifiableList(mSupportedLocales);
514 }
515
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100516 public boolean isPreferredTo(ModelFile model) {
517 // A model is preferred to no model.
518 if (model == null) {
519 return true;
520 }
521
522 // A language-specific model is preferred to a language independent
523 // model.
524 if (!mLanguageIndependent && model.mLanguageIndependent) {
525 return true;
526 }
527
528 // A higher-version model is preferred.
529 if (getVersion() > model.getVersion()) {
530 return true;
531 }
532 return false;
533 }
534
Jan Althausef0156d2018-01-29 19:28:41 +0100535 @Override
536 public boolean equals(Object other) {
537 if (this == other) {
538 return true;
539 } else if (other == null || !ModelFile.class.isAssignableFrom(other.getClass())) {
540 return false;
541 } else {
542 final ModelFile otherModel = (ModelFile) other;
543 return mPath.equals(otherModel.mPath);
544 }
545 }
546
547 @Override
548 public String toString() {
549 final StringJoiner localesJoiner = new StringJoiner(",");
550 for (Locale locale : mSupportedLocales) {
551 localesJoiner.add(locale.toLanguageTag());
552 }
553 return String.format(Locale.US, "ModelFile { path=%s name=%s version=%d locales=%s }",
554 mPath, mName, mVersion, localesJoiner.toString());
555 }
556
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100557 private ModelFile(String path, String name, int version, List<Locale> supportedLocales,
558 boolean languageIndependent) {
Jan Althausef0156d2018-01-29 19:28:41 +0100559 mPath = path;
560 mName = name;
561 mVersion = version;
562 mSupportedLocales = supportedLocales;
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100563 mLanguageIndependent = languageIndependent;
Jan Althausef0156d2018-01-29 19:28:41 +0100564 }
565 }
566
567 /**
Jan Althaus20d346e2018-03-23 14:03:52 +0100568 * Helper class to store the information from which RemoteActions are built.
569 */
570 private static final class LabeledIntent {
Jan Althaus20d346e2018-03-23 14:03:52 +0100571
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100572 static final int DEFAULT_REQUEST_CODE = 0;
573
574 private final String mTitle;
575 private final String mDescription;
576 private final Intent mIntent;
577 private final int mRequestCode;
578
579 /**
580 * Initializes a LabeledIntent.
581 *
582 * <p>NOTE: {@code reqestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
583 * if distinguishing info (e.g. the classified text) is represented in intent extras only.
584 * In such circumstances, the request code should represent the distinguishing info
585 * (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
586 * unique. To be correct, the PendingIntent should be definitely unique but we try a
587 * best effort approach that avoids spamming the system with PendingIntents.
588 */
589 // TODO: Fix the issue mentioned above so the behaviour is correct.
590 LabeledIntent(String title, String description, Intent intent, int requestCode) {
Jan Althaus20d346e2018-03-23 14:03:52 +0100591 mTitle = title;
592 mDescription = description;
593 mIntent = intent;
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100594 mRequestCode = requestCode;
Jan Althaus20d346e2018-03-23 14:03:52 +0100595 }
596
597 String getTitle() {
598 return mTitle;
599 }
600
601 String getDescription() {
602 return mDescription;
603 }
604
605 Intent getIntent() {
606 return mIntent;
607 }
608
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100609 int getRequestCode() {
610 return mRequestCode;
611 }
612
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100613 @Nullable
Jan Althaus20d346e2018-03-23 14:03:52 +0100614 RemoteAction asRemoteAction(Context context) {
615 final PackageManager pm = context.getPackageManager();
616 final ResolveInfo resolveInfo = pm.resolveActivity(mIntent, 0);
617 final String packageName = resolveInfo != null && resolveInfo.activityInfo != null
618 ? resolveInfo.activityInfo.packageName : null;
619 Icon icon = null;
620 boolean shouldShowIcon = false;
621 if (packageName != null && !"android".equals(packageName)) {
622 // There is a default activity handling the intent.
623 mIntent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name));
624 if (resolveInfo.activityInfo.getIconResource() != 0) {
625 icon = Icon.createWithResource(
626 packageName, resolveInfo.activityInfo.getIconResource());
627 shouldShowIcon = true;
628 }
629 }
630 if (icon == null) {
631 // RemoteAction requires that there be an icon.
632 icon = Icon.createWithResource("android",
633 com.android.internal.R.drawable.ic_more_items);
634 }
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100635 final PendingIntent pendingIntent =
636 TextClassification.createPendingIntent(context, mIntent, mRequestCode);
637 if (pendingIntent == null) {
638 return null;
639 }
640 final RemoteAction action = new RemoteAction(icon, mTitle, mDescription, pendingIntent);
Jan Althaus20d346e2018-03-23 14:03:52 +0100641 action.setShouldShowIcon(shouldShowIcon);
642 return action;
643 }
644 }
645
646 /**
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800647 * Creates intents based on the classification type.
648 */
Jan Althaus705b9e92018-01-22 18:22:29 +0100649 static final class IntentFactory {
650
651 private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
652 private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800653
654 private IntentFactory() {}
655
Jan Althaus92d76832017-09-27 18:14:35 +0200656 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100657 public static List<LabeledIntent> create(
Jan Althaus705b9e92018-01-22 18:22:29 +0100658 Context context,
Jan Althausa1652cf2018-03-29 17:51:57 +0200659 @Nullable Instant referenceTime,
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100660 TextClassifierImplNative.ClassificationResult classification,
Jan Althaus705b9e92018-01-22 18:22:29 +0100661 String text) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100662 final String type = classification.getCollection().trim().toLowerCase(Locale.ENGLISH);
Abodunrinwa Toki70d41cd2017-05-02 21:43:41 +0100663 text = text.trim();
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800664 switch (type) {
665 case TextClassifier.TYPE_EMAIL:
Jan Althaus20d346e2018-03-23 14:03:52 +0100666 return createForEmail(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100667 case TextClassifier.TYPE_PHONE:
Jan Althaus05e00512018-02-02 15:28:34 +0100668 return createForPhone(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100669 case TextClassifier.TYPE_ADDRESS:
Jan Althaus20d346e2018-03-23 14:03:52 +0100670 return createForAddress(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100671 case TextClassifier.TYPE_URL:
672 return createForUrl(context, text);
673 case TextClassifier.TYPE_DATE:
674 case TextClassifier.TYPE_DATE_TIME:
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100675 if (classification.getDatetimeResult() != null) {
Jan Althausa1652cf2018-03-29 17:51:57 +0200676 final Instant parsedTime = Instant.ofEpochMilli(
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100677 classification.getDatetimeResult().getTimeMsUtc());
Jan Althausa1652cf2018-03-29 17:51:57 +0200678 return createForDatetime(context, type, referenceTime, parsedTime);
Jan Althaus705b9e92018-01-22 18:22:29 +0100679 } else {
680 return new ArrayList<>();
681 }
682 case TextClassifier.TYPE_FLIGHT_NUMBER:
Jan Althaus20d346e2018-03-23 14:03:52 +0100683 return createForFlight(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100684 default:
685 return new ArrayList<>();
686 }
687 }
688
689 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100690 private static List<LabeledIntent> createForEmail(Context context, String text) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100691 return Arrays.asList(
Jan Althaus20d346e2018-03-23 14:03:52 +0100692 new LabeledIntent(
693 context.getString(com.android.internal.R.string.email),
694 context.getString(com.android.internal.R.string.email_desc),
695 new Intent(Intent.ACTION_SENDTO)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100696 .setData(Uri.parse(String.format("mailto:%s", text))),
697 LabeledIntent.DEFAULT_REQUEST_CODE),
Jan Althaus20d346e2018-03-23 14:03:52 +0100698 new LabeledIntent(
699 context.getString(com.android.internal.R.string.add_contact),
700 context.getString(com.android.internal.R.string.add_contact_desc),
701 new Intent(Intent.ACTION_INSERT_OR_EDIT)
702 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100703 .putExtra(ContactsContract.Intents.Insert.EMAIL, text),
704 text.hashCode()));
Jan Althaus705b9e92018-01-22 18:22:29 +0100705 }
706
707 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100708 private static List<LabeledIntent> createForPhone(Context context, String text) {
709 final List<LabeledIntent> actions = new ArrayList<>();
Jan Althaus05e00512018-02-02 15:28:34 +0100710 final UserManager userManager = context.getSystemService(UserManager.class);
711 final Bundle userRestrictions = userManager != null
712 ? userManager.getUserRestrictions() : new Bundle();
713 if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
Jan Althaus20d346e2018-03-23 14:03:52 +0100714 actions.add(new LabeledIntent(
715 context.getString(com.android.internal.R.string.dial),
716 context.getString(com.android.internal.R.string.dial_desc),
717 new Intent(Intent.ACTION_DIAL).setData(
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100718 Uri.parse(String.format("tel:%s", text))),
719 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althaus05e00512018-02-02 15:28:34 +0100720 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100721 actions.add(new LabeledIntent(
722 context.getString(com.android.internal.R.string.add_contact),
723 context.getString(com.android.internal.R.string.add_contact_desc),
724 new Intent(Intent.ACTION_INSERT_OR_EDIT)
725 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100726 .putExtra(ContactsContract.Intents.Insert.PHONE, text),
727 text.hashCode()));
Jan Althaus05e00512018-02-02 15:28:34 +0100728 if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
Jan Althaus20d346e2018-03-23 14:03:52 +0100729 actions.add(new LabeledIntent(
730 context.getString(com.android.internal.R.string.sms),
731 context.getString(com.android.internal.R.string.sms_desc),
732 new Intent(Intent.ACTION_SENDTO)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100733 .setData(Uri.parse(String.format("smsto:%s", text))),
734 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althaus05e00512018-02-02 15:28:34 +0100735 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100736 return actions;
Jan Althaus705b9e92018-01-22 18:22:29 +0100737 }
738
739 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100740 private static List<LabeledIntent> createForAddress(Context context, String text) {
741 final List<LabeledIntent> actions = new ArrayList<>();
Jan Althauseaff57e2018-02-12 12:47:27 +0100742 try {
743 final String encText = URLEncoder.encode(text, "UTF-8");
Jan Althaus20d346e2018-03-23 14:03:52 +0100744 actions.add(new LabeledIntent(
745 context.getString(com.android.internal.R.string.map),
746 context.getString(com.android.internal.R.string.map_desc),
747 new Intent(Intent.ACTION_VIEW)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100748 .setData(Uri.parse(String.format("geo:0,0?q=%s", encText))),
749 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althauseaff57e2018-02-12 12:47:27 +0100750 } catch (UnsupportedEncodingException e) {
751 Log.e(LOG_TAG, "Could not encode address", e);
752 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100753 return actions;
Jan Althaus705b9e92018-01-22 18:22:29 +0100754 }
755
756 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100757 private static List<LabeledIntent> createForUrl(Context context, String text) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100758 final String httpPrefix = "http://";
759 final String httpsPrefix = "https://";
760 if (text.toLowerCase().startsWith(httpPrefix)) {
761 text = httpPrefix + text.substring(httpPrefix.length());
762 } else if (text.toLowerCase().startsWith(httpsPrefix)) {
763 text = httpsPrefix + text.substring(httpsPrefix.length());
764 } else {
765 text = httpPrefix + text;
766 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100767 return Arrays.asList(new LabeledIntent(
768 context.getString(com.android.internal.R.string.browse),
769 context.getString(com.android.internal.R.string.browse_desc),
770 new Intent(Intent.ACTION_VIEW, Uri.parse(text))
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100771 .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()),
772 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althaus705b9e92018-01-22 18:22:29 +0100773 }
774
775 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100776 private static List<LabeledIntent> createForDatetime(
Jan Althausa1652cf2018-03-29 17:51:57 +0200777 Context context, String type, @Nullable Instant referenceTime,
778 Instant parsedTime) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100779 if (referenceTime == null) {
780 // If no reference time was given, use now.
Jan Althausa1652cf2018-03-29 17:51:57 +0200781 referenceTime = Instant.now();
Jan Althaus705b9e92018-01-22 18:22:29 +0100782 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100783 List<LabeledIntent> actions = new ArrayList<>();
Jan Althausa1652cf2018-03-29 17:51:57 +0200784 actions.add(createCalendarViewIntent(context, parsedTime));
785 final long millisUntilEvent = referenceTime.until(parsedTime, MILLIS);
786 if (millisUntilEvent > MIN_EVENT_FUTURE_MILLIS) {
787 actions.add(createCalendarCreateEventIntent(context, parsedTime, type));
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800788 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100789 return actions;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800790 }
791
Jan Althaus705b9e92018-01-22 18:22:29 +0100792 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100793 private static List<LabeledIntent> createForFlight(Context context, String text) {
794 return Arrays.asList(new LabeledIntent(
795 context.getString(com.android.internal.R.string.view_flight),
796 context.getString(com.android.internal.R.string.view_flight_desc),
797 new Intent(Intent.ACTION_WEB_SEARCH)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100798 .putExtra(SearchManager.QUERY, text),
799 text.hashCode()));
Jan Althaus705b9e92018-01-22 18:22:29 +0100800 }
801
802 @NonNull
Jan Althausa1652cf2018-03-29 17:51:57 +0200803 private static LabeledIntent createCalendarViewIntent(Context context, Instant parsedTime) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100804 Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
805 builder.appendPath("time");
Jan Althausa1652cf2018-03-29 17:51:57 +0200806 ContentUris.appendId(builder, parsedTime.toEpochMilli());
Jan Althaus20d346e2018-03-23 14:03:52 +0100807 return new LabeledIntent(
808 context.getString(com.android.internal.R.string.view_calendar),
809 context.getString(com.android.internal.R.string.view_calendar_desc),
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100810 new Intent(Intent.ACTION_VIEW).setData(builder.build()),
811 LabeledIntent.DEFAULT_REQUEST_CODE);
Jan Althaus705b9e92018-01-22 18:22:29 +0100812 }
813
814 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100815 private static LabeledIntent createCalendarCreateEventIntent(
Jan Althausa1652cf2018-03-29 17:51:57 +0200816 Context context, Instant parsedTime, @EntityType String type) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100817 final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
Jan Althaus20d346e2018-03-23 14:03:52 +0100818 return new LabeledIntent(
819 context.getString(com.android.internal.R.string.add_calendar_event),
820 context.getString(com.android.internal.R.string.add_calendar_event_desc),
821 new Intent(Intent.ACTION_INSERT)
822 .setData(CalendarContract.Events.CONTENT_URI)
823 .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
824 .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,
Jan Althausa1652cf2018-03-29 17:51:57 +0200825 parsedTime.toEpochMilli())
Jan Althaus20d346e2018-03-23 14:03:52 +0100826 .putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100827 parsedTime.toEpochMilli() + DEFAULT_EVENT_DURATION),
828 parsedTime.hashCode());
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800829 }
830 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800831}