1 /*
2  * Copyright (C) 2015 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.settings.device.displaysound
17 
18 import android.app.tvsettings.TvSettingsEnums
19 import android.content.ContentResolver
20 import android.content.Context
21 import android.hardware.display.DisplayManager
22 import android.hardware.hdmi.HdmiControlManager
23 import android.media.AudioManager
24 import android.os.Bundle
25 import android.provider.Settings
26 import android.text.TextUtils
27 import android.view.Display
28 import android.view.LayoutInflater
29 import android.view.View
30 import android.view.ViewGroup
31 import androidx.annotation.Keep
32 import androidx.annotation.VisibleForTesting
33 import androidx.lifecycle.Lifecycle
34 import androidx.lifecycle.lifecycleScope
35 import androidx.lifecycle.repeatOnLifecycle
36 import androidx.preference.Preference
37 import androidx.preference.SwitchPreference
38 import androidx.preference.TwoStatePreference
39 import com.android.tv.settings.R
40 import com.android.tv.settings.SettingsPreferenceFragment
41 import com.android.tv.settings.device.displaysound.PreferredDynamicRangeInfo.MatchContentDynamicRangeInfoFragment
42 import com.android.tv.settings.overlay.FlavorUtils
43 import com.android.tv.settings.util.InstrumentationUtils
44 import com.android.tv.settings.util.ResolutionSelectionUtils
45 import com.android.tv.settings.util.SliceUtils
46 import com.android.tv.settings.util.SliceUtilsKt
47 import com.android.tv.twopanelsettings.slices.SlicePreference
48 import kotlinx.coroutines.launch
49 
50 /**
51  * The "Display & sound" screen in TV Settings.
52  */
53 @Keep
54 class DisplaySoundFragment : SettingsPreferenceFragment(), DisplayManager.DisplayListener {
55     lateinit var mAudioManager: AudioManager
56     lateinit var mHdmiControlManager: HdmiControlManager
57     lateinit var mDisplayManager: DisplayManager
58     private var mCurrentMode: Display.Mode? = null
59 
onAttachnull60     override fun onAttach(context: Context) {
61         mAudioManager = context.getSystemService(AudioManager::class.java) as AudioManager
62         mHdmiControlManager =
63                 context.getSystemService(HdmiControlManager::class.java) as HdmiControlManager
64         super.onAttach(context)
65     }
66 
67     private val preferenceScreenResId: Int
68         get() = when (FlavorUtils.getFlavor(context)) {
69             FlavorUtils.FLAVOR_CLASSIC, FlavorUtils.FLAVOR_TWO_PANEL -> R.xml.display_sound
70             FlavorUtils.FLAVOR_X, FlavorUtils.FLAVOR_VENDOR -> R.xml.display_sound_x
71             else -> R.xml.display_sound
72         }
73 
onCreatePreferencesnull74     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
75         setPreferencesFromResource(preferenceScreenResId, null)
76     }
77 
onCreateViewnull78     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
79                               savedInstanceState: Bundle?): View {
80         findPreference<TwoStatePreference>(KEY_SOUND_EFFECTS)?.isChecked = soundEffectsEnabled
81         updateCecPreference()
82         mDisplayManager = displayManager
83         val display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY)
84         if (display.systemPreferredDisplayMode != null) {
85             mDisplayManager.registerDisplayListener(this, null)
86             mCurrentMode = mDisplayManager.globalUserPreferredDisplayMode
87             updateResolutionTitleDescription(ResolutionSelectionUtils.modeToString(
88                     mCurrentMode, context))
89         } else {
90             removePreference(findPreference(KEY_RESOLUTION_TITLE))
91         }
92         val dynamicRangePreference = findPreference<SwitchPreference>(KEY_DYNAMIC_RANGE)
93         if (mDisplayManager.supportedHdrOutputTypes.isEmpty()) {
94             removePreference(dynamicRangePreference)
95         } else if (FlavorUtils.getFlavor(context) != FlavorUtils.FLAVOR_CLASSIC) {
96             createInfoFragments()
97         }
98         viewLifecycleOwner.lifecycleScope.launch {
99             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
100                 updateDefaultAudioOutputSettings()
101             }
102         }
103         return checkNotNull(super.onCreateView(inflater, container, savedInstanceState))
104     }
105 
onDestroynull106     override fun onDestroy() {
107         super.onDestroy()
108         mDisplayManager.unregisterDisplayListener(this)
109     }
110 
onResumenull111     override fun onResume() {
112         super.onResume()
113         // Update the subtitle of CEC setting when navigating back to this page.
114         updateCecPreference()
115         findPreference<SwitchPreference>(KEY_DYNAMIC_RANGE)?.isChecked =
116                 DisplaySoundUtils.getMatchContentDynamicRangeStatus(mDisplayManager)
117     }
118 
onPreferenceTreeClicknull119     override fun onPreferenceTreeClick(preference: Preference): Boolean {
120         if (TextUtils.equals(preference.key, KEY_SOUND_EFFECTS)) {
121             val soundPref = preference as TwoStatePreference
122             InstrumentationUtils
123                     .logToggleInteracted(
124                             TvSettingsEnums.DISPLAY_SOUND_SYSTEM_SOUNDS, soundPref.isChecked)
125             soundEffectsEnabled = soundPref.isChecked
126         } else if (TextUtils.equals(preference.key, KEY_DYNAMIC_RANGE)) {
127             val dynamicPref = preference as SwitchPreference
128             DisplaySoundUtils
129                     .setMatchContentDynamicRangeStatus(context, mDisplayManager, dynamicPref.isChecked)
130         }
131         return super.onPreferenceTreeClick(preference)
132     }
133 
134     private var soundEffectsEnabled: Boolean
135         get() = getSoundEffectsEnabled(requireActivity().contentResolver)
136         private set(enabled) {
137             if (enabled) {
138                 mAudioManager.loadSoundEffects()
139             } else {
140                 mAudioManager.unloadSoundEffects()
141             }
142             Settings.System.putInt(requireActivity().contentResolver,
143                     Settings.System.SOUND_EFFECTS_ENABLED, if (enabled) 1 else 0)
144         }
145 
updateCecPreferencenull146     private fun updateCecPreference() {
147         findPreference<Preference>(KEY_CEC)?.apply{
148             if (this is SlicePreference && SliceUtils.isSliceProviderValid(
149                             context, this.uri)) {
150                 val cecEnabled = (mHdmiControlManager.getHdmiCecEnabled()
151                         == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED)
152                 setSummary(if (cecEnabled) R.string.enabled else R.string.disabled)
153                 isVisible = true
154             } else {
155                 isVisible = false
156             }
157         }
158     }
159 
updateDefaultAudioOutputSettingsnull160     private suspend fun updateDefaultAudioOutputSettings() {
161         findPreference<SlicePreference>(KEY_DEFAULT_AUDIO_OUTPUT_SETTINGS_SLICE)?.apply {
162             isVisible = SliceUtils.isSliceProviderValid(context,
163                     this.uri)
164                     && SliceUtilsKt.isSettingsSliceEnabled(context,
165                     this.uri, null)
166         }
167     }
168 
getPageIdnull169     override fun getPageId(): Int {
170         return TvSettingsEnums.DISPLAY_SOUND
171     }
172 
onDisplayAddednull173     override fun onDisplayAdded(displayId: Int) {}
onDisplayRemovednull174     override fun onDisplayRemoved(displayId: Int) {}
onDisplayChangednull175     override fun onDisplayChanged(displayId: Int) {
176         val newMode = mDisplayManager.globalUserPreferredDisplayMode
177         if (mCurrentMode != newMode) {
178             updateResolutionTitleDescription(
179                     ResolutionSelectionUtils.modeToString(newMode, context))
180             mCurrentMode = newMode
181         }
182     }
183 
184     @get:VisibleForTesting
185     val displayManager: DisplayManager
186         get() = requireContext().getSystemService(DisplayManager::class.java) as DisplayManager
187 
updateResolutionTitleDescriptionnull188     private fun updateResolutionTitleDescription(summary: String) {
189         findPreference<Preference>(KEY_RESOLUTION_TITLE)?.summary = summary
190     }
191 
removePreferencenull192     private fun removePreference(preference: Preference?) {
193         if (preference != null) {
194             preferenceScreen.removePreference(preference)
195         }
196     }
197 
createInfoFragmentsnull198     private fun createInfoFragments() {
199         findPreference<Preference>(KEY_DYNAMIC_RANGE)?.fragment =
200                 MatchContentDynamicRangeInfoFragment::class.java.name
201     }
202 
203     companion object {
204         const val KEY_SOUND_EFFECTS = "sound_effects"
205         private const val KEY_CEC = "cec"
206         private const val KEY_DEFAULT_AUDIO_OUTPUT_SETTINGS_SLICE = "default_audio_output_settings"
207         private const val KEY_RESOLUTION_TITLE = "resolution_selection"
208         private const val KEY_DYNAMIC_RANGE = "match_content_dynamic_range"
newInstancenull209         fun newInstance(): DisplaySoundFragment {
210             return DisplaySoundFragment()
211         }
212 
getSoundEffectsEnablednull213         fun getSoundEffectsEnabled(contentResolver: ContentResolver?): Boolean {
214             return (Settings.System.getInt(contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 1)
215                     != 0)
216         }
217     }
218 }
219