diff options
19 files changed, 1258 insertions, 15 deletions
diff --git a/Android.mk b/Android.mk index 88be12f23a77..427b88848d46 100644 --- a/Android.mk +++ b/Android.mk @@ -402,6 +402,8 @@ LOCAL_SRC_FILES += \ media/java/android/media/IMediaRouterService.aidl \ media/java/android/media/IMediaScannerListener.aidl \ media/java/android/media/IMediaScannerService.aidl \ + media/java/android/media/IPlaybackConfigDispatcher.aidl \ + media/java/android/media/IPlayer.aidl \ media/java/android/media/IRecordingConfigDispatcher.aidl \ media/java/android/media/IRemoteDisplayCallback.aidl \ media/java/android/media/IRemoteDisplayProvider.aidl \ diff --git a/api/current.txt b/api/current.txt index f27cf03187ad..4351cbafd420 100644 --- a/api/current.txt +++ b/api/current.txt @@ -20084,6 +20084,7 @@ package android.media { method public void adjustVolume(int, int); method public void dispatchMediaKeyEvent(android.view.KeyEvent); method public int generateAudioSessionId(); + method public java.util.List<android.media.AudioPlaybackConfiguration> getActivePlaybackConfigurations(); method public java.util.List<android.media.AudioRecordingConfiguration> getActiveRecordingConfigurations(); method public android.media.AudioDeviceInfo[] getDevices(int); method public int getMode(); @@ -20107,6 +20108,7 @@ package android.media { method public void playSoundEffect(int); method public void playSoundEffect(int, float); method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler); + method public void registerAudioPlaybackCallback(android.media.AudioManager.AudioPlaybackCallback, android.os.Handler); method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler); method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName); method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent); @@ -20131,6 +20133,7 @@ package android.media { method public void stopBluetoothSco(); method public void unloadSoundEffects(); method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback); + method public void unregisterAudioPlaybackCallback(android.media.AudioManager.AudioPlaybackCallback); method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback); method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName); method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent); @@ -20229,6 +20232,11 @@ package android.media { field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0 } + public static abstract class AudioManager.AudioPlaybackCallback { + ctor public AudioManager.AudioPlaybackCallback(); + method public void onPlaybackConfigChanged(java.util.List<android.media.AudioPlaybackConfiguration>); + } + public static abstract class AudioManager.AudioRecordingCallback { ctor public AudioManager.AudioRecordingCallback(); method public void onRecordingConfigChanged(java.util.List<android.media.AudioRecordingConfiguration>); @@ -20238,6 +20246,13 @@ package android.media { method public abstract void onAudioFocusChange(int); } + public final class AudioPlaybackConfiguration implements android.os.Parcelable { + method public int describeContents(); + method public android.media.AudioAttributes getAudioAttributes(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.media.AudioPlaybackConfiguration> CREATOR; + } + public class AudioRecord implements android.media.AudioRouting { ctor public AudioRecord(int, int, int, int, int) throws java.lang.IllegalArgumentException; method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler); diff --git a/api/system-current.txt b/api/system-current.txt index 8f91d85d79dc..f0303a0d3b73 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -21590,6 +21590,7 @@ package android.media { method public void adjustVolume(int, int); method public void dispatchMediaKeyEvent(android.view.KeyEvent); method public int generateAudioSessionId(); + method public java.util.List<android.media.AudioPlaybackConfiguration> getActivePlaybackConfigurations(); method public java.util.List<android.media.AudioRecordingConfiguration> getActiveRecordingConfigurations(); method public android.media.AudioDeviceInfo[] getDevices(int); method public int getMode(); @@ -21614,6 +21615,7 @@ package android.media { method public void playSoundEffect(int); method public void playSoundEffect(int, float); method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler); + method public void registerAudioPlaybackCallback(android.media.AudioManager.AudioPlaybackCallback, android.os.Handler); method public int registerAudioPolicy(android.media.audiopolicy.AudioPolicy); method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler); method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName); @@ -21641,6 +21643,7 @@ package android.media { method public void stopBluetoothSco(); method public void unloadSoundEffects(); method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback); + method public void unregisterAudioPlaybackCallback(android.media.AudioManager.AudioPlaybackCallback); method public void unregisterAudioPolicyAsync(android.media.audiopolicy.AudioPolicy); method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback); method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName); @@ -21743,6 +21746,11 @@ package android.media { field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0 } + public static abstract class AudioManager.AudioPlaybackCallback { + ctor public AudioManager.AudioPlaybackCallback(); + method public void onPlaybackConfigChanged(java.util.List<android.media.AudioPlaybackConfiguration>); + } + public static abstract class AudioManager.AudioRecordingCallback { ctor public AudioManager.AudioRecordingCallback(); method public void onRecordingConfigChanged(java.util.List<android.media.AudioRecordingConfiguration>); @@ -21752,6 +21760,28 @@ package android.media { method public abstract void onAudioFocusChange(int); } + public final class AudioPlaybackConfiguration implements android.os.Parcelable { + method public int describeContents(); + method public android.media.AudioAttributes getAudioAttributes(); + method public int getClientPid(); + method public int getClientUid(); + method public int getPlayerState(); + method public int getPlayerType(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.media.AudioPlaybackConfiguration> CREATOR; + field public static final int PLAYER_STATE_IDLE = 1; // 0x1 + field public static final int PLAYER_STATE_PAUSED = 3; // 0x3 + field public static final int PLAYER_STATE_RELEASED = 0; // 0x0 + field public static final int PLAYER_STATE_STARTED = 2; // 0x2 + field public static final int PLAYER_STATE_STOPPED = 4; // 0x4 + field public static final int PLAYER_STATE_UNKNOWN = -1; // 0xffffffff + field public static final int PLAYER_TYPE_JAM_AUDIOTRACK = 1; // 0x1 + field public static final int PLAYER_TYPE_JAM_MEDIAPLAYER = 2; // 0x2 + field public static final int PLAYER_TYPE_JAM_SOUNDPOOL = 3; // 0x3 + field public static final int PLAYER_TYPE_SLES_AUDIOPLAYER = 11; // 0xb + field public static final int PLAYER_TYPE_UNKNOWN = -1; // 0xffffffff + } + public class AudioRecord implements android.media.AudioRouting { ctor public AudioRecord(int, int, int, int, int) throws java.lang.IllegalArgumentException; ctor public AudioRecord(android.media.AudioAttributes, android.media.AudioFormat, int, int) throws java.lang.IllegalArgumentException; diff --git a/api/test-current.txt b/api/test-current.txt index 4f1f147724b2..0ecf4bd5dbc8 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -20171,6 +20171,7 @@ package android.media { method public void adjustVolume(int, int); method public void dispatchMediaKeyEvent(android.view.KeyEvent); method public int generateAudioSessionId(); + method public java.util.List<android.media.AudioPlaybackConfiguration> getActivePlaybackConfigurations(); method public java.util.List<android.media.AudioRecordingConfiguration> getActiveRecordingConfigurations(); method public android.media.AudioDeviceInfo[] getDevices(int); method public int getMode(); @@ -20194,6 +20195,7 @@ package android.media { method public void playSoundEffect(int); method public void playSoundEffect(int, float); method public void registerAudioDeviceCallback(android.media.AudioDeviceCallback, android.os.Handler); + method public void registerAudioPlaybackCallback(android.media.AudioManager.AudioPlaybackCallback, android.os.Handler); method public void registerAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback, android.os.Handler); method public deprecated void registerMediaButtonEventReceiver(android.content.ComponentName); method public deprecated void registerMediaButtonEventReceiver(android.app.PendingIntent); @@ -20218,6 +20220,7 @@ package android.media { method public void stopBluetoothSco(); method public void unloadSoundEffects(); method public void unregisterAudioDeviceCallback(android.media.AudioDeviceCallback); + method public void unregisterAudioPlaybackCallback(android.media.AudioManager.AudioPlaybackCallback); method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback); method public deprecated void unregisterMediaButtonEventReceiver(android.content.ComponentName); method public deprecated void unregisterMediaButtonEventReceiver(android.app.PendingIntent); @@ -20316,6 +20319,11 @@ package android.media { field public static final deprecated int VIBRATE_TYPE_RINGER = 0; // 0x0 } + public static abstract class AudioManager.AudioPlaybackCallback { + ctor public AudioManager.AudioPlaybackCallback(); + method public void onPlaybackConfigChanged(java.util.List<android.media.AudioPlaybackConfiguration>); + } + public static abstract class AudioManager.AudioRecordingCallback { ctor public AudioManager.AudioRecordingCallback(); method public void onRecordingConfigChanged(java.util.List<android.media.AudioRecordingConfiguration>); @@ -20325,6 +20333,13 @@ package android.media { method public abstract void onAudioFocusChange(int); } + public final class AudioPlaybackConfiguration implements android.os.Parcelable { + method public int describeContents(); + method public android.media.AudioAttributes getAudioAttributes(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.media.AudioPlaybackConfiguration> CREATOR; + } + public class AudioRecord implements android.media.AudioRouting { ctor public AudioRecord(int, int, int, int, int) throws java.lang.IllegalArgumentException; method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler); diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 497600212095..664c7ea02181 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -326,6 +326,12 @@ android_media_AudioSystem_newAudioSessionId(JNIEnv *env, jobject thiz) } static jint +android_media_AudioSystem_newAudioPlayerId(JNIEnv *env, jobject thiz) +{ + return AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_PLAYER); +} + +static jint android_media_AudioSystem_setParameters(JNIEnv *env, jobject thiz, jstring keyValuePairs) { const jchar* c_keyValuePairs = env->GetStringCritical(keyValuePairs, 0); @@ -1755,6 +1761,7 @@ static const JNINativeMethod gMethods[] = { {"isStreamActiveRemotely","(II)Z", (void *)android_media_AudioSystem_isStreamActiveRemotely}, {"isSourceActive", "(I)Z", (void *)android_media_AudioSystem_isSourceActive}, {"newAudioSessionId", "()I", (void *)android_media_AudioSystem_newAudioSessionId}, + {"newAudioPlayerId", "()I", (void *)android_media_AudioSystem_newAudioPlayerId}, {"setDeviceConnectionState", "(IILjava/lang/String;Ljava/lang/String;)I", (void *)android_media_AudioSystem_setDeviceConnectionState}, {"getDeviceConnectionState", "(ILjava/lang/String;)I", (void *)android_media_AudioSystem_getDeviceConnectionState}, {"setPhoneState", "(I)I", (void *)android_media_AudioSystem_setPhoneState}, diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 65eadb6e59a7..23bb6f94a1c3 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -67,7 +67,8 @@ public class AudioManager { private long mVolumeKeyUpTime; private final boolean mUseVolumeKeySounds; private final boolean mUseFixedVolume; - private static String TAG = "AudioManager"; + private static final String TAG = "AudioManager"; + private static final boolean DEBUG = false; private static final AudioPortEventHandler sAudioPortEventHandler = new AudioPortEventHandler(); /** @@ -2130,6 +2131,7 @@ public class AudioManager { */ private final static int MSSG_FOCUS_CHANGE = 0; private final static int MSSG_RECORDING_CONFIG_CHANGE = 1; + private final static int MSSG_PLAYBACK_CONFIG_CHANGE = 2; /** * Helper class to handle the forwarding of audio service events to the appropriate listener @@ -2153,7 +2155,7 @@ public class AudioManager { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSSG_FOCUS_CHANGE: + case MSSG_FOCUS_CHANGE: { OnAudioFocusChangeListener listener = null; synchronized(mFocusListenerLock) { listener = findFocusListener((String)msg.obj); @@ -2163,14 +2165,24 @@ public class AudioManager { + msg.arg1 + ") for " + msg.obj); listener.onAudioFocusChange(msg.arg1); } - break; - case MSSG_RECORDING_CONFIG_CHANGE: + } break; + case MSSG_RECORDING_CONFIG_CHANGE: { final RecordConfigChangeCallbackData cbData = (RecordConfigChangeCallbackData) msg.obj; if (cbData.mCb != null) { cbData.mCb.onRecordingConfigChanged(cbData.mConfigs); } - break; + } break; + case MSSG_PLAYBACK_CONFIG_CHANGE: { + final PlaybackConfigChangeCallbackData cbData = + (PlaybackConfigChangeCallbackData) msg.obj; + if (cbData.mCb != null) { + if (DEBUG) { + Log.d(TAG, "dispatching onPlaybackConfigChanged()"); + } + cbData.mCb.onPlaybackConfigChanged(cbData.mConfigs); + } + } break; default: Log.e(TAG, "Unknown event " + msg.what); } @@ -2740,9 +2752,193 @@ public class AudioManager { } } + //==================================================================== + // Notification of playback activity & playback configuration + /** + * Interface for receiving update notifications about the playback activity on the system. + * Extend this abstract class and register it with + * {@link AudioManager#registerAudioPlaybackCallback(AudioPlaybackCallback, Handler)} + * to be notified. + * Use {@link AudioManager#getActivePlaybackConfigurations()} to query the current + * configuration. + * @see AudioPlaybackConfiguration + */ + public static abstract class AudioPlaybackCallback { + /** + * Called whenever the playback activity and configuration has changed. + * @param configs list containing the results of + * {@link AudioManager#getActivePlaybackConfigurations()}. + */ + public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {} + } + + private static class AudioPlaybackCallbackInfo { + final AudioPlaybackCallback mCb; + final Handler mHandler; + AudioPlaybackCallbackInfo(AudioPlaybackCallback cb, Handler handler) { + mCb = cb; + mHandler = handler; + } + } + + private final static class PlaybackConfigChangeCallbackData { + final AudioPlaybackCallback mCb; + final List<AudioPlaybackConfiguration> mConfigs; + + PlaybackConfigChangeCallbackData(AudioPlaybackCallback cb, + List<AudioPlaybackConfiguration> configs) { + mCb = cb; + mConfigs = configs; + } + } + + /** + * Register a callback to be notified of audio playback changes through + * {@link AudioPlaybackCallback} + * @param cb non-null callback to register + * @param handler the {@link Handler} object for the thread on which to execute + * the callback. If <code>null</code>, the {@link Handler} associated with the main + * {@link Looper} will be used. + */ + public void registerAudioPlaybackCallback(@NonNull AudioPlaybackCallback cb, Handler handler) + { + if (cb == null) { + throw new IllegalArgumentException("Illegal null AudioPlaybackCallback argument"); + } + + synchronized(mPlaybackCallbackLock) { + // lazy initialization of the list of playback callbacks + if (mPlaybackCallbackList == null) { + mPlaybackCallbackList = new ArrayList<AudioPlaybackCallbackInfo>(); + } + final int oldCbCount = mPlaybackCallbackList.size(); + if (!hasPlaybackCallback_sync(cb)) { + mPlaybackCallbackList.add(new AudioPlaybackCallbackInfo(cb, + new ServiceEventHandlerDelegate(handler).getHandler())); + final int newCbCount = mPlaybackCallbackList.size(); + if ((oldCbCount == 0) && (newCbCount > 0)) { + // register binder for callbacks + try { + getService().registerPlaybackCallback(mPlayCb); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } else { + Log.w(TAG, "attempt to call registerAudioPlaybackCallback() on a previously" + + "registered callback"); + } + } + } + + /** + * Unregister an audio playback callback previously registered with + * {@link #registerAudioPlaybackCallback(AudioPlaybackCallback, Handler)}. + * @param cb non-null callback to unregister + */ + public void unregisterAudioPlaybackCallback(@NonNull AudioPlaybackCallback cb) { + if (cb == null) { + throw new IllegalArgumentException("Illegal null AudioPlaybackCallback argument"); + } + synchronized(mPlaybackCallbackLock) { + if (mPlaybackCallbackList == null) { + Log.w(TAG, "attempt to call unregisterAudioPlaybackCallback() on a callback" + + " that was never registered"); + return; + } + final int oldCbCount = mPlaybackCallbackList.size(); + if (removePlaybackCallback_sync(cb)) { + final int newCbCount = mPlaybackCallbackList.size(); + if ((oldCbCount > 0) && (newCbCount == 0)) { + // unregister binder for callbacks + try { + getService().unregisterPlaybackCallback(mPlayCb); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } else { + Log.w(TAG, "attempt to call unregisterAudioPlaybackCallback() on a callback" + + " already unregistered or never registered"); + } + } + } + + /** + * Returns the current active audio playback configurations of the device + * @return a non-null list of playback configurations. An empty list indicates there is no + * playback active when queried. + * @see AudioPlaybackConfiguration + */ + public @NonNull List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() { + final IAudioService service = getService(); + try { + return service.getActivePlaybackConfigurations(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * All operations on this list are sync'd on mPlaybackCallbackLock. + * List is lazy-initialized in + * {@link #registerAudioPlaybackCallback(AudioPlaybackCallback, Handler)}. + * List can be null. + */ + private List<AudioPlaybackCallbackInfo> mPlaybackCallbackList; + private final Object mPlaybackCallbackLock = new Object(); + + /** + * Must be called synchronized on mPlaybackCallbackLock + */ + private boolean hasPlaybackCallback_sync(@NonNull AudioPlaybackCallback cb) { + if (mPlaybackCallbackList != null) { + for (int i=0 ; i < mPlaybackCallbackList.size() ; i++) { + if (cb.equals(mPlaybackCallbackList.get(i).mCb)) { + return true; + } + } + } + return false; + } + + /** + * Must be called synchronized on mPlaybackCallbackLock + */ + private boolean removePlaybackCallback_sync(@NonNull AudioPlaybackCallback cb) { + if (mPlaybackCallbackList != null) { + for (int i=0 ; i < mPlaybackCallbackList.size() ; i++) { + if (cb.equals(mPlaybackCallbackList.get(i).mCb)) { + mPlaybackCallbackList.remove(i); + return true; + } + } + } + return false; + } + + private final IPlaybackConfigDispatcher mPlayCb = new IPlaybackConfigDispatcher.Stub() { + + public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs) { + synchronized(mPlaybackCallbackLock) { + if (mPlaybackCallbackList != null) { + for (int i=0 ; i < mPlaybackCallbackList.size() ; i++) { + final AudioPlaybackCallbackInfo arci = mPlaybackCallbackList.get(i); + if (arci.mHandler != null) { + final Message m = arci.mHandler.obtainMessage( + MSSG_PLAYBACK_CONFIG_CHANGE/*what*/, + new PlaybackConfigChangeCallbackData(arci.mCb, configs)/*obj*/); + arci.mHandler.sendMessage(m); + } + } + } + } + } + + }; //==================================================================== - // Recording configuration + // Notification of recording activity & recording configuration /** * Interface for receiving update notifications about the recording configuration. Extend * this abstract class and register it with diff --git a/media/java/android/media/AudioPlaybackConfiguration.aidl b/media/java/android/media/AudioPlaybackConfiguration.aidl new file mode 100644 index 000000000000..122fad059abe --- /dev/null +++ b/media/java/android/media/AudioPlaybackConfiguration.aidl @@ -0,0 +1,18 @@ +/* Copyright 2016, 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; + +parcelable AudioPlaybackConfiguration; diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java new file mode 100644 index 000000000000..3382cd95163c --- /dev/null +++ b/media/java/android/media/AudioPlaybackConfiguration.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2016 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; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Binder; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * The AudioPlaybackConfiguration class collects the information describing an audio playback + * session. + */ +public final class AudioPlaybackConfiguration implements Parcelable { + private final static String TAG = new String("AudioPlaybackConfiguration"); + + // information about the implementation + /** + * @hide + * An unknown type of player + */ + @SystemApi + public final static int PLAYER_TYPE_UNKNOWN = -1; + /** + * @hide + * Player backed by a java android.media.AudioTrack player + */ + @SystemApi + public final static int PLAYER_TYPE_JAM_AUDIOTRACK = 1; + /** + * @hide + * Player backed by a java android.media.MediaPlayer player + */ + @SystemApi + public final static int PLAYER_TYPE_JAM_MEDIAPLAYER = 2; + /** + * @hide + * Player backed by a java android.media.SoundPool player + */ + @SystemApi + public final static int PLAYER_TYPE_JAM_SOUNDPOOL = 3; + /** + * @hide + * Player backed by a C OpenSL ES AudioPlayer player + */ + @SystemApi + public final static int PLAYER_TYPE_SLES_AUDIOPLAYER = 11; + + /** @hide */ + @IntDef({ + PLAYER_TYPE_UNKNOWN, + PLAYER_TYPE_JAM_AUDIOTRACK, + PLAYER_TYPE_JAM_MEDIAPLAYER, + PLAYER_TYPE_JAM_SOUNDPOOL, + PLAYER_TYPE_SLES_AUDIOPLAYER + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PlayerType {} + + /** + * @hide + * An unknown player state + */ + @SystemApi + public static final int PLAYER_STATE_UNKNOWN = -1; + /** + * @hide + * The resources of the player have been released, it cannot play anymore + */ + @SystemApi + public static final int PLAYER_STATE_RELEASED = 0; + /** + * @hide + * The state of a player when it's created + */ + @SystemApi + public static final int PLAYER_STATE_IDLE = 1; + /** + * @hide + * The state of a player that is actively playing + */ + @SystemApi + public static final int PLAYER_STATE_STARTED = 2; + /** + * @hide + * The state of a player where playback is paused + */ + @SystemApi + public static final int PLAYER_STATE_PAUSED = 3; + /** + * @hide + * The state of a player where playback is stopped + */ + @SystemApi + public static final int PLAYER_STATE_STOPPED = 4; + + /** @hide */ + @IntDef({ + PLAYER_STATE_UNKNOWN, + PLAYER_STATE_RELEASED, + PLAYER_STATE_IDLE, + PLAYER_STATE_STARTED, + PLAYER_STATE_PAUSED, + PLAYER_STATE_STOPPED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PlayerState {} + + // immutable data + private final int mPlayerIId; + + // not final due to anonymization step + private int mPlayerType; + private int mClientUid; + private int mClientPid; + + private int mPlayerState; + private AudioAttributes mPlayerAttr; // never null + + /** + * Never use without initializing parameters afterwards + */ + private AudioPlaybackConfiguration(int piid) { + mPlayerIId = piid; + } + + /** + * @hide + */ + public AudioPlaybackConfiguration(PlayerBase.PlayerIdCard pic) { + mPlayerIId = pic.mPIId; + mPlayerType = pic.mPlayerType; + mClientUid = pic.mClientUid; + mClientPid = pic.mClientPid; + mPlayerState = PLAYER_STATE_IDLE; + mPlayerAttr = pic.mAttributes; + } + + // Note that this method is called server side, so no "privileged" information is ever sent + // to a client that is not supposed to have access to it. + /** + * @hide + * Creates a copy of the playback configuration that is stripped of any data enabling + * identification of which application it is associated with ("anonymized"). + * @param toSanitize + */ + public static AudioPlaybackConfiguration anonymizedCopy(AudioPlaybackConfiguration in) { + final AudioPlaybackConfiguration anonymCopy = new AudioPlaybackConfiguration(in.mPlayerIId); + anonymCopy.mPlayerState = in.mPlayerState; + // do not reuse the full attributes: only usage, content type and public flags are allowed + anonymCopy.mPlayerAttr = new AudioAttributes.Builder() + .setUsage(in.mPlayerAttr.getUsage()) + .setContentType(in.mPlayerAttr.getContentType()) + .setFlags(in.mPlayerAttr.getFlags()) + .build(); + // anonymized data + anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN; + anonymCopy.mClientUid = 0; + anonymCopy.mClientPid = 0; + return anonymCopy; + } + + /** + * Return the {@link AudioAttributes} of the corresponding player. + * @return the audio attributes of the player + */ + public AudioAttributes getAudioAttributes() { + return mPlayerAttr; + } + + /** + * @hide + * Return the uid of the client application that created this player. + * @return the uid of the client + */ + @SystemApi + public int getClientUid() { + return mClientUid; + } + + /** + * @hide + * Return the pid of the client application that created this player. + * @return the pid of the client + */ + @SystemApi + public int getClientPid() { + return mClientPid; + } + + /** + * @hide + * Return the type of player linked to this configuration. The return value is one of + * {@link #PLAYER_TYPE_JAM_AUDIOTRACK}, {@link #PLAYER_TYPE_JAM_MEDIAPLAYER}, + * {@link #PLAYER_TYPE_JAM_SOUNDPOOL}, {@link #PLAYER_TYPE_SLES_AUDIOPLAYER}, + * or {@link #PLAYER_TYPE_UNKNOWN}. + * @return the type of the player. + */ + @SystemApi + public @PlayerType int getPlayerType() { + return mPlayerType; + } + + /** + * @hide + * Return the current state of the player linked to this configuration. The return value is one + * of {@link #PLAYER_STATE_IDLE}, {@link #PLAYER_STATE_PAUSED}, {@link #PLAYER_STATE_STARTED}, + * {@link #PLAYER_STATE_STOPPED}, {@link #PLAYER_STATE_RELEASED} or + * {@link #PLAYER_STATE_UNKNOWN}. + * @return the state of the player. + */ + @SystemApi + public @PlayerState int getPlayerState() { + return mPlayerState; + } + + /** + * @hide + * Handle a change of audio attributes + * @param attr + */ + public boolean handleAudioAttributesEvent(@NonNull AudioAttributes attr) { + final boolean changed = !attr.equals(mPlayerAttr); + mPlayerAttr = attr; + return changed; + } + + /** + * @hide + * Handle a player state change + * @param event + * @return true if the state changed, false otherwise + */ + public boolean handleStateEvent(int event) { + final boolean changed = (mPlayerState != event); + mPlayerState = event; + return changed; + } + + /** + * @hide + * Returns true if the player is considered "active", i.e. actively playing, and thus + * in a state that should make it considered for the list public (sanitized) active playback + * configurations + * @return true if active + */ + public boolean isActive() { + switch (mPlayerState) { + case PLAYER_STATE_STARTED: + return true; + case PLAYER_STATE_UNKNOWN: + case PLAYER_STATE_RELEASED: + case PLAYER_STATE_IDLE: + case PLAYER_STATE_PAUSED: + case PLAYER_STATE_STOPPED: + default: + return false; + } + } + + /** + * @hide + * For AudioService dump + * @param pw + */ + public void dump(PrintWriter pw) { + pw.println(" ID:" + mPlayerIId + + " -- type:" + toLogFriendlyPlayerType(mPlayerType) + + " -- u/pid:" + mClientUid +"/" + mClientPid + + " -- state:" + toLogFriendlyPlayerState(mPlayerState) + + " -- attr:" + mPlayerAttr); + } + + public static final Parcelable.Creator<AudioPlaybackConfiguration> CREATOR + = new Parcelable.Creator<AudioPlaybackConfiguration>() { + /** + * Rebuilds an AudioPlaybackConfiguration previously stored with writeToParcel(). + * @param p Parcel object to read the AudioPlaybackConfiguration from + * @return a new AudioPlaybackConfiguration created from the data in the parcel + */ + public AudioPlaybackConfiguration createFromParcel(Parcel p) { + return new AudioPlaybackConfiguration(p); + } + public AudioPlaybackConfiguration[] newArray(int size) { + return new AudioPlaybackConfiguration[size]; + } + }; + + @Override + public int hashCode() { + return Objects.hash(mPlayerIId, mPlayerType, mClientUid, mClientPid); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPlayerIId); + dest.writeInt(mPlayerType); + dest.writeInt(mClientUid); + dest.writeInt(mClientPid); + dest.writeInt(mPlayerState); + mPlayerAttr.writeToParcel(dest, 0); + } + + private AudioPlaybackConfiguration(Parcel in) { + mPlayerIId = in.readInt(); + mPlayerType = in.readInt(); + mClientUid = in.readInt(); + mClientPid = in.readInt(); + mPlayerState = in.readInt(); + mPlayerAttr = AudioAttributes.CREATOR.createFromParcel(in); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof AudioPlaybackConfiguration)) return false; + + AudioPlaybackConfiguration that = (AudioPlaybackConfiguration) o; + + return ((mPlayerIId == that.mPlayerIId) + && (mPlayerType == that.mPlayerType) + && (mClientUid == that.mClientUid) + && (mClientPid == that.mClientPid)); + } + + //===================================================================== + // Utilities + + /** @hide */ + public static String toLogFriendlyPlayerType(int type) { + switch (type) { + case PLAYER_TYPE_UNKNOWN: return "unknown"; + case PLAYER_TYPE_JAM_AUDIOTRACK: return "android.media.AudioTrack"; + case PLAYER_TYPE_JAM_MEDIAPLAYER: return "android.media.MediaPlayer"; + case PLAYER_TYPE_JAM_SOUNDPOOL: return "android.media.SoundPool"; + case PLAYER_TYPE_SLES_AUDIOPLAYER: return "OpenSL ES AudioPlayer"; + default: + return "unknown player type - FIXME"; + } + } + + /** @hide */ + public static String toLogFriendlyPlayerState(int state) { + switch (state) { + case PLAYER_STATE_UNKNOWN: return "unknown"; + case PLAYER_STATE_RELEASED: return "released"; + case PLAYER_STATE_IDLE: return "idle"; + case PLAYER_STATE_STARTED: return "started"; + case PLAYER_STATE_PAUSED: return "paused"; + case PLAYER_STATE_STOPPED: return "stopped"; + default: + return "unknown player state - FIXME"; + } + } +} diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 28c7253f8c76..179289952e65 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -161,6 +161,12 @@ public class AudioSystem public static native int newAudioSessionId(); /* + * Returns a new unused audio player ID + */ + public static native int newAudioPlayerId(); + + + /* * Sets a group generic audio configuration parameters. The use of these parameters * are platform dependent, see libaudio * diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 16b331560d2a..464cbdb985a4 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -471,7 +471,7 @@ public class AudioTrack extends PlayerBase public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId) throws IllegalArgumentException { - super(attributes); + super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK); // mState already == STATE_UNINITIALIZED if (format == null) { @@ -551,7 +551,8 @@ public class AudioTrack extends PlayerBase * OpenSLES interface is realized. */ /*package*/ AudioTrack(long nativeTrackInJavaObj) { - super(new AudioAttributes.Builder().build()); + super(new AudioAttributes.Builder().build(), + AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK); // "final"s mNativeTrackInJavaObj = 0; mJniData = 0; @@ -1749,8 +1750,8 @@ public class AudioTrack extends PlayerBase if (mState != STATE_INITIALIZED) { throw new IllegalStateException("play() called on uninitialized AudioTrack."); } - baseStart(); synchronized(mPlayStateLock) { + baseStart(); native_start(); mPlayState = PLAYSTATE_PLAYING; } @@ -1773,6 +1774,7 @@ public class AudioTrack extends PlayerBase // stop playing synchronized(mPlayStateLock) { native_stop(); + baseStop(); mPlayState = PLAYSTATE_STOPPED; mAvSyncHeader = null; mAvSyncBytesRemaining = 0; @@ -1791,11 +1793,11 @@ public class AudioTrack extends PlayerBase if (mState != STATE_INITIALIZED) { throw new IllegalStateException("pause() called on uninitialized AudioTrack."); } - //logd("pause()"); // pause playback synchronized(mPlayStateLock) { native_pause(); + basePause(); mPlayState = PLAYSTATE_PAUSED; } } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index c7931fcd297b..151b4d58fd65 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -20,13 +20,16 @@ import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.media.AudioAttributes; +import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; import android.media.AudioRoutesInfo; import android.media.IAudioFocusDispatcher; import android.media.IAudioRoutesObserver; +import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.IVolumeController; +import android.media.PlayerBase; import android.media.Rating; import android.media.VolumePolicy; import android.media.audiopolicy.AudioPolicyConfig; @@ -165,4 +168,18 @@ interface IAudioService { oneway void unregisterRecordingCallback(in IRecordingConfigDispatcher rcdb); List<AudioRecordingConfiguration> getActiveRecordingConfigurations(); + + void registerPlaybackCallback(in IPlaybackConfigDispatcher pcdb); + + oneway void unregisterPlaybackCallback(in IPlaybackConfigDispatcher pcdb); + + List<AudioPlaybackConfiguration> getActivePlaybackConfigurations(); + + oneway void trackPlayer(in PlayerBase.PlayerIdCard pic); + + oneway void playerAttributes(in int piid, in AudioAttributes attr); + + oneway void playerEvent(in int piid, in int event); + + oneway void releasePlayer(in int piid); } diff --git a/media/java/android/media/IPlaybackConfigDispatcher.aidl b/media/java/android/media/IPlaybackConfigDispatcher.aidl new file mode 100644 index 000000000000..3cb52161d9bf --- /dev/null +++ b/media/java/android/media/IPlaybackConfigDispatcher.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 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; + +import android.media.AudioPlaybackConfiguration; + +/** + * AIDL for the PlaybackActivityMonitor in AudioService to signal audio playback updates. + * + * {@hide} + */ +oneway interface IPlaybackConfigDispatcher { + + void dispatchPlaybackConfigChange(in List<AudioPlaybackConfiguration> configs); + +} diff --git a/media/java/android/media/IPlayer.aidl b/media/java/android/media/IPlayer.aidl new file mode 100644 index 000000000000..32984f98264c --- /dev/null +++ b/media/java/android/media/IPlayer.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 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; + + +/** + * @hide + */ +interface IPlayer { + oneway void start(); + oneway void pause(); + oneway void stop(); +} diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index 78da59c35754..77a0fbde6eb1 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -626,7 +626,8 @@ public class MediaPlayer extends PlayerBase * result in an exception.</p> */ public MediaPlayer() { - super(new AudioAttributes.Builder().build()); + super(new AudioAttributes.Builder().build(), + AudioPlaybackConfiguration.PLAYER_TYPE_JAM_MEDIAPLAYER); Looper looper; if ((looper = Looper.myLooper()) != null) { @@ -1241,6 +1242,7 @@ public class MediaPlayer extends PlayerBase public void stop() throws IllegalStateException { stayAwake(false); _stop(); + baseStop(); } private native void _stop() throws IllegalStateException; @@ -1254,6 +1256,7 @@ public class MediaPlayer extends PlayerBase public void pause() throws IllegalStateException { stayAwake(false); _pause(); + basePause(); } private native void _pause() throws IllegalStateException; @@ -2973,6 +2976,7 @@ public class MediaPlayer extends PlayerBase case MEDIA_PLAYBACK_COMPLETE: { + mOnCompletionInternalListener.onCompletion(mMediaPlayer); OnCompletionListener onCompletionListener = mOnCompletionListener; if (onCompletionListener != null) onCompletionListener.onCompletion(mMediaPlayer); @@ -3037,6 +3041,7 @@ public class MediaPlayer extends PlayerBase error_was_handled = onErrorListener.onError(mMediaPlayer, msg.arg1, msg.arg2); } { + mOnCompletionInternalListener.onCompletion(mMediaPlayer); OnCompletionListener onCompletionListener = mOnCompletionListener; if (onCompletionListener != null && ! error_was_handled) { onCompletionListener.onCompletion(mMediaPlayer); @@ -3215,6 +3220,17 @@ public class MediaPlayer extends PlayerBase private OnCompletionListener mOnCompletionListener; /** + * @hide + * Internal completion listener to update PlayerBase of the play state. Always "registered". + */ + private final OnCompletionListener mOnCompletionInternalListener = new OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + baseStop(); + } + }; + + /** * Interface definition of a callback to be invoked indicating buffering * status of a media resource being streamed over the network. */ diff --git a/media/java/android/media/PlayerBase.aidl b/media/java/android/media/PlayerBase.aidl new file mode 100644 index 000000000000..4ae2125a0dff --- /dev/null +++ b/media/java/android/media/PlayerBase.aidl @@ -0,0 +1,18 @@ +/* Copyright 2016, 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; + +parcelable PlayerBase.PlayerIdCard; diff --git a/media/java/android/media/PlayerBase.java b/media/java/android/media/PlayerBase.java index 42f6b830126d..49c89a35a0cb 100644 --- a/media/java/android/media/PlayerBase.java +++ b/media/java/android/media/PlayerBase.java @@ -16,13 +16,14 @@ package android.media; -import java.lang.IllegalArgumentException; - import android.annotation.NonNull; import android.app.ActivityThread; import android.app.AppOpsManager; import android.content.Context; +import android.os.Binder; import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -31,6 +32,9 @@ import android.util.Log; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; +import java.lang.IllegalArgumentException; +import java.util.Objects; + /** * Class to encapsulate a number of common player operations: * - AppOps for OP_PLAY_AUDIO @@ -40,6 +44,7 @@ import com.android.internal.app.IAppOpsService; public abstract class PlayerBase { private final static String TAG = "PlayerBase"; + private final static boolean DEBUG = false; private static IAudioService sService; //lazy initialization, use getService() /** Debug app ops */ protected static final boolean DEBUG_APP_OPS = Log.isLoggable(TAG + ".AO", Log.DEBUG); @@ -56,15 +61,24 @@ public abstract class PlayerBase { private boolean mHasAppOpsPlayAudio = true; private final Object mAppOpsLock = new Object(); + private final int mImplType; + // uniquely identifies the Player Interface throughout the system (P I Id) + private final int mPlayerIId; + + private int mState; + /** * Constructor. Must be given audio attributes, as they are required for AppOps. * @param attr non-null audio attributes + * @param class non-null class of the implementation of this abstract class */ - PlayerBase(@NonNull AudioAttributes attr) { + PlayerBase(@NonNull AudioAttributes attr, int implType) { if (attr == null) { throw new IllegalArgumentException("Illegal null AudioAttributes"); } mAttributes = attr; + mImplType = implType; + mPlayerIId = AudioSystem.newAudioPlayerId(); IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE); mAppOps = IAppOpsService.Stub.asInterface(b); // initialize mHasAppOpsPlayAudio @@ -85,6 +99,11 @@ public abstract class PlayerBase { } catch (RemoteException e) { mHasAppOpsPlayAudio = false; } + try { + getService().trackPlayer(new PlayerIdCard(mPlayerIId, mImplType, mAttributes)); + } catch (RemoteException e) { + Log.e(TAG, "Error talking to audio service, player will not be tracked", e); + } } @@ -96,6 +115,11 @@ public abstract class PlayerBase { if (attr == null) { throw new IllegalArgumentException("Illegal null AudioAttributes"); } + try { + getService().playerAttributes(mPlayerIId, attr); + } catch (RemoteException e) { + Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e); + } synchronized (mAppOpsLock) { mAttributes = attr; updateAppOpsPlayAudio_sync(); @@ -103,6 +127,12 @@ public abstract class PlayerBase { } void baseStart() { + if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); } + try { + getService().playerEvent(mPlayerIId, AudioPlaybackConfiguration.PLAYER_STATE_STARTED); + } catch (RemoteException e) { + Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e); + } synchronized (mAppOpsLock) { if (isRestricted_sync()) { playerSetVolume(true/*muting*/,0, 0); @@ -110,6 +140,24 @@ public abstract class PlayerBase { } } + void basePause() { + if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); } + try { + getService().playerEvent(mPlayerIId, AudioPlaybackConfiguration.PLAYER_STATE_PAUSED); + } catch (RemoteException e) { + Log.e(TAG, "Error talking to audio service, PAUSED state will not be tracked", e); + } + } + + void baseStop() { + if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); } + try { + getService().playerEvent(mPlayerIId, AudioPlaybackConfiguration.PLAYER_STATE_STOPPED); + } catch (RemoteException e) { + Log.e(TAG, "Error talking to audio service, STOPPED state will not be tracked", e); + } + } + void baseSetVolume(float leftVolume, float rightVolume) { synchronized (mAppOpsLock) { mLeftVolume = leftVolume; @@ -136,6 +184,15 @@ public abstract class PlayerBase { * Releases AppOps related resources. */ void baseRelease() { + if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId); } + try { + if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) { + getService().releasePlayer(mPlayerIId); + mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED; + } + } catch (RemoteException e) { + Log.e(TAG, "Error talking to audio service, the player will still be tracked", e); + } try { mAppOps.stopWatchingMode(mAppOpsCallback); } catch (RemoteException e) { @@ -227,6 +284,7 @@ public abstract class PlayerBase { return sService; } + //===================================================================== // Abstract methods a subclass needs to implement /** * Abstract method for the subclass behavior's for volume and muting commands @@ -238,6 +296,92 @@ public abstract class PlayerBase { abstract int playerSetAuxEffectSendLevel(boolean muting, float level); //===================================================================== + // Implementation of IPlayer + private final IPlayer mIPlayer = new IPlayer.Stub() { + @Override + public void start() {} + @Override + public void pause() {} + @Override + public void stop() {} + }; + + //===================================================================== + /** + * Class holding all the information about a player that needs to be known at registration time + */ + public static class PlayerIdCard implements Parcelable { + public final int mPIId; + public final int mPlayerType; + public final int mClientUid; + public final int mClientPid; + + public final static int AUDIO_ATTRIBUTES_NONE = 0; + public final static int AUDIO_ATTRIBUTES_DEFINED = 1; + public final AudioAttributes mAttributes; + + PlayerIdCard(int piid, int type, @NonNull AudioAttributes attr) { + mPIId = piid; + mPlayerType = type; + mClientUid = Binder.getCallingUid(); + mClientPid = Binder.getCallingPid(); + mAttributes = attr; + } + + @Override + public int hashCode() { + return Objects.hash(mPIId, mPlayerType); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPIId); + dest.writeInt(mPlayerType); + dest.writeInt(mClientUid); + dest.writeInt(mClientPid); + mAttributes.writeToParcel(dest, 0); + } + + public static final Parcelable.Creator<PlayerIdCard> CREATOR + = new Parcelable.Creator<PlayerIdCard>() { + /** + * Rebuilds an PlayerIdCard previously stored with writeToParcel(). + * @param p Parcel object to read the PlayerIdCard from + * @return a new PlayerIdCard created from the data in the parcel + */ + public PlayerIdCard createFromParcel(Parcel p) { + return new PlayerIdCard(p); + } + public PlayerIdCard[] newArray(int size) { + return new PlayerIdCard[size]; + } + }; + + private PlayerIdCard(Parcel in) { + mPIId = in.readInt(); + mPlayerType = in.readInt(); + mClientUid = in.readInt(); + mClientPid = in.readInt(); + mAttributes = AudioAttributes.CREATOR.createFromParcel(in); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof PlayerIdCard)) return false; + + PlayerIdCard that = (PlayerIdCard) o; + + return (mPIId == that.mPIId); + } + } + + //===================================================================== // Utilities /** diff --git a/media/java/android/media/SoundPool.java b/media/java/android/media/SoundPool.java index 838767ce137e..c985cbd010d4 100644 --- a/media/java/android/media/SoundPool.java +++ b/media/java/android/media/SoundPool.java @@ -151,7 +151,7 @@ public class SoundPool extends PlayerBase { } private SoundPool(int maxStreams, AudioAttributes attributes) { - super(attributes); + super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL); // do native setup if (native_setup(new WeakReference<SoundPool>(this), maxStreams, attributes) != 0) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 67f361441af7..788a28cfac25 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -59,11 +59,13 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioManagerInternal; import android.media.AudioPort; +import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; import android.media.AudioRoutesInfo; import android.media.IAudioFocusDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioService; +import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.IVolumeController; @@ -72,6 +74,7 @@ import android.media.SoundPool; import android.media.VolumePolicy; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; +import android.media.PlayerBase; import android.media.audiopolicy.AudioMix; import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.AudioPolicyConfig; @@ -6023,6 +6026,8 @@ public class AudioService extends IAudioService.Stub pw.print(" mVolumePolicy="); pw.println(mVolumePolicy); dumpAudioPolicies(pw); + + mPlaybackMonitor.dump(pw); } private static String safeMediaVolumeStateToString(Integer state) { @@ -6443,6 +6448,42 @@ public class AudioService extends IAudioService.Stub } //====================== + // Audio policy callbacks from players for playback configuration updates + //====================== + private final PlaybackActivityMonitor mPlaybackMonitor = new PlaybackActivityMonitor(); + + public void registerPlaybackCallback(IPlaybackConfigDispatcher pcdb) { + final boolean isPrivileged = + (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission( + android.Manifest.permission.MODIFY_AUDIO_ROUTING)); + mPlaybackMonitor.registerPlaybackCallback(pcdb, isPrivileged); + } + + public void unregisterPlaybackCallback(IPlaybackConfigDispatcher pcdb) { + mPlaybackMonitor.unregisterPlaybackCallback(pcdb); + } + + public List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() { + return mPlaybackMonitor.getActivePlaybackConfigurations(); + } + + public void trackPlayer(PlayerBase.PlayerIdCard pic) { + mPlaybackMonitor.trackPlayer(pic); + } + + public void playerAttributes(int piid, AudioAttributes attr) { + mPlaybackMonitor.playerAttributes(piid, attr); + } + + public void playerEvent(int piid, int event) { + mPlaybackMonitor.playerEvent(piid, event); + } + + public void releasePlayer(int piid) { + mPlaybackMonitor.releasePlayer(piid); + } + + //====================== // Audio policy proxy //====================== /** diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java new file mode 100644 index 000000000000..b99e4e93a95a --- /dev/null +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2016 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.audio; + +import android.annotation.NonNull; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioPlaybackConfiguration; +import android.media.AudioSystem; +import android.media.IPlaybackConfigDispatcher; +import android.media.MediaRecorder; +import android.media.PlayerBase; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.io.PrintWriter; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +/** + * Class to receive and dispatch updates from AudioSystem about recording configurations. + */ +public final class PlaybackActivityMonitor { + + public final static String TAG = "AudioService.PlaybackActivityMonitor"; + private final static boolean DEBUG = false; + + private ArrayList<PlayMonitorClient> mClients = new ArrayList<PlayMonitorClient>(); + // a public client is one that needs an anonymized version of the playback configurations, we + // keep track of whether there is at least one to know when we need to create the list of + // playback configurations that do not contain uid/pid/package name information. + private boolean mHasPublicClients = false; + + private final Object mPlayerLock = new Object(); + private HashMap<Integer, AudioPlaybackConfiguration> mPlayers = + new HashMap<Integer, AudioPlaybackConfiguration>(); + + PlaybackActivityMonitor() { + PlayMonitorClient.sMonitor = this; + } + + //================================================================= + // Track players and their states + // methods trackPlayer, playerAttributes, playerEvent, releasePlayer are all oneway calls + // into AudioService. They trigger synchronous dispatchPlaybackChange() which updates + // all listeners as oneway calls. + + public void trackPlayer(PlayerBase.PlayerIdCard pic) { + if (DEBUG) { Log.v(TAG, "trackPlayer() for piid=" + pic.mPIId); } + final AudioPlaybackConfiguration apc = new AudioPlaybackConfiguration(pic); + synchronized(mPlayerLock) { + mPlayers.put(pic.mPIId, apc); + } + } + + public void playerAttributes(int piid, @NonNull AudioAttributes attr) { + final boolean change; + synchronized(mPlayerLock) { + final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid)); + if (apc == null) { + Log.e(TAG, "Unknown player " + piid + " for audio attributes change"); + change = false; + } else { + change = apc.handleAudioAttributesEvent(attr); + } + } + if (change) { + dispatchPlaybackChange(); + } + } + + public void playerEvent(int piid, int event) { + if (DEBUG) { Log.v(TAG, String.format("trackPlayer(piid=%d, event=%d)", piid, event)); } + final boolean change; + synchronized(mPlayerLock) { + final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid)); + if (apc == null) { + Log.e(TAG, "Unknown player " + piid + " for event " + event); + change = false; + } else { + //TODO add generation counter to only update to the latest state + change = apc.handleStateEvent(event); + } + } + if (change) { + dispatchPlaybackChange(); + } + } + + public void releasePlayer(int piid) { + if (DEBUG) { Log.v(TAG, "releasePlayer() for piid=" + piid); } + synchronized(mPlayerLock) { + if (!mPlayers.containsKey(new Integer(piid))) { + Log.e(TAG, "Unknown player " + piid + " for release"); + } else { + mPlayers.remove(new Integer(piid)); + } + } + } + + protected void dump(PrintWriter pw) { + pw.println("\nPlaybackActivityMonitor dump time: " + + DateFormat.getTimeInstance().format(new Date())); + synchronized(mPlayerLock) { + for (AudioPlaybackConfiguration conf : mPlayers.values()) { + conf.dump(pw); + } + } + } + + private void dispatchPlaybackChange() { + synchronized (mClients) { + // typical use case, nobody is listening, don't do any work + if (mClients.isEmpty()) { + return; + } + } + if (DEBUG) { Log.v(TAG, "dispatchPlaybackChange to " + mClients.size() + " clients"); } + final List<AudioPlaybackConfiguration> configsSystem; + // list of playback configurations for "public consumption". It is only computed if there + // are non-system playback activity listeners. + final List<AudioPlaybackConfiguration> configsPublic; + synchronized (mPlayerLock) { + if (mPlayers.isEmpty()) { + return; + } + configsSystem = new ArrayList<AudioPlaybackConfiguration>(mPlayers.values()); + } + synchronized (mClients) { + // was done at beginning of method, but could have changed + if (mClients.isEmpty()) { + return; + } + configsPublic = mHasPublicClients ? anonymizeForPublicConsumption(configsSystem) : null; + final Iterator<PlayMonitorClient> clientIterator = mClients.iterator(); + while (clientIterator.hasNext()) { + final PlayMonitorClient pmc = clientIterator.next(); + try { + // do not spam the logs if there are problems communicating with this client + if (pmc.mErrorCount < PlayMonitorClient.MAX_ERRORS) { + if (pmc.mIsPrivileged) { + pmc.mDispatcherCb.dispatchPlaybackConfigChange(configsSystem); + } else { + pmc.mDispatcherCb.dispatchPlaybackConfigChange(configsPublic); + } + } + } catch (RemoteException e) { + pmc.mErrorCount++; + Log.e(TAG, "Error (" + pmc.mErrorCount + + ") trying to dispatch playback config change to " + pmc, e); + } + } + } + } + + private ArrayList<AudioPlaybackConfiguration> anonymizeForPublicConsumption( + List<AudioPlaybackConfiguration> sysConfigs) { + ArrayList<AudioPlaybackConfiguration> publicConfigs = + new ArrayList<AudioPlaybackConfiguration>(); + // only add active anonymized configurations, + for (AudioPlaybackConfiguration config : sysConfigs) { + if (config.isActive()) { + publicConfigs.add(AudioPlaybackConfiguration.anonymizedCopy(config)); + } + } + return publicConfigs; + } + + //================================================================= + // Track playback activity listeners + + void registerPlaybackCallback(IPlaybackConfigDispatcher pcdb, boolean isPrivileged) { + if (pcdb == null) { + return; + } + synchronized(mClients) { + final PlayMonitorClient pmc = new PlayMonitorClient(pcdb, isPrivileged); + if (pmc.init()) { + if (!isPrivileged) { + mHasPublicClients = true; + } + mClients.add(pmc); + } + } + } + + void unregisterPlaybackCallback(IPlaybackConfigDispatcher pcdb) { + if (pcdb == null) { + return; + } + synchronized(mClients) { + final Iterator<PlayMonitorClient> clientIterator = mClients.iterator(); + boolean hasPublicClients = false; + // iterate over the clients to remove the dispatcher to remove, and reevaluate at + // the same time if we still have a public client. + while (clientIterator.hasNext()) { + PlayMonitorClient pmc = clientIterator.next(); + if (pcdb.equals(pmc.mDispatcherCb)) { + pmc.release(); + clientIterator.remove(); + } else { + if (!pmc.mIsPrivileged) { + hasPublicClients = true; + } + } + } + mHasPublicClients = hasPublicClients; + } + } + + List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() { + synchronized(mPlayers) { + return new ArrayList<AudioPlaybackConfiguration>(mPlayers.values()); + } + } + + + /** + * Inner class to track clients that want to be notified of playback updates + */ + private final static class PlayMonitorClient implements IBinder.DeathRecipient { + + // can afford to be static because only one PlaybackActivityMonitor ever instantiated + static PlaybackActivityMonitor sMonitor; + + final IPlaybackConfigDispatcher mDispatcherCb; + final boolean mIsPrivileged; + + int mErrorCount = 0; + // number of errors after which we don't update this client anymore to not spam the logs + static final int MAX_ERRORS = 5; + + PlayMonitorClient(IPlaybackConfigDispatcher pcdb, boolean isPrivileged) { + mDispatcherCb = pcdb; + mIsPrivileged = isPrivileged; + } + + public void binderDied() { + Log.w(TAG, "client died"); + sMonitor.unregisterPlaybackCallback(mDispatcherCb); + } + + boolean init() { + try { + mDispatcherCb.asBinder().linkToDeath(this, 0); + return true; + } catch (RemoteException e) { + Log.w(TAG, "Could not link to client death", e); + return false; + } + } + + void release() { + mDispatcherCb.asBinder().unlinkToDeath(this, 0); + } + } +} |