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