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