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