1 /*
2  * Copyright (C) 2022 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 
17 package com.android.permissioncontroller.safetycenter.ui.model
18 
19 import android.annotation.SuppressLint
20 import android.app.Application
21 import android.content.ClipboardManager
22 import android.content.Intent
23 import android.hardware.SensorPrivacyManager
24 import android.hardware.SensorPrivacyManager.Sensors
25 import android.hardware.SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE
26 import android.os.Build
27 import android.os.Process
28 import android.os.UserManager
29 import android.provider.DeviceConfig
30 import android.provider.Settings
31 import androidx.annotation.RequiresApi
32 import androidx.annotation.StringRes
33 import androidx.fragment.app.Fragment
34 import androidx.lifecycle.AndroidViewModel
35 import androidx.lifecycle.ViewModel
36 import androidx.lifecycle.ViewModelProvider
37 import com.android.modules.utils.build.SdkLevel
38 import com.android.permission.flags.Flags
39 import com.android.permissioncontroller.R
40 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
41 import com.android.settingslib.RestrictedLockUtils
42 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
43 
44 /** Viewmodel for the privacy controls page. */
45 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
46 // Suppress warnings related to the camera/mic privacy and clipboard privacy APIs. The PC has the
47 // permissions.
48 @SuppressLint("MissingPermission")
49 class PrivacyControlsViewModel(private val app: Application) : AndroidViewModel(app) {
50 
51     private val sensorPrivacyManager: SensorPrivacyManager =
52         app.getSystemService(SensorPrivacyManager::class.java)!!
53     private val clipboardManager: ClipboardManager =
54         app.getSystemService(ClipboardManager::class.java)!!
55     private val userManager: UserManager = app.getSystemService(UserManager::class.java)!!
56 
57     private val CONFIG_CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS =
58         app.getString(R.string.clipboard_show_access_notifications_config)
59     private val CONFIG_SHOW_ACCESS_NOTIFICATIONS_DEFAULT =
60         app.getString(R.string.show_access_notifications_default_config)
61     private val CONFIG_MIC_TOGGLE_ENABLED = app.getString(R.string.mic_toggle_enable_config)
62     private val CONFIG_CAMERA_TOGGLE_ENABLED = app.getString(R.string.camera_toggle_enable_config)
63 
64     enum class Pref(val key: String, @StringRes val titleResId: Int) {
65         MIC("privacy_mic_toggle", R.string.mic_toggle_title),
66         CAMERA("privacy_camera_toggle", R.string.camera_toggle_title),
67         LOCATION("privacy_location_access", R.string.location_settings),
68         CLIPBOARD("show_clip_access_notification", R.string.show_clip_access_notification_title),
69         SHOW_PASSWORD("show_password", R.string.show_password_title);
70 
71         companion object {
<lambda>null72             @JvmStatic fun findByKey(inputKey: String) = values().find { it.key == inputKey }
73         }
74     }
75 
76     data class PrefState(val visible: Boolean, val checked: Boolean, val admin: EnforcedAdmin?)
77 
78     val controlStateLiveData: SmartUpdateMediatorLiveData<Map<Pref, PrefState>> =
79         object :
80             SmartUpdateMediatorLiveData<@JvmSuppressWildcards Map<Pref, PrefState>>(),
81             SensorPrivacyManager.OnSensorPrivacyChangedListener {
82 
onUpdatenull83             override fun onUpdate() {
84                 val shownPrefs = mutableMapOf<Pref, PrefState>()
85                 shownPrefs[Pref.CAMERA] =
86                     getSensorToggleState(
87                         Sensors.CAMERA,
88                         UserManager.DISALLOW_CAMERA_TOGGLE,
89                         CONFIG_CAMERA_TOGGLE_ENABLED
90                     )
91                 shownPrefs[Pref.MIC] =
92                     getSensorToggleState(
93                         Sensors.MICROPHONE,
94                         UserManager.DISALLOW_MICROPHONE_TOGGLE,
95                         CONFIG_MIC_TOGGLE_ENABLED
96                     )
97                 shownPrefs[Pref.CLIPBOARD] =
98                     PrefState(visible = true, checked = isClipboardEnabled(), admin = null)
99                 shownPrefs[Pref.SHOW_PASSWORD] =
100                     PrefState(
101                         visible = shouldDisplayShowPasswordToggle(),
102                         checked = isShowPasswordEnabled(),
103                         admin = null
104                     )
105                 value = shownPrefs
106             }
107 
onActivenull108             override fun onActive() {
109                 sensorPrivacyManager.addSensorPrivacyListener(this)
110                 super.onActive()
111                 update()
112             }
113 
onInactivenull114             override fun onInactive() {
115                 super.onInactive()
116                 sensorPrivacyManager.removeSensorPrivacyListener(this)
117             }
118 
119             @Suppress("OVERRIDE_DEPRECATION")
onSensorPrivacyChangednull120             override fun onSensorPrivacyChanged(sensor: Int, enabled: Boolean) {
121                 update()
122             }
123         }
124 
handlePrefClicknull125     fun handlePrefClick(fragment: Fragment, pref: Pref, admin: EnforcedAdmin?) {
126         when (pref) {
127             Pref.MIC -> toggleSensorOrShowAdmin(fragment, Sensors.MICROPHONE, admin)
128             Pref.CAMERA -> toggleSensorOrShowAdmin(fragment, Sensors.CAMERA, admin)
129             Pref.LOCATION -> goToLocation(fragment)
130             Pref.CLIPBOARD -> toggleClipboard()
131             Pref.SHOW_PASSWORD -> toggleShowPassword()
132         }
133     }
134 
toggleSensorOrShowAdminnull135     private fun toggleSensorOrShowAdmin(fragment: Fragment, sensor: Int, admin: EnforcedAdmin?) {
136         if (admin != null) {
137             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(fragment.context, admin)
138             return
139         }
140         val blocked = sensorPrivacyManager.isSensorPrivacyEnabled(TOGGLE_TYPE_SOFTWARE, sensor)
141         sensorPrivacyManager.setSensorPrivacy(sensor, !blocked)
142     }
143 
goToLocationnull144     private fun goToLocation(fragment: Fragment) {
145         fragment.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
146     }
147 
getSensorToggleStatenull148     private fun getSensorToggleState(
149         sensor: Int,
150         restriction: String,
151         enableConfig: String
152     ): PrefState {
153         val admin = RestrictedLockUtils.getProfileOrDeviceOwner(app, Process.myUserHandle())
154         val sensorConfigEnabled =
155             DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, enableConfig, true)
156         return PrefState(
157             visible = sensorConfigEnabled && sensorPrivacyManager.supportsSensorToggle(sensor),
158             checked = !sensorPrivacyManager.isSensorPrivacyEnabled(TOGGLE_TYPE_SOFTWARE, sensor),
159             admin =
160                 if (
161                     userManager
162                         .getUserRestrictionSources(restriction, Process.myUserHandle())
163                         .isNotEmpty()
164                 ) {
165                     admin
166                 } else {
167                     null
168                 }
169         )
170     }
171 
isClipboardEnablednull172     private fun isClipboardEnabled(): Boolean {
173         if (SdkLevel.isAtLeastV()) {
174             return clipboardManager.areClipboardAccessNotificationsEnabled()
175         }
176 
177         val clipboardDefaultEnabled =
178             DeviceConfig.getBoolean(
179                 DeviceConfig.NAMESPACE_CLIPBOARD,
180                 CONFIG_SHOW_ACCESS_NOTIFICATIONS_DEFAULT,
181                 true
182             )
183         val defaultSetting = if (clipboardDefaultEnabled) 1 else 0
184         return Settings.Secure.getInt(
185             app.contentResolver,
186             CONFIG_CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS,
187             defaultSetting
188         ) != 0
189     }
190 
toggleClipboardnull191     private fun toggleClipboard() {
192         if (SdkLevel.isAtLeastV()) {
193             clipboardManager.setClipboardAccessNotificationsEnabled(!isClipboardEnabled())
194             return
195         }
196 
197         val newState = if (isClipboardEnabled()) 0 else 1
198         Settings.Secure.putInt(
199             app.contentResolver,
200             CONFIG_CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS,
201             newState
202         )
203     }
204 
isShowPasswordEnablednull205     private fun isShowPasswordEnabled(): Boolean {
206         return Settings.System.getInt(app.contentResolver, Settings.System.TEXT_SHOW_PASSWORD, 1) !=
207             0
208     }
209 
toggleShowPasswordnull210     private fun toggleShowPassword() {
211         Settings.System.putInt(
212             app.contentResolver,
213             Settings.System.TEXT_SHOW_PASSWORD,
214             if (isShowPasswordEnabled()) 0 else 1
215         )
216     }
217 
shouldDisplayShowPasswordTogglenull218     private fun shouldDisplayShowPasswordToggle(): Boolean {
219         return app.resources.getBoolean(R.bool.config_display_show_password_toggle)
220     }
221 }
222 
223 /**
224  * Factory for a PrivacyControlsViewModel
225  *
226  * @param app The current application
227  */
228 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
229 class PrivacyControlsViewModelFactory(
230     private val app: Application,
231 ) : ViewModelProvider.Factory {
createnull232     override fun <T : ViewModel> create(modelClass: Class<T>): T {
233         @Suppress("UNCHECKED_CAST") return PrivacyControlsViewModel(app) as T
234     }
235 }
236