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