Merge "Update IntentFactory to construct intents using RemoteActionTemplate... objects that are returned by the model"
This commit is contained in:
committed by
Android (Google) Code Review
commit
577c93bd2e
@@ -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>
|
||||
|
||||
56
core/java/android/view/textclassifier/IntentFactory.java
Normal file
56
core/java/android/view/textclassifier/IntentFactory.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
260
core/java/android/view/textclassifier/LegacyIntentFactory.java
Normal file
260
core/java/android/view/textclassifier/LegacyIntentFactory.java
Normal 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;
|
||||
}
|
||||
}
|
||||
167
core/java/android/view/textclassifier/TemplateIntentFactory.java
Normal file
167
core/java/android/view/textclassifier/TemplateIntentFactory.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user