blob: 910fcaace159f25a19345e0b9a12da245d45ed37 [file] [log] [blame]
Abodunrinwa Toki43e03502017-01-13 13:46:33 -08001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view.textclassifier;
18
Jan Althausa1652cf2018-03-29 17:51:57 +020019import static java.time.temporal.ChronoUnit.MILLIS;
20
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080021import android.annotation.NonNull;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080022import android.annotation.Nullable;
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +000023import android.annotation.WorkerThread;
Jan Althaus20d346e2018-03-23 14:03:52 +010024import android.app.RemoteAction;
Jan Althaus705b9e92018-01-22 18:22:29 +010025import android.app.SearchManager;
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +000026import android.content.ComponentName;
Jan Althaus705b9e92018-01-22 18:22:29 +010027import android.content.ContentUris;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080028import android.content.Context;
29import android.content.Intent;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
Jan Althaus20d346e2018-03-23 14:03:52 +010032import android.graphics.drawable.Icon;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080033import android.net.Uri;
Jan Althaus05e00512018-02-02 15:28:34 +010034import android.os.Bundle;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000035import android.os.LocaleList;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080036import android.os.ParcelFileDescriptor;
Jan Althaus05e00512018-02-02 15:28:34 +010037import android.os.UserManager;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000038import android.provider.Browser;
Jan Althaus705b9e92018-01-22 18:22:29 +010039import android.provider.CalendarContract;
Jan Althaus92d76832017-09-27 18:14:35 +020040import android.provider.ContactsContract;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080041
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010042import com.android.internal.annotations.GuardedBy;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080043import com.android.internal.util.Preconditions;
44
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010045import java.io.File;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080046import java.io.FileNotFoundException;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010047import java.io.IOException;
Jan Althauseaff57e2018-02-12 12:47:27 +010048import java.io.UnsupportedEncodingException;
Jan Althauseaff57e2018-02-12 12:47:27 +010049import java.net.URLEncoder;
Jan Althausa1652cf2018-03-29 17:51:57 +020050import java.time.Instant;
51import java.time.ZonedDateTime;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080052import java.util.ArrayList;
Richard Ledleydb18a572017-11-30 17:33:51 +000053import java.util.Arrays;
54import java.util.Collection;
55import java.util.Collections;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010056import java.util.HashMap;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080057import java.util.List;
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +000058import java.util.Locale;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -080059import java.util.Map;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010060import java.util.Objects;
Jan Althausef0156d2018-01-29 19:28:41 +010061import java.util.StringJoiner;
Jan Althaus705b9e92018-01-22 18:22:29 +010062import java.util.concurrent.TimeUnit;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010063import java.util.regex.Matcher;
64import java.util.regex.Pattern;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080065
66/**
67 * Default implementation of the {@link TextClassifier} interface.
68 *
69 * <p>This class uses machine learning to recognize entities in text.
70 * Unless otherwise stated, methods of this class are blocking operations and should most
71 * likely not be called on the UI thread.
72 *
73 * @hide
74 */
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080075public final class TextClassifierImpl implements TextClassifier {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080076
Abodunrinwa Toki692b1962017-08-15 15:05:11 +010077 private static final String LOG_TAG = DEFAULT_LOG_TAG;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +010078 private static final String MODEL_DIR = "/etc/textclassifier/";
Jan Althausabb4fc82018-01-31 11:51:34 +010079 private static final String MODEL_FILE_REGEX = "textclassifier\\.(.*)\\.model";
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +010080 private static final String UPDATED_MODEL_FILE_PATH =
Jan Althaus67d234d2018-01-26 17:53:31 +010081 "/data/misc/textclassifier/textclassifier.model";
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +000082
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080083 private final Context mContext;
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080084 private final TextClassifier mFallback;
Jan Althaus31efdc32018-02-19 22:23:13 +010085 private final GenerateLinksLogger mGenerateLinksLogger;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010086
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080087 private final Object mLock = new Object();
88 @GuardedBy("mLock") // Do not access outside this lock.
Jan Althausef0156d2018-01-29 19:28:41 +010089 private List<ModelFile> mAllModelFiles;
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080090 @GuardedBy("mLock") // Do not access outside this lock.
Jan Althausef0156d2018-01-29 19:28:41 +010091 private ModelFile mModel;
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -080092 @GuardedBy("mLock") // Do not access outside this lock.
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +010093 private TextClassifierImplNative mNative;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -080094
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +000095 private final Object mLoggerLock = new Object();
96 @GuardedBy("mLoggerLock") // Do not access outside this lock.
Jan Althaus5a030942018-04-04 19:40:38 +020097 private SelectionSessionLogger mSessionLogger;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +000098
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +000099 private final TextClassificationConstants mSettings;
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100100
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000101 public TextClassifierImpl(Context context, TextClassificationConstants settings) {
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800102 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800103 mFallback = TextClassifier.NO_OP;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000104 mSettings = Preconditions.checkNotNull(settings);
105 mGenerateLinksLogger = new GenerateLinksLogger(mSettings.getGenerateLinksLogSampleRate());
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800106 }
107
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800108 /** @inheritDoc */
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800109 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000110 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100111 public TextSelection suggestSelection(TextSelection.Request request) {
112 Preconditions.checkNotNull(request);
113 Utils.checkMainThread();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800114 try {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100115 final int rangeLength = request.getEndIndex() - request.getStartIndex();
116 final String string = request.getText().toString();
117 if (string.length() > 0
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000118 && rangeLength <= mSettings.getSuggestSelectionMaxRangeLength()) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100119 final String localesString = concatenateLocales(request.getDefaultLocales());
Jan Althausa1652cf2018-03-29 17:51:57 +0200120 final ZonedDateTime refTime = ZonedDateTime.now();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100121 final TextClassifierImplNative nativeImpl = getNative(request.getDefaultLocales());
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100122 final int start;
123 final int end;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100124 if (mSettings.isModelDarkLaunchEnabled() && !request.isDarkLaunchAllowed()) {
125 start = request.getStartIndex();
126 end = request.getEndIndex();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100127 } else {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100128 final int[] startEnd = nativeImpl.suggestSelection(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100129 string, request.getStartIndex(), request.getEndIndex(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100130 new TextClassifierImplNative.SelectionOptions(localesString));
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100131 start = startEnd[0];
132 end = startEnd[1];
133 }
Jan Althausb7f7d362018-01-26 13:51:31 +0100134 if (start < end
Abodunrinwa Tokib4162972017-05-05 18:07:17 +0100135 && start >= 0 && end <= string.length()
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100136 && start <= request.getStartIndex() && end >= request.getEndIndex()) {
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100137 final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100138 final TextClassifierImplNative.ClassificationResult[] results =
139 nativeImpl.classifyText(
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000140 string, start, end,
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100141 new TextClassifierImplNative.ClassificationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200142 refTime.toInstant().toEpochMilli(),
143 refTime.getZone().getId(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100144 localesString));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000145 final int size = results.length;
146 for (int i = 0; i < size; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100147 tsBuilder.setEntityType(results[i].getCollection(), results[i].getScore());
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000148 }
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100149 return tsBuilder.setId(createId(
150 string, request.getStartIndex(), request.getEndIndex()))
Abodunrinwa Toki692b1962017-08-15 15:05:11 +0100151 .build();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800152 } else {
153 // We can not trust the result. Log the issue and ignore the result.
154 Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
155 }
156 }
157 } catch (Throwable t) {
158 // Avoid throwing from this method. Log the error.
159 Log.e(LOG_TAG,
160 "Error suggesting selection for text. No changes to selection suggested.",
161 t);
162 }
163 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100164 return mFallback.suggestSelection(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100165 }
166
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800167 /** @inheritDoc */
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100168 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000169 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100170 public TextClassification classifyText(TextClassification.Request request) {
171 Preconditions.checkNotNull(request);
172 Utils.checkMainThread();
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800173 try {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100174 final int rangeLength = request.getEndIndex() - request.getStartIndex();
175 final String string = request.getText().toString();
176 if (string.length() > 0 && rangeLength <= mSettings.getClassifyTextMaxRangeLength()) {
177 final String localesString = concatenateLocales(request.getDefaultLocales());
178 final ZonedDateTime refTime = request.getReferenceTime() != null
179 ? request.getReferenceTime() : ZonedDateTime.now();
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100180 final TextClassifierImplNative.ClassificationResult[] results =
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100181 getNative(request.getDefaultLocales())
182 .classifyText(
183 string, request.getStartIndex(), request.getEndIndex(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100184 new TextClassifierImplNative.ClassificationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200185 refTime.toInstant().toEpochMilli(),
186 refTime.getZone().getId(),
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100187 localesString));
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000188 if (results.length > 0) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100189 return createClassificationResult(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100190 results, string,
191 request.getStartIndex(), request.getEndIndex(), refTime.toInstant());
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800192 }
193 }
194 } catch (Throwable t) {
195 // Avoid throwing from this method. Log the error.
Abodunrinwa Toki78618852017-10-17 15:31:39 +0100196 Log.e(LOG_TAG, "Error getting text classification info.", t);
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800197 }
198 // Getting here means something went wrong, return a NO_OP result.
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100199 return mFallback.classifyText(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100200 }
201
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800202 /** @inheritDoc */
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100203 @Override
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000204 @WorkerThread
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100205 public TextLinks generateLinks(@NonNull TextLinks.Request request) {
206 Preconditions.checkNotNull(request);
207 Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength());
208 Utils.checkMainThread();
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000209
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100210 if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
211 return Utils.generateLegacyLinks(request);
Abodunrinwa Toki65638332018-03-16 21:08:50 +0000212 }
213
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100214 final String textString = request.getText().toString();
Richard Ledley68d94522017-10-05 10:52:19 +0100215 final TextLinks.Builder builder = new TextLinks.Builder(textString);
Richard Ledley9cfa6062018-01-15 13:13:29 +0000216
Richard Ledley68d94522017-10-05 10:52:19 +0100217 try {
Jan Althaus31efdc32018-02-19 22:23:13 +0100218 final long startTimeMs = System.currentTimeMillis();
Jan Althausa1652cf2018-03-29 17:51:57 +0200219 final ZonedDateTime refTime = ZonedDateTime.now();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100220 final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
221 ? request.getEntityConfig().resolveEntityListModifications(
222 getEntitiesForHints(request.getEntityConfig().getHints()))
223 : mSettings.getEntityListDefault();
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100224 final TextClassifierImplNative nativeImpl =
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100225 getNative(request.getDefaultLocales());
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100226 final TextClassifierImplNative.AnnotatedSpan[] annotations =
227 nativeImpl.annotate(
228 textString,
229 new TextClassifierImplNative.AnnotationOptions(
Jan Althausa1652cf2018-03-29 17:51:57 +0200230 refTime.toInstant().toEpochMilli(),
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100231 refTime.getZone().getId(),
232 concatenateLocales(request.getDefaultLocales())));
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100233 for (TextClassifierImplNative.AnnotatedSpan span : annotations) {
234 final TextClassifierImplNative.ClassificationResult[] results =
235 span.getClassification();
236 if (results.length == 0
237 || !entitiesToIdentify.contains(results[0].getCollection())) {
Richard Ledleydb18a572017-11-30 17:33:51 +0000238 continue;
239 }
240 final Map<String, Float> entityScores = new HashMap<>();
Richard Ledley68d94522017-10-05 10:52:19 +0100241 for (int i = 0; i < results.length; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100242 entityScores.put(results[i].getCollection(), results[i].getScore());
Richard Ledley68d94522017-10-05 10:52:19 +0100243 }
Abodunrinwa Tokife20cdd2017-12-12 02:31:25 +0000244 builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores);
Richard Ledley68d94522017-10-05 10:52:19 +0100245 }
Jan Althaus31efdc32018-02-19 22:23:13 +0100246 final TextLinks links = builder.build();
247 final long endTimeMs = System.currentTimeMillis();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100248 final String callingPackageName = request.getCallingPackageName() == null
249 ? mContext.getPackageName() // local (in process) TC.
250 : request.getCallingPackageName();
Jan Althaus31efdc32018-02-19 22:23:13 +0100251 mGenerateLinksLogger.logGenerateLinks(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100252 request.getText(), links, callingPackageName, endTimeMs - startTimeMs);
Jan Althaus31efdc32018-02-19 22:23:13 +0100253 return links;
Richard Ledley68d94522017-10-05 10:52:19 +0100254 } catch (Throwable t) {
255 // Avoid throwing from this method. Log the error.
256 Log.e(LOG_TAG, "Error getting links info.", t);
257 }
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100258 return mFallback.generateLinks(request);
Richard Ledley68d94522017-10-05 10:52:19 +0100259 }
260
Jan Althaus108aad32018-01-30 15:26:55 +0100261 /** @inheritDoc */
262 @Override
263 public int getMaxGenerateLinksTextLength() {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000264 return mSettings.getGenerateLinksMaxTextLength();
Jan Althaus108aad32018-01-30 15:26:55 +0100265 }
266
Richard Ledley1fc998b2018-02-16 15:45:06 +0000267 private Collection<String> getEntitiesForHints(Collection<String> hints) {
Jan Althaus0aacdb62018-02-19 11:44:37 +0100268 final boolean editable = hints.contains(HINT_TEXT_IS_EDITABLE);
269 final boolean notEditable = hints.contains(HINT_TEXT_IS_NOT_EDITABLE);
270
271 // Use the default if there is no hint, or conflicting ones.
272 final boolean useDefault = editable == notEditable;
273 if (useDefault) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000274 return mSettings.getEntityListDefault();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100275 } else if (editable) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000276 return mSettings.getEntityListEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100277 } else { // notEditable
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000278 return mSettings.getEntityListNotEditable();
Jan Althaus0aacdb62018-02-19 11:44:37 +0100279 }
Richard Ledleydb18a572017-11-30 17:33:51 +0000280 }
281
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000282 @Override
283 public void onSelectionEvent(SelectionEvent event) {
284 Preconditions.checkNotNull(event);
285 synchronized (mLoggerLock) {
Jan Althaus5a030942018-04-04 19:40:38 +0200286 if (mSessionLogger == null) {
287 mSessionLogger = new SelectionSessionLogger();
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000288 }
Jan Althaus5a030942018-04-04 19:40:38 +0200289 mSessionLogger.writeEvent(event);
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000290 }
291 }
292
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100293 private TextClassifierImplNative getNative(LocaleList localeList)
294 throws FileNotFoundException {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800295 synchronized (mLock) {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100296 localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
Jan Althausef0156d2018-01-29 19:28:41 +0100297 final ModelFile bestModel = findBestModelLocked(localeList);
298 if (bestModel == null) {
299 throw new FileNotFoundException("No model for " + localeList.toLanguageTags());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100300 }
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100301 if (mNative == null || !Objects.equals(mModel, bestModel)) {
Jan Althausef0156d2018-01-29 19:28:41 +0100302 Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100303 destroyNativeIfExistsLocked();
Jan Althausef0156d2018-01-29 19:28:41 +0100304 final ParcelFileDescriptor fd = ParcelFileDescriptor.open(
305 new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100306 mNative = new TextClassifierImplNative(fd.getFd());
Abodunrinwa Toki6ace8932017-04-28 19:25:24 +0100307 closeAndLogError(fd);
Jan Althausef0156d2018-01-29 19:28:41 +0100308 mModel = bestModel;
Abodunrinwa Tokib89cf022017-02-06 19:53:22 +0000309 }
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100310 return mNative;
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800311 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800312 }
313
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100314 private String createId(String text, int start, int end) {
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800315 synchronized (mLock) {
Jan Althaus5a030942018-04-04 19:40:38 +0200316 return SelectionSessionLogger.createId(text, start, end, mContext, mModel.getVersion(),
Jan Althausef0156d2018-01-29 19:28:41 +0100317 mModel.getSupportedLocales());
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100318 }
319 }
320
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800321 @GuardedBy("mLock") // Do not call outside this lock.
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100322 private void destroyNativeIfExistsLocked() {
323 if (mNative != null) {
324 mNative.close();
325 mNative = null;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100326 }
327 }
328
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100329 private static String concatenateLocales(@Nullable LocaleList locales) {
330 return (locales == null) ? "" : locales.toLanguageTags();
331 }
332
Jan Althausef0156d2018-01-29 19:28:41 +0100333 /**
334 * Finds the most appropriate model to use for the given target locale list.
335 *
336 * The basic logic is: we ignore all models that don't support any of the target locales. For
337 * the remaining candidates, we take the update model unless its version number is lower than
338 * the factory version. It's assumed that factory models do not have overlapping locale ranges
339 * and conflict resolution between these models hence doesn't matter.
340 */
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800341 @GuardedBy("mLock") // Do not call outside this lock.
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100342 @Nullable
Jan Althausef0156d2018-01-29 19:28:41 +0100343 private ModelFile findBestModelLocked(LocaleList localeList) {
Abodunrinwa Tokia2df6e52017-04-13 09:56:52 +0100344 // Specified localeList takes priority over the system default, so it is listed first.
345 final String languages = localeList.isEmpty()
346 ? LocaleList.getDefault().toLanguageTags()
347 : localeList.toLanguageTags() + "," + LocaleList.getDefault().toLanguageTags();
348 final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100349
Jan Althausef0156d2018-01-29 19:28:41 +0100350 ModelFile bestModel = null;
Jan Althausef0156d2018-01-29 19:28:41 +0100351 for (ModelFile model : listAllModelsLocked()) {
352 if (model.isAnyLanguageSupported(languageRangeList)) {
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100353 if (model.isPreferredTo(bestModel)) {
Jan Althausef0156d2018-01-29 19:28:41 +0100354 bestModel = model;
Jan Althausef0156d2018-01-29 19:28:41 +0100355 }
356 }
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100357 }
Jan Althausef0156d2018-01-29 19:28:41 +0100358 return bestModel;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100359 }
360
Jan Althausef0156d2018-01-29 19:28:41 +0100361 /** Returns a list of all model files available, in order of precedence. */
Abodunrinwa Tokid32906c2018-01-18 04:34:44 -0800362 @GuardedBy("mLock") // Do not call outside this lock.
Jan Althausef0156d2018-01-29 19:28:41 +0100363 private List<ModelFile> listAllModelsLocked() {
364 if (mAllModelFiles == null) {
365 final List<ModelFile> allModels = new ArrayList<>();
366 // The update model has the highest precedence.
367 if (new File(UPDATED_MODEL_FILE_PATH).exists()) {
368 final ModelFile updatedModel = ModelFile.fromPath(UPDATED_MODEL_FILE_PATH);
369 if (updatedModel != null) {
370 allModels.add(updatedModel);
371 }
372 }
373 // Factory models should never have overlapping locales, so the order doesn't matter.
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100374 final File modelsDir = new File(MODEL_DIR);
375 if (modelsDir.exists() && modelsDir.isDirectory()) {
Jan Althausef0156d2018-01-29 19:28:41 +0100376 final File[] modelFiles = modelsDir.listFiles();
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100377 final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX);
Jan Althausef0156d2018-01-29 19:28:41 +0100378 for (File modelFile : modelFiles) {
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100379 final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName());
380 if (matcher.matches() && modelFile.isFile()) {
Jan Althausef0156d2018-01-29 19:28:41 +0100381 final ModelFile model = ModelFile.fromPath(modelFile.getAbsolutePath());
382 if (model != null) {
383 allModels.add(model);
384 }
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100385 }
386 }
387 }
Jan Althausef0156d2018-01-29 19:28:41 +0100388 mAllModelFiles = allModels;
Abodunrinwa Tokic39006a12017-03-29 01:25:23 +0100389 }
Jan Althausef0156d2018-01-29 19:28:41 +0100390 return mAllModelFiles;
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100391 }
392
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100393 private TextClassification createClassificationResult(
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100394 TextClassifierImplNative.ClassificationResult[] classifications,
Jan Althausa1652cf2018-03-29 17:51:57 +0200395 String text, int start, int end, @Nullable Instant referenceTime) {
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000396 final String classifiedText = text.substring(start, end);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100397 final TextClassification.Builder builder = new TextClassification.Builder()
Abodunrinwa Toki008f3872017-11-27 19:32:35 +0000398 .setText(classifiedText);
Abodunrinwa Toki9b4c82a2017-02-06 20:29:36 +0000399
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000400 final int size = classifications.length;
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100401 TextClassifierImplNative.ClassificationResult highestScoringResult = null;
Jan Althaus705b9e92018-01-22 18:22:29 +0100402 float highestScore = Float.MIN_VALUE;
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000403 for (int i = 0; i < size; i++) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100404 builder.setEntityType(classifications[i].getCollection(),
405 classifications[i].getScore());
406 if (classifications[i].getScore() > highestScore) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100407 highestScoringResult = classifications[i];
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100408 highestScore = classifications[i].getScore();
Jan Althaus705b9e92018-01-22 18:22:29 +0100409 }
Abodunrinwa Tokia6096f62017-03-08 17:21:40 +0000410 }
411
Jan Althaus20d346e2018-03-23 14:03:52 +0100412 boolean isPrimaryAction = true;
413 for (LabeledIntent labeledIntent : IntentFactory.create(
414 mContext, referenceTime, highestScoringResult, classifiedText)) {
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100415 final RemoteAction action = labeledIntent.asRemoteAction(mContext);
Jan Althaus20d346e2018-03-23 14:03:52 +0100416 if (isPrimaryAction) {
417 // For O backwards compatibility, the first RemoteAction is also written to the
418 // legacy API fields.
419 builder.setIcon(action.getIcon().loadDrawable(mContext));
420 builder.setLabel(action.getTitle().toString());
421 builder.setIntent(labeledIntent.getIntent());
422 builder.setOnClickListener(TextClassification.createIntentOnClickListener(
423 TextClassification.createPendingIntent(mContext,
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100424 labeledIntent.getIntent(), labeledIntent.getRequestCode())));
Jan Althaus20d346e2018-03-23 14:03:52 +0100425 isPrimaryAction = false;
426 }
427 builder.addAction(action);
428 }
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100429
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100430 return builder.setId(createId(text, start, end)).build();
Jan Althaus92d76832017-09-27 18:14:35 +0200431 }
432
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800433 /**
Abodunrinwa Toki146d0d42017-04-25 01:39:19 +0100434 * Closes the ParcelFileDescriptor and logs any errors that occur.
435 */
436 private static void closeAndLogError(ParcelFileDescriptor fd) {
437 try {
438 fd.close();
439 } catch (IOException e) {
440 Log.e(LOG_TAG, "Error closing file.", e);
441 }
442 }
443
444 /**
Jan Althausef0156d2018-01-29 19:28:41 +0100445 * Describes TextClassifier model files on disk.
446 */
447 private static final class ModelFile {
448
449 private final String mPath;
450 private final String mName;
451 private final int mVersion;
452 private final List<Locale> mSupportedLocales;
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100453 private final boolean mLanguageIndependent;
Jan Althausef0156d2018-01-29 19:28:41 +0100454
455 /** Returns null if the path did not point to a compatible model. */
456 static @Nullable ModelFile fromPath(String path) {
457 final File file = new File(path);
458 try {
459 final ParcelFileDescriptor modelFd = ParcelFileDescriptor.open(
460 file, ParcelFileDescriptor.MODE_READ_ONLY);
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100461 final int version = TextClassifierImplNative.getVersion(modelFd.getFd());
462 final String supportedLocalesStr =
463 TextClassifierImplNative.getLocales(modelFd.getFd());
Jan Althausef0156d2018-01-29 19:28:41 +0100464 if (supportedLocalesStr.isEmpty()) {
465 Log.d(DEFAULT_LOG_TAG, "Ignoring " + file.getAbsolutePath());
466 return null;
467 }
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100468 final boolean languageIndependent = supportedLocalesStr.equals("*");
Jan Althausef0156d2018-01-29 19:28:41 +0100469 final List<Locale> supportedLocales = new ArrayList<>();
470 for (String langTag : supportedLocalesStr.split(",")) {
471 supportedLocales.add(Locale.forLanguageTag(langTag));
472 }
473 closeAndLogError(modelFd);
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100474 return new ModelFile(path, file.getName(), version, supportedLocales,
475 languageIndependent);
Jan Althausef0156d2018-01-29 19:28:41 +0100476 } catch (FileNotFoundException e) {
477 Log.e(DEFAULT_LOG_TAG, "Failed to peek " + file.getAbsolutePath(), e);
478 return null;
479 }
480 }
481
482 /** The absolute path to the model file. */
483 String getPath() {
484 return mPath;
485 }
486
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100487 /** A name to use for id generation. Effectively the name of the model file. */
Jan Althausef0156d2018-01-29 19:28:41 +0100488 String getName() {
489 return mName;
490 }
491
492 /** Returns the version tag in the model's metadata. */
493 int getVersion() {
494 return mVersion;
495 }
496
497 /** Returns whether the language supports any language in the given ranges. */
498 boolean isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges) {
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100499 return mLanguageIndependent || Locale.lookup(languageRanges, mSupportedLocales) != null;
Jan Althausef0156d2018-01-29 19:28:41 +0100500 }
501
502 /** All locales supported by the model. */
503 List<Locale> getSupportedLocales() {
504 return Collections.unmodifiableList(mSupportedLocales);
505 }
506
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100507 public boolean isPreferredTo(ModelFile model) {
508 // A model is preferred to no model.
509 if (model == null) {
510 return true;
511 }
512
513 // A language-specific model is preferred to a language independent
514 // model.
515 if (!mLanguageIndependent && model.mLanguageIndependent) {
516 return true;
517 }
518
519 // A higher-version model is preferred.
520 if (getVersion() > model.getVersion()) {
521 return true;
522 }
523 return false;
524 }
525
Jan Althausef0156d2018-01-29 19:28:41 +0100526 @Override
527 public boolean equals(Object other) {
528 if (this == other) {
529 return true;
530 } else if (other == null || !ModelFile.class.isAssignableFrom(other.getClass())) {
531 return false;
532 } else {
533 final ModelFile otherModel = (ModelFile) other;
534 return mPath.equals(otherModel.mPath);
535 }
536 }
537
538 @Override
539 public String toString() {
540 final StringJoiner localesJoiner = new StringJoiner(",");
541 for (Locale locale : mSupportedLocales) {
542 localesJoiner.add(locale.toLanguageTag());
543 }
544 return String.format(Locale.US, "ModelFile { path=%s name=%s version=%d locales=%s }",
545 mPath, mName, mVersion, localesJoiner.toString());
546 }
547
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100548 private ModelFile(String path, String name, int version, List<Locale> supportedLocales,
549 boolean languageIndependent) {
Jan Althausef0156d2018-01-29 19:28:41 +0100550 mPath = path;
551 mName = name;
552 mVersion = version;
553 mSupportedLocales = supportedLocales;
Lukas Zilka0fcacdd2018-03-14 11:06:26 +0100554 mLanguageIndependent = languageIndependent;
Jan Althausef0156d2018-01-29 19:28:41 +0100555 }
556 }
557
558 /**
Jan Althaus20d346e2018-03-23 14:03:52 +0100559 * Helper class to store the information from which RemoteActions are built.
560 */
561 private static final class LabeledIntent {
Jan Althaus20d346e2018-03-23 14:03:52 +0100562
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100563 static final int DEFAULT_REQUEST_CODE = 0;
564
565 private final String mTitle;
566 private final String mDescription;
567 private final Intent mIntent;
568 private final int mRequestCode;
569
570 /**
571 * Initializes a LabeledIntent.
572 *
573 * <p>NOTE: {@code reqestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
574 * if distinguishing info (e.g. the classified text) is represented in intent extras only.
575 * In such circumstances, the request code should represent the distinguishing info
576 * (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
577 * unique. To be correct, the PendingIntent should be definitely unique but we try a
578 * best effort approach that avoids spamming the system with PendingIntents.
579 */
580 // TODO: Fix the issue mentioned above so the behaviour is correct.
581 LabeledIntent(String title, String description, Intent intent, int requestCode) {
Jan Althaus20d346e2018-03-23 14:03:52 +0100582 mTitle = title;
583 mDescription = description;
584 mIntent = intent;
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100585 mRequestCode = requestCode;
Jan Althaus20d346e2018-03-23 14:03:52 +0100586 }
587
588 String getTitle() {
589 return mTitle;
590 }
591
592 String getDescription() {
593 return mDescription;
594 }
595
596 Intent getIntent() {
597 return mIntent;
598 }
599
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100600 int getRequestCode() {
601 return mRequestCode;
602 }
603
Jan Althaus20d346e2018-03-23 14:03:52 +0100604 RemoteAction asRemoteAction(Context context) {
605 final PackageManager pm = context.getPackageManager();
606 final ResolveInfo resolveInfo = pm.resolveActivity(mIntent, 0);
607 final String packageName = resolveInfo != null && resolveInfo.activityInfo != null
608 ? resolveInfo.activityInfo.packageName : null;
609 Icon icon = null;
610 boolean shouldShowIcon = false;
611 if (packageName != null && !"android".equals(packageName)) {
612 // There is a default activity handling the intent.
613 mIntent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name));
614 if (resolveInfo.activityInfo.getIconResource() != 0) {
615 icon = Icon.createWithResource(
616 packageName, resolveInfo.activityInfo.getIconResource());
617 shouldShowIcon = true;
618 }
619 }
620 if (icon == null) {
621 // RemoteAction requires that there be an icon.
622 icon = Icon.createWithResource("android",
623 com.android.internal.R.drawable.ic_more_items);
624 }
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100625 final RemoteAction action = new RemoteAction(icon, mTitle, mDescription,
626 TextClassification.createPendingIntent(context, mIntent, mRequestCode));
Jan Althaus20d346e2018-03-23 14:03:52 +0100627 action.setShouldShowIcon(shouldShowIcon);
628 return action;
629 }
630 }
631
632 /**
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800633 * Creates intents based on the classification type.
634 */
Jan Althaus705b9e92018-01-22 18:22:29 +0100635 static final class IntentFactory {
636
637 private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
638 private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800639
640 private IntentFactory() {}
641
Jan Althaus92d76832017-09-27 18:14:35 +0200642 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100643 public static List<LabeledIntent> create(
Jan Althaus705b9e92018-01-22 18:22:29 +0100644 Context context,
Jan Althausa1652cf2018-03-29 17:51:57 +0200645 @Nullable Instant referenceTime,
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100646 TextClassifierImplNative.ClassificationResult classification,
Jan Althaus705b9e92018-01-22 18:22:29 +0100647 String text) {
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100648 final String type = classification.getCollection().trim().toLowerCase(Locale.ENGLISH);
Abodunrinwa Toki70d41cd2017-05-02 21:43:41 +0100649 text = text.trim();
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800650 switch (type) {
651 case TextClassifier.TYPE_EMAIL:
Jan Althaus20d346e2018-03-23 14:03:52 +0100652 return createForEmail(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100653 case TextClassifier.TYPE_PHONE:
Jan Althaus05e00512018-02-02 15:28:34 +0100654 return createForPhone(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100655 case TextClassifier.TYPE_ADDRESS:
Jan Althaus20d346e2018-03-23 14:03:52 +0100656 return createForAddress(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100657 case TextClassifier.TYPE_URL:
658 return createForUrl(context, text);
659 case TextClassifier.TYPE_DATE:
660 case TextClassifier.TYPE_DATE_TIME:
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100661 if (classification.getDatetimeResult() != null) {
Jan Althausa1652cf2018-03-29 17:51:57 +0200662 final Instant parsedTime = Instant.ofEpochMilli(
Lukas Zilkaf8c36bf2018-02-07 20:30:08 +0100663 classification.getDatetimeResult().getTimeMsUtc());
Jan Althausa1652cf2018-03-29 17:51:57 +0200664 return createForDatetime(context, type, referenceTime, parsedTime);
Jan Althaus705b9e92018-01-22 18:22:29 +0100665 } else {
666 return new ArrayList<>();
667 }
668 case TextClassifier.TYPE_FLIGHT_NUMBER:
Jan Althaus20d346e2018-03-23 14:03:52 +0100669 return createForFlight(context, text);
Jan Althaus705b9e92018-01-22 18:22:29 +0100670 default:
671 return new ArrayList<>();
672 }
673 }
674
675 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100676 private static List<LabeledIntent> createForEmail(Context context, String text) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100677 return Arrays.asList(
Jan Althaus20d346e2018-03-23 14:03:52 +0100678 new LabeledIntent(
679 context.getString(com.android.internal.R.string.email),
680 context.getString(com.android.internal.R.string.email_desc),
681 new Intent(Intent.ACTION_SENDTO)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100682 .setData(Uri.parse(String.format("mailto:%s", text))),
683 LabeledIntent.DEFAULT_REQUEST_CODE),
Jan Althaus20d346e2018-03-23 14:03:52 +0100684 new LabeledIntent(
685 context.getString(com.android.internal.R.string.add_contact),
686 context.getString(com.android.internal.R.string.add_contact_desc),
687 new Intent(Intent.ACTION_INSERT_OR_EDIT)
688 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100689 .putExtra(ContactsContract.Intents.Insert.EMAIL, text),
690 text.hashCode()));
Jan Althaus705b9e92018-01-22 18:22:29 +0100691 }
692
693 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100694 private static List<LabeledIntent> createForPhone(Context context, String text) {
695 final List<LabeledIntent> actions = new ArrayList<>();
Jan Althaus05e00512018-02-02 15:28:34 +0100696 final UserManager userManager = context.getSystemService(UserManager.class);
697 final Bundle userRestrictions = userManager != null
698 ? userManager.getUserRestrictions() : new Bundle();
699 if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
Jan Althaus20d346e2018-03-23 14:03:52 +0100700 actions.add(new LabeledIntent(
701 context.getString(com.android.internal.R.string.dial),
702 context.getString(com.android.internal.R.string.dial_desc),
703 new Intent(Intent.ACTION_DIAL).setData(
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100704 Uri.parse(String.format("tel:%s", text))),
705 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althaus05e00512018-02-02 15:28:34 +0100706 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100707 actions.add(new LabeledIntent(
708 context.getString(com.android.internal.R.string.add_contact),
709 context.getString(com.android.internal.R.string.add_contact_desc),
710 new Intent(Intent.ACTION_INSERT_OR_EDIT)
711 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100712 .putExtra(ContactsContract.Intents.Insert.PHONE, text),
713 text.hashCode()));
Jan Althaus05e00512018-02-02 15:28:34 +0100714 if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
Jan Althaus20d346e2018-03-23 14:03:52 +0100715 actions.add(new LabeledIntent(
716 context.getString(com.android.internal.R.string.sms),
717 context.getString(com.android.internal.R.string.sms_desc),
718 new Intent(Intent.ACTION_SENDTO)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100719 .setData(Uri.parse(String.format("smsto:%s", text))),
720 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althaus05e00512018-02-02 15:28:34 +0100721 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100722 return actions;
Jan Althaus705b9e92018-01-22 18:22:29 +0100723 }
724
725 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100726 private static List<LabeledIntent> createForAddress(Context context, String text) {
727 final List<LabeledIntent> actions = new ArrayList<>();
Jan Althauseaff57e2018-02-12 12:47:27 +0100728 try {
729 final String encText = URLEncoder.encode(text, "UTF-8");
Jan Althaus20d346e2018-03-23 14:03:52 +0100730 actions.add(new LabeledIntent(
731 context.getString(com.android.internal.R.string.map),
732 context.getString(com.android.internal.R.string.map_desc),
733 new Intent(Intent.ACTION_VIEW)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100734 .setData(Uri.parse(String.format("geo:0,0?q=%s", encText))),
735 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althauseaff57e2018-02-12 12:47:27 +0100736 } catch (UnsupportedEncodingException e) {
737 Log.e(LOG_TAG, "Could not encode address", e);
738 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100739 return actions;
Jan Althaus705b9e92018-01-22 18:22:29 +0100740 }
741
742 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100743 private static List<LabeledIntent> createForUrl(Context context, String text) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100744 final String httpPrefix = "http://";
745 final String httpsPrefix = "https://";
746 if (text.toLowerCase().startsWith(httpPrefix)) {
747 text = httpPrefix + text.substring(httpPrefix.length());
748 } else if (text.toLowerCase().startsWith(httpsPrefix)) {
749 text = httpsPrefix + text.substring(httpsPrefix.length());
750 } else {
751 text = httpPrefix + text;
752 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100753 return Arrays.asList(new LabeledIntent(
754 context.getString(com.android.internal.R.string.browse),
755 context.getString(com.android.internal.R.string.browse_desc),
756 new Intent(Intent.ACTION_VIEW, Uri.parse(text))
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100757 .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()),
758 LabeledIntent.DEFAULT_REQUEST_CODE));
Jan Althaus705b9e92018-01-22 18:22:29 +0100759 }
760
761 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100762 private static List<LabeledIntent> createForDatetime(
Jan Althausa1652cf2018-03-29 17:51:57 +0200763 Context context, String type, @Nullable Instant referenceTime,
764 Instant parsedTime) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100765 if (referenceTime == null) {
766 // If no reference time was given, use now.
Jan Althausa1652cf2018-03-29 17:51:57 +0200767 referenceTime = Instant.now();
Jan Althaus705b9e92018-01-22 18:22:29 +0100768 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100769 List<LabeledIntent> actions = new ArrayList<>();
Jan Althausa1652cf2018-03-29 17:51:57 +0200770 actions.add(createCalendarViewIntent(context, parsedTime));
771 final long millisUntilEvent = referenceTime.until(parsedTime, MILLIS);
772 if (millisUntilEvent > MIN_EVENT_FUTURE_MILLIS) {
773 actions.add(createCalendarCreateEventIntent(context, parsedTime, type));
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800774 }
Jan Althaus20d346e2018-03-23 14:03:52 +0100775 return actions;
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800776 }
777
Jan Althaus705b9e92018-01-22 18:22:29 +0100778 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100779 private static List<LabeledIntent> createForFlight(Context context, String text) {
780 return Arrays.asList(new LabeledIntent(
781 context.getString(com.android.internal.R.string.view_flight),
782 context.getString(com.android.internal.R.string.view_flight_desc),
783 new Intent(Intent.ACTION_WEB_SEARCH)
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100784 .putExtra(SearchManager.QUERY, text),
785 text.hashCode()));
Jan Althaus705b9e92018-01-22 18:22:29 +0100786 }
787
788 @NonNull
Jan Althausa1652cf2018-03-29 17:51:57 +0200789 private static LabeledIntent createCalendarViewIntent(Context context, Instant parsedTime) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100790 Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
791 builder.appendPath("time");
Jan Althausa1652cf2018-03-29 17:51:57 +0200792 ContentUris.appendId(builder, parsedTime.toEpochMilli());
Jan Althaus20d346e2018-03-23 14:03:52 +0100793 return new LabeledIntent(
794 context.getString(com.android.internal.R.string.view_calendar),
795 context.getString(com.android.internal.R.string.view_calendar_desc),
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100796 new Intent(Intent.ACTION_VIEW).setData(builder.build()),
797 LabeledIntent.DEFAULT_REQUEST_CODE);
Jan Althaus705b9e92018-01-22 18:22:29 +0100798 }
799
800 @NonNull
Jan Althaus20d346e2018-03-23 14:03:52 +0100801 private static LabeledIntent createCalendarCreateEventIntent(
Jan Althausa1652cf2018-03-29 17:51:57 +0200802 Context context, Instant parsedTime, @EntityType String type) {
Jan Althaus705b9e92018-01-22 18:22:29 +0100803 final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
Jan Althaus20d346e2018-03-23 14:03:52 +0100804 return new LabeledIntent(
805 context.getString(com.android.internal.R.string.add_calendar_event),
806 context.getString(com.android.internal.R.string.add_calendar_event_desc),
807 new Intent(Intent.ACTION_INSERT)
808 .setData(CalendarContract.Events.CONTENT_URI)
809 .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
810 .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,
Jan Althausa1652cf2018-03-29 17:51:57 +0200811 parsedTime.toEpochMilli())
Jan Althaus20d346e2018-03-23 14:03:52 +0100812 .putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
Abodunrinwa Toki904a9312018-04-18 21:21:27 +0100813 parsedTime.toEpochMilli() + DEFAULT_EVENT_DURATION),
814 parsedTime.hashCode());
Abodunrinwa Toki6b766752017-01-17 16:25:38 -0800815 }
816 }
Abodunrinwa Toki43e03502017-01-13 13:46:33 -0800817}