1 /*
2  * Copyright 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.Slice.HINT_ACTIONS;
20 import static android.app.slice.Slice.HINT_PARTIAL;
21 import static android.app.slice.Slice.HINT_SHORTCUT;
22 import static android.app.slice.SliceItem.FORMAT_ACTION;
23 import static android.app.slice.SliceItem.FORMAT_IMAGE;
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 androidx.slice.SliceMetadata.LOADED_ALL;
29 import static androidx.slice.SliceMetadata.LOADED_NONE;
30 import static androidx.slice.SliceMetadata.LOADED_PARTIAL;
31 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
32 
33 import static java.lang.annotation.RetentionPolicy.SOURCE;
34 
35 import android.content.Context;
36 import android.net.Uri;
37 import android.text.TextUtils;
38 
39 import androidx.annotation.IntDef;
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.annotation.RestrictTo;
43 import androidx.slice.core.SliceQuery;
44 
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 import java.lang.annotation.Retention;
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 /**
53  * Utilities for dealing with slices.
54  */
55 public class SliceUtils {
56 
SliceUtils()57     private SliceUtils() {
58     }
59 
60     /**
61      * Serialize a slice to an OutputStream.
62      * <p>
63      * The slice can later be read into slice form again with {@link #parseSlice}.
64      * Some slice types cannot be serialized, their handling is controlled by
65      * {@link SerializeOptions}.
66      *
67      * @param s The slice to serialize.
68      * @param context Context used to load any resources in the slice.
69      * @param output The output of the serialization.
70      * @param encoding The encoding to use for serialization.
71      * @param options Options defining how to handle non-serializable items.
72      * @throws IllegalArgumentException if the slice cannot be serialized using the given options.
73      */
serializeSlice(@onNull Slice s, @NonNull Context context, @NonNull OutputStream output, @NonNull String encoding, @NonNull SerializeOptions options)74     public static void serializeSlice(@NonNull Slice s, @NonNull Context context,
75             @NonNull OutputStream output, @NonNull String encoding,
76             @NonNull SerializeOptions options) throws IOException, IllegalArgumentException {
77         SliceXml.serializeSlice(s, context, output, encoding, options);
78     }
79 
80     /**
81      * Parse a slice that has been previously serialized.
82      * <p>
83      * Parses a slice that was serialized with {@link #serializeSlice}.
84      * <p>
85      * Note: Slices returned by this cannot be passed to {@link SliceConvert#unwrap(Slice)}.
86      *
87      * @param input The input stream to read from.
88      * @param encoding The encoding to read as.
89      * @param listener Listener used to handle actions when reconstructing the slice.
90      * @throws SliceParseException if the InputStream cannot be parsed.
91      */
parseSlice(@onNull Context context, @NonNull InputStream input, @NonNull String encoding, @NonNull SliceActionListener listener)92     public static @NonNull Slice parseSlice(@NonNull Context context, @NonNull InputStream input,
93             @NonNull String encoding, @NonNull SliceActionListener listener)
94             throws IOException, SliceParseException {
95         return SliceXml.parseSlice(context, input, encoding, listener);
96     }
97 
98     /**
99      * Holds options for how to handle SliceItems that cannot be serialized.
100      */
101     public static class SerializeOptions {
102         /**
103          * Constant indicating that the an {@link IllegalArgumentException} should be thrown
104          * when this format is encountered.
105          */
106         public static final int MODE_THROW = 0;
107         /**
108          * Constant indicating that the SliceItem should be removed when this format is encountered.
109          */
110         public static final int MODE_REMOVE = 1;
111         /**
112          * Constant indicating that the SliceItem should be serialized as much as possible.
113          * <p>
114          * For images this means they will be attempted to be serialized. For actions, the
115          * action will be removed but the content of the action will be serialized. The action
116          * may be triggered later on a de-serialized slice by binding the slice again and activating
117          * a pending-intent at the same location as the serialized action.
118          */
119         public static final int MODE_CONVERT = 2;
120 
121         @IntDef({MODE_THROW, MODE_REMOVE, MODE_CONVERT})
122         @Retention(SOURCE)
123         @interface FormatMode {
124         }
125 
126         private int mActionMode = MODE_THROW;
127         private int mImageMode = MODE_THROW;
128         private int mMaxWidth = 1000;
129         private int mMaxHeight = 1000;
130 
131         /**
132          * @hide
133          */
134         @RestrictTo(RestrictTo.Scope.LIBRARY)
checkThrow(String format)135         public void checkThrow(String format) {
136             switch (format) {
137                 case FORMAT_ACTION:
138                 case FORMAT_REMOTE_INPUT:
139                     if (mActionMode != MODE_THROW) return;
140                     break;
141                 case FORMAT_IMAGE:
142                     if (mImageMode != MODE_THROW) return;
143                     break;
144                 default:
145                     return;
146             }
147             throw new IllegalArgumentException(format + " cannot be serialized");
148         }
149 
150         /**
151          * @hide
152          */
153         @RestrictTo(RestrictTo.Scope.LIBRARY)
getActionMode()154         public @FormatMode int getActionMode() {
155             return mActionMode;
156         }
157 
158         /**
159          * @hide
160          */
161         @RestrictTo(RestrictTo.Scope.LIBRARY)
getImageMode()162         public @FormatMode int getImageMode() {
163             return mImageMode;
164         }
165 
166         /**
167          * @hide
168          */
169         @RestrictTo(RestrictTo.Scope.LIBRARY)
getMaxWidth()170         public int getMaxWidth() {
171             return mMaxWidth;
172         }
173 
174         /**
175          * @hide
176          */
177         @RestrictTo(RestrictTo.Scope.LIBRARY)
getMaxHeight()178         public int getMaxHeight() {
179             return mMaxHeight;
180         }
181 
182         /**
183          * Sets how {@link android.app.slice.SliceItem#FORMAT_ACTION} items should be handled.
184          *
185          * The default mode is {@link #MODE_THROW}.
186          * @param mode The desired mode.
187          */
setActionMode(@ormatMode int mode)188         public SerializeOptions setActionMode(@FormatMode int mode) {
189             mActionMode = mode;
190             return this;
191         }
192 
193         /**
194          * Sets how {@link android.app.slice.SliceItem#FORMAT_IMAGE} items should be handled.
195          *
196          * The default mode is {@link #MODE_THROW}.
197          * @param mode The desired mode.
198          */
setImageMode(@ormatMode int mode)199         public SerializeOptions setImageMode(@FormatMode int mode) {
200             mImageMode = mode;
201             return this;
202         }
203 
204         /**
205          * Set the maximum width of an image to use when serializing.
206          * <p>
207          * Will only be used if the {@link #setImageMode(int)} is set to {@link #MODE_CONVERT}.
208          * Any images larger than the maximum size will be scaled down to fit within that size.
209          * The default value is 1000.
210          */
setMaxImageWidth(int width)211         public SerializeOptions setMaxImageWidth(int width) {
212             mMaxWidth = width;
213             return this;
214         }
215 
216         /**
217          * Set the maximum height of an image to use when serializing.
218          * <p>
219          * Will only be used if the {@link #setImageMode(int)} is set to {@link #MODE_CONVERT}.
220          * Any images larger than the maximum size will be scaled down to fit within that size.
221          * The default value is 1000.
222          */
setMaxImageHeight(int height)223         public SerializeOptions setMaxImageHeight(int height) {
224             mMaxHeight = height;
225             return this;
226         }
227     }
228 
229     /**
230      * Indicates this slice is empty and waiting for content to be loaded.
231      *
232      * @deprecated TO BE REMOVED: use {@link SliceMetadata#LOADED_NONE}
233      */
234     @Deprecated
235     public static final int LOADING_ALL = 0;
236     /**
237      * Indicates this slice has some content but is waiting for other content to be loaded.
238      *
239      * @deprecated TO BE REMOVED: use {@link SliceMetadata#LOADED_PARTIAL}
240      */
241     @Deprecated
242     public static final int LOADING_PARTIAL = 1;
243     /**
244      * Indicates this slice has fully loaded and is not waiting for other content.
245      *
246      * @deprecated TO BE REMOVED: use {@link SliceMetadata#LOADED_ALL}
247      */
248     @Deprecated
249     public static final int LOADING_COMPLETE = 2;
250 
251     /**
252      * @return the current loading state of the provided {@link Slice}.
253      *
254      * @deprecated TO BE REMOVED: use {@link SliceMetadata#getLoadingState()}
255      */
256     @Deprecated
getLoadingState(@onNull Slice slice)257     public static int getLoadingState(@NonNull Slice slice) {
258         // Check loading state
259         boolean hasHintPartial =
260                 SliceQuery.find(slice, null, HINT_PARTIAL, null) != null;
261         if (slice.getItems().size() == 0) {
262             // Empty slice
263             return LOADED_NONE;
264         } else if (hasHintPartial) {
265             // Slice with specific content to load
266             return LOADED_PARTIAL;
267         } else {
268             // Full slice
269             return LOADED_ALL;
270         }
271     }
272 
273     /**
274      * @return the group of actions associated with the provided slice, if they exist.
275      *
276      * @deprecated TO BE REMOVED; use {@link SliceMetadata#getSliceActions()}
277      */
278     @Deprecated
279     @Nullable
getSliceActions(@onNull Slice slice)280     public static List<SliceItem> getSliceActions(@NonNull Slice slice) {
281         SliceItem actionGroup = SliceQuery.find(slice, FORMAT_SLICE, HINT_ACTIONS, null);
282         String[] hints = new String[] {HINT_ACTIONS, HINT_SHORTCUT};
283         return (actionGroup != null)
284                 ? SliceQuery.findAll(actionGroup, FORMAT_SLICE, hints, null)
285                 : null;
286     }
287 
288     /**
289      * @return the list of keywords associated with the provided slice, null if no keywords were
290      * specified or an empty list if the slice was specified to have no keywords.
291      *
292      * @deprecated TO BE REMOVED; use {@link SliceMetadata#getSliceKeywords()}
293      */
294     @Deprecated
295     @Nullable
getSliceKeywords(@onNull Slice slice)296     public static List<String> getSliceKeywords(@NonNull Slice slice) {
297         SliceItem keywordGroup = SliceQuery.find(slice, FORMAT_SLICE, HINT_KEYWORDS, null);
298         if (keywordGroup != null) {
299             List<SliceItem> itemList = SliceQuery.findAll(keywordGroup, FORMAT_TEXT);
300             if (itemList != null) {
301                 ArrayList<String> stringList = new ArrayList<>();
302                 for (int i = 0; i < itemList.size(); i++) {
303                     String keyword = (String) itemList.get(i).getText();
304                     if (!TextUtils.isEmpty(keyword)) {
305                         stringList.add(keyword);
306                     }
307                 }
308                 return stringList;
309             }
310         }
311         return null;
312     }
313 
314     /**
315      * A listener used to receive events on slices parsed with
316      * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
317      */
318     public interface SliceActionListener {
319         /**
320          * Called when an action is triggered on a slice parsed with
321          * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
322          * @param actionUri The uri of the action selected.
323          */
onSliceAction(Uri actionUri)324         void onSliceAction(Uri actionUri);
325     }
326 
327     /**
328      * Exception thrown during
329      * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
330      */
331     public static class SliceParseException extends Exception {
332         /**
333          * @hide
334          */
335         @RestrictTo(RestrictTo.Scope.LIBRARY)
SliceParseException(String s, Throwable e)336         public SliceParseException(String s, Throwable e) {
337             super(s, e);
338         }
339 
340         /**
341          * @hide
342          */
343         @RestrictTo(RestrictTo.Scope.LIBRARY)
SliceParseException(String s)344         public SliceParseException(String s) {
345             super(s);
346         }
347     }
348 }
349