Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fc039c36 authored by Tony Mak's avatar Tony Mak
Browse files

Update IntentFactory to construct intents using RemoteActionTemplate...

objects that are returned by the model

1. TemplateIntentFactory is the intent generator. It reads from the
   templates that are returned from the model, and construct the
   intents accordingly. If template is missing, we fallback to use
   LegacyIntentFactory.
2. LegacyIntentFactory is the old(existing) intent generator.
3. Added a flag to allow us to switch between them.

Test: atest TemplateIntentFactoryTest.java
Test: atest LegacyIntentFactoryTest.java

Change-Id: I7bdcc73321f5a0160c5ff0edf1a2095119f4dcb1
parent 92fd2908
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -11724,6 +11724,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 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -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();
    }
Loading