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