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