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