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