• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 
17 package com.android.tv.twopanelsettings.slices.compat;
18 
19 import static android.app.slice.SliceItem.FORMAT_ACTION;
20 import static android.app.slice.SliceItem.FORMAT_BUNDLE;
21 import static android.app.slice.SliceItem.FORMAT_IMAGE;
22 import static android.app.slice.SliceItem.FORMAT_INT;
23 import static android.app.slice.SliceItem.FORMAT_LONG;
24 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
25 import static android.app.slice.SliceItem.FORMAT_SLICE;
26 import static android.app.slice.SliceItem.FORMAT_TEXT;
27 
28 import static com.android.tv.twopanelsettings.slices.compat.Slice.appendHints;
29 
30 import android.annotation.SuppressLint;
31 import android.app.PendingIntent;
32 import android.app.RemoteInput;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.graphics.Color;
36 import android.os.Bundle;
37 import android.os.Parcelable;
38 import android.text.Annotation;
39 import android.text.ParcelableSpan;
40 import android.text.Spannable;
41 import android.text.SpannableString;
42 import android.text.SpannableStringBuilder;
43 import android.text.Spanned;
44 import android.text.TextUtils;
45 import android.text.format.DateUtils;
46 import android.text.style.AlignmentSpan;
47 import android.text.style.ForegroundColorSpan;
48 import android.text.style.RelativeSizeSpan;
49 import android.text.style.StyleSpan;
50 import android.util.Log;
51 
52 import androidx.annotation.NonNull;
53 import androidx.annotation.Nullable;
54 import androidx.annotation.RequiresApi;
55 import androidx.annotation.StringDef;
56 import androidx.core.graphics.drawable.IconCompat;
57 import androidx.core.util.ObjectsCompat;
58 import androidx.core.util.Pair;
59 
60 import java.lang.annotation.Retention;
61 import java.lang.annotation.RetentionPolicy;
62 import java.net.URISyntaxException;
63 import java.util.Arrays;
64 import java.util.Calendar;
65 import java.util.List;
66 
67 /**
68  * A SliceItem is a single unit in the tree structure of a {@link Slice}.
69  *
70  * <p>A SliceItem a piece of content and some hints about what that content means or how it should
71  * be displayed. The types of content can be:
72  *
73  * <ul>
74  *   <li>{@link android.app.slice.SliceItem#FORMAT_SLICE}
75  *   <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}
76  *   <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}
77  *   <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}
78  *   <li>{@link android.app.slice.SliceItem#FORMAT_INT}
79  *   <li>{@link android.app.slice.SliceItem#FORMAT_LONG}
80  * </ul>
81  *
82  * <p>The hints that a {@link SliceItem} are a set of strings which annotate the content. The hints
83  * that are guaranteed to be understood by the system are defined on {@link Slice}.
84  *
85  * <p>Slice framework has been deprecated, it will not receive any updates moving forward. If you
86  * are looking for a framework that handles communication across apps, consider using {@link
87  * android.app.appsearch.AppSearchManager}.
88  */
89 public final class SliceItem {
90   private static final String TAG = "SliceItem";
91 
92   private static final String HINTS = "hints";
93   private static final String FORMAT = "format";
94   private static final String SUBTYPE = "subtype";
95   private static final String OBJ = "obj";
96   private static final String OBJ_2 = "obj_2";
97   private static final String INTENT = "intent";
98   private static final String SLICE_CONTENT = "androidx.slice.content";
99   private static final String SLICE_CONTENT_SENSITIVE = "sensitive";
100 
101   /** */
102   // @RestrictTo(Scope.LIBRARY)
103   @StringDef({
104     FORMAT_SLICE,
105     FORMAT_TEXT,
106     FORMAT_IMAGE,
107     FORMAT_ACTION,
108     FORMAT_INT,
109     FORMAT_LONG,
110     FORMAT_REMOTE_INPUT,
111     FORMAT_LONG,
112     FORMAT_BUNDLE
113   })
114   @Retention(RetentionPolicy.SOURCE)
115   public @interface SliceType {}
116 
117   /** */
118   @NonNull
119   @Slice.SliceHint
120   String[] mHints = Slice.NO_HINTS;
121 
122   @NonNull
123   String mFormat = FORMAT_TEXT;
124 
125   String mSubType = null;
126 
127   @Nullable Object mObj;
128 
129   CharSequence mSanitizedText;
130 
131   /** */
132   @SuppressWarnings("NullableProblems")
133   @SuppressLint("UnknownNullness") // obj cannot be correctly annotated
134   // @RestrictTo(Scope.LIBRARY_GROUP)
SliceItem( Object obj, @NonNull @SliceType String format, @Nullable String subType, @NonNull @Slice.SliceHint String[] hints)135   public SliceItem(
136       Object obj,
137       @NonNull @SliceType String format,
138       @Nullable String subType,
139       @NonNull @Slice.SliceHint String[] hints) {
140     mHints = hints;
141     mFormat = format;
142     mSubType = subType;
143     mObj = obj;
144   }
145 
146   /** */
147   @SuppressLint("UnknownNullness") // obj cannot be correctly annotated
148   // @RestrictTo(Scope.LIBRARY_GROUP)
SliceItem( Object obj, @NonNull @SliceType String format, @Nullable String subType, @NonNull @Slice.SliceHint List<String> hints)149   public SliceItem(
150       Object obj,
151       @NonNull @SliceType String format,
152       @Nullable String subType,
153       @NonNull @Slice.SliceHint List<String> hints) {
154     this(obj, format, subType, hints.toArray(new String[hints.size()]));
155   }
156 
157   /** */
SliceItem( @onNull Parcelable intent, @Nullable Slice slice, @NonNull @SliceType String format, @Nullable String subType, @NonNull @Slice.SliceHint String[] hints)158   public SliceItem(
159       @NonNull Parcelable intent,
160       @Nullable Slice slice,
161       @NonNull @SliceType String format,
162       @Nullable String subType,
163       @NonNull @Slice.SliceHint String[] hints) {
164     this(new Pair<Object, Slice>(intent, slice), format, subType, hints);
165   }
166 
167   /** */
168   @SuppressLint("LambdaLast")
169   // @RestrictTo(Scope.LIBRARY_GROUP)
SliceItem( @onNull ActionHandler action, @Nullable Slice slice, @NonNull @SliceType String format, @Nullable String subType, @NonNull @Slice.SliceHint String[] hints)170   public SliceItem(
171       @NonNull ActionHandler action,
172       @Nullable Slice slice,
173       @NonNull @SliceType String format,
174       @Nullable String subType,
175       @NonNull @Slice.SliceHint String[] hints) {
176     this(new Pair<Object, Slice>(action, slice), format, subType, hints);
177   }
178 
179   /**
180    * Gets all hints associated with this SliceItem.
181    *
182    * @return Array of hints.
183    */
getHints()184   public @NonNull @Slice.SliceHint List<String> getHints() {
185     return Arrays.asList(mHints);
186   }
187 
188   /** */
189   @SuppressWarnings("unused")
190   // @RestrictTo(Scope.LIBRARY)
getHintArray()191   public @NonNull @Slice.SliceHint String[] getHintArray() {
192     return mHints;
193   }
194 
195   /** */
196   // @RestrictTo(Scope.LIBRARY_GROUP)
addHint(@lice.SliceHint @onNull String hint)197   public void addHint(@Slice.SliceHint @NonNull String hint) {
198     mHints = ArrayUtils.appendElement(String.class, mHints, hint);
199   }
200 
201   /**
202    * Get the format of this SliceItem.
203    *
204    * <p>The format will be one of the following types supported by the platform:
205    *
206    * <ul>
207    *   <li>{@link android.app.slice.SliceItem#FORMAT_SLICE}
208    *   <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}
209    *   <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}
210    *   <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}
211    *   <li>{@link android.app.slice.SliceItem#FORMAT_INT}
212    *   <li>{@link android.app.slice.SliceItem#FORMAT_LONG}
213    *   <li>{@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}
214    * </ul>
215    *
216    * @see #getSubType() ()
217    */
218   @NonNull
getFormat()219   public @SliceType String getFormat() {
220     return mFormat;
221   }
222 
223   /**
224    * Get the sub-type of this SliceItem.
225    *
226    * <p>Subtypes provide additional information about the type of this information beyond basic
227    * interpretations inferred by {@link #getFormat()}. For example a slice may contain many {@link
228    * android.app.slice.SliceItem#FORMAT_TEXT} items, but only some of them may be {@link
229    * android.app.slice.Slice#SUBTYPE_MESSAGE}.
230    *
231    * @see #getFormat()
232    */
233   @Nullable
getSubType()234   public String getSubType() {
235     return mSubType;
236   }
237 
238   /**
239    * @return The text held by this {@link android.app.slice.SliceItem#FORMAT_TEXT} SliceItem
240    */
241   @Nullable
getText()242   public CharSequence getText() {
243     return (CharSequence) mObj;
244   }
245 
246   /**  */
247   @Nullable
getBundle()248   public Bundle getBundle() {
249     return (Bundle) mObj;
250   }
251 
252   /**
253    * @return The text held by this {@link android.app.slice.SliceItem#FORMAT_TEXT} SliceItem with
254    *     ony spans that are unsupported by the androidx Slice renderer removed.
255    */
256   // @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
257   @Nullable
getSanitizedText()258   public CharSequence getSanitizedText() {
259     if (mSanitizedText == null) {
260       mSanitizedText = sanitizeText(getText());
261     }
262     return mSanitizedText;
263   }
264 
265   /**
266    * Get the same content as {@link #getText()} except with content that should be excluded from
267    * persistent logs because it was tagged with {@link #createSensitiveSpan()}.
268    *
269    * @return The text held by this {@link android.app.slice.SliceItem#FORMAT_TEXT} SliceItem
270    */
271   @Nullable
getRedactedText()272   public CharSequence getRedactedText() {
273     return redactSensitiveText(getText());
274   }
275 
276   /**
277    * @return The icon held by this {@link android.app.slice.SliceItem#FORMAT_IMAGE} SliceItem
278    */
279   @Nullable
getIcon()280   public IconCompat getIcon() {
281     return (IconCompat) mObj;
282   }
283 
284   /**
285    * @return The pending intent held by this {@link android.app.slice.SliceItem#FORMAT_ACTION}
286    *     SliceItem
287    */
288   @Nullable
289   @SuppressWarnings("unchecked")
getAction()290   public PendingIntent getAction() {
291     ObjectsCompat.requireNonNull(mObj, "Object must be non-null");
292     Object action = ((Pair<Object, Slice>) mObj).first;
293     if (action instanceof PendingIntent) {
294       return (PendingIntent) action;
295     }
296     return null;
297   }
298 
299   /**
300    * @return The pending intent held by this {@link android.app.slice.SliceItem#FORMAT_ACTION}
301    *     SliceItem
302    */
303   @Nullable
304   @SuppressWarnings("unchecked")
getActionIntent()305   public Intent getActionIntent() {
306     ObjectsCompat.requireNonNull(mObj, "Object must be non-null");
307     Object action = ((Pair<Object, Slice>) mObj).first;
308     if (action instanceof Intent) {
309       return (Intent) action;
310     }
311     return null;
312   }
313 
314   @Nullable
getActionParcelable()315   public Parcelable getActionParcelable() {
316     ObjectsCompat.requireNonNull(mObj, "Object must be non-null");
317     Object action = ((Pair<Object, Slice>) mObj).first;
318     if (action instanceof Parcelable) {
319       return (Parcelable) action;
320     }
321     return null;
322   }
323 
324   @Nullable
325   @SuppressWarnings("unchecked")
getActionHandler()326   private ActionHandler getActionHandler() {
327     ObjectsCompat.requireNonNull(mObj, "Object must be non-null");
328     Object action = ((Pair<Object, Slice>) mObj).first;
329     if (action instanceof ActionHandler) {
330       return (ActionHandler) action;
331     }
332     return null;
333   }
334 
335   /**
336    * Trigger the action on this SliceItem.
337    *
338    * @param context The Context to use when sending the PendingIntent.
339    * @param i The intent to use when sending the PendingIntent.
340    */
fireAction(@ullable Context context, @Nullable Intent i)341   public void fireAction(@Nullable Context context, @Nullable Intent i)
342       throws PendingIntent.CanceledException {
343     fireActionInternal(context, i);
344   }
345 
346   /** */
347   @SuppressWarnings("unchecked")
348   // @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
fireActionInternal(@ullable Context context, @Nullable Intent i)349   public boolean fireActionInternal(@Nullable Context context, @Nullable Intent i)
350       throws PendingIntent.CanceledException {
351     Parcelable action = getActionParcelable();
352     if (action != null) {
353       SliceUtils.fireAction(context, action, i);
354     }
355 
356     ActionHandler handler = getActionHandler();
357     if (handler != null) {
358       handler.onAction(this, context, i);
359     }
360 
361     return true;
362   }
363 
364   /**
365    * @return The remote input held by this {@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}
366    *     SliceItem
367    */
368   @Nullable
369   @RequiresApi(20)
370   // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
getRemoteInput()371   public RemoteInput getRemoteInput() {
372     return (RemoteInput) mObj;
373   }
374 
375   /**
376    * @return The color held by this {@link android.app.slice.SliceItem#FORMAT_INT} SliceItem
377    */
getInt()378   public int getInt() {
379     ObjectsCompat.requireNonNull(mObj, "Object must be non-null for FORMAT_INT");
380     return (Integer) mObj;
381   }
382 
383   /**
384    * @return The slice held by this {@link android.app.slice.SliceItem#FORMAT_ACTION} or {@link
385    *     android.app.slice.SliceItem#FORMAT_SLICE} SliceItem
386    */
387   @Nullable
388   @SuppressWarnings("unchecked")
getSlice()389   public Slice getSlice() {
390     ObjectsCompat.requireNonNull(mObj, "Object must be non-null for FORMAT_SLICE");
391     if (FORMAT_ACTION.equals(getFormat())) {
392       return ((Pair<Object, Slice>) mObj).second;
393     }
394     return (Slice) mObj;
395   }
396 
397   /**
398    * @return The long held by this {@link android.app.slice.SliceItem#FORMAT_LONG} SliceItem
399    */
getLong()400   public long getLong() {
401     ObjectsCompat.requireNonNull(mObj, "Object must be non-null for FORMAT_LONG");
402     return (Long) mObj;
403   }
404 
405   /**
406    * @param hint The hint to check for
407    * @return true if this item contains the given hint
408    */
hasHint(@onNull @lice.SliceHint String hint)409   public boolean hasHint(@NonNull @Slice.SliceHint String hint) {
410     return ArrayUtils.contains(mHints, hint);
411   }
412 
413   /** */
414   // @RestrictTo(Scope.LIBRARY)
SliceItem(@onNull Bundle in)415   public SliceItem(@NonNull Bundle in) {
416     mHints = in.getStringArray(HINTS);
417     mFormat = in.getString(FORMAT);
418     mSubType = in.getString(SUBTYPE);
419     mObj = readObj(mFormat, in);
420   }
421 
422   /** */
423   @NonNull
424   // @RestrictTo(Scope.LIBRARY)
toBundle()425   public Bundle toBundle() {
426     Bundle b = new Bundle();
427     b.putStringArray(HINTS, mHints);
428     b.putString(FORMAT, mFormat);
429     b.putString(SUBTYPE, mSubType);
430     writeObj(b, mObj, mFormat);
431     return b;
432   }
433 
434   /** */
435   @SuppressWarnings("unused")
436   // @RestrictTo(Scope.LIBRARY)
hasHints(@ullable @lice.SliceHint String[] hints)437   public boolean hasHints(@Nullable @Slice.SliceHint String[] hints) {
438     if (hints == null) {
439       return true;
440     }
441     for (String hint : hints) {
442       if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) {
443         return false;
444       }
445     }
446     return true;
447   }
448 
449   /** */
450   // @RestrictTo(Scope.LIBRARY_GROUP)
hasAnyHints(@ullable @lice.SliceHint String... hints)451   public boolean hasAnyHints(@Nullable @Slice.SliceHint String... hints) {
452     if (hints == null) {
453       return false;
454     }
455     for (String hint : hints) {
456       if (ArrayUtils.contains(mHints, hint)) {
457         return true;
458       }
459     }
460     return false;
461   }
462 
463   @SuppressWarnings("unchecked")
writeObj(@onNull Bundle dest, Object obj, @NonNull String type)464   private void writeObj(@NonNull Bundle dest, Object obj, @NonNull String type) {
465     switch (type) {
466       case FORMAT_IMAGE:
467         dest.putBundle(OBJ, ((IconCompat) obj).toBundle());
468         break;
469       case FORMAT_REMOTE_INPUT:
470         dest.putParcelable(OBJ, (Parcelable) obj);
471         break;
472       case FORMAT_SLICE:
473         dest.putParcelable(OBJ, ((Slice) obj).toBundle());
474         break;
475       case FORMAT_ACTION:
476         {
477           Parcelable action = (Parcelable) ((Pair<Object, Slice>) obj).first;
478           if (action instanceof Intent) {
479             dest.putString(INTENT, ((Intent) action).toUri(Intent.URI_INTENT_SCHEME));
480           } else {
481             dest.putParcelable(OBJ, (Parcelable) ((Pair<Object, Slice>) obj).first);
482           }
483         dest.putBundle(OBJ_2, ((Pair<Object, Slice>) obj).second.toBundle());
484         break;
485         }
486       case FORMAT_TEXT:
487         dest.putCharSequence(OBJ, (CharSequence) obj);
488         break;
489       case FORMAT_INT:
490         ObjectsCompat.requireNonNull(mObj, "Object must be non-null for FORMAT_INT");
491         dest.putInt(OBJ, (Integer) mObj);
492         break;
493       case FORMAT_LONG:
494         ObjectsCompat.requireNonNull(mObj, "Object must be non-null for FORMAT_LONG");
495         dest.putLong(OBJ, (Long) mObj);
496         break;
497       case FORMAT_BUNDLE:
498         dest.putBundle(OBJ, (Bundle) mObj);
499     }
500   }
501 
502   @Nullable
readObj(@onNull String type, @NonNull Bundle in)503   private static Object readObj(@NonNull String type, @NonNull Bundle in) {
504     switch (type) {
505       case FORMAT_IMAGE:
506         return IconCompat.createFromBundle(in.getBundle(OBJ));
507       case FORMAT_REMOTE_INPUT:
508         return in.getParcelable(OBJ);
509       case FORMAT_SLICE:
510         return new Slice(in.getBundle(OBJ));
511       case FORMAT_TEXT:
512         return in.getCharSequence(OBJ);
513       case FORMAT_ACTION:
514         String intentUri = in.getString(INTENT);
515         if (intentUri == null) {
516           return new Pair<>(in.getParcelable(OBJ), new Slice(in.getBundle(OBJ_2)));
517         }
518         Intent intent;
519         try {
520           intent = Intent.parseUri(intentUri, 0);
521         } catch (URISyntaxException e) {
522           Log.e(TAG, "Malformed intent: " + intentUri, e);
523           intent = new Intent();
524         }
525         return new Pair<>(intent, new Slice(in.getBundle(OBJ_2)));
526       case FORMAT_INT:
527         return in.getInt(OBJ);
528       case FORMAT_LONG:
529         return in.getLong(OBJ);
530       case FORMAT_BUNDLE:
531         return in.getBundle(OBJ);
532     }
533     throw new RuntimeException("Unsupported type " + type);
534   }
535 
536   /** */
537   @NonNull
538   // @RestrictTo(Scope.LIBRARY)
typeToString(@onNull String format)539   public static String typeToString(@NonNull String format) {
540     switch (format) {
541       case FORMAT_SLICE:
542         return "Slice";
543       case FORMAT_TEXT:
544         return "Text";
545       case FORMAT_IMAGE:
546         return "Image";
547       case FORMAT_ACTION:
548         return "Action";
549       case FORMAT_INT:
550         return "Int";
551       case FORMAT_LONG:
552         return "Long";
553       case FORMAT_REMOTE_INPUT:
554         return "RemoteInput";
555     }
556     return "Unrecognized format: " + format;
557   }
558 
559   /**
560    * @return A string representation of this slice item.
561    */
562   @NonNull
563   @Override
toString()564   public String toString() {
565     return toString("");
566   }
567 
568   /**
569    * @return A string representation of this slice item.
570    */
571   @NonNull
572   // @RestrictTo(Scope.LIBRARY)
573   @SuppressWarnings("unchecked")
toString(@onNull String indent)574   public String toString(@NonNull String indent) {
575     StringBuilder sb = new StringBuilder();
576     sb.append(indent);
577     sb.append(getFormat());
578     if (getSubType() != null) {
579       sb.append('<');
580       sb.append(getSubType());
581       sb.append('>');
582     }
583     sb.append(' ');
584     if (mHints.length > 0) {
585       appendHints(sb, mHints);
586       sb.append(' ');
587     }
588     final String nextIndent = indent + "  ";
589     switch (getFormat()) {
590       case FORMAT_SLICE:
591         {
592           Slice slice = getSlice();
593           ObjectsCompat.requireNonNull(slice, "Slice must be non-null for FORMAT_SLICE");
594           sb.append("{\n");
595           sb.append(slice.toString(nextIndent));
596           sb.append('\n').append(indent).append('}');
597           break;
598         }
599       case FORMAT_ACTION:
600         {
601           Slice slice = getSlice();
602           ObjectsCompat.requireNonNull(mObj, "Object must be non-null for FORMAT_ACTION");
603           ObjectsCompat.requireNonNull(slice, "Slice must be non-null for FORMAT_SLICE");
604           // Not using getAction because the action can actually be other types.
605           Object action = ((Pair<Object, Slice>) mObj).first;
606           sb.append('[').append(action).append("] ");
607           sb.append("{\n");
608           sb.append(getSlice().toString(nextIndent));
609           sb.append('\n').append(indent).append('}');
610           break;
611         }
612       case FORMAT_TEXT:
613         sb.append('"').append(getText()).append('"');
614         break;
615       case FORMAT_IMAGE:
616         sb.append(getIcon());
617         break;
618       case FORMAT_INT:
619         if (android.app.slice.Slice.SUBTYPE_COLOR.equals(getSubType())) {
620           int color = getInt();
621           sb.append(
622               String.format(
623                   "a=0x%02x r=0x%02x g=0x%02x b=0x%02x",
624                   Color.alpha(color), Color.red(color), Color.green(color), Color.blue(color)));
625         } else if (android.app.slice.Slice.SUBTYPE_LAYOUT_DIRECTION.equals(getSubType())) {
626           sb.append(layoutDirectionToString(getInt()));
627         } else {
628           sb.append(getInt());
629         }
630         break;
631       case FORMAT_LONG:
632         if (android.app.slice.Slice.SUBTYPE_MILLIS.equals(getSubType())) {
633           if (getLong() == -1L) {
634             sb.append("INFINITY");
635           } else {
636             sb.append(
637                 DateUtils.getRelativeTimeSpanString(
638                     getLong(),
639                     Calendar.getInstance().getTimeInMillis(),
640                     DateUtils.SECOND_IN_MILLIS,
641                     DateUtils.FORMAT_ABBREV_RELATIVE));
642           }
643         } else {
644           sb.append(getLong()).append('L');
645         }
646         break;
647       default:
648         sb.append(SliceItem.typeToString(getFormat()));
649         break;
650     }
651     sb.append("\n");
652     return sb.toString();
653   }
654 
655   /**
656    * Creates a span object that identifies content that should be redacted when acquired using
657    * {@link #getRedactedText()}.
658    */
659   @NonNull
createSensitiveSpan()660   public static ParcelableSpan createSensitiveSpan() {
661     return new Annotation(SLICE_CONTENT, SLICE_CONTENT_SENSITIVE);
662   }
663 
664   @NonNull
layoutDirectionToString(int layoutDirection)665   private static String layoutDirectionToString(int layoutDirection) {
666     switch (layoutDirection) {
667       case android.util.LayoutDirection.LTR:
668         return "LTR";
669       case android.util.LayoutDirection.RTL:
670         return "RTL";
671       case android.util.LayoutDirection.INHERIT:
672         return "INHERIT";
673       case android.util.LayoutDirection.LOCALE:
674         return "LOCALE";
675       default:
676         return Integer.toString(layoutDirection);
677     }
678   }
679 
redactSensitiveText(CharSequence text)680   private static CharSequence redactSensitiveText(CharSequence text) {
681     if (text instanceof Spannable) {
682       return redactSpannableText((Spannable) text);
683     } else if (text instanceof Spanned) {
684       if (!isRedactionNeeded((Spanned) text)) {
685         return text;
686       }
687       Spannable fixedText = new SpannableString(text);
688       return redactSpannableText(fixedText);
689     } else {
690       return text;
691     }
692   }
693 
isRedactionNeeded(@onNull Spanned text)694   private static boolean isRedactionNeeded(@NonNull Spanned text) {
695     for (Annotation span : text.getSpans(0, text.length(), Annotation.class)) {
696       if (SLICE_CONTENT.equals(span.getKey()) && SLICE_CONTENT_SENSITIVE.equals(span.getValue())) {
697         return true;
698       }
699     }
700     return false;
701   }
702 
703   @NonNull
redactSpannableText(@onNull Spannable text)704   private static CharSequence redactSpannableText(@NonNull Spannable text) {
705     Spanned out = text;
706     for (Annotation span : text.getSpans(0, text.length(), Annotation.class)) {
707       if (!SLICE_CONTENT.equals(span.getKey())
708           || !SLICE_CONTENT_SENSITIVE.equals(span.getValue())) {
709         continue;
710       }
711       int spanStart = text.getSpanStart(span);
712       int spanEnd = text.getSpanEnd(span);
713       out =
714           new SpannableStringBuilder()
715               .append(out.subSequence(0, spanStart))
716               .append(createRedacted(spanEnd - spanStart))
717               .append(out.subSequence(spanEnd, text.length()));
718     }
719     return out;
720   }
721 
722   @NonNull
createRedacted(final int n)723   private static String createRedacted(final int n) {
724     StringBuilder s = new StringBuilder();
725     for (int i = 0; i < n; i++) {
726       s.append('*');
727     }
728     return s.toString();
729   }
730 
731   @Nullable
sanitizeText(@ullable CharSequence text)732   private static CharSequence sanitizeText(@Nullable CharSequence text) {
733     if (text instanceof Spannable) {
734       fixSpannableText((Spannable) text);
735       return text;
736     } else if (text instanceof Spanned) {
737       if (checkSpannedText((Spanned) text)) {
738         return text;
739       }
740       Spannable fixedText = new SpannableString(text);
741       fixSpannableText(fixedText);
742       return fixedText;
743     } else {
744       return text;
745     }
746   }
747 
checkSpannedText(@onNull Spanned text)748   private static boolean checkSpannedText(@NonNull Spanned text) {
749     for (Object span : text.getSpans(0, text.length(), Object.class)) {
750       if (!checkSpan(span)) {
751         return false;
752       }
753     }
754     return true;
755   }
756 
fixSpannableText(@onNull Spannable text)757   private static void fixSpannableText(@NonNull Spannable text) {
758     for (Object span : text.getSpans(0, text.length(), Object.class)) {
759       Object fixedSpan = fixSpan(span);
760       if (fixedSpan == span) {
761         continue;
762       }
763 
764       if (fixedSpan != null) {
765         int spanStart = text.getSpanStart(span);
766         int spanEnd = text.getSpanEnd(span);
767         int spanFlags = text.getSpanFlags(span);
768         text.setSpan(fixedSpan, spanStart, spanEnd, spanFlags);
769       }
770 
771       text.removeSpan(span);
772     }
773   }
774 
775   // TODO: Allow only highlight color in ForegroundColorSpan.
776   // TODO: Cap smallest/largest sizeChange for RelativeSizeSpans, minding nested ones.
777 
checkSpan(Object span)778   private static boolean checkSpan(Object span) {
779     return span instanceof AlignmentSpan
780         || span instanceof ForegroundColorSpan
781         || span instanceof RelativeSizeSpan
782         || span instanceof StyleSpan;
783   }
784 
785   @Nullable
fixSpan(Object span)786   private static Object fixSpan(Object span) {
787     return checkSpan(span) ? span : null;
788   }
789 
790   /** */
791   // @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
792   public interface ActionHandler {
793     /** Called when a pending intent would be sent on a real slice. */
onAction(@onNull SliceItem item, @Nullable Context context, @Nullable Intent intent)794     void onAction(@NonNull SliceItem item, @Nullable Context context, @Nullable Intent intent);
795   }
796 }
797