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