1 /* 2 * Copyright (C) 2024 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.systemui.tv.media.settings; 18 19 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; 20 import static android.app.slice.Slice.HINT_PARTIAL; 21 22 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_STATUS; 23 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_KEY; 24 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_SLICE_FOLLOWUP; 25 26 import android.app.Activity; 27 import android.app.PendingIntent; 28 import android.app.PendingIntent.CanceledException; 29 import android.app.tvsettings.TvSettingsEnums; 30 import android.content.ContentProviderClient; 31 import android.content.Intent; 32 import android.content.IntentSender; 33 import android.database.ContentObserver; 34 import android.graphics.drawable.Drawable; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.Parcelable; 39 import android.text.TextUtils; 40 import android.util.Log; 41 import android.view.LayoutInflater; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.widget.SeekBar; 45 import android.widget.TextView; 46 import android.widget.Toast; 47 48 import androidx.activity.result.ActivityResult; 49 import androidx.activity.result.ActivityResultCallback; 50 import androidx.activity.result.ActivityResultLauncher; 51 import androidx.activity.result.IntentSenderRequest; 52 import androidx.activity.result.contract.ActivityResultContracts; 53 import androidx.annotation.Keep; 54 import androidx.annotation.NonNull; 55 import androidx.fragment.app.Fragment; 56 import androidx.lifecycle.Observer; 57 import androidx.preference.Preference; 58 import androidx.preference.PreferenceDialogFragmentCompat; 59 import androidx.preference.PreferenceFragmentCompat; 60 import androidx.preference.PreferenceManager; 61 import androidx.preference.PreferenceScreen; 62 import androidx.preference.TwoStatePreference; 63 import androidx.recyclerview.widget.RecyclerView; 64 65 import com.android.systemui.tv.media.FadingEdgeUtil; 66 import com.android.systemui.tv.res.R; 67 68 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment.SliceFragmentCallback; 69 import com.android.tv.twopanelsettings.slices.EmbeddedSlicePreference; 70 import com.android.tv.twopanelsettings.slices.HasCustomContentDescription; 71 import com.android.tv.twopanelsettings.slices.HasSliceAction; 72 import com.android.tv.twopanelsettings.slices.HasSliceUri; 73 import com.android.tv.twopanelsettings.slices.SettingsPreferenceFragment; 74 import com.android.tv.twopanelsettings.slices.SlicePreference; 75 import com.android.tv.twopanelsettings.slices.SliceRadioPreference; 76 import com.android.tv.twopanelsettings.slices.SliceSeekbarPreference; 77 import com.android.tv.twopanelsettings.slices.SlicesConstants; 78 import com.android.tv.twopanelsettings.slices.ContextSingleton; 79 import com.android.tv.twopanelsettings.slices.compat.Slice; 80 import com.android.tv.twopanelsettings.slices.compat.SliceItem; 81 import com.android.tv.twopanelsettings.slices.compat.widget.ListContent; 82 import com.android.tv.twopanelsettings.slices.compat.widget.SliceContent; 83 84 import java.util.ArrayList; 85 import java.util.HashMap; 86 import java.util.IdentityHashMap; 87 import java.util.List; 88 import java.util.Map; 89 import java.util.Objects; 90 91 /** 92 * A screen presenting a slice in TV settings. 93 * Forked from {@link com.android.tv.twopanelsettings.slices.SliceFragment}. 94 */ 95 @Keep 96 public class SliceFragment extends SettingsPreferenceFragment implements Observer<Slice>, 97 SliceFragmentCallback, PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { 98 99 private static final String TAG = "SliceFragment"; 100 private static final boolean DEBUG = false; 101 102 public static final String TAG_SCREEN_SUBTITLE = "TAG_SCREEN_SUBTITLE"; 103 104 // Keys for saving state 105 private static final String KEY_PREFERENCE_FOLLOWUP_INTENT = "key_preference_followup_intent"; 106 private static final String KEY_PREFERENCE_FOLLOWUP_RESULT_CODE = 107 "key_preference_followup_result_code"; 108 private static final String KEY_SCREEN_TITLE = "key_screen_title"; 109 private static final String KEY_SCREEN_SUBTITLE = "key_screen_subtitle"; 110 private static final String KEY_LAST_PREFERENCE = "key_last_preference"; 111 private static final String KEY_URI_STRING = "key_uri_string"; 112 113 private Slice mSlice; 114 private String mUriString = null; 115 private int mCurrentPageId; 116 private CharSequence mScreenTitle; 117 private CharSequence mScreenSubtitle; 118 private PendingIntent mPreferenceFollowupIntent; 119 private int mFollowupPendingIntentResultCode; 120 private Intent mFollowupPendingIntentExtras; 121 private Intent mFollowupPendingIntentExtrasCopy; 122 private String mLastFocusedPreferenceKey; 123 124 private final ActivityResultLauncher<IntentSenderRequest> mActivityResultLauncher = 125 registerForActivityResult(new ActivityResultContracts.StartIntentSenderForResult(), 126 new ActivityResultCallback<>() { 127 @Override 128 public void onActivityResult(ActivityResult result) { 129 Intent data = result.getData(); 130 mFollowupPendingIntentExtras = data; 131 mFollowupPendingIntentExtrasCopy = data == null ? null : new Intent( 132 data); 133 mFollowupPendingIntentResultCode = result.getResultCode(); 134 } 135 }); 136 private final ContentObserver mContentObserver = new ContentObserver(new Handler()) { 137 @Override 138 public void onChange(boolean selfChange, Uri uri) { 139 handleUri(uri); 140 super.onChange(selfChange, uri); 141 } 142 }; 143 144 @Override onCreate(Bundle savedInstanceState)145 public void onCreate(Bundle savedInstanceState) { 146 mUriString = getArguments().getString(SlicesConstants.TAG_TARGET_URI); 147 if (!TextUtils.isEmpty(mUriString)) { 148 ContextSingleton.getInstance().grantFullAccess(getContext(), Uri.parse(mUriString)); 149 } 150 if (TextUtils.isEmpty(mScreenTitle)) { 151 mScreenTitle = getArguments().getCharSequence(SlicesConstants.TAG_SCREEN_TITLE, ""); 152 } 153 if (TextUtils.isEmpty(mScreenSubtitle)) { 154 mScreenSubtitle = getArguments().getCharSequence(TAG_SCREEN_SUBTITLE, ""); 155 } 156 super.onCreate(savedInstanceState); 157 getPreferenceManager().setPreferenceComparisonCallback( 158 new PreferenceManager.SimplePreferenceComparisonCallback() { 159 @Override 160 public boolean arePreferenceContentsTheSame(Preference preference1, 161 Preference preference2) { 162 // Should only check for the default SlicePreference objects, and ignore 163 // other instances of slice reference classes since they all override 164 // Preference.onBindViewHolder(PreferenceViewHolder) 165 return preference1.getClass() == SlicePreference.class 166 && super.arePreferenceContentsTheSame(preference1, preference2); 167 } 168 }); 169 } 170 171 @Override onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)172 public final boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, 173 Preference pref) { 174 if (DEBUG) Log.d(TAG, "onPreferenceStartFragment"); 175 if (pref.getFragment() != null) { 176 if (pref instanceof SlicePreference) { 177 SlicePreference slicePref = (SlicePreference) pref; 178 if (slicePref.getUri() == null || !isUriValid(slicePref.getUri())) { 179 return false; 180 } 181 Bundle b = pref.getExtras(); 182 b.putString(SlicesConstants.TAG_TARGET_URI, slicePref.getUri()); 183 b.putCharSequence(SlicesConstants.TAG_SCREEN_TITLE, slicePref.getTitle()); 184 if (DEBUG) Log.d(TAG, "TAG_TARGET_URI: " + slicePref.getUri() 185 + ", TAG_SCREEN_TITLE: " + slicePref.getTitle()); 186 } 187 } 188 final Fragment f = 189 Fragment.instantiate(getActivity(), pref.getFragment(), pref.getExtras()); 190 f.setTargetFragment(caller, 0); 191 if (f instanceof PreferenceFragmentCompat || f instanceof PreferenceDialogFragmentCompat) { 192 startPreferenceFragment(f); 193 } 194 return true; 195 } 196 startPreferenceFragment(@onNull Fragment fragment)197 public void startPreferenceFragment(@NonNull Fragment fragment) { 198 if (DEBUG) Log.d(TAG, "startPreferenceFragment"); 199 200 getParentFragmentManager().beginTransaction() 201 .replace(R.id.media_output_fragment, fragment) 202 .addToBackStack(null) 203 .commit(); 204 } 205 206 @Override onResume()207 public void onResume() { 208 this.setTitle(mScreenTitle); 209 this.setSubtitle(mScreenSubtitle); 210 211 showProgressBar(); 212 if (!TextUtils.isEmpty(mUriString)) { 213 ContextSingleton.getInstance() 214 .addSliceObserver(getActivity(), Uri.parse(mUriString), this); 215 } 216 217 super.onResume(); 218 if (!TextUtils.isEmpty(mUriString)) { 219 getContext().getContentResolver().registerContentObserver( 220 SlicePreferencesUtil.getStatusPath(mUriString), false, mContentObserver); 221 } 222 fireFollowupPendingIntent(); 223 } 224 fireFollowupPendingIntent()225 private void fireFollowupPendingIntent() { 226 if (mFollowupPendingIntentExtras == null) { 227 return; 228 } 229 // If there is followup pendingIntent returned from initial activity, send it. 230 // Otherwise send the followup pendingIntent provided by slice api. 231 Parcelable followupPendingIntent; 232 try { 233 followupPendingIntent = mFollowupPendingIntentExtrasCopy.getParcelableExtra( 234 EXTRA_SLICE_FOLLOWUP); 235 } catch (Throwable ex) { 236 // unable to parse, the Intent has custom Parcelable, fallback 237 followupPendingIntent = null; 238 } 239 if (followupPendingIntent instanceof PendingIntent) { 240 try { 241 ((PendingIntent) followupPendingIntent).send(); 242 } catch (CanceledException e) { 243 Log.e(TAG, "Followup PendingIntent for slice cannot be sent", e); 244 } 245 } else { 246 if (mPreferenceFollowupIntent == null) { 247 return; 248 } 249 try { 250 mPreferenceFollowupIntent.send(getContext(), 251 mFollowupPendingIntentResultCode, mFollowupPendingIntentExtras); 252 } catch (CanceledException e) { 253 Log.e(TAG, "Followup PendingIntent for slice cannot be sent", e); 254 } 255 mPreferenceFollowupIntent = null; 256 } 257 } 258 259 @Override onPause()260 public void onPause() { 261 super.onPause(); 262 hideProgressBar(); 263 getContext().getContentResolver().unregisterContentObserver(mContentObserver); 264 if (!TextUtils.isEmpty(mUriString)) { 265 ContextSingleton.getInstance() 266 .removeSliceObserver(getActivity(), Uri.parse(mUriString), this); 267 } 268 } 269 270 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)271 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 272 PreferenceScreen preferenceScreen = getPreferenceManager() 273 .createPreferenceScreen(getContext()); 274 setPreferenceScreen(preferenceScreen); 275 } 276 isUriValid(String uri)277 private boolean isUriValid(String uri) { 278 if (uri == null) { 279 return false; 280 } 281 ContentProviderClient client = 282 getContext().getContentResolver().acquireContentProviderClient(Uri.parse(uri)); 283 if (client != null) { 284 client.close(); 285 return true; 286 } else { 287 return false; 288 } 289 } 290 update()291 private void update() { 292 PreferenceScreen preferenceScreen = 293 getPreferenceManager().getPreferenceScreen(); 294 295 if (preferenceScreen == null) { 296 return; 297 } 298 299 List<SliceContent> items = new ListContent(mSlice).getRowItems(); 300 if (items.isEmpty()) { 301 return; 302 } 303 304 SliceItem redirectSliceItem = SlicePreferencesUtil.getRedirectSlice(items); 305 String redirectSlice = null; 306 if (redirectSliceItem != null) { 307 SlicePreferencesUtil.Data data = SlicePreferencesUtil.extract(redirectSliceItem); 308 CharSequence title = SlicePreferencesUtil.getText(data.mTitleItem); 309 if (!TextUtils.isEmpty(title)) { 310 redirectSlice = title.toString(); 311 } 312 } 313 if (isUriValid(redirectSlice)) { 314 ContextSingleton.getInstance() 315 .removeSliceObserver(getActivity(), Uri.parse(mUriString), this); 316 getContext().getContentResolver().unregisterContentObserver(mContentObserver); 317 mUriString = redirectSlice; 318 ContextSingleton.getInstance() 319 .addSliceObserver(getActivity(), Uri.parse(mUriString), this); 320 getContext().getContentResolver().registerContentObserver( 321 SlicePreferencesUtil.getStatusPath(mUriString), false, mContentObserver); 322 } 323 324 SliceItem screenTitleItem = SlicePreferencesUtil.getScreenTitleItem(items); 325 if (screenTitleItem == null) { 326 setTitle(mScreenTitle); 327 setSubtitle(mScreenSubtitle); 328 } else { 329 SlicePreferencesUtil.Data data = SlicePreferencesUtil.extract(screenTitleItem); 330 mCurrentPageId = SlicePreferencesUtil.getPageId(screenTitleItem); 331 CharSequence title = SlicePreferencesUtil.getText(data.mTitleItem); 332 if (!TextUtils.isEmpty(title)) { 333 mScreenTitle = title; 334 } 335 setTitle(mScreenTitle); 336 337 CharSequence subtitle = SlicePreferencesUtil.getText(data.mSubtitleItem); 338 if (!TextUtils.isEmpty(subtitle)) { 339 mScreenSubtitle = subtitle; 340 } 341 setSubtitle(subtitle); 342 } 343 344 SliceItem focusedPrefItem = SlicePreferencesUtil.getFocusedPreferenceItem(items); 345 CharSequence defaultFocusedKey = null; 346 if (focusedPrefItem != null) { 347 SlicePreferencesUtil.Data data = SlicePreferencesUtil.extract(focusedPrefItem); 348 CharSequence title = SlicePreferencesUtil.getText(data.mTitleItem); 349 if (!TextUtils.isEmpty(title)) { 350 defaultFocusedKey = title; 351 } 352 } 353 354 List<Preference> newPrefs = new ArrayList<>(); 355 for (SliceContent contentItem : items) { 356 SliceItem item = contentItem.getSliceItem(); 357 if (SlicesConstants.TYPE_PREFERENCE.equals(item.getSubType()) 358 || SlicesConstants.TYPE_PREFERENCE_CATEGORY.equals(item.getSubType()) 359 || SlicesConstants.TYPE_PREFERENCE_EMBEDDED_PLACEHOLDER.equals( 360 item.getSubType())) { 361 Preference preference = 362 SlicePreferencesUtil.getPreference( 363 item, getContext(), getClass().getCanonicalName()); 364 if (preference != null) { 365 // Listen to changes of the seekbar. 366 if (preference instanceof SeekbarSlicePreference) { 367 SeekbarSlicePreference seekbarPreference = 368 (SeekbarSlicePreference) preference; 369 seekbarPreference.setOnSeekbarChangedListener( 370 new SeekBar.OnSeekBarChangeListener() { 371 @Override 372 public void onProgressChanged(SeekBar seekBar, int progress, 373 boolean fromUser) { 374 onSeekbarPreferenceValueChanged(seekbarPreference, 375 progress); 376 } 377 378 @Override 379 public void onStartTrackingTouch(SeekBar seekBar) { 380 // NOOP 381 } 382 383 @Override 384 public void onStopTrackingTouch(SeekBar seekBar) { 385 // NOOP 386 } 387 }); 388 } 389 newPrefs.add(preference); 390 } 391 } 392 } 393 updatePreferenceScreen(preferenceScreen, newPrefs); 394 if (defaultFocusedKey != null) { 395 scrollToPreference(defaultFocusedKey.toString()); 396 } else if (mLastFocusedPreferenceKey != null) { 397 scrollToPreference(mLastFocusedPreferenceKey); 398 } 399 } 400 back()401 private void back() { 402 if (DEBUG) Log.d(TAG, "back"); 403 getParentFragmentManager().popBackStack(); 404 } 405 updatePreferenceScreen(PreferenceScreen screen, List<Preference> newPrefs)406 private void updatePreferenceScreen(PreferenceScreen screen, List<Preference> newPrefs) { 407 // Remove all the preferences in the screen that satisfy such three cases: 408 // (a) Preference without key 409 // (b) Preference with key which does not appear in the new list. 410 // (c) Preference with key which does appear in the new list, but the preference has changed 411 // ability to handle slices and needs to be replaced instead of re-used. 412 int index = 0; 413 IdentityHashMap<Preference, Preference> newToOld = new IdentityHashMap<>(); 414 while (index < screen.getPreferenceCount()) { 415 boolean needToRemoveCurrentPref = true; 416 Preference oldPref = screen.getPreference(index); 417 for (Preference newPref : newPrefs) { 418 if (isSamePreference(oldPref, newPref)) { 419 needToRemoveCurrentPref = false; 420 newToOld.put(newPref, oldPref); 421 break; 422 } 423 } 424 425 if (needToRemoveCurrentPref) { 426 screen.removePreference(oldPref); 427 } else { 428 index++; 429 } 430 } 431 432 Map<Integer, Boolean> twoStatePreferenceIsCheckedByOrder = new HashMap<>(); 433 for (int i = 0; i < newPrefs.size(); i++) { 434 if (newPrefs.get(i) instanceof TwoStatePreference) { 435 twoStatePreferenceIsCheckedByOrder.put( 436 i, ((TwoStatePreference) newPrefs.get(i)).isChecked()); 437 } 438 } 439 440 //Iterate the new preferences list and give each preference a correct order 441 for (int i = 0; i < newPrefs.size(); i++) { 442 Preference newPref = newPrefs.get(i); 443 // If the newPref has a key and has a corresponding old preference, update the old 444 // preference and give it a new order. 445 446 Preference oldPref = newToOld.get(newPref); 447 if (oldPref == null) { 448 newPref.setOrder(i); 449 screen.addPreference(newPref); 450 continue; 451 } 452 453 oldPref.setOrder(i); 454 if (oldPref instanceof EmbeddedSlicePreference) { 455 // EmbeddedSlicePreference has its own slice observer 456 // (EmbeddedSlicePreferenceHelper). Should therefore not be updated by 457 // slice observer in SliceFragment. 458 // The order will however still need to be updated, as this can not be handled 459 // by EmbeddedSlicePreferenceHelper. 460 continue; 461 } 462 463 oldPref.setTitle(newPref.getTitle()); 464 oldPref.setSummary(newPref.getSummary()); 465 oldPref.setEnabled(newPref.isEnabled()); 466 oldPref.setSelectable(newPref.isSelectable()); 467 oldPref.setFragment(newPref.getFragment()); 468 oldPref.getExtras().putAll(newPref.getExtras()); 469 if ((oldPref instanceof HasSliceAction) 470 && (newPref instanceof HasSliceAction)) { 471 ((HasSliceAction) oldPref) 472 .setSliceAction( 473 ((HasSliceAction) newPref).getSliceAction()); 474 } 475 if ((oldPref instanceof HasSliceUri) 476 && (newPref instanceof HasSliceUri)) { 477 ((HasSliceUri) oldPref) 478 .setUri(((HasSliceUri) newPref).getUri()); 479 } 480 if ((oldPref instanceof HasCustomContentDescription) 481 && (newPref instanceof HasCustomContentDescription)) { 482 ((HasCustomContentDescription) oldPref).setContentDescription( 483 ((HasCustomContentDescription) newPref) 484 .getContentDescription()); 485 } 486 } 487 488 //addPreference will reset the checked status of TwoStatePreference. 489 //So we need to add them back 490 for (int i = 0; i < screen.getPreferenceCount(); i++) { 491 Preference screenPref = screen.getPreference(i); 492 if (screenPref instanceof TwoStatePreference 493 && twoStatePreferenceIsCheckedByOrder.get(screenPref.getOrder()) != null) { 494 ((TwoStatePreference) screenPref) 495 .setChecked(twoStatePreferenceIsCheckedByOrder.get(screenPref.getOrder())); 496 } 497 } 498 } 499 isSamePreference(Preference oldPref, Preference newPref)500 private static boolean isSamePreference(Preference oldPref, Preference newPref) { 501 if (oldPref == null || newPref == null) { 502 return false; 503 } 504 505 if (newPref instanceof HasSliceUri != oldPref instanceof HasSliceUri) { 506 return false; 507 } 508 509 if (newPref instanceof EmbeddedSlicePreference) { 510 return oldPref instanceof EmbeddedSlicePreference 511 && Objects.equals(((EmbeddedSlicePreference) newPref).getUri(), 512 ((EmbeddedSlicePreference) oldPref).getUri()); 513 } else if (oldPref instanceof EmbeddedSlicePreference) { 514 return false; 515 } 516 517 return newPref.getKey() != null && newPref.getKey().equals(oldPref.getKey()); 518 } 519 520 @Override onPreferenceFocused(Preference preference)521 public void onPreferenceFocused(Preference preference) { 522 setLastFocused(preference); 523 } 524 525 @Override onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue)526 public void onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue) { 527 if (DEBUG) Log.d(TAG, "onSeekbarPreferenceChanged, addValue: " + addValue); 528 int curValue = preference.getValue(); 529 onSeekbarPreferenceValueChanged(preference, curValue); 530 } 531 onSeekbarPreferenceValueChanged(SliceSeekbarPreference preference, int newValue)532 public void onSeekbarPreferenceValueChanged(SliceSeekbarPreference preference, int newValue) { 533 if (DEBUG) Log.d(TAG, "onSeekbarPreferenceChanged, newValue: " + newValue); 534 535 try { 536 Intent fillInIntent = 537 new Intent() 538 .putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()) 539 .putExtra(SlicesConstants.SUBTYPE_SEEKBAR_VALUE, newValue); 540 firePendingIntent(preference, fillInIntent); 541 } catch (Exception e) { 542 Log.e(TAG, "PendingIntent for slice cannot be sent", e); 543 } 544 } 545 546 @Override onPreferenceTreeClick(Preference preference)547 public boolean onPreferenceTreeClick(Preference preference) { 548 if (preference instanceof SliceRadioPreference) { 549 SliceRadioPreference radioPref = (SliceRadioPreference) preference; 550 if (!radioPref.isChecked()) { 551 radioPref.setChecked(true); 552 if (TextUtils.isEmpty(radioPref.getUri())) { 553 return true; 554 } 555 } 556 557 Intent fillInIntent = 558 new Intent().putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()); 559 boolean result = firePendingIntent(radioPref, fillInIntent); 560 radioPref.clearOtherRadioPreferences(getPreferenceScreen()); 561 if (result) { 562 return true; 563 } 564 } else if (preference instanceof TwoStatePreference 565 && preference instanceof HasSliceAction) { 566 boolean isChecked = ((TwoStatePreference) preference).isChecked(); 567 preference.getExtras().putBoolean(EXTRA_PREFERENCE_INFO_STATUS, isChecked); 568 Intent fillInIntent = 569 new Intent() 570 .putExtra(EXTRA_TOGGLE_STATE, isChecked) 571 .putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()); 572 if (firePendingIntent((HasSliceAction) preference, fillInIntent)) { 573 return true; 574 } 575 return true; 576 } else if (preference instanceof SlicePreference) { 577 Intent fillInIntent = 578 new Intent().putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()); 579 if (firePendingIntent((HasSliceAction) preference, fillInIntent)) { 580 return true; 581 } 582 } 583 584 return super.onPreferenceTreeClick(preference); 585 } 586 firePendingIntent(@onNull HasSliceAction preference, Intent fillInIntent)587 private boolean firePendingIntent(@NonNull HasSliceAction preference, Intent fillInIntent) { 588 if (preference.getSliceAction() == null) { 589 return false; 590 } 591 IntentSender intentSender = preference.getSliceAction().getAction().getIntentSender(); 592 mActivityResultLauncher.launch( 593 new IntentSenderRequest.Builder(intentSender).setFillInIntent( 594 fillInIntent).build()); 595 if (preference.getFollowupSliceAction() != null) { 596 mPreferenceFollowupIntent = preference.getFollowupSliceAction().getAction(); 597 } 598 599 return true; 600 } 601 602 @Override onSaveInstanceState(Bundle outState)603 public void onSaveInstanceState(Bundle outState) { 604 super.onSaveInstanceState(outState); 605 outState.putParcelable(KEY_PREFERENCE_FOLLOWUP_INTENT, mPreferenceFollowupIntent); 606 outState.putInt(KEY_PREFERENCE_FOLLOWUP_RESULT_CODE, mFollowupPendingIntentResultCode); 607 outState.putCharSequence(KEY_SCREEN_TITLE, mScreenTitle); 608 outState.putCharSequence(KEY_SCREEN_SUBTITLE, mScreenSubtitle); 609 outState.putString(KEY_LAST_PREFERENCE, mLastFocusedPreferenceKey); 610 outState.putString(KEY_URI_STRING, mUriString); 611 } 612 613 @Override onActivityCreated(Bundle savedInstanceState)614 public void onActivityCreated(Bundle savedInstanceState) { 615 super.onActivityCreated(savedInstanceState); 616 if (savedInstanceState != null) { 617 mPreferenceFollowupIntent = 618 savedInstanceState.getParcelable(KEY_PREFERENCE_FOLLOWUP_INTENT); 619 mFollowupPendingIntentResultCode = 620 savedInstanceState.getInt(KEY_PREFERENCE_FOLLOWUP_RESULT_CODE); 621 mScreenTitle = savedInstanceState.getCharSequence(KEY_SCREEN_TITLE); 622 mScreenSubtitle = savedInstanceState.getCharSequence(KEY_SCREEN_SUBTITLE); 623 mLastFocusedPreferenceKey = savedInstanceState.getString(KEY_LAST_PREFERENCE); 624 mUriString = savedInstanceState.getString(KEY_URI_STRING); 625 } 626 } 627 628 @Override onChanged(Slice slice)629 public void onChanged(Slice slice) { 630 mSlice = slice; 631 // Make TvSettings guard against the case that slice provider is not set up correctly 632 if (slice == null || slice.getHints() == null) { 633 return; 634 } 635 636 if (slice.getHints().contains(HINT_PARTIAL)) { 637 showProgressBar(); 638 } else { 639 hideProgressBar(); 640 } 641 update(); 642 } 643 showProgressBar()644 private void showProgressBar() { 645 View view = this.getView(); 646 View progressBar = view == null ? null : getView().findViewById(R.id.progress_bar); 647 if (progressBar != null) { 648 progressBar.bringToFront(); 649 progressBar.setVisibility(View.VISIBLE); 650 } 651 } 652 hideProgressBar()653 private void hideProgressBar() { 654 View view = this.getView(); 655 View progressBar = view == null ? null : getView().findViewById(R.id.progress_bar); 656 if (progressBar != null) { 657 progressBar.setVisibility(View.GONE); 658 } 659 } 660 setSubtitle(CharSequence subtitle)661 private void setSubtitle(CharSequence subtitle) { 662 View view = this.getView(); 663 if (view == null) { 664 return; 665 } 666 TextView decorSubtitle = view.findViewById(R.id.decor_subtitle); 667 if (decorSubtitle != null) { 668 if (TextUtils.isEmpty(subtitle)) { 669 decorSubtitle.setVisibility(View.GONE); 670 } else { 671 decorSubtitle.setVisibility(View.VISIBLE); 672 decorSubtitle.setText(subtitle); 673 } 674 } 675 mScreenSubtitle = subtitle; 676 } 677 678 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)679 public View onCreateView( 680 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 681 final ViewGroup view = 682 (ViewGroup) super.onCreateView(inflater, container, savedInstanceState); 683 684 LayoutInflater themedInflater = LayoutInflater.from(getContext()); 685 686 final View newTitleContainer = themedInflater.inflate( 687 R.layout.media_output_settings_title, null); 688 if (newTitleContainer != null) { 689 newTitleContainer.setOutlineProvider(null); 690 } 691 view.removeView( 692 view.findViewById(androidx.leanback.preference.R.id.decor_title_container)); 693 view.addView(newTitleContainer, 0); 694 view.setBackgroundResource(android.R.color.transparent); 695 696 RecyclerView recyclerView = view.findViewById(androidx.leanback.preference.R.id.list); 697 if (recyclerView != null) { 698 recyclerView.addOnScrollListener( 699 new RecyclerView.OnScrollListener() { 700 @Override 701 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 702 super.onScrolled(recyclerView, dx, dy); 703 Drawable foreground = FadingEdgeUtil.getForegroundDrawable( 704 recyclerView, requireContext()); 705 if (foreground != recyclerView.getForeground()) { 706 recyclerView.setForeground(foreground); 707 } 708 } 709 }); 710 } 711 712 final View newContainer = 713 themedInflater.inflate(R.layout.media_output_settings_progress, null); 714 if (newContainer != null) { 715 ((ViewGroup) newContainer).addView(view); 716 } 717 return newContainer; 718 } 719 setLastFocused(Preference preference)720 public void setLastFocused(Preference preference) { 721 mLastFocusedPreferenceKey = preference.getKey(); 722 } 723 handleUri(Uri uri)724 private void handleUri(Uri uri) { 725 String uriString = uri.getQueryParameter(SlicesConstants.PARAMETER_URI); 726 String errorMessage = uri.getQueryParameter(SlicesConstants.PARAMETER_ERROR); 727 if (DEBUG) Log.d(TAG, "handleUri: " + uri); 728 729 if (errorMessage != null) { 730 Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); 731 } 732 // Provider should provide the correct slice uri in the parameter if it wants to do certain 733 // action(includes go back, forward), otherwise TvSettings would ignore it. 734 if (uriString == null || !uriString.equals(mUriString)) { 735 return; 736 } 737 String direction = uri.getQueryParameter(SlicesConstants.PARAMETER_DIRECTION); 738 if (DEBUG) Log.d(TAG, "direction: " + direction); 739 if (direction != null) { 740 if (direction.equals(SlicesConstants.BACKWARD)) { 741 back(); 742 } else if (direction.equals(SlicesConstants.EXIT)) { 743 finish(); 744 } 745 } 746 } 747 finish()748 private void finish() { 749 if (getActivity() != null) { 750 getActivity().setResult(Activity.RESULT_OK); 751 getActivity().finish(); 752 } 753 } 754 getPreferenceActionId(Preference preference)755 private int getPreferenceActionId(Preference preference) { 756 if (preference instanceof HasSliceAction) { 757 return ((HasSliceAction) preference).getActionId() != 0 758 ? ((HasSliceAction) preference).getActionId() 759 : TvSettingsEnums.ENTRY_DEFAULT; 760 } 761 return TvSettingsEnums.ENTRY_DEFAULT; 762 } 763 764 @Override getPageId()765 protected int getPageId() { 766 return mCurrentPageId != 0 ? mCurrentPageId : TvSettingsEnums.PAGE_SLICE_DEFAULT; 767 } 768 769 @Deprecated getMetricsCategory()770 public int getMetricsCategory() { 771 return 0; 772 } 773 } 774