ConversationActions.java

/*
 * Copyright 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 androidx.textclassifier;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.os.Bundle;
import android.text.SpannedString;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
import androidx.core.app.Person;
import androidx.core.util.Preconditions;

import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Represents a list of actions suggested by a {@link TextClassifier} on a given conversation.
 * <p>
 * This is an object to store the result of {@link TextClassifier#suggestConversationActions(Request)}.
 *
 * @see TextClassifier#suggestConversationActions(Request)
 */
public final class ConversationActions {
    private static final String EXTRA_CONVERSATION_ACTIONS = "conversation_actions";
    private static final String EXTRA_ID = "id";

    private final List<ConversationAction> mConversationActions;
    private final String mId;

    /** Constructs a {@link ConversationActions} object. */
    public ConversationActions(
            @NonNull List<ConversationAction> conversationActions, @Nullable String id) {
        mConversationActions =
                Collections.unmodifiableList(Preconditions.checkNotNull(conversationActions));
        mId = id;
    }

    /**
     * Returns an immutable list of {@link ConversationAction} objects, which are ordered from high
     * confidence to low confidence.
     */
    @NonNull
    public List<ConversationAction> getConversationActions() {
        return mConversationActions;
    }

    /**
     * Returns the id, if one exists, for this object.
     */
    @Nullable
    public String getId() {
        return mId;
    }

    /**
     * Adds this object to a Bundle that can be read back with the same parameters
     * to {@link #createFromBundle(Bundle)}.
     */
    @NonNull
    public Bundle toBundle() {
        Bundle bundle = new Bundle();
        BundleUtils.putConversationActionsList(
                bundle, EXTRA_CONVERSATION_ACTIONS, mConversationActions);
        bundle.putString(EXTRA_ID, mId);
        return bundle;
    }

    /**
     * Converts a bundle that was created using {@link #toBundle()} to a
     * {@link ConversationActions}.
     */
    @NonNull
    public static ConversationActions createFromBundle(@NonNull Bundle bundle) {
        return new ConversationActions(
                BundleUtils.getConversationActionsList(bundle, EXTRA_CONVERSATION_ACTIONS),
                bundle.getString(EXTRA_ID));
    }

    /** Represents a message in the conversation. */
    public static final class Message {
        private static final String EXTRA_AUTHOR = "author";
        private static final String EXTRA_REFERENCE_TIME = "reference_time";
        private static final String EXTRA_TEXT = "text";
        private static final String EXTRA_EXTRAS = "extras";

        /**
         * Represents the local user.
         */
        @NonNull
        public static final Person PERSON_USER_SELF =
                new Person.Builder()
                        .setKey("text-classifier-conversation-actions-user-self")
                        .build();

        /**
         * Represents the remote user.
         * <p>
         * If possible, you are suggested to create a {@link Person} object that can identify
         * the remote user better, so that the underlying model could differentiate between
         * different remote users.
         */
        @NonNull
        public static final Person PERSON_USER_OTHERS =
                new Person.Builder()
                        .setKey("text-classifier-conversation-actions-user-others")
                        .build();

        @NonNull
        private final Person mAuthor;
        @Nullable
        private final Long mReferenceTime;
        @Nullable
        private final CharSequence mText;
        @NonNull
        private final Bundle mExtras;

        Message(
                @Nullable Person author,
                @Nullable Long referenceTime,
                @Nullable CharSequence text,
                @NonNull Bundle bundle) {
            mAuthor = author;
            mReferenceTime = referenceTime;
            mText = text;
            mExtras = Preconditions.checkNotNull(bundle);
        }

        /** Returns the person that composed the message. */
        @NonNull
        public Person getAuthor() {
            return mAuthor;
        }

        /**
         * Returns the reference time of the message, for example it could be the compose or send
         * time of this message. This should be milliseconds from the epoch of
         * 1970-01-01T00:00:00Z(UTC timezone). If no reference time or {@code null} is set,
         * now is used.
         */
        @Nullable
        public Long getReferenceTime() {
            return mReferenceTime;
        }

        /** Returns the text of the message. */
        @Nullable
        public CharSequence getText() {
            return mText;
        }

        /**
         * Returns the extended data related to this conversation action.
         *
         * <p><b>NOTE: </b>Each call to this method returns a new bundle copy so clients should
         * prefer to hold a reference to the returned bundle rather than frequently calling this
         * method.
         */
        @NonNull
        public Bundle getExtras() {
            return mExtras;
        }

