blob: d7149ee05b575cc6a9ce01ae0adf28c0cf03978d [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) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000133 mContext = Objects.requireNonNull(context);
134 mFallback = Objects.requireNonNull(fallback);
135 mSettings = Objects.requireNonNull(settings);
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000136 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) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000183 Objects.requireNonNull(request);
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100184 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) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000248 Objects.requireNonNull(request);
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100249 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) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000288 Objects.requireNonNull(request);
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100289 Utils.checkMainThread();
Tony Makc5a74322020-02-04 17:18:15 +0000290 if (!Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength())) {
291 return mFallback.generateLinks(request);
292 }
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000293
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100294 if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
295 return Utils.generateLegacyLinks(request);
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000296 }
297
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100298 final String textString = request.getText().toString();
Richard Ledley68d94522017-10-05 10:52:19 +0100299 final TextLinks.Builder builder = new TextLinks.Builder(textString);
Richard Ledley9cfa6062018-01-15 13:13:29 +0000300
Richard Ledley68d94522017-10-05 10:52:19 +0100301 try {
Jan Althaus31efdc32018-02-19 22:23:13 +0100302 final long startTimeMs = System.currentTimeMillis();
Jan Althausa1652cf2018-03-29 17:51:57 +0200303 final ZonedDateTime refTime = ZonedDateTime.now();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100304 final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
305 ? request.getEntityConfig().resolveEntityListModifications(
Abodunrinwa Toki0634af32019-04-04 13:10:59 +0100306 getEntitiesForHints(request.getEntityConfig().getHints()))
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100307 : mSettings.getEntityListDefault();
Tony Mak159f0282019-03-01 14:03:25 +0000308 final String localesString = concatenateLocales(request.getDefaultLocales());
309 final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100310 final AnnotatorModel annotatorImpl =
311 getAnnotatorImpl(request.getDefaultLocales());
Tony Makb6afd3c2019-04-05 15:45:18 +0100312 final boolean isSerializedEntityDataEnabled =
313 ExtrasUtils.isSerializedEntityDataEnabled(request);
Tony Mak64c4cb22018-09-25 13:38:28 +0100314 final AnnotatorModel.AnnotatedSpan[] annotations =
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100315 annotatorImpl.annotate(
Tony Makf99ee172018-11-23 12:14:39 +0000316 textString,
317 new AnnotatorModel.AnnotationOptions(
318 refTime.toInstant().toEpochMilli(),
319 refTime.getZone().getId(),
Tony Mak159f0282019-03-01 14:03:25 +0000320 localesString,
Tony Makb6afd3c2019-04-05 15:45:18 +0100321 detectLanguageTags,
322 entitiesToIdentify,
323 AnnotatorModel.AnnotationUsecase.SMART.getValue(),
324 isSerializedEntityDataEnabled));
Tony Mak64c4cb22018-09-25 13:38:28 +0100325 for (AnnotatorModel.AnnotatedSpan span : annotations) {
326 final AnnotatorModel.ClassificationResult[] results =
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100327 span.getClassification();
328 if (results.length == 0
329 || !entitiesToIdentify.contains(results[0].getCollection())) {
Richard Ledleydb18a572017-11-30 17:33:51 +0000330 continue;
331 }
Tony Mak72e17972019-03-16 10:28:42 +0000332 final Map<String, Float> entityScores = new ArrayMap<>();
Richard Ledley68d94522017-10-05 10:52:19 +0100333 for (int i = 0; i < results.length; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100334 entityScores.put(results[i].getCollection(), results[i].getScore());
Richard Ledley68d94522017-10-05 10:52:19 +0100335 }
Tony Makb6afd3c2019-04-05 15:45:18 +0100336 Bundle extras = new Bundle();
337 if (isSerializedEntityDataEnabled) {
338 ExtrasUtils.putEntities(extras, results);
339 }
340 builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores, extras);
Richard Ledley68d94522017-10-05 10:52:19 +0100341 }
Jan Althaus31efdc32018-02-19 22:23:13 +0100342 final TextLinks links = builder.build();
343 final long endTimeMs = System.currentTimeMillis();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100344 final String callingPackageName = request.getCallingPackageName() == null
345 ? mContext.getPackageName() // local (in process) TC.
346 : request.getCallingPackageName();
Jan Althaus31efdc32018-02-19 22:23:13 +0100347 mGenerateLinksLogger.logGenerateLinks(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100348 request.getText(), links, callingPackageName, endTimeMs - startTimeMs);
Jan Althaus31efdc32018-02-19 22:23:13 +0100349 return links;
Richard Ledley68d94522017-10-05 10:52:19 +0100350 } catch (Throwable t) {
351 // Avoid throwing from this method. Log the error.
352 Log.e(LOG_TAG, "Error getting links info.", t);
353 }
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100354 return mFallback.generateLinks(request);
Richard Ledley68d94522017-10-05 10:52:19 +0100355 }
356
Jan Althaus108aad32018-01-30 15:26:55 +0100357 /** @inheritDoc */
358 @Override
359 public int getMaxGenerateLinksTextLength() {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000360 return mSettings.getGenerateLinksMaxTextLength();
Jan Althaus108aad32018-01-30 15:26:55 +0100361 }
362
Richard Ledley1fc998b2018-02-16 15:45:06 +0000363 private Collection<String> getEntitiesForHints(Collection<String> hints) {
Jan Althaus0aacdb62018-02-19 11:44:37 +0100364 final boolean editable = hints.contains(HINT_TEXT_IS_EDITABLE);
365 final boolean notEditable = hints.contains(HINT_TEXT_IS_NOT_EDITABLE);
366
367 // Use the default if there is no hint, or conflicting ones.
368 final boolean useDefault = editable == notEditable;
369 if (useDefault) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000370 return mSettings.getEntityListDefault();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100371 } else if (editable) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000372 return mSettings.getEntityListEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100373 } else { // notEditable
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000374 return mSettings.getEntityListNotEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100375 }
Richard Ledleydb18a572017-11-30 17:33:51 +0000376 }
377
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100378 /** @inheritDoc */
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000379 @Override
380 public void onSelectionEvent(SelectionEvent event) {
Tony Mak5a5f0d52019-01-08 11:07:23 +0000381 mSessionLogger.writeEvent(event);
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000382 }
383
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000384 @Override
Tony Make94e0782018-12-14 11:57:54 +0800385 public void onTextClassifierEvent(TextClassifierEvent event) {
386 if (DEBUG) {
387 Log.d(DEFAULT_LOG_TAG, "onTextClassifierEvent() called with: event = [" + event + "]");
388 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000389 try {
Abodunrinwa Toki52586332019-05-22 18:43:16 +0100390 final SelectionEvent selEvent = event.toSelectionEvent();
391 if (selEvent != null) {
392 mSessionLogger.writeEvent(selEvent);
393 } else {
394 mTextClassifierEventTronLogger.writeEvent(event);
395 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000396 } catch (Exception e) {
397 Log.e(LOG_TAG, "Error writing event", e);
398 }
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000399 }
400
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100401 /** @inheritDoc */
402 @Override
403 public TextLanguage detectLanguage(@NonNull TextLanguage.Request request) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000404 Objects.requireNonNull(request);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100405 Utils.checkMainThread();
406 try {
407 final TextLanguage.Builder builder = new TextLanguage.Builder();
408 final LangIdModel.LanguageResult[] langResults =
409 getLangIdImpl().detectLanguages(request.getText().toString());
410 for (int i = 0; i < langResults.length; i++) {
411 builder.putLocale(
412 ULocale.forLanguageTag(langResults[i].getLanguage()),
413 langResults[i].getScore());
414 }
415 return builder.build();
416 } catch (Throwable t) {
417 // Avoid throwing from this method. Log the error.
418 Log.e(LOG_TAG, "Error detecting text language.", t);
419 }
420 return mFallback.detectLanguage(request);
421 }
422
Tony Makadbebcc2018-10-30 18:59:06 +0000423 @Override
424 public ConversationActions suggestConversationActions(ConversationActions.Request request) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000425 Objects.requireNonNull(request);
Tony Makadbebcc2018-10-30 18:59:06 +0000426 Utils.checkMainThread();
427 try {
428 ActionsSuggestionsModel actionsImpl = getActionsImpl();
429 if (actionsImpl == null) {
430 // Actions model is optional, fallback if it is not available.
431 return mFallback.suggestConversationActions(request);
432 }
Tony Makf99ee172018-11-23 12:14:39 +0000433 ActionsSuggestionsModel.ConversationMessage[] nativeMessages =
Tony Mak159f0282019-03-01 14:03:25 +0000434 ActionsSuggestionsHelper.toNativeMessages(
435 request.getConversation(), this::detectLanguageTagsFromText);
Tony Makf99ee172018-11-23 12:14:39 +0000436 if (nativeMessages.length == 0) {
437 return mFallback.suggestConversationActions(request);
Tony Makadbebcc2018-10-30 18:59:06 +0000438 }
439 ActionsSuggestionsModel.Conversation nativeConversation =
Tony Makf99ee172018-11-23 12:14:39 +0000440 new ActionsSuggestionsModel.Conversation(nativeMessages);
Tony Makadbebcc2018-10-30 18:59:06 +0000441
442 ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions =
Tony Makae33c3b2019-01-31 14:29:19 +0000443 actionsImpl.suggestActionsWithIntents(
444 nativeConversation,
445 null,
446 mContext,
Tony Mak2a3adb72019-03-20 17:55:53 +0000447 getResourceLocalesString(),
448 getAnnotatorImpl(LocaleList.getDefault()));
Tony Makae33c3b2019-01-31 14:29:19 +0000449 return createConversationActionResult(request, nativeSuggestions);
Tony Makadbebcc2018-10-30 18:59:06 +0000450 } catch (Throwable t) {
451 // Avoid throwing from this method. Log the error.
452 Log.e(LOG_TAG, "Error suggesting conversation actions.", t);
453 }
454 return mFallback.suggestConversationActions(request);
455 }
456
Tony Makc12035e2019-02-26 17:45:34 +0000457 /**
458 * Returns the {@link ConversationAction} result, with a non-null extras.
459 * <p>
460 * Whenever the RemoteAction is non-null, you can expect its corresponding intent
461 * with a non-null component name is in the extras.
462 */
Tony Makae33c3b2019-01-31 14:29:19 +0000463 private ConversationActions createConversationActionResult(
464 ConversationActions.Request request,
465 ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions) {
466 Collection<String> expectedTypes = resolveActionTypesFromRequest(request);
467 List<ConversationAction> conversationActions = new ArrayList<>();
468 for (ActionsSuggestionsModel.ActionSuggestion nativeSuggestion : nativeSuggestions) {
Tony Makae33c3b2019-01-31 14:29:19 +0000469 String actionType = nativeSuggestion.getActionType();
470 if (!expectedTypes.contains(actionType)) {
471 continue;
472 }
Tony Mak8ab9b182019-03-01 16:44:17 +0000473 LabeledIntent.Result labeledIntentResult =
474 ActionsSuggestionsHelper.createLabeledIntentResult(
475 mContext,
476 mTemplateIntentFactory,
477 nativeSuggestion);
Tony Makae33c3b2019-01-31 14:29:19 +0000478 RemoteAction remoteAction = null;
Tony Mak8ab9b182019-03-01 16:44:17 +0000479 Bundle extras = new Bundle();
480 if (labeledIntentResult != null) {
481 remoteAction = labeledIntentResult.remoteAction;
482 ExtrasUtils.putActionIntent(extras, labeledIntentResult.resolvedIntent);
Tony Makae33c3b2019-01-31 14:29:19 +0000483 }
Tony Makfdb35542019-03-22 12:01:50 +0000484 ExtrasUtils.putSerializedEntityData(extras, nativeSuggestion.getSerializedEntityData());
Tony Mak09214422019-03-01 18:25:23 +0000485 ExtrasUtils.putEntitiesExtras(
486 extras,
487 TemplateIntentFactory.nameVariantsToBundle(nativeSuggestion.getEntityData()));
Tony Makae33c3b2019-01-31 14:29:19 +0000488 conversationActions.add(
489 new ConversationAction.Builder(actionType)
490 .setConfidenceScore(nativeSuggestion.getScore())
491 .setTextReply(nativeSuggestion.getResponseText())
492 .setAction(remoteAction)
Tony Makc12035e2019-02-26 17:45:34 +0000493 .setExtras(extras)
Tony Makae33c3b2019-01-31 14:29:19 +0000494 .build());
495 }
Tony Makc12035e2019-02-26 17:45:34 +0000496 conversationActions =
497 ActionsSuggestionsHelper.removeActionsWithDuplicates(conversationActions);
Tony Makaa496d02019-04-11 17:38:47 +0100498 if (request.getMaxSuggestions() >= 0
499 && conversationActions.size() > request.getMaxSuggestions()) {
500 conversationActions = conversationActions.subList(0, request.getMaxSuggestions());
501 }
Tony Makae33c3b2019-01-31 14:29:19 +0000502 String resultId = ActionsSuggestionsHelper.createResultId(
503 mContext,
504 request.getConversation(),
505 mActionModelInUse.getVersion(),
506 mActionModelInUse.getSupportedLocales());
507 return new ConversationActions(conversationActions, resultId);
508 }
509
Tony Mak82fa8d92018-12-07 17:37:43 +0000510 @Nullable
511 private String detectLanguageTagsFromText(CharSequence text) {
Tony Mak159f0282019-03-01 14:03:25 +0000512 if (!mSettings.isDetectLanguagesFromTextEnabled()) {
513 return null;
514 }
515 final float threshold = getLangIdThreshold();
516 if (threshold < 0 || threshold > 1) {
517 Log.w(LOG_TAG,
518 "[detectLanguageTagsFromText] unexpected threshold is found: " + threshold);
519 return null;
520 }
Tony Mak82fa8d92018-12-07 17:37:43 +0000521 TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
522 TextLanguage textLanguage = detectLanguage(request);
523 int localeHypothesisCount = textLanguage.getLocaleHypothesisCount();
524 List<String> languageTags = new ArrayList<>();
Tony Mak82fa8d92018-12-07 17:37:43 +0000525 for (int i = 0; i < localeHypothesisCount; i++) {
526 ULocale locale = textLanguage.getLocale(i);
Tony Mak159f0282019-03-01 14:03:25 +0000527 if (textLanguage.getConfidenceScore(locale) < threshold) {
Tony Mak82fa8d92018-12-07 17:37:43 +0000528 break;
529 }
530 languageTags.add(locale.toLanguageTag());
531 }
532 if (languageTags.isEmpty()) {
Tony Mak159f0282019-03-01 14:03:25 +0000533 return null;
Tony Mak82fa8d92018-12-07 17:37:43 +0000534 }
535 return String.join(",", languageTags);
536 }
537
Tony Makadbebcc2018-10-30 18:59:06 +0000538 private Collection<String> resolveActionTypesFromRequest(ConversationActions.Request request) {
539 List<String> defaultActionTypes =
Tony Makae85aae2019-01-09 15:59:56 +0000540 request.getHints().contains(ConversationActions.Request.HINT_FOR_NOTIFICATION)
Tony Makadbebcc2018-10-30 18:59:06 +0000541 ? mSettings.getNotificationConversationActionTypes()
542 : mSettings.getInAppConversationActionTypes();
Tony Makae85aae2019-01-09 15:59:56 +0000543 return request.getTypeConfig().resolveEntityListModifications(defaultActionTypes);
Tony Makadbebcc2018-10-30 18:59:06 +0000544 }
545
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100546 private AnnotatorModel getAnnotatorImpl(LocaleList localeList)
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100547 throws FileNotFoundException {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800548 synchronized (mLock) {
Tony Makadbebcc2018-10-30 18:59:06 +0000549 localeList = localeList == null ? LocaleList.getDefault() : localeList;
Tony Makba228422018-10-25 21:30:40 +0100550 final ModelFileManager.ModelFile bestModel =
551 mAnnotatorModelFileManager.findBestModelFile(localeList);
Jan Althausef0156d2018-01-29 19:28:41 +0100552 if (bestModel == null) {
Tony Makba228422018-10-25 21:30:40 +0100553 throw new FileNotFoundException(
554 "No annotator model for " + localeList.toLanguageTags());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100555 }
Tony Makba228422018-10-25 21:30:40 +0100556 if (mAnnotatorImpl == null || !Objects.equals(mAnnotatorModelInUse, bestModel)) {
Jan Althausef0156d2018-01-29 19:28:41 +0100557 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Tony Makba228422018-10-25 21:30:40 +0100558 final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
Jan Althausef0156d2018-01-29 19:28:41 +0100559 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100560 try {
Tony Makba228422018-10-25 21:30:40 +0100561 if (pfd != null) {
Tony Makae33c3b2019-01-31 14:29:19 +0000562 // The current annotator model may be still used by another thread / model.
563 // Do not call close() here, and let the GC to clean it up when no one else
564 // is using it.
Tony Makba228422018-10-25 21:30:40 +0100565 mAnnotatorImpl = new AnnotatorModel(pfd.getFd());
566 mAnnotatorModelInUse = bestModel;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100567 }
568 } finally {
Tony Makba228422018-10-25 21:30:40 +0100569 maybeCloseAndLogError(pfd);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100570 }
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000571 }
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100572 return mAnnotatorImpl;
573 }
574 }
575
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100576 private LangIdModel getLangIdImpl() throws FileNotFoundException {
577 synchronized (mLock) {
Tony Mak6fc43182019-03-07 17:30:33 +0000578 final ModelFileManager.ModelFile bestModel =
579 mLangIdModelFileManager.findBestModelFile(null);
580 if (bestModel == null) {
581 throw new FileNotFoundException("No LangID model is found");
582 }
583 if (mLangIdImpl == null || !Objects.equals(mLangIdModelInUse, bestModel)) {
584 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Tony Makba228422018-10-25 21:30:40 +0100585 final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
586 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100587 try {
Tony Makba228422018-10-25 21:30:40 +0100588 if (pfd != null) {
589 mLangIdImpl = new LangIdModel(pfd.getFd());
Tony Mak6fc43182019-03-07 17:30:33 +0000590 mLangIdModelInUse = bestModel;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100591 }
592 } finally {
Tony Makba228422018-10-25 21:30:40 +0100593 maybeCloseAndLogError(pfd);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100594 }
595 }
596 return mLangIdImpl;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800597 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800598 }
599
Tony Makadbebcc2018-10-30 18:59:06 +0000600 @Nullable
601 private ActionsSuggestionsModel getActionsImpl() throws FileNotFoundException {
602 synchronized (mLock) {
Tony Mak6fc43182019-03-07 17:30:33 +0000603 // TODO: Use LangID to determine the locale we should use here?
604 final ModelFileManager.ModelFile bestModel =
605 mActionsModelFileManager.findBestModelFile(LocaleList.getDefault());
606 if (bestModel == null) {
607 return null;
608 }
609 if (mActionsImpl == null || !Objects.equals(mActionModelInUse, bestModel)) {
610 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Tony Makadbebcc2018-10-30 18:59:06 +0000611 final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
612 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
613 try {
Tony Mak20fe1872019-03-22 15:35:15 +0000614 if (pfd == null) {
615 Log.d(LOG_TAG, "Failed to read the model file: " + bestModel.getPath());
616 return null;
Tony Makadbebcc2018-10-30 18:59:06 +0000617 }
Tony Mak20fe1872019-03-22 15:35:15 +0000618 ActionsModelParams params = mActionsModelParamsSupplier.get();
619 mActionsImpl = new ActionsSuggestionsModel(
620 pfd.getFd(), params.getSerializedPreconditions(bestModel));
621 mActionModelInUse = bestModel;
Tony Makadbebcc2018-10-30 18:59:06 +0000622 } finally {
623 maybeCloseAndLogError(pfd);
624 }
625 }
626 return mActionsImpl;
627 }
628 }
629
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100630 private String createId(String text, int start, int end) {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800631 synchronized (mLock) {
Tony Makba228422018-10-25 21:30:40 +0100632 return SelectionSessionLogger.createId(text, start, end, mContext,
633 mAnnotatorModelInUse.getVersion(),
634 mAnnotatorModelInUse.getSupportedLocales());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100635 }
636 }
637
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100638 private static String concatenateLocales(@Nullable LocaleList locales) {
639 return (locales == null) ? "" : locales.toLanguageTags();
640 }
641
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100642 private TextClassification createClassificationResult(
Tony Mak64c4cb22018-09-25 13:38:28 +0100643 AnnotatorModel.ClassificationResult[] classifications,
Jan Althausa1652cf2018-03-29 17:51:57 +0200644 String text, int start, int end, @Nullable Instant referenceTime) {
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000645 final String classifiedText = text.substring(start, end);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100646 final TextClassification.Builder builder = new TextClassification.Builder()
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000647 .setText(classifiedText);
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +0000648
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100649 final int typeCount = classifications.length;
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100650 AnnotatorModel.ClassificationResult highestScoringResult =
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100651 typeCount > 0 ? classifications[0] : null;
652 for (int i = 0; i < typeCount; i++) {
Tony Makfdb35542019-03-22 12:01:50 +0000653 builder.setEntityType(classifications[i]);
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100654 if (classifications[i].getScore() > highestScoringResult.getScore()) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100655 highestScoringResult = classifications[i];
Jan Althaus705b9e92018-01-22 18:22:29 +0100656 }
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000657 }
658
Tony Mak72e17972019-03-16 10:28:42 +0000659 final Pair<Bundle, Bundle> languagesBundles = generateLanguageBundles(text, start, end);
660 final Bundle textLanguagesBundle = languagesBundles.first;
661 final Bundle foreignLanguageBundle = languagesBundles.second;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000662 builder.setForeignLanguageExtra(foreignLanguageBundle);
663
Jan Althaus20d346e2018-03-23 14:03:52 +0100664 boolean isPrimaryAction = true;
Tony Mak72e17972019-03-16 10:28:42 +0000665 final List<LabeledIntent> labeledIntents = mClassificationIntentFactory.create(
Tony Makfc039c32019-01-17 19:32:08 +0000666 mContext,
667 classifiedText,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000668 foreignLanguageBundle != null,
Tony Makfc039c32019-01-17 19:32:08 +0000669 referenceTime,
670 highestScoringResult);
Tony Mak72e17972019-03-16 10:28:42 +0000671 final LabeledIntent.TitleChooser titleChooser =
Tony Makac9b4d82019-02-15 13:57:38 +0000672 (labeledIntent, resolveInfo) -> labeledIntent.titleWithoutEntity;
Tony Mak72e17972019-03-16 10:28:42 +0000673
Tony Makfc039c32019-01-17 19:32:08 +0000674 for (LabeledIntent labeledIntent : labeledIntents) {
Tony Mak72e17972019-03-16 10:28:42 +0000675 final LabeledIntent.Result result =
676 labeledIntent.resolve(mContext, titleChooser, textLanguagesBundle);
Tony Makac9b4d82019-02-15 13:57:38 +0000677 if (result == null) {
Abodunrinwa Toki253827f2018-04-24 19:19:48 +0100678 continue;
679 }
Tony Mak72e17972019-03-16 10:28:42 +0000680
681 final Intent intent = result.resolvedIntent;
Tony Makac9b4d82019-02-15 13:57:38 +0000682 final RemoteAction action = result.remoteAction;
Jan Althaus20d346e2018-03-23 14:03:52 +0100683 if (isPrimaryAction) {
684 // For O backwards compatibility, the first RemoteAction is also written to the
685 // legacy API fields.
686 builder.setIcon(action.getIcon().loadDrawable(mContext));
687 builder.setLabel(action.getTitle().toString());
Tony Mak72e17972019-03-16 10:28:42 +0000688 builder.setIntent(intent);
Jan Althaus20d346e2018-03-23 14:03:52 +0100689 builder.setOnClickListener(TextClassification.createIntentOnClickListener(
Tony Mak72e17972019-03-16 10:28:42 +0000690 TextClassification.createPendingIntent(
691 mContext, intent, labeledIntent.requestCode)));
Jan Althaus20d346e2018-03-23 14:03:52 +0100692 isPrimaryAction = false;
693 }
Tony Mak72e17972019-03-16 10:28:42 +0000694 builder.addAction(action, intent);
Jan Althaus20d346e2018-03-23 14:03:52 +0100695 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000696 return builder.setId(createId(text, start, end)).build();
Jan Althaus92d76832017-09-27 18:14:35 +0200697 }
698
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000699 /**
Tony Mak72e17972019-03-16 10:28:42 +0000700 * Returns a bundle pair with language detection information for extras.
701 * <p>
702 * Pair.first = textLanguagesBundle - A bundle containing information about all detected
703 * languages in the text. May be null if language detection fails or is disabled. This is
704 * typically expected to be added to a textClassifier generated remote action intent.
705 * See {@link ExtrasUtils#putTextLanguagesExtra(Bundle, Bundle)}.
706 * See {@link ExtrasUtils#getTopLanguage(Intent)}.
707 * <p>
708 * Pair.second = foreignLanguageBundle - A bundle with the language and confidence score if the
709 * system finds the text to be in a foreign language. Otherwise is null.
710 * See {@link TextClassification.Builder#setForeignLanguageExtra(Bundle)}.
711 *
712 * @param context the context of the text to detect languages for
713 * @param start the start index of the text
714 * @param end the end index of the text
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000715 */
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000716 // TODO: Revisit this algorithm.
717 // TODO: Consider making this public API.
Tony Mak72e17972019-03-16 10:28:42 +0000718 private Pair<Bundle, Bundle> generateLanguageBundles(String context, int start, int end) {
Tony Mak159f0282019-03-01 14:03:25 +0000719 if (!mSettings.isTranslateInClassificationEnabled()) {
720 return null;
721 }
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100722 try {
Tony Mak159f0282019-03-01 14:03:25 +0000723 final float threshold = getLangIdThreshold();
724 if (threshold < 0 || threshold > 1) {
725 Log.w(LOG_TAG,
726 "[detectForeignLanguage] unexpected threshold is found: " + threshold);
Tony Mak72e17972019-03-16 10:28:42 +0000727 return Pair.create(null, null);
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000728 }
729
Tony Mak72e17972019-03-16 10:28:42 +0000730 final EntityConfidence languageScores = detectLanguages(context, start, end);
731 if (languageScores.getEntities().isEmpty()) {
732 return Pair.create(null, null);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100733 }
734
Tony Mak72e17972019-03-16 10:28:42 +0000735 final Bundle textLanguagesBundle = new Bundle();
736 ExtrasUtils.putTopLanguageScores(textLanguagesBundle, languageScores);
737
738 final String language = languageScores.getEntities().get(0);
739 final float score = languageScores.getConfidenceScore(language);
740 if (score < threshold) {
741 return Pair.create(textLanguagesBundle, null);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100742 }
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000743
Tony Mak72e17972019-03-16 10:28:42 +0000744 Log.v(LOG_TAG, String.format(
745 Locale.US, "Language detected: <%s:%.2f>", language, score));
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100746
Tony Mak72e17972019-03-16 10:28:42 +0000747 final Locale detected = new Locale(language);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100748 final LocaleList deviceLocales = LocaleList.getDefault();
749 final int size = deviceLocales.size();
750 for (int i = 0; i < size; i++) {
751 if (deviceLocales.get(i).getLanguage().equals(detected.getLanguage())) {
Tony Mak72e17972019-03-16 10:28:42 +0000752 return Pair.create(textLanguagesBundle, null);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100753 }
754 }
Tony Mak72e17972019-03-16 10:28:42 +0000755 final Bundle foreignLanguageBundle = ExtrasUtils.createForeignLanguageExtra(
756 detected.getLanguage(), score, getLangIdImpl().getVersion());
757 return Pair.create(textLanguagesBundle, foreignLanguageBundle);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100758 } catch (Throwable t) {
Tony Mak72e17972019-03-16 10:28:42 +0000759 Log.e(LOG_TAG, "Error generating language bundles.", t);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100760 }
Tony Mak72e17972019-03-16 10:28:42 +0000761 return Pair.create(null, null);
762 }
763
764 /**
765 * Detect the language of a piece of text by taking surrounding text into consideration.
766 *
767 * @param text text providing context for the text for which its language is to be detected
768 * @param start the start index of the text to detect its language
769 * @param end the end index of the text to detect its language
770 */
771 // TODO: Revisit this algorithm.
772 private EntityConfidence detectLanguages(String text, int start, int end)
773 throws FileNotFoundException {
774 Preconditions.checkArgument(start >= 0);
775 Preconditions.checkArgument(end <= text.length());
776 Preconditions.checkArgument(start <= end);
777
778 final float[] langIdContextSettings = mSettings.getLangIdContextSettings();
779 // The minimum size of text to prefer for detection.
780 final int minimumTextSize = (int) langIdContextSettings[0];
781 // For reducing the score when text is less than the preferred size.
782 final float penalizeRatio = langIdContextSettings[1];
783 // Original detection score to surrounding text detection score ratios.
784 final float subjectTextScoreRatio = langIdContextSettings[2];
785 final float moreTextScoreRatio = 1f - subjectTextScoreRatio;
786 Log.v(LOG_TAG,
787 String.format(Locale.US, "LangIdContextSettings: "
Abodunrinwa Toki0634af32019-04-04 13:10:59 +0100788 + "minimumTextSize=%d, penalizeRatio=%.2f, "
789 + "subjectTextScoreRatio=%.2f, moreTextScoreRatio=%.2f",
Tony Mak72e17972019-03-16 10:28:42 +0000790 minimumTextSize, penalizeRatio, subjectTextScoreRatio, moreTextScoreRatio));
791
792 if (end - start < minimumTextSize && penalizeRatio <= 0) {
793 return new EntityConfidence(Collections.emptyMap());
794 }
795
796 final String subject = text.substring(start, end);
797 final EntityConfidence scores = detectLanguages(subject);
798
799 if (subject.length() >= minimumTextSize
800 || subject.length() == text.length()
801 || subjectTextScoreRatio * penalizeRatio >= 1) {
802 return scores;
803 }
804
805 final EntityConfidence moreTextScores;
806 if (moreTextScoreRatio >= 0) {
807 // Attempt to grow the detection text to be at least minimumTextSize long.
808 final String moreText = Utils.getSubString(text, start, end, minimumTextSize);
809 moreTextScores = detectLanguages(moreText);
810 } else {
811 moreTextScores = new EntityConfidence(Collections.emptyMap());
812 }
813
814 // Combine the original detection scores with the those returned after including more text.
815 final Map<String, Float> newScores = new ArrayMap<>();
816 final Set<String> languages = new ArraySet<>();
817 languages.addAll(scores.getEntities());
818 languages.addAll(moreTextScores.getEntities());
819 for (String language : languages) {
820 final float score = (subjectTextScoreRatio * scores.getConfidenceScore(language)
821 + moreTextScoreRatio * moreTextScores.getConfidenceScore(language))
822 * penalizeRatio;
823 newScores.put(language, score);
824 }
825 return new EntityConfidence(newScores);
826 }
827
828 /**
829 * Detect languages for the specified text.
830 */
831 private EntityConfidence detectLanguages(String text) throws FileNotFoundException {
832 final LangIdModel langId = getLangIdImpl();
833 final LangIdModel.LanguageResult[] langResults = langId.detectLanguages(text);
834 final Map<String, Float> languagesMap = new ArrayMap<>();
835 for (LanguageResult langResult : langResults) {
836 languagesMap.put(langResult.getLanguage(), langResult.getScore());
837 }
838 return new EntityConfidence(languagesMap);
Abodunrinwa Toki4bad09c2018-10-25 18:12:28 +0100839 }
840
Tony Mak159f0282019-03-01 14:03:25 +0000841 private float getLangIdThreshold() {
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000842 try {
843 return mSettings.getLangIdThresholdOverride() >= 0
844 ? mSettings.getLangIdThresholdOverride()
Tony Mak159f0282019-03-01 14:03:25 +0000845 : getLangIdImpl().getLangIdThreshold();
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000846 } catch (FileNotFoundException e) {
847 final float defaultThreshold = 0.5f;
848 Log.v(LOG_TAG, "Using default foreign language threshold: " + defaultThreshold);
849 return defaultThreshold;
850 }
851 }
852
Tony Makf93e9e52018-07-16 14:46:29 +0200853 @Override
854 public void dump(@NonNull IndentingPrintWriter printWriter) {
855 synchronized (mLock) {
Tony Makf93e9e52018-07-16 14:46:29 +0200856 printWriter.println("TextClassifierImpl:");
857 printWriter.increaseIndent();
Tony Makba228422018-10-25 21:30:40 +0100858 printWriter.println("Annotator model file(s):");
Tony Makf93e9e52018-07-16 14:46:29 +0200859 printWriter.increaseIndent();
Tony Makba228422018-10-25 21:30:40 +0100860 for (ModelFileManager.ModelFile modelFile :
861 mAnnotatorModelFileManager.listModelFiles()) {
862 printWriter.println(modelFile.toString());
863 }
864 printWriter.decreaseIndent();
865 printWriter.println("LangID model file(s):");
Tony Makadbebcc2018-10-30 18:59:06 +0000866 printWriter.increaseIndent();
Tony Makba228422018-10-25 21:30:40 +0100867 for (ModelFileManager.ModelFile modelFile :
868 mLangIdModelFileManager.listModelFiles()) {
Tony Makf93e9e52018-07-16 14:46:29 +0200869 printWriter.println(modelFile.toString());
870 }
871 printWriter.decreaseIndent();
Tony Makadbebcc2018-10-30 18:59:06 +0000872 printWriter.println("Actions model file(s):");
873 printWriter.increaseIndent();
874 for (ModelFileManager.ModelFile modelFile :
875 mActionsModelFileManager.listModelFiles()) {
876 printWriter.println(modelFile.toString());
877 }
878 printWriter.decreaseIndent();
Tony Makf93e9e52018-07-16 14:46:29 +0200879 printWriter.printPair("mFallback", mFallback);
880 printWriter.decreaseIndent();
881 printWriter.println();
882 }
883 }
884
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800885 /**
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100886 * Closes the ParcelFileDescriptor, if non-null, and logs any errors that occur.
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100887 */
Abodunrinwa Tokiee3a48e2018-10-19 20:58:26 +0100888 private static void maybeCloseAndLogError(@Nullable ParcelFileDescriptor fd) {
889 if (fd == null) {
890 return;
891 }
892
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100893 try {
894 fd.close();
895 } catch (IOException e) {
896 Log.e(LOG_TAG, "Error closing file.", e);
897 }
898 }
899
900 /**
Tony Mak159f0282019-03-01 14:03:25 +0000901 * Returns the locales string for the current resources configuration.
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000902 */
Tony Mak159f0282019-03-01 14:03:25 +0000903 private String getResourceLocalesString() {
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000904 try {
Tony Mak159f0282019-03-01 14:03:25 +0000905 return mContext.getResources().getConfiguration().getLocales().toLanguageTags();
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000906 } catch (NullPointerException e) {
907 // NPE is unexpected. Erring on the side of caution.
Tony Mak159f0282019-03-01 14:03:25 +0000908 return LocaleList.getDefault().toLanguageTags();
Abodunrinwa Toki25f7fdc2019-02-19 23:42:30 +0000909 }
910 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800911}