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.slices; 18 19 import static android.app.slice.Slice.EXTRA_SLIDER_VALUE; 20 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; 21 import static android.app.slice.Slice.HINT_PARTIAL; 22 23 import static com.android.tv.twopanelsettings.slices.InstrumentationUtils.logEntrySelected; 24 import static com.android.tv.twopanelsettings.slices.InstrumentationUtils.logToggleInteracted; 25 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_INFO_STATUS; 26 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_PREFERENCE_KEY; 27 import static com.android.tv.twopanelsettings.slices.SlicesConstants.EXTRA_SLICE_FOLLOWUP; 28 29 import android.app.Activity; 30 import android.app.PendingIntent; 31 import android.app.PendingIntent.CanceledException; 32 import android.app.tvsettings.TvSettingsEnums; 33 import android.content.ContentProviderClient; 34 import android.content.Intent; 35 import android.content.IntentSender; 36 import android.database.ContentObserver; 37 import android.graphics.drawable.Icon; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.Parcelable; 42 import android.provider.Settings; 43 import android.text.TextUtils; 44 import android.util.Log; 45 import android.util.TypedValue; 46 import android.view.ContextThemeWrapper; 47 import android.view.Gravity; 48 import android.view.LayoutInflater; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.widget.ImageView; 52 import android.widget.TextView; 53 import android.widget.Toast; 54 55 import androidx.activity.result.ActivityResult; 56 import androidx.activity.result.ActivityResultCallback; 57 import androidx.activity.result.ActivityResultLauncher; 58 import androidx.activity.result.IntentSenderRequest; 59 import androidx.activity.result.contract.ActivityResultContracts; 60 import androidx.annotation.Keep; 61 import androidx.annotation.NonNull; 62 import androidx.lifecycle.Observer; 63 import androidx.preference.Preference; 64 import androidx.preference.PreferenceScreen; 65 import androidx.preference.TwoStatePreference; 66 import androidx.slice.Slice; 67 import androidx.slice.SliceItem; 68 import androidx.slice.widget.ListContent; 69 import androidx.slice.widget.SliceContent; 70 71 import com.android.tv.twopanelsettings.R; 72 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment; 73 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment.SliceFragmentCallback; 74 import com.android.tv.twopanelsettings.slices.PreferenceSliceLiveData.SliceLiveDataImpl; 75 import com.android.tv.twopanelsettings.slices.SlicePreferencesUtil.Data; 76 77 import java.util.ArrayList; 78 import java.util.HashMap; 79 import java.util.List; 80 import java.util.Map; 81 82 /** 83 * A screen presenting a slice in TV settings. 84 */ 85 @Keep 86 public class SliceFragment extends SettingsPreferenceFragment implements Observer<Slice>, 87 SliceFragmentCallback { 88 private static final int SLICE_REQUEST_CODE = 10000; 89 private static final int A11Y_FOCUS_REQUEST_DELAY = 1000; 90 private static final String TAG = "SliceFragment"; 91 private static final String KEY_PREFERENCE_FOLLOWUP_INTENT = "key_preference_followup_intent"; 92 private static final String KEY_PREFERENCE_FOLLOWUP_RESULT_CODE = 93 "key_preference_followup_result_code"; 94 private static final String KEY_SCREEN_TITLE = "key_screen_title"; 95 private static final String KEY_SCREEN_SUBTITLE = "key_screen_subtitle"; 96 private static final String KEY_SCREEN_ICON = "key_screen_icon"; 97 private static final String KEY_LAST_PREFERENCE = "key_last_preference"; 98 private static final String KEY_URI_STRING = "key_uri_string"; 99 private ListContent mListContent; 100 private Slice mSlice; 101 private ContextThemeWrapper mContextThemeWrapper; 102 private String mUriString = null; 103 private int mCurrentPageId; 104 private CharSequence mScreenTitle; 105 private CharSequence mScreenSubtitle; 106 private Icon mScreenIcon; 107 private PendingIntent mPreferenceFollowupIntent; 108 private int mFollowupPendingIntentResultCode; 109 private Intent mFollowupPendingIntentExtras; 110 private Intent mFollowupPendingIntentExtrasCopy; 111 private String mLastFocusedPreferenceKey; 112 private boolean mIsMainPanelReady = true; 113 114 private final Handler mHandler = new Handler(); 115 private final ActivityResultLauncher<IntentSenderRequest> mActivityResultLauncher = 116 registerForActivityResult(new ActivityResultContracts.StartIntentSenderForResult(), 117 new ActivityResultCallback<ActivityResult>() { 118 @Override 119 public void onActivityResult(ActivityResult result) { 120 Intent data = result.getData(); 121 mFollowupPendingIntentExtras = data; 122 mFollowupPendingIntentExtrasCopy = data == null ? null : new Intent( 123 data); 124 mFollowupPendingIntentResultCode = result.getResultCode(); 125 } 126 }); 127 private final ContentObserver mContentObserver = new ContentObserver(new Handler()) { 128 @Override 129 public void onChange(boolean selfChange, Uri uri) { 130 handleUri(uri); 131 super.onChange(selfChange, uri); 132 } 133 }; 134 135 /** Callback for one panel settings fragment **/ 136 public interface OnePanelSliceFragmentContainer { navigateBack()137 void navigateBack(); 138 } 139 140 @Override onCreate(Bundle savedInstanceState)141 public void onCreate(Bundle savedInstanceState) { 142 mUriString = getArguments().getString(SlicesConstants.TAG_TARGET_URI); 143 if (!TextUtils.isEmpty(mUriString)) { 144 ContextSingleton.getInstance().grantFullAccess(getContext(), Uri.parse(mUriString)); 145 } 146 if (TextUtils.isEmpty(mScreenTitle)) { 147 mScreenTitle = getArguments().getCharSequence(SlicesConstants.TAG_SCREEN_TITLE, ""); 148 } 149 super.onCreate(savedInstanceState); 150 } 151 152 @Override onResume()153 public void onResume() { 154 this.setTitle(mScreenTitle); 155 this.setSubtitle(mScreenSubtitle); 156 this.setIcon(mScreenIcon); 157 this.getPreferenceScreen().removeAll(); 158 159 showProgressBar(); 160 if (!TextUtils.isEmpty(mUriString)) { 161 getSliceLiveData().observeForever(this); 162 } 163 if (TextUtils.isEmpty(mScreenTitle)) { 164 mScreenTitle = getArguments().getCharSequence(SlicesConstants.TAG_SCREEN_TITLE, ""); 165 } 166 super.onResume(); 167 if (!TextUtils.isEmpty(mUriString)) { 168 getContext().getContentResolver().registerContentObserver( 169 SlicePreferencesUtil.getStatusPath(mUriString), false, mContentObserver); 170 } 171 fireFollowupPendingIntent(); 172 } 173 getSliceLiveData()174 private SliceLiveDataImpl getSliceLiveData() { 175 return ContextSingleton.getInstance() 176 .getSliceLiveData(getActivity(), Uri.parse(mUriString)); 177 } 178 fireFollowupPendingIntent()179 private void fireFollowupPendingIntent() { 180 if (mFollowupPendingIntentExtras == null) { 181 return; 182 } 183 // If there is followup pendingIntent returned from initial activity, send it. 184 // Otherwise send the followup pendingIntent provided by slice api. 185 Parcelable followupPendingIntent; 186 try { 187 followupPendingIntent = mFollowupPendingIntentExtrasCopy.getParcelableExtra( 188 EXTRA_SLICE_FOLLOWUP); 189 } catch (Throwable ex) { 190 // unable to parse, the Intent has custom Parcelable, fallback 191 followupPendingIntent = null; 192 } 193 if (followupPendingIntent instanceof PendingIntent) { 194 try { 195 ((PendingIntent) followupPendingIntent).send(); 196 } catch (CanceledException e) { 197 Log.e(TAG, "Followup PendingIntent for slice cannot be sent", e); 198 } 199 } else { 200 if (mPreferenceFollowupIntent == null) { 201 return; 202 } 203 try { 204 mPreferenceFollowupIntent.send(getContext(), 205 mFollowupPendingIntentResultCode, mFollowupPendingIntentExtras); 206 } catch (CanceledException e) { 207 Log.e(TAG, "Followup PendingIntent for slice cannot be sent", e); 208 } 209 mPreferenceFollowupIntent = null; 210 } 211 } 212 213 @Override onPause()214 public void onPause() { 215 super.onPause(); 216 hideProgressBar(); 217 getContext().getContentResolver().unregisterContentObserver(mContentObserver); 218 getSliceLiveData().removeObserver(this); 219 } 220 221 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)222 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 223 PreferenceScreen preferenceScreen = getPreferenceManager() 224 .createPreferenceScreen(getContext()); 225 setPreferenceScreen(preferenceScreen); 226 227 TypedValue themeTypedValue = new TypedValue(); 228 getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, themeTypedValue, true); 229 mContextThemeWrapper = new ContextThemeWrapper(getActivity(), themeTypedValue.resourceId); 230 231 } 232 isUriValid(String uri)233 private boolean isUriValid(String uri) { 234 if (uri == null) { 235 return false; 236 } 237 ContentProviderClient client = 238 getContext().getContentResolver().acquireContentProviderClient(Uri.parse(uri)); 239 if (client != null) { 240 client.close(); 241 return true; 242 } else { 243 return false; 244 } 245 } 246 update()247 private void update() { 248 mListContent = new ListContent(mSlice); 249 PreferenceScreen preferenceScreen = 250 getPreferenceManager().getPreferenceScreen(); 251 252 if (preferenceScreen == null) { 253 return; 254 } 255 256 List<SliceContent> items = mListContent.getRowItems(); 257 if (items == null || items.size() == 0) { 258 return; 259 } 260 261 SliceItem redirectSliceItem = SlicePreferencesUtil.getRedirectSlice(items); 262 String redirectSlice = null; 263 if (redirectSliceItem != null) { 264 Data data = SlicePreferencesUtil.extract(redirectSliceItem); 265 CharSequence title = SlicePreferencesUtil.getText(data.mTitleItem); 266 if (!TextUtils.isEmpty(title)) { 267 redirectSlice = title.toString(); 268 } 269 } 270 if (isUriValid(redirectSlice)) { 271 getSliceLiveData().removeObserver(this); 272 getContext().getContentResolver().unregisterContentObserver(mContentObserver); 273 mUriString = redirectSlice; 274 getSliceLiveData().observeForever(this); 275 getContext().getContentResolver().registerContentObserver( 276 SlicePreferencesUtil.getStatusPath(mUriString), false, mContentObserver); 277 } 278 279 SliceItem screenTitleItem = SlicePreferencesUtil.getScreenTitleItem(items); 280 if (screenTitleItem == null) { 281 setTitle(mScreenTitle); 282 } else { 283 Data data = SlicePreferencesUtil.extract(screenTitleItem); 284 mCurrentPageId = SlicePreferencesUtil.getPageId(screenTitleItem); 285 CharSequence title = SlicePreferencesUtil.getText(data.mTitleItem); 286 if (!TextUtils.isEmpty(title)) { 287 setTitle(title); 288 mScreenTitle = title; 289 } else { 290 setTitle(mScreenTitle); 291 } 292 293 CharSequence subtitle = SlicePreferencesUtil.getText(data.mSubtitleItem); 294 setSubtitle(subtitle); 295 296 Icon icon = SlicePreferencesUtil.getIcon(data.mStartItem); 297 setIcon(icon); 298 } 299 300 SliceItem focusedPrefItem = SlicePreferencesUtil.getFocusedPreferenceItem(items); 301 CharSequence defaultFocusedKey = null; 302 if (focusedPrefItem != null) { 303 Data data = SlicePreferencesUtil.extract(focusedPrefItem); 304 CharSequence title = SlicePreferencesUtil.getText(data.mTitleItem); 305 if (!TextUtils.isEmpty(title)) { 306 defaultFocusedKey = title; 307 } 308 } 309 310 List<Preference> newPrefs = new ArrayList<>(); 311 for (SliceContent contentItem : items) { 312 SliceItem item = contentItem.getSliceItem(); 313 if (SlicesConstants.TYPE_PREFERENCE.equals(item.getSubType()) 314 || SlicesConstants.TYPE_PREFERENCE_CATEGORY.equals(item.getSubType()) 315 || SlicesConstants.TYPE_PREFERENCE_EMBEDDED_PLACEHOLDER.equals( 316 item.getSubType())) { 317 Preference preference = 318 SlicePreferencesUtil.getPreference( 319 item, mContextThemeWrapper, getClass().getCanonicalName(), 320 getParentFragment() instanceof TwoPanelSettingsFragment); 321 if (preference != null) { 322 newPrefs.add(preference); 323 } 324 } 325 } 326 updatePreferenceScreen(preferenceScreen, newPrefs); 327 if (defaultFocusedKey != null) { 328 scrollToPreference(defaultFocusedKey.toString()); 329 } else if (mLastFocusedPreferenceKey != null) { 330 scrollToPreference(mLastFocusedPreferenceKey); 331 } 332 333 if (getParentFragment() instanceof TwoPanelSettingsFragment) { 334 ((TwoPanelSettingsFragment) getParentFragment()).refocusPreference(this); 335 } 336 mIsMainPanelReady = true; 337 338 resetA11yFocusIfNeeded(); 339 } 340 341 // Because the SliceProvider may call for updates an uncertain amount of times, we 342 // should have the current focus request a11yFocus after the update, since it will 343 // be lost otherwise. The delay is to give the screen reader enough time to 344 // process the update. resetA11yFocusIfNeeded()345 private void resetA11yFocusIfNeeded() { 346 if (isA11yOn()) { 347 mHandler.postDelayed(() -> { 348 if (isResumed() && getListView() != null && getListView().findFocus() != null) { 349 getListView().findFocus().requestAccessibilityFocus(); 350 } 351 }, A11Y_FOCUS_REQUEST_DELAY); 352 } 353 } 354 isA11yOn()355 private boolean isA11yOn() { 356 if (getActivity() == null) { 357 return false; 358 } 359 return Settings.Secure.getInt( 360 getActivity().getContentResolver(), 361 Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1; 362 } 363 364 back()365 private void back() { 366 if (getCallbackFragment() instanceof TwoPanelSettingsFragment) { 367 TwoPanelSettingsFragment parentFragment = 368 (TwoPanelSettingsFragment) getCallbackFragment(); 369 if (parentFragment.isFragmentInTheMainPanel(this)) { 370 parentFragment.navigateBack(); 371 } 372 } else if (getCallbackFragment() instanceof OnePanelSliceFragmentContainer) { 373 ((OnePanelSliceFragmentContainer) getCallbackFragment()).navigateBack(); 374 } 375 } 376 forward()377 private void forward() { 378 if (mIsMainPanelReady) { 379 if (getCallbackFragment() instanceof TwoPanelSettingsFragment) { 380 TwoPanelSettingsFragment parentFragment = 381 (TwoPanelSettingsFragment) getCallbackFragment(); 382 Preference chosenPreference = TwoPanelSettingsFragment.getChosenPreference(this); 383 if (chosenPreference == null && mLastFocusedPreferenceKey != null) { 384 chosenPreference = findPreference(mLastFocusedPreferenceKey); 385 } 386 if (chosenPreference != null && chosenPreference instanceof HasSliceUri 387 && ((HasSliceUri) chosenPreference).getUri() != null) { 388 chosenPreference.setFragment(SliceFragment.class.getCanonicalName()); 389 parentFragment.refocusPreferenceForceRefresh(chosenPreference, this); 390 } 391 if (parentFragment.isFragmentInTheMainPanel(this)) { 392 parentFragment.navigateToPreviewFragment(); 393 } 394 } 395 } else { 396 mHandler.post(() -> forward()); 397 } 398 } 399 updatePreferenceScreen(PreferenceScreen screen, List<Preference> newPrefs)400 private void updatePreferenceScreen(PreferenceScreen screen, List<Preference> newPrefs) { 401 // Remove all the preferences in the screen that satisfy such three cases: 402 // (a) Preference without key 403 // (b) Preference with key which does not appear in the new list. 404 // (c) Preference with key which does appear in the new list, but the preference has changed 405 // ability to handle slices and needs to be replaced instead of re-used. 406 int index = 0; 407 while (index < screen.getPreferenceCount()) { 408 boolean needToRemoveCurrentPref = true; 409 Preference oldPref = screen.getPreference(index); 410 if (oldPref != null && oldPref.getKey() != null) { 411 for (Preference newPref : newPrefs) { 412 if (newPref.getKey() != null && newPref.getKey().equals(oldPref.getKey()) 413 && (newPref instanceof HasSliceUri) 414 == (oldPref instanceof HasSliceUri)) { 415 needToRemoveCurrentPref = false; 416 break; 417 } 418 } 419 } 420 if (needToRemoveCurrentPref) { 421 screen.removePreference(oldPref); 422 } else { 423 index++; 424 } 425 } 426 427 Map<Integer, Boolean> twoStatePreferenceIsCheckedByOrder = new HashMap<>(); 428 for (int i = 0; i < newPrefs.size(); i++) { 429 if (newPrefs.get(i) instanceof TwoStatePreference) { 430 twoStatePreferenceIsCheckedByOrder.put( 431 i, ((TwoStatePreference) newPrefs.get(i)).isChecked()); 432 } 433 } 434 435 //Iterate the new preferences list and give each preference a correct order 436 for (int i = 0; i < newPrefs.size(); i++) { 437 Preference newPref = newPrefs.get(i); 438 boolean neededToAddNewPref = true; 439 // If the newPref has a key and has a corresponding old preference, update the old 440 // preference and give it a new order. 441 if (newPref.getKey() != null) { 442 for (int j = 0; j < screen.getPreferenceCount(); j++) { 443 Preference oldPref = screen.getPreference(j); 444 // EmbeddedSlicePreference has its own slice observer 445 // (EmbeddedSlicePreferenceHelper). Should therefore not be updated by 446 // slice observer in SliceFragment. 447 boolean allowUpdate = !(oldPref instanceof EmbeddedSlicePreference); 448 boolean sameKey = oldPref.getKey() != null 449 && oldPref.getKey().equals(newPref.getKey()); 450 if (allowUpdate && sameKey) { 451 oldPref.setIcon(newPref.getIcon()); 452 oldPref.setTitle(newPref.getTitle()); 453 oldPref.setSummary(newPref.getSummary()); 454 oldPref.setEnabled(newPref.isEnabled()); 455 oldPref.setSelectable(newPref.isSelectable()); 456 oldPref.setFragment(newPref.getFragment()); 457 oldPref.getExtras().putAll(newPref.getExtras()); 458 if ((oldPref instanceof HasSliceAction) 459 && (newPref instanceof HasSliceAction)) { 460 ((HasSliceAction) oldPref) 461 .setSliceAction(((HasSliceAction) newPref).getSliceAction()); 462 } 463 if ((oldPref instanceof HasSliceUri) 464 && (newPref instanceof HasSliceUri)) { 465 ((HasSliceUri) oldPref) 466 .setUri(((HasSliceUri) newPref).getUri()); 467 } 468 if ((oldPref instanceof HasCustomContentDescription) 469 && (newPref instanceof HasCustomContentDescription)) { 470 ((HasCustomContentDescription) oldPref).setContentDescription( 471 ((HasCustomContentDescription) newPref) 472 .getContentDescription()); 473 } 474 oldPref.setOrder(i); 475 } 476 if (sameKey) { 477 neededToAddNewPref = false; 478 break; 479 } 480 } 481 } 482 // If the newPref cannot find a corresponding old preference, or it does not have a key, 483 // add it to the screen with the correct order. 484 if (neededToAddNewPref) { 485 newPref.setOrder(i); 486 screen.addPreference(newPref); 487 } 488 } 489 //addPreference will reset the checked status of TwoStatePreference. 490 //So we need to add them back 491 for (int i = 0; i < screen.getPreferenceCount(); i++) { 492 Preference screenPref = screen.getPreference(i); 493 if (screenPref instanceof TwoStatePreference 494 && twoStatePreferenceIsCheckedByOrder.get(i) != null) { 495 ((TwoStatePreference) screenPref) 496 .setChecked(twoStatePreferenceIsCheckedByOrder.get(i)); 497 } 498 } 499 removeAnimationClipping(getView()); 500 } 501 removeAnimationClipping(View v)502 protected void removeAnimationClipping(View v) { 503 if (v instanceof ViewGroup) { 504 ((ViewGroup) v).setClipChildren(false); 505 ((ViewGroup) v).setClipToPadding(false); 506 for (int index = 0; index < ((ViewGroup) v).getChildCount(); index++) { 507 View child = ((ViewGroup) v).getChildAt(index); 508 removeAnimationClipping(child); 509 } 510 } 511 } 512 513 @Override onPreferenceFocused(Preference preference)514 public void onPreferenceFocused(Preference preference) { 515 setLastFocused(preference); 516 } 517 518 @Override onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue)519 public void onSeekbarPreferenceChanged(SliceSeekbarPreference preference, int addValue) { 520 int curValue = preference.getValue(); 521 if((addValue > 0 && curValue < preference.getMax()) || 522 (addValue < 0 && curValue > preference.getMin())) { 523 preference.setValue(curValue + addValue); 524 525 try { 526 Intent fillInIntent = 527 new Intent() 528 .putExtra(EXTRA_SLIDER_VALUE, preference.getValue()) 529 .putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()); 530 firePendingIntent((HasSliceAction) preference, fillInIntent); 531 } catch (Exception e) { 532 Log.e(TAG, "PendingIntent for slice cannot be sent", e); 533 } 534 } 535 } 536 537 @Override onPreferenceTreeClick(Preference preference)538 public boolean onPreferenceTreeClick(Preference preference) { 539 if (preference instanceof SliceRadioPreference) { 540 SliceRadioPreference radioPref = (SliceRadioPreference) preference; 541 if (!radioPref.isChecked()) { 542 radioPref.setChecked(true); 543 if (TextUtils.isEmpty(radioPref.getUri())) { 544 return true; 545 } 546 } 547 548 logEntrySelected(getPreferenceActionId(preference)); 549 Intent fillInIntent = new Intent().putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()); 550 551 boolean result = firePendingIntent(radioPref, fillInIntent); 552 radioPref.clearOtherRadioPreferences(getPreferenceScreen()); 553 if (result) { 554 return true; 555 } 556 } else if (preference instanceof TwoStatePreference 557 && preference instanceof HasSliceAction) { 558 boolean isChecked = ((TwoStatePreference) preference).isChecked(); 559 preference.getExtras().putBoolean(EXTRA_PREFERENCE_INFO_STATUS, isChecked); 560 if (getParentFragment() instanceof TwoPanelSettingsFragment) { 561 ((TwoPanelSettingsFragment) getParentFragment()).refocusPreference(this); 562 } 563 logToggleInteracted(getPreferenceActionId(preference), isChecked); 564 Intent fillInIntent = 565 new Intent() 566 .putExtra(EXTRA_TOGGLE_STATE, isChecked) 567 .putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()); 568 if (firePendingIntent((HasSliceAction) preference, fillInIntent)) { 569 return true; 570 } 571 return true; 572 } else if (preference instanceof SlicePreference) { 573 // In this case, we may intentionally ignore this entry selection to avoid double 574 // logging as the action should result in a PAGE_FOCUSED event being logged. 575 if (getPreferenceActionId(preference) != TvSettingsEnums.ENTRY_DEFAULT) { 576 logEntrySelected(getPreferenceActionId(preference)); 577 } 578 Intent fillInIntent = 579 new Intent().putExtra(EXTRA_PREFERENCE_KEY, preference.getKey()); 580 if (firePendingIntent((HasSliceAction) preference, fillInIntent)) { 581 return true; 582 } 583 } 584 585 return super.onPreferenceTreeClick(preference); 586 } 587 firePendingIntent(@onNull HasSliceAction preference, Intent fillInIntent)588 private boolean firePendingIntent(@NonNull HasSliceAction preference, Intent fillInIntent) { 589 if (preference.getSliceAction() == null) { 590 return false; 591 } 592 IntentSender intentSender = preference.getSliceAction().getAction().getIntentSender(); 593 mActivityResultLauncher.launch( 594 new IntentSenderRequest.Builder(intentSender).setFillInIntent( 595 fillInIntent).build()); 596 if (preference.getFollowupSliceAction() != null) { 597 mPreferenceFollowupIntent = preference.getFollowupSliceAction().getAction(); 598 } 599 600 return true; 601 } 602 603 @Override onSaveInstanceState(Bundle outState)604 public void onSaveInstanceState(Bundle outState) { 605 super.onSaveInstanceState(outState); 606 outState.putParcelable(KEY_PREFERENCE_FOLLOWUP_INTENT, mPreferenceFollowupIntent); 607 outState.putInt(KEY_PREFERENCE_FOLLOWUP_RESULT_CODE, mFollowupPendingIntentResultCode); 608 outState.putCharSequence(KEY_SCREEN_TITLE, mScreenTitle); 609 outState.putCharSequence(KEY_SCREEN_SUBTITLE, mScreenSubtitle); 610 outState.putParcelable(KEY_SCREEN_ICON, mScreenIcon); 611 outState.putString(KEY_LAST_PREFERENCE, mLastFocusedPreferenceKey); 612 outState.putString(KEY_URI_STRING, mUriString); 613 } 614 615 @Override onActivityCreated(Bundle savedInstanceState)616 public void onActivityCreated(Bundle savedInstanceState) { 617 super.onActivityCreated(savedInstanceState); 618 if (savedInstanceState != null) { 619 mPreferenceFollowupIntent = 620 savedInstanceState.getParcelable(KEY_PREFERENCE_FOLLOWUP_INTENT); 621 mFollowupPendingIntentResultCode = 622 savedInstanceState.getInt(KEY_PREFERENCE_FOLLOWUP_RESULT_CODE); 623 mScreenTitle = savedInstanceState.getCharSequence(KEY_SCREEN_TITLE); 624 mScreenSubtitle = savedInstanceState.getCharSequence(KEY_SCREEN_SUBTITLE); 625 mScreenIcon = savedInstanceState.getParcelable(KEY_SCREEN_ICON); 626 mLastFocusedPreferenceKey = savedInstanceState.getString(KEY_LAST_PREFERENCE); 627 mUriString = savedInstanceState.getString(KEY_URI_STRING); 628 } 629 } 630 631 @Override onChanged(@onNull Slice slice)632 public void onChanged(@NonNull Slice slice) { 633 mSlice = slice; 634 // Make TvSettings guard against the case that slice provider is not set up correctly 635 if (slice == null || slice.getHints() == null) { 636 return; 637 } 638 639 if (slice.getHints().contains(HINT_PARTIAL)) { 640 showProgressBar(); 641 } else { 642 hideProgressBar(); 643 } 644 mIsMainPanelReady = false; 645 update(); 646 } 647 showProgressBar()648 private void showProgressBar() { 649 View view = this.getView(); 650 View progressBar = view == null ? null : getView().findViewById(R.id.progress_bar); 651 if (progressBar != null) { 652 progressBar.bringToFront(); 653 progressBar.setVisibility(View.VISIBLE); 654 } 655 } 656 hideProgressBar()657 private void hideProgressBar() { 658 View view = this.getView(); 659 View progressBar = view == null ? null : getView().findViewById(R.id.progress_bar); 660 if (progressBar != null) { 661 progressBar.setVisibility(View.GONE); 662 } 663 } 664 setSubtitle(CharSequence subtitle)665 private void setSubtitle(CharSequence subtitle) { 666 View view = this.getView(); 667 TextView decorSubtitle = view == null 668 ? null 669 : (TextView) view.findViewById(R.id.decor_subtitle); 670 if (decorSubtitle != null) { 671 // This is to remedy some complicated RTL scenario such as Hebrew RTL Account slice with 672 // English account name subtitle. 673 if (getResources().getConfiguration().getLayoutDirection() 674 == View.LAYOUT_DIRECTION_RTL) { 675 decorSubtitle.setGravity(Gravity.TOP | Gravity.RIGHT); 676 } 677 if (TextUtils.isEmpty(subtitle)) { 678 decorSubtitle.setVisibility(View.GONE); 679 } else { 680 decorSubtitle.setVisibility(View.VISIBLE); 681 decorSubtitle.setText(subtitle); 682 } 683 } 684 mScreenSubtitle = subtitle; 685 } 686 setIcon(Icon icon)687 private void setIcon(Icon icon) { 688 View view = this.getView(); 689 ImageView decorIcon = view == null ? null : (ImageView) view.findViewById(R.id.decor_icon); 690 if (decorIcon != null && icon != null) { 691 TextView decorTitle = view.findViewById(R.id.decor_title); 692 if (decorTitle != null) { 693 decorTitle.setMaxWidth( 694 getResources().getDimensionPixelSize(R.dimen.decor_title_width)); 695 } 696 decorIcon.setImageDrawable(icon.loadDrawable(mContextThemeWrapper)); 697 decorIcon.setVisibility(View.VISIBLE); 698 } else if (decorIcon != null) { 699 decorIcon.setVisibility(View.GONE); 700 } 701 mScreenIcon = icon; 702 } 703 704 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)705 public View onCreateView( 706 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 707 final ViewGroup view = 708 (ViewGroup) super.onCreateView(inflater, container, savedInstanceState); 709 LayoutInflater themedInflater = LayoutInflater.from(view.getContext()); 710 final View newTitleContainer = themedInflater.inflate( 711 R.layout.slice_title_container, container, false); 712 view.removeView(view.findViewById(R.id.decor_title_container)); 713 view.addView(newTitleContainer, 0); 714 715 if (newTitleContainer != null) { 716 newTitleContainer.setOutlineProvider(null); 717 newTitleContainer.setBackgroundResource(R.color.tp_preference_panel_background_color); 718 } 719 720 final View newContainer = 721 themedInflater.inflate(R.layout.slice_progress_bar, container, false); 722 if (newContainer != null) { 723 ((ViewGroup) newContainer).addView(view); 724 } 725 return newContainer; 726 } 727 setLastFocused(Preference preference)728 public void setLastFocused(Preference preference) { 729 mLastFocusedPreferenceKey = preference.getKey(); 730 } 731 handleUri(Uri uri)732 private void handleUri(Uri uri) { 733 String uriString = uri.getQueryParameter(SlicesConstants.PARAMETER_URI); 734 String errorMessage = uri.getQueryParameter(SlicesConstants.PARAMETER_ERROR); 735 // Display the errorMessage based upon two different scenarios: 736 // a) If the provided uri string matches with current page slice uri(usually happens 737 // when the data fails to correctly load), show the errors in the current panel using 738 // InfoFragment UI. 739 // b) If the provided uri string does not match with current page slice uri(usually happens 740 // when the data fails to save), show the error message as the toast. 741 if (uriString != null && errorMessage != null) { 742 if (!uriString.equals(mUriString)) { 743 showErrorMessageAsToast(errorMessage); 744 } else { 745 showErrorMessage(errorMessage); 746 } 747 } 748 // Provider should provide the correct slice uri in the parameter if it wants to do certain 749 // action(includes go back, forward), otherwise TvSettings would ignore it. 750 if (uriString == null || !uriString.equals(mUriString)) { 751 return; 752 } 753 String direction = uri.getQueryParameter(SlicesConstants.PARAMETER_DIRECTION); 754 if (direction != null) { 755 if (direction.equals(SlicesConstants.FORWARD)) { 756 forward(); 757 } else if (direction.equals(SlicesConstants.BACKWARD)) { 758 back(); 759 } else if (direction.equals(SlicesConstants.EXIT)) { 760 finish(); 761 } 762 } 763 } 764 showErrorMessageAsToast(String errorMessage)765 private void showErrorMessageAsToast(String errorMessage) { 766 Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_SHORT).show(); 767 } 768 finish()769 private void finish() { 770 getActivity().setResult(Activity.RESULT_OK); 771 getActivity().finish(); 772 } 773 showErrorMessage(String errorMessage)774 private void showErrorMessage(String errorMessage) { 775 if (getCallbackFragment() instanceof TwoPanelSettingsFragment) { 776 ((TwoPanelSettingsFragment) getCallbackFragment()).showErrorMessage(errorMessage, this); 777 } 778 } 779 getPreferenceActionId(Preference preference)780 private int getPreferenceActionId(Preference preference) { 781 if (preference instanceof HasSliceAction) { 782 return ((HasSliceAction) preference).getActionId() != 0 783 ? ((HasSliceAction) preference).getActionId() 784 : TvSettingsEnums.ENTRY_DEFAULT; 785 } 786 return TvSettingsEnums.ENTRY_DEFAULT; 787 } 788 getScreenTitle()789 public CharSequence getScreenTitle() { 790 return mScreenTitle; 791 } 792 793 @Override getPageId()794 protected int getPageId() { 795 return mCurrentPageId != 0 ? mCurrentPageId : TvSettingsEnums.PAGE_SLICE_DEFAULT; 796 } 797 798 @Deprecated getMetricsCategory()799 public int getMetricsCategory() { 800 return 0; 801 } 802 } 803