        /**
         * Adds this object to a Bundle that can be read back with the same parameters
         * to {@link #createFromBundle(Bundle)}.
         */
        @NonNull
        public Bundle toBundle() {
            Bundle bundle = new Bundle();
            bundle.putBundle(EXTRA_AUTHOR, mAuthor.toBundle());
            BundleUtils.putLong(bundle, EXTRA_REFERENCE_TIME, mReferenceTime);
            bundle.putCharSequence(EXTRA_TEXT, mText);
            bundle.putBundle(EXTRA_EXTRAS, mExtras);
            return bundle;
        }

        /**
         * Converts a bundle that was created using {@link #toBundle()} to a
         * {@link ConversationActions.Message}.
         */
        @NonNull
        public static Message createFromBundle(@NonNull Bundle bundle) {
            return new ConversationActions.Message(
                    Person.fromBundle(bundle.getBundle(EXTRA_AUTHOR)),
                    BundleUtils.getLong(bundle, EXTRA_REFERENCE_TIME),
                    bundle.getCharSequence(EXTRA_TEXT),
                    bundle.getBundle(EXTRA_EXTRAS));
        }

        /** Builder class to construct a {@link Message} */
        public static final class Builder {
            @Nullable
            Person mAuthor;
            @Nullable
            private Long mReferenceTime;
            @Nullable
            private CharSequence mText;
            @Nullable
            private Bundle mExtras;

            /**
             * Constructs a builder.
             *
             * @param author the person that composed the message, use {@link #PERSON_USER_SELF}
             *               to represent the local user. If it is not possible to identify the
             *               remote user that the local user is conversing with, use
             *               {@link #PERSON_USER_OTHERS} to represent a remote user.
             */
            public Builder(@NonNull Person author) {
                mAuthor = Preconditions.checkNotNull(author);
            }

            /** Sets the text of this message. */
            @NonNull
            public Builder setText(@Nullable CharSequence text) {
                mText = text;
                return this;
            }

            /**
             * Sets the reference time of this message, for example it could be the compose or send
             * time of this message. This should be milliseconds from the epoch of
             * 1970-01-01T00:00:00Z(UTC timezone). If no reference time or {@code null} is set,
             * now is used.
             */
            @NonNull
            public Builder setReferenceTime(@Nullable Long referenceTime) {
                mReferenceTime = referenceTime;
                return this;
            }

            /** Sets a set of extended data to the message. */
            @NonNull
            public Builder setExtras(@Nullable Bundle bundle) {
                this.mExtras = bundle;
                return this;
            }

            /** Builds the {@link Message} object. */
            @NonNull
            public Message build() {
                return new Message(
                        mAuthor,
                        mReferenceTime,
                        mText == null ? null : new SpannedString(mText),
                        mExtras == null ? Bundle.EMPTY : mExtras);
            }
        }
    }

    /**
     * A request object for generating conversation action suggestions.
     *
     * @see TextClassifier#suggestConversationActions(Request)
     */
    public static final class Request {
        private static final String EXTRA_MESSAGES = "messages";
        private static final String EXTRA_ENTITY_CONFIG = "entity_config";
        private static final String EXTRA_MAX_SUGGESTION = "max_suggestion";
        private static final String EXTRA_HINTS = "hints";
        private static final String EXTRA_EXTRAS = "extras";


        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        @Retention(SOURCE)
        @StringDef(
                value = {
                        HINT_FOR_NOTIFICATION,
                        HINT_FOR_IN_APP,
                })
        public @interface Hint {}

        /**
         * To indicate the generated actions will be used within the app.
         */
        public static final String HINT_FOR_IN_APP = "in_app";
        /**
         * To indicate the generated actions will be used for notification.
         */
        public static final String HINT_FOR_NOTIFICATION = "notification";

        @NonNull
        private final List<Message> mConversation;
        @NonNull
        private final TextClassifier.EntityConfig mTypeConfig;
        private final int mMaxSuggestions;
        @NonNull
        @Hint
        private final List<String> mHints;
        @NonNull
        private final Bundle mExtras;

        Request(
                @NonNull List<Message> conversation,
                @NonNull TextClassifier.EntityConfig typeConfig,
                int maxSuggestions,
                @NonNull @Hint List<String> hints,
                @NonNull Bundle extras) {
            mConversation = Preconditions.checkNotNull(conversation);
            mTypeConfig = Preconditions.checkNotNull(typeConfig);
            mMaxSuggestions = maxSuggestions;
            mHints = Preconditions.checkNotNull(hints);
            mExtras = Preconditions.checkNotNull(extras);
        }

