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"); + } +}