blob: 7db0e76d901f714d70b760dd875d56773a0ed60c [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;
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +000021import android.content.ComponentName;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080022import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.graphics.drawable.Drawable;
27import android.net.Uri;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000028import android.os.LocaleList;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080029import android.os.ParcelFileDescriptor;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000030import android.provider.Browser;
Jan Althaus92d76832017-09-27 18:14:35 +020031import android.provider.ContactsContract;
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +010032import android.provider.Settings;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080033import android.text.util.Linkify;
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +000034import android.util.Patterns;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080035
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010036import com.android.internal.annotations.GuardedBy;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010037import com.android.internal.logging.MetricsLogger;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080038import com.android.internal.util.Preconditions;
39
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010040import java.io.File;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080041import java.io.FileNotFoundException;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010042import java.io.IOException;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080043import java.util.ArrayList;
Richard Ledleydb18a572017-11-30 17:33:51 +000044import java.util.Arrays;
45import java.util.Collection;
46import java.util.Collections;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010047import java.util.HashMap;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080048import java.util.List;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000049import java.util.Locale;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080050import java.util.Map;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010051import java.util.Objects;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010052import java.util.regex.Matcher;
53import java.util.regex.Pattern;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080054
55/**
56 * Default implementation of the {@link TextClassifier} interface.
57 *
58 * <p>This class uses machine learning to recognize entities in text.
59 * Unless otherwise stated, methods of this class are blocking operations and should most
60 * likely not be called on the UI thread.
61 *
62 * @hide
63 */
64final class TextClassifierImpl implements TextClassifier {
65
Abodunrinwa Toki692b1962017-08-15 15:05:11 +010066 private static final String LOG_TAG = DEFAULT_LOG_TAG;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010067 private static final String MODEL_DIR = "/etc/textclassifier/";
68 private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model";
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010069 private static final String UPDATED_MODEL_FILE_PATH =
70 "/data/misc/textclassifier/textclassifier.smartselection.model";
Richard Ledleydb18a572017-11-30 17:33:51 +000071 private static final List<String> ENTITY_TYPES_ALL =
72 Collections.unmodifiableList(Arrays.asList(
73 TextClassifier.TYPE_ADDRESS,
74 TextClassifier.TYPE_EMAIL,
75 TextClassifier.TYPE_PHONE,
76 TextClassifier.TYPE_URL));
77 private static final List<String> ENTITY_TYPES_BASE =
78 Collections.unmodifiableList(Arrays.asList(
79 TextClassifier.TYPE_ADDRESS,
80 TextClassifier.TYPE_EMAIL,
81 TextClassifier.TYPE_PHONE,
82 TextClassifier.TYPE_URL));
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +000083
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080084 private final Context mContext;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010085
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010086 private final MetricsLogger mMetricsLogger = new MetricsLogger();
87
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010088 private final Object mSmartSelectionLock = new Object();
89 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
90 private Map<Locale, String> mModelFilePaths;
91 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
92 private Locale mLocale;
93 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
Abodunrinwa Toki692b1962017-08-15 15:05:11 +010094 private int mVersion;
95 @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080096 private SmartSelection mSmartSelection;
97
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +010098 private TextClassifierConstants mSettings;
99
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100100 TextClassifierImpl(Context context) {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800101 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800102 }
103
104 @Override
105 public TextSelection suggestSelection(
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000106 @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
Abodunrinwa Toki4d232d62017-11-23 12:22:45 +0000107 @NonNull TextSelection.Options options) {
108 Utils.validateInput(text, selectionStartIndex, selectionEndIndex);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800109 try {
110 if (text.length() > 0) {
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100111 final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
112 final SmartSelection smartSelection = getSmartSelection(locales);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800113 final String string = text.toString();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100114 final int start;
115 final int end;
116 if (getSettings().isDarkLaunch() && !options.isDarkLaunchAllowed()) {
117 start = selectionStartIndex;
118 end = selectionEndIndex;
119 } else {
120 final int[] startEnd = smartSelection.suggest(
121 string, selectionStartIndex, selectionEndIndex);
122 start = startEnd[0];
123 end = startEnd[1];
124 }
Abodunrinwa Tokib4162972017-05-05 18:07:17 +0100125 if (start <= end
126 && start >= 0 && end <= string.length()
127 && start <= selectionStartIndex && end >= selectionEndIndex) {
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100128 final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000129 final SmartSelection.ClassificationResult[] results =
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100130 smartSelection.classifyText(
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000131 string, start, end,
132 getHintFlags(string, start, end));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000133 final int size = results.length;
134 for (int i = 0; i < size; i++) {
135 tsBuilder.setEntityType(results[i].mCollection, results[i].mScore);
136 }
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100137 return tsBuilder
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000138 .setSignature(
139 getSignature(string, selectionStartIndex, selectionEndIndex))
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100140 .build();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800141 } else {
142 // We can not trust the result. Log the issue and ignore the result.
143 Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
144 }
145 }
146 } catch (Throwable t) {
147 // Avoid throwing from this method. Log the error.
148 Log.e(LOG_TAG,
149 "Error suggesting selection for text. No changes to selection suggested.",
150 t);
151 }
152 // Getting here means something went wrong, return a NO_OP result.
153 return TextClassifier.NO_OP.suggestSelection(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100154 text, selectionStartIndex, selectionEndIndex, options);
155 }
156
157 @Override
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100158 public TextClassification classifyText(
Abodunrinwa Tokia2df6e52017-04-13 09:56:52 +0100159 @NonNull CharSequence text, int startIndex, int endIndex,
Abodunrinwa Toki4d232d62017-11-23 12:22:45 +0000160 @NonNull TextClassification.Options options) {
161 Utils.validateInput(text, startIndex, endIndex);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800162 try {
163 if (text.length() > 0) {
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000164 final String string = text.toString();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100165 final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
166 final SmartSelection.ClassificationResult[] results = getSmartSelection(locales)
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000167 .classifyText(string, startIndex, endIndex,
168 getHintFlags(string, startIndex, endIndex));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000169 if (results.length > 0) {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100170 final TextClassification classificationResult =
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000171 createClassificationResult(results, string, startIndex, endIndex);
Abodunrinwa Toki9841ea02017-03-22 20:11:35 +0000172 return classificationResult;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800173 }
174 }
175 } catch (Throwable t) {
176 // Avoid throwing from this method. Log the error.
Abodunrinwa Toki78618852017-10-17 15:31:39 +0100177 Log.e(LOG_TAG, "Error getting text classification info.", t);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800178 }
179 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100180 return TextClassifier.NO_OP.classifyText(text, startIndex, endIndex, options);
181 }
182
183 @Override
Richard Ledley68d94522017-10-05 10:52:19 +0100184 public TextLinks generateLinks(
Richard Ledleydb18a572017-11-30 17:33:51 +0000185 @NonNull CharSequence text, @Nullable TextLinks.Options options) {
Abodunrinwa Toki4d232d62017-11-23 12:22:45 +0000186 Utils.validateInput(text);
Richard Ledley68d94522017-10-05 10:52:19 +0100187 final String textString = text.toString();
188 final TextLinks.Builder builder = new TextLinks.Builder(textString);
Richard Ledley9cfa6062018-01-15 13:13:29 +0000189
190 if (!getSettings().isSmartLinkifyEnabled()) {
191 return builder.build();
192 }
193
Richard Ledley68d94522017-10-05 10:52:19 +0100194 try {
Richard Ledleydb18a572017-11-30 17:33:51 +0000195 final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;
196 final Collection<String> entitiesToIdentify =
197 options != null && options.getEntityConfig() != null
198 ? options.getEntityConfig().getEntities(this) : ENTITY_TYPES_ALL;
Richard Ledley68d94522017-10-05 10:52:19 +0100199 final SmartSelection smartSelection = getSmartSelection(defaultLocales);
200 final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString);
201 for (SmartSelection.AnnotatedSpan span : annotations) {
Richard Ledley68d94522017-10-05 10:52:19 +0100202 final SmartSelection.ClassificationResult[] results = span.getClassification();
Richard Ledleydb18a572017-11-30 17:33:51 +0000203 if (results.length == 0 || !entitiesToIdentify.contains(results[0].mCollection)) {
204 continue;
205 }
206 final Map<String, Float> entityScores = new HashMap<>();
Richard Ledley68d94522017-10-05 10:52:19 +0100207 for (int i = 0; i < results.length; i++) {
208 entityScores.put(results[i].mCollection, results[i].mScore);
209 }
210 builder.addLink(new TextLinks.TextLink(
211 textString, span.getStartIndex(), span.getEndIndex(), entityScores));
212 }
213 } catch (Throwable t) {
214 // Avoid throwing from this method. Log the error.
215 Log.e(LOG_TAG, "Error getting links info.", t);
216 }
217 return builder.build();
218 }
219
220 @Override
Richard Ledleydb18a572017-11-30 17:33:51 +0000221 public Collection<String> getEntitiesForPreset(@TextClassifier.EntityPreset int entityPreset) {
222 switch (entityPreset) {
223 case TextClassifier.ENTITY_PRESET_NONE:
224 return Collections.emptyList();
225 case TextClassifier.ENTITY_PRESET_BASE:
226 return ENTITY_TYPES_BASE;
227 case TextClassifier.ENTITY_PRESET_ALL:
228 // fall through
229 default:
230 return ENTITY_TYPES_ALL;
231 }
232 }
233
234 @Override
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100235 public void logEvent(String source, String event) {
236 if (LOG_TAG.equals(source)) {
237 mMetricsLogger.count(event, 1);
238 }
239 }
240
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100241 @Override
242 public TextClassifierConstants getSettings() {
243 if (mSettings == null) {
244 mSettings = TextClassifierConstants.loadFromString(Settings.Global.getString(
245 mContext.getContentResolver(), Settings.Global.TEXT_CLASSIFIER_CONSTANTS));
246 }
247 return mSettings;
248 }
249
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100250 private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException {
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000251 synchronized (mSmartSelectionLock) {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100252 localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
253 final Locale locale = findBestSupportedLocaleLocked(localeList);
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100254 if (locale == null) {
255 throw new FileNotFoundException("No file for null locale");
256 }
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100257 if (mSmartSelection == null || !Objects.equals(mLocale, locale)) {
258 destroySmartSelectionIfExistsLocked();
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100259 final ParcelFileDescriptor fd = getFdLocked(locale);
Jan Althause750c6c2017-11-13 12:07:04 +0100260 final int modelFd = fd.getFd();
261 mVersion = SmartSelection.getVersion(modelFd);
262 mSmartSelection = new SmartSelection(modelFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100263 closeAndLogError(fd);
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100264 mLocale = locale;
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000265 }
266 return mSmartSelection;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800267 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800268 }
269
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000270 private String getSignature(String text, int start, int end) {
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100271 synchronized (mSmartSelectionLock) {
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000272 final String versionInfo = (mLocale != null)
273 ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion)
274 : "";
275 final int hash = Objects.hash(text, start, end, mContext.getPackageName());
276 return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash);
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100277 }
278 }
279
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100280 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100281 private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException {
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100282 ParcelFileDescriptor updateFd;
Jan Althause750c6c2017-11-13 12:07:04 +0100283 int updateVersion = -1;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100284 try {
285 updateFd = ParcelFileDescriptor.open(
286 new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
Jan Althause750c6c2017-11-13 12:07:04 +0100287 if (updateFd != null) {
288 updateVersion = SmartSelection.getVersion(updateFd.getFd());
289 }
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100290 } catch (FileNotFoundException e) {
291 updateFd = null;
292 }
293 ParcelFileDescriptor factoryFd;
Jan Althause750c6c2017-11-13 12:07:04 +0100294 int factoryVersion = -1;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100295 try {
296 final String factoryModelFilePath = getFactoryModelFilePathsLocked().get(locale);
297 if (factoryModelFilePath != null) {
298 factoryFd = ParcelFileDescriptor.open(
299 new File(factoryModelFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
Jan Althause750c6c2017-11-13 12:07:04 +0100300 if (factoryFd != null) {
301 factoryVersion = SmartSelection.getVersion(factoryFd.getFd());
302 }
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100303 } else {
304 factoryFd = null;
305 }
306 } catch (FileNotFoundException e) {
307 factoryFd = null;
308 }
309
310 if (updateFd == null) {
311 if (factoryFd != null) {
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100312 return factoryFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100313 } else {
314 throw new FileNotFoundException(
315 String.format("No model file found for %s", locale));
316 }
317 }
318
319 final int updateFdInt = updateFd.getFd();
320 final boolean localeMatches = Objects.equals(
321 locale.getLanguage().trim().toLowerCase(),
322 SmartSelection.getLanguage(updateFdInt).trim().toLowerCase());
323 if (factoryFd == null) {
324 if (localeMatches) {
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100325 return updateFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100326 } else {
327 closeAndLogError(updateFd);
328 throw new FileNotFoundException(
329 String.format("No model file found for %s", locale));
330 }
331 }
332
333 if (!localeMatches) {
334 closeAndLogError(updateFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100335 return factoryFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100336 }
337
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100338 if (updateVersion > factoryVersion) {
339 closeAndLogError(factoryFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100340 return updateFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100341 } else {
342 closeAndLogError(updateFd);
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100343 return factoryFd;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100344 }
345 }
346
347 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100348 private void destroySmartSelectionIfExistsLocked() {
349 if (mSmartSelection != null) {
350 mSmartSelection.close();
351 mSmartSelection = null;
352 }
353 }
354
355 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
356 @Nullable
357 private Locale findBestSupportedLocaleLocked(LocaleList localeList) {
Abodunrinwa Tokia2df6e52017-04-13 09:56:52 +0100358 // Specified localeList takes priority over the system default, so it is listed first.
359 final String languages = localeList.isEmpty()
360 ? LocaleList.getDefault().toLanguageTags()
361 : localeList.toLanguageTags() + "," + LocaleList.getDefault().toLanguageTags();
362 final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100363
364 final List<Locale> supportedLocales =
365 new ArrayList<>(getFactoryModelFilePathsLocked().keySet());
366 final Locale updatedModelLocale = getUpdatedModelLocale();
367 if (updatedModelLocale != null) {
368 supportedLocales.add(updatedModelLocale);
369 }
370 return Locale.lookup(languageRangeList, supportedLocales);
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100371 }
372
373 @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100374 private Map<Locale, String> getFactoryModelFilePathsLocked() {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100375 if (mModelFilePaths == null) {
376 final Map<Locale, String> modelFilePaths = new HashMap<>();
377 final File modelsDir = new File(MODEL_DIR);
378 if (modelsDir.exists() && modelsDir.isDirectory()) {
379 final File[] models = modelsDir.listFiles();
380 final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX);
381 final int size = models.length;
382 for (int i = 0; i < size; i++) {
383 final File modelFile = models[i];
384 final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName());
385 if (matcher.matches() && modelFile.isFile()) {
386 final String language = matcher.group(1);
387 final Locale locale = Locale.forLanguageTag(language);
388 modelFilePaths.put(locale, modelFile.getAbsolutePath());
389 }
390 }
391 }
392 mModelFilePaths = modelFilePaths;
393 }
394 return mModelFilePaths;
395 }
396
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100397 @Nullable
398 private Locale getUpdatedModelLocale() {
399 try {
400 final ParcelFileDescriptor updateFd = ParcelFileDescriptor.open(
401 new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
402 final Locale locale = Locale.forLanguageTag(
403 SmartSelection.getLanguage(updateFd.getFd()));
404 closeAndLogError(updateFd);
405 return locale;
406 } catch (FileNotFoundException e) {
407 return null;
408 }
409 }
410
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100411 private TextClassification createClassificationResult(
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000412 SmartSelection.ClassificationResult[] classifications,
413 String text, int start, int end) {
414 final String classifiedText = text.substring(start, end);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100415 final TextClassification.Builder builder = new TextClassification.Builder()
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000416 .setText(classifiedText);
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +0000417
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000418 final int size = classifications.length;
419 for (int i = 0; i < size; i++) {
420 builder.setEntityType(classifications[i].mCollection, classifications[i].mScore);
421 }
422
Abodunrinwa Toki7d9b2972017-03-14 21:56:31 +0000423 final String type = getHighestScoringType(classifications);
Abodunrinwa Toki46664a82017-12-11 17:02:06 +0000424 addActions(builder, IntentFactory.create(mContext, type, classifiedText));
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100425
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000426 return builder.setSignature(getSignature(text, start, end)).build();
Jan Althaus92d76832017-09-27 18:14:35 +0200427 }
428
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000429 /** Extends the classification with the intents that can be resolved. */
430 private void addActions(
431 TextClassification.Builder builder, List<Intent> intents) {
432 final PackageManager pm = mContext.getPackageManager();
433 final int size = intents.size();
434 for (int i = 0; i < size; i++) {
435 final Intent intent = intents.get(i);
436 final ResolveInfo resolveInfo;
437 if (intent != null) {
438 resolveInfo = pm.resolveActivity(intent, 0);
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000439 } else {
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000440 resolveInfo = null;
441 }
442 if (resolveInfo != null && resolveInfo.activityInfo != null) {
443 final String packageName = resolveInfo.activityInfo.packageName;
444 CharSequence label;
445 Drawable icon;
446 if ("android".equals(packageName)) {
447 // Requires the chooser to find an activity to handle the intent.
448 label = IntentFactory.getLabel(mContext, intent);
449 icon = null;
450 } else {
451 // A default activity will handle the intent.
452 intent.setComponent(
453 new ComponentName(packageName, resolveInfo.activityInfo.name));
454 icon = resolveInfo.activityInfo.loadIcon(pm);
455 if (icon == null) {
456 icon = resolveInfo.loadIcon(pm);
457 }
458 label = resolveInfo.activityInfo.loadLabel(pm);
459 if (label == null) {
460 label = resolveInfo.loadLabel(pm);
461 }
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000462 }
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000463 final String labelString = (label != null) ? label.toString() : null;
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000464 if (i == 0) {
Jan Althaus0d9fbb92017-11-28 12:19:33 +0100465 builder.setPrimaryAction(intent, labelString, icon);
Abodunrinwa Tokiba385622017-11-29 19:30:32 +0000466 } else {
Jan Althaus0d9fbb92017-11-28 12:19:33 +0100467 builder.addSecondaryAction(intent, labelString, icon);
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000468 }
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +0000469 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800470 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800471 }
472
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000473 private static int getHintFlags(CharSequence text, int start, int end) {
474 int flag = 0;
475 final CharSequence subText = text.subSequence(start, end);
476 if (Patterns.AUTOLINK_EMAIL_ADDRESS.matcher(subText).matches()) {
477 flag |= SmartSelection.HINT_FLAG_EMAIL;
478 }
479 if (Patterns.AUTOLINK_WEB_URL.matcher(subText).matches()
480 && Linkify.sUrlMatchFilter.acceptMatch(text, start, end)) {
481 flag |= SmartSelection.HINT_FLAG_URL;
482 }
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000483 return flag;
484 }
485
Abodunrinwa Toki7d9b2972017-03-14 21:56:31 +0000486 private static String getHighestScoringType(SmartSelection.ClassificationResult[] types) {
487 if (types.length < 1) {
488 return "";
489 }
490
491 String type = types[0].mCollection;
492 float highestScore = types[0].mScore;
493 final int size = types.length;
494 for (int i = 1; i < size; i++) {
495 if (types[i].mScore > highestScore) {
496 type = types[i].mCollection;
497 highestScore = types[i].mScore;
498 }
499 }
500 return type;
501 }
502
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800503 /**
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100504 * Closes the ParcelFileDescriptor and logs any errors that occur.
505 */
506 private static void closeAndLogError(ParcelFileDescriptor fd) {
507 try {
508 fd.close();
509 } catch (IOException e) {
510 Log.e(LOG_TAG, "Error closing file.", e);
511 }
512 }
513
514 /**
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800515 * Creates intents based on the classification type.
516 */
517 private static final class IntentFactory {
518
519 private IntentFactory() {}
520
Jan Althaus92d76832017-09-27 18:14:35 +0200521 @NonNull
522 public static List<Intent> create(Context context, String type, String text) {
523 final List<Intent> intents = new ArrayList<>();
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000524 type = type.trim().toLowerCase(Locale.ENGLISH);
Abodunrinwa Toki70d41cd2017-05-02 21:43:41 +0100525 text = text.trim();
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800526 switch (type) {
527 case TextClassifier.TYPE_EMAIL:
Jan Althaus92d76832017-09-27 18:14:35 +0200528 intents.add(new Intent(Intent.ACTION_SENDTO)
529 .setData(Uri.parse(String.format("mailto:%s", text))));
530 intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT)
Richard Ledley68d94522017-10-05 10:52:19 +0100531 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
532 .putExtra(ContactsContract.Intents.Insert.EMAIL, text));
Jan Althaus92d76832017-09-27 18:14:35 +0200533 break;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800534 case TextClassifier.TYPE_PHONE:
Jan Althaus92d76832017-09-27 18:14:35 +0200535 intents.add(new Intent(Intent.ACTION_DIAL)
536 .setData(Uri.parse(String.format("tel:%s", text))));
537 intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT)
538 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
539 .putExtra(ContactsContract.Intents.Insert.PHONE, text));
540 intents.add(new Intent(Intent.ACTION_SENDTO)
541 .setData(Uri.parse(String.format("smsto:%s", text))));
542 break;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800543 case TextClassifier.TYPE_ADDRESS:
Jan Althaus92d76832017-09-27 18:14:35 +0200544 intents.add(new Intent(Intent.ACTION_VIEW)
545 .setData(Uri.parse(String.format("geo:0,0?q=%s", text))));
546 break;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +0000547 case TextClassifier.TYPE_URL:
Abodunrinwa Toki86ef9822017-05-11 20:05:50 +0100548 final String httpPrefix = "http://";
549 final String httpsPrefix = "https://";
550 if (text.toLowerCase().startsWith(httpPrefix)) {
551 text = httpPrefix + text.substring(httpPrefix.length());
552 } else if (text.toLowerCase().startsWith(httpsPrefix)) {
553 text = httpsPrefix + text.substring(httpsPrefix.length());
554 } else {
555 text = httpPrefix + text;
Abodunrinwa Toki70d41cd2017-05-02 21:43:41 +0100556 }
Jan Althaus92d76832017-09-27 18:14:35 +0200557 intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text))
558 .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()));
559 break;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800560 }
Jan Althaus92d76832017-09-27 18:14:35 +0200561 return intents;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800562 }
563
564 @Nullable
Jan Althaus92d76832017-09-27 18:14:35 +0200565 public static String getLabel(Context context, @Nullable Intent intent) {
566 if (intent == null || intent.getAction() == null) {
567 return null;
568 }
569 switch (intent.getAction()) {
570 case Intent.ACTION_DIAL:
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800571 return context.getString(com.android.internal.R.string.dial);
Jan Althaus92d76832017-09-27 18:14:35 +0200572 case Intent.ACTION_SENDTO:
573 switch (intent.getScheme()) {
574 case "mailto":
575 return context.getString(com.android.internal.R.string.email);
576 case "smsto":
577 return context.getString(com.android.internal.R.string.sms);
578 default:
579 return null;
580 }
581 case Intent.ACTION_INSERT_OR_EDIT:
582 switch (intent.getDataString()) {
583 case ContactsContract.Contacts.CONTENT_ITEM_TYPE:
584 return context.getString(com.android.internal.R.string.add_contact);
585 default:
586 return null;
587 }
588 case Intent.ACTION_VIEW:
589 switch (intent.getScheme()) {
590 case "geo":
591 return context.getString(com.android.internal.R.string.map);
592 case "http": // fall through
593 case "https":
594 return context.getString(com.android.internal.R.string.browse);
595 default:
596 return null;
597 }
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800598 default:
599 return null;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800600 }
601 }
602 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800603}