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