blob: ee8f1b3eec7728f4c51cb0c4aef0780cbe9ccdb3 [file] [log] [blame]
/*
* 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.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.AppOpsManager;
import android.content.Context;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
import java.lang.ref.WeakReference;
import java.util.Objects;
/**
* Class to encapsulate a number of common player operations:
* - AppOps for OP_PLAY_AUDIO
* - more to come (routing, transport control)
* @hide
*/
public abstract class PlayerBase {
private static final String TAG = "PlayerBase";
/** Debug app ops */
private static final boolean DEBUG_APP_OPS = false;
private static final boolean DEBUG = DEBUG_APP_OPS || false;
private static IAudioService sService; //lazy initialization, use getService()
/** if true, only use OP_PLAY_AUDIO monitoring for logging, and rely on muting to happen
* in AudioFlinger */
private static final boolean USE_AUDIOFLINGER_MUTING_FOR_OP = true;
// parameters of the player that affect AppOps
protected AudioAttributes mAttributes;
// volumes of the subclass "player volumes", as seen by the client of the subclass
// (e.g. what was passed in AudioTrack.setVolume(float)). The actual volume applied is
// the combination of the player volume, and the PlayerBase pan and volume multipliers
protected float mLeftVolume = 1.0f;
protected float mRightVolume = 1.0f;
protected float mAuxEffectSendLevel = 0.0f;
// NEVER call into AudioService (see getService()) with mLock held: PlayerBase can run in
// the same process as AudioService, which can synchronously call back into this class,
// causing deadlocks between the two
private final Object mLock = new Object();
// for AppOps
private @Nullable IAppOpsService mAppOps;
private @Nullable IAppOpsCallback mAppOpsCallback;
@GuardedBy("mLock")
private boolean mHasAppOpsPlayAudio = true;
private final int mImplType;
// uniquely identifies the Player Interface throughout the system (P I Id)
private int mPlayerIId = AudioPlaybackConfiguration.PLAYER_PIID_INVALID;
@GuardedBy("mLock")
private int mState;
@GuardedBy("mLock")
private int mStartDelayMs = 0;
@GuardedBy("mLock")
private float mPanMultiplierL = 1.0f;
@GuardedBy("mLock")
private float mPanMultiplierR = 1.0f;
@GuardedBy("mLock")
private float mVolMultiplier = 1.0f;
/**
* 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, int implType) {
if (attr == null) {
throw new IllegalArgumentException("Illegal null AudioAttributes");
}
mAttributes = attr;
mImplType = implType;
mState = AudioPlaybackConfiguration.PLAYER_STATE_IDLE;
};
/**
* Call from derived class when instantiation / initialization is successful
*/
protected void baseRegisterPlayer() {
if (!USE_AUDIOFLINGER_MUTING_FOR_OP) {
IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE);
mAppOps = IAppOpsService.Stub.asInterface(b);
// initialize mHasAppOpsPlayAudio
updateAppOpsPlayAudio();
// register a callback to monitor whether the OP_PLAY_AUDIO is still allowed
mAppOpsCallback = new IAppOpsCallbackWrapper(this);
try {
mAppOps.startWatchingMode(AppOpsManager.OP_PLAY_AUDIO,
ActivityThread.currentPackageName(), mAppOpsCallback);
} catch (RemoteException e) {
Log.e(TAG, "Error registering appOps callback", e);
mHasAppOpsPlayAudio = false;
}
}
try {
mPlayerIId = getService().trackPlayer(
new PlayerIdCard(mImplType, mAttributes, new IPlayerWrapper(this)));
} catch (RemoteException e) {
Log.e(TAG, "Error talking to audio service, player will not be tracked", e);
}
}
/**
* To be called whenever the audio attributes of the player change
* @param attr non-null audio attributes
*/
void baseUpdateAudioAttributes(@NonNull AudioAttributes attr) {
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 (mLock) {
boolean attributesChanged = (mAttributes != attr);
mAttributes = attr;
updateAppOpsPlayAudio_sync(attributesChanged);
}
}
private void updateState(int state) {
final int piid;
synchronized (mLock) {
mState = state;
piid = mPlayerIId;
}
try {
getService().playerEvent(piid, state);
} catch (RemoteException e) {
Log.e(TAG, "Error talking to audio service, "
+ AudioPlaybackConfiguration.toLogFriendlyPlayerState(state)
+ " state will not be tracked for piid=" + piid, e);
}
}
void baseStart() {
if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); }
updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
synchronized (mLock) {
if (isRestricted_sync()) {
playerSetVolume(true/*muting*/,0, 0);
}
}
}
void baseSetStartDelayMs(int delayMs) {
synchronized(mLock) {
mStartDelayMs = Math.max(delayMs, 0);
}
}
protected int getStartDelayMs() {
synchronized(mLock) {
return mStartDelayMs;
}
}
void basePause() {
if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); }
updateState(AudioPlaybackConfiguration.PLAYER_STATE_PAUSED);
}
void baseStop() {
if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); }
updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED);
}
void baseSetPan(float pan) {
final float p = Math.min(Math.max(-1.0f, pan), 1.0f);
synchronized (mLock) {
if (p >= 0.0f) {
mPanMultiplierL = 1.0f - p;
mPanMultiplierR = 1.0f;
} else {
mPanMultiplierL = 1.0f;
mPanMultiplierR = 1.0f + p;
}
}
updatePlayerVolume();
}
private void updatePlayerVolume() {
final float finalLeftVol, finalRightVol;
final boolean isRestricted;
synchronized (mLock) {
finalLeftVol = mVolMultiplier * mLeftVolume * mPanMultiplierL;
finalRightVol = mVolMultiplier * mRightVolume * mPanMultiplierR;
isRestricted = isRestricted_sync();
}
playerSetVolume(isRestricted /*muting*/, finalLeftVol, finalRightVol);
}
void setVolumeMultiplier(float vol) {
synchronized (mLock) {
this.mVolMultiplier = vol;
}
updatePlayerVolume();
}
void baseSetVolume(float leftVolume, float rightVolume) {
synchronized (mLock) {
mLeftVolume = leftVolume;
mRightVolume = rightVolume;
}
updatePlayerVolume();
}
int baseSetAuxEffectSendLevel(float level) {
synchronized (mLock) {
mAuxEffectSendLevel = level;
if (isRestricted_sync()) {
return AudioSystem.SUCCESS;
}
}
return playerSetAuxEffectSendLevel(false/*muting*/, level);
}
/**
* To be called from a subclass release or finalize method.
* Releases AppOps related resources.
*/
void baseRelease() {
if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId + " state=" + mState); }
boolean releasePlayer = false;
synchronized (mLock) {
if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
releasePlayer = true;
mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED;
}
}
try {
if (releasePlayer) {
getService().releasePlayer(mPlayerIId);
}
} catch (RemoteException e) {
Log.e(TAG, "Error talking to audio service, the player will still be tracked", e);
}
try {
if (mAppOps != null) {
mAppOps.stopWatchingMode(mAppOpsCallback);
}
} catch (Exception e) {
// nothing to do here, the object is supposed to be released anyway
}
}
private void updateAppOpsPlayAudio() {
synchronized (mLock) {
updateAppOpsPlayAudio_sync(false);
}
}
/**
* To be called whenever a condition that might affect audibility of this player is updated.
* Must be called synchronized on mLock.
*/
void updateAppOpsPlayAudio_sync(boolean attributesChanged) {
if (USE_AUDIOFLINGER_MUTING_FOR_OP) {
return;
}
boolean oldHasAppOpsPlayAudio = mHasAppOpsPlayAudio;
try {
int mode = AppOpsManager.MODE_IGNORED;
if (mAppOps != null) {
mode = mAppOps.checkAudioOperation(AppOpsManager.OP_PLAY_AUDIO,
mAttributes.getUsage(),
Process.myUid(), ActivityThread.currentPackageName());
}
mHasAppOpsPlayAudio = (mode == AppOpsManager.MODE_ALLOWED);
} catch (RemoteException e) {
mHasAppOpsPlayAudio = false;
}
// AppsOps alters a player's volume; when the restriction changes, reflect it on the actual
// volume used by the player
try {
if (oldHasAppOpsPlayAudio != mHasAppOpsPlayAudio ||
attributesChanged) {
getService().playerHasOpPlayAudio(mPlayerIId, mHasAppOpsPlayAudio);
if (!isRestricted_sync()) {
if (DEBUG_APP_OPS) {
Log.v(TAG, "updateAppOpsPlayAudio: unmuting player, vol=" + mLeftVolume
+ "/" + mRightVolume);
}
playerSetVolume(false/*muting*/,
mLeftVolume * mPanMultiplierL, mRightVolume * mPanMultiplierR);
playerSetAuxEffectSendLevel(false/*muting*/, mAuxEffectSendLevel);
} else {
if (DEBUG_APP_OPS) {
Log.v(TAG, "updateAppOpsPlayAudio: muting player");
}
playerSetVolume(true/*muting*/, 0.0f, 0.0f);
playerSetAuxEffectSendLevel(true/*muting*/, 0.0f);
}
}
} catch (Exception e) {
// failing silently, player might not be in right state
}
}
/**
* To be called by the subclass whenever an operation is potentially restricted.
* As the media player-common behavior are incorporated into this class, the subclass's need
* to call this method should be removed, and this method could become private.
* FIXME can this method be private so subclasses don't have to worry about when to check
* the restrictions.
* @return
*/
boolean isRestricted_sync() {
if (USE_AUDIOFLINGER_MUTING_FOR_OP) {
return false;
}
// check app ops
if (mHasAppOpsPlayAudio) {
return false;
}
// check bypass flag
if ((mAttributes.getAllFlags() & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0) {
return false;
}
// check force audibility flag and camera restriction
if (((mAttributes.getAllFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED) != 0)
&& (mAttributes.getUsage() == AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)) {
boolean cameraSoundForced = false;
try {
cameraSoundForced = getService().isCameraSoundForced();
} catch (RemoteException e) {
Log.e(TAG, "Cannot access AudioService in isRestricted_sync()");
} catch (NullPointerException e) {
Log.e(TAG, "Null AudioService in isRestricted_sync()");
}
if (cameraSoundForced) {
return false;
}
}
return true;
}
private static IAudioService getService()
{
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
sService = IAudioService.Stub.asInterface(b);
return sService;
}
/**
* @hide
* @param delayMs
*/
public void setStartDelayMs(int delayMs) {
baseSetStartDelayMs(delayMs);
}
//=====================================================================
// Abstract methods a subclass needs to implement
/**
* Abstract method for the subclass behavior's for volume and muting commands
* @param muting if true, the player is to be muted, and the volume values can be ignored
* @param leftVolume the left volume to use if muting is false
* @param rightVolume the right volume to use if muting is false
*/
abstract void playerSetVolume(boolean muting, float leftVolume, float rightVolume);
/**
* Abstract method to apply a {@link VolumeShaper.Configuration}
* and a {@link VolumeShaper.Operation} to the Player.
* This should be overridden by the Player to call into the native
* VolumeShaper implementation. Multiple {@code VolumeShapers} may be
* concurrently active for a given Player, each accessible by the
* {@code VolumeShaper} id.
*
* The {@code VolumeShaper} implementation caches the id returned
* when applying a fully specified configuration
* from {VolumeShaper.Configuration.Builder} to track later
* operation changes requested on it.
*
* @param configuration a {@code VolumeShaper.Configuration} object
* created by {@link VolumeShaper.Configuration.Builder} or
* an created from a {@code VolumeShaper} id
* by the {@link VolumeShaper.Configuration} constructor.
* @param operation a {@code VolumeShaper.Operation}.
* @return a negative error status or a
* non-negative {@code VolumeShaper} id on success.
*/
/* package */ abstract int playerApplyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation);
/**
* Abstract method to get the current VolumeShaper state.
* @param id the {@code VolumeShaper} id returned from
* sending a fully specified {@code VolumeShaper.Configuration}
* through {@link #playerApplyVolumeShaper}
* @return a {@code VolumeShaper.State} object or null if
* there is no {@code VolumeShaper} for the id.
*/
/* package */ abstract @Nullable VolumeShaper.State playerGetVolumeShaperState(int id);
abstract int playerSetAuxEffectSendLevel(boolean muting, float level);
abstract void playerStart();
abstract void playerPause();
abstract void playerStop();
//=====================================================================
private static class IAppOpsCallbackWrapper extends IAppOpsCallback.Stub {
private final WeakReference<PlayerBase> mWeakPB;
public IAppOpsCallbackWrapper(PlayerBase pb) {
mWeakPB = new WeakReference<PlayerBase>(pb);
}
@Override
public void opChanged(int op, int uid, String packageName) {
if (op == AppOpsManager.OP_PLAY_AUDIO) {
if (DEBUG_APP_OPS) { Log.v(TAG, "opChanged: op=PLAY_AUDIO pack=" + packageName); }
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.updateAppOpsPlayAudio();
}
}
}
}
//=====================================================================
/**
* Wrapper around an implementation of IPlayer for all subclasses of PlayerBase
* that doesn't keep a strong reference on PlayerBase
*/
private static class IPlayerWrapper extends IPlayer.Stub {
private final WeakReference<PlayerBase> mWeakPB;
public IPlayerWrapper(PlayerBase pb) {
mWeakPB = new WeakReference<PlayerBase>(pb);
}
@Override
public void start() {
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.playerStart();
}
}
@Override
public void pause() {
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.playerPause();
}
}
@Override
public void stop() {
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.playerStop();
}
}
@Override
public void setVolume(float vol) {
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.setVolumeMultiplier(vol);
}
}
@Override
public void setPan(float pan) {
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.baseSetPan(pan);
}
}
@Override
public void setStartDelayMs(int delayMs) {
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.baseSetStartDelayMs(delayMs);
}
}
@Override
public void applyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation) {
final PlayerBase pb = mWeakPB.get();
if (pb != null) {
pb.playerApplyVolumeShaper(configuration, operation);
}
}
}
//=====================================================================
/**
* 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 mPlayerType;
public static final int AUDIO_ATTRIBUTES_NONE = 0;
public static final int AUDIO_ATTRIBUTES_DEFINED = 1;
public final AudioAttributes mAttributes;
public final IPlayer mIPlayer;
PlayerIdCard(int type, @NonNull AudioAttributes attr, @NonNull IPlayer iplayer) {
mPlayerType = type;
mAttributes = attr;
mIPlayer = iplayer;
}
@Override
public int hashCode() {
return Objects.hash(mPlayerType);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mPlayerType);
mAttributes.writeToParcel(dest, 0);
dest.writeStrongBinder(mIPlayer == null ? null : mIPlayer.asBinder());
}
public static final @android.annotation.NonNull 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) {
mPlayerType = in.readInt();
mAttributes = AudioAttributes.CREATOR.createFromParcel(in);
// IPlayer can be null if unmarshalling a Parcel coming from who knows where
final IBinder b = in.readStrongBinder();
mIPlayer = (b == null ? null : IPlayer.Stub.asInterface(b));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof PlayerIdCard)) return false;
PlayerIdCard that = (PlayerIdCard) o;
// FIXME change to the binder player interface once supported as a member
return ((mPlayerType == that.mPlayerType) && mAttributes.equals(that.mAttributes));
}
}
//=====================================================================
// Utilities
/**
* @hide
* Use to generate warning or exception in legacy code paths that allowed passing stream types
* to qualify audio playback.
* @param streamType the stream type to check
* @throws IllegalArgumentException
*/
public static void deprecateStreamTypeForPlayback(int streamType, @NonNull String className,
@NonNull String opName) throws IllegalArgumentException {
// STREAM_ACCESSIBILITY was introduced at the same time the use of stream types
// for audio playback was deprecated, so it is not allowed at all to qualify a playback
// use case
if (streamType == AudioManager.STREAM_ACCESSIBILITY) {
throw new IllegalArgumentException("Use of STREAM_ACCESSIBILITY is reserved for "
+ "volume control");
}
Log.w(className, "Use of stream types is deprecated for operations other than " +
"volume control");
Log.w(className, "See the documentation of " + opName + " for what to use instead with " +
"android.media.AudioAttributes to qualify your playback use case");
}
}