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