• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }