Add system service for music recognition.
The client submits a RecognitionRequest via MusicRecognitionManager. System server opens an audio stream based on the request and sends it to a MusicRecognitionService (which is exposed by a system app). The result is passed back through system server to the original caller.
Test: tracked in b/169662646
Bug: 169403302
Change-Id: I4c7fd9d9d72ddd5678867fd037cab6198bff2c2d
diff --git a/api/system-current.txt b/api/system-current.txt
index 8009d037..25d5b59 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -38,6 +38,7 @@
field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE";
field public static final String BIND_IMS_SERVICE = "android.permission.BIND_IMS_SERVICE";
field public static final String BIND_KEYGUARD_APPWIDGET = "android.permission.BIND_KEYGUARD_APPWIDGET";
+ field public static final String BIND_MUSIC_RECOGNITION_SERVICE = "android.permission.BIND_MUSIC_RECOGNITION_SERVICE";
field public static final String BIND_NETWORK_RECOMMENDATION_SERVICE = "android.permission.BIND_NETWORK_RECOMMENDATION_SERVICE";
field public static final String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE";
field public static final String BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE = "android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE";
@@ -119,6 +120,7 @@
field public static final String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING";
field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
field public static final String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS";
+ field public static final String MANAGE_MUSIC_RECOGNITION = "android.permission.MANAGE_MUSIC_RECOGNITION";
field public static final String MANAGE_ONE_TIME_PERMISSION_SESSIONS = "android.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS";
field public static final String MANAGE_ROLE_HOLDERS = "android.permission.MANAGE_ROLE_HOLDERS";
field public static final String MANAGE_ROLLBACKS = "android.permission.MANAGE_ROLLBACKS";
@@ -4528,6 +4530,58 @@
}
+package android.media.musicrecognition {
+
+ public class MusicRecognitionManager {
+ method @RequiresPermission(android.Manifest.permission.MANAGE_MUSIC_RECOGNITION) public void beginStreamingSearch(@NonNull android.media.musicrecognition.RecognitionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.media.musicrecognition.MusicRecognitionManager.RecognitionCallback);
+ field public static final int RECOGNITION_FAILED_AUDIO_UNAVAILABLE = 7; // 0x7
+ field public static final int RECOGNITION_FAILED_NOT_FOUND = 1; // 0x1
+ field public static final int RECOGNITION_FAILED_NO_CONNECTIVITY = 2; // 0x2
+ field public static final int RECOGNITION_FAILED_SERVICE_KILLED = 5; // 0x5
+ field public static final int RECOGNITION_FAILED_SERVICE_UNAVAILABLE = 3; // 0x3
+ field public static final int RECOGNITION_FAILED_TIMEOUT = 6; // 0x6
+ field public static final int RECOGNITION_FAILED_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static interface MusicRecognitionManager.RecognitionCallback {
+ method public void onAudioStreamClosed();
+ method public void onRecognitionFailed(@NonNull android.media.musicrecognition.RecognitionRequest, int);
+ method public void onRecognitionSucceeded(@NonNull android.media.musicrecognition.RecognitionRequest, @NonNull android.media.MediaMetadata, @Nullable android.os.Bundle);
+ }
+
+ public abstract class MusicRecognitionService extends android.app.Service {
+ ctor public MusicRecognitionService();
+ method public abstract void onRecognize(@NonNull android.os.ParcelFileDescriptor, @NonNull android.media.AudioFormat, @NonNull android.media.musicrecognition.MusicRecognitionService.Callback);
+ }
+
+ public static interface MusicRecognitionService.Callback {
+ method public void onRecognitionFailed(int);
+ method public void onRecognitionSucceeded(@NonNull android.media.MediaMetadata, @Nullable android.os.Bundle);
+ }
+
+ public final class RecognitionRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.media.AudioAttributes getAudioAttributes();
+ method @NonNull public android.media.AudioFormat getAudioFormat();
+ method public int getCaptureSession();
+ method public int getIgnoreBeginningFrames();
+ method public int getMaxAudioLengthSeconds();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.musicrecognition.RecognitionRequest> CREATOR;
+ }
+
+ public static final class RecognitionRequest.Builder {
+ ctor public RecognitionRequest.Builder();
+ method @NonNull public android.media.musicrecognition.RecognitionRequest build();
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setAudioAttributes(@NonNull android.media.AudioAttributes);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setAudioFormat(@NonNull android.media.AudioFormat);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setCaptureSession(int);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setIgnoreBeginningFrames(int);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setMaxAudioLengthSeconds(int);
+ }
+
+}
+
package android.media.session {
public final class MediaSessionManager {
diff --git a/api/test-current.txt b/api/test-current.txt
index af70562..0488bc7 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -2006,6 +2006,57 @@
}
+package android.media.musicrecognition {
+
+ public class MusicRecognitionManager {
+ field public static final int RECOGNITION_FAILED_AUDIO_UNAVAILABLE = 7; // 0x7
+ field public static final int RECOGNITION_FAILED_NOT_FOUND = 1; // 0x1
+ field public static final int RECOGNITION_FAILED_NO_CONNECTIVITY = 2; // 0x2
+ field public static final int RECOGNITION_FAILED_SERVICE_KILLED = 5; // 0x5
+ field public static final int RECOGNITION_FAILED_SERVICE_UNAVAILABLE = 3; // 0x3
+ field public static final int RECOGNITION_FAILED_TIMEOUT = 6; // 0x6
+ field public static final int RECOGNITION_FAILED_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static interface MusicRecognitionManager.RecognitionCallback {
+ method public void onAudioStreamClosed();
+ method public void onRecognitionFailed(@NonNull android.media.musicrecognition.RecognitionRequest, int);
+ method public void onRecognitionSucceeded(@NonNull android.media.musicrecognition.RecognitionRequest, @NonNull android.media.MediaMetadata, @Nullable android.os.Bundle);
+ }
+
+ public abstract class MusicRecognitionService extends android.app.Service {
+ ctor public MusicRecognitionService();
+ method public abstract void onRecognize(@NonNull android.os.ParcelFileDescriptor, @NonNull android.media.AudioFormat, @NonNull android.media.musicrecognition.MusicRecognitionService.Callback);
+ }
+
+ public static interface MusicRecognitionService.Callback {
+ method public void onRecognitionFailed(int);
+ method public void onRecognitionSucceeded(@NonNull android.media.MediaMetadata, @Nullable android.os.Bundle);
+ }
+
+ public final class RecognitionRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.media.AudioAttributes getAudioAttributes();
+ method @NonNull public android.media.AudioFormat getAudioFormat();
+ method public int getCaptureSession();
+ method public int getIgnoreBeginningFrames();
+ method public int getMaxAudioLengthSeconds();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.musicrecognition.RecognitionRequest> CREATOR;
+ }
+
+ public static final class RecognitionRequest.Builder {
+ ctor public RecognitionRequest.Builder();
+ method @NonNull public android.media.musicrecognition.RecognitionRequest build();
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setAudioAttributes(@NonNull android.media.AudioAttributes);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setAudioFormat(@NonNull android.media.AudioFormat);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setCaptureSession(int);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setIgnoreBeginningFrames(int);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setMaxAudioLengthSeconds(int);
+ }
+
+}
+
package android.media.tv {
public final class TvInputManager {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 3b11b0d..9ddc66f 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -106,6 +106,8 @@
import android.media.MediaTranscodeManager;
import android.media.midi.IMidiManager;
import android.media.midi.MidiManager;
+import android.media.musicrecognition.IMusicRecognitionManager;
+import android.media.musicrecognition.MusicRecognitionManager;
import android.media.projection.MediaProjectionManager;
import android.media.soundtrigger.SoundTriggerManager;
import android.media.tv.ITvInputManager;
@@ -1118,6 +1120,17 @@
return new AutofillManager(ctx.getOuterContext(), service);
}});
+ registerService(Context.MUSIC_RECOGNITION_SERVICE, MusicRecognitionManager.class,
+ new CachedServiceFetcher<MusicRecognitionManager>() {
+ @Override
+ public MusicRecognitionManager createService(ContextImpl ctx) {
+ IBinder b = ServiceManager.getService(
+ Context.MUSIC_RECOGNITION_SERVICE);
+ return new MusicRecognitionManager(
+ IMusicRecognitionManager.Stub.asInterface(b));
+ }
+ });
+
registerService(Context.CONTENT_CAPTURE_MANAGER_SERVICE, ContentCaptureManager.class,
new CachedServiceFetcher<ContentCaptureManager>() {
@Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 98f7887..e1c09d9 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4481,6 +4481,14 @@
public static final String SOUND_TRIGGER_MIDDLEWARE_SERVICE = "soundtrigger_middleware";
/**
+ * Used to access {@link MusicRecognitionManagerService}.
+ *
+ * @hide
+ * @see #getSystemService(String)
+ */
+ public static final String MUSIC_RECOGNITION_SERVICE = "music_recognition";
+
+ /**
* Official published name of the (internal) permission service.
*
* @see #getSystemService(String)
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index cdcb24b..aab9fa7 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3442,6 +3442,14 @@
<permission android:name="android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE"
android:protectionLevel="signature" />
+ <!-- Must be declared by a android.service.musicrecognition.MusicRecognitionService,
+ to ensure that only the system can bind to it.
+ @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
+ <p>Protection level: signature
+ -->
+ <permission android:name="android.permission.BIND_MUSIC_RECOGNITION_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- Must be required by a android.service.autofill.augmented.AugmentedAutofillService,
to ensure that only the system can bind to it.
@SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
@@ -4799,6 +4807,11 @@
<permission android:name="android.permission.MANAGE_CONTENT_CAPTURE"
android:protectionLevel="signature" />
+ <!-- @SystemApi Allows an application to manage the music recognition service.
+ @hide <p>Not for use by third-party applications.</p> -->
+ <permission android:name="android.permission.MANAGE_MUSIC_RECOGNITION"
+ android:protectionLevel="signature" />
+
<!-- @SystemApi Allows an application to manage the content suggestions service.
@hide <p>Not for use by third-party applications.</p> -->
<permission android:name="android.permission.MANAGE_CONTENT_SUGGESTIONS"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5f2e4f9..7ff163c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3635,6 +3635,8 @@
-->
<string name="config_defaultContentSuggestionsService" translatable="false"></string>
+ <string name="config_defaultMusicRecognitionService" translatable="false"></string>
+
<!-- The package name for the default retail demo app.
This package must be trusted, as it has the permissions to query the usage stats on the
device.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 35ce780..8134038 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3446,6 +3446,7 @@
<java-symbol type="string" name="config_defaultAugmentedAutofillService" />
<java-symbol type="string" name="config_defaultAppPredictionService" />
<java-symbol type="string" name="config_defaultContentSuggestionsService" />
+ <java-symbol type="string" name="config_defaultMusicRecognitionService" />
<java-symbol type="string" name="config_defaultAttentionService" />
<java-symbol type="string" name="config_defaultSystemCaptionsService" />
<java-symbol type="string" name="config_defaultSystemCaptionsManagerService" />
diff --git a/media/java/android/media/musicrecognition/IMusicRecognitionManager.aidl b/media/java/android/media/musicrecognition/IMusicRecognitionManager.aidl
new file mode 100644
index 0000000..9e71afa
--- /dev/null
+++ b/media/java/android/media/musicrecognition/IMusicRecognitionManager.aidl
@@ -0,0 +1,14 @@
+package android.media.musicrecognition;
+
+import android.media.musicrecognition.RecognitionRequest;
+import android.os.IBinder;
+
+/**
+ * Used by {@link MusicRecognitionManager} to tell system server to begin open an audio stream to
+ * the designated lookup service.
+ *
+ * @hide
+ */
+interface IMusicRecognitionManager {
+ void beginRecognition(in RecognitionRequest recognitionRequest, in IBinder callback);
+}
\ No newline at end of file
diff --git a/media/java/android/media/musicrecognition/IMusicRecognitionManagerCallback.aidl b/media/java/android/media/musicrecognition/IMusicRecognitionManagerCallback.aidl
new file mode 100644
index 0000000..e70504f
--- /dev/null
+++ b/media/java/android/media/musicrecognition/IMusicRecognitionManagerCallback.aidl
@@ -0,0 +1,15 @@
+package android.media.musicrecognition;
+
+import android.os.Bundle;
+import android.media.MediaMetadata;
+
+/**
+ * Callback used by system server to notify invoker of {@link MusicRecognitionManager} of the result
+ *
+ * @hide
+ */
+oneway interface IMusicRecognitionManagerCallback {
+ void onRecognitionSucceeded(in MediaMetadata result, in Bundle extras);
+ void onRecognitionFailed(int failureCode);
+ void onAudioStreamClosed();
+}
\ No newline at end of file
diff --git a/media/java/android/media/musicrecognition/IMusicRecognitionService.aidl b/media/java/android/media/musicrecognition/IMusicRecognitionService.aidl
new file mode 100644
index 0000000..26543ed
--- /dev/null
+++ b/media/java/android/media/musicrecognition/IMusicRecognitionService.aidl
@@ -0,0 +1,18 @@
+package android.media.musicrecognition;
+
+import android.media.AudioFormat;
+import android.os.ParcelFileDescriptor;
+import android.os.IBinder;
+import android.media.musicrecognition.IMusicRecognitionServiceCallback;
+
+/**
+ * Interface from the system to a {@link MusicRecognitionService}.
+ *
+ * @hide
+ */
+oneway interface IMusicRecognitionService {
+ void onAudioStreamStarted(
+ in ParcelFileDescriptor fd,
+ in AudioFormat audioFormat,
+ in IMusicRecognitionServiceCallback callback);
+}
\ No newline at end of file
diff --git a/media/java/android/media/musicrecognition/IMusicRecognitionServiceCallback.aidl b/media/java/android/media/musicrecognition/IMusicRecognitionServiceCallback.aidl
new file mode 100644
index 0000000..15215c4
--- /dev/null
+++ b/media/java/android/media/musicrecognition/IMusicRecognitionServiceCallback.aidl
@@ -0,0 +1,15 @@
+package android.media.musicrecognition;
+
+import android.os.Bundle;
+import android.media.MediaMetadata;
+
+/**
+ * Interface from a {@MusicRecognitionService} the system.
+ *
+ * @hide
+ */
+oneway interface IMusicRecognitionServiceCallback {
+ void onRecognitionSucceeded(in MediaMetadata result, in Bundle extras);
+
+ void onRecognitionFailed(int failureCode);
+}
\ No newline at end of file
diff --git a/media/java/android/media/musicrecognition/MusicRecognitionManager.java b/media/java/android/media/musicrecognition/MusicRecognitionManager.java
new file mode 100644
index 0000000..c7f55df
--- /dev/null
+++ b/media/java/android/media/musicrecognition/MusicRecognitionManager.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.musicrecognition;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.media.MediaMetadata;
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * System service that manages music recognition.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+@SystemService(Context.MUSIC_RECOGNITION_SERVICE)
+public class MusicRecognitionManager {
+
+ /**
+ * Error code provided by RecognitionCallback#onRecognitionFailed()
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"RECOGNITION_FAILED_"},
+ value = {RECOGNITION_FAILED_UNKNOWN,
+ RECOGNITION_FAILED_NOT_FOUND,
+ RECOGNITION_FAILED_NO_CONNECTIVITY,
+ RECOGNITION_FAILED_SERVICE_UNAVAILABLE,
+ RECOGNITION_FAILED_SERVICE_KILLED,
+ RECOGNITION_FAILED_TIMEOUT,
+ RECOGNITION_FAILED_AUDIO_UNAVAILABLE})
+ public @interface RecognitionFailureCode {
+ }
+
+ /** Catchall error code. */
+ public static final int RECOGNITION_FAILED_UNKNOWN = -1;
+ /** Recognition was performed but no result could be identified. */
+ public static final int RECOGNITION_FAILED_NOT_FOUND = 1;
+ /** Recognition failed because the server couldn't be reached. */
+ public static final int RECOGNITION_FAILED_NO_CONNECTIVITY = 2;
+ /**
+ * Recognition was not possible because the application which provides it is not available (for
+ * example, disabled).
+ */
+ public static final int RECOGNITION_FAILED_SERVICE_UNAVAILABLE = 3;
+ /** Recognition failed because the recognizer was killed. */
+ public static final int RECOGNITION_FAILED_SERVICE_KILLED = 5;
+ /** Recognition attempt timed out. */
+ public static final int RECOGNITION_FAILED_TIMEOUT = 6;
+ /** Recognition failed due to an issue with obtaining an audio stream. */
+ public static final int RECOGNITION_FAILED_AUDIO_UNAVAILABLE = 7;
+
+ /** Callback interface for the caller of this api. */
+ public interface RecognitionCallback {
+ /**
+ * Should be invoked by receiving app with the result of the search.
+ *
+ * @param recognitionRequest original request that started the recognition
+ * @param result result of the search
+ * @param extras extra data to be supplied back to the caller. Note that all
+ * executable parameters and file descriptors would be removed from the
+ * supplied bundle
+ */
+ void onRecognitionSucceeded(@NonNull RecognitionRequest recognitionRequest,
+ @NonNull MediaMetadata result, @Nullable Bundle extras);
+
+ /**
+ * Invoked when the search is not successful (possibly but not necessarily due to error).
+ *
+ * @param recognitionRequest original request that started the recognition
+ * @param failureCode failure code describing reason for failure
+ */
+ void onRecognitionFailed(@NonNull RecognitionRequest recognitionRequest,
+ @RecognitionFailureCode int failureCode);
+
+ /**
+ * Invoked by the system once the audio stream is closed either due to error, reaching the
+ * limit, or the remote service closing the stream. Always called per
+ * #beingStreamingSearch() invocation.
+ */
+ void onAudioStreamClosed();
+ }
+
+ private final IMusicRecognitionManager mService;
+
+ /** @hide */
+ public MusicRecognitionManager(IMusicRecognitionManager service) {
+ mService = service;
+ }
+
+ /**
+ * Constructs an {@link android.media.AudioRecord} from the given parameters and streams the
+ * audio bytes to the designated cloud lookup service. After the lookup is done, the given
+ * callback will be invoked by the system with the result or lack thereof.
+ *
+ * @param recognitionRequest audio parameters for the stream to search
+ * @param callbackExecutor where the callback is invoked
+ * @param callback invoked when the result is available
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_MUSIC_RECOGNITION)
+ public void beginStreamingSearch(
+ @NonNull RecognitionRequest recognitionRequest,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull RecognitionCallback callback) {
+ try {
+ mService.beginRecognition(
+ requireNonNull(recognitionRequest),
+ new MusicRecognitionCallbackWrapper(
+ requireNonNull(recognitionRequest),
+ requireNonNull(callback),
+ requireNonNull(callbackExecutor)));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private final class MusicRecognitionCallbackWrapper extends
+ IMusicRecognitionManagerCallback.Stub {
+
+ @NonNull
+ private final RecognitionRequest mRecognitionRequest;
+ @NonNull
+ private final RecognitionCallback mCallback;
+ @NonNull
+ private final Executor mCallbackExecutor;
+
+ MusicRecognitionCallbackWrapper(
+ RecognitionRequest recognitionRequest,
+ RecognitionCallback callback,
+ Executor callbackExecutor) {
+ mRecognitionRequest = recognitionRequest;
+ mCallback = callback;
+ mCallbackExecutor = callbackExecutor;
+ }
+
+ @Override
+ public void onRecognitionSucceeded(MediaMetadata result, Bundle extras) {
+ mCallbackExecutor.execute(
+ () -> mCallback.onRecognitionSucceeded(mRecognitionRequest, result, extras));
+ }
+
+ @Override
+ public void onRecognitionFailed(@RecognitionFailureCode int failureCode) {
+ mCallbackExecutor.execute(
+ () -> mCallback.onRecognitionFailed(mRecognitionRequest, failureCode));
+ }
+
+ @Override
+ public void onAudioStreamClosed() {
+ mCallbackExecutor.execute(mCallback::onAudioStreamClosed);
+ }
+ }
+}
diff --git a/media/java/android/media/musicrecognition/MusicRecognitionService.java b/media/java/android/media/musicrecognition/MusicRecognitionService.java
new file mode 100644
index 0000000..b75d2c4
--- /dev/null
+++ b/media/java/android/media/musicrecognition/MusicRecognitionService.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.musicrecognition;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.Intent;
+import android.media.AudioFormat;
+import android.media.MediaMetadata;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Implemented by an app that wants to offer music search lookups. The system will start the
+ * service and stream up to 16 seconds of audio over the given file descriptor.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class MusicRecognitionService extends Service {
+
+ private static final String TAG = MusicRecognitionService.class.getSimpleName();
+
+ /** Callback for the result of the remote search. */
+ public interface Callback {
+ /**
+ * Call this method to pass back a successful search result.
+ *
+ * @param result successful result of the search
+ * @param extras extra data to be supplied back to the caller. Note that all executable
+ * parameters and file descriptors would be removed from the supplied bundle
+ */
+ void onRecognitionSucceeded(@NonNull MediaMetadata result, @Nullable Bundle extras);
+
+ /**
+ * Call this method if the search does not find a result on an error occurred.
+ */
+ void onRecognitionFailed(@MusicRecognitionManager.RecognitionFailureCode int failureCode);
+ }
+
+ /**
+ * Action used to start this service.
+ *
+ * @hide
+ */
+ public static final String ACTION_MUSIC_SEARCH_LOOKUP =
+ "android.service.musicrecognition.MUSIC_RECOGNITION";
+
+ private Handler mHandler;
+ private final IMusicRecognitionService mServiceInterface =
+ new IMusicRecognitionService.Stub() {
+ @Override
+ public void onAudioStreamStarted(ParcelFileDescriptor fd,
+ AudioFormat audioFormat,
+ IMusicRecognitionServiceCallback callback) {
+ mHandler.sendMessage(
+ obtainMessage(MusicRecognitionService.this::onRecognize, fd,
+ audioFormat,
+ new Callback() {
+ @Override
+ public void onRecognitionSucceeded(
+ @NonNull MediaMetadata result,
+ @Nullable Bundle extras) {
+ try {
+ callback.onRecognitionSucceeded(result, extras);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void onRecognitionFailed(int failureCode) {
+ try {
+ callback.onRecognitionFailed(failureCode);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }));
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ }
+
+ /**
+ * Read audio from this stream. You must invoke the callback whether the music is recognized or
+ * not.
+ *
+ * @param stream containing music to be recognized. Close when you are finished.
+ * @param audioFormat describes sample rate, channels and endianness of the stream
+ * @param callback to invoke after lookup is finished. Must always be called.
+ */
+ public abstract void onRecognize(@NonNull ParcelFileDescriptor stream,
+ @NonNull AudioFormat audioFormat,
+ @NonNull Callback callback);
+
+ /**
+ * @hide
+ */
+ @Nullable
+ @Override
+ public IBinder onBind(@NonNull Intent intent) {
+ if (ACTION_MUSIC_SEARCH_LOOKUP.equals(intent.getAction())) {
+ return mServiceInterface.asBinder();
+ }
+ Log.w(TAG,
+ "Tried to bind to wrong intent (should be " + ACTION_MUSIC_SEARCH_LOOKUP + ": "
+ + intent);
+ return null;
+ }
+}
diff --git a/media/java/android/media/musicrecognition/RecognitionRequest.aidl b/media/java/android/media/musicrecognition/RecognitionRequest.aidl
new file mode 100644
index 0000000..757b5701
--- /dev/null
+++ b/media/java/android/media/musicrecognition/RecognitionRequest.aidl
@@ -0,0 +1,18 @@
+/* Copyright 2020, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.media.musicrecognition;
+
+parcelable RecognitionRequest;
\ No newline at end of file
diff --git a/media/java/android/media/musicrecognition/RecognitionRequest.java b/media/java/android/media/musicrecognition/RecognitionRequest.java
new file mode 100644
index 0000000..65b2ccd
--- /dev/null
+++ b/media/java/android/media/musicrecognition/RecognitionRequest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.musicrecognition;
+
+import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+import static android.media.AudioFormat.ENCODING_PCM_16BIT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Encapsulates parameters for making music recognition queries via {@link MusicRecognitionManager}.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class RecognitionRequest implements Parcelable {
+ @NonNull private final AudioAttributes mAudioAttributes;
+ @NonNull private final AudioFormat mAudioFormat;
+ private final int mCaptureSession;
+ private final int mMaxAudioLengthSeconds;
+ private final int mIgnoreBeginningFrames;
+
+ private RecognitionRequest(Builder b) {
+ mAudioAttributes = requireNonNull(b.mAudioAttributes);
+ mAudioFormat = requireNonNull(b.mAudioFormat);
+ mCaptureSession = b.mCaptureSession;
+ mMaxAudioLengthSeconds = b.mMaxAudioLengthSeconds;
+ mIgnoreBeginningFrames = b.mIgnoreBeginningFrames;
+ }
+
+ @NonNull
+ public AudioAttributes getAudioAttributes() {
+ return mAudioAttributes;
+ }
+
+ @NonNull
+ public AudioFormat getAudioFormat() {
+ return mAudioFormat;
+ }
+
+ public int getCaptureSession() {
+ return mCaptureSession;
+ }
+
+ @SuppressWarnings("MethodNameUnits")
+ public int getMaxAudioLengthSeconds() {
+ return mMaxAudioLengthSeconds;
+ }
+
+ public int getIgnoreBeginningFrames() {
+ return mIgnoreBeginningFrames;
+ }
+
+ /**
+ * Builder for constructing StreamSearchRequest objects.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static final class Builder {
+ private AudioFormat mAudioFormat = new AudioFormat.Builder()
+ .setSampleRate(16000)
+ .setEncoding(ENCODING_PCM_16BIT)
+ .build();
+ private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
+ .setContentType(CONTENT_TYPE_MUSIC)
+ .build();
+ private int mCaptureSession = MediaRecorder.AudioSource.MIC;
+ private int mMaxAudioLengthSeconds = 24; // Max enforced in system server.
+ private int mIgnoreBeginningFrames = 0;
+
+ /** Attributes passed to the constructed {@link AudioRecord}. */
+ @NonNull
+ public Builder setAudioAttributes(@NonNull AudioAttributes audioAttributes) {
+ mAudioAttributes = audioAttributes;
+ return this;
+ }
+
+ /** AudioFormat passed to the constructed {@link AudioRecord}. */
+ @NonNull
+ public Builder setAudioFormat(@NonNull AudioFormat audioFormat) {
+ mAudioFormat = audioFormat;
+ return this;
+ }
+
+ /** Constant from {@link android.media.MediaRecorder.AudioSource}. */
+ @NonNull
+ public Builder setCaptureSession(int captureSession) {
+ mCaptureSession = captureSession;
+ return this;
+ }
+
+ /** Maximum number of seconds to stream from the audio source. */
+ @NonNull
+ public Builder setMaxAudioLengthSeconds(int maxAudioLengthSeconds) {
+ mMaxAudioLengthSeconds = maxAudioLengthSeconds;
+ return this;
+ }
+
+ /** Number of samples to drop from the start of the stream. */
+ @NonNull
+ public Builder setIgnoreBeginningFrames(int ignoreBeginningFrames) {
+ mIgnoreBeginningFrames = ignoreBeginningFrames;
+ return this;
+ }
+
+ /** Returns the constructed request. */
+ @NonNull
+ public RecognitionRequest build() {
+ return new RecognitionRequest(this);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mAudioFormat, flags);
+ dest.writeParcelable(mAudioAttributes, flags);
+ dest.writeInt(mCaptureSession);
+ dest.writeInt(mMaxAudioLengthSeconds);
+ dest.writeInt(mIgnoreBeginningFrames);
+ }
+
+ private RecognitionRequest(Parcel in) {
+ mAudioFormat = in.readParcelable(AudioFormat.class.getClassLoader());
+ mAudioAttributes = in.readParcelable(AudioAttributes.class.getClassLoader());
+ mCaptureSession = in.readInt();
+ mMaxAudioLengthSeconds = in.readInt();
+ mIgnoreBeginningFrames = in.readInt();
+ }
+
+ @NonNull public static final Creator<RecognitionRequest> CREATOR =
+ new Creator<RecognitionRequest>() {
+
+ @Override
+ public RecognitionRequest createFromParcel(Parcel p) {
+ return new RecognitionRequest(p);
+ }
+
+ @Override
+ public RecognitionRequest[] newArray(int size) {
+ return new RecognitionRequest[size];
+ }
+ };
+}
diff --git a/non-updatable-api/system-current.txt b/non-updatable-api/system-current.txt
index 024f4ac..374f361 100644
--- a/non-updatable-api/system-current.txt
+++ b/non-updatable-api/system-current.txt
@@ -38,6 +38,7 @@
field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE";
field public static final String BIND_IMS_SERVICE = "android.permission.BIND_IMS_SERVICE";
field public static final String BIND_KEYGUARD_APPWIDGET = "android.permission.BIND_KEYGUARD_APPWIDGET";
+ field public static final String BIND_MUSIC_RECOGNITION_SERVICE = "android.permission.BIND_MUSIC_RECOGNITION_SERVICE";
field public static final String BIND_NETWORK_RECOMMENDATION_SERVICE = "android.permission.BIND_NETWORK_RECOMMENDATION_SERVICE";
field public static final String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE";
field public static final String BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE = "android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE";
@@ -119,6 +120,7 @@
field public static final String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING";
field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
field public static final String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS";
+ field public static final String MANAGE_MUSIC_RECOGNITION = "android.permission.MANAGE_MUSIC_RECOGNITION";
field public static final String MANAGE_ONE_TIME_PERMISSION_SESSIONS = "android.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS";
field public static final String MANAGE_ROLE_HOLDERS = "android.permission.MANAGE_ROLE_HOLDERS";
field public static final String MANAGE_ROLLBACKS = "android.permission.MANAGE_ROLLBACKS";
@@ -4468,6 +4470,58 @@
}
+package android.media.musicrecognition {
+
+ public class MusicRecognitionManager {
+ method @RequiresPermission(android.Manifest.permission.MANAGE_MUSIC_RECOGNITION) public void beginStreamingSearch(@NonNull android.media.musicrecognition.RecognitionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.media.musicrecognition.MusicRecognitionManager.RecognitionCallback);
+ field public static final int RECOGNITION_FAILED_AUDIO_UNAVAILABLE = 7; // 0x7
+ field public static final int RECOGNITION_FAILED_NOT_FOUND = 1; // 0x1
+ field public static final int RECOGNITION_FAILED_NO_CONNECTIVITY = 2; // 0x2
+ field public static final int RECOGNITION_FAILED_SERVICE_KILLED = 5; // 0x5
+ field public static final int RECOGNITION_FAILED_SERVICE_UNAVAILABLE = 3; // 0x3
+ field public static final int RECOGNITION_FAILED_TIMEOUT = 6; // 0x6
+ field public static final int RECOGNITION_FAILED_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static interface MusicRecognitionManager.RecognitionCallback {
+ method public void onAudioStreamClosed();
+ method public void onRecognitionFailed(@NonNull android.media.musicrecognition.RecognitionRequest, int);
+ method public void onRecognitionSucceeded(@NonNull android.media.musicrecognition.RecognitionRequest, @NonNull android.media.MediaMetadata, @Nullable android.os.Bundle);
+ }
+
+ public abstract class MusicRecognitionService extends android.app.Service {
+ ctor public MusicRecognitionService();
+ method public abstract void onRecognize(@NonNull android.os.ParcelFileDescriptor, @NonNull android.media.AudioFormat, @NonNull android.media.musicrecognition.MusicRecognitionService.Callback);
+ }
+
+ public static interface MusicRecognitionService.Callback {
+ method public void onRecognitionFailed(int);
+ method public void onRecognitionSucceeded(@NonNull android.media.MediaMetadata, @Nullable android.os.Bundle);
+ }
+
+ public final class RecognitionRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.media.AudioAttributes getAudioAttributes();
+ method @NonNull public android.media.AudioFormat getAudioFormat();
+ method public int getCaptureSession();
+ method public int getIgnoreBeginningFrames();
+ method public int getMaxAudioLengthSeconds();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.musicrecognition.RecognitionRequest> CREATOR;
+ }
+
+ public static final class RecognitionRequest.Builder {
+ ctor public RecognitionRequest.Builder();
+ method @NonNull public android.media.musicrecognition.RecognitionRequest build();
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setAudioAttributes(@NonNull android.media.AudioAttributes);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setAudioFormat(@NonNull android.media.AudioFormat);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setCaptureSession(int);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setIgnoreBeginningFrames(int);
+ method @NonNull public android.media.musicrecognition.RecognitionRequest.Builder setMaxAudioLengthSeconds(int);
+ }
+
+}
+
package android.media.session {
public final class MediaSessionManager {
diff --git a/services/Android.bp b/services/Android.bp
index ef52c2a..d3577ef 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -28,6 +28,7 @@
":services.coverage-sources",
":services.devicepolicy-sources",
":services.midi-sources",
+ ":services.musicsearch-sources",
":services.net-sources",
":services.print-sources",
":services.profcollect-sources",
@@ -71,6 +72,7 @@
"services.coverage",
"services.devicepolicy",
"services.midi",
+ "services.musicsearch",
"services.net",
"services.people",
"services.print",
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 97ae505..0b35ba4 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -278,6 +278,8 @@
"com.android.server.autofill.AutofillManagerService";
private static final String CONTENT_CAPTURE_MANAGER_SERVICE_CLASS =
"com.android.server.contentcapture.ContentCaptureManagerService";
+ private static final String MUSIC_RECOGNITION_MANAGER_SERVICE_CLASS =
+ "com.android.server.musicrecognition.MusicRecognitionManagerService";
private static final String SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS =
"com.android.server.systemcaptions.SystemCaptionsManagerService";
private static final String TIME_ZONE_RULES_MANAGER_SERVICE_CLASS =
@@ -1402,6 +1404,17 @@
t.traceEnd();
}
+ if (deviceHasConfigString(context,
+ R.string.config_defaultMusicRecognitionService)) {
+ t.traceBegin("StartMusicRecognitionManagerService");
+ mSystemServiceManager.startService(MUSIC_RECOGNITION_MANAGER_SERVICE_CLASS);
+ t.traceEnd();
+ } else {
+ Slog.d(TAG,
+ "MusicRecognitionManagerService not defined by OEM or disabled by flag");
+ }
+
+
startContentCaptureService(context, t);
startAttentionService(context, t);
diff --git a/services/musicrecognition/Android.bp b/services/musicrecognition/Android.bp
new file mode 100644
index 0000000..39b5bb6
--- /dev/null
+++ b/services/musicrecognition/Android.bp
@@ -0,0 +1,13 @@
+filegroup {
+ name: "services.musicsearch-sources",
+ srcs: ["java/**/*.java"],
+ path: "java",
+ visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+ name: "services.musicsearch",
+ defaults: ["services_defaults"],
+ srcs: [":services.musicsearch-sources"],
+ libs: ["services.core", "app-compat-annotations"],
+}
\ No newline at end of file
diff --git a/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerPerUserService.java b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerPerUserService.java
new file mode 100644
index 0000000..e258ef0
--- /dev/null
+++ b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerPerUserService.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.musicrecognition;
+
+import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_KILLED;
+import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_UNAVAILABLE;
+import static android.media.musicrecognition.MusicRecognitionManager.RecognitionFailureCode;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.media.AudioRecord;
+import android.media.MediaMetadata;
+import android.media.musicrecognition.IMusicRecognitionManagerCallback;
+import android.media.musicrecognition.IMusicRecognitionServiceCallback;
+import android.media.musicrecognition.RecognitionRequest;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.infra.AbstractPerUserSystemService;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Handles per-user requests received by
+ * {@link MusicRecognitionManagerService}. Opens an audio stream from the
+ * dsp and writes it into a pipe to {@link RemoteMusicRecognitionService}.
+ */
+public final class MusicRecognitionManagerPerUserService extends
+ AbstractPerUserSystemService<MusicRecognitionManagerPerUserService,
+ MusicRecognitionManagerService>
+ implements RemoteMusicRecognitionService.Callbacks {
+
+ private static final String TAG = MusicRecognitionManagerPerUserService.class.getSimpleName();
+ // Number of bytes per sample of audio (which is a short).
+ private static final int BYTES_PER_SAMPLE = 2;
+ private static final int MAX_STREAMING_SECONDS = 24;
+
+ @Nullable
+ @GuardedBy("mLock")
+ private RemoteMusicRecognitionService mRemoteService;
+
+ private MusicRecognitionServiceCallback mRemoteServiceCallback =
+ new MusicRecognitionServiceCallback();
+ private IMusicRecognitionManagerCallback mCallback;
+
+ MusicRecognitionManagerPerUserService(
+ @NonNull MusicRecognitionManagerService primary,
+ @NonNull Object lock, int userId) {
+ super(primary, lock, userId);
+ }
+
+ @NonNull
+ @GuardedBy("mLock")
+ @Override
+ protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
+ throws PackageManager.NameNotFoundException {
+ ServiceInfo si;
+ try {
+ si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
+ PackageManager.GET_META_DATA, mUserId);
+ } catch (RemoteException e) {
+ throw new PackageManager.NameNotFoundException(
+ "Could not get service for " + serviceComponent);
+ }
+ if (!Manifest.permission.BIND_MUSIC_RECOGNITION_SERVICE.equals(si.permission)) {
+ Slog.w(TAG, "MusicRecognitionService from '" + si.packageName
+ + "' does not require permission "
+ + Manifest.permission.BIND_MUSIC_RECOGNITION_SERVICE);
+ throw new SecurityException("Service does not require permission "
+ + Manifest.permission.BIND_MUSIC_RECOGNITION_SERVICE);
+ }
+ // TODO(b/158194857): check process which owns the service has RECORD_AUDIO permission. How?
+ return si;
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private RemoteMusicRecognitionService ensureRemoteServiceLocked() {
+ if (mRemoteService == null) {
+ final String serviceName = getComponentNameLocked();
+ if (serviceName == null) {
+ if (mMaster.verbose) {
+ Slog.v(TAG, "ensureRemoteServiceLocked(): not set");
+ }
+ return null;
+ }
+ ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
+
+ mRemoteService = new RemoteMusicRecognitionService(getContext(),
+ serviceComponent, mUserId, this,
+ mRemoteServiceCallback, mMaster.isBindInstantServiceAllowed(), mMaster.verbose);
+ }
+
+ return mRemoteService;
+ }
+
+ /**
+ * Read audio from the given capture session using an AudioRecord and writes it to a
+ * ParcelFileDescriptor.
+ */
+ @GuardedBy("mLock")
+ public void beginRecognitionLocked(
+ @NonNull RecognitionRequest recognitionRequest,
+ @NonNull IBinder callback) {
+ int maxAudioLengthSeconds = Math.min(recognitionRequest.getMaxAudioLengthSeconds(),
+ MAX_STREAMING_SECONDS);
+ mCallback = IMusicRecognitionManagerCallback.Stub.asInterface(callback);
+ AudioRecord audioRecord = createAudioRecord(recognitionRequest, maxAudioLengthSeconds);
+
+ mRemoteService = ensureRemoteServiceLocked();
+ if (mRemoteService == null) {
+ try {
+ mCallback.onRecognitionFailed(
+ RECOGNITION_FAILED_SERVICE_UNAVAILABLE);
+ } catch (RemoteException e) {
+ // Ignored.
+ }
+ return;
+ }
+
+ Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe();
+ if (clientPipe == null) {
+ try {
+ mCallback.onAudioStreamClosed();
+ } catch (RemoteException ignored) {
+ // Ignored.
+ }
+ return;
+ }
+ ParcelFileDescriptor audioSink = clientPipe.second;
+ 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) {
+ 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 {
+ mCallback.onAudioStreamClosed();
+ } catch (RemoteException ignored) {
+ // Ignored.
+ }
+
+ }
+ });
+ // Send the pipe down to the lookup service while we write to it asynchronously.
+ mRemoteService.writeAudioToPipe(clientRead, recognitionRequest.getAudioFormat());
+ }
+
+ /**
+ * Callback invoked by {@link android.service.musicrecognition.MusicRecognitionService} to pass
+ * back the music search result.
+ */
+ private final class MusicRecognitionServiceCallback extends
+ IMusicRecognitionServiceCallback.Stub {
+ @Override
+ public void onRecognitionSucceeded(MediaMetadata result, Bundle extras) {
+ try {
+ sanitizeBundle(extras);
+ mCallback.onRecognitionSucceeded(result, extras);
+ } catch (RemoteException ignored) {
+ // Ignored.
+ }
+ }
+
+ @Override
+ public void onRecognitionFailed(@RecognitionFailureCode int failureCode) {
+ try {
+ mCallback.onRecognitionFailed(failureCode);
+ } catch (RemoteException ignored) {
+ // Ignored.
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDied(@NonNull RemoteMusicRecognitionService service) {
+ try {
+ mCallback.onRecognitionFailed(RECOGNITION_FAILED_SERVICE_KILLED);
+ } catch (RemoteException e) {
+ // Ignored.
+ }
+ Slog.w(TAG, "remote service died: " + service);
+ }
+
+ /** Establishes an audio stream from the DSP audio source. */
+ private static AudioRecord createAudioRecord(
+ @NonNull RecognitionRequest recognitionRequest,
+ int maxAudioLengthSeconds) {
+ int sampleRate = recognitionRequest.getAudioFormat().getSampleRate();
+ int bufferSize = getBufferSizeInBytes(sampleRate, maxAudioLengthSeconds);
+ return new AudioRecord(recognitionRequest.getAudioAttributes(),
+ recognitionRequest.getAudioFormat(), bufferSize,
+ recognitionRequest.getCaptureSession());
+ }
+
+ /**
+ * Returns the number of bytes required to store {@code bufferLengthSeconds} of audio sampled at
+ * {@code sampleRate} Hz, using the format returned by DSP audio capture.
+ */
+ private static int getBufferSizeInBytes(int sampleRate, int bufferLengthSeconds) {
+ return BYTES_PER_SAMPLE * sampleRate * bufferLengthSeconds;
+ }
+
+ private static Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() {
+ ParcelFileDescriptor[] fileDescriptors;
+ try {
+ fileDescriptors = ParcelFileDescriptor.createPipe();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to create audio stream pipe", e);
+ return null;
+ }
+
+ if (fileDescriptors.length != 2) {
+ Slog.e(TAG, "Failed to create audio stream pipe, "
+ + "unexpected number of file descriptors");
+ return null;
+ }
+
+ if (!fileDescriptors[0].getFileDescriptor().valid()
+ || !fileDescriptors[1].getFileDescriptor().valid()) {
+ Slog.e(TAG, "Failed to create audio stream pipe, didn't "
+ + "receive a pair of valid file descriptors.");
+ return null;
+ }
+
+ return Pair.create(fileDescriptors[0], fileDescriptors[1]);
+ }
+
+ /** Removes remote objects from the bundle. */
+ private static void sanitizeBundle(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return;
+ }
+
+ for (String key : bundle.keySet()) {
+ Object o = bundle.get(key);
+
+ if (o instanceof Bundle) {
+ sanitizeBundle((Bundle) o);
+ } else if (o instanceof IBinder || o instanceof ParcelFileDescriptor) {
+ bundle.remove(key);
+ }
+ }
+ }
+}
diff --git a/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerService.java b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerService.java
new file mode 100644
index 0000000..b4cb337
--- /dev/null
+++ b/services/musicrecognition/java/com/android/server/musicrecognition/MusicRecognitionManagerService.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.musicrecognition;
+
+import static android.content.PermissionChecker.PERMISSION_GRANTED;
+import static android.media.musicrecognition.MusicRecognitionManager.RECOGNITION_FAILED_SERVICE_UNAVAILABLE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.musicrecognition.IMusicRecognitionManager;
+import android.media.musicrecognition.IMusicRecognitionManagerCallback;
+import android.media.musicrecognition.RecognitionRequest;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.server.infra.AbstractMasterSystemService;
+import com.android.server.infra.FrameworkResourcesServiceNameResolver;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Service which allows a DSP audio event to be securely streamed to a designated {@link
+ * MusicRecognitionService}.
+ */
+public class MusicRecognitionManagerService extends
+ AbstractMasterSystemService<MusicRecognitionManagerService,
+ MusicRecognitionManagerPerUserService> {
+
+ private static final String TAG = MusicRecognitionManagerService.class.getSimpleName();
+
+ private MusicRecognitionManagerStub mMusicRecognitionManagerStub;
+ final ExecutorService mExecutorService = Executors.newCachedThreadPool();
+
+ /**
+ * Initializes the system service.
+ *
+ * Subclasses must define a single argument constructor that accepts the context
+ * and passes it to super.
+ *
+ * @param context The system server context.
+ */
+ public MusicRecognitionManagerService(@NonNull Context context) {
+ super(context, new FrameworkResourcesServiceNameResolver(context,
+ com.android.internal.R.string.config_defaultMusicRecognitionService),
+ /** disallowProperty */null);
+ }
+
+ @Nullable
+ @Override
+ protected MusicRecognitionManagerPerUserService newServiceLocked(int resolvedUserId,
+ boolean disabled) {
+ return new MusicRecognitionManagerPerUserService(this, mLock, resolvedUserId);
+ }
+
+ @Override
+ public void onStart() {
+ mMusicRecognitionManagerStub = new MusicRecognitionManagerStub();
+ publishBinderService(Context.MUSIC_RECOGNITION_SERVICE, mMusicRecognitionManagerStub);
+ }
+
+ private void enforceCaller(String func) {
+ Context ctx = getContext();
+ if (ctx.checkCallingPermission(android.Manifest.permission.MANAGE_MUSIC_RECOGNITION)
+ == PERMISSION_GRANTED) {
+ return;
+ }
+
+ String msg = "Permission Denial: " + func + " from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid()
+ + " doesn't hold " + android.Manifest.permission.MANAGE_MUSIC_RECOGNITION;
+ throw new SecurityException(msg);
+ }
+
+ final class MusicRecognitionManagerStub extends IMusicRecognitionManager.Stub {
+ @Override
+ public void beginRecognition(
+ @NonNull RecognitionRequest recognitionRequest,
+ @NonNull IBinder callback) {
+ enforceCaller("beginRecognition");
+
+ synchronized (mLock) {
+ final MusicRecognitionManagerPerUserService service = getServiceForUserLocked(
+ UserHandle.getCallingUserId());
+ if (service != null) {
+ service.beginRecognitionLocked(recognitionRequest, callback);
+ } else {
+ try {
+ IMusicRecognitionManagerCallback.Stub.asInterface(callback)
+ .onRecognitionFailed(RECOGNITION_FAILED_SERVICE_UNAVAILABLE);
+ } catch (RemoteException e) {
+ // ignored.
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/services/musicrecognition/java/com/android/server/musicrecognition/RemoteMusicRecognitionService.java b/services/musicrecognition/java/com/android/server/musicrecognition/RemoteMusicRecognitionService.java
new file mode 100644
index 0000000..4814a82
--- /dev/null
+++ b/services/musicrecognition/java/com/android/server/musicrecognition/RemoteMusicRecognitionService.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.musicrecognition;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.musicrecognition.IMusicRecognitionService;
+import android.media.musicrecognition.IMusicRecognitionServiceCallback;
+import android.media.musicrecognition.MusicRecognitionService;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.text.format.DateUtils;
+
+import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService;
+
+/** Remote connection to an instance of {@link MusicRecognitionService}. */
+public class RemoteMusicRecognitionService extends
+ AbstractMultiplePendingRequestsRemoteService<RemoteMusicRecognitionService,
+ IMusicRecognitionService> {
+
+ // Maximum time allotted for the remote service to return a result. Up to 24s of audio plus
+ // time to fingerprint and make rpcs.
+ private static final long TIMEOUT_IDLE_BIND_MILLIS = 40 * DateUtils.SECOND_IN_MILLIS;
+
+ // Allows the remote service to send back a result.
+ private final IMusicRecognitionServiceCallback mServerCallback;
+
+ public RemoteMusicRecognitionService(Context context, ComponentName serviceName,
+ int userId, MusicRecognitionManagerPerUserService perUserService,
+ IMusicRecognitionServiceCallback callback,
+ boolean bindInstantServiceAllowed, boolean verbose) {
+ super(context, MusicRecognitionService.ACTION_MUSIC_SEARCH_LOOKUP, serviceName, userId,
+ perUserService,
+ context.getMainThreadHandler(),
+ // Prevents the service from having its permissions stripped while in background.
+ Context.BIND_INCLUDE_CAPABILITIES | (bindInstantServiceAllowed
+ ? Context.BIND_ALLOW_INSTANT : 0), verbose,
+ /* initialCapacity= */ 1);
+ mServerCallback = callback;
+ }
+
+ @NonNull
+ @Override
+ protected IMusicRecognitionService getServiceInterface(@NonNull IBinder service) {
+ return IMusicRecognitionService.Stub.asInterface(service);
+ }
+
+ @Override
+ protected long getTimeoutIdleBindMillis() {
+ return TIMEOUT_IDLE_BIND_MILLIS;
+ }
+
+ /**
+ * Required, but empty since we don't need to notify the callback implementation of the request
+ * results.
+ */
+ interface Callbacks extends VultureCallback<RemoteMusicRecognitionService> {}
+
+ /**
+ * Sends the given descriptor to the app's {@link MusicRecognitionService} to read the
+ * audio.
+ */
+ public void writeAudioToPipe(@NonNull ParcelFileDescriptor fd,
+ @NonNull AudioFormat audioFormat) {
+ scheduleAsyncRequest(
+ binder -> binder.onAudioStreamStarted(fd, audioFormat, mServerCallback));
+ }
+}