Merge "Update IntentFactory to construct intents using RemoteActionTemplate... objects that are returned by the model"

This commit is contained in:
TreeHugger Robot
2019-01-29 22:03:03 +00:00
committed by Android (Google) Code Review
8 changed files with 783 additions and 255 deletions

View File

@@ -11766,6 +11766,7 @@ public final class Settings {
* entity_list_not_editable (String[])
* entity_list_editable (String[])
* lang_id_threshold_override (float)
* template_intent_factory_enabled (boolean)
* </pre>
*
* <p>

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view.textclassifier;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import com.google.android.textclassifier.AnnotatorModel;
import java.time.Instant;
import java.util.List;
/**
* @hide
*/
public interface IntentFactory {
/**
* Return a list of LabeledIntent from the classification result.
*/
List<TextClassifierImpl.LabeledIntent> create(
Context context,
String text,
boolean foreignText,
@Nullable Instant referenceTime,
@Nullable AnnotatorModel.ClassificationResult classification);
/**
* Inserts translate action to the list if it is a foreign text.
*/
static void insertTranslateAction(
List<TextClassifierImpl.LabeledIntent> actions, Context context, String text) {
actions.add(new TextClassifierImpl.LabeledIntent(
context.getString(com.android.internal.R.string.translate),
context.getString(com.android.internal.R.string.translate_desc),
new Intent(Intent.ACTION_TRANSLATE)
// TODO: Probably better to introduce a "translate" scheme instead of
// using EXTRA_TEXT.
.putExtra(Intent.EXTRA_TEXT, text),
text.hashCode()));
}
}

View File

@@ -0,0 +1,260 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view.textclassifier;
import static java.time.temporal.ChronoUnit.MILLIS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.SearchManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserManager;
import android.provider.Browser;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.view.textclassifier.TextClassifierImpl.LabeledIntent;
import com.google.android.textclassifier.AnnotatorModel;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Creates intents based on the classification type.
* @hide
*/
public final class LegacyIntentFactory implements IntentFactory {
private static final String TAG = "LegacyIntentFactory";
private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
public LegacyIntentFactory() {}
@NonNull
@Override
public List<LabeledIntent> create(Context context, String text, boolean foreignText,
@Nullable Instant referenceTime,
AnnotatorModel.ClassificationResult classification) {
final String type = classification != null
? classification.getCollection().trim().toLowerCase(Locale.ENGLISH)
: "";
text = text.trim();
final List<LabeledIntent> actions;
switch (type) {
case TextClassifier.TYPE_EMAIL:
actions = createForEmail(context, text);
break;
case TextClassifier.TYPE_PHONE:
actions = createForPhone(context, text);
break;
case TextClassifier.TYPE_ADDRESS:
actions = createForAddress(context, text);
break;
case TextClassifier.TYPE_URL:
actions = createForUrl(context, text);
break;
case TextClassifier.TYPE_DATE: // fall through
case TextClassifier.TYPE_DATE_TIME:
if (classification.getDatetimeResult() != null) {
final Instant parsedTime = Instant.ofEpochMilli(
classification.getDatetimeResult().getTimeMsUtc());
actions = createForDatetime(context, type, referenceTime, parsedTime);
} else {
actions = new ArrayList<>();
}
break;
case TextClassifier.TYPE_FLIGHT_NUMBER:
actions = createForFlight(context, text);
break;
case TextClassifier.TYPE_DICTIONARY:
actions = createForDictionary(context, text);
break;
default:
actions = new ArrayList<>();
break;
}
if (foreignText) {
IntentFactory.insertTranslateAction(actions, context, text);
}
actions.forEach(
action -> action.getIntent()
.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
return actions;
}
@NonNull
private static List<LabeledIntent> createForEmail(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.email),
context.getString(com.android.internal.R.string.email_desc),
new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("mailto:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.add_contact),
context.getString(com.android.internal.R.string.add_contact_desc),
new Intent(Intent.ACTION_INSERT_OR_EDIT)
.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
.putExtra(ContactsContract.Intents.Insert.EMAIL, text),
text.hashCode()));
return actions;
}
@NonNull
private static List<LabeledIntent> createForPhone(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
final UserManager userManager = context.getSystemService(UserManager.class);
final Bundle userRestrictions = userManager != null
? userManager.getUserRestrictions() : new Bundle();
if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.dial),
context.getString(com.android.internal.R.string.dial_desc),
new Intent(Intent.ACTION_DIAL).setData(
Uri.parse(String.format("tel:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
}
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.add_contact),
context.getString(com.android.internal.R.string.add_contact_desc),
new Intent(Intent.ACTION_INSERT_OR_EDIT)
.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
.putExtra(ContactsContract.Intents.Insert.PHONE, text),
text.hashCode()));
if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.sms),
context.getString(com.android.internal.R.string.sms_desc),
new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("smsto:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForAddress(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
try {
final String encText = URLEncoder.encode(text, "UTF-8");
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.map),
context.getString(com.android.internal.R.string.map_desc),
new Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(String.format("geo:0,0?q=%s", encText))),
LabeledIntent.DEFAULT_REQUEST_CODE));
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Could not encode address", e);
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForUrl(Context context, String text) {
if (Uri.parse(text).getScheme() == null) {
text = "http://" + text;
}
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.browse),
context.getString(com.android.internal.R.string.browse_desc),
new Intent(Intent.ACTION_VIEW, Uri.parse(text))
.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()),
LabeledIntent.DEFAULT_REQUEST_CODE));
return actions;
}
@NonNull
private static List<LabeledIntent> createForDatetime(
Context context, String type, @Nullable Instant referenceTime,
Instant parsedTime) {
if (referenceTime == null) {
// If no reference time was given, use now.
referenceTime = Instant.now();
}
List<LabeledIntent> actions = new ArrayList<>();
actions.add(createCalendarViewIntent(context, parsedTime));
final long millisUntilEvent = referenceTime.until(parsedTime, MILLIS);
if (millisUntilEvent > MIN_EVENT_FUTURE_MILLIS) {
actions.add(createCalendarCreateEventIntent(context, parsedTime, type));
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForFlight(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.view_flight),
context.getString(com.android.internal.R.string.view_flight_desc),
new Intent(Intent.ACTION_WEB_SEARCH)
.putExtra(SearchManager.QUERY, text),
text.hashCode()));
return actions;
}
@NonNull
private static LabeledIntent createCalendarViewIntent(Context context, Instant parsedTime) {
Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
ContentUris.appendId(builder, parsedTime.toEpochMilli());
return new LabeledIntent(
context.getString(com.android.internal.R.string.view_calendar),
context.getString(com.android.internal.R.string.view_calendar_desc),
new Intent(Intent.ACTION_VIEW).setData(builder.build()),
LabeledIntent.DEFAULT_REQUEST_CODE);
}
@NonNull
private static LabeledIntent createCalendarCreateEventIntent(
Context context, Instant parsedTime, @TextClassifier.EntityType String type) {
final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
return new LabeledIntent(
context.getString(com.android.internal.R.string.add_calendar_event),
context.getString(com.android.internal.R.string.add_calendar_event_desc),
new Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,
parsedTime.toEpochMilli())
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
parsedTime.toEpochMilli() + DEFAULT_EVENT_DURATION),
parsedTime.hashCode());
}
@NonNull
private static List<LabeledIntent> createForDictionary(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.define),
context.getString(com.android.internal.R.string.define_desc),
new Intent(Intent.ACTION_DEFINE)
.putExtra(Intent.EXTRA_TEXT, text),
text.hashCode()));
return actions;
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view.textclassifier;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import com.google.android.textclassifier.AnnotatorModel;
import com.google.android.textclassifier.NamedVariant;
import com.google.android.textclassifier.RemoteActionTemplate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Creates intents based on {@link RemoteActionTemplate} objects.
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class TemplateIntentFactory implements IntentFactory {
private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
private final IntentFactory mFallback;
public TemplateIntentFactory(IntentFactory fallback) {
mFallback = Preconditions.checkNotNull(fallback);
}
/**
* Returns a list of {@link android.view.textclassifier.TextClassifierImpl.LabeledIntent}
* that are constructed from the classification result.
*/
@NonNull
@Override
public List<TextClassifierImpl.LabeledIntent> create(
Context context,
String text,
boolean foreignText,
@Nullable Instant referenceTime,
@Nullable AnnotatorModel.ClassificationResult classification) {
if (classification == null) {
return Collections.emptyList();
}
RemoteActionTemplate[] remoteActionTemplates = classification.getRemoteActionTemplates();
if (ArrayUtils.isEmpty(remoteActionTemplates)) {
// RemoteActionTemplate is missing, fallback.
Log.w(TAG, "RemoteActionTemplate is missing, fallback to LegacyIntentFactory.");
return mFallback.create(context, text, foreignText, referenceTime, classification);
}
final List<TextClassifierImpl.LabeledIntent> labeledIntents =
new ArrayList<>(createFromRemoteActionTemplates(remoteActionTemplates));
if (foreignText) {
IntentFactory.insertTranslateAction(labeledIntents, context, text.trim());
}
labeledIntents.forEach(
action -> action.getIntent()
.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
return labeledIntents;
}
private static List<TextClassifierImpl.LabeledIntent> createFromRemoteActionTemplates(
RemoteActionTemplate[] remoteActionTemplates) {
final List<TextClassifierImpl.LabeledIntent> labeledIntents = new ArrayList<>();
for (RemoteActionTemplate remoteActionTemplate : remoteActionTemplates) {
Intent intent = createIntent(remoteActionTemplate);
if (intent == null) {
continue;
}
TextClassifierImpl.LabeledIntent
labeledIntent = new TextClassifierImpl.LabeledIntent(
remoteActionTemplate.title,
remoteActionTemplate.description,
intent,
remoteActionTemplate.requestCode == null
? TextClassifierImpl.LabeledIntent.DEFAULT_REQUEST_CODE
: remoteActionTemplate.requestCode
);
labeledIntents.add(labeledIntent);
}
return labeledIntents;
}
@Nullable
private static Intent createIntent(RemoteActionTemplate remoteActionTemplate) {
Intent intent = new Intent();
if (!TextUtils.isEmpty(remoteActionTemplate.packageName)) {
Log.w(TAG, "A RemoteActionTemplate is skipped as package name is set.");
return null;
}
if (!TextUtils.isEmpty(remoteActionTemplate.action)) {
intent.setAction(remoteActionTemplate.action);
}
Uri data = null;
if (!TextUtils.isEmpty(remoteActionTemplate.data)) {
data = Uri.parse(remoteActionTemplate.data);
}
if (data != null || !TextUtils.isEmpty(remoteActionTemplate.type)) {
intent.setDataAndType(data, remoteActionTemplate.type);
}
if (remoteActionTemplate.flags != null) {
intent.setFlags(remoteActionTemplate.flags);
}
if (remoteActionTemplate.category != null) {
for (String category : remoteActionTemplate.category) {
intent.addCategory(category);
}
}
intent.putExtras(createExtras(remoteActionTemplate.extras));
return intent;
}
private static Bundle createExtras(NamedVariant[] namedVariants) {
if (namedVariants == null) {
return Bundle.EMPTY;
}
Bundle bundle = new Bundle();
for (NamedVariant namedVariant : namedVariants) {
switch (namedVariant.getType()) {
case NamedVariant.TYPE_INT:
bundle.putInt(namedVariant.getName(), namedVariant.getInt());
break;
case NamedVariant.TYPE_LONG:
bundle.putLong(namedVariant.getName(), namedVariant.getLong());
break;
case NamedVariant.TYPE_FLOAT:
bundle.putFloat(namedVariant.getName(), namedVariant.getFloat());
break;
case NamedVariant.TYPE_DOUBLE:
bundle.putDouble(namedVariant.getName(), namedVariant.getDouble());
break;
case NamedVariant.TYPE_BOOL:
bundle.putBoolean(namedVariant.getName(), namedVariant.getBool());
break;
case NamedVariant.TYPE_STRING:
bundle.putString(namedVariant.getName(), namedVariant.getString());
break;
default:
Log.w(TAG,
"Unsupported type found in createExtras : " + namedVariant.getType());
}
}
return bundle;
}
}

View File

@@ -47,6 +47,7 @@ import java.util.StringJoiner;
* entity_list_not_editable (String[])
* entity_list_editable (String[])
* lang_id_threshold_override (float)
* template_intent_factory_enabled (boolean)
* </pre>
*
* <p>
@@ -97,6 +98,7 @@ public final class TextClassificationConstants {
"notification_conversation_action_types_default";
private static final String LANG_ID_THRESHOLD_OVERRIDE =
"lang_id_threshold_override";
private static final String TEMPLATE_INTENT_FACTORY_ENABLED = "template_intent_factory_enabled";
private static final boolean LOCAL_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
private static final boolean SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
@@ -137,6 +139,7 @@ public final class TextClassificationConstants {
* @see EntityConfidence
*/
private static final float LANG_ID_THRESHOLD_OVERRIDE_DEFAULT = -1f;
private static final boolean TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT = true;
private final boolean mSystemTextClassifierEnabled;
private final boolean mLocalTextClassifierEnabled;
@@ -155,6 +158,7 @@ public final class TextClassificationConstants {
private final List<String> mInAppConversationActionTypesDefault;
private final List<String> mNotificationConversationActionTypesDefault;
private final float mLangIdThresholdOverride;
private final boolean mTemplateIntentFactoryEnabled;
private TextClassificationConstants(@Nullable String settings) {
final KeyValueListParser parser = new KeyValueListParser(',');
@@ -215,6 +219,8 @@ public final class TextClassificationConstants {
mLangIdThresholdOverride = parser.getFloat(
LANG_ID_THRESHOLD_OVERRIDE,
LANG_ID_THRESHOLD_OVERRIDE_DEFAULT);
mTemplateIntentFactoryEnabled = parser.getBoolean(
TEMPLATE_INTENT_FACTORY_ENABLED, TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT);
}
/** Load from a settings string. */
@@ -290,6 +296,10 @@ public final class TextClassificationConstants {
return mLangIdThresholdOverride;
}
public boolean isTemplateIntentFactoryEnabled() {
return mTemplateIntentFactoryEnabled;
}
private static List<String> parseStringList(String listStr) {
return Collections.unmodifiableList(Arrays.asList(listStr.split(STRING_LIST_DELIMITER)));
}
@@ -315,6 +325,7 @@ public final class TextClassificationConstants {
pw.printPair("getNotificationConversationActionTypes",
mNotificationConversationActionTypesDefault);
pw.printPair("getLangIdThresholdOverride", mLangIdThresholdOverride);
pw.printPair("isTemplateIntentFactoryEnabled", mTemplateIntentFactoryEnabled);
pw.decreaseIndent();
pw.println();
}

View File

@@ -16,30 +16,21 @@
package android.view.textclassifier;
import static java.time.temporal.ChronoUnit.MILLIS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Icon;
import android.icu.util.ULocale;
import android.net.Uri;
import android.os.Bundle;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.os.UserManager;
import android.provider.Browser;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -53,19 +44,15 @@ import com.google.android.textclassifier.LangIdModel;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Default implementation of the {@link TextClassifier} interface.
@@ -128,6 +115,8 @@ public final class TextClassifierImpl implements TextClassifier {
private final ModelFileManager mLangIdModelFileManager;
private final ModelFileManager mActionsModelFileManager;
private final IntentFactory mIntentFactory;
public TextClassifierImpl(
Context context, TextClassificationConstants settings, TextClassifier fallback) {
mContext = Preconditions.checkNotNull(context);
@@ -155,6 +144,10 @@ public final class TextClassifierImpl implements TextClassifier {
UPDATED_ACTIONS_MODEL,
ActionsSuggestionsModel::getVersion,
ActionsSuggestionsModel::getLocales));
mIntentFactory = mSettings.isTemplateIntentFactoryEnabled()
? new TemplateIntentFactory(new LegacyIntentFactory())
: new LegacyIntentFactory();
}
public TextClassifierImpl(Context context, TextClassificationConstants settings) {
@@ -198,7 +191,8 @@ public final class TextClassifierImpl implements TextClassifier {
new AnnotatorModel.ClassificationOptions(
refTime.toInstant().toEpochMilli(),
refTime.getZone().getId(),
localesString));
localesString),
mContext);
final int size = results.length;
for (int i = 0; i < size; i++) {
tsBuilder.setEntityType(results[i].getCollection(), results[i].getScore());
@@ -241,7 +235,8 @@ public final class TextClassifierImpl implements TextClassifier {
new AnnotatorModel.ClassificationOptions(
refTime.toInstant().toEpochMilli(),
refTime.getZone().getId(),
localesString));
localesString),
mContext);
if (results.length > 0) {
return createClassificationResult(
results, string,
@@ -560,8 +555,9 @@ public final class TextClassifierImpl implements TextClassifier {
AnnotatorModel.ClassificationResult highestScoringResult =
typeCount > 0 ? classifications[0] : null;
for (int i = 0; i < typeCount; i++) {
builder.setEntityType(classifications[i].getCollection(),
classifications[i].getScore());
builder.setEntityType(
classifications[i].getCollection(),
classifications[i].getScore());
if (classifications[i].getScore() > highestScoringResult.getScore()) {
highestScoringResult = classifications[i];
}
@@ -572,9 +568,13 @@ public final class TextClassifierImpl implements TextClassifier {
: 0.5f /* TODO: Load this from the langId model. */;
boolean isPrimaryAction = true;
final ArrayList<Intent> sourceIntents = new ArrayList<>();
for (LabeledIntent labeledIntent : IntentFactory.create(
mContext, classifiedText, isForeignText(classifiedText, foreignTextThreshold),
referenceTime, highestScoringResult)) {
List<LabeledIntent> labeledIntents = mIntentFactory.create(
mContext,
classifiedText,
isForeignText(classifiedText, foreignTextThreshold),
referenceTime,
highestScoringResult);
for (LabeledIntent labeledIntent : labeledIntents) {
final RemoteAction action = labeledIntent.asRemoteAction(mContext);
if (action == null) {
continue;
@@ -720,11 +720,13 @@ public final class TextClassifierImpl implements TextClassifier {
mRequestCode = requestCode;
}
String getTitle() {
@VisibleForTesting
public String getTitle() {
return mTitle;
}
String getDescription() {
@VisibleForTesting
public String getDescription() {
return mDescription;
}
@@ -733,7 +735,8 @@ public final class TextClassifierImpl implements TextClassifier {
return mIntent;
}
int getRequestCode() {
@VisibleForTesting
public int getRequestCode() {
return mRequestCode;
}
@@ -769,233 +772,4 @@ public final class TextClassifierImpl implements TextClassifier {
return action;
}
}
/**
* Creates intents based on the classification type.
*/
@VisibleForTesting
public static final class IntentFactory {
private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
private IntentFactory() {}
@NonNull
public static List<LabeledIntent> create(
Context context,
String text,
boolean foreignText,
@Nullable Instant referenceTime,
@Nullable AnnotatorModel.ClassificationResult classification) {
final String type = classification != null
? classification.getCollection().trim().toLowerCase(Locale.ENGLISH)
: "";
text = text.trim();
final List<LabeledIntent> actions;
switch (type) {
case TextClassifier.TYPE_EMAIL:
actions = createForEmail(context, text);
break;
case TextClassifier.TYPE_PHONE:
actions = createForPhone(context, text);
break;
case TextClassifier.TYPE_ADDRESS:
actions = createForAddress(context, text);
break;
case TextClassifier.TYPE_URL:
actions = createForUrl(context, text);
break;
case TextClassifier.TYPE_DATE: // fall through
case TextClassifier.TYPE_DATE_TIME:
if (classification.getDatetimeResult() != null) {
final Instant parsedTime = Instant.ofEpochMilli(
classification.getDatetimeResult().getTimeMsUtc());
actions = createForDatetime(context, type, referenceTime, parsedTime);
} else {
actions = new ArrayList<>();
}
break;
case TextClassifier.TYPE_FLIGHT_NUMBER:
actions = createForFlight(context, text);
break;
case TextClassifier.TYPE_DICTIONARY:
actions = createForDictionary(context, text);
break;
default:
actions = new ArrayList<>();
break;
}
if (foreignText) {
insertTranslateAction(actions, context, text);
}
actions.forEach(
action -> action.getIntent()
.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
return actions;
}
@NonNull
private static List<LabeledIntent> createForEmail(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.email),
context.getString(com.android.internal.R.string.email_desc),
new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("mailto:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.add_contact),
context.getString(com.android.internal.R.string.add_contact_desc),
new Intent(Intent.ACTION_INSERT_OR_EDIT)
.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
.putExtra(ContactsContract.Intents.Insert.EMAIL, text),
text.hashCode()));
return actions;
}
@NonNull
private static List<LabeledIntent> createForPhone(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
final UserManager userManager = context.getSystemService(UserManager.class);
final Bundle userRestrictions = userManager != null
? userManager.getUserRestrictions() : new Bundle();
if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.dial),
context.getString(com.android.internal.R.string.dial_desc),
new Intent(Intent.ACTION_DIAL).setData(
Uri.parse(String.format("tel:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
}
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.add_contact),
context.getString(com.android.internal.R.string.add_contact_desc),
new Intent(Intent.ACTION_INSERT_OR_EDIT)
.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
.putExtra(ContactsContract.Intents.Insert.PHONE, text),
text.hashCode()));
if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.sms),
context.getString(com.android.internal.R.string.sms_desc),
new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("smsto:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForAddress(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
try {
final String encText = URLEncoder.encode(text, "UTF-8");
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.map),
context.getString(com.android.internal.R.string.map_desc),
new Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(String.format("geo:0,0?q=%s", encText))),
LabeledIntent.DEFAULT_REQUEST_CODE));
} catch (UnsupportedEncodingException e) {
Log.e(LOG_TAG, "Could not encode address", e);
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForUrl(Context context, String text) {
if (Uri.parse(text).getScheme() == null) {
text = "http://" + text;
}
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.browse),
context.getString(com.android.internal.R.string.browse_desc),
new Intent(Intent.ACTION_VIEW, Uri.parse(text))
.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()),
LabeledIntent.DEFAULT_REQUEST_CODE));
return actions;
}
@NonNull
private static List<LabeledIntent> createForDatetime(
Context context, String type, @Nullable Instant referenceTime,
Instant parsedTime) {
if (referenceTime == null) {
// If no reference time was given, use now.
referenceTime = Instant.now();
}
List<LabeledIntent> actions = new ArrayList<>();
actions.add(createCalendarViewIntent(context, parsedTime));
final long millisUntilEvent = referenceTime.until(parsedTime, MILLIS);
if (millisUntilEvent > MIN_EVENT_FUTURE_MILLIS) {
actions.add(createCalendarCreateEventIntent(context, parsedTime, type));
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForFlight(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.view_flight),
context.getString(com.android.internal.R.string.view_flight_desc),
new Intent(Intent.ACTION_WEB_SEARCH)
.putExtra(SearchManager.QUERY, text),
text.hashCode()));
return actions;
}
@NonNull
private static LabeledIntent createCalendarViewIntent(Context context, Instant parsedTime) {
Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
ContentUris.appendId(builder, parsedTime.toEpochMilli());
return new LabeledIntent(
context.getString(com.android.internal.R.string.view_calendar),
context.getString(com.android.internal.R.string.view_calendar_desc),
new Intent(Intent.ACTION_VIEW).setData(builder.build()),
LabeledIntent.DEFAULT_REQUEST_CODE);
}
@NonNull
private static LabeledIntent createCalendarCreateEventIntent(
Context context, Instant parsedTime, @EntityType String type) {
final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
return new LabeledIntent(
context.getString(com.android.internal.R.string.add_calendar_event),
context.getString(com.android.internal.R.string.add_calendar_event_desc),
new Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,
parsedTime.toEpochMilli())
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
parsedTime.toEpochMilli() + DEFAULT_EVENT_DURATION),
parsedTime.hashCode());
}
private static void insertTranslateAction(
List<LabeledIntent> actions, Context context, String text) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.translate),
context.getString(com.android.internal.R.string.translate_desc),
new Intent(Intent.ACTION_TRANSLATE)
// TODO: Probably better to introduce a "translate" scheme instead of
// using EXTRA_TEXT.
.putExtra(Intent.EXTRA_TEXT, text),
text.hashCode()));
}
@NonNull
private static List<LabeledIntent> createForDictionary(Context context, String text) {
return Arrays.asList(new LabeledIntent(
context.getString(com.android.internal.R.string.define),
context.getString(com.android.internal.R.string.define_desc),
new Intent(Intent.ACTION_DEFINE)
.putExtra(Intent.EXTRA_TEXT, text),
text.hashCode()));
}
}
}