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