blob: f434452bb9ecbc5d5519f52a4f0d5ff38ecf6ffa [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
19import android.annotation.NonNull;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080020import android.annotation.Nullable;
Jan Althaus705b9e92018-01-22 18:22:29 +010021import android.app.SearchManager;
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +000022import android.content.ComponentName;
Jan Althaus705b9e92018-01-22 18:22:29 +010023import android.content.ContentUris;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080024import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.graphics.drawable.Drawable;
29import android.net.Uri;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000030import android.os.LocaleList;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080031import android.os.ParcelFileDescriptor;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000032import android.provider.Browser;
Jan Althaus705b9e92018-01-22 18:22:29 +010033import android.provider.CalendarContract;
Jan Althaus92d76832017-09-27 18:14:35 +020034import android.provider.ContactsContract;
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +010035import android.provider.Settings;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080036import android.text.util.Linkify;
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +000037import android.util.Patterns;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080038
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010039import com.android.internal.annotations.GuardedBy;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010040import com.android.internal.logging.MetricsLogger;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080041import com.android.internal.util.Preconditions;
42
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010043import java.io.File;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080044import java.io.FileNotFoundException;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010045import java.io.IOException;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080046import java.util.ArrayList;
Richard Ledleydb18a572017-11-30 17:33:51 +000047import java.util.Arrays;
Jan Althaus705b9e92018-01-22 18:22:29 +010048import java.util.Calendar;
Richard Ledleydb18a572017-11-30 17:33:51 +000049import java.util.Collection;
50import java.util.Collections;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010051import java.util.HashMap;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080052import java.util.List;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000053import java.util.Locale;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080054import java.util.Map;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010055import java.util.Objects;
Jan Althaus705b9e92018-01-22 18:22:29 +010056import java.util.concurrent.TimeUnit;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010057import java.util.regex.Matcher;
58import java.util.regex.Pattern;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080059
60/**
61 * Default implementation of the {@link TextClassifier} interface.
62 *
63 * <p>This class uses machine learning to recognize entities in text.
64 * Unless otherwise stated, methods of this class are blocking operations and should most
65 * likely not be called on the UI thread.
66 *
67 * @hide
68 */
69final class TextClassifierImpl implements TextClassifier {
70
Abodunrinwa Toki692b1962017-08-15 15:05:11 +010071 private static final String LOG_TAG = DEFAULT_LOG_TAG;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010072 private static final String MODEL_DIR = "/etc/textclassifier/";
73 private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model";
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010074 private static final String UPDATED_MODEL_FILE_PATH =
75 "/data/misc/textclassifier/textclassifier.smartselection.model";
Richard Ledleydb18a572017-11-30 17:33:51 +000076 private static final List<String> ENTITY_TYPES_ALL =
77 Collections.unmodifiableList(Arrays.asList(
78 TextClassifier.TYPE_ADDRESS,
79 TextClassifier.TYPE_EMAIL,
80 TextClassifier.TYPE_PHONE,
Jan Althaus705b9e92018-01-22 18:22:29 +010081 TextClassifier.TYPE_URL,
82 TextClassifier.TYPE_DATE,
83 TextClassifier.TYPE_DATE_TIME,
84 TextClassifier.TYPE_FLIGHT_NUMBER));
Richard Ledleydb18a572017-11-30 17:33:51 +000085 private static final List<String> ENTITY_TYPES_BASE =
86 Collections.unmodifiableList(Arrays.asList(
87 TextClassifier.TYPE_ADDRESS,
88 TextClassifier.TYPE_EMAIL,
89 TextClassifier.TYPE_PHONE,
90 TextClassifier.TYPE_URL));
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +000091
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080092 private final Context mContext;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010093
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010094 private final MetricsLogger mMetricsLogger = new MetricsLogger();
95
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010096 private final Object mSmartSelectionLock = new Object();
97 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
98 private Map<Locale, String> mModelFilePaths;
99 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
100 private Locale mLocale;
101 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100102 private int mVersion;
103 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800104 private SmartSelection mSmartSelection;
105
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100106 private TextClassifierConstants mSettings;
107
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100108 TextClassifierImpl(Context context) {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800109 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800110 }
111
112 @Override
113 public TextSelection suggestSelection(
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000114 @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
Abodunrinwa Toki4d232d62017-11-23 12:22:45 +0000115 @NonNull TextSelection.Options options) {
116 Utils.validateInput(text, selectionStartIndex, selectionEndIndex);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800117 try {
118 if (text.length() > 0) {
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100119 final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
120 final SmartSelection smartSelection = getSmartSelection(locales);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800121 final String string = text.toString();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100122 final int start;
123 final int end;
124 if (getSettings().isDarkLaunch() && !options.isDarkLaunchAllowed()) {
125 start = selectionStartIndex;
126 end = selectionEndIndex;
127 } else {
128 final int[] startEnd = smartSelection.suggest(
129 string, selectionStartIndex, selectionEndIndex);
130 start = startEnd[0];
131 end = startEnd[1];
132 }
Abodunrinwa Tokib4162972017-05-05 18:07:17 +0100133 if (start <= end
134 && start >= 0 && end <= string.length()
135 && start <= selectionStartIndex && end >= selectionEndIndex) {
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100136 final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000137 final SmartSelection.ClassificationResult[] results =
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100138 smartSelection.classifyText(
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000139 string, start, end,
140 getHintFlags(string, start, end));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000141 final int size = results.length;
142 for (int i = 0; i < size; i++) {
143 tsBuilder.setEntityType(results[i].mCollection, results[i].mScore);
144 }
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100145 return tsBuilder
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000146 .setSignature(
147 getSignature(string, selectionStartIndex, selectionEndIndex))
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100148 .build();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800149 } else {
150 // We can not trust the result. Log the issue and ignore the result.
151 Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
152 }
153 }
154 } catch (Throwable t) {
155 // Avoid throwing from this method. Log the error.
156 Log.e(LOG_TAG,
157 "Error suggesting selection for text. No changes to selection suggested.",
158 t);
159 }
160 // Getting here means something went wrong, return a NO_OP result.
161 return TextClassifier.NO_OP.suggestSelection(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100162 text, selectionStartIndex, selectionEndIndex, options);
163 }
164
165 @Override
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100166 public TextClassification classifyText(
Abodunrinwa Tokia2df6e52017-04-13 09:56:52 +0100167 @NonNull CharSequence text, int startIndex, int endIndex,
Abodunrinwa Toki4d232d62017-11-23 12:22:45 +0000168 @NonNull TextClassification.Options options) {
169 Utils.validateInput(text, startIndex, endIndex);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800170 try {
171 if (text.length() > 0) {
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000172 final String string = text.toString();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100173 final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
174 final SmartSelection.ClassificationResult[] results = getSmartSelection(locales)
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000175 .classifyText(string, startIndex, endIndex,
176 getHintFlags(string, startIndex, endIndex));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000177 if (results.length > 0) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100178 return createClassificationResult(
179 results, string, startIndex, endIndex, options.getReferenceTime());
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800180 }
181 }
182 } catch (Throwable t) {
183 // Avoid throwing from this method. Log the error.
Abodunrinwa Toki78618852017-10-17 15:31:39 +0100184 Log.e(LOG_TAG, "Error getting text classification info.", t);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800185 }
186 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100187 return TextClassifier.NO_OP.classifyText(text, startIndex, endIndex, options);
188 }
189
190 @Override
Richard Ledley68d94522017-10-05 10:52:19 +0100191 public TextLinks generateLinks(
Richard Ledleydb18a572017-11-30 17:33:51 +0000192 @NonNull CharSequence text, @Nullable TextLinks.Options options) {
Abodunrinwa Toki4d232d62017-11-23 12:22:45 +0000193 Utils.validateInput(text);
Richard Ledley68d94522017-10-05 10:52:19 +0100194 final String textString = text.toString();
195 final TextLinks.Builder builder = new TextLinks.Builder(textString);
Richard Ledley9cfa6062018-01-15 13:13:29 +0000196
197 if (!getSettings().isSmartLinkifyEnabled()) {
198 return builder.build();
199 }
200
Richard Ledley68d94522017-10-05 10:52:19 +0100201 try {
Richard Ledleydb18a572017-11-30 17:33:51 +0000202 final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;
203 final Collection<String> entitiesToIdentify =
204 options != null && options.getEntityConfig() != null
205 ? options.getEntityConfig().getEntities(this) : ENTITY_TYPES_ALL;
Richard Ledley68d94522017-10-05 10:52:19 +0100206 final SmartSelection smartSelection = getSmartSelection(defaultLocales);
207 final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString);
208 for (SmartSelection.AnnotatedSpan span : annotations) {
Richard Ledley68d94522017-10-05 10:52:19 +0100209 final SmartSelection.ClassificationResult[] results = span.getClassification();
Richard Ledleydb18a572017-11-30 17:33:51 +0000210 if (results.length == 0 || !entitiesToIdentify.contains(results[0].mCollection)) {
211 continue;
212 }
213 final Map<String, Float> entityScores = new HashMap<>();
Richard Ledley68d94522017-10-05 10:52:19 +0100214 for (int i = 0; i < results.length; i++) {
215 entityScores.put(results[i].mCollection, results[i].mScore);
216 }
217 builder.addLink(new TextLinks.TextLink(
218 textString, span.getStartIndex(), span.getEndIndex(), entityScores));
219 }
220 } catch (Throwable t) {
221 // Avoid throwing from this method. Log the error.
222 Log.e(LOG_TAG, "Error getting links info.", t);
223 }
224 return builder.build();
225 }
226
227 @Override
Richard Ledleydb18a572017-11-30 17:33:51 +0000228 public Collection<String> getEntitiesForPreset(@TextClassifier.EntityPreset int entityPreset) {
229 switch (entityPreset) {
230 case TextClassifier.ENTITY_PRESET_NONE:
231 return Collections.emptyList();
232 case TextClassifier.ENTITY_PRESET_BASE:
233 return ENTITY_TYPES_BASE;
234 case TextClassifier.ENTITY_PRESET_ALL:
235 // fall through
236 default:
237 return ENTITY_TYPES_ALL;
238 }
239 }
240
241 @Override
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100242 public void logEvent(String source, String event) {
243 if (LOG_TAG.equals(source)) {
244 mMetricsLogger.count(event, 1);
245 }
246 }
247
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100248 @Override
249 public TextClassifierConstants getSettings() {
250 if (mSettings == null) {
251 mSettings = TextClassifierConstants.loadFromString(Settings.Global.getString(
252 mContext.getContentResolver(), Settings.Global.TEXT_CLASSIFIER_CONSTANTS));
253 }
254 return mSettings;
255 }
256
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100257 private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException {
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000258 synchronized (mSmartSelectionLock) {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100259 localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
260 final Locale locale = findBestSupportedLocaleLocked(localeList);
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100261 if (locale == null) {
262 throw new FileNotFoundException("No file for null locale");
263 }
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100264 if (mSmartSelection == null || !Objects.equals(mLocale, locale)) {
265 destroySmartSelectionIfExistsLocked();
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100266 final ParcelFileDescriptor fd = getFdLocked(locale);
Jan Althause750c6c2017-11-13 12:07:04 +0100267 final int modelFd = fd.getFd();
268 mVersion = SmartSelection.getVersion(modelFd);
269 mSmartSelection = new SmartSelection(modelFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100270 closeAndLogError(fd);
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100271 mLocale = locale;
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000272 }
273 return mSmartSelection;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800274 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800275 }
276
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000277 private String getSignature(String text, int start, int end) {
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100278 synchronized (mSmartSelectionLock) {
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000279 final String versionInfo = (mLocale != null)
280 ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion)
281 : "";
282 final int hash = Objects.hash(text, start, end, mContext.getPackageName());
283 return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash);
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100284 }
285 }
286
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100287 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100288 private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException {
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100289 ParcelFileDescriptor updateFd;
Jan Althause750c6c2017-11-13 12:07:04 +0100290 int updateVersion = -1;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100291 try {
292 updateFd = ParcelFileDescriptor.open(
293 new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
Jan Althause750c6c2017-11-13 12:07:04 +0100294 if (updateFd != null) {
295 updateVersion = SmartSelection.getVersion(updateFd.getFd());
296 }
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100297 } catch (FileNotFoundException e) {
298 updateFd = null;
299 }
300 ParcelFileDescriptor factoryFd;
Jan Althause750c6c2017-11-13 12:07:04 +0100301 int factoryVersion = -1;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100302 try {
303 final String factoryModelFilePath = getFactoryModelFilePathsLocked().get(locale);
304 if (factoryModelFilePath != null) {
305 factoryFd = ParcelFileDescriptor.open(
306 new File(factoryModelFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
Jan Althause750c6c2017-11-13 12:07:04 +0100307 if (factoryFd != null) {
308 factoryVersion = SmartSelection.getVersion(factoryFd.getFd());
309 }
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100310 } else {
311 factoryFd = null;
312 }
313 } catch (FileNotFoundException e) {
314 factoryFd = null;
315 }
316
317 if (updateFd == null) {
318 if (factoryFd != null) {
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100319 return factoryFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100320 } else {
321 throw new FileNotFoundException(
322 String.format("No model file found for %s", locale));
323 }
324 }
325
326 final int updateFdInt = updateFd.getFd();
327 final boolean localeMatches = Objects.equals(
328 locale.getLanguage().trim().toLowerCase(),
329 SmartSelection.getLanguage(updateFdInt).trim().toLowerCase());
330 if (factoryFd == null) {
331 if (localeMatches) {
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100332 return updateFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100333 } else {
334 closeAndLogError(updateFd);
335 throw new FileNotFoundException(
336 String.format("No model file found for %s", locale));
337 }
338 }
339
340 if (!localeMatches) {
341 closeAndLogError(updateFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100342 return factoryFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100343 }
344
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100345 if (updateVersion > factoryVersion) {
346 closeAndLogError(factoryFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100347 return updateFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100348 } else {
349 closeAndLogError(updateFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100350 return factoryFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100351 }
352 }
353
354 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100355 private void destroySmartSelectionIfExistsLocked() {
356 if (mSmartSelection != null) {
357 mSmartSelection.close();
358 mSmartSelection = null;
359 }
360 }
361
362 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
363 @Nullable
364 private Locale findBestSupportedLocaleLocked(LocaleList localeList) {
Abodunrinwa Tokia2df6e52017-04-13 09:56:52 +0100365 // Specified localeList takes priority over the system default, so it is listed first.
366 final String languages = localeList.isEmpty()
367 ? LocaleList.getDefault().toLanguageTags()
368 : localeList.toLanguageTags() + "," + LocaleList.getDefault().toLanguageTags();
369 final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100370
371 final List<Locale> supportedLocales =
372 new ArrayList<>(getFactoryModelFilePathsLocked().keySet());
373 final Locale updatedModelLocale = getUpdatedModelLocale();
374 if (updatedModelLocale != null) {
375 supportedLocales.add(updatedModelLocale);
376 }
377 return Locale.lookup(languageRangeList, supportedLocales);
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100378 }
379
380 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100381 private Map<Locale, String> getFactoryModelFilePathsLocked() {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100382 if (mModelFilePaths == null) {
383 final Map<Locale, String> modelFilePaths = new HashMap<>();
384 final File modelsDir = new File(MODEL_DIR);
385 if (modelsDir.exists() && modelsDir.isDirectory()) {
386 final File[] models = modelsDir.listFiles();
387 final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX);
388 final int size = models.length;
389 for (int i = 0; i < size; i++) {
390 final File modelFile = models[i];
391 final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName());
392 if (matcher.matches() && modelFile.isFile()) {
393 final String language = matcher.group(1);
394 final Locale locale = Locale.forLanguageTag(language);
395 modelFilePaths.put(locale, modelFile.getAbsolutePath());
396 }
397 }
398 }
399 mModelFilePaths = modelFilePaths;
400 }
401 return mModelFilePaths;
402 }
403
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100404 @Nullable
405 private Locale getUpdatedModelLocale() {
406 try {
407 final ParcelFileDescriptor updateFd = ParcelFileDescriptor.open(
408 new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
409 final Locale locale = Locale.forLanguageTag(
410 SmartSelection.getLanguage(updateFd.getFd()));
411 closeAndLogError(updateFd);
412 return locale;
413 } catch (FileNotFoundException e) {
414 return null;
415 }
416 }
417
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100418 private TextClassification createClassificationResult(
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000419 SmartSelection.ClassificationResult[] classifications,
Jan Althaus705b9e92018-01-22 18:22:29 +0100420 String text, int start, int end, @Nullable Calendar referenceTime) {
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000421 final String classifiedText = text.substring(start, end);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100422 final TextClassification.Builder builder = new TextClassification.Builder()
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000423 .setText(classifiedText);
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +0000424
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000425 final int size = classifications.length;
Jan Althaus705b9e92018-01-22 18:22:29 +0100426 SmartSelection.ClassificationResult highestScoringResult = null;
427 float highestScore = Float.MIN_VALUE;
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000428 for (int i = 0; i < size; i++) {
429 builder.setEntityType(classifications[i].mCollection, classifications[i].mScore);
Jan Althaus705b9e92018-01-22 18:22:29 +0100430 if (classifications[i].mScore > highestScore) {
431 highestScoringResult = classifications[i];
432 highestScore = classifications[i].mScore;
433 }
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000434 }
435
Jan Althaus705b9e92018-01-22 18:22:29 +0100436 addActions(builder, IntentFactory.create(
437 mContext, referenceTime, highestScoringResult, classifiedText));
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100438
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000439 return builder.setSignature(getSignature(text, start, end)).build();
Jan Althaus92d76832017-09-27 18:14:35 +0200440 }
441
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000442 /** Extends the classification with the intents that can be resolved. */
443 private void addActions(
444 TextClassification.Builder builder, List<Intent> intents) {
445 final PackageManager pm = mContext.getPackageManager();
446 final int size = intents.size();
447 for (int i = 0; i < size; i++) {
448 final Intent intent = intents.get(i);
449 final ResolveInfo resolveInfo;
450 if (intent != null) {
451 resolveInfo = pm.resolveActivity(intent, 0);
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000452 } else {
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000453 resolveInfo = null;
454 }
455 if (resolveInfo != null && resolveInfo.activityInfo != null) {
456 final String packageName = resolveInfo.activityInfo.packageName;
Jan Althaus705b9e92018-01-22 18:22:29 +0100457 final String label = IntentFactory.getLabel(mContext, intent);
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000458 Drawable icon;
459 if ("android".equals(packageName)) {
460 // Requires the chooser to find an activity to handle the intent.
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000461 icon = null;
462 } else {
463 // A default activity will handle the intent.
464 intent.setComponent(
465 new ComponentName(packageName, resolveInfo.activityInfo.name));
466 icon = resolveInfo.activityInfo.loadIcon(pm);
467 if (icon == null) {
468 icon = resolveInfo.loadIcon(pm);
469 }
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000470 }
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000471 if (i == 0) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100472 builder.setPrimaryAction(intent, label, icon);
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000473 } else {
Jan Althaus705b9e92018-01-22 18:22:29 +0100474 builder.addSecondaryAction(intent, label, icon);
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000475 }
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000476 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800477 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800478 }
479
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000480 private static int getHintFlags(CharSequence text, int start, int end) {
481 int flag = 0;
482 final CharSequence subText = text.subSequence(start, end);
483 if (Patterns.AUTOLINK_EMAIL_ADDRESS.matcher(subText).matches()) {
484 flag |= SmartSelection.HINT_FLAG_EMAIL;
485 }
486 if (Patterns.AUTOLINK_WEB_URL.matcher(subText).matches()
487 && Linkify.sUrlMatchFilter.acceptMatch(text, start, end)) {
488 flag |= SmartSelection.HINT_FLAG_URL;
489 }
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000490 return flag;
491 }
492
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800493 /**
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100494 * Closes the ParcelFileDescriptor and logs any errors that occur.
495 */
496 private static void closeAndLogError(ParcelFileDescriptor fd) {
497 try {
498 fd.close();
499 } catch (IOException e) {
500 Log.e(LOG_TAG, "Error closing file.", e);
501 }
502 }
503
504 /**
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800505 * Creates intents based on the classification type.
506 */
Jan Althaus705b9e92018-01-22 18:22:29 +0100507 static final class IntentFactory {
508
509 private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
510 private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800511
512 private IntentFactory() {}
513
Jan Althaus92d76832017-09-27 18:14:35 +0200514 @NonNull
Jan Althaus705b9e92018-01-22 18:22:29 +0100515 public static List<Intent> create(
516 Context context,
517 @Nullable Calendar referenceTime,
518 SmartSelection.ClassificationResult classification,
519 String text) {
520 final String type = classification.mCollection.trim().toLowerCase(Locale.ENGLISH);
Abodunrinwa Toki70d41cd2017-05-02 21:43:41 +0100521 text = text.trim();
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800522 switch (type) {
523 case TextClassifier.TYPE_EMAIL:
Jan Althaus705b9e92018-01-22 18:22:29 +0100524 return createForEmail(text);
525 case TextClassifier.TYPE_PHONE:
526 return createForPhone(text);
527 case TextClassifier.TYPE_ADDRESS:
528 return createForAddress(text);
529 case TextClassifier.TYPE_URL:
530 return createForUrl(context, text);
531 case TextClassifier.TYPE_DATE:
532 case TextClassifier.TYPE_DATE_TIME:
533 if (classification.mDatetime != null) {
534 Calendar eventTime = Calendar.getInstance();
535 eventTime.setTimeInMillis(classification.mDatetime.mMsSinceEpoch);
536 return createForDatetime(type, referenceTime, eventTime);
537 } else {
538 return new ArrayList<>();
539 }
540 case TextClassifier.TYPE_FLIGHT_NUMBER:
541 return createForFlight(text);
542 default:
543 return new ArrayList<>();
544 }
545 }
546
547 @NonNull
548 private static List<Intent> createForEmail(String text) {
549 return Arrays.asList(
550 new Intent(Intent.ACTION_SENDTO)
551 .setData(Uri.parse(String.format("mailto:%s", text))),
552 new Intent(Intent.ACTION_INSERT_OR_EDIT)
Richard Ledley68d94522017-10-05 10:52:19 +0100553 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
554 .putExtra(ContactsContract.Intents.Insert.EMAIL, text));
Jan Althaus705b9e92018-01-22 18:22:29 +0100555 }
556
557 @NonNull
558 private static List<Intent> createForPhone(String text) {
559 return Arrays.asList(
560 new Intent(Intent.ACTION_DIAL)
561 .setData(Uri.parse(String.format("tel:%s", text))),
562 new Intent(Intent.ACTION_INSERT_OR_EDIT)
Jan Althaus92d76832017-09-27 18:14:35 +0200563 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
Jan Althaus705b9e92018-01-22 18:22:29 +0100564 .putExtra(ContactsContract.Intents.Insert.PHONE, text),
565 new Intent(Intent.ACTION_SENDTO)
Jan Althaus92d76832017-09-27 18:14:35 +0200566 .setData(Uri.parse(String.format("smsto:%s", text))));
Jan Althaus705b9e92018-01-22 18:22:29 +0100567 }
568
569 @NonNull
570 private static List<Intent> createForAddress(String text) {
571 return Arrays.asList(new Intent(Intent.ACTION_VIEW)
572 .setData(Uri.parse(String.format("geo:0,0?q=%s", text))));
573 }
574
575 @NonNull
576 private static List<Intent> createForUrl(Context context, String text) {
577 final String httpPrefix = "http://";
578 final String httpsPrefix = "https://";
579 if (text.toLowerCase().startsWith(httpPrefix)) {
580 text = httpPrefix + text.substring(httpPrefix.length());
581 } else if (text.toLowerCase().startsWith(httpsPrefix)) {
582 text = httpsPrefix + text.substring(httpsPrefix.length());
583 } else {
584 text = httpPrefix + text;
585 }
586 return Arrays.asList(new Intent(Intent.ACTION_VIEW, Uri.parse(text))
587 .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()));
588 }
589
590 @NonNull
591 private static List<Intent> createForDatetime(
592 String type, @Nullable Calendar referenceTime, Calendar eventTime) {
593 if (referenceTime == null) {
594 // If no reference time was given, use now.
595 referenceTime = Calendar.getInstance();
596 }
597 List<Intent> intents = new ArrayList<>();
598 intents.add(createCalendarViewIntent(eventTime));
599 final long millisSinceReference =
600 eventTime.getTimeInMillis() - referenceTime.getTimeInMillis();
601 if (millisSinceReference > MIN_EVENT_FUTURE_MILLIS) {
602 intents.add(createCalendarCreateEventIntent(eventTime, type));
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800603 }
Jan Althaus92d76832017-09-27 18:14:35 +0200604 return intents;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800605 }
606
Jan Althaus705b9e92018-01-22 18:22:29 +0100607 @NonNull
608 private static List<Intent> createForFlight(String text) {
609 return Arrays.asList(new Intent(Intent.ACTION_WEB_SEARCH)
610 .putExtra(SearchManager.QUERY, text));
611 }
612
613 @NonNull
614 private static Intent createCalendarViewIntent(Calendar eventTime) {
615 Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
616 builder.appendPath("time");
617 ContentUris.appendId(builder, eventTime.getTimeInMillis());
618 return new Intent(Intent.ACTION_VIEW).setData(builder.build());
619 }
620
621 @NonNull
622 private static Intent createCalendarCreateEventIntent(
623 Calendar eventTime, @EntityType String type) {
624 final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
625 return new Intent(Intent.ACTION_INSERT)
626 .setData(CalendarContract.Events.CONTENT_URI)
627 .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
628 .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, eventTime.getTimeInMillis())
629 .putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
630 eventTime.getTimeInMillis() + DEFAULT_EVENT_DURATION);
631 }
632
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800633 @Nullable
Jan Althaus92d76832017-09-27 18:14:35 +0200634 public static String getLabel(Context context, @Nullable Intent intent) {
635 if (intent == null || intent.getAction() == null) {
636 return null;
637 }
Jan Althaus705b9e92018-01-22 18:22:29 +0100638 final String authority =
639 intent.getData() == null ? null : intent.getData().getAuthority();
Jan Althaus92d76832017-09-27 18:14:35 +0200640 switch (intent.getAction()) {
641 case Intent.ACTION_DIAL:
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800642 return context.getString(com.android.internal.R.string.dial);
Jan Althaus92d76832017-09-27 18:14:35 +0200643 case Intent.ACTION_SENDTO:
644 switch (intent.getScheme()) {
645 case "mailto":
646 return context.getString(com.android.internal.R.string.email);
647 case "smsto":
648 return context.getString(com.android.internal.R.string.sms);
649 default:
650 return null;
651 }
Jan Althaus705b9e92018-01-22 18:22:29 +0100652 case Intent.ACTION_INSERT:
653 if (CalendarContract.AUTHORITY.equals(authority)) {
654 return context.getString(com.android.internal.R.string.add_calendar_event);
655 }
656 return null;
Jan Althaus92d76832017-09-27 18:14:35 +0200657 case Intent.ACTION_INSERT_OR_EDIT:
658 switch (intent.getDataString()) {
659 case ContactsContract.Contacts.CONTENT_ITEM_TYPE:
660 return context.getString(com.android.internal.R.string.add_contact);
661 default:
662 return null;
663 }
664 case Intent.ACTION_VIEW:
Jan Althaus705b9e92018-01-22 18:22:29 +0100665 if (CalendarContract.AUTHORITY.equals(authority)) {
666 return context.getString(com.android.internal.R.string.view_calendar);
667 }
Jan Althaus92d76832017-09-27 18:14:35 +0200668 switch (intent.getScheme()) {
669 case "geo":
670 return context.getString(com.android.internal.R.string.map);
671 case "http": // fall through
672 case "https":
673 return context.getString(com.android.internal.R.string.browse);
674 default:
675 return null;
676 }
Jan Althaus705b9e92018-01-22 18:22:29 +0100677 case Intent.ACTION_WEB_SEARCH:
678 return context.getString(com.android.internal.R.string.view_flight);
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800679 default:
680 return null;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800681 }
682 }
683 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800684}