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