1 /*
2  * Copyright (C) 2018 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
17 
18 import android.app.tvsettings.TvSettingsEnums
19 import android.content.Context
20 import android.content.Intent
21 import android.content.pm.PackageManager
22 import android.content.res.Resources
23 import android.media.AudioManager
24 import android.media.tv.TvInputManager
25 import android.os.Bundle
26 import android.os.UserHandle
27 import android.provider.Settings
28 import android.text.TextUtils
29 import android.util.Log
30 import android.view.LayoutInflater
31 import android.view.View
32 import android.view.ViewGroup
33 import androidx.annotation.Keep
34 import androidx.annotation.VisibleForTesting
35 import androidx.leanback.preference.LeanbackSettingsFragmentCompat
36 import androidx.lifecycle.Lifecycle
37 import androidx.lifecycle.lifecycleScope
38 import androidx.lifecycle.repeatOnLifecycle
39 import androidx.preference.Preference
40 import androidx.preference.TwoStatePreference
41 import com.android.settingslib.development.DevelopmentSettingsEnabler
42 import com.android.tv.settings.LongClickPreference
43 import com.android.tv.settings.MainFragment
44 import com.android.tv.settings.R
45 import com.android.tv.settings.SettingsPreferenceFragment
46 import com.android.tv.settings.about.RebootConfirmFragment
47 import com.android.tv.settings.autofill.AutofillHelper
48 import com.android.tv.settings.customization.CustomizationConstants
49 import com.android.tv.settings.customization.Partner
50 import com.android.tv.settings.customization.PartnerPreferencesMerger
51 import com.android.tv.settings.device.eco.PowerAndEnergyFragment
52 import com.android.tv.settings.inputmethod.InputMethodHelper
53 import com.android.tv.settings.overlay.FlavorUtils
54 import com.android.tv.settings.privacy.PrivacyToggle
55 import com.android.tv.settings.privacy.SensorFragment
56 import com.android.tv.settings.system.SecurityFragment
57 import com.android.tv.settings.util.InstrumentationUtils
58 import com.android.tv.settings.util.SliceUtils
59 import com.android.tv.settings.util.SliceUtilsKt
60 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment
61 import com.android.tv.twopanelsettings.slices.SlicePreference
62 import kotlinx.coroutines.launch
63 
64 /**
65  * The "Device Preferences" screen in TV settings.
66  */
67 @Keep
68 class DevicePrefFragment : SettingsPreferenceFragment(), LongClickPreference.OnLongClickListener {
69     private var mSoundsSwitchPref: TwoStatePreference? = null
70     private var mInputSettingNeeded = false
71     private var mAudioManager: AudioManager? = null
72     private val preferenceScreenResId: Int
73         get() = if (isRestricted) {
74             R.xml.device_restricted
75         } else when (FlavorUtils.getFlavor(context)) {
76             FlavorUtils.FLAVOR_CLASSIC -> R.xml.device
77             FlavorUtils.FLAVOR_TWO_PANEL -> R.xml.device_two_panel
78             FlavorUtils.FLAVOR_X -> R.xml.device_x
79             FlavorUtils.FLAVOR_VENDOR -> R.xml.device_vendor
80             else -> R.xml.device
81         }
82 
onCreatenull83     override fun onCreate(savedInstanceState: Bundle?) {
84         val manager = requireContext().getSystemService(
85                 Context.TV_INPUT_SERVICE) as TvInputManager
86         for (input in manager.tvInputList) {
87             if (input.isPassthroughInput) {
88                 mInputSettingNeeded = true
89             }
90         }
91         mAudioManager = requireContext().getSystemService(AudioManager::class.java) as AudioManager
92         super.onCreate(savedInstanceState)
93     }
94 
onCreatePreferencesnull95     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
96         setPreferencesFromResource(preferenceScreenResId, null)
97         if (Partner.getInstance(context).isCustomizationPackageProvided) {
98             PartnerPreferencesMerger.mergePreferences(
99                     context,
100                     preferenceScreen,
101                     CustomizationConstants.DEVICE_SCREEN
102             )
103         }
104         mSoundsSwitchPref = findPreference(KEY_SOUNDS_SWITCH)
105     }
onCreateViewnull106     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
107                               savedInstanceState: Bundle?): View {
108         mSoundsSwitchPref?.isChecked = soundEffectsEnabled
109         findPreference<Preference>(KEY_INPUTS)?.isVisible = mInputSettingNeeded
110         findPreference<LongClickPreference>(KEY_REBOOT)?.setLongClickListener(this)
111         PrivacyToggle.MIC_TOGGLE.preparePreferenceWithSensorFragment(context,
112                 findPreference(KEY_MIC), SensorFragment.TOGGLE_EXTRA)
113         PrivacyToggle.CAMERA_TOGGLE.preparePreferenceWithSensorFragment(context,
114                 findPreference(KEY_CAMERA), SensorFragment.TOGGLE_EXTRA)
115         updateDeveloperOptions()
116         updateSounds()
117         updateGoogleSettings()
118         updateCastSettings()
119         updateFastpairSettings()
120         updateKeyboardAutofillSettings()
121         updateAmbientSettings()
122         updatePowerAndEnergySettings()
123         updateSystemTvSettings()
124         hideIfIntentUnhandled(findPreference(KEY_HOME_SETTINGS))
125         hideIfIntentUnhandled(findPreference(KEY_CAST_SETTINGS))
126         hideIfIntentUnhandled(findPreference(KEY_USAGE))
127         viewLifecycleOwner.lifecycleScope.launch {
128             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
129                 updateInternalSettings()
130                 updateAssistantBroadcastSlice()
131             }
132         }
133         return checkNotNull(super.onCreateView(inflater, container, savedInstanceState))
134     }
135 
onPreferenceTreeClicknull136     override fun onPreferenceTreeClick(preference: Preference): Boolean {
137         when (preference.key) {
138             KEY_HOME_SETTINGS -> InstrumentationUtils.logEntrySelected(TvSettingsEnums.PREFERENCES_HOME_SCREEN)
139             KEY_GOOGLE_SETTINGS -> InstrumentationUtils.logEntrySelected(TvSettingsEnums.PREFERENCES_ASSISTANT)
140             KEY_CAST_SETTINGS -> InstrumentationUtils.logEntrySelected(TvSettingsEnums.PREFERENCES_CHROMECAST_SHELL)
141             KEY_REBOOT -> InstrumentationUtils.logEntrySelected(TvSettingsEnums.SYSTEM_REBOOT)
142             KEY_SOUNDS_SWITCH ->
143                 mSoundsSwitchPref?.let {
144                 InstrumentationUtils.logToggleInteracted(TvSettingsEnums.DISPLAY_SOUND_SYSTEM_SOUNDS,
145                         it.isChecked)
146                 soundEffectsEnabled = it.isChecked
147             }
148         }
149         return super.onPreferenceTreeClick(preference)
150     }
151 
onPreferenceLongClicknull152     override fun onPreferenceLongClick(preference: Preference): Boolean {
153         if (TextUtils.equals(preference.key, KEY_REBOOT)) {
154             InstrumentationUtils.logEntrySelected(TvSettingsEnums.SYSTEM_REBOOT)
155             val fragment = callbackFragment
156             if (fragment is LeanbackSettingsFragmentCompat) {
157                 fragment.startImmersiveFragment(
158                         RebootConfirmFragment.newInstance(true /* safeMode */))
159                 return true
160             } else if (fragment is TwoPanelSettingsFragment) {
161                 fragment.startImmersiveFragment(
162                         RebootConfirmFragment.newInstance(true /* safeMode */))
163                 return true
164             }
165         }
166         return false
167     }
168 
169     private var soundEffectsEnabled: Boolean
170         get() = Settings.System.getInt(requireActivity().contentResolver,
171                 Settings.System.SOUND_EFFECTS_ENABLED, 1) != 0
172         private set(enabled) {
173             if (enabled) {
174                 mAudioManager?.loadSoundEffects()
175             } else {
176                 mAudioManager?.unloadSoundEffects()
177             }
178             Settings.System.putInt(requireActivity().contentResolver,
179                     Settings.System.SOUND_EFFECTS_ENABLED, if (enabled) 1 else 0)
180         }
181 
hideIfIntentUnhandlednull182     private fun hideIfIntentUnhandled(preference: Preference?) {
183         if (preference == null || !preference.isVisible) {
184             return
185         }
186         preference.isVisible = MainFragment.systemIntentIsHandled(context, preference.intent) != null
187     }
188 
189     private val isRestricted: Boolean
190         get() = SecurityFragment.isRestrictedProfileInEffect(context)
191 
192     @VisibleForTesting
updateDeveloperOptionsnull193     fun updateDeveloperOptions() {
194         findPreference<Preference>(KEY_DEVELOPER)?.isVisible =
195                 DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context)
196     }
197 
updateSoundsnull198     private fun updateSounds() {
199         findPreference<Preference>(KEY_SOUNDS)?.isVisible =
200                 MainFragment
201                         .systemIntentIsHandled(context, Intent(MainFragment.ACTION_SOUND)) == null
202 
203     }
204 
updateGoogleSettingsnull205     private fun updateGoogleSettings() {
206         findPreference<Preference>(KEY_GOOGLE_SETTINGS)?.apply {
207             val info = MainFragment.systemIntentIsHandled(context,
208                     this.intent)
209             isVisible = info != null
210 
211             info?.let {
212                 icon = it.activityInfo.loadIcon(requireContext().packageManager)
213                 title = it.activityInfo.loadLabel(requireContext().packageManager)
214             }
215         }
216     }
217 
218     @VisibleForTesting
updateCastSettingsnull219     fun updateCastSettings() {
220         findPreference<Preference>(KEY_CAST_SETTINGS)?.apply {
221             val info = MainFragment.systemIntentIsHandled(
222                     requireContext(), this.intent)
223             if (info != null) {
224                 try {
225                     val targetContext = requireContext()
226                             .createPackageContext(if (info.resolvePackageName != null) info.resolvePackageName else info.activityInfo.packageName, 0)
227                     this.icon = targetContext.getDrawable(info.getIconResource())
228                 } catch (e: Resources.NotFoundException) {
229                     Log.e(TAG, "Cast settings icon not found", e)
230                 } catch (e: PackageManager.NameNotFoundException) {
231                     Log.e(TAG, "Cast settings icon not found", e)
232                 } catch (e: SecurityException) {
233                     Log.e(TAG, "Cast settings icon not found", e)
234                 }
235                 title = info.activityInfo.loadLabel(requireContext().packageManager)
236             }
237         }
238 
239         findPreference<SlicePreference>(KEY_CAST_SETTINGS_SLICE)?.apply {
240             isVisible = SliceUtils.isSliceProviderValid(requireContext(), this.uri)
241                     && !FlavorUtils.getFeatureFactory(requireContext()).getBasicModeFeatureProvider()
242                     .isBasicMode(requireContext())
243         }
244     }
245 
updateInternalSettingsnull246     private suspend fun updateInternalSettings() {
247         findPreference<SlicePreference>(KEY_OVERLAY_INTERNAL_SETTINGS_SLICE)?.apply {
248             isVisible = SliceUtils.isSliceProviderValid(context, this.uri)
249                     && SliceUtilsKt.isSettingsSliceEnabled(requireContext(), this.uri, null)
250         }
251     }
252 
updateAssistantBroadcastSlicenull253     private suspend fun updateAssistantBroadcastSlice() {
254         findPreference<Preference>(KEY_ASSISTANT_BROADCAST)?.apply {
255             isVisible = SliceUtilsKt.isSettingsSliceEnabled(
256                     requireContext(),
257                     (this as SlicePreference).uri,
258                     RES_TOP_LEVEL_ASSISTANT_SLICE_URI)
259         }
260     }
261 
262     @VisibleForTesting
updateFastpairSettingsnull263     fun updateFastpairSettings() {
264         findPreference<SlicePreference>(KEY_FASTPAIR_SETTINGS_SLICE)?.apply {
265             isVisible = SliceUtils.isSliceProviderValid(context, this.uri)
266         }
267     }
268 
269     @VisibleForTesting
updateKeyboardAutofillSettingsnull270     fun updateKeyboardAutofillSettings() {
271         val keyboardPref = findPreference<Preference>(KEY_KEYBOARD)
272         val candidates = AutofillHelper.getAutofillCandidates(requireContext(),
273                 requireContext().packageManager, UserHandle.myUserId())
274 
275         // Switch title depends on whether there is autofill
276         if (candidates.isEmpty()) {
277             keyboardPref?.setTitle(R.string.system_keyboard)
278         } else {
279             keyboardPref?.setTitle(R.string.system_keyboard_autofill)
280         }
281         var summary: CharSequence = ""
282         // append current keyboard to summary
283         val defaultImId = InputMethodHelper.getDefaultInputMethodId(context)
284         if (!TextUtils.isEmpty(defaultImId)) {
285             val info = InputMethodHelper.findInputMethod(defaultImId,
286                     InputMethodHelper.getEnabledSystemInputMethodList(context))
287             if (info != null) {
288                 summary = info.loadLabel(requireContext().packageManager)
289             }
290         }
291         // append current autofill to summary
292         val appInfo = AutofillHelper.getCurrentAutofill(requireContext(), candidates)
293         if (appInfo != null) {
294             val autofillInfo = appInfo.loadLabel()
295             if (summary.length > 0) {
296                 requireContext().getString(R.string.string_concat, summary, autofillInfo)
297             } else {
298                 summary = autofillInfo
299             }
300         }
301         keyboardPref?.summary = summary
302     }
303 
updateAmbientSettingsnull304     private fun updateAmbientSettings() {
305         findPreference<SlicePreference>(KEY_AMBIENT_SETTINGS)?.apply {
306             isVisible = SliceUtils.isSliceProviderValid(context, this.uri)
307         }
308     }
309 
updatePowerAndEnergySettingsnull310     private fun updatePowerAndEnergySettings() {
311         val energySaverPref = findPreference<Preference>(KEY_ENERGY_SAVER)
312         val powerAndEnergyPref = findPreference<Preference>(KEY_POWER_AND_ENERGY)
313         if (energySaverPref == null || powerAndEnergyPref == null) {
314             return
315         }
316         val showPowerAndEnergy = !PowerAndEnergyFragment.hasOnlyEnergySaverPreference(context)
317         powerAndEnergyPref.isVisible = showPowerAndEnergy
318         energySaverPref.isVisible = !showPowerAndEnergy
319     }
320 
updateSystemTvSettingsnull321     private fun updateSystemTvSettings() {
322         findPreference<SlicePreference>(KEY_SYSTEM_TV_SLICE)?.apply {
323             isVisible = SliceUtils.isSliceProviderValid(context, this.uri)
324         }
325     }
326 
getPageIdnull327     override fun getPageId(): Int {
328         return TvSettingsEnums.SYSTEM
329     }
330 
331     companion object {
332         @JvmField
333         @VisibleForTesting
334         val KEY_DEVELOPER = "developer"
335 
336         @JvmField
337         @VisibleForTesting
338         val KEY_CAST_SETTINGS = "cast"
339         private const val KEY_CAST_SETTINGS_SLICE = "cast_settings"
340 
341         @JvmField
342         @VisibleForTesting
343         val KEY_KEYBOARD = "keyboard"
344         private const val TAG = "DeviceFragment"
345         private const val KEY_USAGE = "usageAndDiag"
346         private const val KEY_INPUTS = "inputs"
347         private const val KEY_SOUNDS = "sound_effects"
348         private const val KEY_SOUNDS_SWITCH = "sound_effects_switch"
349         private const val KEY_GOOGLE_SETTINGS = "google_settings"
350         private const val KEY_HOME_SETTINGS = "home"
351         private const val KEY_REBOOT = "reboot"
352         private const val KEY_MIC = "microphone"
353         private const val KEY_CAMERA = "camera"
354         private const val KEY_FASTPAIR_SETTINGS_SLICE = "fastpair_slice"
355         private const val KEY_OVERLAY_INTERNAL_SETTINGS_SLICE = "overlay_internal"
356         private const val KEY_ASSISTANT_BROADCAST = "assistant_broadcast"
357         private const val KEY_AMBIENT_SETTINGS = "ambient_settings"
358         private const val KEY_ENERGY_SAVER = "energysaver"
359         private const val KEY_POWER_AND_ENERGY = "power_and_energy"
360         private const val RES_TOP_LEVEL_ASSISTANT_SLICE_URI = "top_level_assistant_slice_uri"
361         private const val KEY_SYSTEM_TV_SLICE = "menu_system_tv"
362     }
363 }
364