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 android.animation.ObjectAnimator;
20 import android.app.Fragment;
21 import android.app.FragmentTransaction;
22 import android.content.ContentProviderClient;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.text.TextUtils;
27 import android.transition.Fade;
28 import android.util.Log;
29 import android.view.KeyEvent;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
34 import android.widget.HorizontalScrollView;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.leanback.preference.LeanbackListPreferenceDialogFragment;
39 import androidx.leanback.preference.LeanbackPreferenceFragment;
40 import androidx.leanback.widget.OnChildViewHolderSelectedListener;
41 import androidx.leanback.widget.VerticalGridView;
42 import androidx.preference.ListPreference;
43 import androidx.preference.MultiSelectListPreference;
44 import androidx.preference.Preference;
45 import androidx.preference.PreferenceFragment;
46 import androidx.preference.PreferenceGroupAdapter;
47 import androidx.preference.PreferenceViewHolder;
48 import androidx.recyclerview.widget.RecyclerView;
49 
50 import com.android.tv.twopanelsettings.slices.HasSliceUri;
51 import com.android.tv.twopanelsettings.slices.InfoFragment;
52 import com.android.tv.twopanelsettings.slices.SlicePreference;
53 import com.android.tv.twopanelsettings.slices.SlicesConstants;
54 
55 import java.util.Set;
56 
57 /**
58  * This fragment provides containers for displaying two {@link LeanbackPreferenceFragment}.
59  * The preference fragment on the left works as a main panel on which the user can operate.
60  * The preference fragment on the right works as a preview panel for displaying the preview
61  * information.
62  */
63 public abstract class TwoPanelSettingsFragment extends Fragment implements
64         PreferenceFragment.OnPreferenceStartFragmentCallback,
65         PreferenceFragment.OnPreferenceStartScreenCallback,
66         PreferenceFragment.OnPreferenceDisplayDialogCallback {
67     private static final String TAG = "TwoPanelSettingsFragment";
68     private static final boolean DEBUG = false;
69     private static final String PREVIEW_FRAGMENT_TAG =
70             "com.android.tv.settings.TwoPanelSettingsFragment.PREVIEW_FRAGMENT";
71     private static final String PREFERENCE_FRAGMENT_TAG =
72             "com.android.tv.settings.TwoPanelSettingsFragment.PREFERENCE_FRAGMENT";
73     private static final String EXTRA_PREF_PANEL_IDX =
74             "com.android.tv.twopanelsettings.PREF_PANEL_IDX";
75     private static final int[] frameResIds =
76             {R.id.frame1, R.id.frame2, R.id.frame3, R.id.frame4, R.id.frame5, R.id.frame6,
77                     R.id.frame7, R.id.frame8, R.id.frame9, R.id.frame10};
78     private static final int[] frameResOverlayIds =
79             {R.id.frame1_overlay, R.id.frame2_overlay, R.id.frame3_overlay, R.id.frame4_overlay,
80             R.id.frame5_overlay, R.id.frame6_overlay, R.id.frame7_overlay, R.id.frame8_overlay,
81             R.id.frame9_overlay, R.id.frame10_overlay};
82     private static final long PANEL_ANIMATION_MS = 400;
83     private static final long PANEL_ANIMATION_DELAY_MS = 200;
84 
85     private int mMaxScrollX;
86     private final RootViewOnKeyListener mRootViewOnKeyListener = new RootViewOnKeyListener();
87     private int mPrefPanelIdx;
88     private HorizontalScrollView mScrollView;
89     private Handler mHandler;
90     private boolean mIsNavigatingBack;
91 
92     private OnChildViewHolderSelectedListener mOnChildViewHolderSelectedListener =
93             new OnChildViewHolderSelectedListener() {
94                 @Override
95                 public void onChildViewHolderSelected(RecyclerView parent,
96                         RecyclerView.ViewHolder child, int position, int subposition) {
97                     if (child == null) {
98                         return;
99                     }
100                     int adapterPosition = child.getAdapterPosition();
101                     PreferenceGroupAdapter preferenceGroupAdapter =
102                             (PreferenceGroupAdapter) parent.getAdapter();
103                     Preference preference = preferenceGroupAdapter.getItem(adapterPosition);
104                     onPreferenceFocused(preference);
105                 }
106 
107                 @Override
108                 public void onChildViewHolderSelectedAndPositioned(RecyclerView parent,
109                         RecyclerView.ViewHolder child, int position, int subposition) {
110                 }
111             };
112 
113     private OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
114         @Override
115         public void onGlobalLayout() {
116             getView().getViewTreeObserver().removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
117             moveToPanel(mPrefPanelIdx, false);
118         }
119     };
120 
121     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)122     public View onCreateView(LayoutInflater inflater, ViewGroup container,
123             Bundle savedInstanceState) {
124         final View v = inflater.inflate(R.layout.two_panel_settings_fragment, container, false);
125         mScrollView = v.findViewById(R.id.scrollview);
126         mHandler = new Handler();
127         if (savedInstanceState != null) {
128             mPrefPanelIdx = savedInstanceState.getInt(EXTRA_PREF_PANEL_IDX, mPrefPanelIdx);
129             // Move to correct panel once global layout finishes.
130             v.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
131         }
132         mMaxScrollX = computeMaxRightScroll();
133         return v;
134     }
135 
136     @Override
onSaveInstanceState(Bundle outState)137     public void onSaveInstanceState(Bundle outState) {
138         outState.putInt(EXTRA_PREF_PANEL_IDX, mPrefPanelIdx);
139         super.onSaveInstanceState(outState);
140     }
141 
142     @Override
onViewCreated(View view, Bundle savedInstanceState)143     public void onViewCreated(View view, Bundle savedInstanceState) {
144         super.onViewCreated(view, savedInstanceState);
145         if (savedInstanceState == null) {
146             onPreferenceStartInitialScreen();
147         }
148     }
149 
150     /** Extend this method to provide the initial screen **/
onPreferenceStartInitialScreen()151     public abstract void onPreferenceStartInitialScreen();
152 
shouldDisplay(String fragment)153     private boolean shouldDisplay(String fragment) {
154         try {
155             return LeanbackPreferenceFragment.class.isAssignableFrom(Class.forName(fragment))
156                     || InfoFragment.class.isAssignableFrom(Class.forName(fragment));
157         } catch (ClassNotFoundException e) {
158             throw new RuntimeException("Fragment class not found.", e);
159         }
160     }
161 
162     @Override
onPreferenceStartFragment(PreferenceFragment caller, Preference pref)163     public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
164         if (DEBUG) {
165             Log.d(TAG, "onPreferenceStartFragment " + pref.getTitle());
166         }
167         if (pref.getFragment() == null) {
168             return false;
169         }
170         Fragment preview = getChildFragmentManager().findFragmentById(
171                 frameResIds[mPrefPanelIdx + 1]);
172         if (preview != null && !(preview instanceof DummyFragment)) {
173             if (!(preview instanceof InfoFragment)) {
174                 navigateToPreviewFragment();
175             }
176         } else {
177             // If there is no corresponding slice provider, thus the corresponding fragment is not
178             // created, return false to check the intent of the SlicePreference.
179             if (pref instanceof SlicePreference) {
180                 return false;
181             }
182             startImmersiveFragment(Fragment.instantiate(getActivity(), pref.getFragment(),
183                     pref.getExtras()));
184         }
185         return true;
186     }
187 
188     /** Navigate back to the previous fragment **/
navigateBack()189     public void navigateBack() {
190         back(false);
191     }
192 
193     /** Navigate into current preview fragment */
navigateToPreviewFragment()194     public void navigateToPreviewFragment() {
195         Fragment previewFragment = getChildFragmentManager().findFragmentById(
196                 frameResIds[mPrefPanelIdx + 1]);
197         if (previewFragment instanceof NavigationCallback) {
198             ((NavigationCallback) previewFragment).onNavigateToPreview();
199         }
200         if (previewFragment == null || previewFragment instanceof DummyFragment) {
201             return;
202         }
203         if (DEBUG) {
204             Log.d(TAG, "navigateToPreviewFragment");
205         }
206         if (mPrefPanelIdx + 1 >= frameResIds.length) {
207             Log.w(TAG, "Maximum level of depth reached.");
208             return;
209         }
210         Fragment initialPreviewFragment = getInitialPreviewFragment(previewFragment);
211         if (initialPreviewFragment == null) {
212             initialPreviewFragment = new DummyFragment();
213         }
214         initialPreviewFragment.setExitTransition(null);
215 
216         mPrefPanelIdx++;
217 
218         Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
219         addOrRemovePreferenceFocusedListener(fragment, true);
220 
221         final FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
222         transaction.replace(frameResIds[mPrefPanelIdx + 1], initialPreviewFragment,
223                 PREVIEW_FRAGMENT_TAG);
224         transaction.commit();
225 
226         moveToPanel(mPrefPanelIdx, true);
227         removeFragmentAndAddToBackStack(mPrefPanelIdx - 1);
228     }
229 
addOrRemovePreferenceFocusedListener(Fragment fragment, boolean isAddingListener)230     private void addOrRemovePreferenceFocusedListener(Fragment fragment, boolean isAddingListener) {
231         if (fragment == null || !(fragment instanceof LeanbackPreferenceFragment)) {
232             return;
233         }
234         LeanbackPreferenceFragment leanbackPreferenceFragment =
235                 (LeanbackPreferenceFragment) fragment;
236         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
237         if (listView != null) {
238             if (isAddingListener) {
239                 listView.setOnChildViewHolderSelectedListener(mOnChildViewHolderSelectedListener);
240             } else {
241                 listView.setOnChildViewHolderSelectedListener(null);
242             }
243         }
244     }
245 
246     /**
247      * Displays left panel preference fragment to the user.
248      *
249      * @param fragment Fragment instance to be added.
250      */
startPreferenceFragment(@onNull Fragment fragment)251     public void startPreferenceFragment(@NonNull Fragment fragment) {
252         if (DEBUG) {
253             Log.d(TAG, "startPreferenceFragment");
254         }
255         FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
256         transaction.add(frameResIds[mPrefPanelIdx], fragment, PREFERENCE_FRAGMENT_TAG);
257         transaction.commitNow();
258 
259         Fragment initialPreviewFragment = getInitialPreviewFragment(fragment);
260         if (initialPreviewFragment == null) {
261             initialPreviewFragment = new DummyFragment();
262         }
263         initialPreviewFragment.setExitTransition(null);
264 
265         transaction = getChildFragmentManager().beginTransaction();
266         transaction.add(frameResIds[mPrefPanelIdx + 1], initialPreviewFragment,
267                 initialPreviewFragment.getClass().toString());
268         transaction.commit();
269     }
270 
271     @Override
onPreferenceDisplayDialog(@onNull PreferenceFragment caller, Preference pref)272     public boolean onPreferenceDisplayDialog(@NonNull PreferenceFragment caller, Preference pref) {
273         if (DEBUG) {
274             Log.d(TAG, "PreferenceDisplayDialog");
275         }
276         if (caller == null) {
277             throw new IllegalArgumentException("Cannot display dialog for preference " + pref
278                     + ", Caller must not be null!");
279         }
280         Fragment preview = getChildFragmentManager().findFragmentById(
281                 frameResIds[mPrefPanelIdx + 1]);
282         if (preview != null && !(preview instanceof DummyFragment)) {
283             if (preview instanceof NavigationCallback) {
284                 ((NavigationCallback) preview).onNavigateToPreview();
285             }
286             mPrefPanelIdx++;
287             moveToPanel(mPrefPanelIdx, true);
288             removeFragmentAndAddToBackStack(mPrefPanelIdx - 1);
289             return true;
290         }
291         return false;
292     }
293 
equalArguments(Bundle a, Bundle b)294     private boolean equalArguments(Bundle a, Bundle b) {
295         if (a == null && b == null) {
296             return true;
297         }
298         if (a == null || b == null) {
299             return false;
300         }
301         Set<String> aks = a.keySet();
302         Set<String> bks = b.keySet();
303         if (a.size() != b.size()) {
304             return false;
305         }
306         if (!aks.containsAll(bks)) {
307             return false;
308         }
309         for (String key : aks) {
310             if (a.get(key) == null && b.get(key) == null) {
311                 continue;
312             }
313             if (a.get(key) == null || b.get(key) == null) {
314                 return false;
315             }
316             if (!a.get(key).equals(b.get(key))) {
317                 return false;
318             }
319         }
320         return true;
321     }
322 
323     /** Callback from SliceFragment **/
324     public interface SliceFragmentCallback {
325         /** Triggered when preference is focused **/
onPreferenceFocused(Preference preference)326         void onPreferenceFocused(Preference preference);
327     }
328 
onPreferenceFocused(Preference pref)329     private boolean onPreferenceFocused(Preference pref) {
330         if (DEBUG) {
331             Log.d(TAG, "onPreferenceFocused " + pref.getTitle());
332         }
333         final Fragment prefFragment =
334                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
335         if (prefFragment instanceof SliceFragmentCallback) {
336             ((SliceFragmentCallback) prefFragment).onPreferenceFocused(pref);
337         }
338         Fragment previewFragment = null;
339         try {
340             previewFragment = onCreatePreviewFragment(prefFragment, pref);
341         } catch (Exception e) {
342             Log.w(TAG, "Cannot instantiate the fragment from preference: " + pref, e);
343         }
344         if (previewFragment == null) {
345             previewFragment = new DummyFragment();
346         } else {
347             previewFragment.setTargetFragment(prefFragment, 0);
348         }
349 
350         final Fragment existingPreviewFragment =
351                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
352         if (existingPreviewFragment != null
353                 && existingPreviewFragment.getClass().equals(previewFragment.getClass())
354                 && equalArguments(existingPreviewFragment.getArguments(),
355                 previewFragment.getArguments())) {
356             if (isRTL() && mScrollView.getScrollX() == 0 && mPrefPanelIdx == 0) {
357                 // For RTL we need to reclaim focus to the correct scroll position if a pref
358                 // launches a new activity because the horizontal scroll goes back to 0.
359                 getView().getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
360             }
361             return true;
362         }
363 
364         // If the existing preview fragment is recreated when the activity is recreated, the
365         // animation would fall back to "slide left", in this case, we need to set the exit
366         // transition.
367         if (existingPreviewFragment != null) {
368             existingPreviewFragment.setExitTransition(null);
369         }
370         previewFragment.setEnterTransition(new Fade());
371         previewFragment.setExitTransition(null);
372 
373         final FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
374         transaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
375         transaction.replace(frameResIds[mPrefPanelIdx + 1], previewFragment);
376         transaction.commit();
377 
378         // Some fragments may steal focus on creation. Reclaim focus on main fragment.
379         getView().getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
380         return true;
381     }
382 
isRTL()383     private boolean isRTL() {
384         return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
385     }
386 
387     @Override
onResume()388     public void onResume() {
389         if (DEBUG) {
390             Log.d(TAG, "onResume");
391         }
392         super.onResume();
393         // Trap back button presses
394         final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView();
395         if (rootView != null) {
396             rootView.setOnBackKeyListener(mRootViewOnKeyListener);
397         }
398     }
399 
400     @Override
onPause()401     public void onPause() {
402         if (DEBUG) {
403             Log.d(TAG, "onPause");
404         }
405         super.onPause();
406         final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView();
407         if (rootView != null) {
408             rootView.setOnBackKeyListener(null);
409         }
410     }
411 
412     /**
413      * Displays a fragment to the user, temporarily replacing the contents of this fragment.
414      *
415      * @param fragment Fragment instance to be added.
416      */
startImmersiveFragment(@onNull Fragment fragment)417     public void startImmersiveFragment(@NonNull Fragment fragment) {
418         if (DEBUG) {
419             Log.d(TAG, "Starting immersive fragment.");
420         }
421         final FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
422         Fragment target = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
423         fragment.setTargetFragment(target, 0);
424         transaction
425                 .add(R.id.two_panel_fragment_container, fragment)
426                 .remove(target)
427                 .addToBackStack(null)
428                 .commit();
429     }
430 
431     public static class DummyFragment extends Fragment {
432         @Override
433         public @Nullable
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)434         View onCreateView(LayoutInflater inflater, ViewGroup container,
435                 Bundle savedInstanceState) {
436             return inflater.inflate(R.layout.dummy_fragment, container, false);
437         }
438     }
439 
440     /**
441      * Implement this if fragment needs to handle DPAD_LEFT & DPAD_RIGHT itself in some cases
442      **/
443     public interface NavigationCallback {
444 
445         /**
446          * Returns true if the fragment is in the state that can navigate back on receiving a
447          * navigation DPAD key. When true, TwoPanelSettings will initiate a back operation on
448          * receiving a left key. This method doesn't apply to back key: back key always initiates a
449          * back operation.
450          */
canNavigateBackOnDPAD()451         boolean canNavigateBackOnDPAD();
452 
453         /**
454          * Callback when navigating to preview screen
455          */
onNavigateToPreview()456         void onNavigateToPreview();
457 
458         /**
459          * Callback when returning to previous screen
460          */
onNavigateBack()461         void onNavigateBack();
462     }
463 
464     /**
465      * Implement this if the component (typically a Fragment) is preview-able and would like to get
466      * some lifecycle-like callback(s) when the component becomes the main panel.
467      */
468     public interface PreviewableComponentCallback {
469 
470         /**
471          * Lifecycle-like callback when the component becomes main panel from the preview panel. For
472          * Fragment, this will be invoked right after the preview fragment sliding into the main
473          * panel.
474          *
475          * @param forward means whether the component arrives at main panel when users are
476          *    navigating forwards (deeper into the TvSettings tree).
477          */
onArriveAtMainPanel(boolean forward)478         void onArriveAtMainPanel(boolean forward);
479     }
480 
481     private class RootViewOnKeyListener implements View.OnKeyListener {
482 
483         @Override
onKey(View v, int keyCode, KeyEvent event)484         public boolean onKey(View v, int keyCode, KeyEvent event) {
485             Fragment prefFragment =
486                     getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
487             if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {
488                 return back(true);
489             }
490 
491             if (event.getAction() == KeyEvent.ACTION_DOWN
492                     && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT)
493                     || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT))) {
494                 if (prefFragment instanceof NavigationCallback
495                         && !((NavigationCallback) prefFragment).canNavigateBackOnDPAD()) {
496                     return false;
497                 }
498                 return back(false);
499             }
500 
501             if (event.getAction() == KeyEvent.ACTION_DOWN
502                     && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)
503                     || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT))) {
504                 if (shouldPerformClick()) {
505                     v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
506                             KeyEvent.KEYCODE_DPAD_CENTER));
507                     v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
508                             KeyEvent.KEYCODE_DPAD_CENTER));
509                 } else {
510                     Fragment previewFragment = getChildFragmentManager()
511                             .findFragmentById(frameResIds[mPrefPanelIdx + 1]);
512                     if (!(previewFragment instanceof InfoFragment)) {
513                         navigateToPreviewFragment();
514                     }
515                 }
516                 return true;
517             }
518             return false;
519         }
520     }
521 
shouldPerformClick()522     private boolean shouldPerformClick() {
523         Fragment prefFragment =
524                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
525         Preference preference = getChosenPreference(prefFragment);
526         if (preference == null) {
527             return false;
528         }
529         // This is for the case when a preference has preview but once user navigate to
530         // see the preview, settings actually launch an intent to start external activity.
531         if (preference.getIntent() != null  && !TextUtils.isEmpty(preference.getFragment())) {
532             return true;
533         }
534         if (preference instanceof SlicePreference
535                 && ((SlicePreference) preference).getSliceAction() != null
536                 && ((SlicePreference) preference).getUri() != null) {
537             return true;
538         }
539 
540         return false;
541     }
542 
back(boolean isKeyBackPressed)543     private boolean back(boolean isKeyBackPressed) {
544         if (mIsNavigatingBack) {
545             mHandler.postDelayed(new Runnable() {
546                 @Override
547                 public void run() {
548                     if (DEBUG) {
549                         Log.d(TAG, "Navigating back is deferred.");
550                     }
551                     back(isKeyBackPressed);
552                 }
553             }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS);
554             return true;
555         }
556         if (DEBUG) {
557             Log.d(TAG, "Going back one level.");
558         }
559 
560         final Fragment immersiveFragment =
561                 getChildFragmentManager().findFragmentById(R.id.two_panel_fragment_container);
562         if (immersiveFragment != null) {
563             getChildFragmentManager().popBackStack();
564             moveToPanel(mPrefPanelIdx, false);
565             return true;
566         }
567 
568         if (mPrefPanelIdx < 1) {
569             // Disallow the user to use "dpad left" to finish activity in the first screen
570             if (isKeyBackPressed) {
571                 getActivity().finish();
572             }
573             return true;
574         }
575 
576         mIsNavigatingBack = true;
577         Fragment preferenceFragment =
578                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
579         addOrRemovePreferenceFocusedListener(preferenceFragment, false);
580         getChildFragmentManager().popBackStack();
581 
582         mPrefPanelIdx--;
583 
584         mHandler.postDelayed(() -> {
585             moveToPanel(mPrefPanelIdx, true);
586         }, PANEL_ANIMATION_DELAY_MS);
587 
588         mHandler.postDelayed(() -> {
589             removeFragment(mPrefPanelIdx + 2);
590             mIsNavigatingBack = false;
591             Fragment previewFragment =
592                     getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
593             if (previewFragment instanceof NavigationCallback) {
594                 ((NavigationCallback) previewFragment).onNavigateBack();
595             }
596         }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS);
597         return true;
598     }
599 
removeFragment(int index)600     private void removeFragment(int index) {
601         Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[index]);
602         if (fragment != null) {
603             getChildFragmentManager().beginTransaction().remove(fragment).commit();
604         }
605     }
606 
removeFragmentAndAddToBackStack(int index)607     private void removeFragmentAndAddToBackStack(int index) {
608         if (index < 0) {
609             return;
610         }
611         Fragment removePanel = getChildFragmentManager().findFragmentById(frameResIds[index]);
612         if (removePanel != null) {
613             removePanel.setExitTransition(new Fade());
614             getChildFragmentManager().beginTransaction().remove(removePanel)
615                     .addToBackStack("remove " + removePanel.getClass().getName()).commit();
616         }
617     }
618 
619     /** For RTL layout, we need to know the right edge from where the panels start scrolling. */
computeMaxRightScroll()620     private int computeMaxRightScroll() {
621         int scrollViewWidth = getResources().getDimensionPixelSize(R.dimen.tp_settings_panes_width);
622         int panelWidth = getResources().getDimensionPixelSize(
623                 R.dimen.tp_settings_preference_pane_width);
624         int panelPadding = getResources().getDimensionPixelSize(
625                 R.dimen.preference_pane_extra_padding_start) * 2;
626         int result = frameResIds.length * panelWidth - scrollViewWidth + panelPadding;
627         return result < 0 ? 0 : result;
628     }
629 
630     /** Scrolls such that the panel with given index is the main panel shown on the left. */
moveToPanel(final int index, boolean smoothScroll)631     private void moveToPanel(final int index, boolean smoothScroll) {
632         mHandler.post(() -> {
633             if (DEBUG) {
634                 Log.d(TAG, "Moving to panel " + index);
635             }
636             if (!isAdded()) {
637                 return;
638             }
639             Fragment fragmentToBecomeMainPanel =
640                     getChildFragmentManager().findFragmentById(frameResIds[index]);
641             Fragment fragmentToBecomePreviewPanel =
642                     getChildFragmentManager().findFragmentById(frameResIds[index + 1]);
643             // Positive value means that the panel is scrolling to right (navigate forward for LTR
644             // or navigate backwards for RTL) and vice versa; 0 means that this is likely invoked
645             // by GlobalLayoutListener and there's no actual sliding.
646             int distanceToScrollToRight;
647             int panelWidth = getResources().getDimensionPixelSize(
648                     R.dimen.tp_settings_preference_pane_width);
649             View scrollToPanelOverlay = getView().findViewById(frameResOverlayIds[index]);
650             View previewPanelOverlay = getView().findViewById(frameResOverlayIds[index + 1]);
651             boolean scrollsToPreview =
652                     isRTL() ? mScrollView.getScrollX() >= mMaxScrollX - panelWidth * index
653                             : mScrollView.getScrollX() <= panelWidth * index;
654             boolean hasPreviewFragment = fragmentToBecomePreviewPanel != null
655                     && !(fragmentToBecomePreviewPanel instanceof DummyFragment);
656             if (smoothScroll) {
657                 int animationEnd = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index;
658                 distanceToScrollToRight = animationEnd - mScrollView.getScrollX();
659                 // Slide animation
660                 ObjectAnimator slideAnim = ObjectAnimator.ofInt(mScrollView, "scrollX",
661                         mScrollView.getScrollX(), animationEnd);
662                 slideAnim.setAutoCancel(true);
663                 slideAnim.setDuration(PANEL_ANIMATION_MS);
664                 slideAnim.start();
665                 // Color animation
666                 if (scrollsToPreview) {
667                     previewPanelOverlay.setAlpha(hasPreviewFragment ? 1f : 0f);
668                     ObjectAnimator colorAnim = ObjectAnimator.ofFloat(scrollToPanelOverlay, "alpha",
669                             scrollToPanelOverlay.getAlpha(), 0f);
670                     colorAnim.setAutoCancel(true);
671                     colorAnim.setDuration(PANEL_ANIMATION_MS);
672                     colorAnim.start();
673                 } else {
674                     scrollToPanelOverlay.setAlpha(0f);
675                     ObjectAnimator colorAnim = ObjectAnimator.ofFloat(previewPanelOverlay, "alpha",
676                             previewPanelOverlay.getAlpha(), hasPreviewFragment ? 1f : 0f);
677                     colorAnim.setAutoCancel(true);
678                     colorAnim.setDuration(PANEL_ANIMATION_MS);
679                     colorAnim.start();
680                 }
681             } else {
682                 int scrollToX = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index;
683                 distanceToScrollToRight = scrollToX - mScrollView.getScrollX();
684                 mScrollView.scrollTo(scrollToX, 0);
685                 scrollToPanelOverlay.setAlpha(0f);
686                 previewPanelOverlay.setAlpha(hasPreviewFragment ? 1f : 0f);
687             }
688             if (fragmentToBecomeMainPanel != null && fragmentToBecomeMainPanel.getView() != null) {
689                 fragmentToBecomeMainPanel.getView().requestFocus();
690                 for (int resId : frameResIds) {
691                     Fragment f = getChildFragmentManager().findFragmentById(resId);
692                     if (f != null) {
693                         View view = f.getView();
694                         if (view != null) {
695                             view.setImportantForAccessibility(
696                                     f == fragmentToBecomeMainPanel
697                                             ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
698                                             : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
699                         }
700                     }
701                 }
702                 if (fragmentToBecomeMainPanel instanceof PreviewableComponentCallback) {
703                     if (distanceToScrollToRight > 0) {
704                         ((PreviewableComponentCallback) fragmentToBecomeMainPanel)
705                                 .onArriveAtMainPanel(!isRTL());
706                     } else if (distanceToScrollToRight < 0) {
707                         ((PreviewableComponentCallback) fragmentToBecomeMainPanel)
708                                 .onArriveAtMainPanel(isRTL());
709                     } // distanceToScrollToRight being 0 means no actual panel sliding; thus noop.
710                 }
711             }
712         });
713     }
714 
getInitialPreviewFragment(Fragment fragment)715     private Fragment getInitialPreviewFragment(Fragment fragment) {
716         if (!(fragment instanceof LeanbackPreferenceFragment)) {
717             return null;
718         }
719 
720         LeanbackPreferenceFragment leanbackPreferenceFragment =
721                 (LeanbackPreferenceFragment) fragment;
722         if (leanbackPreferenceFragment.getListView() == null) {
723             return null;
724         }
725 
726         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
727         int position = listView.getSelectedPosition();
728         PreferenceGroupAdapter adapter =
729                 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter());
730         Preference chosenPreference = adapter.getItem(position);
731         // Find the first focusable preference if cannot find the selected preference
732         if (chosenPreference == null || (listView.findViewHolderForPosition(position) != null
733                 && !listView.findViewHolderForPosition(position).itemView.hasFocusable())) {
734             chosenPreference = null;
735             for (int i = 0; i < listView.getChildCount(); i++) {
736                 View view = listView.getChildAt(i);
737                 if (view.hasFocusable()) {
738                     PreferenceViewHolder viewHolder =
739                             (PreferenceViewHolder) listView.getChildViewHolder(view);
740                     chosenPreference = adapter.getItem(viewHolder.getAdapterPosition());
741                     break;
742                 }
743             }
744         }
745 
746         if (chosenPreference == null) {
747             return null;
748         }
749         return onCreatePreviewFragment(fragment, chosenPreference);
750     }
751 
752     /**
753      * Refocus the current selected preference. When a preference is selected and its InfoFragment
754      * slice data changes. We need to call this method to make sure InfoFragment updates in time.
755      */
refocusPreference(Fragment fragment)756     public void refocusPreference(Fragment fragment) {
757         if (!isFragmentInTheMainPanel(fragment)) {
758             return;
759         }
760         Preference chosenPreference = getChosenPreference(fragment);
761         try {
762             if (chosenPreference != null && chosenPreference.getFragment() != null
763                     && InfoFragment.class.isAssignableFrom(
764                     Class.forName(chosenPreference.getFragment()))) {
765                 onPreferenceFocused(chosenPreference);
766             }
767         } catch (ClassNotFoundException e) {
768             e.printStackTrace();
769         }
770     }
771 
getChosenPreference(Fragment fragment)772     private static Preference getChosenPreference(Fragment fragment) {
773         if (!(fragment instanceof LeanbackPreferenceFragment)) {
774             return null;
775         }
776 
777         LeanbackPreferenceFragment leanbackPreferenceFragment =
778                 (LeanbackPreferenceFragment) fragment;
779         if (leanbackPreferenceFragment.getListView() == null) {
780             return null;
781         }
782 
783         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
784         int position = listView.getSelectedPosition();
785         PreferenceGroupAdapter adapter =
786                 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter());
787         Preference chosenPreference = adapter.getItem(position);
788         return chosenPreference;
789     }
790 
791     /** Creates preview preference fragment. */
onCreatePreviewFragment(Fragment caller, Preference preference)792     public Fragment onCreatePreviewFragment(Fragment caller, Preference preference) {
793         if (preference.getFragment() != null) {
794             if (!shouldDisplay(preference.getFragment())) {
795                 return null;
796             }
797             if (preference instanceof HasSliceUri) {
798                 HasSliceUri slicePref = (HasSliceUri) preference;
799                 if (slicePref.getUri() == null || !isUriValid(slicePref.getUri())) {
800                     return null;
801                 }
802                 Bundle b = preference.getExtras();
803                 b.putString(SlicesConstants.TAG_TARGET_URI, slicePref.getUri());
804                 b.putCharSequence(SlicesConstants.TAG_SCREEN_TITLE, preference.getTitle());
805             }
806             return Fragment.instantiate(getActivity(), preference.getFragment(),
807                     preference.getExtras());
808         } else {
809             Fragment f = null;
810             if (preference instanceof ListPreference) {
811                 f = TwoPanelListPreferenceDialogFragment.newInstanceSingle(preference.getKey());
812             } else if (preference instanceof MultiSelectListPreference) {
813                 f = LeanbackListPreferenceDialogFragment.newInstanceMulti(preference.getKey());
814             }
815             if (f != null && caller != null) {
816                 f.setTargetFragment(caller, 0);
817             }
818             return f;
819         }
820     }
821 
isUriValid(String uri)822     private boolean isUriValid(String uri) {
823         if (uri == null) {
824             return false;
825         }
826         ContentProviderClient client =
827                 getContext().getContentResolver().acquireContentProviderClient(Uri.parse(uri));
828         if (client != null) {
829             client.close();
830             return true;
831         } else {
832             return false;
833         }
834     }
835 
836     /** Add focus listener to the child fragment **/
addListenerForFragment(Fragment fragment)837     public void addListenerForFragment(Fragment fragment) {
838         if (isFragmentInTheMainPanel(fragment)) {
839             addOrRemovePreferenceFocusedListener(fragment, true);
840         }
841     }
842 
843     /** Remove focus listener from the child fragment **/
removeListenerForFragment(Fragment fragment)844     public void removeListenerForFragment(Fragment fragment) {
845         addOrRemovePreferenceFocusedListener(fragment, false);
846     }
847 
848     /** Check if fragment is in the main panel **/
isFragmentInTheMainPanel(Fragment fragment)849     public boolean isFragmentInTheMainPanel(Fragment fragment) {
850         return fragment == getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
851     }
852 }
853