Merge "Add system service for music recognition."
This commit is contained in:
committed by
Android (Google) Code Review
commit
4c3ecce019
@@ -38,6 +38,7 @@ package android {
|
||||
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";
|
||||
@@ -120,6 +121,7 @@ package android {
|
||||
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";
|
||||
@@ -4617,6 +4619,58 @@ package android.media.audiopolicy {
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -2035,6 +2035,57 @@ package android.media.audiopolicy {
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -107,6 +107,8 @@ import android.media.MediaRouter;
|
||||
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;
|
||||
@@ -1119,6 +1121,17 @@ public final class SystemServiceRegistry {
|
||||
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
|
||||
|
||||
@@ -4501,6 +4501,14 @@ public abstract class Context {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -3488,6 +3488,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).
|
||||
@@ -4879,6 +4887,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"
|
||||
|
||||
@@ -3660,6 +3660,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.
|
||||
|
||||
@@ -3454,6 +3454,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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -38,6 +38,7 @@ package android {
|
||||
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";
|
||||
@@ -120,6 +121,7 @@ package android {
|
||||
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";
|
||||
@@ -4557,6 +4559,58 @@ package android.media.audiopolicy {
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -35,6 +35,7 @@ filegroup {
|
||||
":services.coverage-sources",
|
||||
":services.devicepolicy-sources",
|
||||
":services.midi-sources",
|
||||
":services.musicsearch-sources",
|
||||
":services.net-sources",
|
||||
":services.print-sources",
|
||||
":services.profcollect-sources",
|
||||
@@ -78,6 +79,7 @@ java_library {
|
||||
"services.coverage",
|
||||
"services.devicepolicy",
|
||||
"services.midi",
|
||||
"services.musicsearch",
|
||||
"services.net",
|
||||
"services.people",
|
||||
"services.print",
|
||||
|
||||
@@ -280,6 +280,8 @@ public final class SystemServer {
|
||||
"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 =
|
||||
@@ -1415,6 +1417,17 @@ public final class SystemServer {
|
||||
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);
|
||||
|
||||
|
||||
13
services/musicrecognition/Android.bp
Normal file
13
services/musicrecognition/Android.bp
Normal file
@@ -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"],
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user