• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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;
18 
19 import static android.app.slice.Slice.HINT_PARTIAL;
20 import static android.app.slice.Slice.HINT_SUMMARY;
21 import static android.app.slice.Slice.HINT_TITLE;
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_INT;
25 import static android.app.slice.SliceItem.FORMAT_LONG;
26 import static android.app.slice.SliceItem.FORMAT_SLICE;
27 import static android.app.slice.SliceItem.FORMAT_TEXT;
28 
29 import static com.android.tv.twopanelsettings.slices.SlicesConstants.CHECKMARK;
30 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_ACTION_ID;
31 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PAGE_ID;
32 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_IMAGE;
33 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_TEXT;
34 import static com.android.tv.twopanelsettings.slices.SlicesConstants.RADIO;
35 import static com.android.tv.twopanelsettings.slices.SlicesConstants.SWITCH;
36 
37 import android.graphics.drawable.Drawable;
38 import android.graphics.drawable.Icon;
39 import android.net.Uri;
40 import android.os.Bundle;
41 import android.text.TextUtils;
42 import android.util.Pair;
43 import android.view.ContextThemeWrapper;
44 
45 import androidx.core.graphics.drawable.IconCompat;
46 import androidx.preference.Preference;
47 import androidx.preference.PreferenceCategory;
48 import androidx.slice.Slice;
49 import androidx.slice.SliceItem;
50 import androidx.slice.core.SliceActionImpl;
51 import androidx.slice.core.SliceQuery;
52 import androidx.slice.widget.SliceContent;
53 
54 import com.android.tv.twopanelsettings.IconUtil;
55 import com.android.tv.twopanelsettings.R;
56 
57 import java.util.ArrayList;
58 import java.util.List;
59 
60 /**
61  * Generate corresponding preference based upon the slice data.
62  */
63 public final class SlicePreferencesUtil {
64 
getPreference(SliceItem item, ContextThemeWrapper contextThemeWrapper, String className)65     static Preference getPreference(SliceItem item, ContextThemeWrapper contextThemeWrapper,
66             String className) {
67         Preference preference = null;
68         if (item == null) {
69             return null;
70         }
71         Data data = extract(item);
72         if (item.getSubType() != null) {
73             String subType = item.getSubType();
74             if (subType.equals(SlicesConstants.TYPE_PREFERENCE)
75                     || subType.equals(SlicesConstants.TYPE_PREFERENCE_EMBEDDED)) {
76                 // TODO: Figure out all the possible cases and reorganize the logic
77                 if (data.mInfoItems.size() > 0) {
78                     preference = new InfoPreference(
79                                 contextThemeWrapper, getInfoList(data.mInfoItems));
80                 } else if (data.mIntentItem != null) {
81                     SliceActionImpl action = new SliceActionImpl(data.mIntentItem);
82                     if (action != null) {
83                         // Currently if we don't set icon for the SliceAction, slice lib will
84                         // automatically treat it as a toggle. To distinguish preference action and
85                         // toggle action, we need to add a subtype if this is a preference action.
86                         preference = new SlicePreference(contextThemeWrapper);
87                         ((SlicePreference) preference).setSliceAction(action);
88                         ((SlicePreference) preference).setActionId(getActionId(item));
89                         if (data.mFollowupIntentItem != null) {
90                             SliceActionImpl followUpAction =
91                                     new SliceActionImpl(data.mFollowupIntentItem);
92                             ((SlicePreference) preference).setFollowupSliceAction(followUpAction);
93                         }
94                     }
95                 } else if (data.mEndItems.size() > 0 && data.mEndItems.get(0) != null) {
96                     SliceActionImpl action = new SliceActionImpl(data.mEndItems.get(0));
97                     if (action != null) {
98                         int buttonStyle = SlicePreferencesUtil.getButtonStyle(item);
99                         switch (buttonStyle) {
100                             case CHECKMARK :
101                                 preference = new SliceCheckboxPreference(
102                                         contextThemeWrapper, action);
103                                 break;
104                             case SWITCH :
105                                 preference = new SliceSwitchPreference(contextThemeWrapper, action);
106                                 break;
107                             case RADIO:
108                                 preference = new SliceRadioPreference(contextThemeWrapper, action);
109                                 preference.setLayoutResource(R.layout.preference_reversed_widget);
110                                 if (getRadioGroup(item) != null) {
111                                     ((SliceRadioPreference) preference).setRadioGroup(
112                                             getRadioGroup(item).toString());
113                                 }
114                                 break;
115                         }
116                         if (preference instanceof HasSliceAction) {
117                             ((HasSliceAction) preference).setActionId(getActionId(item));
118                         }
119                         if (data.mFollowupIntentItem != null) {
120                             SliceActionImpl followUpAction =
121                                     new SliceActionImpl(data.mFollowupIntentItem);
122                             ((HasSliceAction) preference).setFollowupSliceAction(followUpAction);
123 
124                         }
125                     }
126                 }
127 
128                 CharSequence uri = getText(data.mTargetSliceItem);
129                 if (uri == null || TextUtils.isEmpty(uri)) {
130                     if (preference == null) {
131                         preference = new Preference(contextThemeWrapper);
132                     }
133                 } else {
134                     if (preference == null) {
135                         preference = new SlicePreference(contextThemeWrapper);
136                     }
137                     ((HasSliceUri) preference).setUri(uri.toString());
138                     if (preference instanceof HasSliceAction) {
139                         ((HasSliceAction) preference).setActionId(getActionId(item));
140                     }
141                     preference.setFragment(className);
142                 }
143             } else if (item.getSubType().equals(SlicesConstants.TYPE_PREFERENCE_CATEGORY)) {
144                 preference = new PreferenceCategory(contextThemeWrapper);
145             }
146         }
147 
148         if (preference != null) {
149             // Set whether preference is enabled.
150             if (preference instanceof InfoPreference || !enabled(item)) {
151                 preference.setEnabled(false);
152             }
153             // Set whether preference is selectable
154             if (!selectable(item)) {
155                 preference.setSelectable(false);
156             }
157             // Set the key for the preference
158             CharSequence key = getKey(item);
159             if (key != null) {
160                 preference.setKey(key.toString());
161             }
162 
163             Icon icon = getIcon(data.mStartItem);
164             if (icon != null) {
165                 boolean isIconNeedToBeProcessed =
166                         SlicePreferencesUtil.isIconNeedsToBeProcessed(item);
167                 Drawable iconDrawable = icon.loadDrawable(contextThemeWrapper);
168                 if (isIconNeedToBeProcessed) {
169                     preference.setIcon(IconUtil.getCompoundIcon(contextThemeWrapper, iconDrawable));
170                 } else {
171                     preference.setIcon(iconDrawable);
172                 }
173             }
174 
175             if (data.mTitleItem != null) {
176                 preference.setTitle(getText(data.mTitleItem));
177             }
178 
179             //Set summary
180             CharSequence subtitle =
181                     data.mSubtitleItem != null ? data.mSubtitleItem.getText() : null;
182             boolean subtitleExists = !TextUtils.isEmpty(subtitle)
183                     || (data.mSubtitleItem != null && data.mSubtitleItem.hasHint(HINT_PARTIAL));
184             if (subtitleExists) {
185                 preference.setSummary(subtitle);
186             } else {
187                 if (data.mSummaryItem != null) {
188                     preference.setSummary(getText(data.mSummaryItem));
189                 }
190             }
191             // Set preview info image and text
192             CharSequence infoText = getInfoText(item);
193             IconCompat infoImage = getInfoImage(item);
194             Bundle b = preference.getExtras();
195             if (infoImage != null) {
196                 b.putParcelable(EXTRA_PREFERENCE_INFO_IMAGE, infoImage.toIcon());
197             }
198             if (infoText != null) {
199                 b.putCharSequence(EXTRA_PREFERENCE_INFO_TEXT, infoText);
200             }
201             if (infoImage != null || infoText != null) {
202                 preference.setFragment(InfoFragment.class.getCanonicalName());
203             }
204         }
205 
206         return preference;
207     }
208 
209     static class Data {
210         SliceItem mStartItem;
211         SliceItem mTitleItem;
212         SliceItem mSubtitleItem;
213         SliceItem mSummaryItem;
214         SliceItem mTargetSliceItem;
215         SliceItem mRadioGroupItem;
216         SliceItem mIntentItem;
217         SliceItem mFollowupIntentItem;
218         List<SliceItem> mEndItems = new ArrayList<>();
219         List<SliceItem> mInfoItems = new ArrayList<>();
220     }
221 
extract(SliceItem sliceItem)222     static Data extract(SliceItem sliceItem) {
223         Data data = new Data();
224         List<SliceItem> possibleStartItems =
225                 SliceQuery.findAll(sliceItem, null, HINT_TITLE, null);
226         if (possibleStartItems.size() > 0) {
227             // The start item will be at position 0 if it exists
228             String format = possibleStartItems.get(0).getFormat();
229             if ((FORMAT_ACTION.equals(format)
230                     && SliceQuery.find(possibleStartItems.get(0), FORMAT_IMAGE) != null)
231                     || FORMAT_SLICE.equals(format)
232                     || FORMAT_LONG.equals(format)
233                     || FORMAT_IMAGE.equals(format)) {
234                 data.mStartItem = possibleStartItems.get(0);
235             }
236         }
237 
238         List<SliceItem> items = sliceItem.getSlice().getItems();
239         for (int i = 0; i < items.size(); i++) {
240             final SliceItem item = items.get(i);
241             String subType = item.getSubType();
242             if (subType != null) {
243                 switch (subType) {
244                     case SlicesConstants.SUBTYPE_INFO_PREFERENCE :
245                         data.mInfoItems.add(item);
246                         break;
247                     case SlicesConstants.SUBTYPE_INTENT :
248                         data.mIntentItem = item;
249                         break;
250                     case SlicesConstants.SUBTYPE_FOLLOWUP_INTENT :
251                         data.mFollowupIntentItem = item;
252                         break;
253                     case SlicesConstants.TAG_TARGET_URI :
254                         data.mTargetSliceItem = item;
255                         break;
256                 }
257             } else if (FORMAT_TEXT.equals(item.getFormat()) && (item.getSubType() == null)) {
258                 if ((data.mTitleItem == null || !data.mTitleItem.hasHint(HINT_TITLE))
259                         && item.hasHint(HINT_TITLE) && !item.hasHint(HINT_SUMMARY)) {
260                     data.mTitleItem = item;
261                 } else if (data.mSubtitleItem == null && !item.hasHint(HINT_SUMMARY)) {
262                     data.mSubtitleItem = item;
263                 } else if (data.mSummaryItem == null && item.hasHint(HINT_SUMMARY)) {
264                     data.mSummaryItem = item;
265                 }
266             } else {
267                 data.mEndItems.add(item);
268             }
269         }
270         data.mEndItems.remove(data.mStartItem);
271         return data;
272     }
273 
getInfoList(List<SliceItem> sliceItems)274     private static List<Pair<CharSequence, CharSequence>> getInfoList(List<SliceItem> sliceItems) {
275         List<Pair<CharSequence, CharSequence>> infoList = new ArrayList<>();
276         for (SliceItem item : sliceItems) {
277             Slice itemSlice = item.getSlice();
278             if (itemSlice != null) {
279                 CharSequence title = null;
280                 CharSequence summary = null;
281                 for (SliceItem element : itemSlice.getItems()) {
282                     if (element.getHints().contains(HINT_TITLE)) {
283                         title = element.getText();
284                     } else if (element.getHints().contains(HINT_SUMMARY)) {
285                         summary = element.getText();
286                     }
287                 }
288                 infoList.add(new Pair<CharSequence, CharSequence>(title, summary));
289             }
290         }
291         return infoList;
292     }
293 
getKey(SliceItem item)294     private static CharSequence getKey(SliceItem item) {
295         SliceItem target = SliceQuery.findSubtype(item, FORMAT_TEXT, SlicesConstants.TAG_KEY);
296         return target != null ? target.getText() : null;
297     }
298 
getRadioGroup(SliceItem item)299     private static CharSequence getRadioGroup(SliceItem item) {
300         SliceItem target = SliceQuery.findSubtype(
301                 item, FORMAT_TEXT, SlicesConstants.TAG_RADIO_GROUP);
302         return target != null ? target.getText() : null;
303     }
304 
305     /**
306      * Get the screen title item for the slice.
307      * @param sliceItems list of SliceItem extracted from slice data.
308      * @return screen title item.
309      */
getScreenTitleItem(List<SliceContent> sliceItems)310     static SliceItem getScreenTitleItem(List<SliceContent> sliceItems) {
311         for (SliceContent contentItem : sliceItems)  {
312             SliceItem item = contentItem.getSliceItem();
313             if (item.getSubType() != null
314                     && item.getSubType().equals(SlicesConstants.TYPE_PREFERENCE_SCREEN_TITLE)) {
315                 return item;
316             }
317         }
318         return null;
319     }
320 
getFocusedPreferenceItem(List<SliceContent> sliceItems)321     static SliceItem getFocusedPreferenceItem(List<SliceContent> sliceItems) {
322         for (SliceContent contentItem : sliceItems)  {
323             SliceItem item = contentItem.getSliceItem();
324             if (item.getSubType() != null
325                     && item.getSubType().equals(SlicesConstants.TYPE_FOCUSED_PREFERENCE)) {
326                 return item;
327             }
328         }
329         return null;
330     }
331 
getEmbeddedItem(List<SliceContent> sliceItems)332     static SliceItem getEmbeddedItem(List<SliceContent> sliceItems) {
333         for (SliceContent contentItem : sliceItems)  {
334             SliceItem item = contentItem.getSliceItem();
335             if (item.getSubType() != null
336                     && item.getSubType().equals(SlicesConstants.TYPE_PREFERENCE_EMBEDDED)) {
337                 return item;
338             }
339         }
340         return null;
341     }
342 
isIconNeedsToBeProcessed(SliceItem sliceItem)343     private static boolean isIconNeedsToBeProcessed(SliceItem sliceItem) {
344         List<SliceItem> items = sliceItem.getSlice().getItems();
345         for (SliceItem item : items)  {
346             if (item.getSubType() != null && item.getSubType().equals(
347                     SlicesConstants.SUBTYPE_ICON_NEED_TO_BE_PROCESSED)) {
348                 return item.getInt() == 1;
349             }
350         }
351         return false;
352     }
353 
getButtonStyle(SliceItem sliceItem)354     private static int getButtonStyle(SliceItem sliceItem) {
355         List<SliceItem> items = sliceItem.getSlice().getItems();
356         for (SliceItem item : items)  {
357             if (item.getSubType() != null
358                     && item.getSubType().equals(SlicesConstants.SUBTYPE_BUTTON_STYLE)) {
359                 return item.getInt();
360             }
361         }
362         return -1;
363     }
364 
enabled(SliceItem sliceItem)365     private static boolean enabled(SliceItem sliceItem) {
366         List<SliceItem> items = sliceItem.getSlice().getItems();
367         for (SliceItem item : items)  {
368             if (item.getSubType() != null
369                     && item.getSubType().equals(SlicesConstants.SUBTYPE_IS_ENABLED)) {
370                 return item.getInt() == 1;
371             }
372         }
373         return true;
374     }
375 
selectable(SliceItem sliceItem)376     private static boolean selectable(SliceItem sliceItem) {
377         List<SliceItem> items = sliceItem.getSlice().getItems();
378         for (SliceItem item : items)  {
379             if (item.getSubType() != null
380                     && item.getSubType().equals(SlicesConstants.SUBTYPE_IS_SELECTABLE)) {
381                 return item.getInt() == 1;
382             }
383         }
384         return true;
385     }
386 
387     /**
388      * Get the text from the SliceItem.
389      */
getText(SliceItem item)390     static CharSequence getText(SliceItem item) {
391         if (item == null) {
392             return null;
393         }
394         return item.getText();
395     }
396 
397     /** Get the icon from the SlicItem if available */
getIcon(SliceItem startItem)398     static Icon getIcon(SliceItem startItem) {
399         if (startItem != null && startItem.getSlice() != null
400                 && startItem.getSlice().getItems() != null
401                 && startItem.getSlice().getItems().size() > 0) {
402             SliceItem iconItem = startItem.getSlice().getItems().get(0);
403             if (FORMAT_IMAGE.equals(iconItem.getFormat())) {
404                 IconCompat icon = iconItem.getIcon();
405                 return icon.toIcon();
406             }
407         }
408         return null;
409     }
410 
getStatusPath(String uriString)411     static Uri getStatusPath(String uriString) {
412         Uri statusUri = Uri.parse(uriString)
413                 .buildUpon().path("/" + SlicesConstants.PATH_STATUS).build();
414         return statusUri;
415     }
416 
getPageId(SliceItem item)417     static int getPageId(SliceItem item) {
418         SliceItem target = SliceQuery.findSubtype(item, FORMAT_INT, EXTRA_PAGE_ID);
419         return target != null ? target.getInt() : 0;
420     }
421 
getActionId(SliceItem item)422     private static int getActionId(SliceItem item) {
423         SliceItem target = SliceQuery.findSubtype(item, FORMAT_INT, EXTRA_ACTION_ID);
424         return target != null ? target.getInt() : 0;
425     }
426 
427 
getInfoText(SliceItem item)428     private static CharSequence getInfoText(SliceItem item) {
429         SliceItem target = SliceQuery.findSubtype(item, FORMAT_TEXT, EXTRA_PREFERENCE_INFO_TEXT);
430         return target != null ? target.getText() : null;
431     }
432 
getInfoImage(SliceItem item)433     private static IconCompat getInfoImage(SliceItem item) {
434         SliceItem target = SliceQuery.findSubtype(item, FORMAT_IMAGE, EXTRA_PREFERENCE_INFO_IMAGE);
435         return target != null ? target.getIcon() : null;
436     }
437 }
438