[go: nahoru, domu]

blob: 67e9292e2e2ba729e915b6e15a707b6cf35ebf21 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.slice;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static androidx.slice.Slice.appendHints;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.Annotation;
import android.text.ParcelableSpan;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.AlignmentSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.StringDef;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Pair;
import androidx.versionedparcelable.CustomVersionedParcelable;
import androidx.versionedparcelable.NonParcelField;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
/**
* A SliceItem is a single unit in the tree structure of a {@link Slice}.
* <p>
* A SliceItem a piece of content and some hints about what that content
* means or how it should be displayed. The types of content can be:
* <li>{@link android.app.slice.SliceItem#FORMAT_SLICE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
* <p>
* The hints that a {@link SliceItem} are a set of strings which annotate
* the content. The hints that are guaranteed to be understood by the system
* are defined on {@link Slice}.
*/
@VersionedParcelize(allowSerialization = true, ignoreParcelables = true, isCustom = true)
@RequiresApi(19)
public final class SliceItem extends CustomVersionedParcelable {
private static final String HINTS = "hints";
private static final String FORMAT = "format";
private static final String SUBTYPE = "subtype";
private static final String OBJ = "obj";
private static final String OBJ_2 = "obj_2";
private static final String SLICE_CONTENT = "androidx.slice.content";
private static final String SLICE_CONTENT_SENSITIVE = "sensitive";
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY)
@StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT,
FORMAT_LONG, FORMAT_REMOTE_INPUT, FORMAT_LONG})
@Retention(RetentionPolicy.SOURCE)
public @interface SliceType {
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY)
@ParcelField(value = 1, defaultValue = "androidx.slice.Slice.NO_HINTS")
protected @Slice.SliceHint String[] mHints = Slice.NO_HINTS;
@ParcelField(value = 2, defaultValue = FORMAT_TEXT)
String mFormat = FORMAT_TEXT;
@ParcelField(value = 3, defaultValue = "null")
String mSubType = null;
@NonParcelField
Object mObj;
@NonParcelField
CharSequence mSanitizedText;
@ParcelField(4)
SliceItemHolder mHolder;
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public SliceItem(Object obj, @SliceType String format, String subType,
@Slice.SliceHint String[] hints) {
mHints = hints;
mFormat = format;
mSubType = subType;
mObj = obj;
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public SliceItem(Object obj, @SliceType String format, String subType,
@Slice.SliceHint List<String> hints) {
this (obj, format, subType, hints.toArray(new String[hints.size()]));
}
/**
* Used by VersionedParcelable.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public SliceItem() {
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public SliceItem(PendingIntent intent, Slice slice, String format, String subType,
@Slice.SliceHint String[] hints) {
this(new Pair<Object, Slice>(intent, slice), format, subType, hints);
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public SliceItem(ActionHandler action, Slice slice, String format, String subType,
@Slice.SliceHint String[] hints) {
this(new Pair<Object, Slice>(action, slice), format, subType, hints);
}
/**
* Gets all hints associated with this SliceItem.
*
* @return Array of hints.
*/
public @NonNull @Slice.SliceHint List<String> getHints() {
return Arrays.asList(mHints);
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public @NonNull @Slice.SliceHint String[] getHintArray() {
return mHints;
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public void addHint(@Slice.SliceHint String hint) {
mHints = ArrayUtils.appendElement(String.class, mHints, hint);
}
/**
* Get the format of this SliceItem.
* <p>
* The format will be one of the following types supported by the platform:
* <li>{@link android.app.slice.SliceItem#FORMAT_SLICE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}</li>
* @see #getSubType() ()
*/
public @SliceType String getFormat() {
return mFormat;
}
/**
* Get the sub-type of this SliceItem.
* <p>
* Subtypes provide additional information about the type of this information beyond basic
* interpretations inferred by {@link #getFormat()}. For example a slice may contain
* many {@link android.app.slice.SliceItem#FORMAT_TEXT} items, but only some of them may be
* {@link android.app.slice.Slice#SUBTYPE_MESSAGE}.
* @see #getFormat()
*/
public String getSubType() {
return mSubType;
}
/**
* @return The text held by this {@link android.app.slice.SliceItem#FORMAT_TEXT} SliceItem
*/
public CharSequence getText() {
return (CharSequence) mObj;
}
/**
* @hide
* @return The text held by this {@link android.app.slice.SliceItem#FORMAT_TEXT} SliceItem with
* ony spans that are unsupported by the androidx Slice renderer removed.
*/
@RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
public CharSequence getSanitizedText() {
if (mSanitizedText == null) mSanitizedText = sanitizeText(getText());
return mSanitizedText;
}
/**
* Get the same content as {@link #getText()} except with content that should be excluded from
* persistent logs because it was tagged with {@link #createSensitiveSpan()}.
*
* @return The text held by this {@link android.app.slice.SliceItem#FORMAT_TEXT} SliceItem
*/
@Nullable
public CharSequence getRedactedText() {
return redactSensitiveText(getText());
}
/**
* @return The icon held by this {@link android.app.slice.SliceItem#FORMAT_IMAGE} SliceItem
*/
public IconCompat getIcon() {
return (IconCompat) mObj;
}
/**
* @return The pending intent held by this {@link android.app.slice.SliceItem#FORMAT_ACTION}
* SliceItem
*/
@SuppressWarnings("unchecked")
public PendingIntent getAction() {
Object action = ((Pair<Object, Slice>) mObj).first;
if (action instanceof PendingIntent) {
return (PendingIntent) action;
}
return null;
}
/**
* Trigger the action on this SliceItem.
* @param context The Context to use when sending the PendingIntent.
* @param i The intent to use when sending the PendingIntent.
*/
public void fireAction(@Nullable Context context, @Nullable Intent i)
throws PendingIntent.CanceledException {
fireActionInternal(context, i);
}
/**
* @hide
*/
@SuppressWarnings("unchecked")
@RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
public boolean fireActionInternal(@Nullable Context context, @Nullable Intent i)
throws PendingIntent.CanceledException {
Object action = ((Pair<Object, Slice>) mObj).first;
if (action instanceof PendingIntent) {
((PendingIntent) action).send(context, 0, i, null, null);
return false;
} else {
((ActionHandler) action).onAction(this, context, i);
return true;
}
}
/**
* @return The remote input held by this {@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}
* SliceItem
* @hide
*/
@RequiresApi(20)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public RemoteInput getRemoteInput() {
return (RemoteInput) mObj;
}
/**
* @return The color held by this {@link android.app.slice.SliceItem#FORMAT_INT} SliceItem
*/
public int getInt() {
return (Integer) mObj;
}
/**
* @return The slice held by this {@link android.app.slice.SliceItem#FORMAT_ACTION} or
* {@link android.app.slice.SliceItem#FORMAT_SLICE} SliceItem
*/
@SuppressWarnings("unchecked")
public Slice getSlice() {
if (FORMAT_ACTION.equals(getFormat())) {
return ((Pair<Object, Slice>) mObj).second;
}
return (Slice) mObj;
}
/**
* @return The long held by this {@link android.app.slice.SliceItem#FORMAT_LONG}
* SliceItem
*/
public long getLong() {
return (Long) mObj;
}
/**
* @param hint The hint to check for
* @return true if this item contains the given hint
*/
public boolean hasHint(@Slice.SliceHint String hint) {
return ArrayUtils.contains(mHints, hint);
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public SliceItem(Bundle in) {
mHints = in.getStringArray(HINTS);
mFormat = in.getString(FORMAT);
mSubType = in.getString(SUBTYPE);
mObj = readObj(mFormat, in);
}
/**
* @hide
* @return
*/
@RestrictTo(Scope.LIBRARY)
public Bundle toBundle() {
Bundle b = new Bundle();
b.putStringArray(HINTS, mHints);
b.putString(FORMAT, mFormat);
b.putString(SUBTYPE, mSubType);
writeObj(b, mObj, mFormat);
return b;
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public boolean hasHints(@Slice.SliceHint String[] hints) {
if (hints == null) return true;
for (String hint : hints) {
if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) {
return false;
}
}
return true;
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public boolean hasAnyHints(@Slice.SliceHint String... hints) {
if (hints == null) return false;
for (String hint : hints) {
if (ArrayUtils.contains(mHints, hint)) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
private void writeObj(Bundle dest, Object obj, String type) {
switch (type) {
case FORMAT_IMAGE:
dest.putBundle(OBJ, ((IconCompat) obj).toBundle());
break;
case FORMAT_REMOTE_INPUT:
dest.putParcelable(OBJ, (Parcelable) obj);
break;
case FORMAT_SLICE:
dest.putParcelable(OBJ, ((Slice) obj).toBundle());
break;
case FORMAT_ACTION:
dest.putParcelable(OBJ, (PendingIntent) ((Pair<Object, Slice>) obj).first);
dest.putBundle(OBJ_2, ((Pair<Object, Slice>) obj).second.toBundle());
break;
case FORMAT_TEXT:
dest.putCharSequence(OBJ, (CharSequence) obj);
break;
case FORMAT_INT:
dest.putInt(OBJ, (Integer) mObj);
break;
case FORMAT_LONG:
dest.putLong(OBJ, (Long) mObj);
break;
}
}
private static Object readObj(String type, Bundle in) {
switch (type) {
case FORMAT_IMAGE:
return IconCompat.createFromBundle(in.getBundle(OBJ));
case FORMAT_REMOTE_INPUT:
return in.getParcelable(OBJ);
case FORMAT_SLICE:
return new Slice(in.getBundle(OBJ));
case FORMAT_TEXT:
return in.getCharSequence(OBJ);
case FORMAT_ACTION:
return new Pair<>(
in.getParcelable(OBJ),
new Slice(in.getBundle(OBJ_2)));
case FORMAT_INT:
return in.getInt(OBJ);
case FORMAT_LONG:
return in.getLong(OBJ);
}
throw new RuntimeException("Unsupported type " + type);
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public static String typeToString(String format) {
switch (format) {
case FORMAT_SLICE:
return "Slice";
case FORMAT_TEXT:
return "Text";
case FORMAT_IMAGE:
return "Image";
case FORMAT_ACTION:
return "Action";
case FORMAT_INT:
return "Int";
case FORMAT_LONG:
return "Long";
case FORMAT_REMOTE_INPUT:
return "RemoteInput";
}
return "Unrecognized format: " + format;
}
/**
* @return A string representation of this slice item.
*/
@Override
public String toString() {
return toString("");
}
/**
* @return A string representation of this slice item.
* @hide
*/
@RestrictTo(Scope.LIBRARY)
@SuppressWarnings("unchecked")
public String toString(String indent) {
StringBuilder sb = new StringBuilder();
sb.append(indent);
sb.append(getFormat());
if (getSubType() != null) {
sb.append('<');
sb.append(getSubType());
sb.append('>');
}
sb.append(' ');
if (mHints.length > 0) {
appendHints(sb, mHints);
sb.append(' ');
}
final String nextIndent = indent + " ";
switch (getFormat()) {
case FORMAT_SLICE:
sb.append("{\n");
sb.append(getSlice().toString(nextIndent));
sb.append('\n').append(indent).append('}');
break;
case FORMAT_ACTION:
// Not using getAction because the action can actually be other types.
Object action = ((Pair<Object, Slice>) mObj).first;
sb.append('[').append(action).append("] ");
sb.append("{\n");
sb.append(getSlice().toString(nextIndent));
sb.append('\n').append(indent).append('}');
break;
case FORMAT_TEXT:
sb.append('"').append(getText()).append('"');
break;
case FORMAT_IMAGE:
sb.append(getIcon());
break;
case FORMAT_INT:
if (android.app.slice.Slice.SUBTYPE_COLOR.equals(getSubType())) {
int color = getInt();
sb.append(String.format("a=0x%02x r=0x%02x g=0x%02x b=0x%02x",
Color.alpha(color), Color.red(color), Color.green(color),
Color.blue(color)));
} else if (android.app.slice.Slice.SUBTYPE_LAYOUT_DIRECTION.equals(getSubType())) {
sb.append(layoutDirectionToString(getInt()));
} else {
sb.append(getInt());
}
break;
case FORMAT_LONG:
if (android.app.slice.Slice.SUBTYPE_MILLIS.equals(getSubType())) {
if (getLong() == -1L) {
sb.append("INFINITY");
} else {
sb.append(DateUtils.getRelativeTimeSpanString(getLong(),
Calendar.getInstance().getTimeInMillis(),
DateUtils.SECOND_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE));
}
} else {
sb.append(getLong()).append('L');
}
break;
default:
sb.append(SliceItem.typeToString(getFormat()));
break;
}
sb.append("\n");
return sb.toString();
}
@Override
public void onPreParceling(boolean isStream) {
mHolder = new SliceItemHolder(mFormat, mObj, isStream);
}
@Override
public void onPostParceling() {
if (mHolder != null) {
mObj = mHolder.getObj(mFormat);
mHolder.release();
} else {
mObj = null;
}
mHolder = null;
}
/**
* Creates a span object that identifies content that should be redacted when acquired using
* {@link #getRedactedText()}.
*/
@NonNull
public static ParcelableSpan createSensitiveSpan() {
return new Annotation(SLICE_CONTENT, SLICE_CONTENT_SENSITIVE);
}
private static String layoutDirectionToString(int layoutDirection) {
switch (layoutDirection) {
case android.util.LayoutDirection.LTR:
return "LTR";
case android.util.LayoutDirection.RTL:
return "RTL";
case android.util.LayoutDirection.INHERIT:
return "INHERIT";
case android.util.LayoutDirection.LOCALE:
return "LOCALE";
default:
return Integer.toString(layoutDirection);
}
}
private static CharSequence redactSensitiveText(CharSequence text) {
if (text instanceof Spannable) {
return redactSpannableText((Spannable) text);
} else if (text instanceof Spanned) {
if (!isRedactionNeeded((Spanned) text)) return text;
Spannable fixedText = new SpannableString(text);
return redactSpannableText(fixedText);
} else {
return text;
}
}
private static boolean isRedactionNeeded(Spanned text) {
for (Annotation span : text.getSpans(0, text.length(), Annotation.class)) {
if (SLICE_CONTENT.equals(span.getKey())
&& SLICE_CONTENT_SENSITIVE.equals(span.getValue())) {
return true;
}
}
return false;
}
private static CharSequence redactSpannableText(Spannable text) {
Spanned out = text;
for (Annotation span : text.getSpans(0, text.length(), Annotation.class)) {
if (!SLICE_CONTENT.equals(span.getKey())
|| !SLICE_CONTENT_SENSITIVE.equals(span.getValue())) {
continue;
}
int spanStart = text.getSpanStart(span);
int spanEnd = text.getSpanEnd(span);
out = new SpannableStringBuilder()
.append(out.subSequence(0, spanStart))
.append(createRedacted(spanEnd - spanStart))
.append(out.subSequence(spanEnd, text.length()));
}
return out;
}
private static String createRedacted(final int n) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < n; i++) {
s.append('*');
}
return s.toString();
}
private static CharSequence sanitizeText(CharSequence text) {
if (text instanceof Spannable) {
fixSpannableText((Spannable) text);
return text;
} else if (text instanceof Spanned) {
if (checkSpannedText((Spanned) text)) return text;
Spannable fixedText = new SpannableString(text);
fixSpannableText(fixedText);
return fixedText;
} else {
return text;
}
}
private static boolean checkSpannedText(Spanned text) {
for (Object span : text.getSpans(0, text.length(), Object.class)) {
if (!checkSpan(span)) return false;
}
return true;
}
private static void fixSpannableText(Spannable text) {
for (Object span : text.getSpans(0, text.length(), Object.class)) {
Object fixedSpan = fixSpan(span);
if (fixedSpan == span) continue;
if (fixedSpan != null) {
int spanStart = text.getSpanStart(span);
int spanEnd = text.getSpanEnd(span);
int spanFlags = text.getSpanFlags(span);
text.setSpan(fixedSpan, spanStart, spanEnd, spanFlags);
}
text.removeSpan(span);
}
}
// TODO: Allow only highlight color in ForegroundColorSpan.
// TODO: Cap smallest/largest sizeChange for RelativeSizeSpans, minding nested ones.
private static boolean checkSpan(Object span) {
return span instanceof AlignmentSpan
|| span instanceof ForegroundColorSpan
|| span instanceof RelativeSizeSpan
|| span instanceof StyleSpan;
}
private static Object fixSpan(Object span) {
return checkSpan(span) ? span : null;
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
public interface ActionHandler {
/**
* Called when a pending intent would be sent on a real slice.
*/
void onAction(SliceItem item, Context context, Intent intent);
}
}