blob: 0d4338b0af6b46b547411ac03d9368b9f84f29a3 [file] [log] [blame]
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +00001/*
2 * Copyright 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
17package android.view.textclassifier;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.os.Bundle;
23import android.os.Parcel;
24import android.os.Parcelable;
25
26import com.android.internal.util.Preconditions;
27
28import java.lang.annotation.Retention;
29import java.lang.annotation.RetentionPolicy;
Tony Make94e0782018-12-14 11:57:54 +080030import java.util.Arrays;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +000031
32/**
33 * A text classifier event.
34 */
35// TODO: Comprehensive javadoc.
36public final class TextClassifierEvent implements Parcelable {
37
38 public static final Creator<TextClassifierEvent> CREATOR = new Creator<TextClassifierEvent>() {
39 @Override
40 public TextClassifierEvent createFromParcel(Parcel in) {
41 return readFromParcel(in);
42 }
43
44 @Override
45 public TextClassifierEvent[] newArray(int size) {
46 return new TextClassifierEvent[size];
47 }
48 };
49
50 /** @hide **/
51 @Retention(RetentionPolicy.SOURCE)
52 @IntDef({CATEGORY_UNDEFINED, CATEGORY_SELECTION, CATEGORY_LINKIFY,
53 CATEGORY_CONVERSATION_ACTIONS, CATEGORY_LANGUAGE_DETECTION})
54 public @interface Category {
55 // For custom event categories, use range 1000+.
56 }
57 /** Undefined category */
58 public static final int CATEGORY_UNDEFINED = 0;
59 /** Smart selection */
60 public static final int CATEGORY_SELECTION = 1;
61 /** Linkify */
62 public static final int CATEGORY_LINKIFY = 2;
63 /** Conversation actions */
64 public static final int CATEGORY_CONVERSATION_ACTIONS = 3;
65 /** Language detection */
66 public static final int CATEGORY_LANGUAGE_DETECTION = 4;
67
68 /** @hide */
69 @Retention(RetentionPolicy.SOURCE)
70 @IntDef({TYPE_UNDEFINED, TYPE_SELECTION_STARTED, TYPE_SELECTION_MODIFIED,
71 TYPE_SMART_SELECTION_SINGLE, TYPE_SMART_SELECTION_MULTI, TYPE_AUTO_SELECTION,
72 TYPE_ACTIONS_SHOWN, TYPE_LINK_CLICKED, TYPE_OVERTYPE, TYPE_COPY_ACTION,
73 TYPE_PASTE_ACTION, TYPE_CUT_ACTION, TYPE_SHARE_ACTION, TYPE_SMART_ACTION,
74 TYPE_SELECTION_DRAG, TYPE_SELECTION_DESTROYED, TYPE_OTHER_ACTION, TYPE_SELECT_ALL,
Tony Mak03a1d032019-01-24 15:12:00 +000075 TYPE_SELECTION_RESET, TYPE_MANUAL_REPLY, TYPE_ACTIONS_GENERATED})
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +000076 public @interface Type {
77 // For custom event types, use range 1,000,000+.
78 }
79 /** User started a new selection. */
80 public static final int TYPE_UNDEFINED = 0;
81 /** User started a new selection. */
82 public static final int TYPE_SELECTION_STARTED = 1;
83 /** User modified an existing selection. */
84 public static final int TYPE_SELECTION_MODIFIED = 2;
85 /** Smart selection triggered for a single token (word). */
86 public static final int TYPE_SMART_SELECTION_SINGLE = 3;
87 /** Smart selection triggered spanning multiple tokens (words). */
88 public static final int TYPE_SMART_SELECTION_MULTI = 4;
89 /** Something else other than user or the default TextClassifier triggered a selection. */
90 public static final int TYPE_AUTO_SELECTION = 5;
91 /** Smart actions shown to the user. */
92 public static final int TYPE_ACTIONS_SHOWN = 6;
93 /** User clicked a link. */
94 public static final int TYPE_LINK_CLICKED = 7;
95 /** User typed over the selection. */
96 public static final int TYPE_OVERTYPE = 8;
97 /** User clicked on Copy action. */
98 public static final int TYPE_COPY_ACTION = 9;
99 /** User clicked on Paste action. */
100 public static final int TYPE_PASTE_ACTION = 10;
101 /** User clicked on Cut action. */
102 public static final int TYPE_CUT_ACTION = 11;
103 /** User clicked on Share action. */
104 public static final int TYPE_SHARE_ACTION = 12;
105 /** User clicked on a Smart action. */
106 public static final int TYPE_SMART_ACTION = 13;
107 /** User dragged+dropped the selection. */
108 public static final int TYPE_SELECTION_DRAG = 14;
109 /** Selection is destroyed. */
110 public static final int TYPE_SELECTION_DESTROYED = 15;
111 /** User clicked on a custom action. */
112 public static final int TYPE_OTHER_ACTION = 16;
113 /** User clicked on Select All action */
114 public static final int TYPE_SELECT_ALL = 17;
115 /** User reset the smart selection. */
116 public static final int TYPE_SELECTION_RESET = 18;
117 /** User composed a reply. */
118 public static final int TYPE_MANUAL_REPLY = 19;
Tony Mak5a5f0d52019-01-08 11:07:23 +0000119 /** TextClassifier generated some actions */
120 public static final int TYPE_ACTIONS_GENERATED = 20;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000121
122 @Category private final int mEventCategory;
123 @Type private final int mEventType;
Tony Mak03a1d032019-01-24 15:12:00 +0000124 @Nullable private final String[] mEntityTypes;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000125 @Nullable private final TextClassificationContext mEventContext;
126 @Nullable private final String mResultId;
127 private final int mEventIndex;
128 private final long mEventTime;
129 private final Bundle mExtras;
130
131 // Smart selection.
132 private final int mRelativeWordStartIndex;
133 private final int mRelativeWordEndIndex;
134 private final int mRelativeSuggestedWordStartIndex;
135 private final int mRelativeSuggestedWordEndIndex;
136
137 // Smart action.
138 private final int[] mActionIndices;
139
140 // Language detection.
141 @Nullable private final String mLanguage;
Tony Mak03a1d032019-01-24 15:12:00 +0000142 private final float mScore;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000143
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000144 @Nullable private final String mModelName;
145
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000146 private TextClassifierEvent(
147 int eventCategory,
148 int eventType,
Tony Mak03a1d032019-01-24 15:12:00 +0000149 String[] entityTypes,
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000150 TextClassificationContext eventContext,
151 String resultId,
152 int eventIndex,
153 long eventTime,
154 Bundle extras,
155 int relativeWordStartIndex,
156 int relativeWordEndIndex,
157 int relativeSuggestedWordStartIndex,
158 int relativeSuggestedWordEndIndex,
159 int[] actionIndex,
Tony Mak03a1d032019-01-24 15:12:00 +0000160 String language,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000161 float score,
162 String modelVersion) {
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000163 mEventCategory = eventCategory;
164 mEventType = eventType;
Tony Mak03a1d032019-01-24 15:12:00 +0000165 mEntityTypes = entityTypes;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000166 mEventContext = eventContext;
167 mResultId = resultId;
168 mEventIndex = eventIndex;
169 mEventTime = eventTime;
170 mExtras = extras;
171 mRelativeWordStartIndex = relativeWordStartIndex;
172 mRelativeWordEndIndex = relativeWordEndIndex;
173 mRelativeSuggestedWordStartIndex = relativeSuggestedWordStartIndex;
174 mRelativeSuggestedWordEndIndex = relativeSuggestedWordEndIndex;
175 mActionIndices = actionIndex;
176 mLanguage = language;
Tony Mak03a1d032019-01-24 15:12:00 +0000177 mScore = score;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000178 mModelName = modelVersion;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000179 }
180
181 @Override
182 public int describeContents() {
183 return 0;
184 }
185
186 @Override
187 public void writeToParcel(Parcel dest, int flags) {
188 dest.writeInt(mEventCategory);
189 dest.writeInt(mEventType);
Tony Mak03a1d032019-01-24 15:12:00 +0000190 dest.writeStringArray(mEntityTypes);
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000191 dest.writeParcelable(mEventContext, flags);
192 dest.writeString(mResultId);
193 dest.writeInt(mEventIndex);
194 dest.writeLong(mEventTime);
195 dest.writeBundle(mExtras);
196 dest.writeInt(mRelativeWordStartIndex);
197 dest.writeInt(mRelativeWordEndIndex);
198 dest.writeInt(mRelativeSuggestedWordStartIndex);
199 dest.writeInt(mRelativeSuggestedWordEndIndex);
200 dest.writeIntArray(mActionIndices);
201 dest.writeString(mLanguage);
Tony Mak03a1d032019-01-24 15:12:00 +0000202 dest.writeFloat(mScore);
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000203 dest.writeString(mModelName);
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000204 }
205
206 private static TextClassifierEvent readFromParcel(Parcel in) {
207 return new TextClassifierEvent(
208 /* eventCategory= */ in.readInt(),
209 /* eventType= */ in.readInt(),
Tony Mak03a1d032019-01-24 15:12:00 +0000210 /* entityTypes=*/ in.readStringArray(),
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000211 /* eventContext= */ in.readParcelable(null),
212 /* resultId= */ in.readString(),
213 /* eventIndex= */ in.readInt(),
214 /* eventTime= */ in.readLong(),
215 /* extras= */ in.readBundle(),
216 /* relativeWordStartIndex= */ in.readInt(),
217 /* relativeWordEndIndex= */ in.readInt(),
218 /* relativeSuggestedWordStartIndex= */ in.readInt(),
219 /* relativeSuggestedWordEndIndex= */ in.readInt(),
220 /* actionIndices= */ in.createIntArray(),
Tony Mak03a1d032019-01-24 15:12:00 +0000221 /* language= */ in.readString(),
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000222 /* score= */ in.readFloat(),
223 /* modelVersion= */ in.readString());
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000224 }
225
226 /**
227 * Returns the event category. e.g. {@link #CATEGORY_SELECTION}.
228 */
229 @Category
230 public int getEventCategory() {
231 return mEventCategory;
232 }
233
234 /**
235 * Returns the event type. e.g. {@link #TYPE_SELECTION_STARTED}.
236 */
237 @Type
238 public int getEventType() {
239 return mEventType;
240 }
241
242 /**
Tony Mak03a1d032019-01-24 15:12:00 +0000243 * Returns an array of entity types. e.g. {@link TextClassifier#TYPE_ADDRESS}.
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000244 */
Tony Mak03a1d032019-01-24 15:12:00 +0000245 @NonNull
246 public String[] getEntityTypes() {
247 return mEntityTypes;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000248 }
249
250 /**
251 * Returns the event context.
252 */
253 @Nullable
254 public TextClassificationContext getEventContext() {
255 return mEventContext;
256 }
257
258 /**
259 * Returns the id of the text classifier result related to this event.
260 */
261 @Nullable
262 public String getResultId() {
263 return mResultId;
264 }
265
266 /**
267 * Returns the index of this event in the series of event it belongs to.
268 */
269 public int getEventIndex() {
270 return mEventIndex;
271 }
272
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000273 // TODO: Remove this API.
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000274 /**
275 * Returns the time this event occurred. This is the number of milliseconds since
276 * January 1, 1970, 00:00:00 GMT. 0 indicates not set.
277 */
278 public long getEventTime() {
279 return mEventTime;
280 }
281
282 /**
283 * Returns a bundle containing non-structured extra information about this event.
284 *
285 * <p><b>NOTE: </b>Do not modify this bundle.
286 */
287 @NonNull
288 public Bundle getExtras() {
289 return mExtras;
290 }
291
292 /**
293 * For smart selection. Returns the relative word index of the start of the selection.
294 */
295 public int getRelativeWordStartIndex() {
296 return mRelativeWordStartIndex;
297 }
298
299 /**
300 * For smart selection. Returns the relative word (exclusive) index of the end of the selection.
301 */
302 public int getRelativeWordEndIndex() {
303 return mRelativeWordEndIndex;
304 }
305
306 /**
307 * For smart selection. Returns the relative word index of the start of the smart selection.
308 */
309 public int getRelativeSuggestedWordStartIndex() {
310 return mRelativeSuggestedWordStartIndex;
311 }
312
313 /**
314 * For smart selection. Returns the relative word (exclusive) index of the end of the
315 * smart selection.
316 */
317 public int getRelativeSuggestedWordEndIndex() {
318 return mRelativeSuggestedWordEndIndex;
319 }
320
321 /**
322 * Returns the indices of the actions relating to this event.
323 * Actions are usually returned by the text classifier in priority order with the most
324 * preferred action at index 0. This list gives an indication of the position of the actions
325 * that are being reported.
326 */
327 @NonNull
328 public int[] getActionIndices() {
329 return mActionIndices;
330 }
331
332 /**
333 * For language detection. Returns the language tag for the detected locale.
334 * @see java.util.Locale#forLanguageTag(String).
335 */
336 @Nullable
337 public String getLanguage() {
338 return mLanguage;
339 }
340
341 /**
Tony Mak03a1d032019-01-24 15:12:00 +0000342 * Returns the score of the suggestion.
343 */
344 public float getScore() {
345 return mScore;
346 }
347
348 /**
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000349 * Returns the model name.
350 * @hide
351 */
352 @Nullable
353 public String getModelName() {
354 return mModelName;
355 }
356
357 /**
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000358 * Builder to build a text classifier event.
359 */
360 public static final class Builder {
361
362 private final int mEventCategory;
363 private final int mEventType;
Tony Mak03a1d032019-01-24 15:12:00 +0000364 private String[] mEntityTypes = new String[0];
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000365 @Nullable private TextClassificationContext mEventContext;
366 @Nullable private String mResultId;
367 private int mEventIndex;
368 private long mEventTime;
369 @Nullable private Bundle mExtras;
370 private int mRelativeWordStartIndex;
371 private int mRelativeWordEndIndex;
372 private int mRelativeSuggestedWordStartIndex;
373 private int mRelativeSuggestedWordEndIndex;
374 private int[] mActionIndices = new int[0];
375 @Nullable private String mLanguage;
Tony Mak03a1d032019-01-24 15:12:00 +0000376 private float mScore;
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000377
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000378 private String mModelName;
379
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000380 /**
381 * Creates a builder for building {@link TextClassifierEvent}s.
382 *
383 * @param eventCategory The event category. e.g. {@link #CATEGORY_SELECTION}
384 * @param eventType The event type. e.g. {@link #TYPE_SELECTION_STARTED}
385 */
386 public Builder(@Category int eventCategory, @Type int eventType) {
387 mEventCategory = eventCategory;
388 mEventType = eventType;
389 }
390
391 /**
Tony Mak03a1d032019-01-24 15:12:00 +0000392 * Sets the entity types. e.g. {@link TextClassifier#TYPE_ADDRESS}.
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000393 */
394 @NonNull
Tony Mak03a1d032019-01-24 15:12:00 +0000395 public Builder setEntityTypes(@NonNull String... entityTypes) {
396 mEntityTypes = new String[entityTypes.length];
397 System.arraycopy(entityTypes, 0, mEntityTypes, 0, entityTypes.length);
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000398 return this;
399 }
400
401 /**
402 * Sets the event context.
403 */
404 @NonNull
405 public Builder setEventContext(@Nullable TextClassificationContext eventContext) {
406 mEventContext = eventContext;
407 return this;
408 }
409
410 /**
411 * Sets the id of the text classifier result related to this event.
412 */
413 @NonNull
414 public Builder setResultId(@Nullable String resultId) {
415 mResultId = resultId;
416 return this;
417 }
418
419 /**
420 * Sets the index of this events in the series of events it belongs to.
421 */
422 @NonNull
423 public Builder setEventIndex(int eventIndex) {
424 mEventIndex = eventIndex;
425 return this;
426 }
427
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000428 // TODO: Remove this API.
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000429 /**
430 * Sets the time this event occurred. This is the number of milliseconds since
431 * January 1, 1970, 00:00:00 GMT. 0 indicates not set.
432 */
433 @NonNull
434 public Builder setEventTime(long eventTime) {
435 mEventTime = eventTime;
436 return this;
437 }
438
439 /**
440 * Sets a bundle containing non-structured extra information about the event.
441 *
442 * <p><b>NOTE: </b>Prefer to set only immutable values on the bundle otherwise, avoid
443 * updating the internals of this bundle as it may have unexpected consequences on the
444 * clients of the built event object. For similar reasons, avoid depending on mutable
445 * objects in this bundle.
446 */
447 @NonNull
448 public Builder setExtras(@NonNull Bundle extras) {
449 mExtras = Preconditions.checkNotNull(extras);
450 return this;
451 }
452
453 /**
454 * For smart selection. Sets the relative word index of the start of the selection.
455 */
456 @NonNull
457 public Builder setRelativeWordStartIndex(int relativeWordStartIndex) {
458 mRelativeWordStartIndex = relativeWordStartIndex;
459 return this;
460 }
461
462 /**
463 * For smart selection. Sets the relative word (exclusive) index of the end of the
464 * selection.
465 */
466 @NonNull
467 public Builder setRelativeWordEndIndex(int relativeWordEndIndex) {
468 mRelativeWordEndIndex = relativeWordEndIndex;
469 return this;
470 }
471
472 /**
473 * For smart selection. Sets the relative word index of the start of the smart selection.
474 */
475 @NonNull
476 public Builder setRelativeSuggestedWordStartIndex(int relativeSuggestedWordStartIndex) {
477 mRelativeSuggestedWordStartIndex = relativeSuggestedWordStartIndex;
478 return this;
479 }
480
481 /**
482 * For smart selection. Sets the relative word (exclusive) index of the end of the
483 * smart selection.
484 */
485 @NonNull
486 public Builder setRelativeSuggestedWordEndIndex(int relativeSuggestedWordEndIndex) {
487 mRelativeSuggestedWordEndIndex = relativeSuggestedWordEndIndex;
488 return this;
489 }
490
491 /**
492 * Sets the indices of the actions involved in this event. Actions are usually returned by
493 * the text classifier in priority order with the most preferred action at index 0.
494 * This index gives an indication of the position of the action that is being reported.
495 */
496 @NonNull
497 public Builder setActionIndices(@NonNull int... actionIndices) {
498 mActionIndices = new int[actionIndices.length];
499 System.arraycopy(actionIndices, 0, mActionIndices, 0, actionIndices.length);
500 return this;
501 }
502
503 /**
504 * For language detection. Sets the language tag for the detected locale.
505 * @see java.util.Locale#forLanguageTag(String).
506 */
507 @NonNull
508 public Builder setLanguage(@Nullable String language) {
509 mLanguage = language;
510 return this;
511 }
512
513 /**
Tony Mak03a1d032019-01-24 15:12:00 +0000514 * Sets the score of the suggestion.
515 */
516 @NonNull
517 public Builder setScore(float score) {
518 mScore = score;
519 return this;
520 }
521
522 /**
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000523 * Sets the model name string.
524 * @hide
525 */
526 public Builder setModelName(@Nullable String modelVersion) {
527 mModelName = modelVersion;
528 return this;
529 }
530
531 /**
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000532 * Builds and returns a text classifier event.
533 */
534 @NonNull
535 public TextClassifierEvent build() {
536 mExtras = mExtras == null ? Bundle.EMPTY : mExtras;
537 return new TextClassifierEvent(
538 mEventCategory,
539 mEventType,
Tony Mak03a1d032019-01-24 15:12:00 +0000540 mEntityTypes,
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000541 mEventContext,
542 mResultId,
543 mEventIndex,
544 mEventTime,
545 mExtras,
546 mRelativeWordStartIndex,
547 mRelativeWordEndIndex,
548 mRelativeSuggestedWordStartIndex,
549 mRelativeSuggestedWordEndIndex,
550 mActionIndices,
Tony Mak03a1d032019-01-24 15:12:00 +0000551 mLanguage,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000552 mScore,
553 mModelName);
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000554 }
555 // TODO: Add build(boolean validate).
556 }
Tony Make94e0782018-12-14 11:57:54 +0800557
558 @Override
559 public String toString() {
560 StringBuilder out = new StringBuilder(128);
561 out.append("TextClassifierEvent{");
562 out.append("mEventCategory=").append(mEventCategory);
Tony Mak03a1d032019-01-24 15:12:00 +0000563 out.append(", mEventTypes=").append(Arrays.toString(mEntityTypes));
Tony Make94e0782018-12-14 11:57:54 +0800564 out.append(", mEventContext=").append(mEventContext);
565 out.append(", mResultId=").append(mResultId);
566 out.append(", mEventIndex=").append(mEventIndex);
567 out.append(", mEventTime=").append(mEventTime);
568 out.append(", mExtras=").append(mExtras);
569 out.append(", mRelativeWordStartIndex=").append(mRelativeWordStartIndex);
570 out.append(", mRelativeWordEndIndex=").append(mRelativeWordEndIndex);
571 out.append(", mRelativeSuggestedWordStartIndex=").append(mRelativeSuggestedWordStartIndex);
572 out.append(", mRelativeSuggestedWordEndIndex=").append(mRelativeSuggestedWordEndIndex);
573 out.append(", mActionIndices=").append(Arrays.toString(mActionIndices));
574 out.append(", mLanguage=").append(mLanguage);
Tony Mak03a1d032019-01-24 15:12:00 +0000575 out.append(", mScore=").append(mScore);
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000576 out.append(", mModelName=").append(mModelName);
Tony Make94e0782018-12-14 11:57:54 +0800577 out.append("}");
578 return out.toString();
579 }
Abodunrinwa Toki37ccedc2018-12-11 00:35:11 +0000580}