Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media;
     18 
     19 import android.Manifest;
     20 import android.app.ActivityManager;
     21 import android.app.PendingIntent;
     22 import android.app.PendingIntent.CanceledException;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.graphics.Bitmap;
     26 import android.media.IRemoteControlDisplay;
     27 import android.media.MediaMetadataEditor;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.Looper;
     31 import android.os.Message;
     32 import android.os.RemoteException;
     33 import android.os.ServiceManager;
     34 import android.os.SystemClock;
     35 import android.util.DisplayMetrics;
     36 import android.util.Log;
     37 import android.view.KeyEvent;
     38 
     39 import java.lang.ref.WeakReference;
     40 
     41 /**
     42  * The RemoteController class is used to control media playback, display and update media metadata
     43  * and playback status, published by applications using the {@link RemoteControlClient} class.
     44  * <p>
     45  * A RemoteController shall be registered through
     46  * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send
     47  * media event updates to the {@link OnClientUpdateListener} listener set in the class constructor.
     48  * Implement the methods of the interface to receive the information published by the active
     49  * {@link RemoteControlClient} instances.
     50  * <br>By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for
     51  * album art. Use {@link #setArtworkConfiguration(int, int)} to receive images as well.
     52  * <p>
     53  * Registration requires the {@link OnClientUpdateListener} listener to be one of the enabled
     54  * notification listeners (see {@link android.service.notification.NotificationListenerService}).
     55  */
     56 public final class RemoteController
     57 {
     58     private final static int MAX_BITMAP_DIMENSION = 512;
     59     private final static int TRANSPORT_UNKNOWN = 0;
     60     private final static String TAG = "RemoteController";
     61     private final static boolean DEBUG = false;
     62     private final static Object mGenLock = new Object();
     63     private final static Object mInfoLock = new Object();
     64     private final RcDisplay mRcd;
     65     private final Context mContext;
     66     private final AudioManager mAudioManager;
     67     private final int mMaxBitmapDimension;
     68     private MetadataEditor mMetadataEditor;
     69 
     70     /**
     71      * Synchronized on mGenLock
     72      */
     73     private int mClientGenerationIdCurrent = 0;
     74 
     75     /**
     76      * Synchronized on mInfoLock
     77      */
     78     private boolean mIsRegistered = false;
     79     private PendingIntent mClientPendingIntentCurrent;
     80     private OnClientUpdateListener mOnClientUpdateListener;
     81     private PlaybackInfo mLastPlaybackInfo;
     82     private int mArtworkWidth = -1;
     83     private int mArtworkHeight = -1;
     84     private boolean mEnabled = true;
     85 
     86     /**
     87      * Class constructor.
     88      * @param context the {@link Context}, must be non-null.
     89      * @param updateListener the listener to be called whenever new client information is available,
     90      *     must be non-null.
     91      * @throws IllegalArgumentException
     92      */
     93     public RemoteController(Context context, OnClientUpdateListener updateListener)
     94             throws IllegalArgumentException {
     95         this(context, updateListener, null);
     96     }
     97 
     98     /**
     99      * Class constructor.
    100      * @param context the {@link Context}, must be non-null.
    101      * @param updateListener the listener to be called whenever new client information is available,
    102      *     must be non-null.
    103      * @param looper the {@link Looper} on which to run the event loop,
    104      *     or null to use the current thread's looper.
    105      * @throws java.lang.IllegalArgumentException
    106      */
    107     public RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper)
    108             throws IllegalArgumentException {
    109         if (context == null) {
    110             throw new IllegalArgumentException("Invalid null Context");
    111         }
    112         if (updateListener == null) {
    113             throw new IllegalArgumentException("Invalid null OnClientUpdateListener");
    114         }
    115         if (looper != null) {
    116             mEventHandler = new EventHandler(this, looper);
    117         } else {
    118             Looper l = Looper.myLooper();
    119             if (l != null) {
    120                 mEventHandler = new EventHandler(this, l);
    121             } else {
    122                 throw new IllegalArgumentException("Calling thread not associated with a looper");
    123             }
    124         }
    125         mOnClientUpdateListener = updateListener;
    126         mContext = context;
    127         mRcd = new RcDisplay(this);
    128         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    129 
    130         if (ActivityManager.isLowRamDeviceStatic()) {
    131             mMaxBitmapDimension = MAX_BITMAP_DIMENSION;
    132         } else {
    133             final DisplayMetrics dm = context.getResources().getDisplayMetrics();
    134             mMaxBitmapDimension = Math.max(dm.widthPixels, dm.heightPixels);
    135         }
    136     }
    137 
    138 
    139     /**
    140      * Interface definition for the callbacks to be invoked whenever media events, metadata
    141      * and playback status are available.
    142      */
    143     public interface OnClientUpdateListener {
    144         /**
    145          * Called whenever all information, previously received through the other
    146          * methods of the listener, is no longer valid and is about to be refreshed.
    147          * This is typically called whenever a new {@link RemoteControlClient} has been selected
    148          * by the system to have its media information published.
    149          * @param clearing true if there is no selected RemoteControlClient and no information
    150          *     is available.
    151          */
    152         public void onClientChange(boolean clearing);
    153 
    154         /**
    155          * Called whenever the playback state has changed.
    156          * It is called when no information is known about the playback progress in the media and
    157          * the playback speed.
    158          * @param state one of the playback states authorized
    159          *     in {@link RemoteControlClient#setPlaybackState(int)}.
    160          */
    161         public void onClientPlaybackStateUpdate(int state);
    162         /**
    163          * Called whenever the playback state has changed, and playback position
    164          * and speed are known.
    165          * @param state one of the playback states authorized
    166          *     in {@link RemoteControlClient#setPlaybackState(int)}.
    167          * @param stateChangeTimeMs the system time at which the state change was reported,
    168          *     expressed in ms. Based on {@link android.os.SystemClock#elapsedRealtime()}.
    169          * @param currentPosMs a positive value for the current media playback position expressed
    170          *     in ms, a negative value if the position is temporarily unknown.
    171          * @param speed  a value expressed as a ratio of 1x playback: 1.0f is normal playback,
    172          *    2.0f is 2x, 0.5f is half-speed, -2.0f is rewind at 2x speed. 0.0f means nothing is
    173          *    playing (e.g. when state is {@link RemoteControlClient#PLAYSTATE_ERROR}).
    174          */
    175         public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
    176                 long currentPosMs, float speed);
    177         /**
    178          * Called whenever the transport control flags have changed.
    179          * @param transportControlFlags one of the flags authorized
    180          *     in {@link RemoteControlClient#setTransportControlFlags(int)}.
    181          */
    182         public void onClientTransportControlUpdate(int transportControlFlags);
    183         /**
    184          * Called whenever new metadata is available.
    185          * See the {@link MediaMetadataEditor#putLong(int, long)},
    186          *  {@link MediaMetadataEditor#putString(int, String)},
    187          *  {@link MediaMetadataEditor#putBitmap(int, Bitmap)}, and
    188          *  {@link MediaMetadataEditor#putObject(int, Object)} methods for the various keys that
    189          *  can be queried.
    190          * @param metadataEditor the container of the new metadata.
    191          */
    192         public void onClientMetadataUpdate(MetadataEditor metadataEditor);
    193     };
    194 
    195 
    196     /**
    197      * @hide
    198      */
    199     public String getRemoteControlClientPackageName() {
    200         return mClientPendingIntentCurrent != null ?
    201                 mClientPendingIntentCurrent.getCreatorPackage() : null;
    202     }
    203 
    204     /**
    205      * Return the estimated playback position of the current media track or a negative value
    206      * if not available.
    207      *
    208      * <p>The value returned is estimated by the current process and may not be perfect.
    209      * The time returned by this method is calculated from the last state change time based
    210      * on the current play position at that time and the last known playback speed.
    211      * An application may call {@link #setSynchronizationMode(int)} to apply
    212      * a synchronization policy that will periodically re-sync the estimated position
    213      * with the RemoteControlClient.</p>
    214      *
    215      * @return the current estimated playback position in milliseconds or a negative value
    216      *         if not available
    217      *
    218      * @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float)
    219      */
    220     public long getEstimatedMediaPosition() {
    221         if (mLastPlaybackInfo != null) {
    222             if (!RemoteControlClient.playbackPositionShouldMove(mLastPlaybackInfo.mState)) {
    223                 return mLastPlaybackInfo.mCurrentPosMs;
    224             }
    225 
    226             // Take the current position at the time of state change and estimate.
    227             final long thenPos = mLastPlaybackInfo.mCurrentPosMs;
    228             if (thenPos < 0) {
    229                 return -1;
    230             }
    231 
    232             final long now = SystemClock.elapsedRealtime();
    233             final long then = mLastPlaybackInfo.mStateChangeTimeMs;
    234             final long sinceThen = now - then;
    235             final long scaledSinceThen = (long) (sinceThen * mLastPlaybackInfo.mSpeed);
    236             return thenPos + scaledSinceThen;
    237         }
    238         return -1;
    239     }
    240 
    241 
    242     /**
    243      * Send a simulated key event for a media button to be received by the current client.
    244      * To simulate a key press, you must first send a KeyEvent built with
    245      * a {@link KeyEvent#ACTION_DOWN} action, then another event with the {@link KeyEvent#ACTION_UP}
    246      * action.
    247      * <p>The key event will be sent to the registered receiver
    248      * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}) whose associated
    249      * {@link RemoteControlClient}'s metadata and playback state is published (there may be
    250      * none under some circumstances).
    251      * @param keyEvent a {@link KeyEvent} instance whose key code is one of
    252      *     {@link KeyEvent#KEYCODE_MUTE},
    253      *     {@link KeyEvent#KEYCODE_HEADSETHOOK},
    254      *     {@link KeyEvent#KEYCODE_MEDIA_PLAY},
    255      *     {@link KeyEvent#KEYCODE_MEDIA_PAUSE},
    256      *     {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE},
    257      *     {@link KeyEvent#KEYCODE_MEDIA_STOP},
    258      *     {@link KeyEvent#KEYCODE_MEDIA_NEXT},
    259      *     {@link KeyEvent#KEYCODE_MEDIA_PREVIOUS},
    260      *     {@link KeyEvent#KEYCODE_MEDIA_REWIND},
    261      *     {@link KeyEvent#KEYCODE_MEDIA_RECORD},
    262      *     {@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD},
    263      *     {@link KeyEvent#KEYCODE_MEDIA_CLOSE},
    264      *     {@link KeyEvent#KEYCODE_MEDIA_EJECT},
    265      *     or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}.
    266      * @return true if the event was successfully sent, false otherwise.
    267      * @throws IllegalArgumentException
    268      */
    269     public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException {
    270         if (!MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode())) {
    271             throw new IllegalArgumentException("not a media key event");
    272         }
    273         final PendingIntent pi;
    274         synchronized(mInfoLock) {
    275             if (!mIsRegistered) {
    276                 Log.e(TAG, "Cannot use sendMediaKeyEvent() from an unregistered RemoteController");
    277                 return false;
    278             }
    279             if (!mEnabled) {
    280                 Log.e(TAG, "Cannot use sendMediaKeyEvent() from a disabled RemoteController");
    281                 return false;
    282             }
    283             pi = mClientPendingIntentCurrent;
    284         }
    285         if (pi != null) {
    286             Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    287             intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
    288             try {
    289                 pi.send(mContext, 0, intent);
    290             } catch (CanceledException e) {
    291                 Log.e(TAG, "Error sending intent for media button down: ", e);
    292                 return false;
    293             }
    294         } else {
    295             Log.i(TAG, "No-op when sending key click, no receiver right now");
    296             return false;
    297         }
    298         return true;
    299     }
    300 
    301 
    302     /**
    303      * Sets the new playback position.
    304      * This method can only be called on a registered RemoteController.
    305      * @param timeMs a 0 or positive value for the new playback position, expressed in ms.
    306      * @return true if the command to set the playback position was successfully sent.
    307      * @throws IllegalArgumentException
    308      */
    309     public boolean seekTo(long timeMs) throws IllegalArgumentException {
    310         if (!mEnabled) {
    311             Log.e(TAG, "Cannot use seekTo() from a disabled RemoteController");
    312             return false;
    313         }
    314         if (timeMs < 0) {
    315             throw new IllegalArgumentException("illegal negative time value");
    316         }
    317         final int genId;
    318         synchronized (mGenLock) {
    319             genId = mClientGenerationIdCurrent;
    320         }
    321         mAudioManager.setRemoteControlClientPlaybackPosition(genId, timeMs);
    322         return true;
    323     }
    324 
    325 
    326     /**
    327      * @hide
    328      * @param wantBitmap
    329      * @param width
    330      * @param height
    331      * @return true if successful
    332      * @throws IllegalArgumentException
    333      */
    334     public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height)
    335             throws IllegalArgumentException {
    336         synchronized (mInfoLock) {
    337             if (wantBitmap) {
    338                 if ((width > 0) && (height > 0)) {
    339                     if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; }
    340                     if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; }
    341                     mArtworkWidth = width;
    342                     mArtworkHeight = height;
    343                 } else {
    344                     throw new IllegalArgumentException("Invalid dimensions");
    345                 }
    346             } else {
    347                 mArtworkWidth = -1;
    348                 mArtworkHeight = -1;
    349             }
    350             if (mIsRegistered) {
    351                 mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd,
    352                         mArtworkWidth, mArtworkHeight);
    353             } // else new values have been stored, and will be read by AudioManager with
    354               //    RemoteController.getArtworkSize() when AudioManager.registerRemoteController()
    355               //    is called.
    356         }
    357         return true;
    358     }
    359 
    360     /**
    361      * Set the maximum artwork image dimensions to be received in the metadata.
    362      * No bitmaps will be received unless this has been specified.
    363      * @param width the maximum width in pixels
    364      * @param height  the maximum height in pixels
    365      * @return true if the artwork dimension was successfully set.
    366      * @throws IllegalArgumentException
    367      */
    368     public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException {
    369         return setArtworkConfiguration(true, width, height);
    370     }
    371 
    372     /**
    373      * Prevents this RemoteController from receiving artwork images.
    374      * @return true if receiving artwork images was successfully disabled.
    375      */
    376     public boolean clearArtworkConfiguration() {
    377         return setArtworkConfiguration(false, -1, -1);
    378     }
    379 
    380 
    381     /**
    382      * Default playback position synchronization mode where the RemoteControlClient is not
    383      * asked regularly for its playback position to see if it has drifted from the estimated
    384      * position.
    385      */
    386     public static final int POSITION_SYNCHRONIZATION_NONE = 0;
    387 
    388     /**
    389      * The playback position synchronization mode where the RemoteControlClient instances which
    390      * expose their playback position to the framework, will be regularly polled to check
    391      * whether any drift has been noticed between their estimated position and the one they report.
    392      * Note that this mode should only ever be used when needing to display very accurate playback
    393      * position, as regularly polling a RemoteControlClient for its position may have an impact
    394      * on battery life (if applicable) when this query will trigger network transactions in the
    395      * case of remote playback.
    396      */
    397     public static final int POSITION_SYNCHRONIZATION_CHECK = 1;
    398 
    399     /**
    400      * Set the playback position synchronization mode.
    401      * Must be called on a registered RemoteController.
    402      * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK}
    403      * @return true if the synchronization mode was successfully set.
    404      * @throws IllegalArgumentException
    405      */
    406     public boolean setSynchronizationMode(int sync) throws IllegalArgumentException {
    407         if ((sync != POSITION_SYNCHRONIZATION_NONE) && (sync != POSITION_SYNCHRONIZATION_CHECK)) {
    408             throw new IllegalArgumentException("Unknown synchronization mode " + sync);
    409         }
    410         if (!mIsRegistered) {
    411             Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController");
    412             return false;
    413         }
    414         mAudioManager.remoteControlDisplayWantsPlaybackPositionSync(mRcd,
    415                 POSITION_SYNCHRONIZATION_CHECK == sync);
    416         return true;
    417     }
    418 
    419 
    420     /**
    421      * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of
    422      * the current {@link RemoteControlClient}.
    423      * This method can only be called on a registered RemoteController.
    424      * @return a new MetadataEditor instance.
    425      */
    426     public MetadataEditor editMetadata() {
    427         MetadataEditor editor = new MetadataEditor();
    428         editor.mEditorMetadata = new Bundle();
    429         editor.mEditorArtwork = null;
    430         editor.mMetadataChanged = true;
    431         editor.mArtworkChanged = true;
    432         editor.mEditableKeys = 0;
    433         return editor;
    434     }
    435 
    436 
    437     /**
    438      * A class to read the metadata published by a {@link RemoteControlClient}, or send a
    439      * {@link RemoteControlClient} new values for keys that can be edited.
    440      */
    441     public class MetadataEditor extends MediaMetadataEditor {
    442         /**
    443          * @hide
    444          */
    445         protected MetadataEditor() { }
    446 
    447         /**
    448          * @hide
    449          */
    450         protected MetadataEditor(Bundle metadata, long editableKeys) {
    451             mEditorMetadata = metadata;
    452             mEditableKeys = editableKeys;
    453 
    454             mEditorArtwork = (Bitmap) metadata.getParcelable(
    455                     String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK));
    456             if (mEditorArtwork != null) {
    457                 cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK);
    458             }
    459 
    460             mMetadataChanged = true;
    461             mArtworkChanged = true;
    462             mApplied = false;
    463         }
    464 
    465         private void cleanupBitmapFromBundle(int key) {
    466             if (METADATA_KEYS_TYPE.get(key, METADATA_TYPE_INVALID) == METADATA_TYPE_BITMAP) {
    467                 mEditorMetadata.remove(String.valueOf(key));
    468             }
    469         }
    470 
    471         /**
    472          * Applies all of the metadata changes that have been set since the MediaMetadataEditor
    473          * instance was created with {@link RemoteController#editMetadata()}
    474          * or since {@link #clear()} was called.
    475          */
    476         public synchronized void apply() {
    477             // "applying" a metadata bundle in RemoteController is only for sending edited
    478             // key values back to the RemoteControlClient, so here we only care about the only
    479             // editable key we support: RATING_KEY_BY_USER
    480             if (!mMetadataChanged) {
    481                 return;
    482             }
    483             final int genId;
    484             synchronized(mGenLock) {
    485                 genId = mClientGenerationIdCurrent;
    486             }
    487             synchronized(mInfoLock) {
    488                 if (mEditorMetadata.containsKey(
    489                         String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) {
    490                     Rating rating = (Rating) getObject(
    491                             MediaMetadataEditor.RATING_KEY_BY_USER, null);
    492                     mAudioManager.updateRemoteControlClientMetadata(genId,
    493                           MediaMetadataEditor.RATING_KEY_BY_USER,
    494                           rating);
    495                 } else {
    496                     Log.e(TAG, "no metadata to apply");
    497                 }
    498                 // NOT setting mApplied to true as this type of MetadataEditor will be applied
    499                 // multiple times, whenever the user of a RemoteController needs to change the
    500                 // metadata (e.g. user changes the rating of a song more than once during playback)
    501                 mApplied = false;
    502             }
    503         }
    504 
    505     }
    506 
    507 
    508     //==================================================
    509     // Implementation of IRemoteControlDisplay interface
    510     private static class RcDisplay extends IRemoteControlDisplay.Stub {
    511         private final WeakReference<RemoteController> mController;
    512 
    513         RcDisplay(RemoteController rc) {
    514             mController = new WeakReference<RemoteController>(rc);
    515         }
    516 
    517         public void setCurrentClientId(int genId, PendingIntent clientMediaIntent,
    518                 boolean clearing) {
    519             final RemoteController rc = mController.get();
    520             if (rc == null) {
    521                 return;
    522             }
    523             boolean isNew = false;
    524             synchronized(mGenLock) {
    525                 if (rc.mClientGenerationIdCurrent != genId) {
    526                     rc.mClientGenerationIdCurrent = genId;
    527                     isNew = true;
    528                 }
    529             }
    530             if (clientMediaIntent != null) {
    531                 sendMsg(rc.mEventHandler, MSG_NEW_PENDING_INTENT, SENDMSG_REPLACE,
    532                         genId /*arg1*/, 0, clientMediaIntent /*obj*/, 0 /*delay*/);
    533             }
    534             if (isNew || clearing) {
    535                 sendMsg(rc.mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE,
    536                         genId /*arg1*/, clearing ? 1 : 0, null /*obj*/, 0 /*delay*/);
    537             }
    538         }
    539 
    540         public void setEnabled(boolean enabled) {
    541             final RemoteController rc = mController.get();
    542             if (rc == null) {
    543                 return;
    544             }
    545             sendMsg(rc.mEventHandler, MSG_DISPLAY_ENABLE, SENDMSG_REPLACE,
    546                     enabled ? 1 : 0 /*arg1*/, 0, null /*obj*/, 0 /*delay*/);
    547         }
    548 
    549         public void setPlaybackState(int genId, int state,
    550                 long stateChangeTimeMs, long currentPosMs, float speed) {
    551             final RemoteController rc = mController.get();
    552             if (rc == null) {
    553                 return;
    554             }
    555             if (DEBUG) {
    556                 Log.d(TAG, "> new playback state: genId="+genId
    557                         + " state="+ state
    558                         + " changeTime="+ stateChangeTimeMs
    559                         + " pos=" + currentPosMs
    560                         + "ms speed=" + speed);
    561             }
    562 
    563             synchronized(mGenLock) {
    564                 if (rc.mClientGenerationIdCurrent != genId) {
    565                     return;
    566                 }
    567             }
    568             final PlaybackInfo playbackInfo =
    569                     new PlaybackInfo(state, stateChangeTimeMs, currentPosMs, speed);
    570             sendMsg(rc.mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE,
    571                     genId /*arg1*/, 0, playbackInfo /*obj*/, 0 /*delay*/);
    572 
    573         }
    574 
    575         public void setTransportControlInfo(int genId, int transportControlFlags,
    576                 int posCapabilities) {
    577             final RemoteController rc = mController.get();
    578             if (rc == null) {
    579                 return;
    580             }
    581             synchronized(mGenLock) {
    582                 if (rc.mClientGenerationIdCurrent != genId) {
    583                     return;
    584                 }
    585             }
    586             sendMsg(rc.mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE,
    587                     genId /*arg1*/, transportControlFlags /*arg2*/,
    588                     null /*obj*/, 0 /*delay*/);
    589         }
    590 
    591         public void setMetadata(int genId, Bundle metadata) {
    592             final RemoteController rc = mController.get();
    593             if (rc == null) {
    594                 return;
    595             }
    596             if (DEBUG) { Log.e(TAG, "setMetadata("+genId+")"); }
    597             if (metadata == null) {
    598                 return;
    599             }
    600             synchronized(mGenLock) {
    601                 if (rc.mClientGenerationIdCurrent != genId) {
    602                     return;
    603                 }
    604             }
    605             sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
    606                     genId /*arg1*/, 0 /*arg2*/,
    607                     metadata /*obj*/, 0 /*delay*/);
    608         }
    609 
    610         public void setArtwork(int genId, Bitmap artwork) {
    611             final RemoteController rc = mController.get();
    612             if (rc == null) {
    613                 return;
    614             }
    615             if (DEBUG) { Log.v(TAG, "setArtwork("+genId+")"); }
    616             synchronized(mGenLock) {
    617                 if (rc.mClientGenerationIdCurrent != genId) {
    618                     return;
    619                 }
    620             }
    621             Bundle metadata = new Bundle(1);
    622             metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), artwork);
    623             sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
    624                     genId /*arg1*/, 0 /*arg2*/,
    625                     metadata /*obj*/, 0 /*delay*/);
    626         }
    627 
    628         public void setAllMetadata(int genId, Bundle metadata, Bitmap artwork) {
    629             final RemoteController rc = mController.get();
    630             if (rc == null) {
    631                 return;
    632             }
    633             if (DEBUG) { Log.e(TAG, "setAllMetadata("+genId+")"); }
    634             if ((metadata == null) && (artwork == null)) {
    635                 return;
    636             }
    637             synchronized(mGenLock) {
    638                 if (rc.mClientGenerationIdCurrent != genId) {
    639                     return;
    640                 }
    641             }
    642             if (metadata == null) {
    643                 metadata = new Bundle(1);
    644             }
    645             if (artwork != null) {
    646                 metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK),
    647                         artwork);
    648             }
    649             sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
    650                     genId /*arg1*/, 0 /*arg2*/,
    651                     metadata /*obj*/, 0 /*delay*/);
    652         }
    653     }
    654 
    655     //==================================================
    656     // Event handling
    657     private final EventHandler mEventHandler;
    658     private final static int MSG_NEW_PENDING_INTENT = 0;
    659     private final static int MSG_NEW_PLAYBACK_INFO =  1;
    660     private final static int MSG_NEW_TRANSPORT_INFO = 2;
    661     private final static int MSG_NEW_METADATA       = 3; // msg always has non-null obj parameter
    662     private final static int MSG_CLIENT_CHANGE      = 4;
    663     private final static int MSG_DISPLAY_ENABLE     = 5;
    664 
    665     private class EventHandler extends Handler {
    666 
    667         public EventHandler(RemoteController rc, Looper looper) {
    668             super(looper);
    669         }
    670 
    671         @Override
    672         public void handleMessage(Message msg) {
    673             switch(msg.what) {
    674                 case MSG_NEW_PENDING_INTENT:
    675                     onNewPendingIntent(msg.arg1, (PendingIntent) msg.obj);
    676                     break;
    677                 case MSG_NEW_PLAYBACK_INFO:
    678                     onNewPlaybackInfo(msg.arg1, (PlaybackInfo) msg.obj);
    679                     break;
    680                 case MSG_NEW_TRANSPORT_INFO:
    681                     onNewTransportInfo(msg.arg1, msg.arg2);
    682                     break;
    683                 case MSG_NEW_METADATA:
    684                     onNewMetadata(msg.arg1, (Bundle)msg.obj);
    685                     break;
    686                 case MSG_CLIENT_CHANGE:
    687                     onClientChange(msg.arg1, msg.arg2 == 1);
    688                     break;
    689                 case MSG_DISPLAY_ENABLE:
    690                     onDisplayEnable(msg.arg1 == 1);
    691                     break;
    692                 default:
    693                     Log.e(TAG, "unknown event " + msg.what);
    694             }
    695         }
    696     }
    697 
    698     /** If the msg is already queued, replace it with this one. */
    699     private static final int SENDMSG_REPLACE = 0;
    700     /** If the msg is already queued, ignore this one and leave the old. */
    701     private static final int SENDMSG_NOOP = 1;
    702     /** If the msg is already queued, queue this one and leave the old. */
    703     private static final int SENDMSG_QUEUE = 2;
    704 
    705     private static void sendMsg(Handler handler, int msg, int existingMsgPolicy,
    706             int arg1, int arg2, Object obj, int delayMs) {
    707         if (handler == null) {
    708             Log.e(TAG, "null event handler, will not deliver message " + msg);
    709             return;
    710         }
    711         if (existingMsgPolicy == SENDMSG_REPLACE) {
    712             handler.removeMessages(msg);
    713         } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
    714             return;
    715         }
    716         handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs);
    717     }
    718 
    719     private void onNewPendingIntent(int genId, PendingIntent pi) {
    720         synchronized(mGenLock) {
    721             if (mClientGenerationIdCurrent != genId) {
    722                 return;
    723             }
    724         }
    725         synchronized(mInfoLock) {
    726             mClientPendingIntentCurrent = pi;
    727         }
    728     }
    729 
    730     private void onNewPlaybackInfo(int genId, PlaybackInfo pi) {
    731         synchronized(mGenLock) {
    732             if (mClientGenerationIdCurrent != genId) {
    733                 return;
    734             }
    735         }
    736         final OnClientUpdateListener l;
    737         synchronized(mInfoLock) {
    738             l = this.mOnClientUpdateListener;
    739             mLastPlaybackInfo = pi;
    740         }
    741         if (l != null) {
    742             if (pi.mCurrentPosMs == RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN) {
    743                 l.onClientPlaybackStateUpdate(pi.mState);
    744             } else {
    745                 l.onClientPlaybackStateUpdate(pi.mState, pi.mStateChangeTimeMs, pi.mCurrentPosMs,
    746                         pi.mSpeed);
    747             }
    748         }
    749     }
    750 
    751     private void onNewTransportInfo(int genId, int transportControlFlags) {
    752         synchronized(mGenLock) {
    753             if (mClientGenerationIdCurrent != genId) {
    754                 return;
    755             }
    756         }
    757         final OnClientUpdateListener l;
    758         synchronized(mInfoLock) {
    759             l = mOnClientUpdateListener;
    760         }
    761         if (l != null) {
    762             l.onClientTransportControlUpdate(transportControlFlags);
    763         }
    764     }
    765 
    766     /**
    767      * @param genId
    768      * @param metadata guaranteed to be always non-null
    769      */
    770     private void onNewMetadata(int genId, Bundle metadata) {
    771         synchronized(mGenLock) {
    772             if (mClientGenerationIdCurrent != genId) {
    773                 return;
    774             }
    775         }
    776         final OnClientUpdateListener l;
    777         final MetadataEditor metadataEditor;
    778         // prepare the received Bundle to be used inside a MetadataEditor
    779         final long editableKeys = metadata.getLong(
    780                 String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK), 0);
    781         if (editableKeys != 0) {
    782             metadata.remove(String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK));
    783         }
    784         synchronized(mInfoLock) {
    785             l = mOnClientUpdateListener;
    786             if ((mMetadataEditor != null) && (mMetadataEditor.mEditorMetadata != null)) {
    787                 if (mMetadataEditor.mEditorMetadata != metadata) {
    788                     // existing metadata, merge existing and new
    789                     mMetadataEditor.mEditorMetadata.putAll(metadata);
    790                 }
    791 
    792                 mMetadataEditor.putBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK,
    793                         (Bitmap)metadata.getParcelable(
    794                                 String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK)));
    795                 mMetadataEditor.cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK);
    796             } else {
    797                 mMetadataEditor = new MetadataEditor(metadata, editableKeys);
    798             }
    799             metadataEditor = mMetadataEditor;
    800         }
    801         if (l != null) {
    802             l.onClientMetadataUpdate(metadataEditor);
    803         }
    804     }
    805 
    806     private void onClientChange(int genId, boolean clearing) {
    807         synchronized(mGenLock) {
    808             if (mClientGenerationIdCurrent != genId) {
    809                 return;
    810             }
    811         }
    812         final OnClientUpdateListener l;
    813         synchronized(mInfoLock) {
    814             l = mOnClientUpdateListener;
    815             mMetadataEditor = null;
    816         }
    817         if (l != null) {
    818             l.onClientChange(clearing);
    819         }
    820     }
    821 
    822     private void onDisplayEnable(boolean enabled) {
    823         final OnClientUpdateListener l;
    824         synchronized(mInfoLock) {
    825             mEnabled = enabled;
    826             l = this.mOnClientUpdateListener;
    827         }
    828         if (!enabled) {
    829             // when disabling, reset all info sent to the user
    830             final int genId;
    831             synchronized (mGenLock) {
    832                 genId = mClientGenerationIdCurrent;
    833             }
    834             // send "stopped" state, happened "now", playback position is 0, speed 0.0f
    835             final PlaybackInfo pi = new PlaybackInfo(RemoteControlClient.PLAYSTATE_STOPPED,
    836                     SystemClock.elapsedRealtime() /*stateChangeTimeMs*/,
    837                     0 /*currentPosMs*/, 0.0f /*speed*/);
    838             sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE,
    839                     genId /*arg1*/, 0 /*arg2, ignored*/, pi /*obj*/, 0 /*delay*/);
    840             // send "blank" transport control info: no controls are supported
    841             sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE,
    842                     genId /*arg1*/, 0 /*arg2, no flags*/,
    843                     null /*obj, ignored*/, 0 /*delay*/);
    844             // send dummy metadata with empty string for title and artist, duration of 0
    845             Bundle metadata = new Bundle(3);
    846             metadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_TITLE), "");
    847             metadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ARTIST), "");
    848             metadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DURATION), 0);
    849             sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
    850                     genId /*arg1*/, 0 /*arg2, ignored*/, metadata /*obj*/, 0 /*delay*/);
    851         }
    852     }
    853 
    854     //==================================================
    855     private static class PlaybackInfo {
    856         int mState;
    857         long mStateChangeTimeMs;
    858         long mCurrentPosMs;
    859         float mSpeed;
    860 
    861         PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) {
    862             mState = state;
    863             mStateChangeTimeMs = stateChangeTimeMs;
    864             mCurrentPosMs = currentPosMs;
    865             mSpeed = speed;
    866         }
    867     }
    868 
    869     /**
    870      * @hide
    871      * Used by AudioManager to mark this instance as registered.
    872      * @param registered
    873      */
    874     void setIsRegistered(boolean registered) {
    875         synchronized (mInfoLock) {
    876             mIsRegistered = registered;
    877         }
    878     }
    879 
    880     /**
    881      * @hide
    882      * Used by AudioManager to access binder to be registered/unregistered inside MediaFocusControl
    883      * @return
    884      */
    885     RcDisplay getRcDisplay() {
    886         return mRcd;
    887     }
    888 
    889     /**
    890      * @hide
    891      * Used by AudioManager to read the current artwork dimension
    892      * @return array containing width (index 0) and height (index 1) of currently set artwork size
    893      */
    894     int[] getArtworkSize() {
    895         synchronized (mInfoLock) {
    896             int[] size = { mArtworkWidth, mArtworkHeight };
    897             return size;
    898         }
    899     }
    900 
    901     /**
    902      * @hide
    903      * Used by AudioManager to access user listener receiving the client update notifications
    904      * @return
    905      */
    906     OnClientUpdateListener getUpdateListener() {
    907         return mOnClientUpdateListener;
    908     }
    909 }
    910