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