content: /*
   * Copyright (C) 2017 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 com.android.server.media;
  
  import android.annotation.NonNull;
  import android.content.Context;
  import android.media.AudioManager;
  import android.media.AudioPlaybackConfiguration;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.UserHandle;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.IntArray;
  import android.util.Log;
  
  import com.android.internal.annotations.GuardedBy;
  
  import java.io.PrintWriter;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * Monitors the state changes of audio players.
   */
  class AudioPlayerStateMonitor {
      private static boolean DEBUG = MediaSessionService.DEBUG;
      private static String TAG = "AudioPlayerStateMonitor";
  
      private static AudioPlayerStateMonitor sInstance;
  
      /**
       * Listener for handling the active state changes of audio players.
       */
      interface OnAudioPlayerActiveStateChangedListener {
          /**
           * Called when the active state of audio player is changed.
           *
           * @param config The audio playback configuration for the audio player for which active
           *              state was changed. If {@param isRemoved} is {@code true}, this holds
           *              outdated information.
           * @param isRemoved {@code true} if the audio player is removed.
           */
          void onAudioPlayerActiveStateChanged(
                  @NonNull AudioPlaybackConfiguration config, boolean isRemoved);
      }
  
      private final static class MessageHandler extends Handler {
          private static final int MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED = 1;
  
          private final OnAudioPlayerActiveStateChangedListener mListener;
  
          MessageHandler(Looper looper, OnAudioPlayerActiveStateChangedListener listener) {
              super(looper);
              mListener = listener;
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED:
                      mListener.onAudioPlayerActiveStateChanged((AudioPlaybackConfiguration) msg.obj,
                              msg.arg1 != 0);
                      break;
              }
          }
  
          void sendAudioPlayerActiveStateChangedMessage(
                  final AudioPlaybackConfiguration config, final boolean isRemoved) {
              obtainMessage(MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED,
                      isRemoved ? 1 : 0, 0 /* unused */, config).sendToTarget();
          }
      }
  
      private final Object mLock = new Object();
      @GuardedBy("mLock")
      private final Map<OnAudioPlayerActiveStateChangedListener, MessageHandler> mListenerMap =
              new ArrayMap<>();
      @GuardedBy("mLock")
      @SuppressWarnings("WeakerAccess") /* synthetic access */
      final Set<Integer> mActiveAudioUids = new ArraySet<>();
      @GuardedBy("mLock")
      @SuppressWarnings("WeakerAccess") /* synthetic access */
      ArrayMap<Integer, AudioPlaybackConfiguration> mPrevActiveAudioPlaybackConfigs =
              new ArrayMap<>();
      // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
      // The UID whose audio playback becomes active at the last comes first.
      // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
      @GuardedBy("mLock")
      @SuppressWarnings("WeakerAccess") /* synthetic access */
      final IntArray mSortedAudioPlaybackClientUids = new IntArray();
  
      static AudioPlayerStateMonitor getInstance(Context context) {
          synchronized (AudioPlayerStateMonitor.class) {
              if (sInstance == null) {
                  sInstance = new AudioPlayerStateMonitor(context);
              }
              return sInstance;
          }
      }
  
      private AudioPlayerStateMonitor(Context context) {
          AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
          am.registerAudioPlaybackCallback(new AudioManagerPlaybackListener(), null);
      }
  
      /**
       * Registers OnAudioPlayerActiveStateChangedListener.
       */
      public void registerListener(
              OnAudioPlayerActiveStateChangedListener listener, Handler handler) {
          synchronized (mLock) {
              mListenerMap.put(listener, new MessageHandler((handler == null) ?
                      Looper.myLooper() : handler.getLooper(), listener));
          }
      }
  
      /**
       * Unregisters OnAudioPlayerActiveStateChangedListener.
       */
      public void unregisterListener(OnAudioPlayerActiveStateChangedListener listener) {
          synchronized (mLock) {
              mListenerMap.remove(listener);
          }
      }
  
      /**
       * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an
       * audio/video) The UID whose audio is currently playing comes first, then the UID whose audio
       * playback becomes active at the last comes next.
       */
      public IntArray getSortedAudioPlaybackClientUids() {
          IntArray sortedAudioPlaybackClientUids = new IntArray();
          synchronized (mLock) {
              sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
          }
          return sortedAudioPlaybackClientUids;
      }
  
      /**
       * Returns if the audio playback is active for the uid.
       */
      public boolean isPlaybackActive(int uid) {
          synchronized (mLock) {
              return mActiveAudioUids.contains(uid);
          }
      }
  
      /**
       * Cleans up the sorted list of audio playback client UIDs with given {@param
       * mediaButtonSessionUid}.
       * <p>UIDs whose audio playback are inactive and have started before the media button session's
       * audio playback cannot be the lastly played media app. So they won't needed anymore.
       *
       * @param mediaButtonSessionUid UID of the media button session.
       */
      public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) {
          synchronized (mLock) {
              int userId = UserHandle.getUserId(mediaButtonSessionUid);
              for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) {
                  if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) {
                      break;
                  }
                  int uid = mSortedAudioPlaybackClientUids.get(i);
                  if (userId == UserHandle.getUserId(uid) && !isPlaybackActive(uid)) {
                      // Clean up unnecessary UIDs.
                      // It doesn't need to be managed profile aware because it's just to prevent
                      // the list from increasing indefinitely. The media button session updating
                      // shouldn't be affected by cleaning up.
                      mSortedAudioPlaybackClientUids.remove(i);
                  }
              }
          }
      }
  
      /**
       * Dumps {@link AudioPlayerStateMonitor}.
       */
      public void dump(Context context, PrintWriter pw, String prefix) {
          synchronized (mLock) {
              pw.println(prefix + "Audio playback (lastly played comes first)");
              String indent = prefix + "  ";
              for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) {
                  int uid = mSortedAudioPlaybackClientUids.get(i);
                  pw.print(indent + "uid=" + uid + " packages=");
                  String[] packages = context.getPackageManager().getPackagesForUid(uid);
                  if (packages != null && packages.length > 0) {
                      for (int j = 0; j < packages.length; j++) {
                          pw.print(packages[j] + " ");
                      }
                  }
                  pw.println();
              }
          }
      }
  
      @GuardedBy("mLock")
      private void sendAudioPlayerActiveStateChangedMessageLocked(
              final AudioPlaybackConfiguration config, final boolean isRemoved) {
          for (MessageHandler messageHandler : mListenerMap.values()) {
              messageHandler.sendAudioPlayerActiveStateChangedMessage(config, isRemoved);
          }
      }
  
      private class AudioManagerPlaybackListener extends AudioManager.AudioPlaybackCallback {
          @Override
          public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
              synchronized (mLock) {
                  // Update mActiveAudioUids
                  mActiveAudioUids.clear();
                  ArrayMap<Integer, AudioPlaybackConfiguration> activeAudioPlaybackConfigs =
                          new ArrayMap<>();
                  for (AudioPlaybackConfiguration config : configs) {
                      if (config.isActive()) {
                          mActiveAudioUids.add(config.getClientUid());
                          activeAudioPlaybackConfigs.put(config.getPlayerInterfaceId(), config);
                      }
                  }
  
                  // Update mSortedAuioPlaybackClientUids.
                  for (int i = 0; i < activeAudioPlaybackConfigs.size(); ++i) {
                      AudioPlaybackConfiguration config = activeAudioPlaybackConfigs.valueAt(i);
                      final int uid = config.getClientUid();
                      if (!mPrevActiveAudioPlaybackConfigs.containsKey(
                              config.getPlayerInterfaceId())) {
                          if (DEBUG) {
                              Log.d(TAG, "Found a new active media playback. "
                                      + AudioPlaybackConfiguration.toLogFriendlyString(config));
                          }
                          // New active audio playback.
                          int index = mSortedAudioPlaybackClientUids.indexOf(uid);
                          if (index == 0) {
                              // It's the lastly played music app already. Skip updating.
                              continue;
                          } else if (index > 0) {
                              mSortedAudioPlaybackClientUids.remove(index);
                          }
                          mSortedAudioPlaybackClientUids.add(0, uid);
                      }
                  }
  
                  if (mActiveAudioUids.size() > 0
                          && !mActiveAudioUids.contains(mSortedAudioPlaybackClientUids.get(0))) {
                      int firstActiveUid = -1;
                      int firatActiveUidIndex = -1;
                      for (int i = 1; i < mSortedAudioPlaybackClientUids.size(); ++i) {
                          int uid = mSortedAudioPlaybackClientUids.get(i);
                          if (mActiveAudioUids.contains(uid)) {
                              firatActiveUidIndex = i;
                              firstActiveUid = uid;
                              break;
                          }
                      }
                      for (int i = firatActiveUidIndex; i > 0; --i) {
                          mSortedAudioPlaybackClientUids.set(i,
                                  mSortedAudioPlaybackClientUids.get(i - 1));
                      }
                      mSortedAudioPlaybackClientUids.set(0, firstActiveUid);
                  }
  
                  // Notify the active state change of audio players.
                  for (AudioPlaybackConfiguration config : configs) {
                      final int pii = config.getPlayerInterfaceId();
                      boolean wasActive = mPrevActiveAudioPlaybackConfigs.remove(pii) != null;
                      if (wasActive != config.isActive()) {
                          sendAudioPlayerActiveStateChangedMessageLocked(
                                  config, /* isRemoved */ false);
                      }
                  }
                  for (AudioPlaybackConfiguration config : mPrevActiveAudioPlaybackConfigs.values()) {
                      sendAudioPlayerActiveStateChangedMessageLocked(config, /* isRemoved */ true);
                  }
  
                  // Update mPrevActiveAudioPlaybackConfigs
                  mPrevActiveAudioPlaybackConfigs = activeAudioPlaybackConfigs;
              }
          }
      }
  }
  