| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.twopanelsettings; |
| |
| import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_SUMMARY; |
| import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_TEXT; |
| import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_TITLE_ICON; |
| |
| import android.animation.AnimatorSet; |
| import android.animation.ArgbEvaluator; |
| import android.animation.ObjectAnimator; |
| import android.content.ContentProviderClient; |
| import android.graphics.drawable.Icon; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.text.TextUtils; |
| import android.transition.Fade; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver.OnGlobalLayoutListener; |
| import android.widget.HorizontalScrollView; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.fragment.app.Fragment; |
| import androidx.fragment.app.FragmentTransaction; |
| import androidx.leanback.preference.LeanbackListPreferenceDialogFragmentCompat; |
| import androidx.leanback.preference.LeanbackPreferenceFragmentCompat; |
| import androidx.leanback.widget.OnChildViewHolderSelectedListener; |
| import androidx.leanback.widget.VerticalGridView; |
| import androidx.preference.ListPreference; |
| import androidx.preference.MultiSelectListPreference; |
| import androidx.preference.Preference; |
| import androidx.preference.PreferenceFragmentCompat; |
| import androidx.preference.PreferenceGroupAdapter; |
| import androidx.preference.PreferenceViewHolder; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.tv.twopanelsettings.slices.HasSliceUri; |
| import com.android.tv.twopanelsettings.slices.InfoFragment; |
| import com.android.tv.twopanelsettings.slices.SlicePreference; |
| import com.android.tv.twopanelsettings.slices.SlicesConstants; |
| |
| import java.util.Set; |
| |
| /** |
| * This fragment provides containers for displaying two {@link LeanbackPreferenceFragmentCompat}. |
| * The preference fragment on the left works as a main panel on which the user can operate. |
| * The preference fragment on the right works as a preview panel for displaying the preview |
| * information. |
| */ |
| public abstract class TwoPanelSettingsFragment extends Fragment implements |
| PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, |
| PreferenceFragmentCompat.OnPreferenceStartScreenCallback, |
| PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback { |
| private static final String TAG = "TwoPanelSettingsFragment"; |
| private static final boolean DEBUG = false; |
| private static final String PREVIEW_FRAGMENT_TAG = |
| "com.android.tv.settings.TwoPanelSettingsFragment.PREVIEW_FRAGMENT"; |
| private static final String PREFERENCE_FRAGMENT_TAG = |
| "com.android.tv.settings.TwoPanelSettingsFragment.PREFERENCE_FRAGMENT"; |
| private static final String EXTRA_PREF_PANEL_IDX = |
| "com.android.tv.twopanelsettings.PREF_PANEL_IDX"; |
| private static final int[] frameResIds = |
| {R.id.frame1, R.id.frame2, R.id.frame3, R.id.frame4, R.id.frame5, R.id.frame6, |
| R.id.frame7, R.id.frame8, R.id.frame9, R.id.frame10}; |
| |
| private static final long PANEL_ANIMATION_MS = 400; |
| private static final long PANEL_ANIMATION_DELAY_MS = 200; |
| private static final float PREVIEW_PANEL_ALPHA = 0.6f; |
| |
| private int mMaxScrollX; |
| private final RootViewOnKeyListener mRootViewOnKeyListener = new RootViewOnKeyListener(); |
| private int mPrefPanelIdx; |
| private HorizontalScrollView mScrollView; |
| private Handler mHandler; |
| private boolean mIsNavigatingBack; |
| |
| private OnChildViewHolderSelectedListener mOnChildViewHolderSelectedListener = |
| new OnChildViewHolderSelectedListener() { |
| @Override |
| public void onChildViewHolderSelected(RecyclerView parent, |
| RecyclerView.ViewHolder child, int position, int subposition) { |
| if (child == null) { |
| return; |
| } |
| int adapterPosition = child.getAdapterPosition(); |
| PreferenceGroupAdapter preferenceGroupAdapter = |
| (PreferenceGroupAdapter) parent.getAdapter(); |
| Preference preference = preferenceGroupAdapter.getItem(adapterPosition); |
| onPreferenceFocused(preference); |
| } |
| |
| @Override |
| public void onChildViewHolderSelectedAndPositioned(RecyclerView parent, |
| RecyclerView.ViewHolder child, int position, int subposition) { |
| } |
| }; |
| |
| private OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() { |
| @Override |
| public void onGlobalLayout() { |
| getView().getViewTreeObserver().removeOnGlobalLayoutListener(mOnGlobalLayoutListener); |
| moveToPanel(mPrefPanelIdx, false); |
| } |
| }; |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| final View v = inflater.inflate(R.layout.two_panel_settings_fragment, container, false); |
| mScrollView = v.findViewById(R.id.scrollview); |
| mHandler = new Handler(); |
| if (savedInstanceState != null) { |
| mPrefPanelIdx = savedInstanceState.getInt(EXTRA_PREF_PANEL_IDX, mPrefPanelIdx); |
| // Move to correct panel once global layout finishes. |
| v.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); |
| } |
| mMaxScrollX = computeMaxRightScroll(); |
| return v; |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| outState.putInt(EXTRA_PREF_PANEL_IDX, mPrefPanelIdx); |
| super.onSaveInstanceState(outState); |
| } |
| |
| @Override |
| public void onViewCreated(View view, Bundle savedInstanceState) { |
| super.onViewCreated(view, savedInstanceState); |
| if (savedInstanceState == null) { |
| onPreferenceStartInitialScreen(); |
| } |
| } |
| |
| /** Extend this method to provide the initial screen **/ |
| public abstract void onPreferenceStartInitialScreen(); |
| |
| private boolean isPreferenceFragment(String fragment) { |
| try { |
| return LeanbackPreferenceFragmentCompat.class.isAssignableFrom(Class.forName(fragment)); |
| } catch (ClassNotFoundException e) { |
| Log.e(TAG, "Fragment class not found " + e); |
| return false; |
| } |
| } |
| |
| private boolean isInfoFragment(String fragment) { |
| try { |
| return InfoFragment.class.isAssignableFrom(Class.forName(fragment)); |
| } catch (ClassNotFoundException e) { |
| Log.e(TAG, "Fragment class not found " + e); |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { |
| if (DEBUG) { |
| Log.d(TAG, "onPreferenceStartFragment " + pref.getTitle()); |
| } |
| if (pref.getFragment() == null) { |
| return false; |
| } |
| Fragment preview = getChildFragmentManager().findFragmentById( |
| frameResIds[mPrefPanelIdx + 1]); |
| if (preview != null && !(preview instanceof DummyFragment)) { |
| if (!(preview instanceof InfoFragment)) { |
| navigateToPreviewFragment(); |
| } |
| } else { |
| // If there is no corresponding slice provider, thus the corresponding fragment is not |
| // created, return false to check the intent of the SlicePreference. |
| if (pref instanceof SlicePreference) { |
| return false; |
| } |
| try { |
| Fragment immersiveFragment = Fragment.instantiate(getActivity(), pref.getFragment(), |
| pref.getExtras()); |
| startImmersiveFragment(immersiveFragment); |
| } catch (Exception e) { |
| Log.e(TAG, "error trying to instantiate fragment " + e); |
| // return true so it won't be handled by onPreferenceTreeClick in PreferenceFragment |
| return true; |
| } |
| } |
| return true; |
| } |
| |
| /** Navigate back to the previous fragment **/ |
| public void navigateBack() { |
| back(false); |
| } |
| |
| /** Navigate into current preview fragment */ |
| public void navigateToPreviewFragment() { |
| Fragment previewFragment = getChildFragmentManager().findFragmentById( |
| frameResIds[mPrefPanelIdx + 1]); |
| if (previewFragment instanceof NavigationCallback) { |
| ((NavigationCallback) previewFragment).onNavigateToPreview(); |
| } |
| if (previewFragment == null || previewFragment instanceof DummyFragment) { |
| return; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "navigateToPreviewFragment"); |
| } |
| if (mPrefPanelIdx + 1 >= frameResIds.length) { |
| Log.w(TAG, "Maximum level of depth reached."); |
| return; |
| } |
| Fragment initialPreviewFragment = getInitialPreviewFragment(previewFragment); |
| if (initialPreviewFragment == null) { |
| initialPreviewFragment = new DummyFragment(); |
| } |
| initialPreviewFragment.setExitTransition(null); |
| |
| mPrefPanelIdx++; |
| |
| Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| addOrRemovePreferenceFocusedListener(fragment, true); |
| |
| final FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); |
| transaction.replace(frameResIds[mPrefPanelIdx + 1], initialPreviewFragment, |
| PREVIEW_FRAGMENT_TAG); |
| transaction.commit(); |
| |
| moveToPanel(mPrefPanelIdx, true); |
| removeFragmentAndAddToBackStack(mPrefPanelIdx - 1); |
| } |
| |
| private void addOrRemovePreferenceFocusedListener(Fragment fragment, boolean isAddingListener) { |
| if (fragment == null || !(fragment instanceof LeanbackPreferenceFragmentCompat)) { |
| return; |
| } |
| LeanbackPreferenceFragmentCompat leanbackPreferenceFragment = |
| (LeanbackPreferenceFragmentCompat) fragment; |
| VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView(); |
| if (listView != null) { |
| if (isAddingListener) { |
| listView.setOnChildViewHolderSelectedListener(mOnChildViewHolderSelectedListener); |
| } else { |
| listView.setOnChildViewHolderSelectedListener(null); |
| } |
| } |
| } |
| |
| /** |
| * Displays left panel preference fragment to the user. |
| * |
| * @param fragment Fragment instance to be added. |
| */ |
| public void startPreferenceFragment(@NonNull Fragment fragment) { |
| if (DEBUG) { |
| Log.d(TAG, "startPreferenceFragment"); |
| } |
| FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); |
| transaction.add(frameResIds[mPrefPanelIdx], fragment, PREFERENCE_FRAGMENT_TAG); |
| transaction.commitNow(); |
| |
| Fragment initialPreviewFragment = getInitialPreviewFragment(fragment); |
| if (initialPreviewFragment == null) { |
| initialPreviewFragment = new DummyFragment(); |
| } |
| initialPreviewFragment.setExitTransition(null); |
| |
| transaction = getChildFragmentManager().beginTransaction(); |
| transaction.add(frameResIds[mPrefPanelIdx + 1], initialPreviewFragment, |
| initialPreviewFragment.getClass().toString()); |
| transaction.commit(); |
| } |
| |
| @Override |
| public boolean onPreferenceDisplayDialog( |
| @NonNull PreferenceFragmentCompat caller, Preference pref) { |
| if (DEBUG) { |
| Log.d(TAG, "PreferenceDisplayDialog"); |
| } |
| if (caller == null) { |
| throw new IllegalArgumentException("Cannot display dialog for preference " + pref |
| + ", Caller must not be null!"); |
| } |
| Fragment preview = getChildFragmentManager().findFragmentById( |
| frameResIds[mPrefPanelIdx + 1]); |
| if (preview != null && !(preview instanceof DummyFragment)) { |
| if (preview instanceof NavigationCallback) { |
| ((NavigationCallback) preview).onNavigateToPreview(); |
| } |
| mPrefPanelIdx++; |
| moveToPanel(mPrefPanelIdx, true); |
| removeFragmentAndAddToBackStack(mPrefPanelIdx - 1); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean equalArguments(Bundle a, Bundle b) { |
| if (a == null && b == null) { |
| return true; |
| } |
| if (a == null || b == null) { |
| return false; |
| } |
| Set<String> aks = a.keySet(); |
| Set<String> bks = b.keySet(); |
| if (a.size() != b.size()) { |
| return false; |
| } |
| if (!aks.containsAll(bks)) { |
| return false; |
| } |
| for (String key : aks) { |
| if (a.get(key) == null && b.get(key) == null) { |
| continue; |
| } |
| if (a.get(key) == null || b.get(key) == null) { |
| return false; |
| } |
| if (!a.get(key).equals(b.get(key))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** Callback from SliceFragment **/ |
| public interface SliceFragmentCallback { |
| /** Triggered when preference is focused **/ |
| void onPreferenceFocused(Preference preference); |
| } |
| |
| protected boolean onPreferenceFocused(Preference pref) { |
| return onPreferenceFocusedImpl(pref, false); |
| } |
| |
| private boolean onPreferenceFocusedImpl(Preference pref, boolean forceRefresh) { |
| if (DEBUG) { |
| Log.d(TAG, "onPreferenceFocused " + pref.getTitle()); |
| } |
| final Fragment prefFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| if (prefFragment instanceof SliceFragmentCallback) { |
| ((SliceFragmentCallback) prefFragment).onPreferenceFocused(pref); |
| } |
| Fragment previewFragment = null; |
| try { |
| previewFragment = onCreatePreviewFragment(prefFragment, pref); |
| } catch (Exception e) { |
| Log.w(TAG, "Cannot instantiate the fragment from preference: " + pref, e); |
| } |
| if (previewFragment == null) { |
| previewFragment = new DummyFragment(); |
| } else { |
| previewFragment.setTargetFragment(prefFragment, 0); |
| } |
| |
| final Fragment existingPreviewFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]); |
| if (existingPreviewFragment != null |
| && existingPreviewFragment.getClass().equals(previewFragment.getClass()) |
| && equalArguments(existingPreviewFragment.getArguments(), |
| previewFragment.getArguments())) { |
| if (isRTL() && mScrollView.getScrollX() == 0 && mPrefPanelIdx == 0) { |
| // For RTL we need to reclaim focus to the correct scroll position if a pref |
| // launches a new activity because the horizontal scroll goes back to 0. |
| getView().getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); |
| } |
| if (!forceRefresh) { |
| return true; |
| } |
| } |
| |
| // If the existing preview fragment is recreated when the activity is recreated, the |
| // animation would fall back to "slide left", in this case, we need to set the exit |
| // transition. |
| if (existingPreviewFragment != null) { |
| existingPreviewFragment.setExitTransition(null); |
| } |
| previewFragment.setEnterTransition(new Fade()); |
| previewFragment.setExitTransition(null); |
| |
| final FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); |
| transaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out); |
| transaction.replace(frameResIds[mPrefPanelIdx + 1], previewFragment); |
| transaction.commit(); |
| |
| // Some fragments may steal focus on creation. Reclaim focus on main fragment. |
| getView().getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); |
| return true; |
| } |
| |
| private boolean isRTL() { |
| return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; |
| } |
| |
| @Override |
| public void onResume() { |
| if (DEBUG) { |
| Log.d(TAG, "onResume"); |
| } |
| super.onResume(); |
| // Trap back button presses |
| final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView(); |
| if (rootView != null) { |
| rootView.setOnBackKeyListener(mRootViewOnKeyListener); |
| } |
| } |
| |
| @Override |
| public void onPause() { |
| if (DEBUG) { |
| Log.d(TAG, "onPause"); |
| } |
| super.onPause(); |
| final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView(); |
| if (rootView != null) { |
| rootView.setOnBackKeyListener(null); |
| } |
| } |
| |
| /** |
| * Displays a fragment to the user, temporarily replacing the contents of this fragment. |
| * |
| * @param fragment Fragment instance to be added. |
| */ |
| public void startImmersiveFragment(@NonNull Fragment fragment) { |
| if (DEBUG) { |
| Log.d(TAG, "Starting immersive fragment."); |
| } |
| final FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); |
| Fragment target = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| fragment.setTargetFragment(target, 0); |
| transaction |
| .add(R.id.two_panel_fragment_container, fragment) |
| .remove(target) |
| .addToBackStack(null) |
| .commit(); |
| } |
| |
| public static class DummyFragment extends Fragment { |
| @Override |
| public @Nullable |
| View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| return inflater.inflate(R.layout.dummy_fragment, container, false); |
| } |
| } |
| |
| /** |
| * Implement this if fragment needs to handle DPAD_LEFT & DPAD_RIGHT itself in some cases |
| **/ |
| public interface NavigationCallback { |
| |
| /** |
| * Returns true if the fragment is in the state that can navigate back on receiving a |
| * navigation DPAD key. When true, TwoPanelSettings will initiate a back operation on |
| * receiving a left key. This method doesn't apply to back key: back key always initiates a |
| * back operation. |
| */ |
| boolean canNavigateBackOnDPAD(); |
| |
| /** |
| * Callback when navigating to preview screen |
| */ |
| void onNavigateToPreview(); |
| |
| /** |
| * Callback when returning to previous screen |
| */ |
| void onNavigateBack(); |
| } |
| |
| /** |
| * Implement this if the component (typically a Fragment) is preview-able and would like to get |
| * some lifecycle-like callback(s) when the component becomes the main panel. |
| */ |
| public interface PreviewableComponentCallback { |
| |
| /** |
| * Lifecycle-like callback when the component becomes main panel from the preview panel. For |
| * Fragment, this will be invoked right after the preview fragment sliding into the main |
| * panel. |
| * |
| * @param forward means whether the component arrives at main panel when users are |
| * navigating forwards (deeper into the TvSettings tree). |
| */ |
| void onArriveAtMainPanel(boolean forward); |
| } |
| |
| private class RootViewOnKeyListener implements View.OnKeyListener { |
| |
| @Override |
| public boolean onKey(View v, int keyCode, KeyEvent event) { |
| Fragment prefFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { |
| return back(true); |
| } |
| |
| if (event.getAction() == KeyEvent.ACTION_DOWN |
| && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT) |
| || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT))) { |
| if (prefFragment instanceof NavigationCallback |
| && !((NavigationCallback) prefFragment).canNavigateBackOnDPAD()) { |
| return false; |
| } |
| return back(false); |
| } |
| |
| if (event.getAction() == KeyEvent.ACTION_DOWN |
| && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) |
| || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT))) { |
| if (shouldPerformClick()) { |
| v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, |
| KeyEvent.KEYCODE_DPAD_CENTER)); |
| v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, |
| KeyEvent.KEYCODE_DPAD_CENTER)); |
| } else { |
| Fragment previewFragment = getChildFragmentManager() |
| .findFragmentById(frameResIds[mPrefPanelIdx + 1]); |
| if (!(previewFragment instanceof InfoFragment)) { |
| navigateToPreviewFragment(); |
| } |
| } |
| // TODO(b/163432209): improve NavigationCallback and be more specific here. |
| // Do not consume the KeyEvent for NavigationCallback classes such as date & time |
| // picker. |
| if (prefFragment instanceof NavigationCallback) { |
| return false; |
| } |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| private boolean shouldPerformClick() { |
| Fragment prefFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| Preference preference = getChosenPreference(prefFragment); |
| if (preference == null) { |
| return false; |
| } |
| // This is for the case when a preference has preview but once user navigate to |
| // see the preview, settings actually launch an intent to start external activity. |
| if (preference.getIntent() != null && !TextUtils.isEmpty(preference.getFragment())) { |
| return true; |
| } |
| if (preference instanceof SlicePreference |
| && ((SlicePreference) preference).getSliceAction() != null |
| && ((SlicePreference) preference).getUri() != null) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean back(boolean isKeyBackPressed) { |
| if (mIsNavigatingBack) { |
| mHandler.postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| if (DEBUG) { |
| Log.d(TAG, "Navigating back is deferred."); |
| } |
| back(isKeyBackPressed); |
| } |
| }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS); |
| return true; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "Going back one level."); |
| } |
| |
| final Fragment immersiveFragment = |
| getChildFragmentManager().findFragmentById(R.id.two_panel_fragment_container); |
| if (immersiveFragment != null) { |
| getChildFragmentManager().popBackStack(); |
| moveToPanel(mPrefPanelIdx, false); |
| return true; |
| } |
| |
| if (mPrefPanelIdx < 1) { |
| // Disallow the user to use "dpad left" to finish activity in the first screen |
| if (isKeyBackPressed) { |
| getActivity().finish(); |
| } |
| return true; |
| } |
| |
| mIsNavigatingBack = true; |
| Fragment preferenceFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| addOrRemovePreferenceFocusedListener(preferenceFragment, false); |
| getChildFragmentManager().popBackStack(); |
| |
| mPrefPanelIdx--; |
| |
| mHandler.postDelayed(() -> { |
| moveToPanel(mPrefPanelIdx, true); |
| }, PANEL_ANIMATION_DELAY_MS); |
| |
| mHandler.postDelayed(() -> { |
| removeFragment(mPrefPanelIdx + 2); |
| mIsNavigatingBack = false; |
| Fragment previewFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]); |
| if (previewFragment instanceof NavigationCallback) { |
| ((NavigationCallback) previewFragment).onNavigateBack(); |
| } |
| }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS); |
| return true; |
| } |
| |
| private void removeFragment(int index) { |
| Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[index]); |
| if (fragment != null) { |
| getChildFragmentManager().beginTransaction().remove(fragment).commit(); |
| } |
| } |
| |
| private void removeFragmentAndAddToBackStack(int index) { |
| if (index < 0) { |
| return; |
| } |
| Fragment removePanel = getChildFragmentManager().findFragmentById(frameResIds[index]); |
| if (removePanel != null) { |
| removePanel.setExitTransition(new Fade()); |
| getChildFragmentManager().beginTransaction().remove(removePanel) |
| .addToBackStack("remove " + removePanel.getClass().getName()).commit(); |
| } |
| } |
| |
| /** For RTL layout, we need to know the right edge from where the panels start scrolling. */ |
| private int computeMaxRightScroll() { |
| int scrollViewWidth = getResources().getDimensionPixelSize(R.dimen.tp_settings_panes_width); |
| int panelWidth = getResources().getDimensionPixelSize( |
| R.dimen.tp_settings_preference_pane_width); |
| int panelPadding = getResources().getDimensionPixelSize( |
| R.dimen.preference_pane_extra_padding_start) * 2; |
| int result = frameResIds.length * panelWidth - scrollViewWidth + panelPadding; |
| return result < 0 ? 0 : result; |
| } |
| |
| /** Scrolls such that the panel with given index is the main panel shown on the left. */ |
| private void moveToPanel(final int index, boolean smoothScroll) { |
| mHandler.post(() -> { |
| if (DEBUG) { |
| Log.d(TAG, "Moving to panel " + index); |
| } |
| if (!isAdded()) { |
| return; |
| } |
| Fragment fragmentToBecomeMainPanel = |
| getChildFragmentManager().findFragmentById(frameResIds[index]); |
| Fragment fragmentToBecomePreviewPanel = |
| getChildFragmentManager().findFragmentById(frameResIds[index + 1]); |
| // Positive value means that the panel is scrolling to right (navigate forward for LTR |
| // or navigate backwards for RTL) and vice versa; 0 means that this is likely invoked |
| // by GlobalLayoutListener and there's no actual sliding. |
| int distanceToScrollToRight; |
| int panelWidth = getResources().getDimensionPixelSize( |
| R.dimen.tp_settings_preference_pane_width); |
| View scrollToPanel = getView().findViewById(frameResIds[index]); |
| View previewPanel = getView().findViewById(frameResIds[index + 1]); |
| View scrollToPanelHead = scrollToPanel.findViewById(R.id.decor_title_container); |
| View previewPanelHead = previewPanel.findViewById(R.id.decor_title_container); |
| boolean scrollsToPreview = |
| isRTL() ? mScrollView.getScrollX() >= mMaxScrollX - panelWidth * index |
| : mScrollView.getScrollX() <= panelWidth * index; |
| |
| boolean setAlphaForPreview = fragmentToBecomePreviewPanel != null |
| && !(fragmentToBecomePreviewPanel instanceof DummyFragment) |
| && !(fragmentToBecomePreviewPanel instanceof InfoFragment); |
| int previewPanelColor = getResources().getColor( |
| R.color.tp_preview_panel_background_color); |
| int mainPanelColor = getResources().getColor( |
| R.color.tp_preference_panel_background_color); |
| if (smoothScroll) { |
| int animationEnd = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index; |
| distanceToScrollToRight = animationEnd - mScrollView.getScrollX(); |
| // Slide animation |
| ObjectAnimator slideAnim = ObjectAnimator.ofInt(mScrollView, "scrollX", |
| mScrollView.getScrollX(), animationEnd); |
| slideAnim.setAutoCancel(true); |
| slideAnim.setDuration(PANEL_ANIMATION_MS); |
| slideAnim.start(); |
| // Color animation |
| if (scrollsToPreview) { |
| previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f); |
| previewPanel.setBackgroundColor(previewPanelColor); |
| if (previewPanelHead != null) { |
| previewPanelHead.setBackgroundColor(previewPanelColor); |
| } |
| ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(scrollToPanel, "alpha", |
| scrollToPanel.getAlpha(), 1f); |
| ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(scrollToPanel, |
| "backgroundColor", |
| new ArgbEvaluator(), previewPanelColor, mainPanelColor); |
| alphaAnim.setAutoCancel(true); |
| backgroundColorAnim.setAutoCancel(true); |
| AnimatorSet animatorSet = new AnimatorSet(); |
| if (scrollToPanelHead != null) { |
| ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject( |
| scrollToPanelHead, |
| "backgroundColor", |
| new ArgbEvaluator(), previewPanelColor, mainPanelColor); |
| backgroundColorAnimForHead.setAutoCancel(true); |
| animatorSet.playTogether(alphaAnim, backgroundColorAnim, |
| backgroundColorAnimForHead); |
| } else { |
| animatorSet.playTogether(alphaAnim, backgroundColorAnim); |
| } |
| animatorSet.setDuration(PANEL_ANIMATION_MS); |
| animatorSet.start(); |
| } else { |
| scrollToPanel.setAlpha(1f); |
| scrollToPanel.setBackgroundColor(mainPanelColor); |
| if (scrollToPanelHead != null) { |
| scrollToPanelHead.setBackgroundColor(mainPanelColor); |
| } |
| ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(previewPanel, "alpha", |
| previewPanel.getAlpha(), setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f); |
| ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(previewPanel, |
| "backgroundColor", |
| new ArgbEvaluator(), mainPanelColor, previewPanelColor); |
| alphaAnim.setAutoCancel(true); |
| backgroundColorAnim.setAutoCancel(true); |
| AnimatorSet animatorSet = new AnimatorSet(); |
| if (previewPanelHead != null) { |
| ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject( |
| previewPanelHead, |
| "backgroundColor", |
| new ArgbEvaluator(), mainPanelColor, previewPanelColor); |
| backgroundColorAnimForHead.setAutoCancel(true); |
| animatorSet.playTogether(alphaAnim, backgroundColorAnim, |
| backgroundColorAnimForHead); |
| } else { |
| animatorSet.playTogether(alphaAnim, backgroundColorAnim); |
| } |
| animatorSet.setDuration(PANEL_ANIMATION_MS); |
| animatorSet.start(); |
| } |
| } else { |
| int scrollToX = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index; |
| distanceToScrollToRight = scrollToX - mScrollView.getScrollX(); |
| mScrollView.scrollTo(scrollToX, 0); |
| previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f); |
| previewPanel.setBackgroundColor(previewPanelColor); |
| if (previewPanelHead != null) { |
| previewPanelHead.setBackgroundColor(previewPanelColor); |
| } |
| scrollToPanel.setAlpha(1f); |
| scrollToPanel.setBackgroundColor(mainPanelColor); |
| if (scrollToPanelHead != null) { |
| scrollToPanelHead.setBackgroundColor(mainPanelColor); |
| } |
| } |
| if (fragmentToBecomeMainPanel != null && fragmentToBecomeMainPanel.getView() != null) { |
| fragmentToBecomeMainPanel.getView().requestFocus(); |
| for (int resId : frameResIds) { |
| Fragment f = getChildFragmentManager().findFragmentById(resId); |
| if (f != null) { |
| View view = f.getView(); |
| if (view != null) { |
| view.setImportantForAccessibility( |
| f == fragmentToBecomeMainPanel |
| ? View.IMPORTANT_FOR_ACCESSIBILITY_YES |
| : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); |
| } |
| } |
| } |
| if (fragmentToBecomeMainPanel instanceof PreviewableComponentCallback) { |
| if (distanceToScrollToRight > 0) { |
| ((PreviewableComponentCallback) fragmentToBecomeMainPanel) |
| .onArriveAtMainPanel(!isRTL()); |
| } else if (distanceToScrollToRight < 0) { |
| ((PreviewableComponentCallback) fragmentToBecomeMainPanel) |
| .onArriveAtMainPanel(isRTL()); |
| } // distanceToScrollToRight being 0 means no actual panel sliding; thus noop. |
| } |
| } |
| }); |
| } |
| |
| private Fragment getInitialPreviewFragment(Fragment fragment) { |
| if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) { |
| return null; |
| } |
| |
| LeanbackPreferenceFragmentCompat leanbackPreferenceFragment = |
| (LeanbackPreferenceFragmentCompat) fragment; |
| if (leanbackPreferenceFragment.getListView() == null) { |
| return null; |
| } |
| |
| VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView(); |
| int position = listView.getSelectedPosition(); |
| PreferenceGroupAdapter adapter = |
| (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter()); |
| Preference chosenPreference = adapter.getItem(position); |
| // Find the first focusable preference if cannot find the selected preference |
| if (chosenPreference == null || (listView.findViewHolderForPosition(position) != null |
| && !listView.findViewHolderForPosition(position).itemView.hasFocusable())) { |
| chosenPreference = null; |
| for (int i = 0; i < listView.getChildCount(); i++) { |
| View view = listView.getChildAt(i); |
| if (view.hasFocusable()) { |
| PreferenceViewHolder viewHolder = |
| (PreferenceViewHolder) listView.getChildViewHolder(view); |
| chosenPreference = adapter.getItem(viewHolder.getAdapterPosition()); |
| break; |
| } |
| } |
| } |
| |
| if (chosenPreference == null) { |
| return null; |
| } |
| return onCreatePreviewFragment(fragment, chosenPreference); |
| } |
| |
| /** |
| * Refocus the current selected preference. When a preference is selected and its InfoFragment |
| * slice data changes. We need to call this method to make sure InfoFragment updates in time. |
| * This is also helpful in refreshing preview of ListPreference. |
| */ |
| public void refocusPreference(Fragment fragment) { |
| if (!isFragmentInTheMainPanel(fragment)) { |
| return; |
| } |
| Preference chosenPreference = getChosenPreference(fragment); |
| try { |
| if (chosenPreference != null) { |
| if (chosenPreference.getFragment() != null |
| && InfoFragment.class.isAssignableFrom( |
| Class.forName(chosenPreference.getFragment()))) { |
| updateInfoFragmentStatus(fragment); |
| } |
| if (chosenPreference instanceof ListPreference) { |
| refocusPreferenceForceRefresh(chosenPreference); |
| } |
| } |
| } catch (ClassNotFoundException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /** Force refresh preview panel. */ |
| public void refocusPreferenceForceRefresh(Preference chosenPreference) { |
| onPreferenceFocusedImpl(chosenPreference, true); |
| } |
| |
| /** Show error message in preview panel **/ |
| public void showErrorMessage(String errorMessage) { |
| Fragment prefFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| Preference preference = getChosenPreference(prefFragment); |
| preference.setFragment(InfoFragment.class.getCanonicalName()); |
| Bundle b = preference.getExtras(); |
| b.putParcelable(EXTRA_PREFERENCE_INFO_TITLE_ICON, |
| Icon.createWithResource(getContext(), R.drawable.slice_error_icon)); |
| b.putCharSequence(EXTRA_PREFERENCE_INFO_TEXT, getString(R.string.status_unavailable)); |
| b.putCharSequence(EXTRA_PREFERENCE_INFO_SUMMARY, errorMessage); |
| onPreferenceFocused(preference); |
| } |
| |
| private void updateInfoFragmentStatus(Fragment fragment) { |
| if (!isFragmentInTheMainPanel(fragment)) { |
| return; |
| } |
| final Fragment existingPreviewFragment = |
| getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]); |
| if (existingPreviewFragment instanceof InfoFragment) { |
| ((InfoFragment) existingPreviewFragment).updateInfoFragment(); |
| } |
| } |
| |
| /** Get the current chosen preference. */ |
| public static Preference getChosenPreference(Fragment fragment) { |
| if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) { |
| return null; |
| } |
| |
| LeanbackPreferenceFragmentCompat leanbackPreferenceFragment = |
| (LeanbackPreferenceFragmentCompat) fragment; |
| if (leanbackPreferenceFragment.getListView() == null) { |
| return null; |
| } |
| |
| VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView(); |
| int position = listView.getSelectedPosition(); |
| PreferenceGroupAdapter adapter = |
| (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter()); |
| Preference chosenPreference = adapter.getItem(position); |
| return chosenPreference; |
| } |
| |
| /** Creates preview preference fragment. */ |
| public Fragment onCreatePreviewFragment(Fragment caller, Preference preference) { |
| if (preference.getFragment() != null) { |
| if (!isInfoFragment(preference.getFragment()) |
| && !isPreferenceFragment(preference.getFragment())) { |
| return null; |
| } |
| if (isPreferenceFragment(preference.getFragment()) |
| && preference instanceof HasSliceUri) { |
| HasSliceUri slicePref = (HasSliceUri) preference; |
| if (slicePref.getUri() == null || !isUriValid(slicePref.getUri())) { |
| return null; |
| } |
| Bundle b = preference.getExtras(); |
| b.putString(SlicesConstants.TAG_TARGET_URI, slicePref.getUri()); |
| b.putCharSequence(SlicesConstants.TAG_SCREEN_TITLE, preference.getTitle()); |
| } |
| return Fragment.instantiate(getActivity(), preference.getFragment(), |
| preference.getExtras()); |
| } else { |
| Fragment f = null; |
| if (preference instanceof ListPreference) { |
| f = TwoPanelListPreferenceDialogFragment.newInstanceSingle(preference.getKey()); |
| } else if (preference instanceof MultiSelectListPreference) { |
| f = LeanbackListPreferenceDialogFragmentCompat.newInstanceMulti( |
| preference.getKey()); |
| } |
| if (f != null && caller != null) { |
| f.setTargetFragment(caller, 0); |
| } |
| return f; |
| } |
| } |
| |
| private boolean isUriValid(String uri) { |
| if (uri == null) { |
| return false; |
| } |
| ContentProviderClient client = |
| getContext().getContentResolver().acquireContentProviderClient(Uri.parse(uri)); |
| if (client != null) { |
| client.close(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** Add focus listener to the child fragment **/ |
| public void addListenerForFragment(Fragment fragment) { |
| if (isFragmentInTheMainPanel(fragment)) { |
| addOrRemovePreferenceFocusedListener(fragment, true); |
| } |
| } |
| |
| /** Remove focus listener from the child fragment **/ |
| public void removeListenerForFragment(Fragment fragment) { |
| addOrRemovePreferenceFocusedListener(fragment, false); |
| } |
| |
| /** Check if fragment is in the main panel **/ |
| public boolean isFragmentInTheMainPanel(Fragment fragment) { |
| return fragment == getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); |
| } |
| } |