• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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