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 
17 package com.android.systemui.qs.tiles.impl.sensorprivacy
18 
19 import android.hardware.SensorPrivacyManager.Sensors.CAMERA
20 import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE
21 import android.hardware.SensorPrivacyManager.Sensors.Sensor
22 import android.os.UserHandle
23 import android.provider.DeviceConfig
24 import android.util.Log
25 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
26 import com.android.systemui.dagger.qualifiers.Background
27 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
28 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
29 import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel
30 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController
31 import dagger.assisted.Assisted
32 import dagger.assisted.AssistedFactory
33 import dagger.assisted.AssistedInject
34 import kotlin.coroutines.CoroutineContext
35 import kotlinx.coroutines.channels.awaitClose
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.distinctUntilChanged
38 import kotlinx.coroutines.flow.flow
39 import kotlinx.coroutines.flow.flowOn
40 import kotlinx.coroutines.flow.onStart
41 import kotlinx.coroutines.withContext
42 
43 /** Observes SensorPrivacyToggle mode state changes providing the [SensorPrivacyToggleTileModel]. */
44 class SensorPrivacyToggleTileDataInteractor
45 @AssistedInject
46 constructor(
47     @Background private val bgCoroutineContext: CoroutineContext,
48     private val privacyController: IndividualSensorPrivacyController,
49     @Assisted @Sensor private val sensorId: Int,
50 ) : QSTileDataInteractor<SensorPrivacyToggleTileModel> {
51     @AssistedFactory
52     interface Factory {
53         fun create(@Sensor id: Int): SensorPrivacyToggleTileDataInteractor
54     }
55 
56     override fun tileData(
57         user: UserHandle,
58         triggers: Flow<DataUpdateTrigger>
59     ): Flow<SensorPrivacyToggleTileModel> =
60         conflatedCallbackFlow {
61                 val callback =
62                     IndividualSensorPrivacyController.Callback { sensor, blocked ->
63                         if (sensor == sensorId) trySend(SensorPrivacyToggleTileModel(blocked))
64                     }
65                 privacyController.addCallback(callback) // does not emit an initial state
66                 awaitClose { privacyController.removeCallback(callback) }
67             }
68             .onStart {
69                 emit(SensorPrivacyToggleTileModel(privacyController.isSensorBlocked(sensorId)))
70             }
71             .distinctUntilChanged()
72             .flowOn(bgCoroutineContext)
73 
74     override fun availability(user: UserHandle) =
75         flow { emit(isAvailable()) }.flowOn(bgCoroutineContext)
76 
77     private suspend fun isAvailable(): Boolean {
78         return privacyController.supportsSensorToggle(sensorId) && isSensorDeviceConfigSet()
79     }
80 
81     private suspend fun isSensorDeviceConfigSet(): Boolean =
82         withContext(bgCoroutineContext) {
83             try {
84                 val deviceConfigName = getDeviceConfigName(sensorId)
85                 return@withContext DeviceConfig.getBoolean(
86                     DeviceConfig.NAMESPACE_PRIVACY,
87                     deviceConfigName,
88                     true
89                 )
90             } catch (exception: IllegalArgumentException) {
91                 Log.w(
92                     TAG,
93                     "isDeviceConfigSet for sensorId $sensorId: " +
94                         "Defaulting to true due to exception. ",
95                     exception
96                 )
97                 return@withContext true
98             }
99         }
100 
101     private fun getDeviceConfigName(sensorId: Int): String {
102         if (sensorId == MICROPHONE) {
103             return "mic_toggle_enabled"
104         } else if (sensorId == CAMERA) {
105             return "camera_toggle_enabled"
106         } else {
107             throw IllegalArgumentException("getDeviceConfigName: unexpected sensorId: $sensorId")
108         }
109     }
110 
111     private companion object {
112         const val TAG = "SensorPrivacyToggleTileException"
113     }
114 }
115