blob: ea9229d288144f47ff048a15c83e8013a3fc5db5 [file] [log] [blame]
Tony Makfc039c32019-01-17 19:32:08 +00001/*
2 * Copyright (C) 2019 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 */
16package android.view.textclassifier;
17
18import static java.time.temporal.ChronoUnit.MILLIS;
19
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.app.SearchManager;
23import android.content.ContentUris;
24import android.content.Context;
25import android.content.Intent;
26import android.net.Uri;
27import android.os.Bundle;
28import android.os.UserManager;
29import android.provider.Browser;
30import android.provider.CalendarContract;
31import android.provider.ContactsContract;
Tony Makfc039c32019-01-17 19:32:08 +000032
33import com.google.android.textclassifier.AnnotatorModel;
34
35import java.io.UnsupportedEncodingException;
36import java.net.URLEncoder;
37import java.time.Instant;
38import java.util.ArrayList;
39import java.util.List;
40import java.util.Locale;
41import java.util.concurrent.TimeUnit;
42
43/**
44 * Creates intents based on the classification type.
45 * @hide
46 */
47public final class LegacyIntentFactory implements IntentFactory {
48
49 private static final String TAG = "LegacyIntentFactory";
50 private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
51 private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
52
53 public LegacyIntentFactory() {}
54
55 @NonNull
56 @Override
57 public List<LabeledIntent> create(Context context, String text, boolean foreignText,
58 @Nullable Instant referenceTime,
59 AnnotatorModel.ClassificationResult classification) {
60 final String type = classification != null
61 ? classification.getCollection().trim().toLowerCase(Locale.ENGLISH)
62 : "";
63 text = text.trim();
64 final List<LabeledIntent> actions;
65 switch (type) {
66 case TextClassifier.TYPE_EMAIL:
67 actions = createForEmail(context, text);
68 break;
69 case TextClassifier.TYPE_PHONE:
70 actions = createForPhone(context, text);
71 break;
72 case TextClassifier.TYPE_ADDRESS:
73 actions = createForAddress(context, text);
74 break;
75 case TextClassifier.TYPE_URL:
76 actions = createForUrl(context, text);
77 break;
78 case TextClassifier.TYPE_DATE: // fall through
79 case TextClassifier.TYPE_DATE_TIME:
80 if (classification.getDatetimeResult() != null) {
81 final Instant parsedTime = Instant.ofEpochMilli(
82 classification.getDatetimeResult().getTimeMsUtc());
83 actions = createForDatetime(context, type, referenceTime, parsedTime);
84 } else {
85 actions = new ArrayList<>();
86 }
87 break;
88 case TextClassifier.TYPE_FLIGHT_NUMBER:
89 actions = createForFlight(context, text);
90 break;
91 case TextClassifier.TYPE_DICTIONARY:
92 actions = createForDictionary(context, text);
93 break;
94 default:
95 actions = new ArrayList<>();
96 break;
97 }
98 if (foreignText) {
99 IntentFactory.insertTranslateAction(actions, context, text);
100 }
101 actions.forEach(
Tony Makac9b4d82019-02-15 13:57:38 +0000102 action -> action.intent.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
Tony Makfc039c32019-01-17 19:32:08 +0000103 return actions;
104 }
105
106 @NonNull
107 private static List<LabeledIntent> createForEmail(Context context, String text) {
108 final List<LabeledIntent> actions = new ArrayList<>();
109 actions.add(new LabeledIntent(
110 context.getString(com.android.internal.R.string.email),
Tony Makac9b4d82019-02-15 13:57:38 +0000111 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000112 context.getString(com.android.internal.R.string.email_desc),
113 new Intent(Intent.ACTION_SENDTO)
114 .setData(Uri.parse(String.format("mailto:%s", text))),
115 LabeledIntent.DEFAULT_REQUEST_CODE));
116 actions.add(new LabeledIntent(
117 context.getString(com.android.internal.R.string.add_contact),
Tony Makac9b4d82019-02-15 13:57:38 +0000118 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000119 context.getString(com.android.internal.R.string.add_contact_desc),
120 new Intent(Intent.ACTION_INSERT_OR_EDIT)
121 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
122 .putExtra(ContactsContract.Intents.Insert.EMAIL, text),
123 text.hashCode()));
124 return actions;
125 }
126
127 @NonNull
128 private static List<LabeledIntent> createForPhone(Context context, String text) {
129 final List<LabeledIntent> actions = new ArrayList<>();
130 final UserManager userManager = context.getSystemService(UserManager.class);
131 final Bundle userRestrictions = userManager != null
132 ? userManager.getUserRestrictions() : new Bundle();
133 if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
134 actions.add(new LabeledIntent(
135 context.getString(com.android.internal.R.string.dial),
Tony Makac9b4d82019-02-15 13:57:38 +0000136 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000137 context.getString(com.android.internal.R.string.dial_desc),
138 new Intent(Intent.ACTION_DIAL).setData(
139 Uri.parse(String.format("tel:%s", text))),
140 LabeledIntent.DEFAULT_REQUEST_CODE));
141 }
142 actions.add(new LabeledIntent(
143 context.getString(com.android.internal.R.string.add_contact),
Tony Makac9b4d82019-02-15 13:57:38 +0000144 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000145 context.getString(com.android.internal.R.string.add_contact_desc),
146 new Intent(Intent.ACTION_INSERT_OR_EDIT)
147 .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
148 .putExtra(ContactsContract.Intents.Insert.PHONE, text),
149 text.hashCode()));
150 if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
151 actions.add(new LabeledIntent(
152 context.getString(com.android.internal.R.string.sms),
Tony Makac9b4d82019-02-15 13:57:38 +0000153 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000154 context.getString(com.android.internal.R.string.sms_desc),
155 new Intent(Intent.ACTION_SENDTO)
156 .setData(Uri.parse(String.format("smsto:%s", text))),
157 LabeledIntent.DEFAULT_REQUEST_CODE));
158 }
159 return actions;
160 }
161
162 @NonNull
163 private static List<LabeledIntent> createForAddress(Context context, String text) {
164 final List<LabeledIntent> actions = new ArrayList<>();
165 try {
166 final String encText = URLEncoder.encode(text, "UTF-8");
167 actions.add(new LabeledIntent(
168 context.getString(com.android.internal.R.string.map),
Tony Makac9b4d82019-02-15 13:57:38 +0000169 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000170 context.getString(com.android.internal.R.string.map_desc),
171 new Intent(Intent.ACTION_VIEW)
172 .setData(Uri.parse(String.format("geo:0,0?q=%s", encText))),
173 LabeledIntent.DEFAULT_REQUEST_CODE));
174 } catch (UnsupportedEncodingException e) {
175 Log.e(TAG, "Could not encode address", e);
176 }
177 return actions;
178 }
179
180 @NonNull
181 private static List<LabeledIntent> createForUrl(Context context, String text) {
182 if (Uri.parse(text).getScheme() == null) {
183 text = "http://" + text;
184 }
185 final List<LabeledIntent> actions = new ArrayList<>();
186 actions.add(new LabeledIntent(
187 context.getString(com.android.internal.R.string.browse),
Tony Makac9b4d82019-02-15 13:57:38 +0000188 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000189 context.getString(com.android.internal.R.string.browse_desc),
Abodunrinwa Tokic33fc772019-02-06 01:17:10 +0000190 new Intent(Intent.ACTION_VIEW)
191 .setDataAndNormalize(Uri.parse(text))
Tony Makfc039c32019-01-17 19:32:08 +0000192 .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()),
193 LabeledIntent.DEFAULT_REQUEST_CODE));
194 return actions;
195 }
196
197 @NonNull
198 private static List<LabeledIntent> createForDatetime(
199 Context context, String type, @Nullable Instant referenceTime,
200 Instant parsedTime) {
201 if (referenceTime == null) {
202 // If no reference time was given, use now.
203 referenceTime = Instant.now();
204 }
205 List<LabeledIntent> actions = new ArrayList<>();
206 actions.add(createCalendarViewIntent(context, parsedTime));
207 final long millisUntilEvent = referenceTime.until(parsedTime, MILLIS);
208 if (millisUntilEvent > MIN_EVENT_FUTURE_MILLIS) {
209 actions.add(createCalendarCreateEventIntent(context, parsedTime, type));
210 }
211 return actions;
212 }
213
214 @NonNull
215 private static List<LabeledIntent> createForFlight(Context context, String text) {
216 final List<LabeledIntent> actions = new ArrayList<>();
217 actions.add(new LabeledIntent(
218 context.getString(com.android.internal.R.string.view_flight),
Tony Makac9b4d82019-02-15 13:57:38 +0000219 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000220 context.getString(com.android.internal.R.string.view_flight_desc),
221 new Intent(Intent.ACTION_WEB_SEARCH)
222 .putExtra(SearchManager.QUERY, text),
223 text.hashCode()));
224 return actions;
225 }
226
227 @NonNull
228 private static LabeledIntent createCalendarViewIntent(Context context, Instant parsedTime) {
229 Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
230 builder.appendPath("time");
231 ContentUris.appendId(builder, parsedTime.toEpochMilli());
232 return new LabeledIntent(
233 context.getString(com.android.internal.R.string.view_calendar),
Tony Makac9b4d82019-02-15 13:57:38 +0000234 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000235 context.getString(com.android.internal.R.string.view_calendar_desc),
236 new Intent(Intent.ACTION_VIEW).setData(builder.build()),
237 LabeledIntent.DEFAULT_REQUEST_CODE);
238 }
239
240 @NonNull
241 private static LabeledIntent createCalendarCreateEventIntent(
242 Context context, Instant parsedTime, @TextClassifier.EntityType String type) {
243 final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
244 return new LabeledIntent(
245 context.getString(com.android.internal.R.string.add_calendar_event),
Tony Makac9b4d82019-02-15 13:57:38 +0000246 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000247 context.getString(com.android.internal.R.string.add_calendar_event_desc),
248 new Intent(Intent.ACTION_INSERT)
249 .setData(CalendarContract.Events.CONTENT_URI)
250 .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
251 .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,
252 parsedTime.toEpochMilli())
253 .putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
254 parsedTime.toEpochMilli() + DEFAULT_EVENT_DURATION),
255 parsedTime.hashCode());
256 }
257
258 @NonNull
259 private static List<LabeledIntent> createForDictionary(Context context, String text) {
260 final List<LabeledIntent> actions = new ArrayList<>();
261 actions.add(new LabeledIntent(
262 context.getString(com.android.internal.R.string.define),
Tony Makac9b4d82019-02-15 13:57:38 +0000263 /* titleWithEntity */ null,
Tony Makfc039c32019-01-17 19:32:08 +0000264 context.getString(com.android.internal.R.string.define_desc),
265 new Intent(Intent.ACTION_DEFINE)
266 .putExtra(Intent.EXTRA_TEXT, text),
267 text.hashCode()));
268 return actions;
269 }
270}