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