blob: 3e95f1baf4bbeb45054a802c650ddd392531646f [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 Tokiad52f4b2018-02-06 23:32:41 +000021import android.annotation.WorkerThread;
Jan Althaus20d346e2018-03-23 14:03:52 +010022import android.app.RemoteAction;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080023import android.content.Context;
Tony Mak72e17972019-03-16 10:28:42 +000024import android.content.Intent;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +010025import android.icu.util.ULocale;
Jan Althaus05e00512018-02-02 15:28:34 +010026import android.os.Bundle;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000027import android.os.LocaleList;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080028import android.os.ParcelFileDescriptor;
Tony Mak72e17972019-03-16 10:28:42 +000029import android.util.ArrayMap;
30import android.util.ArraySet;
31import android.util.Pair;
Tony Mak20fe1872019-03-22 15:35:15 +000032import android.view.textclassifier.ActionsModelParamsSupplier.ActionsModelParams;
Tony Mak8ab9b182019-03-01 16:44:17 +000033import android.view.textclassifier.intent.ClassificationIntentFactory;
34import android.view.textclassifier.intent.LabeledIntent;
35import android.view.textclassifier.intent.LegacyClassificationIntentFactory;
36import android.view.textclassifier.intent.TemplateClassificationIntentFactory;
37import android.view.textclassifier.intent.TemplateIntentFactory;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080038
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010039import com.android.internal.annotations.GuardedBy;
Tony Makf93e9e52018-07-16 14:46:29 +020040import com.android.internal.util.IndentingPrintWriter;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080041import com.android.internal.util.Preconditions;
42
Tony Makadbebcc2018-10-30 18:59:06 +000043import com.google.android.textclassifier.ActionsSuggestionsModel;
Tony Mak64c4cb22018-09-25 13:38:28 +010044import com.google.android.textclassifier.AnnotatorModel;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +010045import com.google.android.textclassifier.LangIdModel;
Tony Mak72e17972019-03-16 10:28:42 +000046import com.google.android.textclassifier.LangIdModel.LanguageResult;
Tony Mak64c4cb22018-09-25 13:38:28 +010047
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010048import java.io.File;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080049import java.io.FileNotFoundException;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010050import java.io.IOException;
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.Collection;
Tony Mak72e17972019-03-16 10:28:42 +000055import java.util.Collections;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080056import java.util.List;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000057import java.util.Locale;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080058import java.util.Map;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010059import java.util.Objects;
Tony Mak72e17972019-03-16 10:28:42 +000060import java.util.Set;
Tony Mak20fe1872019-03-22 15:35:15 +000061import java.util.function.Supplier;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080062
63/**
64 * Default implementation of the {@link TextClassifier} interface.
65 *
66 * <p>This class uses machine learning to recognize entities in text.
67 * Unless otherwise stated, methods of this class are blocking operations and should most
68 * likely not be called on the UI thread.
69 *
70 * @hide
71 */
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080072public final class TextClassifierImpl implements TextClassifier {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080073
Abodunrinwa Toki692b1962017-08-15 15:05:11 +010074 private static final String LOG_TAG = DEFAULT_LOG_TAG;
Tony Makba228422018-10-25 21:30:40 +010075
Tony Make94e0782018-12-14 11:57:54 +080076 private static final boolean DEBUG = false;
77
Tony Makba228422018-10-25 21:30:40 +010078 private static final File FACTORY_MODEL_DIR = new File("/etc/textclassifier/");
79 // Annotator
80 private static final String ANNOTATOR_FACTORY_MODEL_FILENAME_REGEX =
81 "textclassifier\\.(.*)\\.model";
82 private static final File ANNOTATOR_UPDATED_MODEL_FILE =
83 new File("/data/misc/textclassifier/textclassifier.model");
84
85 // LangID
86 private static final String LANG_ID_FACTORY_MODEL_FILENAME_REGEX = "lang_id.model";
87 private static final File UPDATED_LANG_ID_MODEL_FILE =
88 new File("/data/misc/textclassifier/lang_id.model");
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +000089
Tony Makadbebcc2018-10-30 18:59:06 +000090 // Actions
Tony Mak237f37b2019-03-22 13:35:40 +000091 private static final String ACTIONS_FACTORY_MODEL_FILENAME_REGEX =
92 "actions_suggestions\\.(.*)\\.model";
Tony Makadbebcc2018-10-30 18:59:06 +000093 private static final File UPDATED_ACTIONS_MODEL =
94 new File("/data/misc/textclassifier/actions_suggestions.model");
95
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080096 private final Context mContext;
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080097 private final TextClassifier mFallback;
Jan Althaus31efdc32018-02-19 22:23:13 +010098 private final GenerateLinksLogger mGenerateLinksLogger;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010099
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800100 private final Object mLock = new Object();
Tony Mak6fc43182019-03-07 17:30:33 +0000101
Tony Mak72e17972019-03-16 10:28:42 +0000102 @GuardedBy("mLock")
Tony Makba228422018-10-25 21:30:40 +0100103 private ModelFileManager.ModelFile mAnnotatorModelInUse;
Tony Mak72e17972019-03-16 10:28:42 +0000104 @GuardedBy("mLock")
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100105 private AnnotatorModel mAnnotatorImpl;
Tony Mak6fc43182019-03-07 17:30:33 +0000106
Tony Mak72e17972019-03-16 10:28:42 +0000107 @GuardedBy("mLock")
Tony Mak6fc43182019-03-07 17:30:33 +0000108 private ModelFileManager.ModelFile mLangIdModelInUse;
Tony Mak72e17972019-03-16 10:28:42 +0000109 @GuardedBy("mLock")
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100110 private LangIdModel mLangIdImpl;
Tony Mak6fc43182019-03-07 17:30:33 +0000111
Tony Mak72e17972019-03-16 10:28:42 +0000112 @GuardedBy("mLock")
Tony Make94e0782018-12-14 11:57:54 +0800113 private ModelFileManager.ModelFile mActionModelInUse;
Tony Mak72e17972019-03-16 10:28:42 +0000114 @GuardedBy("mLock")
Tony Makadbebcc2018-10-30 18:59:06 +0000115 private ActionsSuggestionsModel mActionsImpl;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800116
Tony Mak5a5f0d52019-01-08 11:07:23 +0000117 private final SelectionSessionLogger mSessionLogger = new SelectionSessionLogger();
118 private final TextClassifierEventTronLogger mTextClassifierEventTronLogger =
119 new TextClassifierEventTronLogger();
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000120
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000121 private final TextClassificationConstants mSettings;
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100122
Tony Makba228422018-10-25 21:30:40 +0100123 private final ModelFileManager mAnnotatorModelFileManager;
124 private final ModelFileManager mLangIdModelFileManager;
Tony Makadbebcc2018-10-30 18:59:06 +0000125 private final ModelFileManager mActionsModelFileManager;
Tony Makba228422018-10-25 21:30:40 +0100126
Tony Mak8ab9b182019-03-01 16:44:17 +0000127 private final ClassificationIntentFactory mClassificationIntentFactory;
Tony Makae33c3b2019-01-31 14:29:19 +0000128 private final TemplateIntentFactory mTemplateIntentFactory;
Tony Mak20fe1872019-03-22 15:35:15 +0000129 private final Supplier<ActionsModelParams> mActionsModelParamsSupplier;
Tony Makfc039c32019-01-17 19:32:08 +0000130
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100131 public TextClassifierImpl(
132 Context context, TextClassificationConstants settings, TextClassifier fallback) {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800133 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100134 mFallback = Preconditions.checkNotNull(fallback);
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000135 mSettings = Preconditions.checkNotNull(settings);
136 mGenerateLinksLogger = new GenerateLinksLogger(mSettings.getGenerateLinksLogSampleRate());
Tony Makba228422018-10-25 21:30:40 +0100137 mAnnotatorModelFileManager = new ModelFileManager(
138 new ModelFileManager.ModelFileSupplierImpl(
139 FACTORY_MODEL_DIR,
140 ANNOTATOR_FACTORY_MODEL_FILENAME_REGEX,
141 ANNOTATOR_UPDATED_MODEL_FILE,
142 AnnotatorModel::getVersion,
143 AnnotatorModel::getLocales));
144 mLangIdModelFileManager = new ModelFileManager(
145 new ModelFileManager.ModelFileSupplierImpl(
146 FACTORY_MODEL_DIR,
147 LANG_ID_FACTORY_MODEL_FILENAME_REGEX,
148 UPDATED_LANG_ID_MODEL_FILE,
Tony Makc5a46122018-12-05 22:19:22 +0000149 LangIdModel::getVersion,
Tony Makba228422018-10-25 21:30:40 +0100150 fd -> ModelFileManager.ModelFile.LANGUAGE_INDEPENDENT));
Tony Makadbebcc2018-10-30 18:59:06 +0000151 mActionsModelFileManager = new ModelFileManager(
152 new ModelFileManager.ModelFileSupplierImpl(
153 FACTORY_MODEL_DIR,
154 ACTIONS_FACTORY_MODEL_FILENAME_REGEX,
155 UPDATED_ACTIONS_MODEL,
156 ActionsSuggestionsModel::getVersion,
157 ActionsSuggestionsModel::getLocales));
Tony Makfc039c32019-01-17 19:32:08 +0000158
Tony Makae33c3b2019-01-31 14:29:19 +0000159 mTemplateIntentFactory = new TemplateIntentFactory();
Tony Mak8ab9b182019-03-01 16:44:17 +0000160 mClassificationIntentFactory = mSettings.isTemplateIntentFactoryEnabled()
Tony Makae33c3b2019-01-31 14:29:19 +0000161 ? new TemplateClassificationIntentFactory(
Tony Mak8ab9b182019-03-01 16:44:17 +0000162 mTemplateIntentFactory, new LegacyClassificationIntentFactory())
163 : new LegacyClassificationIntentFactory();
Tony Mak20fe1872019-03-22 15:35:15 +0000164 mActionsModelParamsSupplier = new ActionsModelParamsSupplier(mContext,
165 () -> {
166 synchronized (mLock) {
167 // Clear mActionsImpl here, so that we will create a new
168 // ActionsSuggestionsModel object with the new flag in the next request.
169 mActionsImpl = null;
170 mActionModelInUse = null;
171 }
172 });
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800173 }
174
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100175 public TextClassifierImpl(Context context, TextClassificationConstants settings) {
176 this(context, settings, TextClassifier.NO_OP);
177 }
178
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800179 /** @inheritDoc */
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800180 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000181 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100182 public TextSelection suggestSelection(TextSelection.Request request) {
183 Preconditions.checkNotNull(request);
184 Utils.checkMainThread();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800185 try {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100186 final int rangeLength = request.getEndIndex() - request.getStartIndex();
187 final String string = request.getText().toString();
188 if (string.length() > 0
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000189 && rangeLength <= mSettings.getSuggestSelectionMaxRangeLength()) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100190 final String localesString = concatenateLocales(request.getDefaultLocales());
Tony Mak159f0282019-03-01 14:03:25 +0000191 final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
Jan Althausa1652cf2018-03-29 17:51:57 +0200192 final ZonedDateTime refTime = ZonedDateTime.now();
Tony Mak2a3adb72019-03-20 17:55:53 +0000193 final AnnotatorModel annotatorImpl = getAnnotatorImpl(request.getDefaultLocales());
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100194 final int start;
195 final int end;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100196 if (mSettings.isModelDarkLaunchEnabled() && !request.isDarkLaunchAllowed()) {
197 start = request.getStartIndex();
198 end = request.getEndIndex();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100199 } else {
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100200 final int[] startEnd = annotatorImpl.suggestSelection(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100201 string, request.getStartIndex(), request.getEndIndex(),
Tony Mak159f0282019-03-01 14:03:25 +0000202 new AnnotatorModel.SelectionOptions(localesString, detectLanguageTags));
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100203 start = startEnd[0];
204 end = startEnd[1];
205 }
Jan Althausb7f7d362018-01-26 13:51:31 +0100206 if (start < end
Abodunrinwa Tokib4162972017-05-05 18:07:17 +0100207 && start >= 0 && end <= string.length()
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100208 && start <= request.getStartIndex() && end >= request.getEndIndex()) {
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100209 final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
Tony Mak64c4cb22018-09-25 13:38:28 +0100210 final AnnotatorModel.ClassificationResult[] results =
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100211 annotatorImpl.classifyText(
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000212 string, start, end,
Tony Mak64c4cb22018-09-25 13:38:28 +0100213 new AnnotatorModel.ClassificationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200214 refTime.toInstant().toEpochMilli(),
215 refTime.getZone().getId(),
Tony Mak159f0282019-03-01 14:03:25 +0000216 localesString,
217 detectLanguageTags),
Tony Makae33c3b2019-01-31 14:29:19 +0000218 // Passing null here to suppress intent generation
219 // TODO: Use an explicit flag to suppress it.
220 /* appContext */ null,
221 /* deviceLocales */null);
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000222 final int size = results.length;
223 for (int i = 0; i < size; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100224 tsBuilder.setEntityType(results[i].getCollection(), results[i].getScore());
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000225 }
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100226 return tsBuilder.setId(createId(
227 string, request.getStartIndex(), request.getEndIndex()))
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100228 .build();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800229 } else {
230 // We can not trust the result. Log the issue and ignore the result.
231 Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
232 }
233 }
234 } catch (Throwable t) {
235 // Avoid throwing from this method. Log the error.
236 Log.e(LOG_TAG,
237 "Error suggesting selection for text. No changes to selection suggested.",
238 t);
239 }
240 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100241 return mFallback.suggestSelection(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100242 }
243
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800244 /** @inheritDoc */
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100245 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000246 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100247 public TextClassification classifyText(TextClassification.Request request) {
248 Preconditions.checkNotNull(request);
249 Utils.checkMainThread();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800250 try {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100251 final int rangeLength = request.getEndIndex() - request.getStartIndex();
252 final String string = request.getText().toString();
253 if (string.length() > 0 && rangeLength <= mSettings.getClassifyTextMaxRangeLength()) {
254 final String localesString = concatenateLocales(request.getDefaultLocales());
Tony Mak159f0282019-03-01 14:03:25 +0000255 final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100256 final ZonedDateTime refTime = request.getReferenceTime() != null
257 ? request.getReferenceTime() : ZonedDateTime.now();
Tony Mak64c4cb22018-09-25 13:38:28 +0100258 final AnnotatorModel.ClassificationResult[] results =
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100259 getAnnotatorImpl(request.getDefaultLocales())
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100260 .classifyText(
261 string, request.getStartIndex(), request.getEndIndex(),
Tony Mak64c4cb22018-09-25 13:38:28 +0100262 new AnnotatorModel.ClassificationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200263 refTime.toInstant().toEpochMilli(),
264 refTime.getZone().getId(),
Tony Mak159f0282019-03-01 14:03:25 +0000265 localesString,
266 detectLanguageTags),
Tony Makae33c3b2019-01-31 14:29:19 +0000267 mContext,
Tony Mak159f0282019-03-01 14:03:25 +0000268 getResourceLocalesString()
Tony Makae33c3b2019-01-31 14:29:19 +0000269 );
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000270 if (results.length > 0) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100271 return createClassificationResult(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100272 results, string,
273 request.getStartIndex(), request.getEndIndex(), refTime.toInstant());
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800274 }
275 }
276 } catch (Throwable t) {
277 // Avoid throwing from this method. Log the error.
Abodunrinwa Toki78618852017-10-17 15:31:39 +0100278 Log.e(LOG_TAG, "Error getting text classification info.", t);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800279 }
280 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100281 return mFallback.classifyText(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100282 }
283
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800284 /** @inheritDoc */
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100285 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000286 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100287 public TextLinks generateLinks(@NonNull TextLinks.Request request) {
288 Preconditions.checkNotNull(request);
289 Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength());
290 Utils.checkMainThread();
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000291
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100292 if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
293 return Utils.generateLegacyLinks(request);
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000294 }
295
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100296 final String textString = request.getText().toString();
Richard Ledley68d94522017-10-05 10:52:19 +0100297 final TextLinks.Builder builder = new TextLinks.Builder(textString);
Richard Ledley9cfa6062018-01-15 13:13:29 +0000298
Richard Ledley68d94522017-10-05 10:52:19 +0100299 try {
Jan Althaus31efdc32018-02-19 22:23:13 +0100300 final long startTimeMs = System.currentTimeMillis();
Jan Althausa1652cf2018-03-29 17:51:57 +0200301 final ZonedDateTime refTime = ZonedDateTime.now();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100302 final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
303 ? request.getEntityConfig().resolveEntityListModifications(
Abodunrinwa Toki0634af32019-04-04 13:10:59 +0100304 getEntitiesForHints(request.getEntityConfig().getHints()))
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100305 : mSettings.getEntityListDefault();
Tony Mak159f0282019-03-01 14:03:25 +0000306 final String localesString = concatenateLocales(request.getDefaultLocales());
307 final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100308 final AnnotatorModel annotatorImpl =
309 getAnnotatorImpl(request.getDefaultLocales());
Tony Makb6afd3c2019-04-05 15:45:18 +0100310 final boolean isSerializedEntityDataEnabled =
311 ExtrasUtils.isSerializedEntityDataEnabled(request);
Tony Mak64c4cb22018-09-25 13:38:28 +0100312 final AnnotatorModel.AnnotatedSpan[] annotations =
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100313 annotatorImpl.annotate(
Tony Makf99ee172018-11-23 12:14:39 +0000314 textString,
315 new AnnotatorModel.AnnotationOptions(
316 refTime.toInstant().toEpochMilli(),
317 refTime.getZone().getId(),
Tony Mak159f0282019-03-01 14:03:25 +0000318 localesString,
Tony Makb6afd3c2019-04-05 15:45:18 +0100319 detectLanguageTags,
320 entitiesToIdentify,
321 AnnotatorModel.AnnotationUsecase.SMART.getValue(),
322 isSerializedEntityDataEnabled));
Tony Mak64c4cb22018-09-25 13:38:28 +0100323 for (AnnotatorModel.AnnotatedSpan span : annotations) {
324 final AnnotatorModel.ClassificationResult[] results =
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100325 span.getClassification();
326 if (results.length == 0
327 || !entitiesToIdentify.contains(results[0].getCollection())) {
Richard Ledleydb18a572017-11-30 17:33:51 +0000328 continue;
329 }
Tony Mak72e17972019-03-16 10:28:42 +0000330 final Map<String, Float> entityScores = new ArrayMap<>();
Richard Ledley68d94522017-10-05 10:52:19 +0100331 for (int i = 0; i < results.length; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100332 entityScores.put(results[i].getCollection(), results[i].getScore());
Richard Ledley68d94522017-10-05 10:52:19 +0100333 }
Tony Makb6afd3c2019-04-05 15:45:18 +0100334 Bundle extras = new Bundle();
335 if (isSerializedEntityDataEnabled) {
336 ExtrasUtils.putEntities(extras, results);
337 }
338 builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores, extras);
Richard Ledley68d94522017-10-05 10:52:19 +0100339 }
Jan Althaus31efdc32018-02-19 22:23:13 +0100340 final TextLinks links = builder.build();
341 final long endTimeMs = System.currentTimeMillis();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100342 final String callingPackageName = request.getCallingPackageName() == null
343 ? mContext.getPackageName() // local (in process) TC.
344 : request.getCallingPackageName();
Jan Althaus31efdc32018-02-19 22:23:13 +0100345 mGenerateLinksLogger.logGenerateLinks(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100346 request.getText(), links, callingPackageName, endTimeMs - startTimeMs);
Jan Althaus31efdc32018-02-19 22:23:13 +0100347 return links;
Richard Ledley68d94522017-10-05 10:52:19 +0100348 } catch (Throwable t) {
349 // Avoid throwing from this method. Log the error.
350 Log.e(LOG_TAG, "Error getting links info.", t);
351 }
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100352 return mFallback.generateLinks(request);
Richard Ledley68d94522017-10-05 10:52:19 +0100353 }
354
Jan Althaus108aad32018-01-30 15:26:55 +0100355 /** @inheritDoc */
356 @Override
357 public int getMaxGenerateLinksTextLength() {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000358 return mSettings.getGenerateLinksMaxTextLength();
Jan Althaus108aad32018-01-30 15:26:55 +0100359 }
360
Richard Ledley1fc998b2018-02-16 15:45:06 +0000361 private Collection<String> getEntitiesForHints(Collection<String> hints) {
Jan Althaus0aacdb62018-02-19 11:44:37 +0100362 final boolean editable = hints.contains(HINT_TEXT_IS_EDITABLE);
363 final boolean notEditable = hints.contains(HINT_TEXT_IS_NOT_EDITABLE);
364
365 // Use the default if there is no hint, or conflicting ones.
366 final boolean useDefault = editable == notEditable;
367 if (useDefault) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000368 return mSettings.getEntityListDefault();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100369 } else if (editable) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000370 return mSettings.getEntityListEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100371 } else { // notEditable
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000372 return mSettings.getEntityListNotEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100373 }
Richard Ledleydb18a572017-11-30 17:33:51 +0000374 }
375
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100376 /** @inheritDoc */
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000377 @Override
378 public void onSelectionEvent(SelectionEvent event) {
379 Preconditions.checkNotNull(event);
Tony Mak5a5f0d52019-01-08 11:07:23 +0000380 mSessionLogger.writeEvent(event);
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000381 }
382
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000383 @Override
Tony Make94e0782018-12-14 11:57:54 +0800384 public void onTextClassifierEvent(TextClassifierEvent event) {
385 if (DEBUG) {
386 Log.d(DEFAULT_LOG_TAG, "onTextClassifierEvent() called with: event = [" + event + "]");
387 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000388 try {
389 mTextClassifierEventTronLogger.writeEvent(event);
390 } catch (Exception e) {
391 Log.e(LOG_TAG, "Error writing event", e);
392 }
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000393 }
394
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100395 /** @inheritDoc */
396 @Override
397 public TextLanguage detectLanguage(@NonNull TextLanguage.Request request) {
398 Preconditions.checkNotNull(request);
399 Utils.checkMainThread();
400 try {
401 final TextLanguage.Builder builder = new TextLanguage.Builder();
402 final LangIdModel.LanguageResult[] langResults =
403 getLangIdImpl().detectLanguages(request.getText().toString());
404 for (int i = 0; i < langResults.length; i++) {
405 builder.putLocale(
406 ULocale.forLanguageTag(langResults[i].getLanguage()),
407 langResults[i].getScore());
408 }
409 return builder.build();
410 } catch (Throwable t) {
411 // Avoid throwing from this method. Log the error.
412 Log.e(LOG_TAG, "Error detecting text language.", t);
413 }
414 return mFallback.detectLanguage(request);
415 }
416
Tony Makadbebcc2018-10-30 18:59:06 +0000417 @Override
418 public ConversationActions suggestConversationActions(ConversationActions.Request request) {
419 Preconditions.checkNotNull(request);
420 Utils.checkMainThread();
421 try {
422 ActionsSuggestionsModel actionsImpl = getActionsImpl();
423 if (actionsImpl == null) {
424 // Actions model is optional, fallback if it is not available.
425 return mFallback.suggestConversationActions(request);
426 }
Tony Makf99ee172018-11-23 12:14:39 +0000427 ActionsSuggestionsModel.ConversationMessage[] nativeMessages =
Tony Mak159f0282019-03-01 14:03:25 +0000428 ActionsSuggestionsHelper.toNativeMessages(
429 request.getConversation(), this::detectLanguageTagsFromText);
Tony Makf99ee172018-11-23 12:14:39 +0000430 if (nativeMessages.length == 0) {
431 return mFallback.suggestConversationActions(request);
Tony Makadbebcc2018-10-30 18:59:06 +0000432 }
433 ActionsSuggestionsModel.Conversation nativeConversation =
Tony Makf99ee172018-11-23 12:14:39 +0000434 new ActionsSuggestionsModel.Conversation(nativeMessages);
Tony Makadbebcc2018-10-30 18:59:06 +0000435
436 ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions =
Tony Makae33c3b2019-01-31 14:29:19 +0000437 actionsImpl.suggestActionsWithIntents(
438 nativeConversation,
439 null,
440 mContext,
Tony Mak2a3adb72019-03-20 17:55:53 +0000441 getResourceLocalesString(),
442 getAnnotatorImpl(LocaleList.getDefault()));
Tony Makae33c3b2019-01-31 14:29:19 +0000443 return createConversationActionResult(request, nativeSuggestions);
Tony Makadbebcc2018-10-30 18:59:06 +0000444 } catch (Throwable t) {
445 // Avoid throwing from this method. Log the error.
446 Log.e(LOG_TAG, "Error suggesting conversation actions.", t);
447 }
448 return mFallback.suggestConversationActions(request);
449 }
450
Tony Makc12035e2019-02-26 17:45:34 +0000451 /**
452 * Returns the {@link ConversationAction} result, with a non-null extras.
453 * <p>
454 * Whenever the RemoteAction is non-null, you can expect its corresponding intent
455 * with a non-null component name is in the extras.
456 */
Tony Makae33c3b2019-01-31 14:29:19 +0000457 private ConversationActions createConversationActionResult(
458 ConversationActions.Request request,
459 ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions) {
460 Collection<String> expectedTypes = resolveActionTypesFromRequest(request);
461 List<ConversationAction> conversationActions = new ArrayList<>();
462 for (ActionsSuggestionsModel.ActionSuggestion nativeSuggestion : nativeSuggestions) {
Tony Makae33c3b2019-01-31 14:29:19 +0000463 String actionType = nativeSuggestion.getActionType();
464 if (!expectedTypes.contains(actionType)) {
465 continue;
466 }
Tony Mak8ab9b182019-03-01 16:44:17 +0000467 LabeledIntent.Result labeledIntentResult =
468 ActionsSuggestionsHelper.createLabeledIntentResult(
469 mContext,
470 mTemplateIntentFactory,
471 nativeSuggestion);
Tony Makae33c3b2019-01-31 14:29:19 +0000472 RemoteAction remoteAction = null;
Tony Mak8ab9b182019-03-01 16:44:17 +0000473 Bundle extras = new Bundle();
474 if (labeledIntentResult != null) {
475 remoteAction = labeledIntentResult.remoteAction;
476 ExtrasUtils.putActionIntent(extras, labeledIntentResult.resolvedIntent);
Tony Makae33c3b2019-01-31 14:29:19 +0000477 }
Tony Makfdb35542019-03-22 12:01:50 +0000478 ExtrasUtils.putSerializedEntityData(extras, nativeSuggestion.getSerializedEntityData());
Tony Mak09214422019-03-01 18:25:23 +0000479 ExtrasUtils.putEntitiesExtras(
480 extras,
481 TemplateIntentFactory.nameVariantsToBundle(nativeSuggestion.getEntityData()));
Tony Makae33c3b2019-01-31 14:29:19 +0000482 conversationActions.add(
483 new ConversationAction.Builder(actionType)
484 .setConfidenceScore(nativeSuggestion.getScore())
485 .setTextReply(nativeSuggestion.getResponseText())
486 .setAction(remoteAction)
Tony Makc12035e2019-02-26 17:45:34 +0000487 .setExtras(extras)
Tony Makae33c3b2019-01-31 14:29:19 +0000488 .build());
489 }
Tony Makc12035e2019-02-26 17:45:34 +0000490 conversationActions =
491 ActionsSuggestionsHelper.removeActionsWithDuplicates(conversationActions);
Tony Makaa496d02019-04-11 17:38:47 +0100492 if (request.getMaxSuggestions() >= 0
493 && conversationActions.size() > request.getMaxSuggestions()) {
494 conversationActions = conversationActions.subList(0, request.getMaxSuggestions());
495 }
Tony Makae33c3b2019-01-31 14:29:19 +0000496 String resultId = ActionsSuggestionsHelper.createResultId(
497 mContext,
498 request.getConversation(),
499 mActionModelInUse.getVersion(),
500 mActionModelInUse.getSupportedLocales());
501 return new ConversationActions(conversationActions, resultId);
502 }
503
Tony Mak82fa8d92018-12-07 17:37:43 +0000504 @Nullable
505 private String detectLanguageTagsFromText(CharSequence text) {
Tony Mak159f0282019-03-01 14:03:25 +0000506 if (!mSettings.isDetectLanguagesFromTextEnabled()) {
507 return null;
508 }
509 final float threshold = getLangIdThreshold();
510 if (threshold < 0 || threshold > 1) {
511 Log.w(LOG_TAG,
512 "[detectLanguageTagsFromText] unexpected threshold is found: " + threshold);
513 return null;
514 }
Tony Mak82fa8d92018-12-07 17:37:43 +0000515 TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
516 TextLanguage textLanguage = detectLanguage(request);
517 int localeHypothesisCount = textLanguage.getLocaleHypothesisCount();
518 List<String> languageTags = new ArrayList<>();
Tony Mak82fa8d92018-12-07 17:37:43 +0000519 for (int i = 0; i < localeHypothesisCount; i++) {
520 ULocale locale = textLanguage.getLocale(i);
Tony Mak159f0282019-03-01 14:03:25 +0000521 if (textLanguage.getConfidenceScore(locale) < threshold) {
Tony Mak82fa8d92018-12-07 17:37:43 +0000522 break;
523 }
524 languageTags.add(locale.toLanguageTag());
525 }
526 if (languageTags.isEmpty()) {
Tony Mak159f0282019-03-01 14:03:25 +0000527 return null;
Tony Mak82fa8d92018-12-07 17:37:43 +0000528 }
529 return String.join(",", languageTags);
530 }
531
Tony Makadbebcc2018-10-30 18:59:06 +0000532 private Collection<String> resolveActionTypesFromRequest(ConversationActions.Request request) {
533 List<String> defaultActionTypes =
Tony Makae85aae2019-01-09 15:59:56 +0000534 request.getHints().contains(ConversationActions.Request.HINT_FOR_NOTIFICATION)
Tony Makadbebcc2018-10-30 18:59:06 +0000535 ? mSettings.getNotificationConversationActionTypes()
536 : mSettings.getInAppConversationActionTypes();
Tony Makae85aae2019-01-09 15:59:56 +0000537 return request.getTypeConfig().resolveEntityListModifications(defaultActionTypes);
Tony Makadbebcc2018-10-30 18:59:06 +0000538 }
539
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100540 private AnnotatorModel getAnnotatorImpl(LocaleList localeList)
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100541 throws FileNotFoundException {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800542 synchronized (mLock) {
Tony Makadbebcc2018-10-30 18:59:06 +0000543 localeList = localeList == null ? LocaleList.getDefault() : localeList;
Tony Makba228422018-10-25 21:30:40 +0100544 final ModelFileManager.ModelFile bestModel =
545 mAnnotatorModelFileManager.findBestModelFile(localeList);
Jan Althausef0156d2018-01-29 19:28:41 +0100546 if (bestModel == null) {
Tony Makba228422018-10-25 21:30:40 +0100547 throw new FileNotFoundException(
548 "No annotator model for " + localeList.toLanguageTags());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100549 }
Tony Makba228422018-10-25 21:30:40 +0100550 if (mAnnotatorImpl == null || !Objects.equals(mAnnotatorModelInUse, bestModel)) {
Jan Althausef0156d2018-01-29 19:28:41 +0100551 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Tony Makba228422018-10-25 21:30:40 +0100552 final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
Jan Althausef0156d2018-01-29 19:28:41 +0100553 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100554 try {
Tony Makba228422018-10-25 21:30:40 +0100555 if (pfd != null) {
Tony Makae33c3b2019-01-31 14:29:19 +0000556 // The current annotator model may be still used by another thread / model.
557 // Do not call close() here, and let the GC to clean it up when no one else
558 // is using it.
Tony Makba228422018-10-25 21:30:40 +0100559 mAnnotatorImpl = new AnnotatorModel(pfd.getFd());
560 mAnnotatorModelInUse = bestModel;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100561 }
562 } finally {
Tony Makba228422018-10-25 21:30:40 +0100563 maybeCloseAndLogError(pfd);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100564 }
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000565 }
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100566 return mAnnotatorImpl;
567 }
568 }
569
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100570 private LangIdModel getLangIdImpl() throws FileNotFoundException {
571 synchronized (mLock) {
Tony Mak6fc43182019-03-07 17:30:33 +0000572 final ModelFileManager.ModelFile bestModel =
573 mLangIdModelFileManager.findBestModelFile(null);
574 if (bestModel == null) {
575 throw new FileNotFoundException("No LangID model is found");
576 }
577 if (mLangIdImpl == null || !Objects.equals(mLangIdModelInUse, bestModel)) {
578 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Tony Makba228422018-10-25 21:30:40 +0100579 final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
580 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100581 try {
Tony Makba228422018-10-25 21:30:40 +0100582 if (pfd != null) {
583 mLangIdImpl = new LangIdModel(pfd.getFd());
Tony Mak6fc43182019-03-07 17:30:33 +0000584 mLangIdModelInUse = bestModel;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100585 }
586 } finally {
Tony Makba228422018-10-25 21:30:40 +0100587 maybeCloseAndLogError(pfd);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100588 }
589 }
590 return mLangIdImpl;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800591 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800592 }
593
Tony Makadbebcc2018-10-30 18:59:06 +0000594 @Nullable
595 private ActionsSuggestionsModel getActionsImpl() throws FileNotFoundException {
596 synchronized (mLock) {
Tony Mak6fc43182019-03-07 17:30:33 +0000597 // TODO: Use LangID to determine the locale we should use here?
598 final ModelFileManager.ModelFile bestModel =
599 mActionsModelFileManager.findBestModelFile(LocaleList.getDefault());
600 if (bestModel == null) {
601 return null;
602 }
603 if (mActionsImpl == null || !Objects.equals(mActionModelInUse, bestModel)) {
604 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Tony Makadbebcc2018-10-30 18:59:06 +0000605 final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
606 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
607 try {
Tony Mak20fe1872019-03-22 15:35:15 +0000608 if (pfd == null) {
609 Log.d(LOG_TAG, "Failed to read the model file: " + bestModel.getPath());
610 return null;
Tony Makadbebcc2018-10-30 18:59:06 +0000611 }
Tony Mak20fe1872019-03-22 15:35:15 +0000612 ActionsModelParams params = mActionsModelParamsSupplier.get();
613 mActionsImpl = new ActionsSuggestionsModel(
614 pfd.getFd(), params.getSerializedPreconditions(bestModel));
615 mActionModelInUse = bestModel;
Tony Makadbebcc2018-10-30 18:59:06 +0000616 } finally {
617 maybeCloseAndLogError(pfd);
618 }
619 }
620 return mActionsImpl;
621 }
622 }
623
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100624 private String createId(String text, int start, int end) {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800625 synchronized (mLock) {
Tony Makba228422018-10-25 21:30:40 +0100626 return SelectionSessionLogger.createId(text, start, end, mContext,
627 mAnnotatorModelInUse.getVersion(),
628 mAnnotatorModelInUse.getSupportedLocales());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100629 }
630 }
631
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100632 private static String concatenateLocales(@Nullable LocaleList locales) {
633 return (locales == null) ? "" : locales.toLanguageTags();
634 }
635
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100636 private TextClassification createClassificationResult(
Tony Mak64c4cb22018-09-25 13:38:28 +0100637 AnnotatorModel.ClassificationResult[] classifications,
Jan Althausa1652cf2018-03-29 17:51:57 +0200638 String text, int start, int end, @Nullable Instant referenceTime) {
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000639 final String classifiedText = text.substring(start, end);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100640 final TextClassification.Builder builder = new TextClassification.Builder()
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000641 .setText(classifiedText);
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +0000642
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100643 final int typeCount = classifications.length;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100644 AnnotatorModel.ClassificationResult highestScoringResult =
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100645 typeCount > 0 ? classifications[0] : null;
646 for (int i = 0; i < typeCount; i++) {
Tony Makfdb35542019-03-22 12:01:50 +0000647 builder.setEntityType(classifications[i]);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100648 if (classifications[i].getScore() > highestScoringResult.getScore()) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100649 highestScoringResult = classifications[i];
Jan Althaus705b9e92018-01-22 18:22:29 +0100650 }
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000651 }
652
Tony Mak72e17972019-03-16 10:28:42 +0000653 final Pair<Bundle, Bundle> languagesBundles = generateLanguageBundles(text, start, end);
654 final Bundle textLanguagesBundle = languagesBundles.first;
655 final Bundle foreignLanguageBundle = languagesBundles.second;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000656 builder.setForeignLanguageExtra(foreignLanguageBundle);
657
Jan Althaus20d346e2018-03-23 14:03:52 +0100658 boolean isPrimaryAction = true;
Tony Mak72e17972019-03-16 10:28:42 +0000659 final List<LabeledIntent> labeledIntents = mClassificationIntentFactory.create(
Tony Makfc039c32019-01-17 19:32:08 +0000660 mContext,
661 classifiedText,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000662 foreignLanguageBundle != null,
Tony Makfc039c32019-01-17 19:32:08 +0000663 referenceTime,
664 highestScoringResult);
Tony Mak72e17972019-03-16 10:28:42 +0000665 final LabeledIntent.TitleChooser titleChooser =
Tony Makac9b4d82019-02-15 13:57:38 +0000666 (labeledIntent, resolveInfo) -> labeledIntent.titleWithoutEntity;
Tony Mak72e17972019-03-16 10:28:42 +0000667
Tony Makfc039c32019-01-17 19:32:08 +0000668 for (LabeledIntent labeledIntent : labeledIntents) {
Tony Mak72e17972019-03-16 10:28:42 +0000669 final LabeledIntent.Result result =
670 labeledIntent.resolve(mContext, titleChooser, textLanguagesBundle);
Tony Makac9b4d82019-02-15 13:57:38 +0000671 if (result == null) {
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100672 continue;
673 }
Tony Mak72e17972019-03-16 10:28:42 +0000674
675 final Intent intent = result.resolvedIntent;
Tony Makac9b4d82019-02-15 13:57:38 +0000676 final RemoteAction action = result.remoteAction;
Jan Althaus20d346e2018-03-23 14:03:52 +0100677 if (isPrimaryAction) {
678 // For O backwards compatibility, the first RemoteAction is also written to the
679 // legacy API fields.
680 builder.setIcon(action.getIcon().loadDrawable(mContext));
681 builder.setLabel(action.getTitle().toString());
Tony Mak72e17972019-03-16 10:28:42 +0000682 builder.setIntent(intent);
Jan Althaus20d346e2018-03-23 14:03:52 +0100683 builder.setOnClickListener(TextClassification.createIntentOnClickListener(
Tony Mak72e17972019-03-16 10:28:42 +0000684 TextClassification.createPendingIntent(
685 mContext, intent, labeledIntent.requestCode)));
Jan Althaus20d346e2018-03-23 14:03:52 +0100686 isPrimaryAction = false;
687 }
Tony Mak72e17972019-03-16 10:28:42 +0000688 builder.addAction(action, intent);
Jan Althaus20d346e2018-03-23 14:03:52 +0100689 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000690 return builder.setId(createId(text, start, end)).build();
Jan Althaus92d76832017-09-27 18:14:35 +0200691 }
692
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000693 /**
Tony Mak72e17972019-03-16 10:28:42 +0000694 * Returns a bundle pair with language detection information for extras.
695 * <p>
696 * Pair.first = textLanguagesBundle - A bundle containing information about all detected
697 * languages in the text. May be null if language detection fails or is disabled. This is
698 * typically expected to be added to a textClassifier generated remote action intent.
699 * See {@link ExtrasUtils#putTextLanguagesExtra(Bundle, Bundle)}.
700 * See {@link ExtrasUtils#getTopLanguage(Intent)}.
701 * <p>
702 * Pair.second = foreignLanguageBundle - A bundle with the language and confidence score if the
703 * system finds the text to be in a foreign language. Otherwise is null.
704 * See {@link TextClassification.Builder#setForeignLanguageExtra(Bundle)}.
705 *
706 * @param context the context of the text to detect languages for
707 * @param start the start index of the text
708 * @param end the end index of the text
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000709 */
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000710 // TODO: Revisit this algorithm.
711 // TODO: Consider making this public API.
Tony Mak72e17972019-03-16 10:28:42 +0000712 private Pair<Bundle, Bundle> generateLanguageBundles(String context, int start, int end) {
Tony Mak159f0282019-03-01 14:03:25 +0000713 if (!mSettings.isTranslateInClassificationEnabled()) {
714 return null;
715 }
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100716 try {
Tony Mak159f0282019-03-01 14:03:25 +0000717 final float threshold = getLangIdThreshold();
718 if (threshold < 0 || threshold > 1) {
719 Log.w(LOG_TAG,
720 "[detectForeignLanguage] unexpected threshold is found: " + threshold);
Tony Mak72e17972019-03-16 10:28:42 +0000721 return Pair.create(null, null);
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000722 }
723
Tony Mak72e17972019-03-16 10:28:42 +0000724 final EntityConfidence languageScores = detectLanguages(context, start, end);
725 if (languageScores.getEntities().isEmpty()) {
726 return Pair.create(null, null);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100727 }
728
Tony Mak72e17972019-03-16 10:28:42 +0000729 final Bundle textLanguagesBundle = new Bundle();
730 ExtrasUtils.putTopLanguageScores(textLanguagesBundle, languageScores);
731
732 final String language = languageScores.getEntities().get(0);
733 final float score = languageScores.getConfidenceScore(language);
734 if (score < threshold) {
735 return Pair.create(textLanguagesBundle, null);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100736 }
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000737
Tony Mak72e17972019-03-16 10:28:42 +0000738 Log.v(LOG_TAG, String.format(
739 Locale.US, "Language detected: <%s:%.2f>", language, score));
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100740
Tony Mak72e17972019-03-16 10:28:42 +0000741 final Locale detected = new Locale(language);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100742 final LocaleList deviceLocales = LocaleList.getDefault();
743 final int size = deviceLocales.size();
744 for (int i = 0; i < size; i++) {
745 if (deviceLocales.get(i).getLanguage().equals(detected.getLanguage())) {
Tony Mak72e17972019-03-16 10:28:42 +0000746 return Pair.create(textLanguagesBundle, null);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100747 }
748 }
Tony Mak72e17972019-03-16 10:28:42 +0000749 final Bundle foreignLanguageBundle = ExtrasUtils.createForeignLanguageExtra(
750 detected.getLanguage(), score, getLangIdImpl().getVersion());
751 return Pair.create(textLanguagesBundle, foreignLanguageBundle);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100752 } catch (Throwable t) {
Tony Mak72e17972019-03-16 10:28:42 +0000753 Log.e(LOG_TAG, "Error generating language bundles.", t);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100754 }
Tony Mak72e17972019-03-16 10:28:42 +0000755 return Pair.create(null, null);
756 }
757
758 /**
759 * Detect the language of a piece of text by taking surrounding text into consideration.
760 *
761 * @param text text providing context for the text for which its language is to be detected
762 * @param start the start index of the text to detect its language
763 * @param end the end index of the text to detect its language
764 */
765 // TODO: Revisit this algorithm.
766 private EntityConfidence detectLanguages(String text, int start, int end)
767 throws FileNotFoundException {
768 Preconditions.checkArgument(start >= 0);
769 Preconditions.checkArgument(end <= text.length());
770 Preconditions.checkArgument(start <= end);
771
772 final float[] langIdContextSettings = mSettings.getLangIdContextSettings();
773 // The minimum size of text to prefer for detection.
774 final int minimumTextSize = (int) langIdContextSettings[0];
775 // For reducing the score when text is less than the preferred size.
776 final float penalizeRatio = langIdContextSettings[1];
777 // Original detection score to surrounding text detection score ratios.
778 final float subjectTextScoreRatio = langIdContextSettings[2];
779 final float moreTextScoreRatio = 1f - subjectTextScoreRatio;
780 Log.v(LOG_TAG,
781 String.format(Locale.US, "LangIdContextSettings: "
Abodunrinwa Toki0634af32019-04-04 13:10:59 +0100782 + "minimumTextSize=%d, penalizeRatio=%.2f, "
783 + "subjectTextScoreRatio=%.2f, moreTextScoreRatio=%.2f",
Tony Mak72e17972019-03-16 10:28:42 +0000784 minimumTextSize, penalizeRatio, subjectTextScoreRatio, moreTextScoreRatio));
785
786 if (end - start < minimumTextSize && penalizeRatio <= 0) {
787 return new EntityConfidence(Collections.emptyMap());
788 }
789
790 final String subject = text.substring(start, end);
791 final EntityConfidence scores = detectLanguages(subject);
792
793 if (subject.length() >= minimumTextSize
794 || subject.length() == text.length()
795 || subjectTextScoreRatio * penalizeRatio >= 1) {
796 return scores;
797 }
798
799 final EntityConfidence moreTextScores;
800 if (moreTextScoreRatio >= 0) {
801 // Attempt to grow the detection text to be at least minimumTextSize long.
802 final String moreText = Utils.getSubString(text, start, end, minimumTextSize);
803 moreTextScores = detectLanguages(moreText);
804 } else {
805 moreTextScores = new EntityConfidence(Collections.emptyMap());
806 }
807
808 // Combine the original detection scores with the those returned after including more text.
809 final Map<String, Float> newScores = new ArrayMap<>();
810 final Set<String> languages = new ArraySet<>();
811 languages.addAll(scores.getEntities());
812 languages.addAll(moreTextScores.getEntities());
813 for (String language : languages) {
814 final float score = (subjectTextScoreRatio * scores.getConfidenceScore(language)
815 + moreTextScoreRatio * moreTextScores.getConfidenceScore(language))
816 * penalizeRatio;
817 newScores.put(language, score);
818 }
819 return new EntityConfidence(newScores);
820 }
821
822 /**
823 * Detect languages for the specified text.
824 */
825 private EntityConfidence detectLanguages(String text) throws FileNotFoundException {
826 final LangIdModel langId = getLangIdImpl();
827 final LangIdModel.LanguageResult[] langResults = langId.detectLanguages(text);
828 final Map<String, Float> languagesMap = new ArrayMap<>();
829 for (LanguageResult langResult : langResults) {
830 languagesMap.put(langResult.getLanguage(), langResult.getScore());
831 }
832 return new EntityConfidence(languagesMap);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100833 }
834
Tony Mak159f0282019-03-01 14:03:25 +0000835 private float getLangIdThreshold() {
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000836 try {
837 return mSettings.getLangIdThresholdOverride() >= 0
838 ? mSettings.getLangIdThresholdOverride()
Tony Mak159f0282019-03-01 14:03:25 +0000839 : getLangIdImpl().getLangIdThreshold();
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000840 } catch (FileNotFoundException e) {
841 final float defaultThreshold = 0.5f;
842 Log.v(LOG_TAG, "Using default foreign language threshold: " + defaultThreshold);
843 return defaultThreshold;
844 }
845 }
846
Tony Makf93e9e52018-07-16 14:46:29 +0200847 @Override
848 public void dump(@NonNull IndentingPrintWriter printWriter) {
849 synchronized (mLock) {
Tony Makf93e9e52018-07-16 14:46:29 +0200850 printWriter.println("TextClassifierImpl:");
851 printWriter.increaseIndent();
Tony Makba228422018-10-25 21:30:40 +0100852 printWriter.println("Annotator model file(s):");
Tony Makf93e9e52018-07-16 14:46:29 +0200853 printWriter.increaseIndent();
Tony Makba228422018-10-25 21:30:40 +0100854 for (ModelFileManager.ModelFile modelFile :
855 mAnnotatorModelFileManager.listModelFiles()) {
856 printWriter.println(modelFile.toString());
857 }
858 printWriter.decreaseIndent();
859 printWriter.println("LangID model file(s):");
Tony Makadbebcc2018-10-30 18:59:06 +0000860 printWriter.increaseIndent();
Tony Makba228422018-10-25 21:30:40 +0100861 for (ModelFileManager.ModelFile modelFile :
862 mLangIdModelFileManager.listModelFiles()) {
Tony Makf93e9e52018-07-16 14:46:29 +0200863 printWriter.println(modelFile.toString());
864 }
865 printWriter.decreaseIndent();
Tony Makadbebcc2018-10-30 18:59:06 +0000866 printWriter.println("Actions model file(s):");
867 printWriter.increaseIndent();
868 for (ModelFileManager.ModelFile modelFile :
869 mActionsModelFileManager.listModelFiles()) {
870 printWriter.println(modelFile.toString());
871 }
872 printWriter.decreaseIndent();
Tony Makf93e9e52018-07-16 14:46:29 +0200873 printWriter.printPair("mFallback", mFallback);
874 printWriter.decreaseIndent();
875 printWriter.println();
876 }
877 }
878
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800879 /**
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100880 * Closes the ParcelFileDescriptor, if non-null, and logs any errors that occur.
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100881 */
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100882 private static void maybeCloseAndLogError(@Nullable ParcelFileDescriptor fd) {
883 if (fd == null) {
884 return;
885 }
886
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100887 try {
888 fd.close();
889 } catch (IOException e) {
890 Log.e(LOG_TAG, "Error closing file.", e);
891 }
892 }
893
894 /**
Tony Mak159f0282019-03-01 14:03:25 +0000895 * Returns the locales string for the current resources configuration.
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000896 */
Tony Mak159f0282019-03-01 14:03:25 +0000897 private String getResourceLocalesString() {
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000898 try {
Tony Mak159f0282019-03-01 14:03:25 +0000899 return mContext.getResources().getConfiguration().getLocales().toLanguageTags();
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000900 } catch (NullPointerException e) {
901 // NPE is unexpected. Erring on the side of caution.
Tony Mak159f0282019-03-01 14:03:25 +0000902 return LocaleList.getDefault().toLanguageTags();
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000903 }
904 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800905}