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 androidx.slice;
18 
19 import static android.app.slice.SliceItem.FORMAT_ACTION;
20 import static android.app.slice.SliceItem.FORMAT_IMAGE;
21 import static android.app.slice.SliceItem.FORMAT_INT;
22 import static android.app.slice.SliceItem.FORMAT_LONG;
23 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
24 import static android.app.slice.SliceItem.FORMAT_SLICE;
25 import static android.app.slice.SliceItem.FORMAT_TEXT;
26 
27 import static androidx.slice.Slice.addHints;
28 
29 import android.app.PendingIntent;
30 import android.app.RemoteInput;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.net.Uri;
34 import android.os.Bundle;
35 import android.os.Parcelable;
36 import android.text.TextUtils;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.RequiresApi;
40 import androidx.annotation.RestrictTo;
41 import androidx.annotation.RestrictTo.Scope;
42 import androidx.annotation.StringDef;
43 import androidx.core.graphics.drawable.IconCompat;
44 import androidx.core.util.Consumer;
45 import androidx.core.util.Pair;
46 
47 import java.util.Arrays;
48 import java.util.List;
49 
50 
51 /**
52  * A SliceItem is a single unit in the tree structure of a {@link Slice}.
53  * <p>
54  * A SliceItem a piece of content and some hints about what that content
55  * means or how it should be displayed. The types of content can be:
56  * <li>{@link android.app.slice.SliceItem#FORMAT_SLICE}</li>
57  * <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}</li>
58  * <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
59  * <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
60  * <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
61  * <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
62  * <p>
63  * The hints that a {@link SliceItem} are a set of strings which annotate
64  * the content. The hints that are guaranteed to be understood by the system
65  * are defined on {@link Slice}.
66  */
67 public class SliceItem {
68 
69     private static final String HINTS = "hints";
70     private static final String FORMAT = "format";
71     private static final String SUBTYPE = "subtype";
72     private static final String OBJ = "obj";
73     private static final String OBJ_2 = "obj_2";
74 
75     /**
76      * @hide
77      */
78     @RestrictTo(Scope.LIBRARY)
79     @StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT,
80             FORMAT_LONG, FORMAT_REMOTE_INPUT, FORMAT_LONG})
81     public @interface SliceType {
82     }
83 
84     /**
85      * @hide
86      */
87     @RestrictTo(Scope.LIBRARY)
88     protected @Slice.SliceHint String[] mHints;
89     private final String mFormat;
90     private final String mSubType;
91     private final Object mObj;
92 
93     /**
94      * @hide
95      */
96     @RestrictTo(Scope.LIBRARY)
SliceItem(Object obj, @SliceType String format, String subType, @Slice.SliceHint String[] hints)97     public SliceItem(Object obj, @SliceType String format, String subType,
98             @Slice.SliceHint String[] hints) {
99         mHints = hints;
100         mFormat = format;
101         mSubType = subType;
102         mObj = obj;
103     }
104 
105     /**
106      * @hide
107      */
108     @RestrictTo(Scope.LIBRARY)
SliceItem(Object obj, @SliceType String format, String subType, @Slice.SliceHint List<String> hints)109     public SliceItem(Object obj, @SliceType String format, String subType,
110             @Slice.SliceHint List<String> hints) {
111         this (obj, format, subType, hints.toArray(new String[hints.size()]));
112     }
113 
114     /**
115      * @hide
116      */
117     @RestrictTo(Scope.LIBRARY)
SliceItem(PendingIntent intent, Slice slice, String format, String subType, @Slice.SliceHint String[] hints)118     public SliceItem(PendingIntent intent, Slice slice, String format, String subType,
119             @Slice.SliceHint String[] hints) {
120         this(new Pair<Object, Slice>(intent, slice), format, subType, hints);
121     }
122 
123     /**
124      * @hide
125      */
126     @RestrictTo(Scope.LIBRARY)
SliceItem(Consumer<Uri> action, Slice slice, String format, String subType, @Slice.SliceHint String[] hints)127     public SliceItem(Consumer<Uri> action, Slice slice, String format, String subType,
128             @Slice.SliceHint String[] hints) {
129         this(new Pair<Object, Slice>(action, slice), format, subType, hints);
130     }
131 
132     /**
133      * Gets all hints associated with this SliceItem.
134      *
135      * @return Array of hints.
136      */
getHints()137     public @NonNull @Slice.SliceHint List<String> getHints() {
138         return Arrays.asList(mHints);
139     }
140 
141     /**
142      * @hide
143      */
144     @RestrictTo(Scope.LIBRARY)
addHint(@lice.SliceHint String hint)145     public void addHint(@Slice.SliceHint String hint) {
146         mHints = ArrayUtils.appendElement(String.class, mHints, hint);
147     }
148 
149     /**
150      * @hide
151      */
152     @RestrictTo(Scope.LIBRARY)
removeHint(String hint)153     public void removeHint(String hint) {
154         ArrayUtils.removeElement(String.class, mHints, hint);
155     }
156 
157     /**
158      * Get the format of this SliceItem.
159      * <p>
160      * The format will be one of the following types supported by the platform:
161      * <li>{@link android.app.slice.SliceItem#FORMAT_SLICE}</li>
162      * <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}</li>
163      * <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
164      * <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
165      * <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
166      * <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
167      * <li>{@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}</li>
168      * @see #getSubType() ()
169      */
getFormat()170     public @SliceType String getFormat() {
171         return mFormat;
172     }
173 
174     /**
175      * Get the sub-type of this SliceItem.
176      * <p>
177      * Subtypes provide additional information about the type of this information beyond basic
178      * interpretations inferred by {@link #getFormat()}. For example a slice may contain
179      * many {@link android.app.slice.SliceItem#FORMAT_TEXT} items, but only some of them may be
180      * {@link android.app.slice.Slice#SUBTYPE_MESSAGE}.
181      * @see #getFormat()
182      */
getSubType()183     public String getSubType() {
184         return mSubType;
185     }
186 
187     /**
188      * @return The text held by this {@link android.app.slice.SliceItem#FORMAT_TEXT} SliceItem
189      */
getText()190     public CharSequence getText() {
191         return (CharSequence) mObj;
192     }
193 
194     /**
195      * @return The icon held by this {@link android.app.slice.SliceItem#FORMAT_IMAGE} SliceItem
196      */
getIcon()197     public IconCompat getIcon() {
198         return (IconCompat) mObj;
199     }
200 
201     /**
202      * @return The pending intent held by this {@link android.app.slice.SliceItem#FORMAT_ACTION}
203      * SliceItem
204      */
getAction()205     public PendingIntent getAction() {
206         return (PendingIntent) ((Pair<Object, Slice>) mObj).first;
207     }
208 
209     /**
210      * @hide
211      */
212     @RestrictTo(Scope.LIBRARY_GROUP)
fireAction(Context context, Intent i)213     public void fireAction(Context context, Intent i) throws PendingIntent.CanceledException {
214         Object action = ((Pair<Object, Slice>) mObj).first;
215         if (action instanceof PendingIntent) {
216             ((PendingIntent) action).send(context, 0, i, null, null);
217         } else {
218             ((Consumer<Uri>) action).accept(getSlice().getUri());
219         }
220     }
221 
222     /**
223      * @return The remote input held by this {@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}
224      * SliceItem
225      * @hide
226      */
227     @RequiresApi(20)
228     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
getRemoteInput()229     public RemoteInput getRemoteInput() {
230         return (RemoteInput) mObj;
231     }
232 
233     /**
234      * @return The color held by this {@link android.app.slice.SliceItem#FORMAT_INT} SliceItem
235      */
getInt()236     public int getInt() {
237         return (Integer) mObj;
238     }
239 
240     /**
241      * @return The slice held by this {@link android.app.slice.SliceItem#FORMAT_ACTION} or
242      * {@link android.app.slice.SliceItem#FORMAT_SLICE} SliceItem
243      */
getSlice()244     public Slice getSlice() {
245         if (FORMAT_ACTION.equals(getFormat())) {
246             return ((Pair<Object, Slice>) mObj).second;
247         }
248         return (Slice) mObj;
249     }
250 
251     /**
252      * @return The long held by this {@link android.app.slice.SliceItem#FORMAT_LONG}
253      * SliceItem
254      */
getLong()255     public long getLong() {
256         return (Long) mObj;
257     }
258 
259     /**
260      * @deprecated TO BE REMOVED
261      */
262     @Deprecated
getTimestamp()263     public long getTimestamp() {
264         return (Long) mObj;
265     }
266 
267     /**
268      * @param hint The hint to check for
269      * @return true if this item contains the given hint
270      */
hasHint(@lice.SliceHint String hint)271     public boolean hasHint(@Slice.SliceHint String hint) {
272         return ArrayUtils.contains(mHints, hint);
273     }
274 
275     /**
276      * @hide
277      */
278     @RestrictTo(Scope.LIBRARY)
SliceItem(Bundle in)279     public SliceItem(Bundle in) {
280         mHints = in.getStringArray(HINTS);
281         mFormat = in.getString(FORMAT);
282         mSubType = in.getString(SUBTYPE);
283         mObj = readObj(mFormat, in);
284     }
285 
286     /**
287      * @hide
288      * @return
289      */
290     @RestrictTo(Scope.LIBRARY)
toBundle()291     public Bundle toBundle() {
292         Bundle b = new Bundle();
293         b.putStringArray(HINTS, mHints);
294         b.putString(FORMAT, mFormat);
295         b.putString(SUBTYPE, mSubType);
296         writeObj(b, mObj, mFormat);
297         return b;
298     }
299 
300     /**
301      * @hide
302      */
303     @RestrictTo(Scope.LIBRARY)
hasHints(@lice.SliceHint String[] hints)304     public boolean hasHints(@Slice.SliceHint String[] hints) {
305         if (hints == null) return true;
306         for (String hint : hints) {
307             if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) {
308                 return false;
309             }
310         }
311         return true;
312     }
313 
314     /**
315      * @hide
316      */
317     @RestrictTo(Scope.LIBRARY)
hasAnyHints(@lice.SliceHint String... hints)318     public boolean hasAnyHints(@Slice.SliceHint String... hints) {
319         if (hints == null) return false;
320         for (String hint : hints) {
321             if (ArrayUtils.contains(mHints, hint)) {
322                 return true;
323             }
324         }
325         return false;
326     }
327 
writeObj(Bundle dest, Object obj, String type)328     private void writeObj(Bundle dest, Object obj, String type) {
329         switch (type) {
330             case FORMAT_IMAGE:
331                 dest.putBundle(OBJ, ((IconCompat) obj).toBundle());
332                 break;
333             case FORMAT_REMOTE_INPUT:
334                 dest.putParcelable(OBJ, (Parcelable) obj);
335                 break;
336             case FORMAT_SLICE:
337                 dest.putParcelable(OBJ, ((Slice) obj).toBundle());
338                 break;
339             case FORMAT_ACTION:
340                 dest.putParcelable(OBJ, (PendingIntent) ((Pair<Object, Slice>) obj).first);
341                 dest.putBundle(OBJ_2, ((Pair<Object, Slice>) obj).second.toBundle());
342                 break;
343             case FORMAT_TEXT:
344                 dest.putCharSequence(OBJ, (CharSequence) obj);
345                 break;
346             case FORMAT_INT:
347                 dest.putInt(OBJ, (Integer) mObj);
348                 break;
349             case FORMAT_LONG:
350                 dest.putLong(OBJ, (Long) mObj);
351                 break;
352         }
353     }
354 
readObj(String type, Bundle in)355     private static Object readObj(String type, Bundle in) {
356         switch (type) {
357             case FORMAT_IMAGE:
358                 return IconCompat.createFromBundle(in.getBundle(OBJ));
359             case FORMAT_REMOTE_INPUT:
360                 return in.getParcelable(OBJ);
361             case FORMAT_SLICE:
362                 return new Slice(in.getBundle(OBJ));
363             case FORMAT_TEXT:
364                 return in.getCharSequence(OBJ);
365             case FORMAT_ACTION:
366                 return new Pair<>(
367                         in.getParcelable(OBJ),
368                         new Slice(in.getBundle(OBJ_2)));
369             case FORMAT_INT:
370                 return in.getInt(OBJ);
371             case FORMAT_LONG:
372                 return in.getLong(OBJ);
373         }
374         throw new RuntimeException("Unsupported type " + type);
375     }
376 
377     /**
378      * @hide
379      */
380     @RestrictTo(Scope.LIBRARY)
typeToString(String format)381     public static String typeToString(String format) {
382         switch (format) {
383             case FORMAT_SLICE:
384                 return "Slice";
385             case FORMAT_TEXT:
386                 return "Text";
387             case FORMAT_IMAGE:
388                 return "Image";
389             case FORMAT_ACTION:
390                 return "Action";
391             case FORMAT_INT:
392                 return "Int";
393             case FORMAT_LONG:
394                 return "Long";
395             case FORMAT_REMOTE_INPUT:
396                 return "RemoteInput";
397         }
398         return "Unrecognized format: " + format;
399     }
400 
401     /**
402      * @return A string representation of this slice item.
403      */
404     @Override
toString()405     public String toString() {
406         return toString("");
407     }
408 
409     /**
410      * @return A string representation of this slice item.
411      * @hide
412      */
413     @RestrictTo(Scope.LIBRARY)
toString(String indent)414     public String toString(String indent) {
415         StringBuilder sb = new StringBuilder();
416         switch (getFormat()) {
417             case FORMAT_SLICE:
418                 sb.append(getSlice().toString(indent));
419                 break;
420             case FORMAT_ACTION:
421                 sb.append(indent).append(getAction()).append(",\n");
422                 sb.append(getSlice().toString(indent));
423                 break;
424             case FORMAT_TEXT:
425                 sb.append(indent).append('"').append(getText()).append('"');
426                 break;
427             case FORMAT_IMAGE:
428                 sb.append(indent).append(getIcon());
429                 break;
430             case FORMAT_INT:
431                 sb.append(indent).append(getInt());
432                 break;
433             case FORMAT_LONG:
434                 sb.append(indent).append(getLong());
435                 break;
436             default:
437                 sb.append(indent).append(SliceItem.typeToString(getFormat()));
438                 break;
439         }
440         if (!FORMAT_SLICE.equals(getFormat())) {
441             sb.append(' ');
442             addHints(sb, mHints);
443         }
444         sb.append(",\n");
445         return sb.toString();
446     }
447 }
448