Add attribution to the audio access of
MusicRecognitionManagerService.
In particular:
1) Check that the MusicRecognitionService implementation (receiving the
audio) has permission to record audio.
2) Better track audio access and show mic indicator.
Bug: 169403302
Bug: 178174412
Test: Manually tested and observed attributions, atest MusicRecognitionManagerService.
Change-Id: Ie4f11a55df4f6643efeef7c5f037a307fdde01af
diff --git a/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerPerUserService.java b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerPerUserService.java
index 0cb729d..87b2c84 100644
--- a/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerPerUserService.java
+++ b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerPerUserService.java
@@ -16,6 +16,7 @@
package com.android.server.musicrecognition;
+import static android.Manifest.permission.RECORD_AUDIO;
import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_AUDIO_UNAVAILABLE;
import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_KILLED;
import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_UNAVAILABLE;
@@ -25,6 +26,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppGlobals;
+import android.app.AppOpsManager;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
@@ -45,6 +47,7 @@
import java.io.IOException;
import java.io.OutputStream;
+import java.util.Objects;
/**
* Handles per-user requests received by
@@ -64,11 +67,20 @@
@Nullable
@GuardedBy("mLock")
private RemoteMusicRecognitionService mRemoteService;
+ private final AppOpsManager mAppOpsManager;
+
+ private String mAttributionTag;
+ private String mAttributionMessage;
+ private ServiceInfo mServiceInfo;
MusicRecognitionManagerPerUserService(
@NonNull MusicRecognitionManagerService primary,
@NonNull Object lock, int userId) {
super(primary, lock, userId);
+ mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
+ mAttributionMessage = String.format("MusicRecognitionManager.invokedByUid.%s", userId);
+ mAttributionTag = null;
+ mServiceInfo = null;
}
@NonNull
@@ -114,6 +126,13 @@
new MusicRecognitionServiceCallback(clientCallback),
mMaster.isBindInstantServiceAllowed(),
mMaster.verbose);
+ try {
+ mServiceInfo =
+ getContext().getPackageManager().getServiceInfo(
+ mRemoteService.getComponentName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Service was not found.", e);
+ }
}
return mRemoteService;
@@ -127,12 +146,8 @@
public void beginRecognitionLocked(
@NonNull RecognitionRequest recognitionRequest,
@NonNull IBinder callback) {
- int maxAudioLengthSeconds = Math.min(recognitionRequest.getMaxAudioLengthSeconds(),
- MAX_STREAMING_SECONDS);
IMusicRecognitionManagerCallback clientCallback =
IMusicRecognitionManagerCallback.Stub.asInterface(callback);
- AudioRecord audioRecord = createAudioRecord(recognitionRequest, maxAudioLengthSeconds);
-
mRemoteService = ensureRemoteServiceLocked(clientCallback);
if (mRemoteService == null) {
try {
@@ -158,52 +173,92 @@
ParcelFileDescriptor clientRead = clientPipe.first;
mMaster.mExecutorService.execute(() -> {
- try (OutputStream fos =
- new ParcelFileDescriptor.AutoCloseOutputStream(audioSink)) {
- int halfSecondBufferSize =
- audioRecord.getBufferSizeInFrames() / maxAudioLengthSeconds;
- byte[] byteBuffer = new byte[halfSecondBufferSize];
- int bytesRead = 0;
- int totalBytesRead = 0;
- int ignoreBytes =
- recognitionRequest.getIgnoreBeginningFrames() * BYTES_PER_SAMPLE;
- audioRecord.startRecording();
- while (bytesRead >= 0 && totalBytesRead
- < audioRecord.getBufferSizeInFrames() * BYTES_PER_SAMPLE
- && mRemoteService != null) {
- bytesRead = audioRecord.read(byteBuffer, 0, byteBuffer.length);
- if (bytesRead > 0) {
- totalBytesRead += bytesRead;
- // If we are ignoring the first x bytes, update that counter.
- if (ignoreBytes > 0) {
- ignoreBytes -= bytesRead;
- // If we've dipped negative, we've skipped through all ignored bytes
- // and then some. Write out the bytes we shouldn't have skipped.
- if (ignoreBytes < 0) {
- fos.write(byteBuffer, bytesRead + ignoreBytes, -ignoreBytes);
- }
- } else {
- fos.write(byteBuffer);
- }
- }
- }
- Slog.i(TAG, String.format("Streamed %s bytes from audio record", totalBytesRead));
- } catch (IOException e) {
- Slog.e(TAG, "Audio streaming stopped.", e);
- } finally {
- audioRecord.release();
- try {
- clientCallback.onAudioStreamClosed();
- } catch (RemoteException ignored) {
- // Ignored.
- }
- }
+ streamAudio(recognitionRequest, clientCallback, audioSink);
});
// Send the pipe down to the lookup service while we write to it asynchronously.
mRemoteService.writeAudioToPipe(clientRead, recognitionRequest.getAudioFormat());
}
/**
+ * Streams audio based on given request to the given audioSink. Notifies callback of errors.
+ *
+ * @param recognitionRequest the recognition request specifying audio parameters.
+ * @param clientCallback the callback to notify on errors.
+ * @param audioSink the sink to which to stream audio to.
+ */
+ private void streamAudio(@NonNull RecognitionRequest recognitionRequest,
+ IMusicRecognitionManagerCallback clientCallback, ParcelFileDescriptor audioSink) {
+ try {
+ startRecordAudioOp();
+ } catch (SecurityException e) {
+ // A security exception can occur if the MusicRecognitionService (receiving the audio)
+ // does not (or does no longer) hold the necessary permissions to record audio.
+ Slog.e(TAG, "RECORD_AUDIO op not permitted on behalf of "
+ + mServiceInfo.getComponentName(), e);
+ try {
+ clientCallback.onRecognitionFailed(
+ RECOGNITION_FAILED_AUDIO_UNAVAILABLE);
+ } catch (RemoteException ignored) {
+ // Ignored.
+ }
+ return;
+ }
+
+ int maxAudioLengthSeconds = Math.min(recognitionRequest.getMaxAudioLengthSeconds(),
+ MAX_STREAMING_SECONDS);
+ AudioRecord audioRecord = createAudioRecord(recognitionRequest, maxAudioLengthSeconds);
+ try (OutputStream fos =
+ new ParcelFileDescriptor.AutoCloseOutputStream(audioSink)) {
+ streamAudio(recognitionRequest, maxAudioLengthSeconds, audioRecord, fos);
+ } catch (IOException e) {
+ Slog.e(TAG, "Audio streaming stopped.", e);
+ } finally {
+ audioRecord.release();
+ finishRecordAudioOp();
+ try {
+ clientCallback.onAudioStreamClosed();
+ } catch (RemoteException ignored) {
+ // Ignored.
+ }
+ }
+ }
+
+ /** Performs the actual streaming from audioRecord into outputStream. **/
+ private void streamAudio(@NonNull RecognitionRequest recognitionRequest,
+ int maxAudioLengthSeconds, AudioRecord audioRecord, OutputStream outputStream)
+ throws IOException {
+ int halfSecondBufferSize =
+ audioRecord.getBufferSizeInFrames() / maxAudioLengthSeconds;
+ byte[] byteBuffer = new byte[halfSecondBufferSize];
+ int bytesRead = 0;
+ int totalBytesRead = 0;
+ int ignoreBytes =
+ recognitionRequest.getIgnoreBeginningFrames() * BYTES_PER_SAMPLE;
+ audioRecord.startRecording();
+ while (bytesRead >= 0 && totalBytesRead
+ < audioRecord.getBufferSizeInFrames() * BYTES_PER_SAMPLE
+ && mRemoteService != null) {
+ bytesRead = audioRecord.read(byteBuffer, 0, byteBuffer.length);
+ if (bytesRead > 0) {
+ totalBytesRead += bytesRead;
+ // If we are ignoring the first x bytes, update that counter.
+ if (ignoreBytes > 0) {
+ ignoreBytes -= bytesRead;
+ // If we've dipped negative, we've skipped through all ignored bytes
+ // and then some. Write out the bytes we shouldn't have skipped.
+ if (ignoreBytes < 0) {
+ outputStream.write(byteBuffer, bytesRead + ignoreBytes, -ignoreBytes);
+ }
+ } else {
+ outputStream.write(byteBuffer);
+ }
+ }
+ }
+ Slog.i(TAG,
+ String.format("Streamed %s bytes from audio record", totalBytesRead));
+ }
+
+ /**
* Callback invoked by {@link android.service.musicrecognition.MusicRecognitionService} to pass
* back the music search result.
*/
@@ -264,6 +319,29 @@
}
}
+ /**
+ * Tracks that the RECORD_AUDIO operation started (attributes it to the service receiving the
+ * audio).
+ */
+ private void startRecordAudioOp() {
+ mAppOpsManager.startProxyOp(
+ Objects.requireNonNull(AppOpsManager.permissionToOp(RECORD_AUDIO)),
+ mServiceInfo.applicationInfo.uid,
+ mServiceInfo.packageName,
+ mAttributionTag,
+ mAttributionMessage);
+ }
+
+
+ /** Tracks that the RECORD_AUDIO operation finished. */
+ private void finishRecordAudioOp() {
+ mAppOpsManager.finishProxyOp(
+ Objects.requireNonNull(AppOpsManager.permissionToOp(RECORD_AUDIO)),
+ mServiceInfo.applicationInfo.uid,
+ mServiceInfo.packageName,
+ mAttributionTag);
+ }
+
/** Establishes an audio stream from the DSP audio source. */
private static AudioRecord createAudioRecord(
@NonNull RecognitionRequest recognitionRequest,
diff --git a/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerService.java b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerService.java
index 38f43138..e145d33 100644
--- a/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerService.java
+++ b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerService.java
@@ -45,7 +45,7 @@
import java.util.concurrent.Executors;
/**
- * Service which allows a DSP audio event to be securely streamed to a designated {@link
+ * Service which allows audio to be securely streamed to a designated {@link
* MusicRecognitionService}.
*/
public class MusicRecognitionManagerService extends