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