• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 com.android.server.musicrecognition;
18 
19 import static android.Manifest.permission.RECORD_AUDIO;
20 import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_AUDIO_UNAVAILABLE;
21 import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_KILLED;
22 import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_UNAVAILABLE;
23 import static android.media.musicrecognition.MusicRecognitionManager.RecognitionFailureCode;
24 
25 import android.Manifest;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.AppGlobals;
29 import android.app.AppOpsManager;
30 import android.content.ComponentName;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ServiceInfo;
33 import android.media.AudioRecord;
34 import android.media.MediaMetadata;
35 import android.media.musicrecognition.IMusicRecognitionManagerCallback;
36 import android.media.musicrecognition.IMusicRecognitionServiceCallback;
37 import android.media.musicrecognition.RecognitionRequest;
38 import android.os.Bundle;
39 import android.os.IBinder;
40 import android.os.ParcelFileDescriptor;
41 import android.os.RemoteException;
42 import android.util.Pair;
43 import android.util.Slog;
44 
45 import com.android.internal.annotations.GuardedBy;
46 import com.android.server.infra.AbstractPerUserSystemService;
47 
48 import java.io.IOException;
49 import java.io.OutputStream;
50 import java.util.Objects;
51 import java.util.concurrent.CompletableFuture;
52 
53 
54 /**
55  * Handles per-user requests received by
56  * {@link MusicRecognitionManagerService}. Opens an audio stream from the
57  * dsp and writes it into a pipe to {@link RemoteMusicRecognitionService}.
58  */
59 public final class MusicRecognitionManagerPerUserService extends
60         AbstractPerUserSystemService<MusicRecognitionManagerPerUserService,
61                 MusicRecognitionManagerService>
62         implements RemoteMusicRecognitionService.Callbacks {
63 
64     private static final String TAG = MusicRecognitionManagerPerUserService.class.getSimpleName();
65     private static final String MUSIC_RECOGNITION_MANAGER_ATTRIBUTION_TAG =
66             "MusicRecognitionManagerService";
67     private static final String KEY_MUSIC_RECOGNITION_SERVICE_ATTRIBUTION_TAG =
68             "android.media.musicrecognition.attributiontag";
69 
70     // Number of bytes per sample of audio (which is a short).
71     private static final int BYTES_PER_SAMPLE = 2;
72     private static final int MAX_STREAMING_SECONDS = 24;
73 
74     @Nullable
75     @GuardedBy("mLock")
76     private RemoteMusicRecognitionService mRemoteService;
77     private final AppOpsManager mAppOpsManager;
78     private final String mAttributionMessage;
79 
80     // Service info of the remote MusicRecognitionService (which the audio gets forwarded to).
81     private ServiceInfo mServiceInfo;
82     private CompletableFuture<String> mAttributionTagFuture;
83 
MusicRecognitionManagerPerUserService( @onNull MusicRecognitionManagerService primary, @NonNull Object lock, int userId)84     MusicRecognitionManagerPerUserService(
85             @NonNull MusicRecognitionManagerService primary,
86             @NonNull Object lock, int userId) {
87         super(primary, lock, userId);
88 
89         // When attributing audio-access, this establishes that audio access is performed by
90         // MusicRecognitionManager (on behalf of the receiving service, whose attribution tag,
91         // provided by mAttributionTagFuture, is used for the actual calls to startProxyOp(...).
92         mAppOpsManager = getContext().createAttributionContext(
93             MUSIC_RECOGNITION_MANAGER_ATTRIBUTION_TAG).getSystemService(AppOpsManager.class);
94         mAttributionMessage = String.format("MusicRecognitionManager.invokedByUid.%s", userId);
95         mAttributionTagFuture = null;
96         mServiceInfo = null;
97     }
98 
99     @NonNull
100     @GuardedBy("mLock")
101     @Override
newServiceInfoLocked(@onNull ComponentName serviceComponent)102     protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
103             throws PackageManager.NameNotFoundException {
104         ServiceInfo si;
105         try {
106             si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
107                     PackageManager.GET_META_DATA, mUserId);
108         } catch (RemoteException e) {
109             throw new PackageManager.NameNotFoundException(
110                     "Could not get service for " + serviceComponent);
111         }
112         if (!Manifest.permission.BIND_MUSIC_RECOGNITION_SERVICE.equals(si.permission)) {
113             Slog.w(TAG, "MusicRecognitionService from '" + si.packageName
114                     + "' does not require permission "
115                     + Manifest.permission.BIND_MUSIC_RECOGNITION_SERVICE);
116             throw new SecurityException("Service does not require permission "
117                     + Manifest.permission.BIND_MUSIC_RECOGNITION_SERVICE);
118         }
119         // TODO(b/158194857): check process which owns the service has RECORD_AUDIO permission. How?
120         return si;
121     }
122 
123     @GuardedBy("mLock")
124     @Nullable
ensureRemoteServiceLocked( IMusicRecognitionManagerCallback clientCallback)125     private RemoteMusicRecognitionService ensureRemoteServiceLocked(
126             IMusicRecognitionManagerCallback clientCallback) {
127         if (mRemoteService == null) {
128             final String serviceName = getComponentNameLocked();
129             if (serviceName == null) {
130                 if (mMaster.verbose) {
131                     Slog.v(TAG, "ensureRemoteServiceLocked(): not set");
132                 }
133                 return null;
134             }
135             ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
136 
137             mRemoteService = new RemoteMusicRecognitionService(getContext(),
138                     serviceComponent, mUserId, this,
139                     new MusicRecognitionServiceCallback(clientCallback),
140                     mMaster.isBindInstantServiceAllowed(),
141                     mMaster.verbose);
142 
143             try {
144                 mServiceInfo =
145                         getContext().getPackageManager().getServiceInfo(
146                                 mRemoteService.getComponentName(), PackageManager.GET_META_DATA);
147                 mAttributionTagFuture = mRemoteService.getAttributionTag();
148                 Slog.i(TAG, "Remote service bound: " + mRemoteService.getComponentName());
149             } catch (PackageManager.NameNotFoundException e) {
150                 Slog.e(TAG, "Service was not found.", e);
151             }
152         }
153 
154         return mRemoteService;
155     }
156 
157     /**
158      * Read audio from the given capture session using an AudioRecord and writes it to a
159      * ParcelFileDescriptor.
160      */
161     @GuardedBy("mLock")
beginRecognitionLocked( @onNull RecognitionRequest recognitionRequest, @NonNull IBinder callback)162     public void beginRecognitionLocked(
163             @NonNull RecognitionRequest recognitionRequest,
164             @NonNull IBinder callback) {
165         IMusicRecognitionManagerCallback clientCallback =
166                 IMusicRecognitionManagerCallback.Stub.asInterface(callback);
167         mRemoteService = ensureRemoteServiceLocked(clientCallback);
168         if (mRemoteService == null) {
169             try {
170                 clientCallback.onRecognitionFailed(
171                         RECOGNITION_FAILED_SERVICE_UNAVAILABLE);
172             } catch (RemoteException e) {
173                 // Ignored.
174             }
175             return;
176         }
177 
178         Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe();
179         if (clientPipe == null) {
180             try {
181                 clientCallback.onRecognitionFailed(
182                         RECOGNITION_FAILED_AUDIO_UNAVAILABLE);
183             } catch (RemoteException ignored) {
184                 // Ignored.
185             }
186             return;
187         }
188         ParcelFileDescriptor audioSink = clientPipe.second;
189         ParcelFileDescriptor clientRead = clientPipe.first;
190 
191         mAttributionTagFuture.thenAcceptAsync(
192                 tag -> {
193                     streamAudio(tag, recognitionRequest, clientCallback, audioSink);
194                 }, mMaster.mExecutorService);
195 
196         // Send the pipe down to the lookup service while we write to it asynchronously.
197         mRemoteService.onAudioStreamStarted(clientRead, recognitionRequest.getAudioFormat());
198     }
199 
200     /**
201      * Streams audio based on given request to the given audioSink. Notifies callback of errors.
202      *
203      * @param recognitionRequest the recognition request specifying audio parameters.
204      * @param clientCallback the callback to notify on errors.
205      * @param audioSink the sink to which to stream audio to.
206      */
streamAudio(@ullable String attributionTag, @NonNull RecognitionRequest recognitionRequest, IMusicRecognitionManagerCallback clientCallback, ParcelFileDescriptor audioSink)207     private void streamAudio(@Nullable String attributionTag,
208             @NonNull RecognitionRequest recognitionRequest,
209             IMusicRecognitionManagerCallback clientCallback,
210             ParcelFileDescriptor audioSink) {
211         int maxAudioLengthSeconds = Math.min(recognitionRequest.getMaxAudioLengthSeconds(),
212                 MAX_STREAMING_SECONDS);
213         if (maxAudioLengthSeconds <= 0) {
214             // TODO(b/192992319): A request to stream 0s of audio can be used to initialize the
215             //  music recognition service implementation, hence not reporting an error here.
216             // The TODO for Android T is to move this functionality into an init() API call.
217             Slog.i(TAG, "No audio requested. Closing stream.");
218             try {
219                 audioSink.close();
220                 clientCallback.onAudioStreamClosed();
221             } catch (IOException e) {
222                 Slog.e(TAG, "Problem closing stream.", e);
223             } catch (RemoteException ignored) {
224                 // Ignored.
225             }
226             return;
227         }
228 
229         try {
230             startRecordAudioOp(attributionTag);
231         } catch (SecurityException e) {
232             // A security exception can occur if the MusicRecognitionService (receiving the audio)
233             // does not (or does no longer) hold the necessary permissions to record audio.
234             Slog.e(TAG, "RECORD_AUDIO op not permitted on behalf of "
235                     + mServiceInfo.getComponentName(), e);
236             try {
237                 clientCallback.onRecognitionFailed(
238                         RECOGNITION_FAILED_AUDIO_UNAVAILABLE);
239             } catch (RemoteException ignored) {
240                 // Ignored.
241             }
242             return;
243         }
244 
245         AudioRecord audioRecord = createAudioRecord(recognitionRequest, maxAudioLengthSeconds);
246         try (OutputStream fos =
247                      new ParcelFileDescriptor.AutoCloseOutputStream(audioSink)) {
248             streamAudio(recognitionRequest, maxAudioLengthSeconds, audioRecord, fos);
249         } catch (IOException e) {
250             Slog.e(TAG, "Audio streaming stopped.", e);
251         } finally {
252             audioRecord.release();
253             finishRecordAudioOp(attributionTag);
254             try {
255                 clientCallback.onAudioStreamClosed();
256             } catch (RemoteException ignored) {
257                 // Ignored.
258             }
259         }
260     }
261 
262     /** Performs the actual streaming from audioRecord into outputStream. **/
streamAudio(@onNull RecognitionRequest recognitionRequest, int maxAudioLengthSeconds, AudioRecord audioRecord, OutputStream outputStream)263     private void streamAudio(@NonNull RecognitionRequest recognitionRequest,
264             int maxAudioLengthSeconds, AudioRecord audioRecord, OutputStream outputStream)
265             throws IOException {
266         int halfSecondBufferSize =
267                 audioRecord.getBufferSizeInFrames() / maxAudioLengthSeconds;
268         byte[] byteBuffer = new byte[halfSecondBufferSize];
269         int bytesRead = 0;
270         int totalBytesRead = 0;
271         int ignoreBytes =
272                 recognitionRequest.getIgnoreBeginningFrames() * BYTES_PER_SAMPLE;
273         audioRecord.startRecording();
274         while (bytesRead >= 0 && totalBytesRead
275                 < audioRecord.getBufferSizeInFrames() * BYTES_PER_SAMPLE
276                 && mRemoteService != null) {
277             bytesRead = audioRecord.read(byteBuffer, 0, byteBuffer.length);
278             if (bytesRead > 0) {
279                 totalBytesRead += bytesRead;
280                 // If we are ignoring the first x bytes, update that counter.
281                 if (ignoreBytes > 0) {
282                     ignoreBytes -= bytesRead;
283                     // If we've dipped negative, we've skipped through all ignored bytes
284                     // and then some.  Write out the bytes we shouldn't have skipped.
285                     if (ignoreBytes < 0) {
286                         outputStream.write(byteBuffer, bytesRead + ignoreBytes, -ignoreBytes);
287                     }
288                 } else {
289                     outputStream.write(byteBuffer);
290                 }
291             }
292         }
293         Slog.i(TAG,
294                 String.format("Streamed %s bytes from audio record", totalBytesRead));
295     }
296 
297     /**
298      * Callback invoked by {@link android.service.musicrecognition.MusicRecognitionService} to pass
299      * back the music search result.
300      */
301     final class MusicRecognitionServiceCallback extends
302             IMusicRecognitionServiceCallback.Stub {
303 
304         private final IMusicRecognitionManagerCallback mClientCallback;
305 
MusicRecognitionServiceCallback(IMusicRecognitionManagerCallback clientCallback)306         private MusicRecognitionServiceCallback(IMusicRecognitionManagerCallback clientCallback) {
307             mClientCallback = clientCallback;
308         }
309 
310         @Override
onRecognitionSucceeded(MediaMetadata result, Bundle extras)311         public void onRecognitionSucceeded(MediaMetadata result, Bundle extras) {
312             try {
313                 sanitizeBundle(extras);
314                 mClientCallback.onRecognitionSucceeded(result, extras);
315             } catch (RemoteException ignored) {
316                 // Ignored.
317             }
318             destroyService();
319         }
320 
321         @Override
onRecognitionFailed(@ecognitionFailureCode int failureCode)322         public void onRecognitionFailed(@RecognitionFailureCode int failureCode) {
323             try {
324                 mClientCallback.onRecognitionFailed(failureCode);
325             } catch (RemoteException ignored) {
326                 // Ignored.
327             }
328             destroyService();
329         }
330 
getClientCallback()331         private IMusicRecognitionManagerCallback getClientCallback() {
332             return mClientCallback;
333         }
334     }
335 
336     @Override
onServiceDied(@onNull RemoteMusicRecognitionService service)337     public void onServiceDied(@NonNull RemoteMusicRecognitionService service) {
338         try {
339             service.getServerCallback().getClientCallback().onRecognitionFailed(
340                     RECOGNITION_FAILED_SERVICE_KILLED);
341         } catch (RemoteException e) {
342             // Ignored.
343         }
344         Slog.w(TAG, "remote service died: " + service);
345         destroyService();
346     }
347 
348     @GuardedBy("mLock")
destroyService()349     private void destroyService() {
350         synchronized (mLock) {
351             if (mRemoteService != null) {
352                 mRemoteService.destroy();
353                 mRemoteService = null;
354             }
355         }
356     }
357 
358     /**
359      * Tracks that the RECORD_AUDIO operation started (attributes it to the service receiving the
360      * audio).
361      */
startRecordAudioOp(@ullable String attributionTag)362     private void startRecordAudioOp(@Nullable String attributionTag) {
363         int status = mAppOpsManager.startProxyOp(
364                 Objects.requireNonNull(AppOpsManager.permissionToOp(RECORD_AUDIO)),
365                 mServiceInfo.applicationInfo.uid,
366                 mServiceInfo.packageName,
367                 attributionTag,
368                 mAttributionMessage);
369         // The above should already throw a SecurityException. This is just a fallback.
370         if (status != AppOpsManager.MODE_ALLOWED) {
371             throw new SecurityException(String.format(
372                     "Failed to obtain RECORD_AUDIO permission (status: %d) for "
373                     + "receiving service: %s", status, mServiceInfo.getComponentName()));
374         }
375         Slog.i(TAG, String.format(
376                 "Starting audio streaming. Attributing to %s (%d) with tag '%s'",
377                 mServiceInfo.packageName, mServiceInfo.applicationInfo.uid, attributionTag));
378     }
379 
380 
381     /** Tracks that the RECORD_AUDIO operation finished. */
finishRecordAudioOp(@ullable String attributionTag)382     private void finishRecordAudioOp(@Nullable String attributionTag) {
383         mAppOpsManager.finishProxyOp(
384                 Objects.requireNonNull(AppOpsManager.permissionToOp(RECORD_AUDIO)),
385                 mServiceInfo.applicationInfo.uid,
386                 mServiceInfo.packageName,
387                 attributionTag);
388     }
389 
390     /** Establishes an audio stream from the DSP audio source. */
createAudioRecord( @onNull RecognitionRequest recognitionRequest, int maxAudioLengthSeconds)391     private static AudioRecord createAudioRecord(
392             @NonNull RecognitionRequest recognitionRequest,
393             int maxAudioLengthSeconds) {
394         int sampleRate = recognitionRequest.getAudioFormat().getSampleRate();
395         int bufferSize = getBufferSizeInBytes(sampleRate, maxAudioLengthSeconds);
396         return new AudioRecord(recognitionRequest.getAudioAttributes(),
397                 recognitionRequest.getAudioFormat(), bufferSize,
398                 recognitionRequest.getCaptureSession());
399     }
400 
401     /**
402      * Returns the number of bytes required to store {@code bufferLengthSeconds} of audio sampled at
403      * {@code sampleRate} Hz, using the format returned by DSP audio capture.
404      */
getBufferSizeInBytes(int sampleRate, int bufferLengthSeconds)405     private static int getBufferSizeInBytes(int sampleRate, int bufferLengthSeconds) {
406         return BYTES_PER_SAMPLE * sampleRate * bufferLengthSeconds;
407     }
408 
createPipe()409     private static Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() {
410         ParcelFileDescriptor[] fileDescriptors;
411         try {
412             fileDescriptors = ParcelFileDescriptor.createPipe();
413         } catch (IOException e) {
414             Slog.e(TAG, "Failed to create audio stream pipe", e);
415             return null;
416         }
417 
418         if (fileDescriptors.length != 2) {
419             Slog.e(TAG, "Failed to create audio stream pipe, "
420                     + "unexpected number of file descriptors");
421             return null;
422         }
423 
424         if (!fileDescriptors[0].getFileDescriptor().valid()
425                 || !fileDescriptors[1].getFileDescriptor().valid()) {
426             Slog.e(TAG, "Failed to create audio stream pipe, didn't "
427                     + "receive a pair of valid file descriptors.");
428             return null;
429         }
430 
431         return Pair.create(fileDescriptors[0], fileDescriptors[1]);
432     }
433 
434     /** Removes remote objects from the bundle. */
sanitizeBundle(@ullable Bundle bundle)435     private static void sanitizeBundle(@Nullable Bundle bundle) {
436         if (bundle == null) {
437             return;
438         }
439 
440         for (String key : bundle.keySet()) {
441             Object o = bundle.get(key);
442 
443             if (o instanceof Bundle) {
444                 sanitizeBundle((Bundle) o);
445             } else if (o instanceof IBinder || o instanceof ParcelFileDescriptor) {
446                 bundle.remove(key);
447             }
448         }
449     }
450 }
451