• 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;
18 
19 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_SUMMARY;
20 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_TEXT;
21 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_TITLE_ICON;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.AnimatorSet;
26 import android.animation.ArgbEvaluator;
27 import android.animation.ObjectAnimator;
28 import android.content.BroadcastReceiver;
29 import android.content.ContentProviderClient;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentFilter;
33 import android.graphics.drawable.Icon;
34 import android.media.AudioManager;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.provider.Settings;
39 import android.text.TextUtils;
40 import android.transition.Fade;
41 import android.util.Log;
42 import android.view.KeyEvent;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
47 import android.widget.HorizontalScrollView;
48 import android.widget.TextView;
49 
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 import androidx.fragment.app.Fragment;
53 import androidx.fragment.app.FragmentTransaction;
54 import androidx.leanback.app.GuidedStepSupportFragment;
55 import androidx.leanback.preference.LeanbackListPreferenceDialogFragmentCompat;
56 import androidx.leanback.preference.LeanbackPreferenceFragmentCompat;
57 import androidx.leanback.widget.OnChildViewHolderSelectedListener;
58 import androidx.leanback.widget.VerticalGridView;
59 import androidx.preference.ListPreference;
60 import androidx.preference.MultiSelectListPreference;
61 import androidx.preference.Preference;
62 import androidx.preference.PreferenceFragmentCompat;
63 import androidx.preference.PreferenceGroupAdapter;
64 import androidx.preference.PreferenceViewHolder;
65 import androidx.recyclerview.widget.RecyclerView;
66 
67 import com.android.tv.twopanelsettings.slices.CustomContentDescriptionPreference;
68 import com.android.tv.twopanelsettings.slices.HasCustomContentDescription;
69 import com.android.tv.twopanelsettings.slices.HasSliceUri;
70 import com.android.tv.twopanelsettings.slices.InfoFragment;
71 import com.android.tv.twopanelsettings.slices.SliceFragment;
72 import com.android.tv.twopanelsettings.slices.SlicePreference;
73 import com.android.tv.twopanelsettings.slices.SliceSwitchPreference;
74 import com.android.tv.twopanelsettings.slices.SlicesConstants;
75 import com.android.tv.twopanelsettings.slices.SliceSeekbarPreference;
76 
77 import java.util.Set;
78 
79 /**
80  * This fragment provides containers for displaying two {@link LeanbackPreferenceFragmentCompat}.
81  * The preference fragment on the left works as a main panel on which the user can operate.
82  * The preference fragment on the right works as a preview panel for displaying the preview
83  * information.
84  */
85 public abstract class TwoPanelSettingsFragment extends Fragment implements
86         PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
87         PreferenceFragmentCompat.OnPreferenceStartScreenCallback,
88         PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback {
89     private static final String TAG = "TwoPanelSettingsFragment";
90     private static final boolean DEBUG = false;
91     private static final String PREVIEW_FRAGMENT_TAG =
92             "com.android.tv.settings.TwoPanelSettingsFragment.PREVIEW_FRAGMENT";
93     private static final String PREFERENCE_FRAGMENT_TAG =
94             "com.android.tv.settings.TwoPanelSettingsFragment.PREFERENCE_FRAGMENT";
95     private static final String EXTRA_PREF_PANEL_IDX =
96             "com.android.tv.twopanelsettings.PREF_PANEL_IDX";
97     private static final int[] frameResIds =
98             {R.id.frame1, R.id.frame2, R.id.frame3, R.id.frame4, R.id.frame5, R.id.frame6,
99                     R.id.frame7, R.id.frame8, R.id.frame9, R.id.frame10};
100 
101     private static final long PANEL_ANIMATION_MS = 400;
102     private static final long PANEL_ANIMATION_DELAY_MS = 200;
103     private static final long PREVIEW_PANEL_DEFAULT_DELAY_MS = 0;
104     private static final long CHECK_IDLE_STATE_MS = 100;
105     private long mPreviewPanelCreationDelay = 0;
106     private static final float PREVIEW_PANEL_ALPHA = 0.6f;
107 
108     private int mMaxScrollX;
109     private final RootViewOnKeyListener mRootViewOnKeyListener = new RootViewOnKeyListener();
110     private int mPrefPanelIdx;
111     private HorizontalScrollView mScrollView;
112     private Handler mHandler;
113     private boolean mIsNavigatingBack;
114     private boolean mCheckVerticalGridViewScrollState;
115     private Preference mFocusedPreference;
116     private boolean mIsWaitingForUpdatingPreview = false;
117     private AudioManager mAudioManager;
118 
119     private static final String DELAY_MS = "delay_ms";
120     private static final String CHECK_SCROLL_STATE = "check_scroll_state";
121 
122     /** An broadcast receiver to help OEM test best delay for preview panel fragment creation. */
123     private BroadcastReceiver mPreviewPanelDelayReceiver = new BroadcastReceiver() {
124         @Override
125         public void onReceive(Context context, Intent intent) {
126             long delay = intent.getLongExtra(DELAY_MS, PREVIEW_PANEL_DEFAULT_DELAY_MS);
127             boolean checkScrollState = intent.getBooleanExtra(CHECK_SCROLL_STATE, false);
128             Log.d(TAG, "New delay for creating preview panel fragment " + delay
129                     + " check scroll state " + checkScrollState);
130             mPreviewPanelCreationDelay = delay;
131             mCheckVerticalGridViewScrollState = checkScrollState;
132         }
133     };
134 
135     private final OnChildViewHolderSelectedListener mOnChildViewHolderSelectedListener =
136             new OnChildViewHolderSelectedListener() {
137                 @Override
138                 public void onChildViewHolderSelected(RecyclerView parent,
139                         RecyclerView.ViewHolder child, int position, int subposition) {
140                     if (parent == null || child == null) {
141                         return;
142                     }
143                     int adapterPosition = child.getAdapterPosition();
144                     PreferenceGroupAdapter preferenceGroupAdapter =
145                             (PreferenceGroupAdapter) parent.getAdapter();
146                     if (preferenceGroupAdapter != null) {
147                         Preference preference = preferenceGroupAdapter.getItem(adapterPosition);
148                         onPreferenceFocused(preference);
149                     }
150                 }
151 
152                 @Override
153                 public void onChildViewHolderSelectedAndPositioned(RecyclerView parent,
154                         RecyclerView.ViewHolder child, int position, int subposition) {
155                 }
156             };
157 
158     private final OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
159         @Override
160         public void onGlobalLayout() {
161             if (getView() != null && getView().getViewTreeObserver() != null) {
162                 getView().getViewTreeObserver().removeOnGlobalLayoutListener(
163                         mOnGlobalLayoutListener);
164                 moveToPanel(mPrefPanelIdx, false);
165             }
166         }
167     };
168 
169     @Override
onCreate(Bundle savedInstanceState)170     public void onCreate(Bundle savedInstanceState) {
171         super.onCreate(savedInstanceState);
172         mCheckVerticalGridViewScrollState = getContext().getResources()
173                 .getBoolean(R.bool.config_check_scroll_state);
174         mPreviewPanelCreationDelay = getContext().getResources()
175                 .getInteger(R.integer.config_preview_panel_create_delay);
176         mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
177     }
178 
179     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)180     public View onCreateView(LayoutInflater inflater, ViewGroup container,
181             Bundle savedInstanceState) {
182         final View v = inflater.inflate(R.layout.two_panel_settings_fragment, container, false);
183         mScrollView = v.findViewById(R.id.scrollview);
184         mHandler = new Handler();
185         if (savedInstanceState != null) {
186             mPrefPanelIdx = savedInstanceState.getInt(EXTRA_PREF_PANEL_IDX, mPrefPanelIdx);
187             // Move to correct panel once global layout finishes.
188             v.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
189         }
190         mMaxScrollX = computeMaxRightScroll();
191         return v;
192     }
193 
194     @Override
onSaveInstanceState(Bundle outState)195     public void onSaveInstanceState(Bundle outState) {
196         outState.putInt(EXTRA_PREF_PANEL_IDX, mPrefPanelIdx);
197         super.onSaveInstanceState(outState);
198     }
199 
200     @Override
onViewCreated(View view, Bundle savedInstanceState)201     public void onViewCreated(View view, Bundle savedInstanceState) {
202         super.onViewCreated(view, savedInstanceState);
203         if (savedInstanceState == null) {
204             onPreferenceStartInitialScreen();
205         }
206     }
207 
208     /** Extend this method to provide the initial screen **/
onPreferenceStartInitialScreen()209     public abstract void onPreferenceStartInitialScreen();
210 
isPreferenceFragment(String fragment)211     private boolean isPreferenceFragment(String fragment) {
212         try {
213             return LeanbackPreferenceFragmentCompat.class.isAssignableFrom(Class.forName(fragment));
214         } catch (ClassNotFoundException e) {
215             Log.e(TAG, "Fragment class not found " + e);
216             return false;
217         }
218     }
219 
isInfoFragment(String fragment)220     private boolean isInfoFragment(String fragment) {
221         try {
222             return InfoFragment.class.isAssignableFrom(Class.forName(fragment));
223         } catch (ClassNotFoundException e) {
224             Log.e(TAG, "Fragment class not found " + e);
225             return false;
226         }
227     }
228 
229     @Override
onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)230     public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
231         if (pref == null) {
232             return false;
233         }
234         if (DEBUG) {
235             Log.d(TAG, "onPreferenceStartFragment " + pref.getTitle());
236         }
237         if (pref.getFragment() == null) {
238             return false;
239         }
240         Fragment preview = getChildFragmentManager().findFragmentById(
241                 frameResIds[mPrefPanelIdx + 1]);
242         if (preview != null && !(preview instanceof DummyFragment)) {
243             if (!(preview instanceof InfoFragment)) {
244                 if (!mIsWaitingForUpdatingPreview) {
245                     navigateToPreviewFragment();
246                 }
247             }
248         } else {
249             // If there is no corresponding slice provider, thus the corresponding fragment is not
250             // created, return false to check the intent of the SlicePreference.
251             if (pref instanceof SlicePreference) {
252                 return false;
253             }
254             try {
255                 Fragment immersiveFragment = Fragment.instantiate(getActivity(), pref.getFragment(),
256                         pref.getExtras());
257                 startImmersiveFragment(immersiveFragment);
258             } catch (Exception e) {
259                 Log.e(TAG, "error trying to instantiate fragment " + e);
260                 // return true so it won't be handled by onPreferenceTreeClick in PreferenceFragment
261                 return true;
262             }
263         }
264         return true;
265     }
266 
267     /** Navigate back to the previous fragment **/
navigateBack()268     public void navigateBack() {
269         back(false);
270     }
271 
272     /** Navigate into current preview fragment */
navigateToPreviewFragment()273     public void navigateToPreviewFragment() {
274         Fragment previewFragment = getChildFragmentManager().findFragmentById(
275                 frameResIds[mPrefPanelIdx + 1]);
276         if (previewFragment instanceof NavigationCallback) {
277             ((NavigationCallback) previewFragment).onNavigateToPreview();
278         }
279         if (previewFragment == null || previewFragment instanceof DummyFragment) {
280             return;
281         }
282         if (DEBUG) {
283             Log.d(TAG, "navigateToPreviewFragment");
284         }
285         if (mPrefPanelIdx + 1 >= frameResIds.length) {
286             Log.w(TAG, "Maximum level of depth reached.");
287             return;
288         }
289         Fragment initialPreviewFragment = getInitialPreviewFragment(previewFragment);
290         if (initialPreviewFragment == null) {
291             initialPreviewFragment = new DummyFragment();
292         }
293         initialPreviewFragment.setExitTransition(null);
294 
295         if (previewFragment.getView() != null) {
296             previewFragment.getView().setImportantForAccessibility(
297                     View.IMPORTANT_FOR_ACCESSIBILITY_YES);
298         }
299 
300         mPrefPanelIdx++;
301 
302         Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
303         addOrRemovePreferenceFocusedListener(fragment, true);
304 
305         final FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
306         transaction.replace(frameResIds[mPrefPanelIdx + 1], initialPreviewFragment,
307                 PREVIEW_FRAGMENT_TAG);
308         transaction.commit();
309 
310         moveToPanel(mPrefPanelIdx, true);
311         removeFragmentAndAddToBackStack(mPrefPanelIdx - 1);
312     }
313 
isA11yOn()314     private boolean isA11yOn() {
315         return Settings.Secure.getInt(
316                 getActivity().getContentResolver(),
317                 Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
318     }
319 
updateAccessibilityTitle(Fragment fragment)320     private void updateAccessibilityTitle(Fragment fragment) {
321         CharSequence newA11yTitle = "";
322         if (fragment instanceof SliceFragment) {
323             newA11yTitle = ((SliceFragment) fragment).getScreenTitle();
324         } else if (fragment instanceof LeanbackPreferenceFragmentCompat) {
325             newA11yTitle = ((LeanbackPreferenceFragmentCompat) fragment).getPreferenceScreen()
326                     .getTitle();
327         } else if (fragment instanceof GuidedStepSupportFragment) {
328             if (fragment.getView() != null) {
329                 View titleView = fragment.getView().findViewById(R.id.guidance_title);
330                 if (titleView instanceof TextView) {
331                     newA11yTitle = ((TextView) titleView).getText();
332                 }
333             }
334         }
335 
336         if (!TextUtils.isEmpty(newA11yTitle)) {
337             if (DEBUG) {
338                 Log.d(TAG, "changing a11y title to: " + newA11yTitle);
339             }
340 
341             // Set both window title and pane title to avoid messy announcements when coming from
342             // other activities. (window title is announced on activity change)
343             getActivity().getWindow().setTitle(newA11yTitle);
344             if (getView() != null
345                     && getView().findViewById(R.id.two_panel_fragment_container) != null) {
346                 getView().findViewById(R.id.two_panel_fragment_container)
347                         .setAccessibilityPaneTitle(newA11yTitle);
348             }
349         }
350     }
351 
addOrRemovePreferenceFocusedListener(Fragment fragment, boolean isAddingListener)352     private void addOrRemovePreferenceFocusedListener(Fragment fragment, boolean isAddingListener) {
353         if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) {
354             return;
355         }
356         LeanbackPreferenceFragmentCompat leanbackPreferenceFragment =
357                 (LeanbackPreferenceFragmentCompat) fragment;
358         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
359         if (listView != null) {
360             if (isAddingListener) {
361                 listView.setOnChildViewHolderSelectedListener(mOnChildViewHolderSelectedListener);
362             } else {
363                 listView.setOnChildViewHolderSelectedListener(null);
364             }
365         }
366     }
367 
368     /**
369      * Displays left panel preference fragment to the user.
370      *
371      * @param fragment Fragment instance to be added.
372      */
startPreferenceFragment(@onNull Fragment fragment)373     public void startPreferenceFragment(@NonNull Fragment fragment) {
374         if (DEBUG) {
375             Log.d(TAG, "startPreferenceFragment");
376         }
377         FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
378         transaction.add(frameResIds[mPrefPanelIdx], fragment, PREFERENCE_FRAGMENT_TAG);
379         transaction.commitNow();
380 
381         Fragment initialPreviewFragment = getInitialPreviewFragment(fragment);
382         if (initialPreviewFragment == null) {
383             initialPreviewFragment = new DummyFragment();
384         }
385         initialPreviewFragment.setExitTransition(null);
386 
387         transaction = getChildFragmentManager().beginTransaction();
388         transaction.add(frameResIds[mPrefPanelIdx + 1], initialPreviewFragment,
389                 initialPreviewFragment.getClass().toString());
390         transaction.commit();
391     }
392 
393     @Override
onPreferenceDisplayDialog( @onNull PreferenceFragmentCompat caller, Preference pref)394     public boolean onPreferenceDisplayDialog(
395             @NonNull PreferenceFragmentCompat caller, Preference pref) {
396         if (pref == null) {
397             return false;
398         }
399         if (DEBUG) {
400             Log.d(TAG, "PreferenceDisplayDialog");
401         }
402         if (caller == null) {
403             throw new IllegalArgumentException("Cannot display dialog for preference " + pref
404                     + ", Caller must not be null!");
405         }
406         Fragment preview = getChildFragmentManager().findFragmentById(
407                 frameResIds[mPrefPanelIdx + 1]);
408         if (preview != null && !(preview instanceof DummyFragment)) {
409             if (preview instanceof NavigationCallback) {
410                 ((NavigationCallback) preview).onNavigateToPreview();
411             }
412             mPrefPanelIdx++;
413             moveToPanel(mPrefPanelIdx, true);
414             removeFragmentAndAddToBackStack(mPrefPanelIdx - 1);
415             return true;
416         }
417         return false;
418     }
419 
equalArguments(Bundle a, Bundle b)420     private boolean equalArguments(Bundle a, Bundle b) {
421         if (a == null && b == null) {
422             return true;
423         }
424         if (a == null || b == null) {
425             return false;
426         }
427         Set<String> aks = a.keySet();
428         Set<String> bks = b.keySet();
429         if (a.size() != b.size()) {
430             return false;
431         }
432         if (!aks.containsAll(bks)) {
433             return false;
434         }
435         for (String key : aks) {
436             if (a.get(key) == null && b.get(key) == null) {
437                 continue;
438             }
439             if (a.get(key) == null || b.get(key) == null) {
440                 return false;
441             }
442             if (!a.get(key).equals(b.get(key))) {
443                 return false;
444             }
445         }
446         return true;
447     }
448 
449     /** Callback from SliceFragment **/
450     public interface SliceFragmentCallback {
451         /** Triggered when preference is focused **/
onPreferenceFocused(Preference preference)452         void onPreferenceFocused(Preference preference);
453 
454         /** Triggered when Seekbar preference is changed **/
onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue)455         void onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue);
456     }
457 
onPreferenceFocused(Preference pref)458     protected void onPreferenceFocused(Preference pref) {
459         onPreferenceFocusedImpl(pref, false);
460     }
461 
onPreferenceFocusedImpl(Preference pref, boolean forceRefresh)462     private void onPreferenceFocusedImpl(Preference pref, boolean forceRefresh) {
463         if (pref == null) {
464             return;
465         }
466         if (DEBUG) {
467             Log.d(TAG, "onPreferenceFocused " + pref.getTitle());
468         }
469         final Fragment prefFragment =
470                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
471         if (prefFragment instanceof SliceFragmentCallback) {
472             ((SliceFragmentCallback) prefFragment).onPreferenceFocused(pref);
473         }
474         mFocusedPreference = pref;
475         if (mCheckVerticalGridViewScrollState || mPreviewPanelCreationDelay > 0) {
476             mIsWaitingForUpdatingPreview = true;
477             VerticalGridView listView = (VerticalGridView)
478                     ((LeanbackPreferenceFragmentCompat) prefFragment).getListView();
479             mHandler.postDelayed(new PostShowPreviewRunnable(
480                     listView, pref, forceRefresh), mPreviewPanelCreationDelay);
481         } else {
482             handleFragmentTransactionWhenFocused(pref, forceRefresh);
483         }
484     }
485 
486     private final class PostShowPreviewRunnable implements Runnable {
487         private final VerticalGridView mListView;
488         private final Preference mPref;
489         private final boolean mForceFresh;
490 
PostShowPreviewRunnable(VerticalGridView listView, Preference pref, boolean forceFresh)491         PostShowPreviewRunnable(VerticalGridView listView, Preference pref, boolean forceFresh) {
492             this.mListView = listView;
493             this.mPref = pref;
494             this.mForceFresh = forceFresh;
495         }
496 
497         @Override
run()498         public void run() {
499             if (mPref == mFocusedPreference) {
500                 if (mListView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
501                     mHandler.postDelayed(this, CHECK_IDLE_STATE_MS);
502                 } else {
503                     handleFragmentTransactionWhenFocused(mPref, mForceFresh);
504                     mIsWaitingForUpdatingPreview = false;
505                 }
506             }
507         }
508     }
509 
handleFragmentTransactionWhenFocused(Preference pref, boolean forceRefresh)510     private void handleFragmentTransactionWhenFocused(Preference pref, boolean forceRefresh) {
511         Fragment previewFragment = null;
512         final Fragment prefFragment =
513                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
514         try {
515             previewFragment = onCreatePreviewFragment(prefFragment, pref);
516         } catch (Exception e) {
517             Log.w(TAG, "Cannot instantiate the fragment from preference: " + pref, e);
518         }
519         if (previewFragment == null) {
520             previewFragment = new DummyFragment();
521         }
522         final Fragment existingPreviewFragment =
523                 getChildFragmentManager().findFragmentById(
524                         frameResIds[mPrefPanelIdx + 1]);
525         if (existingPreviewFragment != null
526                 && existingPreviewFragment.getClass().equals(previewFragment.getClass())
527                 && equalArguments(existingPreviewFragment.getArguments(),
528                 previewFragment.getArguments())) {
529             if (isRTL() && mScrollView.getScrollX() == 0 && mPrefPanelIdx == 0
530                     && getView() != null && getView().getViewTreeObserver() != null) {
531                 // For RTL we need to reclaim focus to the correct scroll position if a pref
532                 // launches a new activity because the horizontal scroll goes back to 0.
533                 getView().getViewTreeObserver().addOnGlobalLayoutListener(
534                         mOnGlobalLayoutListener);
535             }
536             if (!forceRefresh) {
537                 return;
538             }
539         }
540 
541         // If the existing preview fragment is recreated when the activity is recreated, the
542         // animation would fall back to "slide left", in this case, we need to set the exit
543         // transition.
544         if (existingPreviewFragment != null) {
545             existingPreviewFragment.setExitTransition(null);
546         }
547         previewFragment.setEnterTransition(new Fade());
548         previewFragment.setExitTransition(null);
549 
550         final FragmentTransaction transaction =
551                 getChildFragmentManager().beginTransaction();
552         transaction.setCustomAnimations(android.R.animator.fade_in,
553                 android.R.animator.fade_out);
554         transaction.replace(frameResIds[mPrefPanelIdx + 1], previewFragment);
555         transaction.commit();
556 
557         // Some fragments may steal focus on creation. Reclaim focus on main fragment.
558         if (getView()  != null && getView().getViewTreeObserver() != null) {
559             getView().getViewTreeObserver().addOnGlobalLayoutListener(
560                     mOnGlobalLayoutListener);
561         }
562     }
563 
onSeekbarPreferenceChanged(SliceSeekbarPreference pref, int addValue)564     private boolean onSeekbarPreferenceChanged(SliceSeekbarPreference pref, int addValue) {
565         final Fragment prefFragment =
566                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
567         if (prefFragment instanceof SliceFragmentCallback) {
568             ((SliceFragmentCallback) prefFragment).onSeekbarPreferenceChanged(pref, addValue);
569         }
570         return true;
571     }
572 
isRTL()573     private boolean isRTL() {
574         return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
575     }
576 
577     @Override
onResume()578     public void onResume() {
579         if (DEBUG) {
580             Log.d(TAG, "onResume");
581         }
582         super.onResume();
583         IntentFilter intentFilter = new IntentFilter();
584         intentFilter.addAction("com.android.tv.settings.PREVIEW_DELAY");
585         getContext().registerReceiver(mPreviewPanelDelayReceiver, intentFilter);
586         // Trap back button presses
587         final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView();
588         if (rootView != null) {
589             rootView.setOnBackKeyListener(mRootViewOnKeyListener);
590         }
591     }
592 
593     @Override
onPause()594     public void onPause() {
595         if (DEBUG) {
596             Log.d(TAG, "onPause");
597         }
598         super.onPause();
599         getContext().unregisterReceiver(mPreviewPanelDelayReceiver);
600         final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView();
601         if (rootView != null) {
602             rootView.setOnBackKeyListener(null);
603         }
604     }
605 
606     /**
607      * Displays a fragment to the user, temporarily replacing the contents of this fragment.
608      *
609      * @param fragment Fragment instance to be added.
610      */
startImmersiveFragment(@onNull Fragment fragment)611     public void startImmersiveFragment(@NonNull Fragment fragment) {
612         if (DEBUG) {
613             Log.d(TAG, "Starting immersive fragment.");
614         }
615         final FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
616         Fragment target = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
617         fragment.setTargetFragment(target, 0);
618         transaction
619                 .add(R.id.two_panel_fragment_container, fragment)
620                 .remove(target)
621                 .addToBackStack(null)
622                 .commit();
623         mHandler.post(() -> {
624             updateAccessibilityTitle(fragment);
625         });
626 
627     }
628 
629     public static class DummyFragment extends Fragment {
630         @Override
631         public @Nullable
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)632         View onCreateView(LayoutInflater inflater, ViewGroup container,
633                 Bundle savedInstanceState) {
634             return inflater.inflate(R.layout.dummy_fragment, container, false);
635         }
636     }
637 
638     /**
639      * Implement this if fragment needs to handle DPAD_LEFT & DPAD_RIGHT itself in some cases
640      **/
641     public interface NavigationCallback {
642 
643         /**
644          * Returns true if the fragment is in the state that can navigate back on receiving a
645          * navigation DPAD key. When true, TwoPanelSettings will initiate a back operation on
646          * receiving a left key. This method doesn't apply to back key: back key always initiates a
647          * back operation.
648          */
canNavigateBackOnDPAD()649         boolean canNavigateBackOnDPAD();
650 
651         /**
652          * Callback when navigating to preview screen
653          */
onNavigateToPreview()654         void onNavigateToPreview();
655 
656         /**
657          * Callback when returning to previous screen
658          */
onNavigateBack()659         void onNavigateBack();
660     }
661 
662     /**
663      * Implement this if the component (typically a Fragment) is preview-able and would like to get
664      * some lifecycle-like callback(s) when the component becomes the main panel.
665      */
666     public interface PreviewableComponentCallback {
667 
668         /**
669          * Lifecycle-like callback when the component becomes main panel from the preview panel. For
670          * Fragment, this will be invoked right after the preview fragment sliding into the main
671          * panel.
672          *
673          * @param forward means whether the component arrives at main panel when users are
674          *                navigating forwards (deeper into the TvSettings tree).
675          */
onArriveAtMainPanel(boolean forward)676         void onArriveAtMainPanel(boolean forward);
677     }
678 
679     private class RootViewOnKeyListener implements View.OnKeyListener {
680 
681         @Override
onKey(View v, int keyCode, KeyEvent event)682         public boolean onKey(View v, int keyCode, KeyEvent event) {
683             Fragment prefFragment =
684                     getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
685 
686             if (event.getAction() == KeyEvent.ACTION_DOWN
687                     && (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
688                     || keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) {
689                 Preference preference = getChosenPreference(prefFragment);
690                 if ((preference instanceof SliceSeekbarPreference)) {
691                     SliceSeekbarPreference sbPref = (SliceSeekbarPreference) preference;
692                     if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
693                         onSeekbarPreferenceChanged(sbPref, 1);
694                     } else {
695                         onSeekbarPreferenceChanged(sbPref, -1);
696                     }
697                     return true;
698                 }
699             }
700 
701             if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {
702                 return back(true);
703             }
704 
705             if (event.getAction() == KeyEvent.ACTION_DOWN
706                     && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT)
707                     || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT))) {
708                 if (prefFragment instanceof NavigationCallback
709                         && !((NavigationCallback) prefFragment).canNavigateBackOnDPAD()) {
710                     return false;
711                 }
712                 return back(false);
713             }
714 
715             if (event.getAction() == KeyEvent.ACTION_DOWN
716                     && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)
717                     || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT))) {
718                 if (shouldPerformClick()) {
719                     v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
720                             KeyEvent.KEYCODE_DPAD_CENTER));
721                     v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
722                             KeyEvent.KEYCODE_DPAD_CENTER));
723                 } else {
724                     Fragment previewFragment = getChildFragmentManager()
725                             .findFragmentById(frameResIds[mPrefPanelIdx + 1]);
726                     if (!(previewFragment instanceof InfoFragment)
727                             && !mIsWaitingForUpdatingPreview) {
728                         mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
729 
730                         navigateToPreviewFragment();
731                     }
732                 }
733                 // TODO(b/163432209): improve NavigationCallback and be more specific here.
734                 // Do not consume the KeyEvent for NavigationCallback classes such as date & time
735                 // picker.
736                 return !(prefFragment instanceof NavigationCallback);
737             }
738             return false;
739         }
740     }
741 
shouldPerformClick()742     private boolean shouldPerformClick() {
743         Fragment prefFragment =
744                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
745         Preference preference = getChosenPreference(prefFragment);
746         if (preference == null) {
747             return false;
748         }
749         // This is for the case when a preference has preview but once user navigate to
750         // see the preview, settings actually launch an intent to start external activity.
751         if (preference.getIntent() != null && !TextUtils.isEmpty(preference.getFragment())) {
752             return true;
753         }
754         return preference instanceof SlicePreference
755                 && ((SlicePreference) preference).getSliceAction() != null
756                 && ((SlicePreference) preference).getUri() != null;
757     }
758 
back(boolean isKeyBackPressed)759     private boolean back(boolean isKeyBackPressed) {
760         if (mIsNavigatingBack) {
761             mHandler.postDelayed(new Runnable() {
762                 @Override
763                 public void run() {
764                     if (DEBUG) {
765                         Log.d(TAG, "Navigating back is deferred.");
766                     }
767                     back(isKeyBackPressed);
768                 }
769             }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS);
770             return true;
771         }
772         if (DEBUG) {
773             Log.d(TAG, "Going back one level.");
774         }
775 
776         final Fragment immersiveFragment =
777                 getChildFragmentManager().findFragmentById(R.id.two_panel_fragment_container);
778         if (immersiveFragment != null) {
779             getChildFragmentManager().popBackStack();
780             moveToPanel(mPrefPanelIdx, false);
781             return true;
782         }
783 
784         // When a11y is on, we allow InfoFragments to take focus without scrolling panels. So if
785         // the user presses back button in this state, we should not scroll our panels back, or exit
786         // Settings activity, but rather reinstate the focus to be on the main panel.
787         Fragment preview =
788                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
789         if (isA11yOn() && preview instanceof InfoFragment && preview.getView() != null
790                 && preview.getView().hasFocus()) {
791             View mainPanelView = getChildFragmentManager()
792                     .findFragmentById(frameResIds[mPrefPanelIdx]).getView();
793             if (mainPanelView != null) {
794                 mainPanelView.requestFocus();
795                 return true;
796             }
797         }
798 
799         if (mPrefPanelIdx < 1) {
800             // Disallow the user to use "dpad left" to finish activity in the first screen
801             if (isKeyBackPressed) {
802                 getActivity().finish();
803             }
804             return true;
805         }
806 
807         mIsNavigatingBack = true;
808         Fragment preferenceFragment =
809                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
810         addOrRemovePreferenceFocusedListener(preferenceFragment, false);
811         getChildFragmentManager().popBackStack();
812 
813         mPrefPanelIdx--;
814 
815         mHandler.postDelayed(() -> {
816             if (isKeyBackPressed) {
817                 mAudioManager.playSoundEffect(AudioManager.FX_BACK);
818             } else {
819                 mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
820             }
821             moveToPanel(mPrefPanelIdx, true);
822         }, PANEL_ANIMATION_DELAY_MS);
823 
824         mHandler.postDelayed(() -> {
825             removeFragment(mPrefPanelIdx + 2);
826             mIsNavigatingBack = false;
827             Fragment previewFragment =
828                     getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
829             if (previewFragment instanceof NavigationCallback) {
830                 ((NavigationCallback) previewFragment).onNavigateBack();
831             }
832         }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS);
833         return true;
834     }
835 
removeFragment(int index)836     private void removeFragment(int index) {
837         Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[index]);
838         if (fragment != null) {
839             getChildFragmentManager().beginTransaction().remove(fragment).commit();
840         }
841     }
842 
removeFragmentAndAddToBackStack(int index)843     private void removeFragmentAndAddToBackStack(int index) {
844         if (index < 0) {
845             return;
846         }
847         Fragment removePanel = getChildFragmentManager().findFragmentById(frameResIds[index]);
848         if (removePanel != null) {
849             removePanel.setExitTransition(new Fade());
850             getChildFragmentManager().beginTransaction().remove(removePanel)
851                     .addToBackStack("remove " + removePanel.getClass().getName()).commit();
852         }
853     }
854 
855     /** For RTL layout, we need to know the right edge from where the panels start scrolling. */
computeMaxRightScroll()856     private int computeMaxRightScroll() {
857         int scrollViewWidth = getResources().getDimensionPixelSize(R.dimen.tp_settings_panes_width);
858         int panelWidth = getResources().getDimensionPixelSize(
859                 R.dimen.tp_settings_preference_pane_width);
860         int panelPadding = getResources().getDimensionPixelSize(
861                 R.dimen.preference_pane_extra_padding_start) * 2;
862         int result = frameResIds.length * panelWidth - scrollViewWidth + panelPadding;
863         return result < 0 ? 0 : result;
864     }
865 
866     /** Scrolls such that the panel with given index is the main panel shown on the left. */
moveToPanel(final int index, boolean smoothScroll)867     private void moveToPanel(final int index, boolean smoothScroll) {
868         mHandler.post(() -> {
869             if (DEBUG) {
870                 Log.d(TAG, "Moving to panel " + index);
871             }
872             if (!isAdded()) {
873                 return;
874             }
875             Fragment fragmentToBecomeMainPanel =
876                     getChildFragmentManager().findFragmentById(frameResIds[index]);
877             Fragment fragmentToBecomePreviewPanel =
878                     getChildFragmentManager().findFragmentById(frameResIds[index + 1]);
879             // Positive value means that the panel is scrolling to right (navigate forward for LTR
880             // or navigate backwards for RTL) and vice versa; 0 means that this is likely invoked
881             // by GlobalLayoutListener and there's no actual sliding.
882             int distanceToScrollToRight;
883             int panelWidth = getResources().getDimensionPixelSize(
884                     R.dimen.tp_settings_preference_pane_width);
885             View scrollToPanel = getView().findViewById(frameResIds[index]);
886             View previewPanel = getView().findViewById(frameResIds[index + 1]);
887             if (scrollToPanel == null || previewPanel == null) {
888                 return;
889             }
890             View scrollToPanelHead = scrollToPanel.findViewById(R.id.decor_title_container);
891             View previewPanelHead = previewPanel.findViewById(R.id.decor_title_container);
892             boolean scrollsToPreview =
893                     isRTL() ? mScrollView.getScrollX() >= mMaxScrollX - panelWidth * index
894                             : mScrollView.getScrollX() <= panelWidth * index;
895 
896             boolean setAlphaForPreview = fragmentToBecomePreviewPanel != null
897                     && !(fragmentToBecomePreviewPanel instanceof DummyFragment)
898                     && !(fragmentToBecomePreviewPanel instanceof InfoFragment);
899             int previewPanelColor = getResources().getColor(
900                     R.color.tp_preview_panel_background_color);
901             int mainPanelColor = getResources().getColor(
902                     R.color.tp_preference_panel_background_color);
903             if (smoothScroll) {
904                 int animationEnd = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index;
905                 distanceToScrollToRight = animationEnd - mScrollView.getScrollX();
906                 // Slide animation
907                 ObjectAnimator slideAnim = ObjectAnimator.ofInt(mScrollView, "scrollX",
908                         mScrollView.getScrollX(), animationEnd);
909                 slideAnim.setAutoCancel(true);
910                 slideAnim.setDuration(PANEL_ANIMATION_MS);
911                 slideAnim.addListener(new AnimatorListenerAdapter() {
912                     @Override
913                     public void onAnimationEnd(Animator animation) {
914                         super.onAnimationEnd(animation);
915                         if (isA11yOn() && fragmentToBecomeMainPanel != null
916                                 && fragmentToBecomeMainPanel.getView() != null) {
917                             fragmentToBecomeMainPanel.getView().requestFocus();
918                         }
919                     }
920                 });
921                 slideAnim.start();
922                 // Color animation
923                 if (scrollsToPreview) {
924                     previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f);
925                     previewPanel.setBackgroundColor(previewPanelColor);
926                     if (previewPanelHead != null) {
927                         previewPanelHead.setBackgroundColor(previewPanelColor);
928                     }
929                     ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(scrollToPanel, "alpha",
930                             scrollToPanel.getAlpha(), 1f);
931                     ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(scrollToPanel,
932                             "backgroundColor",
933                             new ArgbEvaluator(), previewPanelColor, mainPanelColor);
934                     alphaAnim.setAutoCancel(true);
935                     backgroundColorAnim.setAutoCancel(true);
936                     AnimatorSet animatorSet = new AnimatorSet();
937                     if (scrollToPanelHead != null) {
938                         ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject(
939                                 scrollToPanelHead,
940                                 "backgroundColor",
941                                 new ArgbEvaluator(), previewPanelColor, mainPanelColor);
942                         backgroundColorAnimForHead.setAutoCancel(true);
943                         animatorSet.playTogether(alphaAnim, backgroundColorAnim,
944                                 backgroundColorAnimForHead);
945                     } else {
946                         animatorSet.playTogether(alphaAnim, backgroundColorAnim);
947                     }
948                     animatorSet.setDuration(PANEL_ANIMATION_MS);
949                     animatorSet.start();
950                 } else {
951                     scrollToPanel.setAlpha(1f);
952                     scrollToPanel.setBackgroundColor(mainPanelColor);
953                     if (scrollToPanelHead != null) {
954                         scrollToPanelHead.setBackgroundColor(mainPanelColor);
955                     }
956                     ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(previewPanel, "alpha",
957                             previewPanel.getAlpha(), setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f);
958                     ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(previewPanel,
959                             "backgroundColor",
960                             new ArgbEvaluator(), mainPanelColor, previewPanelColor);
961                     alphaAnim.setAutoCancel(true);
962                     backgroundColorAnim.setAutoCancel(true);
963                     AnimatorSet animatorSet = new AnimatorSet();
964                     if (previewPanelHead != null) {
965                         ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject(
966                                 previewPanelHead,
967                                 "backgroundColor",
968                                 new ArgbEvaluator(), mainPanelColor, previewPanelColor);
969                         backgroundColorAnimForHead.setAutoCancel(true);
970                         animatorSet.playTogether(alphaAnim, backgroundColorAnim,
971                                 backgroundColorAnimForHead);
972                     } else {
973                         animatorSet.playTogether(alphaAnim, backgroundColorAnim);
974                     }
975                     animatorSet.setDuration(PANEL_ANIMATION_MS);
976                     animatorSet.start();
977                 }
978             } else {
979                 int scrollToX = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index;
980                 distanceToScrollToRight = scrollToX - mScrollView.getScrollX();
981                 mScrollView.scrollTo(scrollToX, 0);
982                 previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f);
983                 previewPanel.setBackgroundColor(previewPanelColor);
984                 if (previewPanelHead != null) {
985                     previewPanelHead.setBackgroundColor(previewPanelColor);
986                 }
987                 scrollToPanel.setAlpha(1f);
988                 scrollToPanel.setBackgroundColor(mainPanelColor);
989                 if (scrollToPanelHead != null) {
990                     scrollToPanelHead.setBackgroundColor(mainPanelColor);
991                 }
992             }
993             if (fragmentToBecomeMainPanel != null && fragmentToBecomeMainPanel.getView() != null) {
994                 if (!isA11yOn()) {
995                     fragmentToBecomeMainPanel.getView().requestFocus();
996                 }
997                 for (int resId : frameResIds) {
998                     Fragment f = getChildFragmentManager().findFragmentById(resId);
999                     if (f != null) {
1000                         View view = f.getView();
1001                         if (view != null) {
1002                             view.setImportantForAccessibility(
1003                                     f == fragmentToBecomeMainPanel || f instanceof InfoFragment
1004                                             ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
1005                                             : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
1006                         }
1007                     }
1008                 }
1009                 if (fragmentToBecomeMainPanel instanceof PreviewableComponentCallback) {
1010                     if (distanceToScrollToRight > 0) {
1011                         ((PreviewableComponentCallback) fragmentToBecomeMainPanel)
1012                                 .onArriveAtMainPanel(!isRTL());
1013                     } else if (distanceToScrollToRight < 0) {
1014                         ((PreviewableComponentCallback) fragmentToBecomeMainPanel)
1015                                 .onArriveAtMainPanel(isRTL());
1016                     } // distanceToScrollToRight being 0 means no actual panel sliding; thus noop.
1017                 }
1018                 updateAccessibilityTitle(fragmentToBecomeMainPanel);
1019             }
1020         });
1021     }
1022 
getInitialPreviewFragment(Fragment fragment)1023     private Fragment getInitialPreviewFragment(Fragment fragment) {
1024         if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) {
1025             return null;
1026         }
1027 
1028         LeanbackPreferenceFragmentCompat leanbackPreferenceFragment =
1029                 (LeanbackPreferenceFragmentCompat) fragment;
1030         if (leanbackPreferenceFragment.getListView() == null) {
1031             return null;
1032         }
1033 
1034         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
1035         int position = listView.getSelectedPosition();
1036         PreferenceGroupAdapter adapter =
1037                 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter());
1038         if (adapter == null) {
1039             return null;
1040         }
1041         Preference chosenPreference = adapter.getItem(position);
1042         // Find the first focusable preference if cannot find the selected preference
1043         if (chosenPreference == null || (listView.findViewHolderForPosition(position) != null
1044                 && !listView.findViewHolderForPosition(position).itemView.hasFocusable())) {
1045             chosenPreference = null;
1046             for (int i = 0; i < listView.getChildCount(); i++) {
1047                 View view = listView.getChildAt(i);
1048                 if (view.hasFocusable()) {
1049                     PreferenceViewHolder viewHolder =
1050                             (PreferenceViewHolder) listView.getChildViewHolder(view);
1051                     chosenPreference = adapter.getItem(viewHolder.getAdapterPosition());
1052                     break;
1053                 }
1054             }
1055         }
1056 
1057         if (chosenPreference == null) {
1058             return null;
1059         }
1060         return onCreatePreviewFragment(fragment, chosenPreference);
1061     }
1062 
1063     /**
1064      * Refocus the current selected preference. When a preference is selected and its InfoFragment
1065      * slice data changes. We need to call this method to make sure InfoFragment updates in time.
1066      * This is also helpful in refreshing preview of ListPreference.
1067      */
refocusPreference(Fragment fragment)1068     public void refocusPreference(Fragment fragment) {
1069         if (!isFragmentInTheMainPanel(fragment)) {
1070             return;
1071         }
1072         Preference chosenPreference = getChosenPreference(fragment);
1073         try {
1074             if (chosenPreference != null) {
1075                 if (chosenPreference.getFragment() != null
1076                         && InfoFragment.class.isAssignableFrom(
1077                         Class.forName(chosenPreference.getFragment()))) {
1078                     updateInfoFragmentStatus(fragment);
1079                 }
1080                 if (chosenPreference instanceof ListPreference) {
1081                     refocusPreferenceForceRefresh(chosenPreference);
1082                 }
1083             }
1084         } catch (ClassNotFoundException e) {
1085             e.printStackTrace();
1086         }
1087     }
1088 
1089     /** Force refresh preview panel. */
refocusPreferenceForceRefresh(Preference chosenPreference)1090     public void refocusPreferenceForceRefresh(Preference chosenPreference) {
1091         onPreferenceFocusedImpl(chosenPreference, true);
1092     }
1093 
1094     /** Show error message in preview panel **/
showErrorMessage(String errorMessage, Fragment fragment)1095     public void showErrorMessage(String errorMessage, Fragment fragment) {
1096         Fragment prefFragment =
1097                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
1098         if (fragment == prefFragment) {
1099             // If user has already navigated to the preview screen, main panel screen should be
1100             // updated to new InFoFragment. Create a fake preference to work around this case.
1101             Preference preference = new Preference(getContext());
1102             updatePreferenceWithErrorMessage(preference, errorMessage, getContext());
1103             Fragment newPrefFragment = onCreatePreviewFragment(null, preference);
1104             final FragmentTransaction transaction =
1105                     getChildFragmentManager().beginTransaction();
1106             transaction.setCustomAnimations(android.R.animator.fade_in,
1107                     android.R.animator.fade_out);
1108             transaction.replace(frameResIds[mPrefPanelIdx], newPrefFragment);
1109             transaction.commit();
1110         } else {
1111             Preference preference = getChosenPreference(prefFragment);
1112             if (preference != null) {
1113                 if (isA11yOn()) {
1114                     appendErrorToContentDescription(prefFragment, errorMessage);
1115                 }
1116                 updatePreferenceWithErrorMessage(preference, errorMessage, getContext());
1117                 onPreferenceFocused(preference);
1118             }
1119         }
1120     }
1121 
updatePreferenceWithErrorMessage( Preference preference, String errorMessage, Context context)1122     private static void updatePreferenceWithErrorMessage(
1123             Preference preference, String errorMessage, Context context) {
1124         preference.setFragment(InfoFragment.class.getCanonicalName());
1125         Bundle b = preference.getExtras();
1126         b.putParcelable(EXTRA_PREFERENCE_INFO_TITLE_ICON,
1127                 Icon.createWithResource(context, R.drawable.slice_error_icon));
1128         b.putCharSequence(EXTRA_PREFERENCE_INFO_TEXT,
1129                 context.getString(R.string.status_unavailable));
1130         b.putCharSequence(EXTRA_PREFERENCE_INFO_SUMMARY, errorMessage);
1131     }
1132 
appendErrorToContentDescription(Fragment fragment, String errorMessage)1133     private void appendErrorToContentDescription(Fragment fragment, String errorMessage) {
1134         Preference preference = getChosenPreference(fragment);
1135 
1136         String errorMessageContentDescription = "";
1137         if (preference.getTitle() != null) {
1138             errorMessageContentDescription += preference.getTitle().toString();
1139         }
1140 
1141         errorMessageContentDescription +=
1142                 HasCustomContentDescription.CONTENT_DESCRIPTION_SEPARATOR
1143                         + getString(R.string.status_unavailable)
1144                         + HasCustomContentDescription.CONTENT_DESCRIPTION_SEPARATOR + errorMessage;
1145 
1146         if (preference instanceof SlicePreference) {
1147             ((SlicePreference) preference).setContentDescription(errorMessageContentDescription);
1148         } else if (preference instanceof SliceSwitchPreference) {
1149             ((SliceSwitchPreference) preference)
1150                     .setContentDescription(errorMessageContentDescription);
1151         } else if (preference instanceof CustomContentDescriptionPreference) {
1152             ((CustomContentDescriptionPreference) preference)
1153                     .setContentDescription(errorMessageContentDescription);
1154         }
1155 
1156         LeanbackPreferenceFragmentCompat leanbackPreferenceFragment =
1157                 (LeanbackPreferenceFragmentCompat) fragment;
1158         if (leanbackPreferenceFragment.getListView() != null
1159                 && leanbackPreferenceFragment.getListView().getAdapter() != null) {
1160             leanbackPreferenceFragment.getListView().getAdapter().notifyDataSetChanged();
1161         }
1162     }
1163 
updateInfoFragmentStatus(Fragment fragment)1164     private void updateInfoFragmentStatus(Fragment fragment) {
1165         if (!isFragmentInTheMainPanel(fragment)) {
1166             return;
1167         }
1168         final Fragment existingPreviewFragment =
1169                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
1170         if (existingPreviewFragment instanceof InfoFragment) {
1171             ((InfoFragment) existingPreviewFragment).updateInfoFragment();
1172         }
1173     }
1174 
1175     /** Get the current chosen preference. */
getChosenPreference(Fragment fragment)1176     public static Preference getChosenPreference(Fragment fragment) {
1177         if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) {
1178             return null;
1179         }
1180 
1181         LeanbackPreferenceFragmentCompat leanbackPreferenceFragment =
1182                 (LeanbackPreferenceFragmentCompat) fragment;
1183         if (leanbackPreferenceFragment.getListView() == null) {
1184             return null;
1185         }
1186 
1187         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
1188         int position = listView.getSelectedPosition();
1189         PreferenceGroupAdapter adapter =
1190                 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter());
1191         return adapter != null ? adapter.getItem(position) : null;
1192     }
1193 
1194     /** Creates preview preference fragment. */
onCreatePreviewFragment(Fragment caller, Preference preference)1195     public Fragment onCreatePreviewFragment(Fragment caller, Preference preference) {
1196         if (preference == null) {
1197             return null;
1198         }
1199         if (preference.getFragment() != null) {
1200             if (!isInfoFragment(preference.getFragment())
1201                     && !isPreferenceFragment(preference.getFragment())) {
1202                 return null;
1203             }
1204             if (isPreferenceFragment(preference.getFragment())
1205                     && preference instanceof HasSliceUri) {
1206                 HasSliceUri slicePref = (HasSliceUri) preference;
1207                 if (slicePref.getUri() == null || !isUriValid(slicePref.getUri())) {
1208                     return null;
1209                 }
1210                 Bundle b = preference.getExtras();
1211                 b.putString(SlicesConstants.TAG_TARGET_URI, slicePref.getUri());
1212                 b.putCharSequence(SlicesConstants.TAG_SCREEN_TITLE, preference.getTitle());
1213             }
1214             return Fragment.instantiate(getActivity(), preference.getFragment(),
1215                     preference.getExtras());
1216         } else {
1217             Fragment f = null;
1218             if (preference instanceof ListPreference) {
1219                 f = TwoPanelListPreferenceDialogFragment.newInstanceSingle(preference.getKey());
1220             } else if (preference instanceof MultiSelectListPreference) {
1221                 f = LeanbackListPreferenceDialogFragmentCompat.newInstanceMulti(
1222                         preference.getKey());
1223             }
1224             if (f != null && caller != null) {
1225                 f.setTargetFragment(caller, 0);
1226             }
1227             return f;
1228         }
1229     }
1230 
isUriValid(String uri)1231     private boolean isUriValid(String uri) {
1232         if (uri == null) {
1233             return false;
1234         }
1235         ContentProviderClient client =
1236                 getContext().getContentResolver().acquireContentProviderClient(Uri.parse(uri));
1237         if (client != null) {
1238             client.close();
1239             return true;
1240         } else {
1241             return false;
1242         }
1243     }
1244 
1245     /** Add focus listener to the child fragment **/
addListenerForFragment(Fragment fragment)1246     public void addListenerForFragment(Fragment fragment) {
1247         if (isFragmentInTheMainPanel(fragment)) {
1248             addOrRemovePreferenceFocusedListener(fragment, true);
1249         }
1250     }
1251 
1252     /** Remove focus listener from the child fragment **/
removeListenerForFragment(Fragment fragment)1253     public void removeListenerForFragment(Fragment fragment) {
1254         addOrRemovePreferenceFocusedListener(fragment, false);
1255     }
1256 
1257     /** Check if fragment is in the main panel **/
isFragmentInTheMainPanel(Fragment fragment)1258     public boolean isFragmentInTheMainPanel(Fragment fragment) {
1259         return fragment == getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
1260     }
1261 }
1262