1 /* <lambda>null2 * Copyright (C) 2024 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 package com.android.tv.twopanelsettings.slices 17 18 import android.app.Activity 19 import android.app.PendingIntent 20 import android.app.slice.Slice.HINT_PARTIAL 21 import android.app.tvsettings.TvSettingsEnums 22 import android.content.ContentProviderClient 23 import android.content.Context 24 import android.content.Intent 25 import android.content.IntentSender 26 import android.content.res.Configuration 27 import android.database.ContentObserver 28 import android.graphics.drawable.Drawable 29 import android.graphics.drawable.Icon 30 import android.net.Uri 31 import android.os.Bundle 32 import android.os.Handler 33 import android.os.Looper 34 import android.os.Parcelable 35 import android.text.TextUtils 36 import android.util.Log 37 import android.view.View 38 import android.view.ViewGroup 39 import android.widget.Toast 40 import androidx.activity.result.ActivityResult 41 import androidx.activity.result.ActivityResultLauncher 42 import androidx.activity.result.IntentSenderRequest 43 import androidx.activity.result.contract.ActivityResultContracts 44 import androidx.leanback.preference.LeanbackPreferenceFragmentCompat 45 import androidx.lifecycle.DefaultLifecycleObserver 46 import androidx.lifecycle.LifecycleOwner 47 import androidx.lifecycle.Observer 48 import androidx.lifecycle.coroutineScope 49 import androidx.preference.Preference 50 import androidx.preference.PreferenceGroup 51 import androidx.preference.PreferenceScreen 52 import androidx.preference.TwoStatePreference 53 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment 54 import com.android.tv.twopanelsettings.slices.compat.Slice 55 import com.android.tv.twopanelsettings.slices.compat.SliceItem 56 import com.android.tv.twopanelsettings.slices.compat.SliceViewManager 57 import com.android.tv.twopanelsettings.slices.compat.widget.ListContent 58 import com.android.tv.twopanelsettings.slices.compat.widget.SliceContent 59 import java.util.IdentityHashMap 60 import kotlinx.coroutines.Dispatchers 61 import kotlinx.coroutines.launch 62 import kotlinx.coroutines.withContext 63 64 /** 65 * Provides functionality for a fragment to display slice data. 66 */ 67 class SliceShard( 68 private val mFragment: LeanbackPreferenceFragmentCompat, uriString: String?, 69 callbacks: Callbacks, initialTitle: CharSequence, prefContext: Context, 70 private val isCached: Boolean = false 71 ) { 72 private val mCallbacks: Callbacks 73 private val mInitialTitle: CharSequence 74 75 private var mUriString: String? = null 76 private var mSlice: Slice? = null 77 private val mPrefContext: Context 78 private val mSliceCacheManager = SliceCacheManager.getInstance(prefContext) 79 var screenTitle: CharSequence? 80 private set 81 private var mScreenSubtitle: CharSequence? = null 82 private var mScreenIcon: Icon? = null 83 private var mPreferenceFollowupIntent: Parcelable? = null 84 private var mFollowupPendingIntentResultCode: Int = 0 85 private var mFollowupPendingIntentExtras: Intent? = null 86 private var mFollowupPendingIntentExtrasCopy: Intent? = null 87 private var mLastFocusedPreferenceKey: String? = null 88 private var mIsMainPanelReady: Boolean = true 89 private var mCurrentPageId: Int = 0 90 91 private val mHandler: Handler = Handler(Looper.getMainLooper()) 92 private val mActivityResultLauncher: ActivityResultLauncher<IntentSenderRequest> 93 private val mActivityResultLauncherIntent: ActivityResultLauncher<Intent> 94 private val mActivityResultLauncherIntentFollowup: ActivityResultLauncher<Intent> 95 private val mSliceObserver: Observer<Slice> 96 private val mContentObserver: ContentObserver = object : ContentObserver(mHandler) { 97 override fun onChange(selfChange: Boolean, uri: Uri?) { 98 handleUri(uri!!) 99 } 100 } 101 102 init { 103 setUri(uriString) 104 mCallbacks = callbacks 105 mInitialTitle = initialTitle 106 mActivityResultLauncher = 107 mFragment.registerForActivityResult<IntentSenderRequest, ActivityResult>( 108 ActivityResultContracts.StartIntentSenderForResult(), 109 { result: ActivityResult -> this.processActionResult(result) }) 110 mActivityResultLauncherIntent = mFragment.registerForActivityResult<Intent, ActivityResult>( 111 ActivityResultContracts.StartActivityForResult(), 112 { result: ActivityResult -> this.processActionResult(result) }) 113 mActivityResultLauncherIntentFollowup = 114 mFragment.registerForActivityResult<Intent, ActivityResult>( 115 ActivityResultContracts.StartActivityForResult(), 116 { result: ActivityResult? -> }) 117 screenTitle = initialTitle 118 mPrefContext = prefContext 119 120 mFragment.lifecycle.addObserver(object : DefaultLifecycleObserver { 121 override fun onResume(owner: LifecycleOwner) { 122 resume() 123 } 124 125 override fun onPause(owner: LifecycleOwner) { 126 pause() 127 } 128 }) 129 130 if (isCached) { 131 mFragment.lifecycle.coroutineScope.launch { 132 mCallbacks.showProgressBar(true) 133 val slice = try { 134 loadCachedSlice(mFragment.resources.configuration) 135 } catch (e: Exception) { 136 Log.e(TAG, "Unable to load $mUriString", e) 137 null 138 } 139 if (slice != null) { 140 mIsMainPanelReady = false 141 mSlice = slice 142 update() 143 } else { 144 mCallbacks.showProgressBar(false) 145 mCallbacks.onSlice(null) 146 } 147 148 } 149 } 150 151 mSliceObserver = Observer { slice: Slice? -> 152 mSlice = slice 153 // Make TvSettings guard against the case that slice provider is not set up correctly 154 if (slice == null || slice.hints == null) { 155 return@Observer 156 } 157 158 if (slice.hints.contains(HINT_PARTIAL)) { 159 mCallbacks.showProgressBar(true) 160 } else { 161 mCallbacks.showProgressBar(false) 162 } 163 mIsMainPanelReady = false 164 update() 165 } 166 } 167 168 private fun resume() { 169 if (TextUtils.isEmpty(screenTitle)) { 170 screenTitle = mInitialTitle 171 } 172 173 mCallbacks.setTitle(screenTitle) 174 mCallbacks.setSubtitle(mScreenSubtitle) 175 mCallbacks.setIcon(if (mScreenIcon != null) mScreenIcon!!.loadDrawable(mPrefContext) else null) 176 177 if (!isCached && !TextUtils.isEmpty(mUriString)) { 178 mCallbacks.showProgressBar(true) 179 sliceLiveData.observeForever(mSliceObserver) 180 mFragment.requireContext().contentResolver.registerContentObserver( 181 SlicePreferencesUtil.getStatusPath(mUriString), false, mContentObserver 182 ) 183 } 184 fireFollowupPendingIntent() 185 } 186 187 private fun pause() { 188 mCallbacks.showProgressBar(false) 189 requireContext().contentResolver.unregisterContentObserver(mContentObserver) 190 sliceLiveData.removeObserver(mSliceObserver) 191 } 192 193 private suspend fun loadCachedSlice(configuration: Configuration) : Slice? { 194 val uri = Uri.parse(mUriString) 195 val cachedSlice = mSliceCacheManager.getCachedSlice(uri, configuration) 196 if (cachedSlice != null) { 197 withContext(Dispatchers.IO) { 198 preloadPreferenceBuilders() 199 } 200 return cachedSlice 201 } 202 203 mCallbacks.showProgressBar(false) // Show fallback while loading slice. 204 205 return withContext(Dispatchers.IO) { 206 val viewManager = SliceViewManager.getInstance(mPrefContext) 207 val slice = viewManager.bindSlice(uri) 208 if (slice != null && !slice.hints.contains(HINT_PARTIAL)) { 209 preloadPreferenceBuilders() 210 mSliceCacheManager.saveCachedSlice(uri, configuration, slice) 211 return@withContext slice; 212 } 213 return@withContext null 214 } 215 } 216 217 private suspend fun preloadPreferenceBuilders() { 218 // Preload to reduce main thread overhead 219 NonSlicePreferenceBuilder.forClassName(PreferenceGroup::class.qualifiedName!!) 220 } 221 222 private val sliceLiveData: PreferenceSliceLiveData.SliceLiveDataImpl 223 get() = ContextSingleton.getInstance() 224 .getSliceLiveData(mFragment.requireActivity(), Uri.parse(mUriString)) 225 226 private fun processActionResult(result: ActivityResult) { 227 val data: Intent? = result.data 228 mFollowupPendingIntentExtras = data 229 mFollowupPendingIntentExtrasCopy = if (data == null) null else Intent( 230 data 231 ) 232 mFollowupPendingIntentResultCode = result.resultCode 233 } 234 235 private fun fireFollowupPendingIntent() { 236 if (mFollowupPendingIntentExtras == null) { 237 return 238 } 239 // If there is followup pendingIntent returned from initial activity, send it. 240 // Otherwise send the followup pendingIntent provided by slice api. 241 var followupPendingIntent: Parcelable? 242 try { 243 followupPendingIntent = mFollowupPendingIntentExtrasCopy!!.getParcelableExtra( 244 SlicesConstants.EXTRA_SLICE_FOLLOWUP 245 ) 246 } catch (ex: Throwable) { 247 // unable to parse, the Intent has custom Parcelable, fallback 248 followupPendingIntent = null 249 } 250 if (followupPendingIntent is PendingIntent) { 251 try { 252 followupPendingIntent.send() 253 } catch (e: PendingIntent.CanceledException) { 254 Log.e(TAG, "Followup PendingIntent for slice cannot be sent", e) 255 } 256 } else { 257 if (mPreferenceFollowupIntent == null) { 258 return 259 } 260 if (mPreferenceFollowupIntent is Intent) { 261 val filledIn: Intent = Intent(mPreferenceFollowupIntent as Intent) 262 filledIn.fillIn(mFollowupPendingIntentExtras!!, 0) 263 if (requireContext().packageManager.resolveActivity(filledIn, 0) != null) { 264 mActivityResultLauncherIntentFollowup.launch(filledIn) 265 } else { 266 requireContext().sendBroadcast(filledIn) 267 } 268 } else { 269 try { 270 (mPreferenceFollowupIntent as PendingIntent).send( 271 requireContext(), 272 mFollowupPendingIntentResultCode, mFollowupPendingIntentExtras 273 ) 274 } catch (e: PendingIntent.CanceledException) { 275 Log.e(TAG, "Followup PendingIntent for slice cannot be sent", e) 276 } 277 } 278 mPreferenceFollowupIntent = null 279 } 280 } 281 282 private fun requireContext(): Context { 283 return mFragment.requireContext() 284 } 285 286 private fun isUriValid(uri: String?): Boolean { 287 if (uri == null) { 288 return false 289 } 290 val client: ContentProviderClient? = 291 requireContext().contentResolver.acquireContentProviderClient(Uri.parse(uri)) 292 if (client != null) { 293 client.close() 294 return true 295 } else { 296 return false 297 } 298 } 299 300 private fun update() { 301 val listContent: ListContent = ListContent( 302 mSlice!! 303 ) 304 val preferenceScreen: PreferenceScreen? = 305 mFragment.preferenceManager.preferenceScreen 306 307 if (preferenceScreen == null) { 308 return 309 } 310 311 val items: List<SliceContent> = listContent.rowItems 312 if (items == null || items.isEmpty()) { 313 return 314 } 315 316 val redirectSliceItem: SliceItem? = SlicePreferencesUtil.getRedirectSlice(items) 317 var redirectSlice: String? = null 318 if (redirectSliceItem != null) { 319 val data: SlicePreferencesUtil.Data = SlicePreferencesUtil.extract(redirectSliceItem) 320 val title: CharSequence = SlicePreferencesUtil.getText(data.mTitleItem) 321 if (!TextUtils.isEmpty(title)) { 322 redirectSlice = title.toString() 323 } 324 } 325 if (isUriValid(redirectSlice)) { 326 sliceLiveData.removeObserver(mSliceObserver) 327 requireContext().contentResolver.unregisterContentObserver(mContentObserver) 328 setUri(redirectSlice) 329 sliceLiveData.observeForever(mSliceObserver) 330 requireContext().contentResolver.registerContentObserver( 331 SlicePreferencesUtil.getStatusPath(mUriString), false, mContentObserver 332 ) 333 } 334 335 val screenTitleItem: SliceItem? = SlicePreferencesUtil.getScreenTitleItem(items) 336 if (screenTitleItem == null) { 337 mCallbacks.setTitle(screenTitle) 338 } else { 339 val data: SlicePreferencesUtil.Data = SlicePreferencesUtil.extract(screenTitleItem) 340 mCurrentPageId = SlicePreferencesUtil.getPageId(screenTitleItem) 341 val title: CharSequence = SlicePreferencesUtil.getText(data.mTitleItem) 342 if (!TextUtils.isEmpty(title)) { 343 mCallbacks.setTitle(title) 344 screenTitle = title 345 } else { 346 mCallbacks.setTitle(screenTitle) 347 } 348 349 val subtitle: CharSequence = SlicePreferencesUtil.getText(data.mSubtitleItem) 350 mScreenSubtitle = subtitle 351 mCallbacks.setSubtitle(subtitle) 352 353 val icon: Icon? = SlicePreferencesUtil.getIcon(data.mStartItem) 354 mScreenIcon = icon 355 mCallbacks.setIcon(if (icon != null) icon.loadDrawable(mPrefContext) else null) 356 } 357 358 val focusedPrefItem: SliceItem? = SlicePreferencesUtil.getFocusedPreferenceItem(items) 359 var defaultFocusedKey: CharSequence? = null 360 if (focusedPrefItem != null) { 361 val data: SlicePreferencesUtil.Data = SlicePreferencesUtil.extract(focusedPrefItem) 362 val title: CharSequence = SlicePreferencesUtil.getText(data.mTitleItem) 363 if (!TextUtils.isEmpty(title)) { 364 defaultFocusedKey = title 365 } 366 } 367 368 val newPrefs: MutableList<Preference> = ArrayList() 369 for (contentItem: SliceContent in items) { 370 val item: SliceItem? = contentItem.sliceItem 371 if (SlicesConstants.TYPE_PREFERENCE == item!!.subType 372 || SlicesConstants.TYPE_PREFERENCE_CATEGORY == item.subType 373 || SlicesConstants.TYPE_PREFERENCE_EMBEDDED_PLACEHOLDER == item.subType 374 ) { 375 val preference: Preference? = 376 SlicePreferencesUtil.getPreference( 377 item, mPrefContext, SliceFragment::class.java.canonicalName, 378 isTwoPanel, 379 mFragment.preferenceScreen 380 ) 381 if (preference != null) { 382 newPrefs.add(preference) 383 } 384 } 385 } 386 updatePreferenceGroup(preferenceScreen, newPrefs) 387 388 if (defaultFocusedKey != null) { 389 mFragment.scrollToPreference(defaultFocusedKey.toString()) 390 } else if (mLastFocusedPreferenceKey != null) { 391 mFragment.scrollToPreference(mLastFocusedPreferenceKey!!) 392 } 393 394 if (isTwoPanel) { 395 (mFragment.parentFragment as TwoPanelSettingsFragment).refocusPreference(mFragment) 396 } 397 mIsMainPanelReady = true 398 mCallbacks.onSlice(mSlice) 399 } 400 401 private fun updatePreferenceGroup(group: PreferenceGroup, newPrefs: List<Preference>) { 402 // Remove all the preferences in the screen that satisfy such three cases: 403 // (a) Preference without key 404 // (b) Preference with key which does not appear in the new list. 405 // (c) Preference with key which does appear in the new list, but the preference has changed 406 // ability to handle slices and needs to be replaced instead of re-used. 407 var index: Int = 0 408 val newToOld: IdentityHashMap<Preference, Preference> = IdentityHashMap() 409 while (index < group.preferenceCount) { 410 var needToRemoveCurrentPref: Boolean = true 411 val oldPref: Preference = group.getPreference(index) 412 for (newPref: Preference in newPrefs) { 413 if (isSamePreference(oldPref, newPref)) { 414 needToRemoveCurrentPref = false 415 newToOld[newPref] = oldPref 416 break 417 } 418 } 419 420 if (needToRemoveCurrentPref) { 421 group.removePreference(oldPref) 422 } else { 423 index++ 424 } 425 } 426 427 val twoStatePreferenceIsCheckedByOrder: MutableMap<Int, Boolean?> = HashMap() 428 for (i in newPrefs.indices) { 429 if (newPrefs.get(i) is TwoStatePreference) { 430 twoStatePreferenceIsCheckedByOrder[i] = 431 (newPrefs.get(i) as TwoStatePreference).isChecked 432 } 433 } 434 435 //Iterate the new preferences list and give each preference a correct order 436 for (i in newPrefs.indices) { 437 val newPref: Preference = newPrefs[i] 438 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 val oldPref: Preference? = newToOld[newPref] 442 if (oldPref == null) { 443 newPref.order = i 444 group.addPreference(newPref) 445 continue 446 } 447 448 oldPref.order = i 449 if (oldPref is EmbeddedSlicePreference) { 450 // EmbeddedSlicePreference has its own slice observer 451 // (EmbeddedSlicePreferenceHelper). Should therefore not be updated by 452 // slice observer in SliceFragment. 453 // The order will however still need to be updated, as this can not be handled 454 // by EmbeddedSlicePreferenceHelper. 455 continue 456 } 457 458 oldPref.icon = newPref.icon 459 oldPref.title = newPref.title 460 oldPref.summary = newPref.summary 461 oldPref.isEnabled = newPref.isEnabled 462 oldPref.isSelectable = newPref.isSelectable 463 oldPref.fragment = newPref.fragment 464 oldPref.extras.putAll(newPref.extras) 465 if ((oldPref is HasSliceAction) 466 && (newPref is HasSliceAction) 467 ) { 468 (oldPref as HasSliceAction).sliceAction = (newPref as HasSliceAction).sliceAction 469 } 470 if ((oldPref is HasSliceUri) 471 && (newPref is HasSliceUri) 472 ) { 473 (oldPref as HasSliceUri).uri = (newPref as HasSliceUri).uri 474 } 475 if ((oldPref is HasCustomContentDescription) 476 && (newPref is HasCustomContentDescription) 477 ) { 478 (oldPref as HasCustomContentDescription).contentDescription = 479 (newPref as HasCustomContentDescription) 480 .contentDescription 481 } 482 483 if (oldPref is PreferenceGroup && newPref is PreferenceGroup) { 484 val newGroup: PreferenceGroup = newPref 485 val newChildren: MutableList<Preference> = ArrayList() 486 for (j in 0 until newGroup.preferenceCount) { 487 newChildren.add(newGroup.getPreference(j)) 488 } 489 newGroup.removeAll() 490 updatePreferenceGroup(oldPref, newChildren) 491 } 492 } 493 494 //addPreference will reset the checked status of TwoStatePreference. 495 //So we need to add them back 496 for (i in 0 until group.preferenceCount) { 497 val screenPref: Preference = group.getPreference(i) 498 if (screenPref is TwoStatePreference 499 && twoStatePreferenceIsCheckedByOrder.get(screenPref.getOrder()) != null 500 ) { 501 screenPref.isChecked = 502 twoStatePreferenceIsCheckedByOrder.get(screenPref.getOrder())!! 503 } 504 } 505 } 506 507 fun onPreferenceFocused(preference: Preference) { 508 mLastFocusedPreferenceKey = preference.key 509 } 510 511 fun onSeekbarPreferenceChanged(preference: SliceSeekbarPreference, addValue: Int) { 512 val curValue: Int = preference.value 513 if ((addValue > 0 && curValue < preference.max) || 514 (addValue < 0 && curValue > preference.min) 515 ) { 516 preference.value = curValue + addValue 517 518 try { 519 val fillInIntent: Intent = 520 Intent() 521 .putExtra(SlicesConstants.EXTRA_PREFERENCE_KEY, preference.key) 522 firePendingIntent(preference, fillInIntent) 523 } catch (e: Exception) { 524 Log.e(TAG, "PendingIntent for slice cannot be sent", e) 525 } 526 } 527 } 528 529 fun onPreferenceTreeClick(preference: Preference): Boolean { 530 if (preference is SliceRadioPreference) { 531 val radioPref: SliceRadioPreference = preference 532 if (!radioPref.isChecked) { 533 radioPref.isChecked = true 534 if (TextUtils.isEmpty(radioPref.uri)) { 535 return true 536 } 537 } 538 539 InstrumentationUtils.logEntrySelected(getPreferenceActionId(preference)) 540 val fillInIntent: Intent = 541 Intent().putExtra(SlicesConstants.EXTRA_PREFERENCE_KEY, preference.getKey()) 542 543 val result: Boolean = firePendingIntent(radioPref, fillInIntent) 544 radioPref.clearOtherRadioPreferences(mFragment.preferenceScreen) 545 if (result) { 546 return true 547 } 548 } else if (preference is TwoStatePreference 549 && preference is HasSliceAction 550 ) { 551 val isChecked: Boolean = (preference as TwoStatePreference).isChecked 552 preference.getExtras() 553 .putBoolean(SlicesConstants.EXTRA_PREFERENCE_INFO_STATUS, isChecked) 554 if (isTwoPanel) { 555 (mFragment.parentFragment as TwoPanelSettingsFragment).refocusPreference( 556 mFragment 557 ) 558 } 559 InstrumentationUtils.logToggleInteracted(getPreferenceActionId(preference), isChecked) 560 val fillInIntent: Intent = 561 Intent() 562 .putExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, isChecked) 563 .putExtra(SlicesConstants.EXTRA_PREFERENCE_KEY, preference.getKey()) 564 if (firePendingIntent(preference as HasSliceAction, fillInIntent)) { 565 return true 566 } 567 return true 568 } else if (preference is SlicePreference) { 569 // In this case, we may intentionally ignore this entry selection to avoid double 570 // logging as the action should result in a PAGE_FOCUSED event being logged. 571 if (getPreferenceActionId(preference) != TvSettingsEnums.ENTRY_DEFAULT) { 572 InstrumentationUtils.logEntrySelected(getPreferenceActionId(preference)) 573 } 574 val fillInIntent: Intent = 575 Intent().putExtra(SlicesConstants.EXTRA_PREFERENCE_KEY, preference.getKey()) 576 if (firePendingIntent(preference as HasSliceAction, fillInIntent)) { 577 return true 578 } 579 } 580 581 return false 582 } 583 584 private val isTwoPanel: Boolean 585 get() { 586 return mFragment.parentFragment is TwoPanelSettingsFragment 587 } 588 589 private fun firePendingIntent(preference: HasSliceAction, fillInIntent: Intent?): Boolean { 590 if (preference.sliceAction == null) { 591 return false 592 } 593 594 val intent: Intent? = preference.sliceAction.actionIntent 595 if (intent != null) { 596 val filledIn: Intent = Intent(intent) 597 if (fillInIntent != null) { 598 filledIn.fillIn(fillInIntent, 0) 599 } 600 601 if (requireContext().packageManager.resolveActivity(filledIn, 0) != null) { 602 mActivityResultLauncherIntent.launch(filledIn) 603 } else { 604 requireContext().sendBroadcast(intent) 605 } 606 } else { 607 val intentSender: IntentSender = preference.sliceAction.action!! 608 .intentSender 609 mActivityResultLauncher.launch( 610 IntentSenderRequest.Builder(intentSender).setFillInIntent( 611 fillInIntent 612 ).build() 613 ) 614 } 615 if (preference.followupSliceAction != null) { 616 mPreferenceFollowupIntent = preference.followupSliceAction.action 617 if (mPreferenceFollowupIntent == null) { 618 mPreferenceFollowupIntent = preference.followupSliceAction.actionIntent 619 } 620 } 621 622 return true 623 } 624 625 private fun back() { 626 if (isTwoPanel) { 627 val parentFragment: TwoPanelSettingsFragment? = 628 mFragment.callbackFragment as TwoPanelSettingsFragment? 629 if (parentFragment!!.isFragmentInTheMainPanel(mFragment)) { 630 parentFragment.navigateBack() 631 } 632 } else if (mFragment.callbackFragment is OnePanelSliceFragmentContainer) { 633 (mFragment.callbackFragment as OnePanelSliceFragmentContainer).navigateBack() 634 } 635 } 636 637 private fun forward() { 638 if (mIsMainPanelReady) { 639 if (isTwoPanel) { 640 val parentFragment: TwoPanelSettingsFragment? = 641 mFragment.callbackFragment as TwoPanelSettingsFragment? 642 var chosenPreference: Preference? = TwoPanelSettingsFragment.getChosenPreference( 643 mFragment 644 ) 645 if (chosenPreference == null && mLastFocusedPreferenceKey != null) { 646 chosenPreference = mFragment.findPreference( 647 mLastFocusedPreferenceKey!! 648 ) 649 } 650 if (chosenPreference != null && chosenPreference is HasSliceUri 651 && (chosenPreference as HasSliceUri).uri != null 652 ) { 653 chosenPreference.fragment = SliceFragment::class.java.canonicalName 654 parentFragment!!.refocusPreferenceForceRefresh(chosenPreference, mFragment) 655 } 656 if (parentFragment!!.isFragmentInTheMainPanel(mFragment)) { 657 parentFragment.navigateToPreviewFragment() 658 } 659 } 660 } else { 661 mHandler.post({ this.forward() }) 662 } 663 } 664 665 666 fun onSaveInstanceState(outState: Bundle) { 667 outState.putParcelable(KEY_PREFERENCE_FOLLOWUP_INTENT, mPreferenceFollowupIntent) 668 outState.putInt(KEY_PREFERENCE_FOLLOWUP_RESULT_CODE, mFollowupPendingIntentResultCode) 669 outState.putCharSequence( 670 KEY_SCREEN_TITLE, 671 screenTitle 672 ) 673 outState.putCharSequence(KEY_SCREEN_SUBTITLE, mScreenSubtitle) 674 outState.putParcelable(KEY_SCREEN_ICON, mScreenIcon) 675 outState.putString(KEY_LAST_PREFERENCE, mLastFocusedPreferenceKey) 676 outState.putString(KEY_URI_STRING, mUriString) 677 } 678 679 fun onRestoreInstanceState(savedInstanceState: Bundle) { 680 mPreferenceFollowupIntent = 681 savedInstanceState.getParcelable(KEY_PREFERENCE_FOLLOWUP_INTENT) 682 mFollowupPendingIntentResultCode = 683 savedInstanceState.getInt(KEY_PREFERENCE_FOLLOWUP_RESULT_CODE) 684 screenTitle = savedInstanceState.getCharSequence(KEY_SCREEN_TITLE) 685 mScreenSubtitle = savedInstanceState.getCharSequence(KEY_SCREEN_SUBTITLE) 686 mScreenIcon = savedInstanceState.getParcelable(KEY_SCREEN_ICON) 687 mLastFocusedPreferenceKey = savedInstanceState.getString(KEY_LAST_PREFERENCE) 688 setUri(savedInstanceState.getString(KEY_URI_STRING)) 689 } 690 691 private fun handleUri(uri: Uri) { 692 val uriString: String? = uri.getQueryParameter(SlicesConstants.PARAMETER_URI) 693 val errorMessage: String? = uri.getQueryParameter(SlicesConstants.PARAMETER_ERROR) 694 // Display the errorMessage based upon two different scenarios: 695 // a) If the provided uri string matches with current page slice uri(usually happens 696 // when the data fails to correctly load), show the errors in the current panel using 697 // InfoFragment UI. 698 // b) If the provided uri string does not match with current page slice uri(usually happens 699 // when the data fails to save), show the error message as the toast. 700 if (uriString != null && errorMessage != null) { 701 if (uriString != mUriString) { 702 showErrorMessageAsToast(errorMessage) 703 } else { 704 showErrorMessage(errorMessage) 705 } 706 } 707 // Provider should provide the correct slice uri in the parameter if it wants to do certain 708 // action(includes go back, forward), otherwise TvSettings would ignore it. 709 if (uriString == null || uriString != mUriString) { 710 return 711 } 712 val direction: String? = uri.getQueryParameter(SlicesConstants.PARAMETER_DIRECTION) 713 if (direction != null) { 714 if (direction == SlicesConstants.FORWARD) { 715 forward() 716 } else if (direction == SlicesConstants.BACKWARD) { 717 back() 718 } else if (direction == SlicesConstants.EXIT) { 719 mFragment.requireActivity().setResult(Activity.RESULT_OK) 720 mFragment.requireActivity().finish() 721 } 722 } 723 } 724 725 private fun showErrorMessage(errorMessage: String) { 726 if (isTwoPanel) { 727 (mFragment.callbackFragment as TwoPanelSettingsFragment).showErrorMessage( 728 errorMessage, mFragment 729 ) 730 } 731 } 732 733 private fun showErrorMessageAsToast(errorMessage: String) { 734 Toast.makeText(mFragment.requireActivity(), errorMessage, Toast.LENGTH_SHORT).show() 735 } 736 737 private fun getPreferenceActionId(preference: Preference): Int { 738 if (preference is HasSliceAction) { 739 return if ((preference as HasSliceAction).actionId != 0) 740 (preference as HasSliceAction).actionId 741 else 742 TvSettingsEnums.ENTRY_DEFAULT 743 } 744 return TvSettingsEnums.ENTRY_DEFAULT 745 } 746 747 private fun setUri(uriString: String?) { 748 mUriString = uriString 749 if (!TextUtils.isEmpty(uriString)) { 750 ContextSingleton.getInstance().grantFullAccess( 751 mFragment.requireContext(), 752 Uri.parse(uriString) 753 ) 754 } 755 } 756 757 val pageId: Int 758 get() { 759 return if (mCurrentPageId != 0) mCurrentPageId else TvSettingsEnums.PAGE_SLICE_DEFAULT 760 } 761 762 interface Callbacks { 763 fun showProgressBar(toShow: Boolean) 764 765 fun setTitle(title: CharSequence?) 766 767 fun setSubtitle(subtitle: CharSequence?) 768 769 fun setIcon(icon: Drawable?) 770 771 fun onSlice(slice: Slice?) 772 } 773 774 /** Callback for one panel settings fragment */ 775 interface OnePanelSliceFragmentContainer { 776 fun navigateBack() 777 } 778 779 companion object { 780 private const val TAG: String = "SliceShard" 781 782 private const val KEY_PREFERENCE_FOLLOWUP_INTENT: String = 783 "slice_key_preference_followup_intent" 784 private const val KEY_PREFERENCE_FOLLOWUP_RESULT_CODE: String = 785 "slice_key_preference_followup_result_code" 786 private const val KEY_SCREEN_TITLE: String = "slice_key_screen_title" 787 private const val KEY_SCREEN_SUBTITLE: String = "slice_key_screen_subtitle" 788 private const val KEY_SCREEN_ICON: String = "slice_key_screen_icon" 789 private const val KEY_LAST_PREFERENCE: String = "slice_key_last_preference" 790 private const val KEY_URI_STRING: String = "slice_key_uri_string" 791 792 private fun isSamePreference(oldPref: Preference?, newPref: Preference?): Boolean { 793 if (oldPref == null || newPref == null) { 794 return false 795 } 796 797 if (newPref is HasSliceUri != oldPref is HasSliceUri) { 798 return false 799 } 800 801 if (newPref is PreferenceGroup != oldPref is PreferenceGroup) { 802 return false 803 } 804 805 if (newPref is EmbeddedSlicePreference) { 806 return oldPref is EmbeddedSlicePreference 807 && newPref.uri == oldPref.uri 808 } else if (oldPref is EmbeddedSlicePreference) { 809 return false 810 } 811 812 return newPref.key != null && newPref.key == oldPref.key 813 } 814 } 815 }