1 /* <lambda>null2 * Copyright 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.media.audio.cts.audiopermissiontests.common 18 19 import android.app.Notification 20 import android.app.NotificationChannel 21 import android.app.NotificationManager 22 import android.app.Service 23 import android.content.AttributionSource 24 import android.content.AttributionSource.myAttributionSource 25 import android.content.Context 26 import android.content.Intent 27 import android.media.AudioFormat 28 import android.media.AudioRecord 29 import android.os.Bundle 30 import android.os.IBinder 31 import android.permission.PermissionManager 32 import android.util.Log 33 34 import java.util.concurrent.CompletableFuture 35 import java.util.concurrent.Executors 36 import java.util.concurrent.Future 37 import java.util.concurrent.atomic.AtomicReference 38 39 import kotlin.coroutines.AbstractCoroutineContextElement 40 import kotlin.coroutines.CoroutineContext 41 import kotlin.coroutines.coroutineContext 42 import kotlin.math.abs 43 import kotlinx.coroutines.CoroutineExceptionHandler 44 import kotlinx.coroutines.CoroutineName 45 import kotlinx.coroutines.CoroutineScope 46 import kotlinx.coroutines.Dispatchers 47 import kotlinx.coroutines.SupervisorJob 48 import kotlinx.coroutines.channels.Channel 49 import kotlinx.coroutines.channels.ClosedReceiveChannelException 50 import kotlinx.coroutines.channels.ReceiveChannel 51 import kotlinx.coroutines.isActive 52 import kotlinx.coroutines.launch 53 54 /** 55 * Service which can records and sends response intents when recording moves between silenced and 56 * unsilenced state. 57 */ 58 open class RecordService : Service() { 59 val TAG = getAppName() + "RecordService" 60 val PREFIX = "android.media.audio.cts." + getAppName() 61 62 private val mJob = 63 SupervisorJob().apply { 64 // Completer on the parent job for all coroutines, so test app is informed that teardown 65 // completes 66 invokeOnCompletion { 67 stopForeground(STOP_FOREGROUND_REMOVE) 68 stopSelf() 69 respond(ACTION_TEARDOWN_FINISHED) 70 } 71 } 72 73 private val handler = object : AbstractCoroutineContextElement(CoroutineExceptionHandler), 74 CoroutineExceptionHandler { 75 override fun handleException(context: CoroutineContext, exception: Throwable) = 76 Log.wtf(TAG, "Uncaught exception", exception).let{} 77 } 78 79 // Parent scope executes on the main thread 80 private val mScope = CoroutineScope(mJob + Dispatchers.Main.immediate + handler) 81 82 // Keyed by record ID provided by the client. Channel is used to communicate with the launched 83 // record coroutine. true/false to start/stop recording, close to end recording. 84 // Main thread (mScope) only for thread safety! 85 private val mRecordings = HashMap<Int, Channel<Boolean>>() 86 87 lateinit var mPermissionManager: PermissionManager 88 val mAttributionSource: AtomicReference<AttributionSource> = AtomicReference(); 89 90 override fun onCreate() { 91 mPermissionManager = getSystemService(PermissionManager::class.java) 92 mAttributionSource.set(mPermissionManager.registerAttributionSource(myAttributionSource())) 93 } 94 95 override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 96 mScope.launch { 97 val recordId = intent.getIntExtra(EXTRA_RECORD_ID, 0) 98 Log.i(TAG, "Receive onStartCommand action: ${intent.getAction()}, id: $recordId") 99 when (intent.getAction()) { 100 PREFIX + ACTION_START_RECORD -> { 101 intent.getExtras() 102 ?.getBinder(EXTRA_ATTRIBUTION) 103 ?.let(IAttrProvider.Stub::asInterface) 104 ?.let { getAttribution(it) } 105 ?.let { next -> 106 mAttributionSource.get().let { old -> 107 mPermissionManager.registerAttributionSource( 108 AttributionSource.Builder(old.getUid()) 109 .setPackageName(old.getPackageName()) 110 .setNext(next) 111 .build()) 112 } 113 } 114 ?.let { mAttributionSource.set(it) } 115 mRecordings 116 .getOrPut(recordId) { 117 // Create the channel, kick off the record and insert into map 118 Channel<Boolean>(Channel.UNLIMITED).also { 119 // IO for unbounded thread-pool, thread per record 120 launch(CoroutineName("Record $recordId") + Dispatchers.IO) { 121 record(recordId, it) 122 } 123 } 124 } 125 .send(true) 126 } 127 PREFIX + ACTION_STOP_RECORD -> 128 mRecordings.get(intent.getIntExtra(EXTRA_RECORD_ID, 0))?.send(false) 129 PREFIX + ACTION_FINISH_RECORD -> 130 mRecordings.get(intent.getIntExtra(EXTRA_RECORD_ID, 0))?.close() 131 PREFIX + ACTION_START_FOREGROUND -> 132 intent.getIntExtra(EXTRA_CAP_OVERRIDE, getCapabilities()).let { 133 Log.i(TAG, "Going foreground with capabilities $it") 134 startForeground(1, buildNotification(), it) 135 } 136 PREFIX + ACTION_STOP_FOREGROUND -> stopForeground(STOP_FOREGROUND_REMOVE) 137 PREFIX + ACTION_TEARDOWN -> { 138 // Finish ongoing records 139 mRecordings.values.forEach { it.close() } 140 mRecordings.clear() 141 // Mark supervisor complete, completer will fire when all children complete. 142 mJob.complete() 143 } 144 PREFIX + ACTION_REQUEST_ATTRIBUTION -> 145 sendBroadcast( 146 Intent(PREFIX + ACTION_SEND_ATTRIBUTION).apply { 147 setPackage(TARGET_PACKAGE) 148 putExtras(Bundle().apply { 149 putBinder(EXTRA_ATTRIBUTION, object: IAttrProvider.Stub() { 150 override fun inject(x: IAttrConsumer) = x.provideAttribution( 151 mAttributionSource.get()) 152 }) 153 }) 154 }) 155 } 156 } 157 return START_NOT_STICKY 158 } 159 160 override fun onDestroy() { 161 Log.i(TAG, "onDestroy") 162 mJob.cancel() 163 } 164 165 // Binding cannot be used since that affects the proc state 166 override fun onBind(intent: Intent): IBinder? = null 167 168 override fun getAttributionSource(): AttributionSource = mAttributionSource.get() 169 170 /** For subclasses to return the package name for receiving intents. */ 171 open fun getAppName(): String = "Base" 172 173 /** For subclasses to return the capabilities to start the service with. */ 174 open fun getCapabilities(): Int = 0 175 176 private fun respond(action: String, recordId: Int? = null) { 177 Log.i(TAG, "Sending $action for id: $recordId") 178 sendBroadcast( 179 Intent(PREFIX + action).apply { 180 setPackage(TARGET_PACKAGE) 181 recordId?.let { putExtra(EXTRA_RECORD_ID, it) } 182 }) 183 } 184 185 /** 186 * Continuously record while {@link mIsRecording} is true. Returns when false. Send intents as 187 * stream moves in and out of being silenced. 188 */ 189 suspend fun record(recordId: Int, channel: ReceiveChannel<Boolean>) { 190 val channelConfig = AudioFormat.CHANNEL_IN_MONO 191 val sampleRate = 32000 192 val RECORD_WARMUP = 800 // 25ms 193 val format = AudioFormat.ENCODING_PCM_16BIT 194 val bufferSizeInBytes = 2 * AudioRecord.getMinBufferSize(sampleRate, channelConfig, format) 195 val audioRecord = 196 AudioRecord.Builder() 197 .setAudioFormat( 198 AudioFormat.Builder() 199 .setEncoding(format) 200 .setSampleRate(sampleRate) 201 .setChannelMask(channelConfig) 202 .build()) 203 .setBufferSizeInBytes(bufferSizeInBytes) 204 .setContext(this) 205 .build() 206 207 var isSilenced: Boolean? = null 208 var isRecording = false 209 val data = ShortArray(bufferSizeInBytes / 2) 210 try { 211 while (coroutineContext.isActive) { 212 val newIsRecording = computeNextRecording(channel, isRecording) ?: break 213 if (!isRecording && newIsRecording) { 214 audioRecord.startRecording() 215 var warmupFrames = 0 216 while (warmupFrames < RECORD_WARMUP) { 217 warmupFrames += audioRecord.read(data, 0, data.size).also { 218 if (it < 0) throw IllegalStateException("AudioRecord read invalid $it") 219 } 220 } 221 mScope.launch { respond(ACTION_RECORD_STARTED, recordId) } 222 } else if (isRecording && !newIsRecording) { 223 audioRecord.stop() 224 mScope.launch { respond(ACTION_RECORD_STOPPED, recordId) } 225 isSilenced = null 226 } 227 isRecording = newIsRecording 228 if (isRecording) { 229 isAudioRecordSilenced(audioRecord, data)?.let { newIsSilenced -> 230 if (isSilenced != newIsSilenced) { 231 mScope.launch { 232 respond( 233 if (newIsSilenced) ACTION_BEGAN_RECEIVE_SILENCE 234 else ACTION_BEGAN_RECEIVE_AUDIO, 235 recordId) 236 } 237 } 238 isSilenced = newIsSilenced 239 } 240 } 241 } 242 } finally { 243 if (isRecording) { 244 audioRecord.stop() 245 mScope.launch { respond(ACTION_RECORD_STOPPED, recordId) } 246 } 247 audioRecord.release() 248 mScope.launch { respond(ACTION_RECORD_FINISHED, recordId) } 249 } 250 } 251 252 private fun getAttribution(prov: IAttrProvider) : AttributionSource { 253 val res = CompletableFuture<AttributionSource>() 254 prov.inject(object : IAttrConsumer.Stub() { 255 override fun provideAttribution(attr: AttributionSource) = res.complete(attr).let {} 256 }) 257 return res.get().also { 258 Log.i(TAG, "Received attr source ${it}") 259 } 260 } 261 262 /** 263 * Consume the data in the channel, and based on the current recording state return the next 264 * state. Returns null to represent ending the task. If not isRecording, block until new data is 265 * available in the channel 266 */ 267 private suspend fun computeNextRecording( 268 channel: ReceiveChannel<Boolean>, 269 isRecording: Boolean 270 ): Boolean? = 271 channel.tryReceive().run { 272 when { 273 isClosed -> null 274 // no update: only wait for state update if we are NOT recording 275 isFailure -> 276 isRecording || 277 try { 278 channel.receive() 279 } catch (e: ClosedReceiveChannelException) { 280 return null 281 } 282 // This shouldn't throw now. Non-blocking read of the record state 283 else -> getOrThrow() 284 } 285 } 286 287 /** 288 * Determine if the audiorecord is silenced. 289 * 290 * @param audioRecord the recording to evaluate 291 * @param data temp data buffer to use 292 * @return true if silenced, false if not silenced, null if no data 293 */ 294 private fun isAudioRecordSilenced(audioRecord: AudioRecord, data: ShortArray): Boolean? = 295 audioRecord.read(data, 0, data.size).let { 296 when { 297 it == 0 -> null 298 it < 0 -> throw IllegalStateException("AudioRecord read invalid result: $it") 299 else -> (data.take(it).map { abs(it.toInt()) }.sum() == 0) 300 } 301 } 302 303 /** Create a notification which is required to start a foreground service */ 304 private fun buildNotification(): Notification { 305 val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 306 307 manager.createNotificationChannel( 308 NotificationChannel("all", "All Notifications", NotificationManager.IMPORTANCE_NONE)) 309 310 return Notification.Builder(this, "all") 311 .setContentTitle("Recording audio") 312 .setContentText("recording...") 313 .setSmallIcon(R.drawable.ic_fg) 314 .build() 315 } 316 } 317