diff --git a/core/java/com/android/internal/widget/TransportControlView.java b/core/java/com/android/internal/widget/TransportControlView.java index 3961de3195de0..1c47ca88ab7da 100644 --- a/core/java/com/android/internal/widget/TransportControlView.java +++ b/core/java/com/android/internal/widget/TransportControlView.java @@ -16,88 +16,369 @@ package com.android.internal.widget; -import com.android.internal.R; +import java.lang.ref.WeakReference; +import com.android.internal.widget.LockScreenWidgetCallback; +import com.android.internal.widget.LockScreenWidgetInterface; + +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.RemoteControlClient; +import android.media.IRemoteControlDisplay; +import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.RemoteException; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; -import android.widget.LinearLayout; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; -/** - * A special widget for displaying audio playback ("transport controls") in LockScreen. - * - */ -public class TransportControlView extends LinearLayout implements LockScreenWidgetInterface, - OnClickListener { - private static final String TAG = "TransportControlView"; - static final int sViewIds[] = { R.id.control_prev, R.id.control_pauseplay, R.id.control_next }; - protected static final int AUDIO_FOCUS_CHANGED = 100; - private LockScreenWidgetCallback mCallback; +import com.android.internal.R; + +public class TransportControlView extends FrameLayout implements OnClickListener, + LockScreenWidgetInterface { + + private static final int MSG_UPDATE_STATE = 100; + private static final int MSG_SET_METADATA = 101; + private static final int MSG_SET_TRANSPORT_CONTROLS = 102; + private static final int MSG_SET_ARTWORK = 103; + private static final int MSG_SET_GENERATION_ID = 104; + private static final int MAXDIM = 512; + protected static final boolean DEBUG = true; + protected static final String TAG = "TransportControlView"; + + private ImageView mAlbumArt; + private TextView mTrackTitle; + private ImageView mBtnPrev; + private ImageView mBtnPlay; + private ImageView mBtnNext; + private int mClientGeneration; + private Metadata mMetadata = new Metadata(); + private boolean mAttached; + private ComponentName mClientName; + private int mTransportControlFlags; + private int mPlayState; + private AudioManager mAudioManager; + private LockScreenWidgetCallback mWidgetCallbacks; + private IRemoteControlDisplayWeak mIRCD; + + /** + * The metadata which should be populated into the view once we've been attached + */ + private Bundle mPopulateMetadataWhenAttached = null; + + // This handler is required to ensure messages from IRCD are handled in sequence and on + // the UI thread. private Handler mHandler = new Handler() { + @Override public void handleMessage(Message msg) { - switch (msg.what){ - case AUDIO_FOCUS_CHANGED: - handleAudioFocusChange(msg.arg1); + switch (msg.what) { + case MSG_UPDATE_STATE: + if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2); + break; + + case MSG_SET_METADATA: + if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj); + break; + + case MSG_SET_TRANSPORT_CONTROLS: + if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2); + break; + + case MSG_SET_ARTWORK: + if (mClientGeneration == msg.arg1) { + mMetadata.bitmap = (Bitmap) msg.obj; + mAlbumArt.setImageBitmap(mMetadata.bitmap); + } + break; + + case MSG_SET_GENERATION_ID: + if (mWidgetCallbacks != null) { + boolean clearing = msg.arg2 != 0; + if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + clearing); + if (!clearing) { + mWidgetCallbacks.requestShow(TransportControlView.this); + } else { + mWidgetCallbacks.requestHide(TransportControlView.this); + } + } + mClientGeneration = msg.arg1; + mClientName = (ComponentName) msg.obj; + break; + } } }; - AudioManager.OnAudioFocusChangeListener mAudioFocusChangeListener = - new AudioManager.OnAudioFocusChangeListener() { - public void onAudioFocusChange(final int focusChange) { - mHandler.obtainMessage(AUDIO_FOCUS_CHANGED, focusChange, 0).sendToTarget(); - } - }; + /** + * This class is required to have weak linkage to the current TransportControlView + * because the remote process can hold a strong reference to this binder object and + * we can't predict when it will be GC'd in the remote process. Without this code, it + * would allow a heavyweight object to be held on this side of the binder when there's + * no requirement to run a GC on the other side. + */ + private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub { + private WeakReference mLocalHandler; - public TransportControlView(Context context) { - this(context, null); - } + IRemoteControlDisplayWeak(Handler handler) { + mLocalHandler = new WeakReference(handler); + } + + public void setPlaybackState(int generationId, int state) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget(); + } + } + + public void setMetadata(int generationId, Bundle metadata) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); + } + } + + public void setTransportControlFlags(int generationId, int flags) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags) + .sendToTarget(); + } + } + + public void setArtwork(int generationId, Bitmap bitmap) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); + } + } + + public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); + handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); + } + } + + public void setCurrentClientId(int clientGeneration, ComponentName clientEventReceiver, + boolean clearing) throws RemoteException { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_GENERATION_ID, + clientGeneration, (clearing ? 1 : 0), clientEventReceiver).sendToTarget(); + } + } + }; public TransportControlView(Context context, AttributeSet attrs) { super(context, attrs); + Log.v(TAG, "Create TCV " + this); + mAudioManager = new AudioManager(mContext); + mIRCD = new IRemoteControlDisplayWeak(mHandler); } - protected void handleAudioFocusChange(int focusChange) { - // TODO - } - - public void setCallback(LockScreenWidgetCallback callback) { - mCallback = callback; + private void updateTransportControls(int transportControlFlags) { + mTransportControlFlags = transportControlFlags; } @Override public void onFinishInflate() { - for (int i = 0; i < sViewIds.length; i++) { - View view = findViewById(sViewIds[i]); - if (view != null) { - view.setOnClickListener(this); + super.onFinishInflate(); + mTrackTitle = (TextView) findViewById(R.id.title); + mTrackTitle.setSelected(true); // enable marquee + mAlbumArt = (ImageView) findViewById(R.id.albumart); + mBtnPrev = (ImageView) findViewById(R.id.btn_prev); + mBtnPlay = (ImageView) findViewById(R.id.btn_play); + mBtnNext = (ImageView) findViewById(R.id.btn_next); + final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; + for (View view : buttons) { + view.setOnClickListener(this); + } + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mPopulateMetadataWhenAttached != null) { + updateMetadata(mPopulateMetadataWhenAttached); + mPopulateMetadataWhenAttached = null; + } + if (!mAttached) { + if (DEBUG) Log.v(TAG, "Registering TCV " + this); + mAudioManager.registerRemoteControlDisplay(mIRCD); + } + mAttached = true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mAttached) { + if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); + mAudioManager.unregisterRemoteControlDisplay(mIRCD); + } + mAttached = false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int dim = Math.min(MAXDIM, Math.max(getWidth(), getHeight())); +// Log.v(TAG, "setting max bitmap size: " + dim + "x" + dim); +// mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim); + } + + class Metadata { + private String artist; + private String trackTitle; + private String albumTitle; + private Bitmap bitmap; + + public String toString() { + return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]"; + } + } + + private String getMdString(Bundle data, int id) { + return data.getString(Integer.toString(id)); + } + + private void updateMetadata(Bundle data) { + if (mAttached) { + mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST); + mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE); + mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM); + populateMetadata(); + } else { + mPopulateMetadataWhenAttached = data; + } + } + + /** + * Populates the given metadata into the view + */ + private void populateMetadata() { + StringBuilder sb = new StringBuilder(); + int trackTitleLength = 0; + if (!TextUtils.isEmpty(mMetadata.trackTitle)) { + sb.append(mMetadata.trackTitle); + trackTitleLength = mMetadata.trackTitle.length(); + } + if (!TextUtils.isEmpty(mMetadata.artist)) { + if (sb.length() != 0) { + sb.append(" - "); + } + sb.append(mMetadata.artist); + } + if (!TextUtils.isEmpty(mMetadata.albumTitle)) { + if (sb.length() != 0) { + sb.append(" - "); + } + sb.append(mMetadata.albumTitle); + } + mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE); + Spannable str = (Spannable) mTrackTitle.getText(); + if (trackTitleLength != 0) { + str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + trackTitleLength++; + } + if (sb.length() > trackTitleLength) { + str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + mAlbumArt.setImageBitmap(mMetadata.bitmap); + final int flags = mTransportControlFlags; + setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); + setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT); + setVisibilityBasedOnFlag(mBtnPrev, flags, + RemoteControlClient.FLAG_KEY_MEDIA_PLAY + | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_STOP); + + updatePlayPauseState(mPlayState); + } + + private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { + if ((flags & flag) != 0) { + view.setVisibility(View.VISIBLE); + } else { + view.setVisibility(View.GONE); + } + } + + private void updatePlayPauseState(int state) { + if (DEBUG) Log.v(TAG, + "updatePlayPauseState(), old=" + mPlayState + ", state=" + state); + if (state == mPlayState) { + return; + } + switch (state) { + case RemoteControlClient.PLAYSTATE_PLAYING: + mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_pause); + break; + + case RemoteControlClient.PLAYSTATE_BUFFERING: + mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_stop); + break; + + case RemoteControlClient.PLAYSTATE_PAUSED: + default: + mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_play); + break; + } + mPlayState = state; + } + + public void onClick(View v) { + int keyCode = -1; + if (v == mBtnPrev) { + keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; + } else if (v == mBtnNext) { + keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; + } else if (v == mBtnPlay) { + keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; + + } + if (keyCode != -1) { + sendMediaButtonClick(keyCode); + if (mWidgetCallbacks != null) { + mWidgetCallbacks.userActivity(this); } } } - public void onClick(View v) { - switch (v.getId()) { - case R.id.control_prev: - // TODO - break; + private void sendMediaButtonClick(int keyCode) { + // TODO: target to specific player based on mClientName + KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + getContext().sendOrderedBroadcast(intent, null); - case R.id.control_pauseplay: - // TODO - break; + keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); + intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + getContext().sendOrderedBroadcast(intent, null); + } - case R.id.control_next: - // TODO - break; - } - // Have any button click extend lockscreen's timeout. - if (mCallback != null) { - mCallback.userActivity(this); - } + public void setCallback(LockScreenWidgetCallback callback) { + mWidgetCallbacks = callback; } } diff --git a/core/res/res/drawable-hdpi/ic_lockscreen_player_background.9.png b/core/res/res/drawable-hdpi/ic_lockscreen_player_background.9.png new file mode 100644 index 0000000000000..b18735871884e Binary files /dev/null and b/core/res/res/drawable-hdpi/ic_lockscreen_player_background.9.png differ diff --git a/core/res/res/drawable-hdpi/ic_media_embed_play.png b/core/res/res/drawable-hdpi/ic_media_embed_play.png index 05778c181f6f3..23ac7e45e426a 100644 Binary files a/core/res/res/drawable-hdpi/ic_media_embed_play.png and b/core/res/res/drawable-hdpi/ic_media_embed_play.png differ diff --git a/core/res/res/drawable-mdpi/ic_lockscreen_player_background.9.png b/core/res/res/drawable-mdpi/ic_lockscreen_player_background.9.png new file mode 100644 index 0000000000000..8cfd1afbeff81 Binary files /dev/null and b/core/res/res/drawable-mdpi/ic_lockscreen_player_background.9.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_embed_play.png b/core/res/res/drawable-mdpi/ic_media_embed_play.png index 3576ce53e4219..fc5d8c622f6ac 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_embed_play.png and b/core/res/res/drawable-mdpi/ic_media_embed_play.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_ff.png b/core/res/res/drawable-mdpi/ic_media_ff.png index 170dd2daaa757..892772eb13e49 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_ff.png and b/core/res/res/drawable-mdpi/ic_media_ff.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_fullscreen.png b/core/res/res/drawable-mdpi/ic_media_fullscreen.png index 960aa851053ef..1c60e15864779 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_fullscreen.png and b/core/res/res/drawable-mdpi/ic_media_fullscreen.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_next.png b/core/res/res/drawable-mdpi/ic_media_next.png index a6feed0e9e385..bbe311b76fdaa 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_next.png and b/core/res/res/drawable-mdpi/ic_media_next.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_pause.png b/core/res/res/drawable-mdpi/ic_media_pause.png index 548ba02193114..e4e8d86b62a63 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_pause.png and b/core/res/res/drawable-mdpi/ic_media_pause.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_play.png b/core/res/res/drawable-mdpi/ic_media_play.png index 0fe680647e947..8eaf96240ed2f 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_play.png and b/core/res/res/drawable-mdpi/ic_media_play.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_previous.png b/core/res/res/drawable-mdpi/ic_media_previous.png index 0163d094596d0..e9abc7f3bd818 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_previous.png and b/core/res/res/drawable-mdpi/ic_media_previous.png differ diff --git a/core/res/res/drawable-mdpi/ic_media_rew.png b/core/res/res/drawable-mdpi/ic_media_rew.png index 5489180eb16e2..a5eb94a25460d 100644 Binary files a/core/res/res/drawable-mdpi/ic_media_rew.png and b/core/res/res/drawable-mdpi/ic_media_rew.png differ diff --git a/core/res/res/drawable-xhdpi/ic_lockscreen_player_background.9.png b/core/res/res/drawable-xhdpi/ic_lockscreen_player_background.9.png new file mode 100644 index 0000000000000..7fb0cbc91b95e Binary files /dev/null and b/core/res/res/drawable-xhdpi/ic_lockscreen_player_background.9.png differ diff --git a/core/res/res/layout/keyguard_screen_password_landscape.xml b/core/res/res/layout/keyguard_screen_password_landscape.xml index 12df99ef414a9..694db50a84ee4 100644 --- a/core/res/res/layout/keyguard_screen_password_landscape.xml +++ b/core/res/res/layout/keyguard_screen_password_landscape.xml @@ -186,6 +186,8 @@ android:layout_rowSpan="6" android:layout_columnSpan="1" android:layout_gravity="fill" + android:layout_width="0dip" + android:layout_height="0dip" /> diff --git a/core/res/res/layout/keyguard_screen_password_portrait.xml b/core/res/res/layout/keyguard_screen_password_portrait.xml index 6145e47fb871a..cf3bd4239a06c 100644 --- a/core/res/res/layout/keyguard_screen_password_portrait.xml +++ b/core/res/res/layout/keyguard_screen_password_portrait.xml @@ -174,6 +174,8 @@ android:layout_rowSpan="3" android:layout_columnSpan="1" android:layout_gravity="fill" + android:layout_width="0dip" + android:layout_height="0dip" /> diff --git a/core/res/res/layout/keyguard_screen_tab_unlock.xml b/core/res/res/layout/keyguard_screen_tab_unlock.xml index 6016d4e3dc6a6..a42d6cb94a1b4 100644 --- a/core/res/res/layout/keyguard_screen_tab_unlock.xml +++ b/core/res/res/layout/keyguard_screen_tab_unlock.xml @@ -188,6 +188,8 @@ android:layout_rowSpan="4" android:layout_columnSpan="1" android:layout_gravity="fill" + android:layout_width="0dip" + android:layout_height="0dip" /> diff --git a/core/res/res/layout/keyguard_screen_tab_unlock_land.xml b/core/res/res/layout/keyguard_screen_tab_unlock_land.xml index 0568dd9ba82d8..b716c29853202 100644 --- a/core/res/res/layout/keyguard_screen_tab_unlock_land.xml +++ b/core/res/res/layout/keyguard_screen_tab_unlock_land.xml @@ -155,6 +155,8 @@ android:layout_rowSpan="5" android:layout_columnSpan="1" android:layout_gravity="fill" + android:layout_width="0dip" + android:layout_height="0dip" /> diff --git a/core/res/res/layout/keyguard_screen_unlock_landscape.xml b/core/res/res/layout/keyguard_screen_unlock_landscape.xml index 9b287310696de..d71dbffd8f9df 100644 --- a/core/res/res/layout/keyguard_screen_unlock_landscape.xml +++ b/core/res/res/layout/keyguard_screen_unlock_landscape.xml @@ -156,6 +156,8 @@ android:layout_rowSpan="5" android:layout_columnSpan="1" android:layout_gravity="fill" + android:layout_width="0dip" + android:layout_height="0dip" /> diff --git a/core/res/res/layout/keyguard_screen_unlock_portrait.xml b/core/res/res/layout/keyguard_screen_unlock_portrait.xml index 433dda7c4eff8..64c479f32c52d 100644 --- a/core/res/res/layout/keyguard_screen_unlock_portrait.xml +++ b/core/res/res/layout/keyguard_screen_unlock_portrait.xml @@ -167,6 +167,8 @@ android:layout_rowSpan="4" android:layout_columnSpan="1" android:layout_gravity="fill" + android:layout_width="0dip" + android:layout_height="0dip" /> diff --git a/core/res/res/layout/keyguard_transport_control.xml b/core/res/res/layout/keyguard_transport_control.xml index 6308b02dff79e..2ebe5fceddc27 100644 --- a/core/res/res/layout/keyguard_transport_control.xml +++ b/core/res/res/layout/keyguard_transport_control.xml @@ -1,53 +1,102 @@ - + + android:id="@+id/transport_controls" + android:background="@drawable/ic_lockscreen_player_background"> -