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