| /* |
| * 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 static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; |
| import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; |
| import static android.content.Context.DEVICE_ID_DEFAULT; |
| import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.ActivityThread; |
| import android.companion.virtual.VirtualDeviceManager; |
| import android.content.Context; |
| import android.os.IBinder; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.text.TextUtils; |
| 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() |
| |
| // 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) |
| protected 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; |
| @GuardedBy("mLock") |
| private int mDeviceId; |
| |
| /** |
| * 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 |
| * @param sessionId the audio session Id |
| */ |
| PlayerBase(@NonNull AudioAttributes attr, int implType) { |
| if (attr == null) { |
| throw new IllegalArgumentException("Illegal null AudioAttributes"); |
| } |
| mAttributes = attr; |
| mImplType = implType; |
| mState = AudioPlaybackConfiguration.PLAYER_STATE_IDLE; |
| }; |
| |
| /** @hide */ |
| public int getPlayerIId() { |
| synchronized (mLock) { |
| return mPlayerIId; |
| } |
| } |
| |
| /** |
| * Call from derived class when instantiation / initialization is successful |
| */ |
| protected void baseRegisterPlayer(int sessionId) { |
| try { |
| mPlayerIId = getService().trackPlayer( |
| new PlayerIdCard(mImplType, mAttributes, new IPlayerWrapper(this), |
| sessionId)); |
| } 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, audio attributes will not be updated", e); |
| } |
| synchronized (mLock) { |
| mAttributes = attr; |
| } |
| } |
| |
| /** |
| * To be called whenever the session ID of the player changes |
| * @param sessionId, the new session Id |
| */ |
| void baseUpdateSessionId(int sessionId) { |
| try { |
| getService().playerSessionId(mPlayerIId, sessionId); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error talking to audio service, the session ID will not be updated", e); |
| } |
| } |
| |
| void baseUpdateDeviceId(@Nullable AudioDeviceInfo deviceInfo) { |
| int deviceId = 0; |
| if (deviceInfo != null) { |
| deviceId = deviceInfo.getId(); |
| } |
| int piid; |
| synchronized (mLock) { |
| piid = mPlayerIId; |
| mDeviceId = deviceId; |
| } |
| try { |
| getService().playerEvent(piid, |
| AudioPlaybackConfiguration.PLAYER_UPDATE_DEVICE_ID, deviceId); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error talking to audio service, " |
| + deviceId |
| + " device id will not be tracked for piid=" + piid, e); |
| } |
| } |
| |
| private void updateState(int state, int deviceId) { |
| final int piid; |
| synchronized (mLock) { |
| mState = state; |
| piid = mPlayerIId; |
| mDeviceId = deviceId; |
| } |
| try { |
| getService().playerEvent(piid, state, deviceId); |
| } 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(int deviceId) { |
| if (DEBUG) { |
| Log.v(TAG, "baseStart() piid=" + mPlayerIId + " deviceId=" + deviceId); |
| } |
| updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED, deviceId); |
| } |
| |
| 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, 0); |
| } |
| |
| void baseStop() { |
| if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); } |
| updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED, 0); |
| } |
| |
| 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; |
| synchronized (mLock) { |
| finalLeftVol = mVolMultiplier * mLeftVolume * mPanMultiplierL; |
| finalRightVol = mVolMultiplier * mRightVolume * mPanMultiplierR; |
| } |
| playerSetVolume(false /*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; |
| } |
| 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 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(); |
| |
| //===================================================================== |
| /** |
| * 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 VolumeShaperConfiguration configuration, |
| @NonNull VolumeShaperOperation operation) { |
| final PlayerBase pb = mWeakPB.get(); |
| if (pb != null) { |
| pb.playerApplyVolumeShaper(VolumeShaper.Configuration.fromParcelable(configuration), |
| VolumeShaper.Operation.fromParcelable(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; |
| public final int mSessionId; |
| |
| PlayerIdCard(int type, @NonNull AudioAttributes attr, @NonNull IPlayer iplayer, |
| int sessionId) { |
| mPlayerType = type; |
| mAttributes = attr; |
| mIPlayer = iplayer; |
| mSessionId = sessionId; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mPlayerType, mSessionId); |
| } |
| |
| @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()); |
| dest.writeInt(mSessionId); |
| } |
| |
| 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)); |
| mSessionId = in.readInt(); |
| } |
| |
| @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) |
| && (mSessionId == that.mSessionId)); |
| } |
| } |
| |
| //===================================================================== |
| // 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"); |
| } |
| |
| protected String getCurrentOpPackageName() { |
| return TextUtils.emptyIfNull(ActivityThread.currentOpPackageName()); |
| } |
| |
| /** |
| * Helper method to resolve which session id should be used for player initialization. |
| * |
| * This method will assign session id in following way: |
| * 1. Explicitly requested session id has the highest priority, if there is one, |
| * it will be used. |
| * 2. If there's device-specific session id associated with the provided context, |
| * it will be used. |
| * 3. Otherwise {@link AUDIO_SESSION_ID_GENERATE} is returned. |
| * |
| * @param context {@link Context} to use for extraction of device specific session id. |
| * @param requestedSessionId explicitly requested session id or AUDIO_SESSION_ID_GENERATE. |
| * @return session id to be passed to AudioService for the player initialization given |
| * provided {@link Context} instance and explicitly requested session id. |
| */ |
| protected static int resolvePlaybackSessionId(@Nullable Context context, |
| int requestedSessionId) { |
| if (requestedSessionId != AUDIO_SESSION_ID_GENERATE) { |
| // Use explicitly requested session id. |
| return requestedSessionId; |
| } |
| |
| if (context == null) { |
| return AUDIO_SESSION_ID_GENERATE; |
| } |
| |
| int deviceId = context.getDeviceId(); |
| if (deviceId == DEVICE_ID_DEFAULT) { |
| return AUDIO_SESSION_ID_GENERATE; |
| } |
| |
| VirtualDeviceManager vdm = context.getSystemService(VirtualDeviceManager.class); |
| if (vdm == null || vdm.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO) |
| == DEVICE_POLICY_DEFAULT) { |
| return AUDIO_SESSION_ID_GENERATE; |
| } |
| |
| return vdm.getAudioPlaybackSessionId(deviceId); |
| } |
| } |