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