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) instanceof Icon && b.get(key) instanceof Icon) { 493 if (!((Icon) a.get(key)).sameAs((Icon) b.get(key))) { 494 return false; 495 } 496 } else if (!a.get(key).equals(b.get(key))) { 497 return false; 498 } 499 } 500 return true; 501 } 502 503 /** Callback from SliceFragment **/ 504 public interface SliceFragmentCallback { 505 /** Triggered when preference is focused **/ onPreferenceFocused(Preference preference)506 void onPreferenceFocused(Preference preference); 507 508 /** Triggered when Seekbar preference is changed **/ onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue)509 void onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue); 510 } 511 onPreferenceFocused(Preference pref, int panelIndex)512 protected void onPreferenceFocused(Preference pref, int panelIndex) { 513 onPreferenceFocusedImpl(pref, false, panelIndex); 514 } 515 onPreferenceFocusedImpl(Preference pref, boolean forceRefresh, int panelIndex)516 private void onPreferenceFocusedImpl(Preference pref, boolean forceRefresh, int panelIndex) { 517 if (pref == null) { 518 return; 519 } 520 if (DEBUG) { 521 Log.d(TAG, "onPreferenceFocused " + pref.getTitle()); 522 } 523 final Fragment prefFragment = 524 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 525 if (prefFragment instanceof SliceFragmentCallback) { 526 ((SliceFragmentCallback) prefFragment).onPreferenceFocused(pref); 527 } 528 mFocusedPreference = pref; 529 if (mCheckVerticalGridViewScrollState || mPreviewPanelCreationDelay > 0) { 530 mIsWaitingForUpdatingPreview = true; 531 VerticalGridView listView = (VerticalGridView) 532 ((LeanbackPreferenceFragmentCompat) prefFragment).getListView(); 533 mHandler.postDelayed(new PostShowPreviewRunnable( 534 listView, pref, forceRefresh, panelIndex), mPreviewPanelCreationDelay); 535 } else { 536 handleFragmentTransactionWhenFocused(pref, forceRefresh, panelIndex); 537 } 538 } 539 540 private final class PostShowPreviewRunnable implements Runnable { 541 private final VerticalGridView mListView; 542 private final Preference mPref; 543 private final boolean mForceFresh; 544 private final int mPanelIndex; 545 PostShowPreviewRunnable(VerticalGridView listView, Preference pref, boolean forceFresh, int panelIndex)546 PostShowPreviewRunnable(VerticalGridView listView, Preference pref, boolean forceFresh, 547 int panelIndex) { 548 this.mListView = listView; 549 this.mPref = pref; 550 this.mForceFresh = forceFresh; 551 mPanelIndex = panelIndex; 552 } 553 554 @Override run()555 public void run() { 556 if (mPref == mFocusedPreference) { 557 if (mListView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 558 mHandler.postDelayed(this, CHECK_IDLE_STATE_MS); 559 } else { 560 handleFragmentTransactionWhenFocused(mPref, mForceFresh, mPanelIndex); 561 mIsWaitingForUpdatingPreview = false; 562 } 563 } 564 } 565 } 566 handleFragmentTransactionWhenFocused(Preference pref, boolean forceRefresh, int panelIndex)567 private void handleFragmentTransactionWhenFocused(Preference pref, boolean forceRefresh, 568 int panelIndex) { 569 if (!isAdded() || panelIndex != mPrefPanelIdx) { 570 return; 571 } 572 Fragment previewFragment = null; 573 final Fragment prefFragment = 574 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 575 try { 576 previewFragment = onCreatePreviewFragment(prefFragment, pref); 577 } catch (Exception e) { 578 Log.w(TAG, "Cannot instantiate the fragment from preference: " + pref, e); 579 } 580 if (previewFragment == null) { 581 previewFragment = new DummyFragment(); 582 } 583 final Fragment existingPreviewFragment = 584 getChildFragmentManager().findFragmentById( 585 frameResIds[mPrefPanelIdx + 1]); 586 if (existingPreviewFragment != null 587 && existingPreviewFragment.getClass().equals(previewFragment.getClass()) 588 && equalArguments(existingPreviewFragment.getArguments(), 589 previewFragment.getArguments())) { 590 if (isRTL() && mScrollView.getScrollX() == 0 && mPrefPanelIdx == 0 591 && getView() != null && getView().getViewTreeObserver() != null) { 592 // For RTL we need to reclaim focus to the correct scroll position if a pref 593 // launches a new activity because the horizontal scroll goes back to 0. 594 getView().getViewTreeObserver().addOnGlobalLayoutListener( 595 mOnGlobalLayoutListener); 596 } 597 if (!forceRefresh) { 598 return; 599 } 600 } 601 602 // If the existing preview fragment is recreated when the activity is recreated, the 603 // animation would fall back to "slide left", in this case, we need to set the exit 604 // transition. 605 if (existingPreviewFragment != null) { 606 existingPreviewFragment.setExitTransition(null); 607 } 608 previewFragment.setEnterTransition(new Fade()); 609 previewFragment.setExitTransition(null); 610 final FragmentTransaction transaction = 611 getChildFragmentManager().beginTransaction(); 612 transaction.setCustomAnimations(R.animator.fade_in_preview_panel, 613 R.animator.fade_out_preview_panel); 614 transaction.replace(frameResIds[mPrefPanelIdx + 1], previewFragment); 615 transaction.commitNow(); 616 617 // Some fragments may steal focus on creation. Reclaim focus on main fragment. 618 if (getView() != null && getView().getViewTreeObserver() != null) { 619 getView().getViewTreeObserver().addOnGlobalLayoutListener( 620 mOnGlobalLayoutListener); 621 } 622 } 623 onSeekbarPreferenceChanged(SliceSeekbarPreference pref, int addValue)624 private boolean onSeekbarPreferenceChanged(SliceSeekbarPreference pref, int addValue) { 625 final Fragment prefFragment = 626 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 627 if (prefFragment instanceof SliceFragmentCallback) { 628 ((SliceFragmentCallback) prefFragment).onSeekbarPreferenceChanged(pref, addValue); 629 } 630 return true; 631 } 632 isRTL()633 private boolean isRTL() { 634 return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 635 } 636 637 @Override onResume()638 public void onResume() { 639 if (DEBUG) { 640 Log.d(TAG, "onResume"); 641 } 642 super.onResume(); 643 IntentFilter intentFilter = new IntentFilter(); 644 intentFilter.addAction("com.android.tv.settings.PREVIEW_DELAY"); 645 getContext().registerReceiver(mPreviewPanelDelayReceiver, intentFilter, 646 Context.RECEIVER_EXPORTED_UNAUDITED); 647 // Trap back button presses 648 final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView(); 649 if (rootView != null) { 650 rootView.setOnBackKeyListener(mRootViewOnKeyListener); 651 } 652 } 653 654 @Override onPause()655 public void onPause() { 656 if (DEBUG) { 657 Log.d(TAG, "onPause"); 658 } 659 super.onPause(); 660 getContext().unregisterReceiver(mPreviewPanelDelayReceiver); 661 final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView(); 662 if (rootView != null) { 663 rootView.setOnBackKeyListener(null); 664 } 665 } 666 667 /** 668 * Displays a fragment to the user, temporarily replacing the contents of this fragment. 669 * 670 * @param fragment Fragment instance to be added. 671 */ startImmersiveFragment(@onNull Fragment fragment)672 public void startImmersiveFragment(@NonNull Fragment fragment) { 673 if (DEBUG) { 674 Log.d(TAG, "Starting immersive fragment."); 675 } 676 addOrRemovePreferenceFocusedListener(fragment, true); 677 final FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); 678 Fragment target = getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 679 fragment.setTargetFragment(target, 0); 680 transaction 681 .add(R.id.two_panel_fragment_container, fragment) 682 .remove(target) 683 .addToBackStack(null) 684 .commit(); 685 mHandler.post(() -> { 686 updateAccessibilityTitle(fragment); 687 }); 688 689 } 690 691 public static class DummyFragment extends Fragment { 692 @Override 693 public @Nullable onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)694 View onCreateView(LayoutInflater inflater, ViewGroup container, 695 Bundle savedInstanceState) { 696 return inflater.inflate(R.layout.dummy_fragment, container, false); 697 } 698 } 699 700 /** 701 * Implement this if fragment needs to handle DPAD_LEFT & DPAD_RIGHT itself in some cases 702 **/ 703 public interface NavigationCallback { 704 705 /** 706 * Returns true if the fragment is in the state that can navigate back on receiving a 707 * navigation DPAD key. When true, TwoPanelSettings will initiate a back operation on 708 * receiving a left key. This method doesn't apply to back key: back key always initiates a 709 * back operation. 710 */ canNavigateBackOnDPAD()711 boolean canNavigateBackOnDPAD(); 712 713 /** 714 * Callback when navigating to preview screen 715 */ onNavigateToPreview()716 void onNavigateToPreview(); 717 718 /** 719 * Callback when returning to previous screen 720 */ onNavigateBack()721 void onNavigateBack(); 722 } 723 724 /** 725 * Implement this if the component (typically a Fragment) is preview-able and would like to get 726 * some lifecycle-like callback(s) when the component becomes the main panel. 727 */ 728 public interface PreviewableComponentCallback { 729 730 /** 731 * Lifecycle-like callback when the component becomes main panel from the preview panel. For 732 * Fragment, this will be invoked right after the preview fragment sliding into the main 733 * panel. 734 * 735 * @param forward means whether the component arrives at main panel when users are 736 * navigating forwards (deeper into the TvSettings tree). 737 */ onArriveAtMainPanel(boolean forward)738 void onArriveAtMainPanel(boolean forward); 739 } 740 741 private class RootViewOnKeyListener implements View.OnKeyListener { 742 743 @Override onKey(View v, int keyCode, KeyEvent event)744 public boolean onKey(View v, int keyCode, KeyEvent event) { 745 if (!isAdded()) { 746 Log.d(TAG, "Fragment not attached yet."); 747 return true; 748 } 749 Fragment prefFragment = 750 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 751 752 if (event.getAction() == KeyEvent.ACTION_DOWN 753 && (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 754 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) { 755 Preference preference = getChosenPreference(prefFragment); 756 if ((preference instanceof SliceSeekbarPreference)) { 757 SliceSeekbarPreference sbPref = (SliceSeekbarPreference) preference; 758 if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 759 onSeekbarPreferenceChanged(sbPref, 1); 760 } else { 761 onSeekbarPreferenceChanged(sbPref, -1); 762 } 763 return true; 764 } 765 } 766 767 if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) { 768 if (event.getRepeatCount() > 0) { 769 // Ignore long press on back button. 770 return false; 771 } 772 return back(true); 773 } 774 775 if (event.getAction() == KeyEvent.ACTION_DOWN 776 && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT) 777 || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT))) { 778 if (prefFragment instanceof NavigationCallback 779 && !((NavigationCallback) prefFragment).canNavigateBackOnDPAD()) { 780 return false; 781 } 782 return back(false); 783 } 784 785 if (event.getAction() == KeyEvent.ACTION_DOWN 786 && ((!isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) 787 || (isRTL() && keyCode == KeyEvent.KEYCODE_DPAD_LEFT))) { 788 forward(); 789 // TODO(b/163432209): improve NavigationCallback and be more specific here. 790 // Do not consume the KeyEvent for NavigationCallback classes such as date & time 791 // picker. 792 return !(prefFragment instanceof NavigationCallback); 793 } 794 return false; 795 } 796 } 797 forward()798 private void forward() { 799 if (!isAdded()) { 800 Log.d(TAG, "Fragment not attached yet."); 801 return; 802 } 803 final TwoPanelSettingsRootView rootView = (TwoPanelSettingsRootView) getView(); 804 if (shouldPerformClick()) { 805 rootView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, 806 KeyEvent.KEYCODE_DPAD_CENTER)); 807 rootView.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, 808 KeyEvent.KEYCODE_DPAD_CENTER)); 809 } else { 810 Fragment previewFragment = getChildFragmentManager() 811 .findFragmentById(frameResIds[mPrefPanelIdx + 1]); 812 if (!(previewFragment instanceof InfoFragment) 813 && !mIsWaitingForUpdatingPreview) { 814 mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT); 815 navigateToPreviewFragment(); 816 } 817 } 818 } 819 shouldPerformClick()820 private boolean shouldPerformClick() { 821 Fragment prefFragment = 822 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 823 Preference preference = getChosenPreference(prefFragment); 824 if (preference == null) { 825 return false; 826 } 827 // This is for the case when a preference has preview but once user navigate to 828 // see the preview, settings actually launch an intent to start external activity. 829 if (preference.getIntent() != null && !TextUtils.isEmpty(preference.getFragment())) { 830 return true; 831 } 832 return preference instanceof SlicePreference 833 && ((SlicePreference) preference).getSliceAction() != null 834 && ((SlicePreference) preference).getUri() != null; 835 } 836 back(boolean isKeyBackPressed)837 private boolean back(boolean isKeyBackPressed) { 838 if (!isAdded()) { 839 Log.d(TAG, "Fragment not attached yet."); 840 return true; 841 } 842 if (mIsNavigatingBack) { 843 mHandler.postDelayed(new Runnable() { 844 @Override 845 public void run() { 846 if (DEBUG) { 847 Log.d(TAG, "Navigating back is deferred."); 848 } 849 back(isKeyBackPressed); 850 } 851 }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS); 852 return true; 853 } 854 if (DEBUG) { 855 Log.d(TAG, "Going back one level."); 856 } 857 858 final Fragment immersiveFragment = 859 getChildFragmentManager().findFragmentById(R.id.two_panel_fragment_container); 860 if (immersiveFragment != null) { 861 getChildFragmentManager().popBackStack(); 862 moveToPanel(mPrefPanelIdx, false); 863 return true; 864 } 865 866 // When a11y is on, we allow InfoFragments to take focus without scrolling panels. So if 867 // the user presses back button in this state, we should not scroll our panels back, or exit 868 // Settings activity, but rather reinstate the focus to be on the main panel. 869 Fragment preview = 870 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]); 871 if (isA11yOn() && preview instanceof InfoFragment && preview.getView() != null 872 && preview.getView().hasFocus()) { 873 View mainPanelView = getChildFragmentManager() 874 .findFragmentById(frameResIds[mPrefPanelIdx]).getView(); 875 if (mainPanelView != null) { 876 mainPanelView.requestFocus(); 877 return true; 878 } 879 } 880 881 if (mPrefPanelIdx < 1) { 882 // Disallow the user to use "dpad left" to finish activity in the first screen 883 if (isKeyBackPressed) { 884 getActivity().finish(); 885 } 886 return true; 887 } 888 889 mIsNavigatingBack = true; 890 getChildFragmentManager().popBackStack(); 891 892 mPrefPanelIdx--; 893 894 mHandler.postDelayed(() -> { 895 if (isKeyBackPressed) { 896 mAudioManager.playSoundEffect(AudioManager.FX_BACK); 897 } else { 898 mAudioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT); 899 } 900 moveToPanel(mPrefPanelIdx, true); 901 }, PANEL_ANIMATION_DELAY_MS); 902 903 mHandler.postDelayed(() -> { 904 removeFragment(mPrefPanelIdx + 2); 905 mIsNavigatingBack = false; 906 Fragment previewFragment = 907 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]); 908 if (previewFragment instanceof NavigationCallback) { 909 ((NavigationCallback) previewFragment).onNavigateBack(); 910 } 911 }, PANEL_ANIMATION_DELAY_MS + PANEL_ANIMATION_DELAY_MS); 912 return true; 913 } 914 removeFragment(int index)915 private void removeFragment(int index) { 916 Fragment fragment = getChildFragmentManager().findFragmentById(frameResIds[index]); 917 if (fragment != null) { 918 getChildFragmentManager().beginTransaction().remove(fragment).commit(); 919 } 920 } 921 removeFragmentAndAddToBackStack(int index)922 private void removeFragmentAndAddToBackStack(int index) { 923 if (index < 0) { 924 return; 925 } 926 Fragment removePanel = getChildFragmentManager().findFragmentById(frameResIds[index]); 927 if (removePanel != null) { 928 removePanel.setExitTransition(new Fade()); 929 getChildFragmentManager().beginTransaction().remove(removePanel) 930 .addToBackStack("remove " + removePanel.getClass().getName()).commit(); 931 } 932 } 933 934 /** For RTL layout, we need to know the right edge from where the panels start scrolling. */ computeMaxRightScroll()935 private int computeMaxRightScroll() { 936 int scrollViewWidth = getResources().getDimensionPixelSize(R.dimen.tp_settings_panes_width); 937 int panelWidth = getResources().getDimensionPixelSize( 938 R.dimen.tp_settings_preference_pane_width); 939 int panelPadding = getResources().getDimensionPixelSize( 940 R.dimen.preference_pane_extra_padding_start) * 2; 941 int result = frameResIds.length * panelWidth - scrollViewWidth + panelPadding; 942 return result < 0 ? 0 : result; 943 } 944 945 /** Scrolls such that the panel with given index is the main panel shown on the left. */ moveToPanel(final int index, boolean smoothScroll)946 private void moveToPanel(final int index, boolean smoothScroll) { 947 mHandler.post(() -> { 948 if (DEBUG) { 949 Log.d(TAG, "Moving to panel " + index); 950 } 951 if (!isAdded()) { 952 return; 953 } 954 Fragment fragmentToBecomeMainPanel = 955 getChildFragmentManager().findFragmentById(frameResIds[index]); 956 Fragment fragmentToBecomePreviewPanel = 957 getChildFragmentManager().findFragmentById(frameResIds[index + 1]); 958 // Positive value means that the panel is scrolling to right (navigate forward for LTR 959 // or navigate backwards for RTL) and vice versa; 0 means that this is likely invoked 960 // by GlobalLayoutListener and there's no actual sliding. 961 int distanceToScrollToRight; 962 int panelWidth = getResources().getDimensionPixelSize( 963 R.dimen.tp_settings_preference_pane_width); 964 TwoPanelSettingsFrameLayout scrollToPanel = getView().findViewById(frameResIds[index]); 965 TwoPanelSettingsFrameLayout previewPanel = getView().findViewById( 966 frameResIds[index + 1]); 967 if (scrollToPanel == null || previewPanel == null) { 968 return; 969 } 970 scrollToPanel.setOnDispatchTouchListener(null); 971 previewPanel.setOnDispatchTouchListener((view, env) -> { 972 if (env.getActionMasked() == MotionEvent.ACTION_UP) { 973 forward(); 974 } 975 return true; 976 }); 977 View scrollToPanelHead = scrollToPanel.findViewById(R.id.decor_title_container); 978 View previewPanelHead = previewPanel.findViewById(R.id.decor_title_container); 979 boolean scrollsToPreview = 980 isRTL() ? mScrollView.getScrollX() >= mMaxScrollX - panelWidth * index 981 : mScrollView.getScrollX() <= panelWidth * index; 982 983 boolean setAlphaForPreview = fragmentToBecomePreviewPanel != null 984 && !(fragmentToBecomePreviewPanel instanceof DummyFragment) 985 && !(fragmentToBecomePreviewPanel instanceof InfoFragment); 986 int previewPanelColor = getResources().getColor( 987 R.color.tp_preview_panel_background_color); 988 int mainPanelColor = getResources().getColor( 989 R.color.tp_preference_panel_background_color); 990 if (smoothScroll) { 991 int animationEnd = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index; 992 distanceToScrollToRight = animationEnd - mScrollView.getScrollX(); 993 // Slide animation 994 ObjectAnimator slideAnim = ObjectAnimator.ofInt(mScrollView, "scrollX", 995 mScrollView.getScrollX(), animationEnd); 996 slideAnim.setAutoCancel(true); 997 slideAnim.setDuration(PANEL_ANIMATION_SLIDE_MS); 998 slideAnim.addListener(new AnimatorListenerAdapter() { 999 @Override 1000 public void onAnimationEnd(Animator animation) { 1001 super.onAnimationEnd(animation); 1002 if (isA11yOn() && fragmentToBecomeMainPanel != null 1003 && fragmentToBecomeMainPanel.getView() != null) { 1004 fragmentToBecomeMainPanel.getView().requestFocus(); 1005 } 1006 } 1007 }); 1008 slideAnim.setInterpolator(AnimationUtils.loadInterpolator( 1009 getContext(), R.anim.easing_browse)); 1010 slideAnim.start(); 1011 // Color animation 1012 if (scrollsToPreview) { 1013 previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f); 1014 previewPanel.setBackgroundColor(previewPanelColor); 1015 if (previewPanelHead != null) { 1016 previewPanelHead.setBackgroundColor(previewPanelColor); 1017 } 1018 ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(scrollToPanel, "alpha", 1019 scrollToPanel.getAlpha(), 1f); 1020 ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(scrollToPanel, 1021 "backgroundColor", 1022 new ArgbEvaluator(), previewPanelColor, mainPanelColor); 1023 alphaAnim.setAutoCancel(true); 1024 alphaAnim.setDuration(PANEL_ANIMATION_ALPHA_MS); 1025 backgroundColorAnim.setAutoCancel(true); 1026 backgroundColorAnim.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS); 1027 AnimatorSet animatorSet = new AnimatorSet(); 1028 if (scrollToPanelHead != null) { 1029 ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject( 1030 scrollToPanelHead, 1031 "backgroundColor", 1032 new ArgbEvaluator(), previewPanelColor, mainPanelColor); 1033 backgroundColorAnimForHead.setAutoCancel(true); 1034 backgroundColorAnimForHead.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS); 1035 animatorSet.playTogether(alphaAnim, backgroundColorAnim, 1036 backgroundColorAnimForHead); 1037 } else { 1038 animatorSet.playTogether(alphaAnim, backgroundColorAnim); 1039 } 1040 animatorSet.setInterpolator(AnimationUtils.loadInterpolator( 1041 getContext(), R.anim.easing_browse)); 1042 animatorSet.start(); 1043 } else { 1044 scrollToPanel.setAlpha(1f); 1045 scrollToPanel.setBackgroundColor(mainPanelColor); 1046 if (scrollToPanelHead != null) { 1047 scrollToPanelHead.setBackgroundColor(mainPanelColor); 1048 } 1049 ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(previewPanel, "alpha", 1050 previewPanel.getAlpha(), setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f); 1051 ObjectAnimator backgroundColorAnim = ObjectAnimator.ofObject(previewPanel, 1052 "backgroundColor", 1053 new ArgbEvaluator(), mainPanelColor, previewPanelColor); 1054 alphaAnim.setAutoCancel(true); 1055 alphaAnim.setDuration(PANEL_ANIMATION_ALPHA_MS); 1056 backgroundColorAnim.setAutoCancel(true); 1057 backgroundColorAnim.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS); 1058 AnimatorSet animatorSet = new AnimatorSet(); 1059 if (previewPanelHead != null) { 1060 ObjectAnimator backgroundColorAnimForHead = ObjectAnimator.ofObject( 1061 previewPanelHead, 1062 "backgroundColor", 1063 new ArgbEvaluator(), mainPanelColor, previewPanelColor); 1064 backgroundColorAnimForHead.setAutoCancel(true); 1065 backgroundColorAnimForHead.setDuration(PANEL_BACKGROUND_ANIMATION_ALPHA_MS); 1066 animatorSet.playTogether(alphaAnim, backgroundColorAnim, 1067 backgroundColorAnimForHead); 1068 } else { 1069 animatorSet.playTogether(alphaAnim, backgroundColorAnim); 1070 } 1071 animatorSet.setInterpolator(AnimationUtils.loadInterpolator( 1072 getContext(), R.anim.easing_browse)); 1073 animatorSet.start(); 1074 } 1075 } else { 1076 int scrollToX = isRTL() ? mMaxScrollX - panelWidth * index : panelWidth * index; 1077 distanceToScrollToRight = scrollToX - mScrollView.getScrollX(); 1078 mScrollView.scrollTo(scrollToX, 0); 1079 previewPanel.setAlpha(setAlphaForPreview ? PREVIEW_PANEL_ALPHA : 1f); 1080 previewPanel.setBackgroundColor(previewPanelColor); 1081 if (previewPanelHead != null) { 1082 previewPanelHead.setBackgroundColor(previewPanelColor); 1083 } 1084 scrollToPanel.setAlpha(1f); 1085 scrollToPanel.setBackgroundColor(mainPanelColor); 1086 if (scrollToPanelHead != null) { 1087 scrollToPanelHead.setBackgroundColor(mainPanelColor); 1088 } 1089 } 1090 if (fragmentToBecomeMainPanel != null && fragmentToBecomeMainPanel.getView() != null) { 1091 if (!isA11yOn()) { 1092 fragmentToBecomeMainPanel.getView().requestFocus(); 1093 } 1094 for (int resId : frameResIds) { 1095 Fragment f = getChildFragmentManager().findFragmentById(resId); 1096 if (f != null) { 1097 View view = f.getView(); 1098 if (view != null) { 1099 view.setImportantForAccessibility( 1100 f == fragmentToBecomeMainPanel || f instanceof InfoFragment 1101 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 1102 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 1103 } 1104 } 1105 } 1106 if (fragmentToBecomeMainPanel instanceof PreviewableComponentCallback) { 1107 if (distanceToScrollToRight > 0) { 1108 ((PreviewableComponentCallback) fragmentToBecomeMainPanel) 1109 .onArriveAtMainPanel(!isRTL()); 1110 } else if (distanceToScrollToRight < 0) { 1111 ((PreviewableComponentCallback) fragmentToBecomeMainPanel) 1112 .onArriveAtMainPanel(isRTL()); 1113 } // distanceToScrollToRight being 0 means no actual panel sliding; thus noop. 1114 } 1115 updateAccessibilityTitle(fragmentToBecomeMainPanel); 1116 } 1117 }); 1118 } 1119 getInitialPreviewFragment(Fragment fragment)1120 private Fragment getInitialPreviewFragment(Fragment fragment) { 1121 if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) { 1122 return null; 1123 } 1124 1125 LeanbackPreferenceFragmentCompat leanbackPreferenceFragment = 1126 (LeanbackPreferenceFragmentCompat) fragment; 1127 if (leanbackPreferenceFragment.getListView() == null) { 1128 return null; 1129 } 1130 1131 VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView(); 1132 int position = listView.getSelectedPosition(); 1133 PreferenceGroupAdapter adapter = 1134 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter()); 1135 if (adapter == null) { 1136 return null; 1137 } 1138 Preference chosenPreference = adapter.getItem(position); 1139 // Find the first focusable preference if cannot find the selected preference 1140 if (chosenPreference == null || (listView.findViewHolderForPosition(position) != null 1141 && !listView.findViewHolderForPosition(position).itemView.hasFocusable())) { 1142 chosenPreference = null; 1143 for (int i = 0; i < listView.getChildCount(); i++) { 1144 View view = listView.getChildAt(i); 1145 if (view.hasFocusable()) { 1146 PreferenceViewHolder viewHolder = 1147 (PreferenceViewHolder) listView.getChildViewHolder(view); 1148 chosenPreference = adapter.getItem(viewHolder.getAdapterPosition()); 1149 break; 1150 } 1151 } 1152 } 1153 1154 if (chosenPreference == null) { 1155 return null; 1156 } 1157 return onCreatePreviewFragment(fragment, chosenPreference); 1158 } 1159 1160 /** 1161 * Refocus the current selected preference. When a preference is selected and its InfoFragment 1162 * slice data changes. We need to call this method to make sure InfoFragment updates in time. 1163 * This is also helpful in refreshing preview of ListPreference. 1164 */ refocusPreference(Fragment fragment)1165 public void refocusPreference(Fragment fragment) { 1166 if (!isFragmentInTheMainPanel(fragment)) { 1167 return; 1168 } 1169 Preference chosenPreference = getChosenPreference(fragment); 1170 try { 1171 if (chosenPreference != null) { 1172 if (chosenPreference.getFragment() != null 1173 && InfoFragment.class.isAssignableFrom( 1174 Class.forName(chosenPreference.getFragment()))) { 1175 updateInfoFragmentStatus(fragment); 1176 } 1177 if (chosenPreference instanceof ListPreference) { 1178 refocusPreferenceForceRefresh(chosenPreference, fragment); 1179 } 1180 } 1181 } catch (ClassNotFoundException e) { 1182 e.printStackTrace(); 1183 } 1184 } 1185 1186 /** Force refresh preview panel. */ refocusPreferenceForceRefresh(Preference chosenPreference, Fragment fragment)1187 public void refocusPreferenceForceRefresh(Preference chosenPreference, Fragment fragment) { 1188 if (!isFragmentInTheMainPanel(fragment)) { 1189 return; 1190 } 1191 onPreferenceFocusedImpl(chosenPreference, true, mPrefPanelIdx); 1192 } 1193 1194 /** Show error message in preview panel **/ showErrorMessage(String errorMessage, Fragment fragment)1195 public void showErrorMessage(String errorMessage, Fragment fragment) { 1196 Fragment prefFragment = 1197 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 1198 if (fragment == prefFragment) { 1199 // If user has already navigated to the preview screen, main panel screen should be 1200 // updated to new InFoFragment. Create a fake preference to work around this case. 1201 Preference preference = new Preference(getContext()); 1202 updatePreferenceWithErrorMessage(preference, errorMessage, getContext()); 1203 Fragment newPrefFragment = onCreatePreviewFragment(null, preference); 1204 final FragmentTransaction transaction = 1205 getChildFragmentManager().beginTransaction(); 1206 transaction.setCustomAnimations(R.animator.fade_in_preview_panel, 1207 R.animator.fade_out_preview_panel); 1208 transaction.replace(frameResIds[mPrefPanelIdx], newPrefFragment); 1209 transaction.commit(); 1210 } else { 1211 Preference preference = getChosenPreference(prefFragment); 1212 if (preference != null) { 1213 if (isA11yOn()) { 1214 appendErrorToContentDescription(prefFragment, errorMessage); 1215 } 1216 updatePreferenceWithErrorMessage(preference, errorMessage, getContext()); 1217 onPreferenceFocused(preference, mPrefPanelIdx); 1218 } 1219 } 1220 } 1221 updatePreferenceWithErrorMessage( Preference preference, String errorMessage, Context context)1222 private static void updatePreferenceWithErrorMessage( 1223 Preference preference, String errorMessage, Context context) { 1224 preference.setFragment(InfoFragment.class.getCanonicalName()); 1225 Bundle b = preference.getExtras(); 1226 b.putParcelable(EXTRA_PREFERENCE_INFO_TITLE_ICON, 1227 Icon.createWithResource(context, R.drawable.slice_error_icon)); 1228 b.putCharSequence(EXTRA_PREFERENCE_INFO_TEXT, 1229 context.getString(R.string.status_unavailable)); 1230 b.putCharSequence(EXTRA_PREFERENCE_INFO_SUMMARY, errorMessage); 1231 } 1232 appendErrorToContentDescription(Fragment fragment, String errorMessage)1233 private void appendErrorToContentDescription(Fragment fragment, String errorMessage) { 1234 Preference preference = getChosenPreference(fragment); 1235 1236 String errorMessageContentDescription = ""; 1237 if (preference.getTitle() != null) { 1238 errorMessageContentDescription += preference.getTitle().toString(); 1239 } 1240 1241 errorMessageContentDescription += 1242 HasCustomContentDescription.CONTENT_DESCRIPTION_SEPARATOR 1243 + getString(R.string.status_unavailable) 1244 + HasCustomContentDescription.CONTENT_DESCRIPTION_SEPARATOR + errorMessage; 1245 1246 if (preference instanceof SlicePreference) { 1247 ((SlicePreference) preference).setContentDescription(errorMessageContentDescription); 1248 } else if (preference instanceof SliceSwitchPreference) { 1249 ((SliceSwitchPreference) preference) 1250 .setContentDescription(errorMessageContentDescription); 1251 } else if (preference instanceof CustomContentDescriptionPreference) { 1252 ((CustomContentDescriptionPreference) preference) 1253 .setContentDescription(errorMessageContentDescription); 1254 } 1255 1256 LeanbackPreferenceFragmentCompat leanbackPreferenceFragment = 1257 (LeanbackPreferenceFragmentCompat) fragment; 1258 if (leanbackPreferenceFragment.getListView() != null 1259 && leanbackPreferenceFragment.getListView().getAdapter() != null) { 1260 leanbackPreferenceFragment.getListView().getAdapter().notifyDataSetChanged(); 1261 } 1262 } 1263 updateInfoFragmentStatus(Fragment fragment)1264 private void updateInfoFragmentStatus(Fragment fragment) { 1265 if (!isFragmentInTheMainPanel(fragment)) { 1266 return; 1267 } 1268 final Fragment existingPreviewFragment = 1269 getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx + 1]); 1270 if (existingPreviewFragment instanceof InfoFragment) { 1271 ((InfoFragment) existingPreviewFragment).updateInfoFragment(); 1272 } 1273 } 1274 1275 /** Get the current chosen preference. */ getChosenPreference(Fragment fragment)1276 public static Preference getChosenPreference(Fragment fragment) { 1277 if (!(fragment instanceof LeanbackPreferenceFragmentCompat)) { 1278 return null; 1279 } 1280 1281 LeanbackPreferenceFragmentCompat leanbackPreferenceFragment = 1282 (LeanbackPreferenceFragmentCompat) fragment; 1283 if (leanbackPreferenceFragment.getListView() == null) { 1284 return null; 1285 } 1286 1287 VerticalGridView listView = (VerticalGridView) leanbackPreferenceFragment.getListView(); 1288 int position = listView.getSelectedPosition(); 1289 PreferenceGroupAdapter adapter = 1290 (PreferenceGroupAdapter) (leanbackPreferenceFragment.getListView().getAdapter()); 1291 return adapter != null ? adapter.getItem(position) : null; 1292 } 1293 1294 /** Creates preview preference fragment. */ onCreatePreviewFragment(Fragment caller, Preference preference)1295 public Fragment onCreatePreviewFragment(Fragment caller, Preference preference) { 1296 if (preference == null) { 1297 return null; 1298 } 1299 if (preference.getFragment() != null) { 1300 if (!isInfoFragment(preference.getFragment()) 1301 && !isPreferenceFragment(preference.getFragment())) { 1302 return null; 1303 } 1304 if (isPreferenceFragment(preference.getFragment()) 1305 && preference instanceof HasSliceUri) { 1306 HasSliceUri slicePref = (HasSliceUri) preference; 1307 if (slicePref.getUri() == null || !isUriValid(slicePref.getUri())) { 1308 return null; 1309 } 1310 Bundle b = preference.getExtras(); 1311 b.putString(SlicesConstants.TAG_TARGET_URI, slicePref.getUri()); 1312 b.putCharSequence(SlicesConstants.TAG_SCREEN_TITLE, preference.getTitle()); 1313 } 1314 return Fragment.instantiate(getActivity(), preference.getFragment(), 1315 preference.getExtras()); 1316 } else { 1317 Fragment f = null; 1318 if (preference instanceof ListPreference 1319 && ((ListPreference) preference).getEntries() != null) { 1320 f = TwoPanelListPreferenceDialogFragment.newInstanceSingle(preference.getKey()); 1321 } else if (preference instanceof MultiSelectListPreference 1322 && ((MultiSelectListPreference) preference).getEntries() != null) { 1323 f = LeanbackListPreferenceDialogFragmentCompat.newInstanceMulti( 1324 preference.getKey()); 1325 } 1326 if (f != null && caller != null) { 1327 f.setTargetFragment(caller, 0); 1328 } 1329 return f; 1330 } 1331 } 1332 isUriValid(String uri)1333 private boolean isUriValid(String uri) { 1334 if (uri == null) { 1335 return false; 1336 } 1337 ContentProviderClient client = 1338 getContext().getContentResolver().acquireContentProviderClient(Uri.parse(uri)); 1339 if (client != null) { 1340 client.close(); 1341 return true; 1342 } else { 1343 return false; 1344 } 1345 } 1346 1347 /** 1348 * Add focus listener to the child fragment. It must always be called after 1349 * the child fragment view is created since the listener is attached to the 1350 * {@link VerticalGridView} in the child fragment view. 1351 */ addListenerForFragment(Fragment fragment)1352 public void addListenerForFragment(Fragment fragment) { 1353 if (isFragmentInTheMainPanel(fragment)) { 1354 addOrRemovePreferenceFocusedListener(fragment, true); 1355 } 1356 } 1357 1358 /** Remove focus listener from the child fragment **/ removeListenerForFragment(Fragment fragment)1359 public void removeListenerForFragment(Fragment fragment) { 1360 addOrRemovePreferenceFocusedListener(fragment, false); 1361 } 1362 1363 /** Check if fragment is in the main panel **/ isFragmentInTheMainPanel(Fragment fragment)1364 public boolean isFragmentInTheMainPanel(Fragment fragment) { 1365 return fragment == getChildFragmentManager().findFragmentById(frameResIds[mPrefPanelIdx]); 1366 } 1367 } 1368