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