• 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).equals(b.get(key))) {
493                 return false;
494             }
495         }
496         return true;
497     }
498 
499     /** Callback from SliceFragment **/
500     public interface SliceFragmentCallback {
501         /** Triggered when preference is focused **/
onPreferenceFocused(Preference preference)502         void onPreferenceFocused(Preference preference);
503 
504         /** Triggered when Seekbar preference is changed **/
onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue)505         void onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue);
506     }
507 
onPreferenceFocused(Preference pref, int panelIndex)508     protected void onPreferenceFocused(Preference pref, int panelIndex) {
509         onPreferenceFocusedImpl(pref, false, panelIndex);
510     }
511 
onPreferenceFocusedImpl(Preference pref, boolean forceRefresh, int panelIndex)512     private void onPreferenceFocusedImpl(Preference pref, boolean forceRefresh, int panelIndex) {
513         if (pref == null) {
514             return;
515         }
516         if (DEBUG) {
517             Log.d(TAG, "onPreferenceFocused " + pref.getTitle());
518         }
519         final Fragment prefFragment =
520                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
521         if (prefFragment instanceof SliceFragmentCallback) {
522             ((SliceFragmentCallback) prefFragment).onPreferenceFocused(pref);
523         }
524         mFocusedPreference = pref;
525         if (mCheckVerticalGridViewScrollState || mPreviewPanelCreationDelay > 0) {
526             mIsWaitingForUpdatingPreview = true;
527             VerticalGridView listView = (VerticalGridView)
528                     ((LeanbackPreferenceFragmentCompat) prefFragment).getListView();
529             mHandler.postDelayed(new PostShowPreviewRunnable(
530                     listView, pref, forceRefresh, panelIndex), mPreviewPanelCreationDelay);
531         } else {
532             handleFragmentTransactionWhenFocused(pref, forceRefresh, panelIndex);
533         }
534     }
535 
536     private final class PostShowPreviewRunnable implements Runnable {
537         private final VerticalGridView mListView;
538         private final Preference mPref;
539         private final boolean mForceFresh;
540         private final int mPanelIndex;
541 
PostShowPreviewRunnable(VerticalGridView listView, Preference pref, boolean forceFresh, int panelIndex)542         PostShowPreviewRunnable(VerticalGridView listView, Preference pref, boolean forceFresh,
543                 int panelIndex) {
544             this.mListView = listView;
545             this.mPref = pref;
546             this.mForceFresh = forceFresh;
547             mPanelIndex = panelIndex;
548         }
549 
550         @Override
run()551         public void run() {
552             if (mPref == mFocusedPreference) {
553                 if (mListView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
554                     mHandler.postDelayed(this, CHECK_IDLE_STATE_MS);
555                 } else {
556                     handleFragmentTransactionWhenFocused(mPref, mForceFresh, mPanelIndex);
557                     mIsWaitingForUpdatingPreview = false;
558                 }
559             }
560         }
561     }
562 
handleFragmentTransactionWhenFocused(Preference pref, boolean forceRefresh, int panelIndex)563     private void handleFragmentTransactionWhenFocused(Preference pref, boolean forceRefresh,
564             int panelIndex) {
565         if (!isAdded() || panelIndex != mPrefPanelIdx) {
566             return;
567         }
568         Fragment previewFragment = null;
569         final Fragment prefFragment =
570                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
571         try {
572             previewFragment = onCreatePreviewFragment(prefFragment, pref);
573         } catch (Exception e) {
574             Log.w(TAG, "Cannot instantiate the fragment from preference: " + pref, e);
575         }
576         if (previewFragment == null) {
577             previewFragment = new DummyFragment();
578         }
579         final Fragment existingPreviewFragment =
580                 getChildFragmentManager().findFragmentById(
581                         frameResIds[mPrefPanelIdx + 1]);
582         if (existingPreviewFragment != null
583                 && existingPreviewFragment.getClass().equals(previewFragment.getClass())
584                 && equalArguments(existingPreviewFragment.getArguments(),
585                 previewFragment.getArguments())) {
586             if (isRTL() && mScrollView.getScrollX() == 0 && mPrefPanelIdx == 0
587                     && getView() != null && getView().getViewTreeObserver() != null) {
588                 // For RTL we need to reclaim focus to the correct scroll position if a pref
589                 // launches a new activity because the horizontal scroll goes back to 0.
590                 getView().getViewTreeObserver().addOnGlobalLayoutListener(
591                         mOnGlobalLayoutListener);
592             }
593             if (!forceRefresh) {
594                 return;
595             }
596         }
597 
598         // If the existing preview fragment is recreated when the activity is recreated, the
599         // animation would fall back to "slide left", in this case, we need to set the exit
600         // transition.
601         if (existingPreviewFragment != null) {
602             existingPreviewFragment.setExitTransition(null);
603         }
604         previewFragment.setEnterTransition(new Fade());
605         previewFragment.setExitTransition(null);
606         final FragmentTransaction transaction =
607                 getChildFragmentManager().beginTransaction();
608         transaction.setCustomAnimations(R.animator.fade_in_preview_panel,
609                 R.animator.fade_out_preview_panel);
610         transaction.replace(frameResIds[mPrefPanelIdx + 1], previewFragment);
611         transaction.commitNow();
612 
613         // Some fragments may steal focus on creation. Reclaim focus on main fragment.
614         if (getView() != null && getView().getViewTreeObserver() != null) {
615             getView().getViewTreeObserver().addOnGlobalLayoutListener(
616                     mOnGlobalLayoutListener);
617         }
618     }
619 
onSeekbarPreferenceChanged(SliceSeekbarPreference pref, int addValue)620     private boolean onSeekbarPreferenceChanged(SliceSeekbarPreference pref, int addValue) {
621         final Fragment prefFragment =
622                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
623         if (prefFragment instanceof SliceFragmentCallback) {
624             ((SliceFragmentCallback) prefFragment).onSeekbarPreferenceChanged(pref, addValue);
625         }
626         return true;
627     }
628 
isRTL()629     private boolean isRTL() {
630         return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
631     }
632 
633     @Override
onResume()634     public void onResume() {
635         if (DEBUG) {
636             Log.d(TAG, "onResume");
637         }
638         super.onResume();
639         IntentFilter intentFilter = new IntentFilter();
640         intentFilter.addAction("com.android.tv.settings.PREVIEW_DELAY");
641         getContext().registerReceiver(mPreviewPanelDelayReceiver, intentFilter,
642                 Context.RECEIVER_EXPORTED_UNAUDITED);
643         // Trap back button presses
644         final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView();
645         if (rootView != null) {
646             rootView.setOnBackKeyListener(mRootViewOnKeyListener);
647         }
648     }
649 
650     @Override
onPause()651     public void onPause() {
652         if (DEBUG) {
653             Log.d(TAG, "onPause");
654         }
655         super.onPause();
656         getContext().unregisterReceiver(mPreviewPanelDelayReceiver);
657         final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView();
658         if (rootView != null) {
659             rootView.setOnBackKeyListener(null);
660         }
661     }
662 
663     /**
664      * Displays a fragment to the user, temporarily replacing the contents of this fragment.
665      *
666      * @param fragment Fragment instance to be added.
667      */
startImmersiveFragment(@onNull Fragment fragment)668     public void startImmersiveFragment(@NonNull Fragment fragment) {
669         if (DEBUG) {
670             Log.d(TAG, "Starting immersive fragment.");
671         }
672         addOrRemovePreferenceFocusedListener(fragment, true);
673         final FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
674         Fragment target = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
675         fragment.setTargetFragment(target, 0);
676         transaction
677                 .add(R.id.two_panel_fragment_container, fragment)
678                 .remove(target)
679                 .addToBackStack(null)
680                 .commit();
681         mHandler.post(() -> {
682             updateAccessibilityTitle(fragment);
683         });
684 
685     }
686 
687     public static class DummyFragment extends Fragment {
688         @Override
689         public @Nullable
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)690         View onCreateView(LayoutInflater inflater, ViewGroup container,
691                 Bundle savedInstanceState) {
692             return inflater.inflate(R.layout.dummy_fragment, container, false);
693         }
694     }
695 
696     /**
697      * Implement this if fragment needs to handle DPAD_LEFT & DPAD_RIGHT itself in some cases
698      **/
699     public interface NavigationCallback {
700 
701         /**
702          * Returns true if the fragment is in the state that can navigate back on receiving a
703          * navigation DPAD key. When true, TwoPanelSettings will initiate a back operation on
704          * receiving a left key. This method doesn't apply to back key: back key always initiates a
705          * back operation.
706          */
canNavigateBackOnDPAD()707         boolean canNavigateBackOnDPAD();
708 
709         /**
710          * Callback when navigating to preview screen
711          */
onNavigateToPreview()712         void onNavigateToPreview();
713 
714         /**
715          * Callback when returning to previous screen
716          */
onNavigateBack()717         void onNavigateBack();
718     }
719 
720     /**
721      * Implement this if the component (typically a Fragment) is preview-able and would like to get
722      * some lifecycle-like callback(s) when the component becomes the main panel.
723      */
724     public interface PreviewableComponentCallback {
725 
726         /**
727          * Lifecycle-like callback when the component becomes main panel from the preview panel. For
728          * Fragment, this will be invoked right after the preview fragment sliding into the main
729          * panel.
730          *
731          * @param forward means whether the component arrives at main panel when users are
732          *                navigating forwards (deeper into the TvSettings tree).
733          */
onArriveAtMainPanel(boolean forward)734         void onArriveAtMainPanel(boolean forward);
735     }
736 
737     private class RootViewOnKeyListener implements View.OnKeyListener {
738 
739         @Override
onKey(View v, int keyCode, KeyEvent event)740         public boolean onKey(View v, int keyCode, KeyEvent event) {
741             if (!isAdded()) {
742                 Log.d(TAG, "Fragment not attached yet.");
743                 return true;
744             }
745             Fragment prefFragment =
746                     getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
747 
748             if (event.getAction() == KeyEvent.ACTION_DOWN
749                     && (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
750                     || keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) {
751                 Preference preference = getChosenPreference(prefFragment);
752                 if ((preference instanceof SliceSeekbarPreference)) {
753                     SliceSeekbarPreference sbPref = (SliceSeekbarPreference) preference;
754                     if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
755                         onSeekbarPreferenceChanged(sbPref, 1);
756                     } else {
757                         onSeekbarPreferenceChanged(sbPref, -1);
758                     }
759                     return true;
760                 }
761             }
762 
763             if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {
764                 return back(true);
765             }
766 
767             if (event.getAction() == KeyEvent.ACTION_DOWN
768                     && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT)
769                     || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT))) {
770                 if (prefFragment instanceof NavigationCallback
771                         && !((NavigationCallback) prefFragment).canNavigateBackOnDPAD()) {
772                     return false;
773                 }
774                 return back(false);
775             }
776 
777             if (event.getAction() == KeyEvent.ACTION_DOWN
778                     && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)
779                     || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT))) {
780                 forward();
781                 // TODO(b/163432209): improve NavigationCallback and be more specific here.
782                 // Do not consume the KeyEvent for NavigationCallback classes such as date & time
783                 // picker.
784                 return !(prefFragment instanceof NavigationCallback);
785             }
786             return false;
787         }
788     }
789 
forward()790     private void forward() {
791         if (!isAdded()) {
792             Log.d(TAG, "Fragment not attached yet.");
793             return;
794         }
795         final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView();
796         if (shouldPerformClick()) {
797             rootView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
798                     KeyEvent.KEYCODE_DPAD_CENTER));
799             rootView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
800                     KeyEvent.KEYCODE_DPAD_CENTER));
801         } else {
802             Fragment previewFragment = getChildFragmentManager()
803                     .findFragmentById(frameResIds[mPrefPanelIdx + 1]);
804             if (!(previewFragment instanceof InfoFragment)
805                     && !mIsWaitingForUpdatingPreview) {
806                 mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
807                 navigateToPreviewFragment();
808             }
809         }
810     }
811 
shouldPerformClick()812     private boolean shouldPerformClick() {
813         Fragment prefFragment =
814                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
815         Preference preference = getChosenPreference(prefFragment);
816         if (preference == null) {
817             return false;
818         }
819         // This is for the case when a preference has preview but once user navigate to
820         // see the preview, settings actually launch an intent to start external activity.
821         if (preference.getIntent() != null && !TextUtils.isEmpty(preference.getFragment())) {
822             return true;
823         }
824         return preference instanceof SlicePreference
825                 && ((SlicePreference) preference).getSliceAction() != null
826                 && ((SlicePreference) preference).getUri() != null;
827     }
828 
back(boolean isKeyBackPressed)829     private boolean back(boolean isKeyBackPressed) {
830         if (!isAdded()) {
831             Log.d(TAG, "Fragment not attached yet.");
832             return true;
833         }
834         if (mIsNavigatingBack) {
835             mHandler.postDelayed(new Runnable() {
836                 @Override
837                 public void run() {
838                     if (DEBUG) {
839                         Log.d(TAG, "Navigating back is deferred.");
840                     }
841                     back(isKeyBackPressed);
842                 }
843             }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS);
844             return true;
845         }
846         if (DEBUG) {
847             Log.d(TAG, "Going back one level.");
848         }
849 
850         final Fragment immersiveFragment =
851                 getChildFragmentManager().findFragmentById(R.id.two_panel_fragment_container);
852         if (immersiveFragment != null) {
853             getChildFragmentManager().popBackStack();
854             moveToPanel(mPrefPanelIdx, false);
855             return true;
856         }
857 
858         // When a11y is on, we allow InfoFragments to take focus without scrolling panels. So if
859         // the user presses back button in this state, we should not scroll our panels back, or exit
860         // Settings activity, but rather reinstate the focus to be on the main panel.
861         Fragment preview =
862                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
863         if (isA11yOn() && preview instanceof InfoFragment && preview.getView() != null
864                 && preview.getView().hasFocus()) {
865             View mainPanelView = getChildFragmentManager()
866                     .findFragmentById(frameResIds[mPrefPanelIdx]).getView();
867             if (mainPanelView != null) {
868                 mainPanelView.requestFocus();
869                 return true;
870             }
871         }
872 
873         if (mPrefPanelIdx < 1) {
874             // Disallow the user to use "dpad left" to finish activity in the first screen
875             if (isKeyBackPressed) {
876                 getActivity().finish();
877             }
878             return true;
879         }
880 
881         mIsNavigatingBack = true;
882         getChildFragmentManager().popBackStack();
883 
884         mPrefPanelIdx--;
885 
886         mHandler.postDelayed(() -> {
887             if (isKeyBackPressed) {
888                 mAudioManager.playSoundEffect(AudioManager.FX_BACK);
889             } else {
890                 mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
891             }
892             moveToPanel(mPrefPanelIdx, true);
893         }, PANEL_ANIMATION_DELAY_MS);
894 
895         mHandler.postDelayed(() -> {
896             removeFragment(mPrefPanelIdx + 2);
897             mIsNavigatingBack = false;
898             Fragment previewFragment =
899                     getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
900             if (previewFragment instanceof NavigationCallback) {
901                 ((NavigationCallback) previewFragment).onNavigateBack();
902             }
903         }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS);
904         return true;
905     }
906 
removeFragment(int index)907     private void removeFragment(int index) {
908         Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[index]);
909         if (fragment != null) {
910             getChildFragmentManager().beginTransaction().remove(fragment).commit();
911         }
912     }
913 
removeFragmentAndAddToBackStack(int index)914     private void removeFragmentAndAddToBackStack(int index) {
915         if (index < 0) {
916             return;
917         }
918         Fragment removePanel = getChildFragmentManager().findFragmentById(frameResIds[index]);
919         if (removePanel != null) {
920             removePanel.setExitTransition(new Fade());
921             getChildFragmentManager().beginTransaction().remove(removePanel)
922                     .addToBackStack("remove " + removePanel.getClass().getName()).commit();
923         }
924     }
925 
926     /** For RTL layout, we need to know the right edge from where the panels start scrolling. */
computeMaxRightScroll()927     private int computeMaxRightScroll() {
928         int scrollViewWidth = getResources().getDimensionPixelSize(R.dimen.tp_settings_panes_width);
929         int panelWidth = getResources().getDimensionPixelSize(
930                 R.dimen.tp_settings_preference_pane_width);
931         int panelPadding = getResources().getDimensionPixelSize(
932                 R.dimen.preference_pane_extra_padding_start) * 2;
933         int result = frameResIds.length * panelWidth - scrollViewWidth + panelPadding;
934         return result < 0 ? 0 : result;
935     }
936 
937     /** Scrolls such that the panel with given index is the main panel shown on the left. */
moveToPanel(final int index, boolean smoothScroll)938     private void moveToPanel(final int index, boolean smoothScroll) {
939         mHandler.post(() -> {
940             if (DEBUG) {
941                 Log.d(TAG, "Moving to panel " + index);
942             }
943             if (!isAdded()) {
944                 return;
945             }
946             Fragment fragmentToBecomeMainPanel =
947                     getChildFragmentManager().findFragmentById(frameResIds[index]);
948             Fragment fragmentToBecomePreviewPanel =
949                     getChildFragmentManager().findFragmentById(frameResIds[index + 1]);
950             // Positive value means that the panel is scrolling to right (navigate forward for LTR
951             // or navigate backwards for RTL) and vice versa; 0 means that this is likely invoked
952             // by GlobalLayoutListener and there's no actual sliding.
953             int distanceToScrollToRight;
954             int panelWidth = getResources().getDimensionPixelSize(
955                     R.dimen.tp_settings_preference_pane_width);
956             TwoPanelSettingsFrameLayout scrollToPanel = getView().findViewById(frameResIds[index]);
957             TwoPanelSettingsFrameLayout previewPanel = getView().findViewById(
958                     frameResIds[index + 1]);
959             if (scrollToPanel == null || previewPanel == null) {
960                 return;
961             }
962             scrollToPanel.setOnDispatchTouchListener(null);
963             previewPanel.setOnDispatchTouchListener((view, env) -> {
964                 if (env.getActionMasked() == MotionEvent.ACTION_UP) {
965                     forward();
966                 }
967                 return true;
968             });
969             View scrollToPanelHead = scrollToPanel.findViewById(R.id.decor_title_container);
970             View previewPanelHead = previewPanel.findViewById(R.id.decor_title_container);
971             boolean scrollsToPreview =
972                     isRTL() ? mScrollView.getScrollX() >= mMaxScrollX - panelWidth * index
973                             : mScrollView.getScrollX() <= panelWidth * index;
974 
975             boolean setAlphaForPreview = fragmentToBecomePreviewPanel != null
976                     && !(fragmentToBecomePreviewPanel instanceof DummyFragment)
977                     && !(fragmentToBecomePreviewPanel instanceof InfoFragment);
978             int previewPanelColor = getResources().getColor(
979                     R.color.tp_preview_panel_background_color);
980             int mainPanelColor = getResources().getColor(
981                     R.color.tp_preference_panel_background_color);
982             if (smoothScroll) {
983                 int animationEnd = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index;
984                 distanceToScrollToRight = animationEnd - mScrollView.getScrollX();
985                 // Slide animation
986                 ObjectAnimator slideAnim = ObjectAnimator.ofInt(mScrollView, "scrollX",
987                         mScrollView.getScrollX(), animationEnd);
988                 slideAnim.setAutoCancel(true);
989                 slideAnim.setDuration(PANEL_ANIMATION_SLIDE_MS);
990                 slideAnim.addListener(new AnimatorListenerAdapter() {
991                     @Override
992                     public void onAnimationEnd(Animator animation) {
993                         super.onAnimationEnd(animation);
994                         if (isA11yOn() && fragmentToBecomeMainPanel != null
995                                 && fragmentToBecomeMainPanel.getView() != null) {
996                             fragmentToBecomeMainPanel.getView().requestFocus();
997                         }
998                     }
999                 });
1000                 slideAnim.setInterpolator(AnimationUtils.loadInterpolator(
1001                         getContext(), R.anim.easing_browse));
1002                 slideAnim.start();
1003                 // Color animation
1004                 if (scrollsToPreview) {
1005                     previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f);
1006                     previewPanel.setBackgroundColor(previewPanelColor);
1007                     if (previewPanelHead != null) {
1008                         previewPanelHead.setBackgroundColor(previewPanelColor);
1009                     }
1010                     ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(scrollToPanel, "alpha",
1011                             scrollToPanel.getAlpha(), 1f);
1012                     ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(scrollToPanel,
1013                             "backgroundColor",
1014                             new ArgbEvaluator(), previewPanelColor, mainPanelColor);
1015                     alphaAnim.setAutoCancel(true);
1016                     alphaAnim.setDuration(PANEL_ANIMATION_ALPHA_MS);
1017                     backgroundColorAnim.setAutoCancel(true);
1018                     backgroundColorAnim.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS);
1019                     AnimatorSet animatorSet = new AnimatorSet();
1020                     if (scrollToPanelHead != null) {
1021                         ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject(
1022                                 scrollToPanelHead,
1023                                 "backgroundColor",
1024                                 new ArgbEvaluator(), previewPanelColor, mainPanelColor);
1025                         backgroundColorAnimForHead.setAutoCancel(true);
1026                         backgroundColorAnimForHead.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS);
1027                         animatorSet.playTogether(alphaAnim, backgroundColorAnim,
1028                                 backgroundColorAnimForHead);
1029                     } else {
1030                         animatorSet.playTogether(alphaAnim, backgroundColorAnim);
1031                     }
1032                     animatorSet.setInterpolator(AnimationUtils.loadInterpolator(
1033                             getContext(), R.anim.easing_browse));
1034                     animatorSet.start();
1035                 } else {
1036                     scrollToPanel.setAlpha(1f);
1037                     scrollToPanel.setBackgroundColor(mainPanelColor);
1038                     if (scrollToPanelHead != null) {
1039                         scrollToPanelHead.setBackgroundColor(mainPanelColor);
1040                     }
1041                     ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(previewPanel, "alpha",
1042                             previewPanel.getAlpha(), setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f);
1043                     ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(previewPanel,
1044                             "backgroundColor",
1045                             new ArgbEvaluator(), mainPanelColor, previewPanelColor);
1046                     alphaAnim.setAutoCancel(true);
1047                     alphaAnim.setDuration(PANEL_ANIMATION_ALPHA_MS);
1048                     backgroundColorAnim.setAutoCancel(true);
1049                     backgroundColorAnim.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS);
1050                     AnimatorSet animatorSet = new AnimatorSet();
1051                     if (previewPanelHead != null) {
1052                         ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject(
1053                                 previewPanelHead,
1054                                 "backgroundColor",
1055                                 new ArgbEvaluator(), mainPanelColor, previewPanelColor);
1056                         backgroundColorAnimForHead.setAutoCancel(true);
1057                         backgroundColorAnimForHead.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS);
1058                         animatorSet.playTogether(alphaAnim, backgroundColorAnim,
1059                                 backgroundColorAnimForHead);
1060                     } else {
1061                         animatorSet.playTogether(alphaAnim, backgroundColorAnim);
1062                     }
1063                     animatorSet.setInterpolator(AnimationUtils.loadInterpolator(
1064                             getContext(), R.anim.easing_browse));
1065                     animatorSet.start();
1066                 }
1067             } else {
1068                 int scrollToX = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index;
1069                 distanceToScrollToRight = scrollToX - mScrollView.getScrollX();
1070                 mScrollView.scrollTo(scrollToX, 0);
1071                 previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f);
1072                 previewPanel.setBackgroundColor(previewPanelColor);
1073                 if (previewPanelHead != null) {
1074                     previewPanelHead.setBackgroundColor(previewPanelColor);
1075                 }
1076                 scrollToPanel.setAlpha(1f);
1077                 scrollToPanel.setBackgroundColor(mainPanelColor);
1078                 if (scrollToPanelHead != null) {
1079                     scrollToPanelHead.setBackgroundColor(mainPanelColor);
1080                 }
1081             }
1082             if (fragmentToBecomeMainPanel != null && fragmentToBecomeMainPanel.getView() != null) {
1083                 if (!isA11yOn()) {
1084                     fragmentToBecomeMainPanel.getView().requestFocus();
1085                 }
1086                 for (int resId : frameResIds) {
1087                     Fragment f = getChildFragmentManager().findFragmentById(resId);
1088                     if (f != null) {
1089                         View view = f.getView();
1090                         if (view != null) {
1091                             view.setImportantForAccessibility(
1092                                     f == fragmentToBecomeMainPanel || f instanceof InfoFragment
1093                                             ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
1094                                             : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
1095                         }
1096                     }
1097                 }
1098                 if (fragmentToBecomeMainPanel instanceof PreviewableComponentCallback) {
1099                     if (distanceToScrollToRight > 0) {
1100                         ((PreviewableComponentCallback) fragmentToBecomeMainPanel)
1101                                 .onArriveAtMainPanel(!isRTL());
1102                     } else if (distanceToScrollToRight < 0) {
1103                         ((PreviewableComponentCallback) fragmentToBecomeMainPanel)
1104                                 .onArriveAtMainPanel(isRTL());
1105                     } // distanceToScrollToRight being 0 means no actual panel sliding; thus noop.
1106                 }
1107                 updateAccessibilityTitle(fragmentToBecomeMainPanel);
1108             }
1109         });
1110     }
1111 
getInitialPreviewFragment(Fragment fragment)1112     private Fragment getInitialPreviewFragment(Fragment fragment) {
1113         if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) {
1114             return null;
1115         }
1116 
1117         LeanbackPreferenceFragmentCompat leanbackPreferenceFragment =
1118                 (LeanbackPreferenceFragmentCompat) fragment;
1119         if (leanbackPreferenceFragment.getListView() == null) {
1120             return null;
1121         }
1122 
1123         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
1124         int position = listView.getSelectedPosition();
1125         PreferenceGroupAdapter adapter =
1126                 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter());
1127         if (adapter == null) {
1128             return null;
1129         }
1130         Preference chosenPreference = adapter.getItem(position);
1131         // Find the first focusable preference if cannot find the selected preference
1132         if (chosenPreference == null || (listView.findViewHolderForPosition(position) != null
1133                 && !listView.findViewHolderForPosition(position).itemView.hasFocusable())) {
1134             chosenPreference = null;
1135             for (int i = 0; i < listView.getChildCount(); i++) {
1136                 View view = listView.getChildAt(i);
1137                 if (view.hasFocusable()) {
1138                     PreferenceViewHolder viewHolder =
1139                             (PreferenceViewHolder) listView.getChildViewHolder(view);
1140                     chosenPreference = adapter.getItem(viewHolder.getAdapterPosition());
1141                     break;
1142                 }
1143             }
1144         }
1145 
1146         if (chosenPreference == null) {
1147             return null;
1148         }
1149         return onCreatePreviewFragment(fragment, chosenPreference);
1150     }
1151 
1152     /**
1153      * Refocus the current selected preference. When a preference is selected and its InfoFragment
1154      * slice data changes. We need to call this method to make sure InfoFragment updates in time.
1155      * This is also helpful in refreshing preview of ListPreference.
1156      */
refocusPreference(Fragment fragment)1157     public void refocusPreference(Fragment fragment) {
1158         if (!isFragmentInTheMainPanel(fragment)) {
1159             return;
1160         }
1161         Preference chosenPreference = getChosenPreference(fragment);
1162         try {
1163             if (chosenPreference != null) {
1164                 if (chosenPreference.getFragment() != null
1165                         && InfoFragment.class.isAssignableFrom(
1166                         Class.forName(chosenPreference.getFragment()))) {
1167                     updateInfoFragmentStatus(fragment);
1168                 }
1169                 if (chosenPreference instanceof ListPreference) {
1170                     refocusPreferenceForceRefresh(chosenPreference, fragment);
1171                 }
1172             }
1173         } catch (ClassNotFoundException e) {
1174             e.printStackTrace();
1175         }
1176     }
1177 
1178     /** Force refresh preview panel. */
refocusPreferenceForceRefresh(Preference chosenPreference, Fragment fragment)1179     public void refocusPreferenceForceRefresh(Preference chosenPreference, Fragment fragment) {
1180         if (!isFragmentInTheMainPanel(fragment)) {
1181             return;
1182         }
1183         onPreferenceFocusedImpl(chosenPreference, true, mPrefPanelIdx);
1184     }
1185 
1186     /** Show error message in preview panel **/
showErrorMessage(String errorMessage, Fragment fragment)1187     public void showErrorMessage(String errorMessage, Fragment fragment) {
1188         Fragment prefFragment =
1189                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
1190         if (fragment == prefFragment) {
1191             // If user has already navigated to the preview screen, main panel screen should be
1192             // updated to new InFoFragment. Create a fake preference to work around this case.
1193             Preference preference = new Preference(getContext());
1194             updatePreferenceWithErrorMessage(preference, errorMessage, getContext());
1195             Fragment newPrefFragment = onCreatePreviewFragment(null, preference);
1196             final FragmentTransaction transaction =
1197                     getChildFragmentManager().beginTransaction();
1198             transaction.setCustomAnimations(R.animator.fade_in_preview_panel,
1199                     R.animator.fade_out_preview_panel);
1200             transaction.replace(frameResIds[mPrefPanelIdx], newPrefFragment);
1201             transaction.commit();
1202         } else {
1203             Preference preference = getChosenPreference(prefFragment);
1204             if (preference != null) {
1205                 if (isA11yOn()) {
1206                     appendErrorToContentDescription(prefFragment, errorMessage);
1207                 }
1208                 updatePreferenceWithErrorMessage(preference, errorMessage, getContext());
1209                 onPreferenceFocused(preference, mPrefPanelIdx);
1210             }
1211         }
1212     }
1213 
updatePreferenceWithErrorMessage( Preference preference, String errorMessage, Context context)1214     private static void updatePreferenceWithErrorMessage(
1215             Preference preference, String errorMessage, Context context) {
1216         preference.setFragment(InfoFragment.class.getCanonicalName());
1217         Bundle b = preference.getExtras();
1218         b.putParcelable(EXTRA_PREFERENCE_INFO_TITLE_ICON,
1219                 Icon.createWithResource(context, R.drawable.slice_error_icon));
1220         b.putCharSequence(EXTRA_PREFERENCE_INFO_TEXT,
1221                 context.getString(R.string.status_unavailable));
1222         b.putCharSequence(EXTRA_PREFERENCE_INFO_SUMMARY, errorMessage);
1223     }
1224 
appendErrorToContentDescription(Fragment fragment, String errorMessage)1225     private void appendErrorToContentDescription(Fragment fragment, String errorMessage) {
1226         Preference preference = getChosenPreference(fragment);
1227 
1228         String errorMessageContentDescription = "";
1229         if (preference.getTitle() != null) {
1230             errorMessageContentDescription += preference.getTitle().toString();
1231         }
1232 
1233         errorMessageContentDescription +=
1234                 HasCustomContentDescription.CONTENT_DESCRIPTION_SEPARATOR
1235                         + getString(R.string.status_unavailable)
1236                         + HasCustomContentDescription.CONTENT_DESCRIPTION_SEPARATOR + errorMessage;
1237 
1238         if (preference instanceof SlicePreference) {
1239             ((SlicePreference) preference).setContentDescription(errorMessageContentDescription);
1240         } else if (preference instanceof SliceSwitchPreference) {
1241             ((SliceSwitchPreference) preference)
1242                     .setContentDescription(errorMessageContentDescription);
1243         } else if (preference instanceof CustomContentDescriptionPreference) {
1244             ((CustomContentDescriptionPreference) preference)
1245                     .setContentDescription(errorMessageContentDescription);
1246         }
1247 
1248         LeanbackPreferenceFragmentCompat leanbackPreferenceFragment =
1249                 (LeanbackPreferenceFragmentCompat) fragment;
1250         if (leanbackPreferenceFragment.getListView() != null
1251                 && leanbackPreferenceFragment.getListView().getAdapter() != null) {
1252             leanbackPreferenceFragment.getListView().getAdapter().notifyDataSetChanged();
1253         }
1254     }
1255 
updateInfoFragmentStatus(Fragment fragment)1256     private void updateInfoFragmentStatus(Fragment fragment) {
1257         if (!isFragmentInTheMainPanel(fragment)) {
1258             return;
1259         }
1260         final Fragment existingPreviewFragment =
1261                 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]);
1262         if (existingPreviewFragment instanceof InfoFragment) {
1263             ((InfoFragment) existingPreviewFragment).updateInfoFragment();
1264         }
1265     }
1266 
1267     /** Get the current chosen preference. */
getChosenPreference(Fragment fragment)1268     public static Preference getChosenPreference(Fragment fragment) {
1269         if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) {
1270             return null;
1271         }
1272 
1273         LeanbackPreferenceFragmentCompat leanbackPreferenceFragment =
1274                 (LeanbackPreferenceFragmentCompat) fragment;
1275         if (leanbackPreferenceFragment.getListView() == null) {
1276             return null;
1277         }
1278 
1279         VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView();
1280         int position = listView.getSelectedPosition();
1281         PreferenceGroupAdapter adapter =
1282                 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter());
1283         return adapter != null ? adapter.getItem(position) : null;
1284     }
1285 
1286     /** Creates preview preference fragment. */
onCreatePreviewFragment(Fragment caller, Preference preference)1287     public Fragment onCreatePreviewFragment(Fragment caller, Preference preference) {
1288         if (preference == null) {
1289             return null;
1290         }
1291         if (preference.getFragment() != null) {
1292             if (!isInfoFragment(preference.getFragment())
1293                     && !isPreferenceFragment(preference.getFragment())) {
1294                 return null;
1295             }
1296             if (isPreferenceFragment(preference.getFragment())
1297                     && preference instanceof HasSliceUri) {
1298                 HasSliceUri slicePref = (HasSliceUri) preference;
1299                 if (slicePref.getUri() == null || !isUriValid(slicePref.getUri())) {
1300                     return null;
1301                 }
1302                 Bundle b = preference.getExtras();
1303                 b.putString(SlicesConstants.TAG_TARGET_URI, slicePref.getUri());
1304                 b.putCharSequence(SlicesConstants.TAG_SCREEN_TITLE, preference.getTitle());
1305             }
1306             return Fragment.instantiate(getActivity(), preference.getFragment(),
1307                     preference.getExtras());
1308         } else {
1309             Fragment f = null;
1310             if (preference instanceof ListPreference
1311                     && ((ListPreference) preference).getEntries() != null) {
1312                 f = TwoPanelListPreferenceDialogFragment.newInstanceSingle(preference.getKey());
1313             } else if (preference instanceof MultiSelectListPreference
1314                     && ((MultiSelectListPreference) preference).getEntries() != null) {
1315                 f = LeanbackListPreferenceDialogFragmentCompat.newInstanceMulti(
1316                         preference.getKey());
1317             }
1318             if (f != null && caller != null) {
1319                 f.setTargetFragment(caller, 0);
1320             }
1321             return f;
1322         }
1323     }
1324 
isUriValid(String uri)1325     private boolean isUriValid(String uri) {
1326         if (uri == null) {
1327             return false;
1328         }
1329         ContentProviderClient client =
1330                 getContext().getContentResolver().acquireContentProviderClient(Uri.parse(uri));
1331         if (client != null) {
1332             client.close();
1333             return true;
1334         } else {
1335             return false;
1336         }
1337     }
1338 
1339     /**
1340      * Add focus listener to the child fragment. It must always be called after
1341      * the child fragment view is created since the listener is attached to the
1342      * {@link VerticalGridView} in the child fragment view.
1343      */
addListenerForFragment(Fragment fragment)1344     public void addListenerForFragment(Fragment fragment) {
1345         if (isFragmentInTheMainPanel(fragment)) {
1346             addOrRemovePreferenceFocusedListener(fragment, true);
1347         }
1348     }
1349 
1350     /** Remove focus listener from the child fragment **/
removeListenerForFragment(Fragment fragment)1351     public void removeListenerForFragment(Fragment fragment) {
1352         addOrRemovePreferenceFocusedListener(fragment, false);
1353     }
1354 
1355     /** Check if fragment is in the main panel **/
isFragmentInTheMainPanel(Fragment fragment)1356     public boolean isFragmentInTheMainPanel(Fragment fragment) {
1357         return fragment == getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]);
1358     }
1359 }
1360