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.util.DeviceUtils
42 import com.android.tv.settings.device.displaysound.PreferredDynamicRangeInfo.MatchContentDynamicRangeInfoFragment
43 import com.android.tv.settings.overlay.FlavorUtils
44 import com.android.tv.settings.util.InstrumentationUtils
45 import com.android.tv.settings.util.ResolutionSelectionUtils
46 import com.android.tv.settings.util.SliceUtils
47 import com.android.tv.settings.util.SliceUtilsKt
48 import com.android.tv.twopanelsettings.slices.SlicePreference
49 import kotlinx.coroutines.launch
50 
51 /**
52  * The "Display & sound" screen in TV Settings.
53  */
54 @Keep
55 class DisplaySoundFragment : SettingsPreferenceFragment(), DisplayManager.DisplayListener {
56     lateinit var mAudioManager: AudioManager
57     lateinit var mHdmiControlManager: HdmiControlManager
58     lateinit var mDisplayManager: DisplayManager
59     private var mCurrentDeviceName: String? = null
60     private var mCurrentMode: Display.Mode? = null
61 
onAttachnull62     override fun onAttach(context: Context) {
63         mAudioManager = context.getSystemService(AudioManager::class.java) as AudioManager
64         mHdmiControlManager =
65             context.getSystemService(HdmiControlManager::class.java) as HdmiControlManager
66         mDisplayManager = displayManager
67         super.onAttach(context)
68     }
69 
70     private val preferenceScreenResId: Int
71         get() = when (FlavorUtils.getFlavor(context)) {
72             FlavorUtils.FLAVOR_CLASSIC, FlavorUtils.FLAVOR_TWO_PANEL -> R.xml.display_sound
73             FlavorUtils.FLAVOR_X, FlavorUtils.FLAVOR_VENDOR -> R.xml.display_sound_x
74             else -> R.xml.display_sound
75         }
76 
onCreatePreferencesnull77     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
78         setPreferencesFromResource(preferenceScreenResId, null)
79     }
80 
onCreateViewnull81     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
82         savedInstanceState: Bundle?): View {
83         findPreference<TwoStatePreference>(KEY_SOUND_EFFECTS)?.isChecked = soundEffectsEnabled
84         updateCecPreference()
85 
86         mCurrentDeviceName = DeviceUtils.getDeviceName(context)
87         updateVolumeChangePreference()
88 
89         val display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY)
90         if (display.systemPreferredDisplayMode != null) {
91             mDisplayManager.registerDisplayListener(this, null)
92             mCurrentMode = mDisplayManager.globalUserPreferredDisplayMode
93             updateResolutionTitleDescription(ResolutionSelectionUtils.modeToString(
94                 mCurrentMode, context))
95         } else {
96             removePreference(findPreference(KEY_RESOLUTION_TITLE))
97         }
98         val dynamicRangePreference = findPreference<SwitchPreference>(KEY_DYNAMIC_RANGE)
99         if (mDisplayManager.supportedHdrOutputTypes.isEmpty()) {
100             removePreference(dynamicRangePreference)
101         } else if (FlavorUtils.getFlavor(context) != FlavorUtils.FLAVOR_CLASSIC) {
102             createInfoFragments()
103         }
104         viewLifecycleOwner.lifecycleScope.launch {
105             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
106                 updateDefaultAudioOutputSettings()
107             }
108         }
109         return checkNotNull(super.onCreateView(inflater, container, savedInstanceState))
110     }
111 
onDestroynull112     override fun onDestroy() {
113         super.onDestroy()
114         if (this::mDisplayManager.isInitialized) {
115             mDisplayManager.unregisterDisplayListener(this)
116         }
117     }
118 
onResumenull119     override fun onResume() {
120         super.onResume()
121         // Update the subtitle of CEC setting when navigating back to this page.
122         updateCecPreference()
123         findPreference<SwitchPreference>(KEY_DYNAMIC_RANGE)?.isChecked =
124             DisplaySoundUtils.getMatchContentDynamicRangeStatus(mDisplayManager)
125     }
126 
onPreferenceTreeClicknull127     override fun onPreferenceTreeClick(preference: Preference): Boolean {
128         if (TextUtils.equals(preference.key, KEY_SOUND_EFFECTS)) {
129             val soundPref = preference as TwoStatePreference
130             InstrumentationUtils
131                 .logToggleInteracted(
132                     TvSettingsEnums.DISPLAY_SOUND_SYSTEM_SOUNDS, soundPref.isChecked)
133             soundEffectsEnabled = soundPref.isChecked
134         } else if (TextUtils.equals(preference.key, KEY_DYNAMIC_RANGE)) {
135             val dynamicPref = preference as SwitchPreference
136             DisplaySoundUtils
137                 .setMatchContentDynamicRangeStatus(context, mDisplayManager, dynamicPref.isChecked)
138         }
139         return super.onPreferenceTreeClick(preference)
140     }
141 
142     private var soundEffectsEnabled: Boolean
143         get() = getSoundEffectsEnabled(requireActivity().contentResolver)
144         private set(enabled) {
145             if (enabled) {
146                 mAudioManager.loadSoundEffects()
147             } else {
148                 mAudioManager.unloadSoundEffects()
149             }
150             Settings.System.putInt(requireActivity().contentResolver,
151                 Settings.System.SOUND_EFFECTS_ENABLED, if (enabled) 1 else 0)
152         }
153 
updateCecPreferencenull154     private fun updateCecPreference() {
155         findPreference<Preference>(KEY_CEC)?.apply{
156             if (this is SlicePreference && SliceUtils.isSliceProviderValid(
157                     context, this.uri)) {
158                 val cecEnabled = (mHdmiControlManager.getHdmiCecEnabled()
159                         == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED)
160                 setSummary(if (cecEnabled) R.string.enabled else R.string.disabled)
161                 isVisible = true
162             } else {
163                 isVisible = false
164             }
165         }
166     }
167 
updateDefaultAudioOutputSettingsnull168     private suspend fun updateDefaultAudioOutputSettings() {
169         findPreference<SlicePreference>(KEY_DEFAULT_AUDIO_OUTPUT_SETTINGS_SLICE)?.apply {
170             isVisible = SliceUtils.isSliceProviderValid(context,
171                 this.uri)
172                     && SliceUtilsKt.isSettingsSliceEnabled(context,
173                 this.uri, null)
174         }
175     }
176 
updateVolumeChangePreferencenull177     private fun updateVolumeChangePreference() {
178         findPreference<Preference>(VOLUME_CHANGE)?.apply {
179             setTitle(requireContext().resources.getString(R.string.volume_change_settings_title,
180                 mCurrentDeviceName))
181             isVisible = requireContext().resources.getBoolean(R.bool.config_volume_change)
182         }
183     }
184 
getPageIdnull185     override fun getPageId(): Int {
186         return TvSettingsEnums.DISPLAY_SOUND
187     }
188 
onDisplayAddednull189     override fun onDisplayAdded(displayId: Int) {}
onDisplayRemovednull190     override fun onDisplayRemoved(displayId: Int) {}
onDisplayChangednull191     override fun onDisplayChanged(displayId: Int) {
192         val newMode = mDisplayManager.globalUserPreferredDisplayMode
193         if (mCurrentMode != newMode) {
194             updateResolutionTitleDescription(
195                 ResolutionSelectionUtils.modeToString(newMode, context))
196             mCurrentMode = newMode
197         }
198     }
199 
200     @get:VisibleForTesting
201     val displayManager: DisplayManager
202         get() = requireContext().getSystemService(DisplayManager::class.java) as DisplayManager
203 
updateResolutionTitleDescriptionnull204     private fun updateResolutionTitleDescription(summary: String) {
205         findPreference<Preference>(KEY_RESOLUTION_TITLE)?.summary = summary
206     }
207 
removePreferencenull208     private fun removePreference(preference: Preference?) {
209         if (preference != null) {
210             preferenceScreen.removePreference(preference)
211         }
212     }
213 
createInfoFragmentsnull214     private fun createInfoFragments() {
215         findPreference<Preference>(KEY_DYNAMIC_RANGE)?.fragment =
216             MatchContentDynamicRangeInfoFragment::class.java.name
217     }
218 
219     companion object {
220         const val KEY_SOUND_EFFECTS = "sound_effects"
221         private const val KEY_CEC = "cec"
222         private const val KEY_DEFAULT_AUDIO_OUTPUT_SETTINGS_SLICE = "default_audio_output_settings"
223         private const val KEY_RESOLUTION_TITLE = "resolution_selection"
224         private const val KEY_DYNAMIC_RANGE = "match_content_dynamic_range"
225 
226         private const val VOLUME_CHANGE = "volume_change"
227 
newInstancenull228         fun newInstance(): DisplaySoundFragment {
229             return DisplaySoundFragment()
230         }
231 
getSoundEffectsEnablednull232         fun getSoundEffectsEnabled(contentResolver: ContentResolver?): Boolean {
233             return (Settings.System.getInt(contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 1)
234                     != 0)
235         }
236     }
237 }