• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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