Add SDK 29 sources.
Test: N/A
Change-Id: Iedb7a31029e003928eb16f7e69ed147e72bb6235
diff --git a/android/media/PlayerBase.java b/android/media/PlayerBase.java
new file mode 100644
index 0000000..ee8f1b3
--- /dev/null
+++ b/android/media/PlayerBase.java
@@ -0,0 +1,625 @@
+/*
+ * 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");
+ }
+}