content: /*
   * Copyright (C) 2013 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.UnsupportedAppUsage;
  import android.app.ActivityManager;
  import android.content.ComponentName;
  import android.content.Context;
  import android.graphics.Bitmap;
  import android.media.session.MediaController;
  import android.media.session.MediaSession;
  import android.media.session.MediaSessionLegacyHelper;
  import android.media.session.MediaSessionManager;
  import android.media.session.PlaybackState;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.UserHandle;
  import android.util.DisplayMetrics;
  import android.util.Log;
  import android.view.KeyEvent;
  
  import java.util.List;
  
  /**
   * The RemoteController class is used to control media playback, display and update media metadata
   * and playback status, published by applications using the {@link RemoteControlClient} class.
   * <p>
   * A RemoteController shall be registered through
   * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send
   * media event updates to the {@link OnClientUpdateListener} listener set in the class constructor.
   * Implement the methods of the interface to receive the information published by the active
   * {@link RemoteControlClient} instances.
   * <br>By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for
   * album art. Use {@link #setArtworkConfiguration(int, int)} to receive images as well.
   * <p>
   * Registration requires the {@link OnClientUpdateListener} listener to be one of the enabled
   * notification listeners (see {@link android.service.notification.NotificationListenerService}).
   *
   * @deprecated Use {@link MediaController} instead.
   */
  @Deprecated public final class RemoteController
  {
      private final static int MAX_BITMAP_DIMENSION = 512;
      private final static String TAG = "RemoteController";
      private final static boolean DEBUG = false;
      private final static Object mInfoLock = new Object();
      private final Context mContext;
      private final int mMaxBitmapDimension;
      private MetadataEditor mMetadataEditor;
  
      private MediaSessionManager mSessionManager;
      private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
      private MediaController.Callback mSessionCb = new MediaControllerCallback();
  
      /**
       * Synchronized on mInfoLock
       */
      private boolean mIsRegistered = false;
      private OnClientUpdateListener mOnClientUpdateListener;
      private PlaybackInfo mLastPlaybackInfo;
      private int mArtworkWidth = -1;
      private int mArtworkHeight = -1;
      private boolean mEnabled = true;
      // synchronized on mInfoLock, for USE_SESSION apis.
      @UnsupportedAppUsage
      private MediaController mCurrentSession;
  
      /**
       * Class constructor.
       * @param context the {@link Context}, must be non-null.
       * @param updateListener the listener to be called whenever new client information is available,
       *     must be non-null.
       * @throws IllegalArgumentException
       */
      public RemoteController(Context context, OnClientUpdateListener updateListener)
              throws IllegalArgumentException {
          this(context, updateListener, null);
      }
  
      /**
       * Class constructor.
       * @param context the {@link Context}, must be non-null.
       * @param updateListener the listener to be called whenever new client information is available,
       *     must be non-null.
       * @param looper the {@link Looper} on which to run the event loop,
       *     or null to use the current thread's looper.
       * @throws java.lang.IllegalArgumentException
       */
      public RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper)
              throws IllegalArgumentException {
          if (context == null) {
              throw new IllegalArgumentException("Invalid null Context");
          }
          if (updateListener == null) {
              throw new IllegalArgumentException("Invalid null OnClientUpdateListener");
          }
          if (looper != null) {
              mEventHandler = new EventHandler(this, looper);
          } else {
              Looper l = Looper.myLooper();
              if (l != null) {
                  mEventHandler = new EventHandler(this, l);
              } else {
                  throw new IllegalArgumentException("Calling thread not associated with a looper");
              }
          }
          mOnClientUpdateListener = updateListener;
          mContext = context;
          mSessionManager = (MediaSessionManager) context
                  .getSystemService(Context.MEDIA_SESSION_SERVICE);
          mSessionListener = new TopTransportSessionListener();
  
          if (ActivityManager.isLowRamDeviceStatic()) {
              mMaxBitmapDimension = MAX_BITMAP_DIMENSION;
          } else {
              final DisplayMetrics dm = context.getResources().getDisplayMetrics();
              mMaxBitmapDimension = Math.max(dm.widthPixels, dm.heightPixels);
          }
      }
  
  
      /**
       * Interface definition for the callbacks to be invoked whenever media events, metadata
       * and playback status are available.
       */
      public interface OnClientUpdateListener {
          /**
           * Called whenever all information, previously received through the other
           * methods of the listener, is no longer valid and is about to be refreshed.
           * This is typically called whenever a new {@link RemoteControlClient} has been selected
           * by the system to have its media information published.
           * @param clearing true if there is no selected RemoteControlClient and no information
           *     is available.
           */
          public void onClientChange(boolean clearing);
  
          /**
           * Called whenever the playback state has changed.
           * It is called when no information is known about the playback progress in the media and
           * the playback speed.
           * @param state one of the playback states authorized
           *     in {@link RemoteControlClient#setPlaybackState(int)}.
           */
          public void onClientPlaybackStateUpdate(int state);
          /**
           * Called whenever the playback state has changed, and playback position
           * and speed are known.
           * @param state one of the playback states authorized
           *     in {@link RemoteControlClient#setPlaybackState(int)}.
           * @param stateChangeTimeMs the system time at which the state change was reported,
           *     expressed in ms. Based on {@link android.os.SystemClock#elapsedRealtime()}.
           * @param currentPosMs a positive value for the current media playback position expressed
           *     in ms, a negative value if the position is temporarily unknown.
           * @param speed  a value expressed as a ratio of 1x playback: 1.0f is normal playback,
           *    2.0f is 2x, 0.5f is half-speed, -2.0f is rewind at 2x speed. 0.0f means nothing is
           *    playing (e.g. when state is {@link RemoteControlClient#PLAYSTATE_ERROR}).
           */
          public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
                  long currentPosMs, float speed);
          /**
           * Called whenever the transport control flags have changed.
           * @param transportControlFlags one of the flags authorized
           *     in {@link RemoteControlClient#setTransportControlFlags(int)}.
           */
          public void onClientTransportControlUpdate(int transportControlFlags);
          /**
           * Called whenever new metadata is available.
           * See the {@link MediaMetadataEditor#putLong(int, long)},
           *  {@link MediaMetadataEditor#putString(int, String)},
           *  {@link MediaMetadataEditor#putBitmap(int, Bitmap)}, and
           *  {@link MediaMetadataEditor#putObject(int, Object)} methods for the various keys that
           *  can be queried.
           * @param metadataEditor the container of the new metadata.
           */
          public void onClientMetadataUpdate(MetadataEditor metadataEditor);
      };
  
      /**
       * Return the estimated playback position of the current media track or a negative value
       * if not available.
       *
       * <p>The value returned is estimated by the current process and may not be perfect.
       * The time returned by this method is calculated from the last state change time based
       * on the current play position at that time and the last known playback speed.
       * An application may call {@link #setSynchronizationMode(int)} to apply
       * a synchronization policy that will periodically re-sync the estimated position
       * with the RemoteControlClient.</p>
       *
       * @return the current estimated playback position in milliseconds or a negative value
       *         if not available
       *
       * @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float)
       */
      public long getEstimatedMediaPosition() {
          synchronized (mInfoLock) {
              if (mCurrentSession != null) {
                  PlaybackState state = mCurrentSession.getPlaybackState();
                  if (state != null) {
                      return state.getPosition();
                  }
              }
          }
          return -1;
      }
  
  
      /**
       * Send a simulated key event for a media button to be received by the current client.
       * To simulate a key press, you must first send a KeyEvent built with
       * a {@link KeyEvent#ACTION_DOWN} action, then another event with the {@link KeyEvent#ACTION_UP}
       * action.
       * <p>The key event will be sent to the registered receiver
       * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}) whose associated
       * {@link RemoteControlClient}'s metadata and playback state is published (there may be
       * none under some circumstances).
       * @param keyEvent a {@link KeyEvent} instance whose key code is one of
       *     {@link KeyEvent#KEYCODE_MUTE},
       *     {@link KeyEvent#KEYCODE_HEADSETHOOK},
       *     {@link KeyEvent#KEYCODE_MEDIA_PLAY},
       *     {@link KeyEvent#KEYCODE_MEDIA_PAUSE},
       *     {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE},
       *     {@link KeyEvent#KEYCODE_MEDIA_STOP},
       *     {@link KeyEvent#KEYCODE_MEDIA_NEXT},
       *     {@link KeyEvent#KEYCODE_MEDIA_PREVIOUS},
       *     {@link KeyEvent#KEYCODE_MEDIA_REWIND},
       *     {@link KeyEvent#KEYCODE_MEDIA_RECORD},
       *     {@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD},
       *     {@link KeyEvent#KEYCODE_MEDIA_CLOSE},
       *     {@link KeyEvent#KEYCODE_MEDIA_EJECT},
       *     or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}.
       * @return true if the event was successfully sent, false otherwise.
       * @throws IllegalArgumentException
       */
      public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException {
          if (!KeyEvent.isMediaSessionKey(keyEvent.getKeyCode())) {
              throw new IllegalArgumentException("not a media key event");
          }
          synchronized (mInfoLock) {
              if (mCurrentSession != null) {
                  return mCurrentSession.dispatchMediaButtonEvent(keyEvent);
              }
              return false;
          }
      }
  
  
      /**
       * Sets the new playback position.
       * This method can only be called on a registered RemoteController.
       * @param timeMs a 0 or positive value for the new playback position, expressed in ms.
       * @return true if the command to set the playback position was successfully sent.
       * @throws IllegalArgumentException
       */
      public boolean seekTo(long timeMs) throws IllegalArgumentException {
          if (!mEnabled) {
              Log.e(TAG, "Cannot use seekTo() from a disabled RemoteController");
              return false;
          }
          if (timeMs < 0) {
              throw new IllegalArgumentException("illegal negative time value");
          }
          synchronized (mInfoLock) {
              if (mCurrentSession != null) {
                  mCurrentSession.getTransportControls().seekTo(timeMs);
              }
          }
          return true;
      }
  
  
      /**
       * @hide
       * @param wantBitmap
       * @param width
       * @param height
       * @return true if successful
       * @throws IllegalArgumentException
       */
      @UnsupportedAppUsage
      public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height)
              throws IllegalArgumentException {
          synchronized (mInfoLock) {
              if (wantBitmap) {
                  if ((width > 0) && (height > 0)) {
                      if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; }
                      if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; }
                      mArtworkWidth = width;
                      mArtworkHeight = height;
                  } else {
                      throw new IllegalArgumentException("Invalid dimensions");
                  }
              } else {
                  mArtworkWidth = -1;
                  mArtworkHeight = -1;
              }
          }
          return true;
      }
  
      /**
       * Set the maximum artwork image dimensions to be received in the metadata.
       * No bitmaps will be received unless this has been specified.
       * @param width the maximum width in pixels
       * @param height  the maximum height in pixels
       * @return true if the artwork dimension was successfully set.
       * @throws IllegalArgumentException
       */
      public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException {
          return setArtworkConfiguration(true, width, height);
      }
  
      /**
       * Prevents this RemoteController from receiving artwork images.
       * @return true if receiving artwork images was successfully disabled.
       */
      public boolean clearArtworkConfiguration() {
          return setArtworkConfiguration(false, -1, -1);
      }
  
  
      /**
       * Default playback position synchronization mode where the RemoteControlClient is not
       * asked regularly for its playback position to see if it has drifted from the estimated
       * position.
       */
      public static final int POSITION_SYNCHRONIZATION_NONE = 0;
  
      /**
       * The playback position synchronization mode where the RemoteControlClient instances which
       * expose their playback position to the framework, will be regularly polled to check
       * whether any drift has been noticed between their estimated position and the one they report.
       * Note that this mode should only ever be used when needing to display very accurate playback
       * position, as regularly polling a RemoteControlClient for its position may have an impact
       * on battery life (if applicable) when this query will trigger network transactions in the
       * case of remote playback.
       */
      public static final int POSITION_SYNCHRONIZATION_CHECK = 1;
  
      /**
       * Set the playback position synchronization mode.
       * Must be called on a registered RemoteController.
       * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK}
       * @return true if the synchronization mode was successfully set.
       * @throws IllegalArgumentException
       */
      public boolean setSynchronizationMode(int sync) throws IllegalArgumentException {
          if ((sync != POSITION_SYNCHRONIZATION_NONE) && (sync != POSITION_SYNCHRONIZATION_CHECK)) {
              throw new IllegalArgumentException("Unknown synchronization mode " + sync);
          }
          if (!mIsRegistered) {
              Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController");
              return false;
          }
          // deprecated, no-op
          return true;
      }
  
  
      /**
       * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of
       * the current {@link RemoteControlClient}.
       * This method can only be called on a registered RemoteController.
       * @return a new MetadataEditor instance.
       */
      public MetadataEditor editMetadata() {
          MetadataEditor editor = new MetadataEditor();
          editor.mEditorMetadata = new Bundle();
          editor.mEditorArtwork = null;
          editor.mMetadataChanged = true;
          editor.mArtworkChanged = true;
          editor.mEditableKeys = 0;
          return editor;
      }
  
      /**
       * A class to read the metadata published by a {@link RemoteControlClient}, or send a
       * {@link RemoteControlClient} new values for keys that can be edited.
       */
      public class MetadataEditor extends MediaMetadataEditor {
          /**
           * @hide
           */
          protected MetadataEditor() { }
  
          /**
           * @hide
           */
          protected MetadataEditor(Bundle metadata, long editableKeys) {
              mEditorMetadata = metadata;
              mEditableKeys = editableKeys;
  
              mEditorArtwork = (Bitmap) metadata.getParcelable(
                      String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK));
              if (mEditorArtwork != null) {
                  cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK);
              }
  
              mMetadataChanged = true;
              mArtworkChanged = true;
              mApplied = false;
          }
  
          private void cleanupBitmapFromBundle(int key) {
              if (METADATA_KEYS_TYPE.get(key, METADATA_TYPE_INVALID) == METADATA_TYPE_BITMAP) {
                  mEditorMetadata.remove(String.valueOf(key));
              }
          }
  
          /**
           * Applies all of the metadata changes that have been set since the MediaMetadataEditor
           * instance was created with {@link RemoteController#editMetadata()}
           * or since {@link #clear()} was called.
           */
          public synchronized void apply() {
              // "applying" a metadata bundle in RemoteController is only for sending edited
              // key values back to the RemoteControlClient, so here we only care about the only
              // editable key we support: RATING_KEY_BY_USER
              if (!mMetadataChanged) {
                  return;
              }
              synchronized (mInfoLock) {
                  if (mCurrentSession != null) {
                      if (mEditorMetadata.containsKey(
                              String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) {
                          Rating rating = (Rating) getObject(
                                  MediaMetadataEditor.RATING_KEY_BY_USER, null);
                          if (rating != null) {
                              mCurrentSession.getTransportControls().setRating(rating);
                          }
                      }
                  }
              }
              // NOT setting mApplied to true as this type of MetadataEditor will be applied
              // multiple times, whenever the user of a RemoteController needs to change the
              // metadata (e.g. user changes the rating of a song more than once during playback)
              mApplied = false;
          }
  
      }
  
      /**
       * This receives updates when the current session changes. This is
       * registered to receive the updates on the handler thread so it can call
       * directly into the appropriate methods.
       */
      private class MediaControllerCallback extends MediaController.Callback {
          @Override
          public void onPlaybackStateChanged(PlaybackState state) {
              onNewPlaybackState(state);
          }
  
          @Override
          public void onMetadataChanged(MediaMetadata metadata) {
              onNewMediaMetadata(metadata);
          }
      }
  
      /**
       * Listens for changes to the active session stack and replaces the
       * currently tracked session if it has changed.
       */
      private class TopTransportSessionListener implements
              MediaSessionManager.OnActiveSessionsChangedListener {
  
          @Override
          public void onActiveSessionsChanged(List<MediaController> controllers) {
              int size = controllers.size();
              for (int i = 0; i < size; i++) {
                  MediaController controller = controllers.get(i);
                  long flags = controller.getFlags();
                  // We only care about sessions that handle transport controls,
                  // which will be true for apps using RCC
                  if ((flags & MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) {
                      updateController(controller);
                      return;
                  }
              }
              updateController(null);
          }
  
      }
  
      //==================================================
      // Event handling
      private final EventHandler mEventHandler;
      private final static int MSG_CLIENT_CHANGE      = 0;
      private final static int MSG_NEW_PLAYBACK_STATE = 1;
      private final static int MSG_NEW_MEDIA_METADATA = 2;
  
      private class EventHandler extends Handler {
  
          public EventHandler(RemoteController rc, Looper looper) {
              super(looper);
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch(msg.what) {
                  case MSG_CLIENT_CHANGE:
                      onClientChange(msg.arg2 == 1);
                      break;
                  case MSG_NEW_PLAYBACK_STATE:
                      onNewPlaybackState((PlaybackState) msg.obj);
                      break;
                  case MSG_NEW_MEDIA_METADATA:
                      onNewMediaMetadata((MediaMetadata) msg.obj);
                      break;
                  default:
                      Log.e(TAG, "unknown event " + msg.what);
              }
          }
      }
  
      /**
       * @hide
       */
      void startListeningToSessions() {
          final ComponentName listenerComponent = new ComponentName(mContext,
                  mOnClientUpdateListener.getClass());
          Handler handler = null;
          if (Looper.myLooper() == null) {
              handler = new Handler(Looper.getMainLooper());
          }
          mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, listenerComponent,
                  UserHandle.myUserId(), handler);
          mSessionListener.onActiveSessionsChanged(mSessionManager
                  .getActiveSessions(listenerComponent));
          if (DEBUG) {
              Log.d(TAG, "Registered session listener with component " + listenerComponent
                      + " for user " + UserHandle.myUserId());
          }
      }
  
      /**
       * @hide
       */
      void stopListeningToSessions() {
          mSessionManager.removeOnActiveSessionsChangedListener(mSessionListener);
          if (DEBUG) {
              Log.d(TAG, "Unregistered session listener for user "
                      + UserHandle.myUserId());
          }
      }
  
      /** If the msg is already queued, replace it with this one. */
      private static final int SENDMSG_REPLACE = 0;
      /** If the msg is already queued, ignore this one and leave the old. */
      private static final int SENDMSG_NOOP = 1;
      /** If the msg is already queued, queue this one and leave the old. */
      private static final int SENDMSG_QUEUE = 2;
  
      private static void sendMsg(Handler handler, int msg, int existingMsgPolicy,
              int arg1, int arg2, Object obj, int delayMs) {
          if (handler == null) {
              Log.e(TAG, "null event handler, will not deliver message " + msg);
              return;
          }
          if (existingMsgPolicy == SENDMSG_REPLACE) {
              handler.removeMessages(msg);
          } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
              return;
          }
          handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs);
      }
  
      private void onClientChange(boolean clearing) {
          final OnClientUpdateListener l;
          synchronized(mInfoLock) {
              l = mOnClientUpdateListener;
              mMetadataEditor = null;
          }
          if (l != null) {
              l.onClientChange(clearing);
          }
      }
  
      private void updateController(MediaController controller) {
          if (DEBUG) {
              Log.d(TAG, "Updating controller to " + controller + " previous controller is "
                      + mCurrentSession);
          }
          synchronized (mInfoLock) {
              if (controller == null) {
                  if (mCurrentSession != null) {
                      mCurrentSession.unregisterCallback(mSessionCb);
                      mCurrentSession = null;
                      sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE,
                              0 /* arg1 ignored */, 1 /* clearing */, null /* obj */, 0 /* delay */);
                  }
              } else if (mCurrentSession == null
                      || !controller.getSessionToken()
                              .equals(mCurrentSession.getSessionToken())) {
                  if (mCurrentSession != null) {
                      mCurrentSession.unregisterCallback(mSessionCb);
                  }
                  sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE,
                          0 /* arg1 ignored */, 0 /* clearing */, null /* obj */, 0 /* delay */);
                  mCurrentSession = controller;
                  mCurrentSession.registerCallback(mSessionCb, mEventHandler);
  
                  PlaybackState state = controller.getPlaybackState();
                  sendMsg(mEventHandler, MSG_NEW_PLAYBACK_STATE, SENDMSG_REPLACE,
                          0 /* arg1 ignored */, 0 /* arg2 ignored */, state /* obj */, 0 /* delay */);
  
                  MediaMetadata metadata = controller.getMetadata();
                  sendMsg(mEventHandler, MSG_NEW_MEDIA_METADATA, SENDMSG_REPLACE,
                          0 /* arg1 ignored */, 0 /* arg2 ignored*/, metadata /* obj */, 0 /*delay*/);
              }
              // else same controller, no need to update
          }
      }
  
      private void onNewPlaybackState(PlaybackState state) {
          final OnClientUpdateListener l;
          synchronized (mInfoLock) {
              l = this.mOnClientUpdateListener;
          }
          if (l != null) {
              int playstate = state == null ? RemoteControlClient.PLAYSTATE_NONE
                      : RemoteControlClient.getRccStateFromState(state.getState());
              if (state == null || state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
                  l.onClientPlaybackStateUpdate(playstate);
              } else {
                  l.onClientPlaybackStateUpdate(playstate, state.getLastPositionUpdateTime(),
                          state.getPosition(), state.getPlaybackSpeed());
              }
              if (state != null) {
                  l.onClientTransportControlUpdate(
                          RemoteControlClient.getRccControlFlagsFromActions(state.getActions()));
              }
          }
      }
  
      private void onNewMediaMetadata(MediaMetadata metadata) {
          if (metadata == null) {
              // RemoteController only handles non-null metadata
              return;
          }
          final OnClientUpdateListener l;
          final MetadataEditor metadataEditor;
          // prepare the received Bundle to be used inside a MetadataEditor
          synchronized(mInfoLock) {
              l = mOnClientUpdateListener;
              boolean canRate = mCurrentSession != null
                      && mCurrentSession.getRatingType() != Rating.RATING_NONE;
              long editableKeys = canRate ? MediaMetadataEditor.RATING_KEY_BY_USER : 0;
              Bundle legacyMetadata = MediaSessionLegacyHelper.getOldMetadata(metadata,
                      mArtworkWidth, mArtworkHeight);
              mMetadataEditor = new MetadataEditor(legacyMetadata, editableKeys);
              metadataEditor = mMetadataEditor;
          }
          if (l != null) {
              l.onClientMetadataUpdate(metadataEditor);
          }
      }
  
      //==================================================
      private static class PlaybackInfo {
          int mState;
          long mStateChangeTimeMs;
          long mCurrentPosMs;
          float mSpeed;
  
          PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) {
              mState = state;
              mStateChangeTimeMs = stateChangeTimeMs;
              mCurrentPosMs = currentPosMs;
              mSpeed = speed;
          }
      }
  
      /**
       * @hide
       * Used by AudioManager to access user listener receiving the client update notifications
       * @return
       */
      @UnsupportedAppUsage
      OnClientUpdateListener getUpdateListener() {
          return mOnClientUpdateListener;
      }
  }
  