        /** Returns the type config. */
        @NonNull
        public TextClassifier.EntityConfig getTypeConfig() {
            return mTypeConfig;
        }

        /** Returns an immutable list of messages that make up the conversation. */
        @NonNull
        public List<Message> getConversation() {
            return mConversation;
        }

        /**
         * Return the maximal number of suggestions the caller wants, value -1 means no restriction
         * and this is the default.
         */
        @IntRange(from = -1)
        public int getMaxSuggestions() {
            return mMaxSuggestions;
        }

        /** Returns an immutable list of hints */
        @NonNull
        @Hint
        public List<String> getHints() {
            return mHints;
        }

        /**
         * Returns the extended data related to this request.
         *
         * <p><b>NOTE: </b>Do not modify this bundle.
         */
        @NonNull
        public Bundle getExtras() {
            return mExtras;
        }

        /**
         * Adds this object to a Bundle that can be read back with the same parameters
         * to {@link #createFromBundle(Bundle)}.
         */
        @NonNull
        public Bundle toBundle() {
            Bundle bundle = new Bundle();
            BundleUtils.putConversationActionsMessageList(bundle, EXTRA_MESSAGES, mConversation);
            bundle.putBundle(EXTRA_ENTITY_CONFIG, mTypeConfig.toBundle());
            bundle.putInt(EXTRA_MAX_SUGGESTION, mMaxSuggestions);
            bundle.putStringArrayList(EXTRA_HINTS, new ArrayList<>(mHints));
            bundle.putBundle(EXTRA_EXTRAS, mExtras);
            return bundle;
        }

        /**
         * Converts a bundle that was created using {@link #toBundle()} to a
         * {@link ConversationActions.Request}.
         */
        @NonNull
        public static Request createFromBundle(@NonNull Bundle bundle) {
            return new Request(
                    BundleUtils.getConversationActionsMessageList(bundle, EXTRA_MESSAGES),
                    TextClassifier.EntityConfig.createFromBundle(
                            bundle.getBundle(EXTRA_ENTITY_CONFIG)),
                    bundle.getInt(EXTRA_MAX_SUGGESTION),
                    bundle.getStringArrayList(EXTRA_HINTS),
                    bundle.getBundle(EXTRA_EXTRAS));
        }

        /** Builder object to construct the {@link Request} object. */
        public static final class Builder {
            @NonNull
            private List<Message> mConversation;
            @Nullable
            private TextClassifier.EntityConfig mTypeConfig;
            private int mMaxSuggestions = -1;
            @Nullable
            @Hint
            private List<String> mHints;
            @Nullable
            private Bundle mExtras;

            /**
             * Constructs a builder.
             *
             * @param conversation the conversation that the text classifier is going to generate
             *     actions for.
             */
            public Builder(@NonNull List<Message> conversation) {
                mConversation = Preconditions.checkNotNull(conversation);
            }

            /**
             * Sets the hints to help text classifier to generate actions. It could be used to help
             * text classifier to infer what types of actions the caller may be interested in.
             */
            @NonNull
            public Builder setHints(@Nullable @Hint List<String> hints) {
                mHints = hints;
                return this;
            }

            /** Sets the type config. */
            @NonNull
            public Builder setTypeConfig(@Nullable TextClassifier.EntityConfig typeConfig) {
                mTypeConfig = typeConfig;
                return this;
            }

            /**
             * Sets the maximum number of suggestions you want. Value -1 means no restriction and
             * this is the default.
             */
            @NonNull
            public Builder setMaxSuggestions(@IntRange(from = -1) int maxSuggestions) {
                mMaxSuggestions = Preconditions.checkArgumentNonnegative(maxSuggestions);
                return this;
            }

            /** Sets a set of extended data to the request. */
            @NonNull
            public Builder setExtras(@Nullable Bundle bundle) {
                mExtras = bundle;
                return this;
            }

            /** Builds the {@link Request} object. */
            @NonNull
            public Request build() {
                return new Request(
                        Collections.unmodifiableList(mConversation),
                        mTypeConfig == null
                                ? new TextClassifier.EntityConfig.Builder().build()
                                : mTypeConfig,
                        mMaxSuggestions,
                        mHints == null
                                ? Collections.<String>emptyList()
                                : Collections.unmodifiableList(mHints),
                        mExtras == null ? Bundle.EMPTY : mExtras);
            }
        }
    }
}