1 /*
2  * Copyright (C) 2023 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 android.permissionui.cts.appthataccessescameraandmic
18 
19 import android.app.Activity
20 import android.app.AppOpsManager
21 import android.content.pm.PackageManager
22 import android.hardware.camera2.CameraAccessException
23 import android.hardware.camera2.CameraCaptureSession
24 import android.hardware.camera2.CameraCharacteristics
25 import android.hardware.camera2.CameraDevice
26 import android.hardware.camera2.CameraManager
27 import android.hardware.camera2.params.OutputConfiguration
28 import android.hardware.camera2.params.SessionConfiguration
29 import android.media.AudioFormat.CHANNEL_IN_MONO
30 import android.media.AudioFormat.ENCODING_PCM_16BIT
31 import android.media.AudioRecord
32 import android.media.ImageReader
33 import android.media.MediaRecorder.AudioSource.MIC
34 import android.os.Bundle
35 import android.os.Handler
36 import android.os.Process
37 import android.util.Log
38 import android.util.Size
39 import android.view.WindowManager
40 import androidx.annotation.NonNull
41 import kotlinx.coroutines.GlobalScope
42 import kotlinx.coroutines.delay
43 import kotlinx.coroutines.launch
44 
45 private const val USE_CAMERA = "use_camera"
46 private const val USE_MICROPHONE = "use_microphone"
47 private const val USE_HOTWORD = "use_hotword"
48 private const val FINISH_EARLY = "finish_early"
49 private const val USE_DURATION_MS = 10000L
50 private const val SAMPLE_RATE_HZ = 44100
51 
52 /**
53  * Activity which will, depending on the extra passed in the intent, use the camera, the microphone,
54  * or both.
55  */
56 class AccessCameraOrMicActivity : Activity() {
57     private lateinit var cameraManager: CameraManager
58     private lateinit var cameraId: String
59     private var cameraDevice: CameraDevice? = null
60     private var recorder: AudioRecord? = null
61     private var appOpsManager: AppOpsManager? = null
62     private var cameraFinished = false
63     private var runCamera = false
64     private var backupCameraOpRunning = true
65     private var micFinished = false
66     private var runMic = false
67     private var hotwordFinished = false
68     private var runHotword = false
69     private var finishEarly = false
70     private var isWatch = false
71 
onCreatenull72     override fun onCreate(savedInstanceState: Bundle?) {
73         super.onCreate(savedInstanceState)
74 
75         if (savedInstanceState != null) {
76             throw RuntimeException(
77                 "Activity was recreated (perhaps due to a configuration change?) " +
78                     "and this activity doesn't currently know how to gracefully handle " +
79                     "configuration changes."
80             )
81         }
82     }
83 
onStartnull84     override fun onStart() {
85         super.onStart()
86         runCamera = intent.getBooleanExtra(USE_CAMERA, false)
87         runMic = intent.getBooleanExtra(USE_MICROPHONE, false)
88         runHotword = intent.getBooleanExtra(USE_HOTWORD, false)
89         finishEarly = intent.getBooleanExtra(FINISH_EARLY, false)
90         isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
91 
92         if (runMic) {
93             useMic()
94         }
95 
96         if (runCamera) {
97             useCamera()
98         }
99 
100         if (runHotword) {
101             useHotword()
102         }
103 
104         if (isWatch) {
105             // Make it possible for uiautomator to find the microphone icon
106             // The icon is shown on the home screen so it is hidden behind the activity unless the
107             // activity is set to translucent.
108             getWindow()
109                 .setFlags(
110                     WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
111                     WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
112                 )
113             getWindow().setLayout(100, 100)
114         }
115     }
116 
finishnull117     override fun finish() {
118         super.finish()
119         cameraDevice?.close()
120         cameraDevice = null
121         recorder?.stop()
122         recorder = null
123         if (runCamera) {
124             appOpsManager?.finishOp(AppOpsManager.OPSTR_CAMERA, Process.myUid(), packageName)
125         }
126         if (runHotword) {
127             appOpsManager?.finishOp(
128                 AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
129                 Process.myUid(),
130                 packageName
131             )
132         }
133         appOpsManager = null
134     }
135 
onStopnull136     override fun onStop() {
137         super.onStop()
138         finish()
139     }
140 
141     private val stateCallback =
142         object : CameraDevice.StateCallback() {
onOpenednull143             override fun onOpened(@NonNull camDevice: CameraDevice) {
144                 cameraDevice = camDevice
145                 val config =
146                     cameraManager!!
147                         .getCameraCharacteristics(cameraId)
148                         .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
149                 val outputFormat = config!!.outputFormats[0]
150                 val outputSize: Size = config!!.getOutputSizes(outputFormat)[0]
151                 val handler = Handler(mainLooper)
152 
153                 val imageReader =
154                     ImageReader.newInstance(outputSize.width, outputSize.height, outputFormat, 2)
155 
156                 val builder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
157                 builder.addTarget(imageReader.surface)
158                 val captureRequest = builder.build()
159                 val sessionConfiguration =
160                     SessionConfiguration(
161                         SessionConfiguration.SESSION_REGULAR,
162                         listOf(OutputConfiguration(imageReader.surface)),
163                         mainExecutor,
164                         object : CameraCaptureSession.StateCallback() {
165                             override fun onConfigured(session: CameraCaptureSession) {
166                                 session.capture(captureRequest, null, handler)
167                             }
168 
169                             override fun onConfigureFailed(session: CameraCaptureSession) {}
170 
171                             override fun onReady(session: CameraCaptureSession) {}
172                         }
173                     )
174 
175                 imageReader.setOnImageAvailableListener(
176                     {
177                         GlobalScope.launch {
178                             delay(USE_DURATION_MS)
179                             if (!backupCameraOpRunning) {
180                                 cameraFinished = true
181                                 if (!runMic || micFinished) {
182                                     finish()
183                                 }
184                             }
185                         }
186                     },
187                     handler
188                 )
189                 cameraDevice!!.createCaptureSession(sessionConfiguration)
190             }
191 
onDisconnectednull192             override fun onDisconnected(@NonNull camDevice: CameraDevice) {
193                 Log.e("CameraMicIndicatorsPermissionTest", "camera disconnected")
194                 startBackupCamera(camDevice)
195             }
196 
onErrornull197             override fun onError(@NonNull camDevice: CameraDevice, error: Int) {
198                 Log.e("CameraMicIndicatorsPermissionTest", "camera error $error")
199                 startBackupCamera(camDevice)
200             }
201         }
202 
startBackupCameranull203     private fun startBackupCamera(camDevice: CameraDevice?) {
204         // Something went wrong with the camera. Fallback to direct app op usage
205         if (runCamera && !cameraFinished) {
206             backupCameraOpRunning = true
207             appOpsManager = getSystemService(AppOpsManager::class.java)
208             appOpsManager?.startOpNoThrow(AppOpsManager.OPSTR_CAMERA, Process.myUid(), packageName)
209 
210             GlobalScope.launch {
211                 delay(USE_DURATION_MS)
212                 cameraFinished = true
213                 backupCameraOpRunning = false
214                 finishIfAllDone()
215             }
216         }
217         camDevice?.close()
218         if (camDevice == cameraDevice) {
219             cameraDevice = null
220         }
221     }
222 
223     @Throws(CameraAccessException::class)
useCameranull224     private fun useCamera() {
225         // TODO 192690992: determine why the camera manager code is flaky
226         startBackupCamera(null)
227         /*
228         cameraManager = getSystemService(CameraManager::class.java)!!
229         cameraId = cameraManager.cameraIdList[0]
230         cameraManager.openCamera(cameraId, mainExecutor, stateCallback)
231          */
232     }
233 
useMicnull234     private fun useMic() {
235         val minSize =
236             AudioRecord.getMinBufferSize(SAMPLE_RATE_HZ, CHANNEL_IN_MONO, ENCODING_PCM_16BIT)
237         recorder = AudioRecord(MIC, SAMPLE_RATE_HZ, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, minSize)
238         recorder?.startRecording()
239         if (finishEarly) {
240             appOpsManager = getSystemService(AppOpsManager::class.java)
241             appOpsManager?.finishOp(AppOpsManager.OPSTR_RECORD_AUDIO, Process.myUid(), packageName)
242             return
243         }
244         GlobalScope.launch {
245             delay(USE_DURATION_MS)
246             micFinished = true
247             finishIfAllDone()
248         }
249     }
250 
useHotwordnull251     private fun useHotword() {
252         appOpsManager = getSystemService(AppOpsManager::class.java)
253         appOpsManager?.startOpNoThrow(
254             AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
255             Process.myUid(),
256             packageName
257         )
258 
259         GlobalScope.launch {
260             delay(USE_DURATION_MS)
261             hotwordFinished = true
262             finishIfAllDone()
263         }
264     }
265 
finishIfAllDonenull266     private fun finishIfAllDone() {
267         if (
268             (!runMic || micFinished) &&
269                 (!runCamera || cameraFinished) &&
270                 (!runHotword || hotwordFinished)
271         ) {
272             finish()
273         }
274     }
275 }
276