Slice.java revision 209af08eca265eb2af741bd2fa807802636ec570
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
17package android.app.slice;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.StringDef;
22import android.app.PendingIntent;
23import android.app.RemoteInput;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.IContentProvider;
27import android.content.Intent;
28import android.graphics.drawable.Icon;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.os.RemoteException;
34
35import com.android.internal.util.ArrayUtils;
36import com.android.internal.util.Preconditions;
37
38import java.lang.annotation.Retention;
39import java.lang.annotation.RetentionPolicy;
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.List;
43import java.util.Objects;
44
45/**
46 * A slice is a piece of app content and actions that can be surfaced outside of the app.
47 *
48 * <p>They are constructed using {@link Builder} in a tree structure
49 * that provides the OS some information about how the content should be displayed.
50 */
51public final class Slice implements Parcelable {
52
53    /**
54     * @hide
55     */
56    @StringDef(prefix = { "HINT_" }, value = {
57            HINT_TITLE,
58            HINT_LIST,
59            HINT_LIST_ITEM,
60            HINT_LARGE,
61            HINT_ACTIONS,
62            HINT_SELECTED,
63            HINT_NO_TINT,
64            HINT_SHORTCUT,
65            HINT_TOGGLE,
66            HINT_HORIZONTAL,
67            HINT_PARTIAL,
68            HINT_SEE_MORE,
69            HINT_KEYWORDS,
70            HINT_ERROR,
71            HINT_TTL,
72            HINT_LAST_UPDATED,
73            HINT_PERMISSION_REQUEST,
74    })
75    @Retention(RetentionPolicy.SOURCE)
76    public @interface SliceHint {}
77    /**
78     * @hide
79     */
80    @StringDef(prefix = { "SUBTYPE_" }, value = {
81            SUBTYPE_COLOR,
82            SUBTYPE_CONTENT_DESCRIPTION,
83            SUBTYPE_MAX,
84            SUBTYPE_MESSAGE,
85            SUBTYPE_PRIORITY,
86            SUBTYPE_RANGE,
87            SUBTYPE_SOURCE,
88            SUBTYPE_TOGGLE,
89            SUBTYPE_VALUE,
90            SUBTYPE_LAYOUT_DIRECTION,
91    })
92    @Retention(RetentionPolicy.SOURCE)
93    public @interface SliceSubtype {}
94
95    /**
96     * Hint that this content is a title of other content in the slice. This can also indicate that
97     * the content should be used in the shortcut representation of the slice (icon, label, action),
98     * normally this should be indicated by adding the hint on the action containing that content.
99     *
100     * @see SliceItem#FORMAT_ACTION
101     */
102    public static final String HINT_TITLE       = "title";
103    /**
104     * Hint that all sub-items/sub-slices within this content should be considered
105     * to have {@link #HINT_LIST_ITEM}.
106     */
107    public static final String HINT_LIST        = "list";
108    /**
109     * Hint that this item is part of a list and should be formatted as if is part
110     * of a list.
111     */
112    public static final String HINT_LIST_ITEM   = "list_item";
113    /**
114     * Hint that this content is important and should be larger when displayed if
115     * possible.
116     */
117    public static final String HINT_LARGE       = "large";
118    /**
119     * Hint that this slice contains a number of actions that can be grouped together
120     * in a sort of controls area of the UI.
121     */
122    public static final String HINT_ACTIONS     = "actions";
123    /**
124     * Hint indicating that this item (and its sub-items) are the current selection.
125     */
126    public static final String HINT_SELECTED    = "selected";
127    /**
128     * Hint to indicate that this content should not be tinted.
129     */
130    public static final String HINT_NO_TINT     = "no_tint";
131    /**
132     * Hint to indicate that this content should only be displayed if the slice is presented
133     * as a shortcut.
134     */
135    public static final String HINT_SHORTCUT = "shortcut";
136    /**
137     * Hint indicating this content should be shown instead of the normal content when the slice
138     * is in small format.
139     */
140    public static final String HINT_SUMMARY = "summary";
141    /**
142     * Hint to indicate that this content has a toggle action associated with it. To indicate that
143     * the toggle is on, use {@link #HINT_SELECTED}. When the toggle state changes, the intent
144     * associated with it will be sent along with an extra {@link #EXTRA_TOGGLE_STATE} which can be
145     * retrieved to see the new state of the toggle.
146     * @hide
147     */
148    public static final String HINT_TOGGLE = "toggle";
149    /**
150     * Hint that list items within this slice or subslice would appear better
151     * if organized horizontally.
152     */
153    public static final String HINT_HORIZONTAL = "horizontal";
154    /**
155     * Hint to indicate that this slice is incomplete and an update will be sent once
156     * loading is complete. Slices which contain HINT_PARTIAL will not be cached by the
157     * OS and should not be cached by apps.
158     */
159    public static final String HINT_PARTIAL     = "partial";
160    /**
161     * A hint representing that this item should be used to indicate that there's more
162     * content associated with this slice.
163     */
164    public static final String HINT_SEE_MORE = "see_more";
165    /**
166     * @see Builder#setCallerNeeded
167     * @hide
168     */
169    public static final String HINT_CALLER_NEEDED = "caller_needed";
170    /**
171     * A hint to indicate that the contents of this subslice represent a list of keywords
172     * related to the parent slice.
173     * Expected to be on an item of format {@link SliceItem#FORMAT_SLICE}.
174     */
175    public static final String HINT_KEYWORDS = "keywords";
176    /**
177     * A hint to indicate that this slice represents an error.
178     */
179    public static final String HINT_ERROR = "error";
180    /**
181     * Hint indicating an item representing a time-to-live for the content.
182     */
183    public static final String HINT_TTL = "ttl";
184    /**
185     * Hint indicating an item representing when the content was created or last updated.
186     */
187    public static final String HINT_LAST_UPDATED = "last_updated";
188    /**
189     * A hint to indicate that this slice represents a permission request for showing
190     * slices.
191     */
192    public static final String HINT_PERMISSION_REQUEST = "permission_request";
193    /**
194     * Subtype to indicate that this item indicates the layout direction for content
195     * in the slice.
196     * Expected to be an item of format {@link SliceItem#FORMAT_INT}.
197     */
198    public static final String SUBTYPE_LAYOUT_DIRECTION = "layout_direction";
199    /**
200     * Key to retrieve an extra added to an intent when a control is changed.
201     */
202    public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
203    /**
204     * Key to retrieve an extra added to an intent when the value of a slider is changed.
205     * @deprecated remove once support lib is update to use EXTRA_RANGE_VALUE instead
206     */
207    @Deprecated
208    public static final String EXTRA_SLIDER_VALUE = "android.app.slice.extra.SLIDER_VALUE";
209    /**
210     * Key to retrieve an extra added to an intent when the value of an input range is changed.
211     */
212    public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
213    /**
214     * Subtype to indicate that this is a message as part of a communication
215     * sequence in this slice.
216     * Expected to be on an item of format {@link SliceItem#FORMAT_SLICE}.
217     */
218    public static final String SUBTYPE_MESSAGE = "message";
219    /**
220     * Subtype to tag the source (i.e. sender) of a {@link #SUBTYPE_MESSAGE}.
221     * Expected to be on an item of format {@link SliceItem#FORMAT_TEXT},
222     * {@link SliceItem#FORMAT_IMAGE} or an {@link SliceItem#FORMAT_SLICE} containing them.
223     */
224    public static final String SUBTYPE_SOURCE = "source";
225    /**
226     * Subtype to tag an item as representing a color.
227     * Expected to be on an item of format {@link SliceItem#FORMAT_INT}.
228     */
229    public static final String SUBTYPE_COLOR = "color";
230    /**
231     * Subtype to tag an item as representing a slider.
232     * @deprecated remove once support lib is update to use SUBTYPE_RANGE instead
233     */
234    @Deprecated
235    public static final String SUBTYPE_SLIDER = "slider";
236    /**
237     * Subtype to tag an item as representing a range.
238     * Expected to be on an item of format {@link SliceItem#FORMAT_SLICE} containing
239     * a {@link #SUBTYPE_VALUE} and possibly a {@link #SUBTYPE_MAX}.
240     */
241    public static final String SUBTYPE_RANGE = "range";
242    /**
243     * Subtype to tag an item as representing the max int value for a {@link #SUBTYPE_RANGE}.
244     * Expected to be on an item of format {@link SliceItem#FORMAT_INT}.
245     */
246    public static final String SUBTYPE_MAX = "max";
247    /**
248     * Subtype to tag an item as representing the current int value for a {@link #SUBTYPE_RANGE}.
249     * Expected to be on an item of format {@link SliceItem#FORMAT_INT}.
250     */
251    public static final String SUBTYPE_VALUE = "value";
252    /**
253     * Subtype to indicate that this content has a toggle action associated with it. To indicate
254     * that the toggle is on, use {@link #HINT_SELECTED}. When the toggle state changes, the
255     * intent associated with it will be sent along with an extra {@link #EXTRA_TOGGLE_STATE}
256     * which can be retrieved to see the new state of the toggle.
257     */
258    public static final String SUBTYPE_TOGGLE = "toggle";
259    /**
260     * Subtype to tag an item representing priority.
261     * Expected to be on an item of format {@link SliceItem#FORMAT_INT}.
262     */
263    public static final String SUBTYPE_PRIORITY = "priority";
264    /**
265     * Subtype to tag an item to use as a content description.
266     * Expected to be on an item of format {@link SliceItem#FORMAT_TEXT}.
267     */
268    public static final String SUBTYPE_CONTENT_DESCRIPTION = "content_description";
269    /**
270     * Subtype to tag an item as representing a time in milliseconds since midnight,
271     * January 1, 1970 UTC.
272     */
273    public static final String SUBTYPE_MILLIS = "millis";
274
275    private final SliceItem[] mItems;
276    private final @SliceHint String[] mHints;
277    private SliceSpec mSpec;
278    private Uri mUri;
279
280    Slice(ArrayList<SliceItem> items, @SliceHint String[] hints, Uri uri, SliceSpec spec) {
281        mHints = hints;
282        mItems = items.toArray(new SliceItem[items.size()]);
283        mUri = uri;
284        mSpec = spec;
285    }
286
287    protected Slice(Parcel in) {
288        mHints = in.readStringArray();
289        int n = in.readInt();
290        mItems = new SliceItem[n];
291        for (int i = 0; i < n; i++) {
292            mItems[i] = SliceItem.CREATOR.createFromParcel(in);
293        }
294        mUri = Uri.CREATOR.createFromParcel(in);
295        mSpec = in.readTypedObject(SliceSpec.CREATOR);
296    }
297
298    /**
299     * @return The spec for this slice
300     */
301    public @Nullable SliceSpec getSpec() {
302        return mSpec;
303    }
304
305    /**
306     * @return The Uri that this Slice represents.
307     */
308    public Uri getUri() {
309        return mUri;
310    }
311
312    /**
313     * @return All child {@link SliceItem}s that this Slice contains.
314     */
315    public List<SliceItem> getItems() {
316        return Arrays.asList(mItems);
317    }
318
319    /**
320     * @return All hints associated with this Slice.
321     */
322    public @SliceHint List<String> getHints() {
323        return Arrays.asList(mHints);
324    }
325
326    @Override
327    public void writeToParcel(Parcel dest, int flags) {
328        dest.writeStringArray(mHints);
329        dest.writeInt(mItems.length);
330        for (int i = 0; i < mItems.length; i++) {
331            mItems[i].writeToParcel(dest, flags);
332        }
333        mUri.writeToParcel(dest, 0);
334        dest.writeTypedObject(mSpec, flags);
335    }
336
337    @Override
338    public int describeContents() {
339        return 0;
340    }
341
342    /**
343     * @hide
344     */
345    public boolean hasHint(@SliceHint String hint) {
346        return ArrayUtils.contains(mHints, hint);
347    }
348
349    /**
350     * Returns whether the caller for this slice matters.
351     * @see Builder#setCallerNeeded
352     */
353    public boolean isCallerNeeded() {
354        return hasHint(HINT_CALLER_NEEDED);
355    }
356
357    /**
358     * A Builder used to construct {@link Slice}s
359     */
360    public static class Builder {
361
362        private final Uri mUri;
363        private ArrayList<SliceItem> mItems = new ArrayList<>();
364        private @SliceHint ArrayList<String> mHints = new ArrayList<>();
365        private SliceSpec mSpec;
366
367        /**
368         * @deprecated TO BE REMOVED
369         */
370        @Deprecated
371        public Builder(@NonNull Uri uri) {
372            mUri = uri;
373        }
374
375        /**
376         * Create a builder which will construct a {@link Slice} for the given Uri.
377         * @param uri Uri to tag for this slice.
378         * @param spec the spec for this slice.
379         */
380        public Builder(@NonNull Uri uri, SliceSpec spec) {
381            mUri = uri;
382            mSpec = spec;
383        }
384
385        /**
386         * Create a builder for a {@link Slice} that is a sub-slice of the slice
387         * being constructed by the provided builder.
388         * @param parent The builder constructing the parent slice
389         */
390        public Builder(@NonNull Slice.Builder parent) {
391            mUri = parent.mUri.buildUpon().appendPath("_gen")
392                    .appendPath(String.valueOf(mItems.size())).build();
393        }
394
395        /**
396         * Tells the system whether for this slice the return value of
397         * {@link SliceProvider#onBindSlice(Uri, List)} may be different depending on
398         * {@link SliceProvider#getCallingPackage()} and should not be cached for multiple
399         * apps.
400         */
401        public Builder setCallerNeeded(boolean callerNeeded) {
402            if (callerNeeded) {
403                mHints.add(HINT_CALLER_NEEDED);
404            } else {
405                mHints.remove(HINT_CALLER_NEEDED);
406            }
407            return this;
408        }
409
410        /**
411         * Add hints to the Slice being constructed
412         */
413        public Builder addHints(@SliceHint List<String> hints) {
414            mHints.addAll(hints);
415            return this;
416        }
417
418        /**
419         * @deprecated TO BE REMOVED
420         */
421        public Builder setSpec(SliceSpec spec) {
422            mSpec = spec;
423            return this;
424        }
425
426        /**
427         * Add a sub-slice to the slice being constructed
428         * @param subType Optional template-specific type information
429         * @see {@link SliceItem#getSubType()}
430         */
431        public Builder addSubSlice(@NonNull Slice slice, @Nullable @SliceSubtype String subType) {
432            mItems.add(new SliceItem(slice, SliceItem.FORMAT_SLICE, subType,
433                    slice.getHints().toArray(new String[slice.getHints().size()])));
434            return this;
435        }
436
437        /**
438         * Add an action to the slice being constructed
439         * @param subType Optional template-specific type information
440         * @see {@link SliceItem#getSubType()}
441         */
442        public Slice.Builder addAction(@NonNull PendingIntent action, @NonNull Slice s,
443                @Nullable @SliceSubtype String subType) {
444            List<String> hints = s.getHints();
445            s.mSpec = null;
446            mItems.add(new SliceItem(action, s, SliceItem.FORMAT_ACTION, subType, hints.toArray(
447                    new String[hints.size()])));
448            return this;
449        }
450
451        /**
452         * Add text to the slice being constructed
453         * @param subType Optional template-specific type information
454         * @see {@link SliceItem#getSubType()}
455         */
456        public Builder addText(CharSequence text, @Nullable @SliceSubtype String subType,
457                @SliceHint List<String> hints) {
458            mItems.add(new SliceItem(text, SliceItem.FORMAT_TEXT, subType, hints));
459            return this;
460        }
461
462        /**
463         * Add an image to the slice being constructed
464         * @param subType Optional template-specific type information
465         * @see {@link SliceItem#getSubType()}
466         */
467        public Builder addIcon(Icon icon, @Nullable @SliceSubtype String subType,
468                @SliceHint List<String> hints) {
469            mItems.add(new SliceItem(icon, SliceItem.FORMAT_IMAGE, subType, hints));
470            return this;
471        }
472
473        /**
474         * Add remote input to the slice being constructed
475         * @param subType Optional template-specific type information
476         * @see {@link SliceItem#getSubType()}
477         */
478        public Slice.Builder addRemoteInput(RemoteInput remoteInput,
479                @Nullable @SliceSubtype String subType,
480                @SliceHint List<String> hints) {
481            mItems.add(new SliceItem(remoteInput, SliceItem.FORMAT_REMOTE_INPUT,
482                    subType, hints));
483            return this;
484        }
485
486        /**
487         * Add an integer to the slice being constructed
488         * @param subType Optional template-specific type information
489         * @see {@link SliceItem#getSubType()}
490         */
491        public Builder addInt(int value, @Nullable @SliceSubtype String subType,
492                @SliceHint List<String> hints) {
493            mItems.add(new SliceItem(value, SliceItem.FORMAT_INT, subType, hints));
494            return this;
495        }
496
497        /**
498         * @deprecated TO BE REMOVED.
499         */
500        @Deprecated
501        public Slice.Builder addTimestamp(long time, @Nullable @SliceSubtype String subType,
502                @SliceHint List<String> hints) {
503            return addLong(time, subType, hints);
504        }
505
506        /**
507         * Add a long to the slice being constructed
508         * @param subType Optional template-specific type information
509         * @see {@link SliceItem#getSubType()}
510         */
511        public Slice.Builder addLong(long value, @Nullable @SliceSubtype String subType,
512                @SliceHint List<String> hints) {
513            mItems.add(new SliceItem(value, SliceItem.FORMAT_LONG, subType,
514                    hints.toArray(new String[hints.size()])));
515            return this;
516        }
517
518        /**
519         * Add a bundle to the slice being constructed.
520         * <p>Expected to be used for support library extension, should not be used for general
521         * development
522         * @param subType Optional template-specific type information
523         * @see {@link SliceItem#getSubType()}
524         */
525        public Slice.Builder addBundle(Bundle bundle, @Nullable @SliceSubtype String subType,
526                @SliceHint List<String> hints) {
527            mItems.add(new SliceItem(bundle, SliceItem.FORMAT_BUNDLE, subType,
528                    hints));
529            return this;
530        }
531
532        /**
533         * Construct the slice.
534         */
535        public Slice build() {
536            return new Slice(mItems, mHints.toArray(new String[mHints.size()]), mUri, mSpec);
537        }
538    }
539
540    public static final Creator<Slice> CREATOR = new Creator<Slice>() {
541        @Override
542        public Slice createFromParcel(Parcel in) {
543            return new Slice(in);
544        }
545
546        @Override
547        public Slice[] newArray(int size) {
548            return new Slice[size];
549        }
550    };
551
552    /**
553     * @hide
554     * @return A string representation of this slice.
555     */
556    public String toString() {
557        return toString("");
558    }
559
560    private String toString(String indent) {
561        StringBuilder sb = new StringBuilder();
562        for (int i = 0; i < mItems.length; i++) {
563            sb.append(indent);
564            if (Objects.equals(mItems[i].getFormat(), SliceItem.FORMAT_SLICE)) {
565                sb.append("slice:\n");
566                sb.append(mItems[i].getSlice().toString(indent + "   "));
567            } else if (Objects.equals(mItems[i].getFormat(), SliceItem.FORMAT_TEXT)) {
568                sb.append("text: ");
569                sb.append(mItems[i].getText());
570                sb.append("\n");
571            } else {
572                sb.append(mItems[i].getFormat());
573                sb.append("\n");
574            }
575        }
576        return sb.toString();
577    }
578
579    /**
580     * @deprecated TO BE REMOVED.
581     */
582    @Deprecated
583    public static @Nullable Slice bindSlice(ContentResolver resolver,
584            @NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
585        Preconditions.checkNotNull(uri, "uri");
586        IContentProvider provider = resolver.acquireProvider(uri);
587        if (provider == null) {
588            throw new IllegalArgumentException("Unknown URI " + uri);
589        }
590        try {
591            Bundle extras = new Bundle();
592            extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
593            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
594                    new ArrayList<>(supportedSpecs));
595            final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE,
596                    null, extras);
597            Bundle.setDefusable(res, true);
598            if (res == null) {
599                return null;
600            }
601            return res.getParcelable(SliceProvider.EXTRA_SLICE);
602        } catch (RemoteException e) {
603            // Arbitrary and not worth documenting, as Activity
604            // Manager will kill this process shortly anyway.
605            return null;
606        } finally {
607            resolver.releaseProvider(provider);
608        }
609    }
610
611    /**
612     * @deprecated TO BE REMOVED.
613     */
614    @Deprecated
615    public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent,
616            @NonNull List<SliceSpec> supportedSpecs) {
617        return context.getSystemService(SliceManager.class).bindSlice(intent, supportedSpecs);
618    }
619}
620