content: /*
   * 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");
      }
  }
  