1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.view.textclassifier;
17 
18 import static java.lang.annotation.RetentionPolicy.SOURCE;
19 
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.StringDef;
24 import android.annotation.UserIdInt;
25 import android.app.Person;
26 import android.os.Bundle;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.os.UserHandle;
30 import android.text.SpannedString;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.util.Preconditions;
34 
35 import java.lang.annotation.Retention;
36 import java.time.ZonedDateTime;
37 import java.time.format.DateTimeFormatter;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 
42 /**
43  * Represents a list of actions suggested by a {@link TextClassifier} on a given conversation.
44  *
45  * @see TextClassifier#suggestConversationActions(Request)
46  */
47 public final class ConversationActions implements Parcelable {
48 
49     public static final @android.annotation.NonNull Creator<ConversationActions> CREATOR =
50             new Creator<ConversationActions>() {
51                 @Override
52                 public ConversationActions createFromParcel(Parcel in) {
53                     return new ConversationActions(in);
54                 }
55 
56                 @Override
57                 public ConversationActions[] newArray(int size) {
58                     return new ConversationActions[size];
59                 }
60             };
61 
62     private final List<ConversationAction> mConversationActions;
63     private final String mId;
64 
65     /** Constructs a {@link ConversationActions} object. */
ConversationActions( @onNull List<ConversationAction> conversationActions, @Nullable String id)66     public ConversationActions(
67             @NonNull List<ConversationAction> conversationActions, @Nullable String id) {
68         mConversationActions =
69                 Collections.unmodifiableList(Preconditions.checkNotNull(conversationActions));
70         mId = id;
71     }
72 
ConversationActions(Parcel in)73     private ConversationActions(Parcel in) {
74         mConversationActions =
75                 Collections.unmodifiableList(in.createTypedArrayList(ConversationAction.CREATOR));
76         mId = in.readString();
77     }
78 
79     @Override
describeContents()80     public int describeContents() {
81         return 0;
82     }
83 
84     @Override
writeToParcel(Parcel parcel, int flags)85     public void writeToParcel(Parcel parcel, int flags) {
86         parcel.writeTypedList(mConversationActions);
87         parcel.writeString(mId);
88     }
89 
90     /**
91      * Returns an immutable list of {@link ConversationAction} objects, which are ordered from high
92      * confidence to low confidence.
93      */
94     @NonNull
getConversationActions()95     public List<ConversationAction> getConversationActions() {
96         return mConversationActions;
97     }
98 
99     /**
100      * Returns the id, if one exists, for this object.
101      */
102     @Nullable
getId()103     public String getId() {
104         return mId;
105     }
106 
107     /** Represents a message in the conversation. */
108     public static final class Message implements Parcelable {
109         /**
110          * Represents the local user.
111          *
112          * @see Builder#Builder(Person)
113          */
114         @NonNull
115         public static final Person PERSON_USER_SELF =
116                 new Person.Builder()
117                         .setKey("text-classifier-conversation-actions-user-self")
118                         .build();
119 
120         /**
121          * Represents the remote user.
122          * <p>
123          * If possible, you are suggested to create a {@link Person} object that can identify
124          * the remote user better, so that the underlying model could differentiate between
125          * different remote users.
126          *
127          * @see Builder#Builder(Person)
128          */
129         @NonNull
130         public static final Person PERSON_USER_OTHERS =
131                 new Person.Builder()
132                         .setKey("text-classifier-conversation-actions-user-others")
133                         .build();
134 
135         @Nullable
136         private final Person mAuthor;
137         @Nullable
138         private final ZonedDateTime mReferenceTime;
139         @Nullable
140         private final CharSequence mText;
141         @NonNull
142         private final Bundle mExtras;
143 
Message( @ullable Person author, @Nullable ZonedDateTime referenceTime, @Nullable CharSequence text, @NonNull Bundle bundle)144         private Message(
145                 @Nullable Person author,
146                 @Nullable ZonedDateTime referenceTime,
147                 @Nullable CharSequence text,
148                 @NonNull Bundle bundle) {
149             mAuthor = author;
150             mReferenceTime = referenceTime;
151             mText = text;
152             mExtras = Preconditions.checkNotNull(bundle);
153         }
154 
Message(Parcel in)155         private Message(Parcel in) {
156             mAuthor = in.readParcelable(null);
157             mReferenceTime =
158                     in.readInt() == 0
159                             ? null
160                             : ZonedDateTime.parse(
161                                     in.readString(), DateTimeFormatter.ISO_ZONED_DATE_TIME);
162             mText = in.readCharSequence();
163             mExtras = in.readBundle();
164         }
165 
166         @Override
writeToParcel(Parcel parcel, int flags)167         public void writeToParcel(Parcel parcel, int flags) {
168             parcel.writeParcelable(mAuthor, flags);
169             parcel.writeInt(mReferenceTime != null ? 1 : 0);
170             if (mReferenceTime != null) {
171                 parcel.writeString(mReferenceTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
172             }
173             parcel.writeCharSequence(mText);
174             parcel.writeBundle(mExtras);
175         }
176 
177         @Override
describeContents()178         public int describeContents() {
179             return 0;
180         }
181 
182         public static final @android.annotation.NonNull Creator<Message> CREATOR =
183                 new Creator<Message>() {
184                     @Override
185                     public Message createFromParcel(Parcel in) {
186                         return new Message(in);
187                     }
188 
189                     @Override
190                     public Message[] newArray(int size) {
191                         return new Message[size];
192                     }
193                 };
194 
195         /** Returns the person that composed the message. */
196         @NonNull
getAuthor()197         public Person getAuthor() {
198             return mAuthor;
199         }
200 
201         /**
202          * Returns the reference time of the message, for example it could be the compose or send
203          * time of this message.
204          */
205         @Nullable
getReferenceTime()206         public ZonedDateTime getReferenceTime() {
207             return mReferenceTime;
208         }
209 
210         /** Returns the text of the message. */
211         @Nullable
getText()212         public CharSequence getText() {
213             return mText;
214         }
215 
216         /**
217          * Returns the extended data related to this conversation action.
218          *
219          * <p><b>NOTE: </b>Do not modify this bundle.
220          */
221         @NonNull
getExtras()222         public Bundle getExtras() {
223             return mExtras;
224         }
225 
226         /** Builder class to construct a {@link Message} */
227         public static final class Builder {
228             @Nullable
229             private Person mAuthor;
230             @Nullable
231             private ZonedDateTime mReferenceTime;
232             @Nullable
233             private CharSequence mText;
234             @Nullable
235             private Bundle mExtras;
236 
237             /**
238              * Constructs a builder.
239              *
240              * @param author the person that composed the message, use {@link #PERSON_USER_SELF}
241              *               to represent the local user. If it is not possible to identify the
242              *               remote user that the local user is conversing with, use
243              *               {@link #PERSON_USER_OTHERS} to represent a remote user.
244              */
Builder(@onNull Person author)245             public Builder(@NonNull Person author) {
246                 mAuthor = Preconditions.checkNotNull(author);
247             }
248 
249             /** Sets the text of this message. */
250             @NonNull
setText(@ullable CharSequence text)251             public Builder setText(@Nullable CharSequence text) {
252                 mText = text;
253                 return this;
254             }
255 
256             /**
257              * Sets the reference time of this message, for example it could be the compose or send
258              * time of this message.
259              */
260             @NonNull
setReferenceTime(@ullable ZonedDateTime referenceTime)261             public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) {
262                 mReferenceTime = referenceTime;
263                 return this;
264             }
265 
266             /** Sets a set of extended data to the message. */
267             @NonNull
setExtras(@ullable Bundle bundle)268             public Builder setExtras(@Nullable Bundle bundle) {
269                 this.mExtras = bundle;
270                 return this;
271             }
272 
273             /** Builds the {@link Message} object. */
274             @NonNull
build()275             public Message build() {
276                 return new Message(
277                         mAuthor,
278                         mReferenceTime,
279                         mText == null ? null : new SpannedString(mText),
280                         mExtras == null ? Bundle.EMPTY : mExtras);
281             }
282         }
283     }
284 
285     /**
286      * A request object for generating conversation action suggestions.
287      *
288      * @see TextClassifier#suggestConversationActions(Request)
289      */
290     public static final class Request implements Parcelable {
291 
292         /** @hide */
293         @Retention(SOURCE)
294         @StringDef(
295                 value = {
296                         HINT_FOR_NOTIFICATION,
297                         HINT_FOR_IN_APP,
298                 },
299                 prefix = "HINT_")
300         public @interface Hint {}
301 
302         /**
303          * To indicate the generated actions will be used within the app.
304          */
305         public static final String HINT_FOR_IN_APP = "in_app";
306         /**
307          * To indicate the generated actions will be used for notification.
308          */
309         public static final String HINT_FOR_NOTIFICATION = "notification";
310 
311         @NonNull
312         private final List<Message> mConversation;
313         @NonNull
314         private final TextClassifier.EntityConfig mTypeConfig;
315         private final int mMaxSuggestions;
316         @NonNull
317         @Hint
318         private final List<String> mHints;
319         @Nullable
320         private String mCallingPackageName;
321         @UserIdInt
322         private int mUserId = UserHandle.USER_NULL;
323         @NonNull
324         private Bundle mExtras;
325 
Request( @onNull List<Message> conversation, @NonNull TextClassifier.EntityConfig typeConfig, int maxSuggestions, @Nullable @Hint List<String> hints, @NonNull Bundle extras)326         private Request(
327                 @NonNull List<Message> conversation,
328                 @NonNull TextClassifier.EntityConfig typeConfig,
329                 int maxSuggestions,
330                 @Nullable @Hint List<String> hints,
331                 @NonNull Bundle extras) {
332             mConversation = Preconditions.checkNotNull(conversation);
333             mTypeConfig = Preconditions.checkNotNull(typeConfig);
334             mMaxSuggestions = maxSuggestions;
335             mHints = hints;
336             mExtras = extras;
337         }
338 
readFromParcel(Parcel in)339         private static Request readFromParcel(Parcel in) {
340             List<Message> conversation = new ArrayList<>();
341             in.readParcelableList(conversation, null);
342             TextClassifier.EntityConfig typeConfig = in.readParcelable(null);
343             int maxSuggestions = in.readInt();
344             List<String> hints = new ArrayList<>();
345             in.readStringList(hints);
346             String callingPackageName = in.readString();
347             int userId = in.readInt();
348             Bundle extras = in.readBundle();
349             Request request = new Request(
350                     conversation,
351                     typeConfig,
352                     maxSuggestions,
353                     hints,
354                     extras);
355             request.setCallingPackageName(callingPackageName);
356             request.setUserId(userId);
357             return request;
358         }
359 
360         @Override
writeToParcel(Parcel parcel, int flags)361         public void writeToParcel(Parcel parcel, int flags) {
362             parcel.writeParcelableList(mConversation, flags);
363             parcel.writeParcelable(mTypeConfig, flags);
364             parcel.writeInt(mMaxSuggestions);
365             parcel.writeStringList(mHints);
366             parcel.writeString(mCallingPackageName);
367             parcel.writeInt(mUserId);
368             parcel.writeBundle(mExtras);
369         }
370 
371         @Override
describeContents()372         public int describeContents() {
373             return 0;
374         }
375 
376         public static final @android.annotation.NonNull Creator<Request> CREATOR =
377                 new Creator<Request>() {
378                     @Override
379                     public Request createFromParcel(Parcel in) {
380                         return readFromParcel(in);
381                     }
382 
383                     @Override
384                     public Request[] newArray(int size) {
385                         return new Request[size];
386                     }
387                 };
388 
389         /** Returns the type config. */
390         @NonNull
getTypeConfig()391         public TextClassifier.EntityConfig getTypeConfig() {
392             return mTypeConfig;
393         }
394 
395         /** Returns an immutable list of messages that make up the conversation. */
396         @NonNull
getConversation()397         public List<Message> getConversation() {
398             return mConversation;
399         }
400 
401         /**
402          * Return the maximal number of suggestions the caller wants, value -1 means no restriction
403          * and this is the default.
404          */
405         @IntRange(from = -1)
getMaxSuggestions()406         public int getMaxSuggestions() {
407             return mMaxSuggestions;
408         }
409 
410         /** Returns an immutable list of hints */
411         @NonNull
412         @Hint
getHints()413         public List<String> getHints() {
414             return mHints;
415         }
416 
417         /**
418          * Sets the name of the package that is sending this request.
419          * <p>
420          * Package-private for SystemTextClassifier's use.
421          * @hide
422          */
423         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
setCallingPackageName(@ullable String callingPackageName)424         public void setCallingPackageName(@Nullable String callingPackageName) {
425             mCallingPackageName = callingPackageName;
426         }
427 
428         /**
429          * Returns the name of the package that sent this request.
430          * This returns {@code null} if no calling package name is set.
431          */
432         @Nullable
getCallingPackageName()433         public String getCallingPackageName() {
434             return mCallingPackageName;
435         }
436 
437         /**
438          * Sets the id of the user that sent this request.
439          * <p>
440          * Package-private for SystemTextClassifier's use.
441          */
setUserId(@serIdInt int userId)442         void setUserId(@UserIdInt int userId) {
443             mUserId = userId;
444         }
445 
446         /**
447          * Returns the id of the user that sent this request.
448          * @hide
449          */
450         @UserIdInt
getUserId()451         public int getUserId() {
452             return mUserId;
453         }
454 
455         /**
456          * Returns the extended data related to this request.
457          *
458          * <p><b>NOTE: </b>Do not modify this bundle.
459          */
460         @NonNull
getExtras()461         public Bundle getExtras() {
462             return mExtras;
463         }
464 
465         /** Builder object to construct the {@link Request} object. */
466         public static final class Builder {
467             @NonNull
468             private List<Message> mConversation;
469             @Nullable
470             private TextClassifier.EntityConfig mTypeConfig;
471             private int mMaxSuggestions = -1;
472             @Nullable
473             @Hint
474             private List<String> mHints;
475             @Nullable
476             private Bundle mExtras;
477 
478             /**
479              * Constructs a builder.
480              *
481              * @param conversation the conversation that the text classifier is going to generate
482              *     actions for.
483              */
Builder(@onNull List<Message> conversation)484             public Builder(@NonNull List<Message> conversation) {
485                 mConversation = Preconditions.checkNotNull(conversation);
486             }
487 
488             /**
489              * Sets the hints to help text classifier to generate actions. It could be used to help
490              * text classifier to infer what types of actions the caller may be interested in.
491              */
492             @NonNull
setHints(@ullable @int List<String> hints)493             public Builder setHints(@Nullable @Hint List<String> hints) {
494                 mHints = hints;
495                 return this;
496             }
497 
498             /** Sets the type config. */
499             @NonNull
setTypeConfig(@ullable TextClassifier.EntityConfig typeConfig)500             public Builder setTypeConfig(@Nullable TextClassifier.EntityConfig typeConfig) {
501                 mTypeConfig = typeConfig;
502                 return this;
503             }
504 
505             /**
506              * Sets the maximum number of suggestions you want. Value -1 means no restriction and
507              * this is the default.
508              */
509             @NonNull
setMaxSuggestions(@ntRangefrom = -1) int maxSuggestions)510             public Builder setMaxSuggestions(@IntRange(from = -1) int maxSuggestions) {
511                 mMaxSuggestions = Preconditions.checkArgumentNonnegative(maxSuggestions);
512                 return this;
513             }
514 
515             /** Sets a set of extended data to the request. */
516             @NonNull
setExtras(@ullable Bundle bundle)517             public Builder setExtras(@Nullable Bundle bundle) {
518                 mExtras = bundle;
519                 return this;
520             }
521 
522             /** Builds the {@link Request} object. */
523             @NonNull
build()524             public Request build() {
525                 return new Request(
526                         Collections.unmodifiableList(mConversation),
527                         mTypeConfig == null
528                                 ? new TextClassifier.EntityConfig.Builder().build()
529                                 : mTypeConfig,
530                         mMaxSuggestions,
531                         mHints == null
532                                 ? Collections.emptyList()
533                                 : Collections.unmodifiableList(mHints),
534                         mExtras == null ? Bundle.EMPTY : mExtras);
535             }
536         }
537     }
538 }
539