blob: 4c71aced56c5abee377facc883de14eeb29fd227 [file] [log] [blame]
/*
* Copyright (C) 2011 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.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.media.MediaMetadataRetriever;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.util.Log;
import java.lang.IllegalArgumentException;
/**
* RemoteControlClient enables exposing information meant to be consumed by remote controls
* capable of displaying metadata, artwork and media transport control buttons.
*
* <p>A remote control client object is associated with a media button event receiver. This
* event receiver must have been previously registered with
* {@link AudioManager#registerMediaButtonEventReceiver(ComponentName)} before the
* RemoteControlClient can be registered through
* {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.
*
* <p>Here is an example of creating a RemoteControlClient instance after registering a media
* button event receiver:
* <pre>ComponentName myEventReceiver = new ComponentName(getPackageName(), MyRemoteControlEventReceiver.class.getName());
* AudioManager myAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
* myAudioManager.registerMediaButtonEventReceiver(myEventReceiver);
* // build the PendingIntent for the remote control client
* Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
* mediaButtonIntent.setComponent(myEventReceiver);
* PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0);
* // create and register the remote control client
* RemoteControlClient myRemoteControlClient = new RemoteControlClient(mediaPendingIntent);
* myAudioManager.registerRemoteControlClient(myRemoteControlClient);</pre>
*/
public class RemoteControlClient
{
private final static String TAG = "RemoteControlClient";
/**
* Playback state of a RemoteControlClient which is stopped.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_STOPPED = 1;
/**
* Playback state of a RemoteControlClient which is paused.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_PAUSED = 2;
/**
* Playback state of a RemoteControlClient which is playing media.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_PLAYING = 3;
/**
* Playback state of a RemoteControlClient which is fast forwarding in the media
* it is currently playing.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_FAST_FORWARDING = 4;
/**
* Playback state of a RemoteControlClient which is fast rewinding in the media
* it is currently playing.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_REWINDING = 5;
/**
* Playback state of a RemoteControlClient which is skipping to the next
* logical chapter (such as a song in a playlist) in the media it is currently playing.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_SKIPPING_FORWARDS = 6;
/**
* Playback state of a RemoteControlClient which is skipping back to the previous
* logical chapter (such as a song in a playlist) in the media it is currently playing.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_SKIPPING_BACKWARDS = 7;
/**
* Playback state of a RemoteControlClient which is buffering data to play before it can
* start or resume playback.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_BUFFERING = 8;
/**
* Playback state of a RemoteControlClient which cannot perform any playback related
* operation because of an internal error. Examples of such situations are no network
* connectivity when attempting to stream data from a server, or expired user credentials
* when trying to play subscription-based content.
*
* @see #setPlaybackState(int)
*/
public final static int PLAYSTATE_ERROR = 9;
/**
* @hide
* The value of a playback state when none has been declared.
* Intentionally hidden as an application shouldn't set such a playback state value.
*/
public final static int PLAYSTATE_NONE = 0;
/**
* @hide
* The default playback type, "local", indicating the presentation of the media is happening on
* the same device (e.g. a phone, a tablet) as where it is controlled from.
*/
public final static int PLAYBACK_TYPE_LOCAL = 0;
/**
* @hide
* A playback type indicating the presentation of the media is happening on
* a different device (i.e. the remote device) than where it is controlled from.
*/
public final static int PLAYBACK_TYPE_REMOTE = 1;
private final static int PLAYBACK_TYPE_MIN = PLAYBACK_TYPE_LOCAL;
private final static int PLAYBACK_TYPE_MAX = PLAYBACK_TYPE_REMOTE;
/**
* @hide
* Playback information indicating the playback volume is fixed, i.e. it cannot be controlled
* from this object. An example of fixed playback volume is a remote player, playing over HDMI
* where the user prefer to control the volume on the HDMI sink, rather than attenuate at the
* source.
* @see #PLAYBACKINFO_VOLUME_HANDLING.
*/
public final static int PLAYBACK_VOLUME_FIXED = 0;
/**
* @hide
* Playback information indicating the playback volume is variable and can be controlled from
* this object.
* @see #PLAYBACKINFO_VOLUME_HANDLING.
*/
public final static int PLAYBACK_VOLUME_VARIABLE = 1;
/**
* @hide (to be un-hidden)
* The playback information value indicating the value of a given information type is invalid.
* @see #PLAYBACKINFO_VOLUME_HANDLING.
*/
public final static int PLAYBACKINFO_INVALID_VALUE = Integer.MIN_VALUE;
//==========================================
// Public keys for playback information
/**
* @hide
* Playback information that defines the type of playback associated with this
* RemoteControlClient. See {@link #PLAYBACK_TYPE_LOCAL} and {@link #PLAYBACK_TYPE_REMOTE}.
*/
public final static int PLAYBACKINFO_PLAYBACK_TYPE = 1;
/**
* @hide
* Playback information that defines at what volume the playback associated with this
* RemoteControlClient is performed. This information is only used when the playback type is not
* local (see {@link #PLAYBACKINFO_PLAYBACK_TYPE}).
*/
public final static int PLAYBACKINFO_VOLUME = 2;
/**
* @hide
* Playback information that defines the maximum volume volume value that is supported
* by the playback associated with this RemoteControlClient. This information is only used
* when the playback type is not local (see {@link #PLAYBACKINFO_PLAYBACK_TYPE}).
*/
public final static int PLAYBACKINFO_VOLUME_MAX = 3;
/**
* @hide
* Playback information that defines how volume is handled for the presentation of the media.
* @see #PLAYBACK_VOLUME_FIXED
* @see #PLAYBACK_VOLUME_VARIABLE
*/
public final static int PLAYBACKINFO_VOLUME_HANDLING = 4;
/**
* @hide
* Playback information that defines over what stream type the media is presented.
*/
public final static int PLAYBACKINFO_USES_STREAM = 5;
//==========================================
// Private keys for playback information
/**
* @hide
* Used internally to relay playback state (set by the application with
* {@link #setPlaybackState(int)}) to AudioService
*/
public final static int PLAYBACKINFO_PLAYSTATE = 255;
/**
* Flag indicating a RemoteControlClient makes use of the "previous" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_PREVIOUS
*/
public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
/**
* Flag indicating a RemoteControlClient makes use of the "rewind" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_REWIND
*/
public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
/**
* Flag indicating a RemoteControlClient makes use of the "play" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_PLAY
*/
public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
/**
* Flag indicating a RemoteControlClient makes use of the "play/pause" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE
*/
public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
/**
* Flag indicating a RemoteControlClient makes use of the "pause" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_PAUSE
*/
public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
/**
* Flag indicating a RemoteControlClient makes use of the "stop" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_STOP
*/
public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
/**
* Flag indicating a RemoteControlClient makes use of the "fast forward" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_FAST_FORWARD
*/
public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
/**
* Flag indicating a RemoteControlClient makes use of the "next" media key.
*
* @see #setTransportControlFlags(int)
* @see android.view.KeyEvent#KEYCODE_MEDIA_NEXT
*/
public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
/**
* @hide
* The flags for when no media keys are declared supported.
* Intentionally hidden as an application shouldn't set the transport control flags
* to this value.
*/
public final static int FLAGS_KEY_MEDIA_NONE = 0;
/**
* @hide
* Flag used to signal some type of metadata exposed by the RemoteControlClient is requested.
*/
public final static int FLAG_INFORMATION_REQUEST_METADATA = 1 << 0;
/**
* @hide
* Flag used to signal that the transport control buttons supported by the
* RemoteControlClient are requested.
* This can for instance happen when playback is at the end of a playlist, and the "next"
* operation is not supported anymore.
*/
public final static int FLAG_INFORMATION_REQUEST_KEY_MEDIA = 1 << 1;
/**
* @hide
* Flag used to signal that the playback state of the RemoteControlClient is requested.
*/
public final static int FLAG_INFORMATION_REQUEST_PLAYSTATE = 1 << 2;
/**
* @hide
* Flag used to signal that the album art for the RemoteControlClient is requested.
*/
public final static int FLAG_INFORMATION_REQUEST_ALBUM_ART = 1 << 3;
/**
* Class constructor.
* @param mediaButtonIntent The intent that will be sent for the media button events sent
* by remote controls.
* This intent needs to have been constructed with the {@link Intent#ACTION_MEDIA_BUTTON}
* action, and have a component that will handle the intent (set with
* {@link Intent#setComponent(ComponentName)}) registered with
* {@link AudioManager#registerMediaButtonEventReceiver(ComponentName)}
* before this new RemoteControlClient can itself be registered with
* {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.
* @see AudioManager#registerMediaButtonEventReceiver(ComponentName)
* @see AudioManager#registerRemoteControlClient(RemoteControlClient)
*/
public RemoteControlClient(PendingIntent mediaButtonIntent) {
mRcMediaIntent = mediaButtonIntent;
Looper looper;
if ((looper = Looper.myLooper()) != null) {
mEventHandler = new EventHandler(this, looper);
} else if ((looper = Looper.getMainLooper()) != null) {
mEventHandler = new EventHandler(this, looper);
} else {
mEventHandler = null;
Log.e(TAG, "RemoteControlClient() couldn't find main application thread");
}
}
/**
* Class constructor for a remote control client whose internal event handling
* happens on a user-provided Looper.
* @param mediaButtonIntent The intent that will be sent for the media button events sent
* by remote controls.
* This intent needs to have been constructed with the {@link Intent#ACTION_MEDIA_BUTTON}
* action, and have a component that will handle the intent (set with
* {@link Intent#setComponent(ComponentName)}) registered with
* {@link AudioManager#registerMediaButtonEventReceiver(ComponentName)}
* before this new RemoteControlClient can itself be registered with
* {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.
* @param looper The Looper running the event loop.
* @see AudioManager#registerMediaButtonEventReceiver(ComponentName)
* @see AudioManager#registerRemoteControlClient(RemoteControlClient)
*/
public RemoteControlClient(PendingIntent mediaButtonIntent, Looper looper) {
mRcMediaIntent = mediaButtonIntent;
mEventHandler = new EventHandler(this, looper);
}
private static final int[] METADATA_KEYS_TYPE_STRING = {
MediaMetadataRetriever.METADATA_KEY_ALBUM,
MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
MediaMetadataRetriever.METADATA_KEY_TITLE,
MediaMetadataRetriever.METADATA_KEY_ARTIST,
MediaMetadataRetriever.METADATA_KEY_AUTHOR,
MediaMetadataRetriever.METADATA_KEY_COMPILATION,
MediaMetadataRetriever.METADATA_KEY_COMPOSER,
MediaMetadataRetriever.METADATA_KEY_DATE,
MediaMetadataRetriever.METADATA_KEY_GENRE,
MediaMetadataRetriever.METADATA_KEY_TITLE,
MediaMetadataRetriever.METADATA_KEY_WRITER };
private static final int[] METADATA_KEYS_TYPE_LONG = {
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER,
MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER,
MediaMetadataRetriever.METADATA_KEY_DURATION };
/**
* Class used to modify metadata in a {@link RemoteControlClient} object.
* Use {@link RemoteControlClient#editMetadata(boolean)} to create an instance of an editor,
* on which you set the metadata for the RemoteControlClient instance. Once all the information
* has been set, use {@link #apply()} to make it the new metadata that should be displayed
* for the associated client. Once the metadata has been "applied", you cannot reuse this
* instance of the MetadataEditor.
*/
public class MetadataEditor {
/**
* @hide
*/
protected boolean mMetadataChanged;
/**
* @hide
*/
protected boolean mArtworkChanged;
/**
* @hide
*/
protected Bitmap mEditorArtwork;
/**
* @hide
*/
protected Bundle mEditorMetadata;
private boolean mApplied = false;
// only use RemoteControlClient.editMetadata() to get a MetadataEditor instance
private MetadataEditor() { }
/**
* @hide
*/
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* The metadata key for the content artwork / album art.
*/
public final static int BITMAP_KEY_ARTWORK = 100;
/**
* @hide
* TODO(jmtrivi) have lockscreen and music move to the new key name
*/
public final static int METADATA_KEY_ARTWORK = BITMAP_KEY_ARTWORK;
/**
* Adds textual information to be displayed.
* Note that none of the information added after {@link #apply()} has been called,
* will be displayed.
* @param key The identifier of a the metadata field to set. Valid values are
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUM},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_ARTIST},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_AUTHOR},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPILATION},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPOSER},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_DATE},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_GENRE},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_WRITER}.
* @param value The text for the given key, or {@code null} to signify there is no valid
* information for the field.
* @return Returns a reference to the same MetadataEditor object, so you can chain put
* calls together.
*/
public synchronized MetadataEditor putString(int key, String value)
throws IllegalArgumentException {
if (mApplied) {
Log.e(TAG, "Can't edit a previously applied MetadataEditor");
return this;
}
if (!validTypeForKey(key, METADATA_KEYS_TYPE_STRING)) {
throw(new IllegalArgumentException("Invalid type 'String' for key "+ key));
}
mEditorMetadata.putString(String.valueOf(key), value);
mMetadataChanged = true;
return this;
}
/**
* Adds numerical information to be displayed.
* Note that none of the information added after {@link #apply()} has been called,
* will be displayed.
* @param key the identifier of a the metadata field to set. Valid values are
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER},
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_DURATION} (with a value
* expressed in milliseconds),
* {@link android.media.MediaMetadataRetriever#METADATA_KEY_YEAR}.
* @param value The long value for the given key
* @return Returns a reference to the same MetadataEditor object, so you can chain put
* calls together.
* @throws IllegalArgumentException
*/
public synchronized MetadataEditor putLong(int key, long value)
throws IllegalArgumentException {
if (mApplied) {
Log.e(TAG, "Can't edit a previously applied MetadataEditor");
return this;
}
if (!validTypeForKey(key, METADATA_KEYS_TYPE_LONG)) {
throw(new IllegalArgumentException("Invalid type 'long' for key "+ key));
}
mEditorMetadata.putLong(String.valueOf(key), value);
mMetadataChanged = true;
return this;
}
/**
* Sets the album / artwork picture to be displayed on the remote control.
* @param key the identifier of the bitmap to set. The only valid value is
* {@link #BITMAP_KEY_ARTWORK}
* @param bitmap The bitmap for the artwork, or null if there isn't any.
* @return Returns a reference to the same MetadataEditor object, so you can chain put
* calls together.
* @throws IllegalArgumentException
* @see android.graphics.Bitmap
*/
public synchronized MetadataEditor putBitmap(int key, Bitmap bitmap)
throws IllegalArgumentException {
if (mApplied) {
Log.e(TAG, "Can't edit a previously applied MetadataEditor");
return this;
}
if (key != BITMAP_KEY_ARTWORK) {
throw(new IllegalArgumentException("Invalid type 'Bitmap' for key "+ key));
}
if ((mArtworkExpectedWidth > 0) && (mArtworkExpectedHeight > 0)) {
mEditorArtwork = scaleBitmapIfTooBig(bitmap,
mArtworkExpectedWidth, mArtworkExpectedHeight);
} else {
// no valid resize dimensions, store as is
mEditorArtwork = bitmap;
}
mArtworkChanged = true;
return this;
}
/**
* Clears all the metadata that has been set since the MetadataEditor instance was
* created with {@link RemoteControlClient#editMetadata(boolean)}.
*/
public synchronized void clear() {
if (mApplied) {
Log.e(TAG, "Can't clear a previously applied MetadataEditor");
return;
}
mEditorMetadata.clear();
mEditorArtwork = null;
}
/**
* Associates all the metadata that has been set since the MetadataEditor instance was
* created with {@link RemoteControlClient#editMetadata(boolean)}, or since
* {@link #clear()} was called, with the RemoteControlClient. Once "applied",
* this MetadataEditor cannot be reused to edit the RemoteControlClient's metadata.
*/
public synchronized void apply() {
if (mApplied) {
Log.e(TAG, "Can't apply a previously applied MetadataEditor");
return;
}
synchronized(mCacheLock) {
// assign the edited data
mMetadata = new Bundle(mEditorMetadata);
if ((mArtwork != null) && (!mArtwork.equals(mEditorArtwork))) {
mArtwork.recycle();
}
mArtwork = mEditorArtwork;
mEditorArtwork = null;
if (mMetadataChanged & mArtworkChanged) {
// send to remote control display if conditions are met
sendMetadataWithArtwork_syncCacheLock();
} else if (mMetadataChanged) {
// send to remote control display if conditions are met
sendMetadata_syncCacheLock();
} else if (mArtworkChanged) {
// send to remote control display if conditions are met
sendArtwork_syncCacheLock();
}
mApplied = true;
}
}
}
/**
* Creates a {@link MetadataEditor}.
* @param startEmpty Set to false if you want the MetadataEditor to contain the metadata that
* was previously applied to the RemoteControlClient, or true if it is to be created empty.
* @return a new MetadataEditor instance.
*/
public MetadataEditor editMetadata(boolean startEmpty) {
MetadataEditor editor = new MetadataEditor();
if (startEmpty) {
editor.mEditorMetadata = new Bundle();
editor.mEditorArtwork = null;
editor.mMetadataChanged = true;
editor.mArtworkChanged = true;
} else {
editor.mEditorMetadata = new Bundle(mMetadata);
editor.mEditorArtwork = mArtwork;
editor.mMetadataChanged = false;
editor.mArtworkChanged = false;
}
return editor;
}
/**
* Sets the current playback state.
* @param state The current playback state, one of the following values:
* {@link #PLAYSTATE_STOPPED},
* {@link #PLAYSTATE_PAUSED},
* {@link #PLAYSTATE_PLAYING},
* {@link #PLAYSTATE_FAST_FORWARDING},
* {@link #PLAYSTATE_REWINDING},
* {@link #PLAYSTATE_SKIPPING_FORWARDS},
* {@link #PLAYSTATE_SKIPPING_BACKWARDS},
* {@link #PLAYSTATE_BUFFERING},
* {@link #PLAYSTATE_ERROR}.
*/
public void setPlaybackState(int state) {
synchronized(mCacheLock) {
if (mPlaybackState != state) {
// store locally
mPlaybackState = state;
// keep track of when the state change occurred
mPlaybackStateChangeTimeMs = SystemClock.elapsedRealtime();
// send to remote control display if conditions are met
sendPlaybackState_syncCacheLock();
// update AudioService
sendAudioServiceNewPlaybackInfo_syncCacheLock(PLAYBACKINFO_PLAYSTATE, state);
}
}
}
/**
* Sets the flags for the media transport control buttons that this client supports.
* @param transportControlFlags A combination of the following flags:
* {@link #FLAG_KEY_MEDIA_PREVIOUS},
* {@link #FLAG_KEY_MEDIA_REWIND},
* {@link #FLAG_KEY_MEDIA_PLAY},
* {@link #FLAG_KEY_MEDIA_PLAY_PAUSE},
* {@link #FLAG_KEY_MEDIA_PAUSE},
* {@link #FLAG_KEY_MEDIA_STOP},
* {@link #FLAG_KEY_MEDIA_FAST_FORWARD},
* {@link #FLAG_KEY_MEDIA_NEXT}
*/
public void setTransportControlFlags(int transportControlFlags) {
synchronized(mCacheLock) {
// store locally
mTransportControlFlags = transportControlFlags;
// send to remote control display if conditions are met
sendTransportControlFlags_syncCacheLock();
}
}
/** @hide */
public final static int DEFAULT_PLAYBACK_VOLUME_HANDLING = PLAYBACK_VOLUME_VARIABLE;
/** @hide */
// hard-coded to the same number of steps as AudioService.MAX_STREAM_VOLUME[STREAM_MUSIC]
public final static int DEFAULT_PLAYBACK_VOLUME = 15;
private int mPlaybackType = PLAYBACK_TYPE_LOCAL;
private int mPlaybackVolumeMax = DEFAULT_PLAYBACK_VOLUME;
private int mPlaybackVolume = DEFAULT_PLAYBACK_VOLUME;
private int mPlaybackVolumeHandling = DEFAULT_PLAYBACK_VOLUME_HANDLING;
private int mPlaybackStream = AudioManager.STREAM_MUSIC;
/**
* @hide
* Set information describing information related to the playback of media so the system
* can implement additional behavior to handle non-local playback usecases.
* @param what a key to specify the type of information to set. Valid keys are
* {@link #PLAYBACKINFO_PLAYBACK_TYPE},
* {@link #PLAYBACKINFO_USES_STREAM},
* {@link #PLAYBACKINFO_VOLUME},
* {@link #PLAYBACKINFO_VOLUME_MAX},
* and {@link #PLAYBACKINFO_VOLUME_HANDLING}.
* @param value the value for the supplied information to set.
*/
public void setPlaybackInformation(int what, int value) {
synchronized(mCacheLock) {
switch (what) {
case PLAYBACKINFO_PLAYBACK_TYPE:
if ((value >= PLAYBACK_TYPE_MIN) && (value <= PLAYBACK_TYPE_MAX)) {
if (mPlaybackType != value) {
mPlaybackType = value;
sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value);
}
} else {
Log.w(TAG, "using invalid value for PLAYBACKINFO_PLAYBACK_TYPE");
}
break;
case PLAYBACKINFO_VOLUME:
if ((value > -1) && (value <= mPlaybackVolumeMax)) {
if (mPlaybackVolume != value) {
mPlaybackVolume = value;
sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value);
}
} else {
Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME");
}
break;
case PLAYBACKINFO_VOLUME_MAX:
if (value > 0) {
if (mPlaybackVolumeMax != value) {
mPlaybackVolumeMax = value;
sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value);
}
} else {
Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME_MAX");
}
break;
case PLAYBACKINFO_USES_STREAM:
if ((value >= 0) && (value < AudioSystem.getNumStreamTypes())) {
mPlaybackStream = value;
} else {
Log.w(TAG, "using invalid value for PLAYBACKINFO_USES_STREAM");
}
break;
case PLAYBACKINFO_VOLUME_HANDLING:
if ((value >= PLAYBACK_VOLUME_FIXED) && (value <= PLAYBACK_VOLUME_VARIABLE)) {
if (mPlaybackVolumeHandling != value) {
mPlaybackVolumeHandling = value;
sendAudioServiceNewPlaybackInfo_syncCacheLock(what, value);
}
} else {
Log.w(TAG, "using invalid value for PLAYBACKINFO_VOLUME_HANDLING");
}
break;
default:
// not throwing an exception or returning an error if more keys are to be
// supported in the future
Log.w(TAG, "setPlaybackInformation() ignoring unknown key " + what);
break;
}
}
}
/**
* @hide
* Return playback information represented as an integer value.
* @param what a key to specify the type of information to retrieve. Valid keys are
* {@link #PLAYBACKINFO_PLAYBACK_TYPE},
* {@link #PLAYBACKINFO_USES_STREAM},
* {@link #PLAYBACKINFO_VOLUME},
* {@link #PLAYBACKINFO_VOLUME_MAX},
* and {@link #PLAYBACKINFO_VOLUME_HANDLING}.
* @return the current value for the given information type, or
* {@link #PLAYBACKINFO_INVALID_VALUE} if an error occurred or the request is invalid, or
* the value is unknown.
*/
public int getIntPlaybackInformation(int what) {
synchronized(mCacheLock) {
switch (what) {
case PLAYBACKINFO_PLAYBACK_TYPE:
return mPlaybackType;
case PLAYBACKINFO_VOLUME:
return mPlaybackVolume;
case PLAYBACKINFO_VOLUME_MAX:
return mPlaybackVolumeMax;
case PLAYBACKINFO_USES_STREAM:
return mPlaybackStream;
case PLAYBACKINFO_VOLUME_HANDLING:
return mPlaybackVolumeHandling;
default:
Log.e(TAG, "getIntPlaybackInformation() unknown key " + what);
return PLAYBACKINFO_INVALID_VALUE;
}
}
}
/**
* Lock for all cached data
*/
private final Object mCacheLock = new Object();
/**
* Cache for the playback state.
* Access synchronized on mCacheLock
*/
private int mPlaybackState = PLAYSTATE_NONE;
/**
* Time of last play state change
* Access synchronized on mCacheLock
*/
private long mPlaybackStateChangeTimeMs = 0;
/**
* Cache for the artwork bitmap.
* Access synchronized on mCacheLock
* Artwork and metadata are not kept in one Bundle because the bitmap sometimes needs to be
* accessed to be resized, in which case a copy will be made. This would add overhead in
* Bundle operations.
*/
private Bitmap mArtwork;
private final int ARTWORK_DEFAULT_SIZE = 256;
private final int ARTWORK_INVALID_SIZE = -1;
private int mArtworkExpectedWidth = ARTWORK_DEFAULT_SIZE;
private int mArtworkExpectedHeight = ARTWORK_DEFAULT_SIZE;
/**
* Cache for the transport control mask.
* Access synchronized on mCacheLock
*/
private int mTransportControlFlags = FLAGS_KEY_MEDIA_NONE;
/**
* Cache for the metadata strings.
* Access synchronized on mCacheLock
* This is re-initialized in apply() and so cannot be final.
*/
private Bundle mMetadata = new Bundle();
/**
* The current remote control client generation ID across the system
*/
private int mCurrentClientGenId = -1;
/**
* The remote control client generation ID, the last time it was told it was the current RC.
* If (mCurrentClientGenId == mInternalClientGenId) is true, it means that this remote control
* client is the "focused" one, and that whenever this client's info is updated, it needs to
* send it to the known IRemoteControlDisplay interfaces.
*/
private int mInternalClientGenId = -2;
/**
* The media button intent description associated with this remote control client
* (can / should include target component for intent handling)
*/
private final PendingIntent mRcMediaIntent;
/**
* The remote control display to which this client will send information.
* NOTE: Only one IRemoteControlDisplay supported in this implementation
*/
private IRemoteControlDisplay mRcDisplay;
/**
* @hide
* Accessor to media button intent description (includes target component)
*/
public PendingIntent getRcMediaIntent() {
return mRcMediaIntent;
}
/**
* @hide
* Accessor to IRemoteControlClient
*/
public IRemoteControlClient getIRemoteControlClient() {
return mIRCC;
}
/**
* The IRemoteControlClient implementation
*/
private final IRemoteControlClient mIRCC = new IRemoteControlClient.Stub() {
public void onInformationRequested(int clientGeneration, int infoFlags,
int artWidth, int artHeight) {
// only post messages, we can't block here
if (mEventHandler != null) {
// signal new client
mEventHandler.removeMessages(MSG_NEW_INTERNAL_CLIENT_GEN);
mEventHandler.dispatchMessage(
mEventHandler.obtainMessage(
MSG_NEW_INTERNAL_CLIENT_GEN,
artWidth, artHeight,
new Integer(clientGeneration)));
// send the information
mEventHandler.removeMessages(MSG_REQUEST_PLAYBACK_STATE);
mEventHandler.removeMessages(MSG_REQUEST_METADATA);
mEventHandler.removeMessages(MSG_REQUEST_TRANSPORTCONTROL);
mEventHandler.removeMessages(MSG_REQUEST_ARTWORK);
mEventHandler.dispatchMessage(
mEventHandler.obtainMessage(MSG_REQUEST_PLAYBACK_STATE));
mEventHandler.dispatchMessage(
mEventHandler.obtainMessage(MSG_REQUEST_TRANSPORTCONTROL));
mEventHandler.dispatchMessage(mEventHandler.obtainMessage(MSG_REQUEST_METADATA));
mEventHandler.dispatchMessage(mEventHandler.obtainMessage(MSG_REQUEST_ARTWORK));
}
}
public void setCurrentClientGenerationId(int clientGeneration) {
// only post messages, we can't block here
if (mEventHandler != null) {
mEventHandler.removeMessages(MSG_NEW_CURRENT_CLIENT_GEN);
mEventHandler.dispatchMessage(mEventHandler.obtainMessage(
MSG_NEW_CURRENT_CLIENT_GEN, clientGeneration, 0/*ignored*/));
}
}
public void plugRemoteControlDisplay(IRemoteControlDisplay rcd) {
// only post messages, we can't block here
if (mEventHandler != null) {
mEventHandler.dispatchMessage(mEventHandler.obtainMessage(
MSG_PLUG_DISPLAY, rcd));
}
}
public void unplugRemoteControlDisplay(IRemoteControlDisplay rcd) {
// only post messages, we can't block here
if (mEventHandler != null) {
mEventHandler.dispatchMessage(mEventHandler.obtainMessage(
MSG_UNPLUG_DISPLAY, rcd));
}
}
};
/**
* @hide
* Default value for the unique identifier
*/
public final static int RCSE_ID_UNREGISTERED = -1;
/**
* Unique identifier of the RemoteControlStackEntry in AudioService with which
* this RemoteControlClient is associated.
*/
private int mRcseId = RCSE_ID_UNREGISTERED;
/**
* @hide
* To be only used by AudioManager after it has received the unique id from
* IAudioService.registerRemoteControlClient()
* @param id the unique identifier of the RemoteControlStackEntry in AudioService with which
* this RemoteControlClient is associated.
*/
public void setRcseId(int id) {
mRcseId = id;
}
/**
* @hide
*/
public int getRcseId() {
return mRcseId;
}
private EventHandler mEventHandler;
private final static int MSG_REQUEST_PLAYBACK_STATE = 1;
private final static int MSG_REQUEST_METADATA = 2;
private final static int MSG_REQUEST_TRANSPORTCONTROL = 3;
private final static int MSG_REQUEST_ARTWORK = 4;
private final static int MSG_NEW_INTERNAL_CLIENT_GEN = 5;
private final static int MSG_NEW_CURRENT_CLIENT_GEN = 6;
private final static int MSG_PLUG_DISPLAY = 7;
private final static int MSG_UNPLUG_DISPLAY = 8;
private class EventHandler extends Handler {
public EventHandler(RemoteControlClient rcc, Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch(msg.what) {
case MSG_REQUEST_PLAYBACK_STATE:
synchronized (mCacheLock) {
sendPlaybackState_syncCacheLock();
}
break;
case MSG_REQUEST_METADATA:
synchronized (mCacheLock) {
sendMetadata_syncCacheLock();
}
break;
case MSG_REQUEST_TRANSPORTCONTROL:
synchronized (mCacheLock) {
sendTransportControlFlags_syncCacheLock();
}
break;
case MSG_REQUEST_ARTWORK:
synchronized (mCacheLock) {
sendArtwork_syncCacheLock();
}
break;
case MSG_NEW_INTERNAL_CLIENT_GEN:
onNewInternalClientGen((Integer)msg.obj, msg.arg1, msg.arg2);
break;
case MSG_NEW_CURRENT_CLIENT_GEN:
onNewCurrentClientGen(msg.arg1);
break;
case MSG_PLUG_DISPLAY:
onPlugDisplay((IRemoteControlDisplay)msg.obj);
break;
case MSG_UNPLUG_DISPLAY:
onUnplugDisplay((IRemoteControlDisplay)msg.obj);
break;
default:
Log.e(TAG, "Unknown event " + msg.what + " in RemoteControlClient handler");
}
}
}
//===========================================================
// Communication with IRemoteControlDisplay
private void detachFromDisplay_syncCacheLock() {
mRcDisplay = null;
mArtworkExpectedWidth = ARTWORK_INVALID_SIZE;
mArtworkExpectedHeight = ARTWORK_INVALID_SIZE;
}
private void sendPlaybackState_syncCacheLock() {
if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) {
try {
mRcDisplay.setPlaybackState(mInternalClientGenId, mPlaybackState,
mPlaybackStateChangeTimeMs);
} catch (RemoteException e) {
Log.e(TAG, "Error in setPlaybackState(), dead display "+e);
detachFromDisplay_syncCacheLock();
}
}
}
private void sendMetadata_syncCacheLock() {
if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) {
try {
mRcDisplay.setMetadata(mInternalClientGenId, mMetadata);
} catch (RemoteException e) {
Log.e(TAG, "Error in sendPlaybackState(), dead display "+e);
detachFromDisplay_syncCacheLock();
}
}
}
private void sendTransportControlFlags_syncCacheLock() {
if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) {
try {
mRcDisplay.setTransportControlFlags(mInternalClientGenId,
mTransportControlFlags);
} catch (RemoteException e) {
Log.e(TAG, "Error in sendTransportControlFlags(), dead display "+e);
detachFromDisplay_syncCacheLock();
}
}
}
private void sendArtwork_syncCacheLock() {
if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) {
// even though we have already scaled in setArtwork(), when this client needs to
// send the bitmap, there might be newer and smaller expected dimensions, so we have
// to check again.
mArtwork = scaleBitmapIfTooBig(mArtwork, mArtworkExpectedWidth, mArtworkExpectedHeight);
try {
mRcDisplay.setArtwork(mInternalClientGenId, mArtwork);
} catch (RemoteException e) {
Log.e(TAG, "Error in sendArtwork(), dead display "+e);
detachFromDisplay_syncCacheLock();
}
}
}
private void sendMetadataWithArtwork_syncCacheLock() {
if ((mCurrentClientGenId == mInternalClientGenId) && (mRcDisplay != null)) {
// even though we have already scaled in setArtwork(), when this client needs to
// send the bitmap, there might be newer and smaller expected dimensions, so we have
// to check again.
mArtwork = scaleBitmapIfTooBig(mArtwork, mArtworkExpectedWidth, mArtworkExpectedHeight);
try {
mRcDisplay.setAllMetadata(mInternalClientGenId, mMetadata, mArtwork);
} catch (RemoteException e) {
Log.e(TAG, "Error in setAllMetadata(), dead display "+e);
detachFromDisplay_syncCacheLock();
}
}
}
//===========================================================
// Communication with AudioService
private static IAudioService sService;
private static IAudioService getService()
{
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
sService = IAudioService.Stub.asInterface(b);
return sService;
}
private void sendAudioServiceNewPlaybackInfo_syncCacheLock(int what, int value) {
if (mRcseId == RCSE_ID_UNREGISTERED) {
return;
}
//Log.d(TAG, "sending to AudioService key=" + what + ", value=" + value);
IAudioService service = getService();
try {
service.setPlaybackInfoForRcc(mRcseId, what, value);
} catch (RemoteException e) {
Log.e(TAG, "Dead object in sendAudioServiceNewPlaybackInfo_syncCacheLock", e);
}
}
//===========================================================
// Message handlers
private void onNewInternalClientGen(Integer clientGeneration, int artWidth, int artHeight) {
synchronized (mCacheLock) {
// this remote control client is told it is the "focused" one:
// it implies that now (mCurrentClientGenId == mInternalClientGenId) is true
mInternalClientGenId = clientGeneration.intValue();
if (artWidth > 0) {
mArtworkExpectedWidth = artWidth;
mArtworkExpectedHeight = artHeight;
}
}
}
private void onNewCurrentClientGen(int clientGeneration) {
synchronized (mCacheLock) {
mCurrentClientGenId = clientGeneration;
}
}
private void onPlugDisplay(IRemoteControlDisplay rcd) {
synchronized(mCacheLock) {
mRcDisplay = rcd;
}
}
private void onUnplugDisplay(IRemoteControlDisplay rcd) {
synchronized(mCacheLock) {
if ((mRcDisplay != null) && (mRcDisplay.asBinder().equals(rcd.asBinder()))) {
mRcDisplay = null;
mArtworkExpectedWidth = ARTWORK_DEFAULT_SIZE;
mArtworkExpectedHeight = ARTWORK_DEFAULT_SIZE;
}
}
}
//===========================================================
// Internal utilities
/**
* Scale a bitmap to fit the smallest dimension by uniformly scaling the incoming bitmap.
* If the bitmap fits, then do nothing and return the original.
*
* @param bitmap
* @param maxWidth
* @param maxHeight
* @return
*/
private Bitmap scaleBitmapIfTooBig(Bitmap bitmap, int maxWidth, int maxHeight) {
if (bitmap != null) {
final int width = bitmap.getWidth();
final int height = bitmap.getHeight();
if (width > maxWidth || height > maxHeight) {
float scale = Math.min((float) maxWidth / width, (float) maxHeight / height);
int newWidth = Math.round(scale * width);
int newHeight = Math.round(scale * height);
Bitmap.Config newConfig = bitmap.getConfig();
if (newConfig == null) {
newConfig = Bitmap.Config.ARGB_8888;
}
Bitmap outBitmap = Bitmap.createBitmap(newWidth, newHeight, newConfig);
Canvas canvas = new Canvas(outBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
canvas.drawBitmap(bitmap, null,
new RectF(0, 0, outBitmap.getWidth(), outBitmap.getHeight()), paint);
bitmap = outBitmap;
}
}
return bitmap;
}
/**
* Fast routine to go through an array of allowed keys and return whether the key is part
* of that array
* @param key the key value
* @param validKeys the array of valid keys for a given type
* @return true if the key is part of the array, false otherwise
*/
private static boolean validTypeForKey(int key, int[] validKeys) {
try {
for (int i = 0 ; ; i++) {
if (key == validKeys[i]) {
return true;
}
}
} catch (ArrayIndexOutOfBoundsException e) {
return false;
}
}
}