am 310796de: am d6239b92: Merge "Add a scrubber to keyguard; layout tweaks" into klp-dev

* commit '310796de0b86074229ad804001b1ef8d466ee69c':
  Add a scrubber to keyguard; layout tweaks
This commit is contained in:
Adam Powell
2013-10-03 15:51:26 -07:00
committed by Android Git Automerger
7 changed files with 563 additions and 225 deletions

View File

@@ -13356,6 +13356,7 @@ package android.media {
ctor public RemoteController(android.content.Context, android.os.Looper) throws java.lang.IllegalArgumentException; ctor public RemoteController(android.content.Context, android.os.Looper) throws java.lang.IllegalArgumentException;
method public int clearArtworkConfiguration(); method public int clearArtworkConfiguration();
method public android.media.RemoteController.MetadataEditor editMetadata(); method public android.media.RemoteController.MetadataEditor editMetadata();
method public long getEstimatedMediaPosition();
method public int seekTo(long); method public int seekTo(long);
method public int sendMediaKeyEvent(android.view.KeyEvent); method public int sendMediaKeyEvent(android.view.KeyEvent);
method public int setArtworkConfiguration(int, int); method public int setArtworkConfiguration(int, int);

View File

@@ -1674,7 +1674,7 @@ public class RemoteControlClient
* @return true during any form of playback, false if it's not playing anything while in this * @return true during any form of playback, false if it's not playing anything while in this
* playback state * playback state
*/ */
private static boolean playbackPositionShouldMove(int playstate) { static boolean playbackPositionShouldMove(int playstate) {
switch(playstate) { switch(playstate) {
case PLAYSTATE_STOPPED: case PLAYSTATE_STOPPED:
case PLAYSTATE_PAUSED: case PLAYSTATE_PAUSED:

View File

@@ -17,6 +17,7 @@
package android.media; package android.media;
import android.Manifest; import android.Manifest;
import android.app.ActivityManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException; import android.app.PendingIntent.CanceledException;
import android.content.Context; import android.content.Context;
@@ -30,6 +31,8 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.ServiceManager; import android.os.ServiceManager;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -59,6 +62,7 @@ public final class RemoteController
private final RcDisplay mRcd; private final RcDisplay mRcd;
private final Context mContext; private final Context mContext;
private final AudioManager mAudioManager; private final AudioManager mAudioManager;
private final int mMaxBitmapDimension;
private MetadataEditor mMetadataEditor; private MetadataEditor mMetadataEditor;
/** /**
@@ -110,6 +114,13 @@ public final class RemoteController
mContext = context; mContext = context;
mRcd = new RcDisplay(); mRcd = new RcDisplay();
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (ActivityManager.isLowRamDeviceStatic()) {
mMaxBitmapDimension = MAX_BITMAP_DIMENSION;
} else {
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
mMaxBitmapDimension = Math.max(dm.widthPixels, dm.heightPixels);
}
} }
@@ -142,7 +153,7 @@ public final class RemoteController
* @param state one of the playback states authorized * @param state one of the playback states authorized
* in {@link RemoteControlClient#setPlaybackState(int)}. * in {@link RemoteControlClient#setPlaybackState(int)}.
* @param stateChangeTimeMs the system time at which the state change was reported, * @param stateChangeTimeMs the system time at which the state change was reported,
* expressed in ms. * expressed in ms. Based on {@link android.os.SystemClock.elapsedRealtime()}.
* @param currentPosMs a positive value for the current media playback position expressed * @param currentPosMs a positive value for the current media playback position expressed
* in ms, a negative value if the position is temporarily unknown. * in ms, a negative value if the position is temporarily unknown.
* @param speed a value expressed as a ratio of 1x playback: 1.0f is normal playback, * @param speed a value expressed as a ratio of 1x playback: 1.0f is normal playback,
@@ -200,6 +211,50 @@ public final class RemoteController
} }
} }
/**
* @hide
*/
public String getRemoteControlClientPackageName() {
return mClientPendingIntentCurrent != null ?
mClientPendingIntentCurrent.getCreatorPackage() : null;
}
/**
* Return the estimated playback position of the current media track or a negative value
* if not available.
*
* <p>The value returned is estimated by the current process and may not be perfect.
* The time returned by this method is calculated from the last state change time based
* on the current play position at that time and the last known playback speed.
* An application may call {@link #setSynchronizationMode(int)} to apply
* a synchronization policy that will periodically re-sync the estimated position
* with the RemoteControlClient.</p>
*
* @return the current estimated playback position in milliseconds or a negative value
* if not available
*
* @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float)
*/
public long getEstimatedMediaPosition() {
if (mLastPlaybackInfo != null) {
if (!RemoteControlClient.playbackPositionShouldMove(mLastPlaybackInfo.mState)) {
return mLastPlaybackInfo.mCurrentPosMs;
}
// Take the current position at the time of state change and estimate.
final long thenPos = mLastPlaybackInfo.mCurrentPosMs;
if (thenPos < 0) {
return -1;
}
final long now = SystemClock.elapsedRealtime();
final long then = mLastPlaybackInfo.mStateChangeTimeMs;
final long sinceThen = now - then;
final long scaledSinceThen = (long) (sinceThen * mLastPlaybackInfo.mSpeed);
return thenPos + scaledSinceThen;
}
return -1;
}
/** /**
* Send a simulated key event for a media button to be received by the current client. * Send a simulated key event for a media button to be received by the current client.
@@ -301,8 +356,8 @@ public final class RemoteController
synchronized (mInfoLock) { synchronized (mInfoLock) {
if (wantBitmap) { if (wantBitmap) {
if ((width > 0) && (height > 0)) { if ((width > 0) && (height > 0)) {
if (width > MAX_BITMAP_DIMENSION) { width = MAX_BITMAP_DIMENSION; } if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; }
if (height > MAX_BITMAP_DIMENSION) { height = MAX_BITMAP_DIMENSION; } if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; }
mArtworkWidth = width; mArtworkWidth = width;
mArtworkHeight = height; mArtworkHeight = height;
} else { } else {
@@ -415,7 +470,13 @@ public final class RemoteController
protected MetadataEditor(Bundle metadata, long editableKeys) { protected MetadataEditor(Bundle metadata, long editableKeys) {
mEditorMetadata = metadata; mEditorMetadata = metadata;
mEditableKeys = editableKeys; mEditableKeys = editableKeys;
mEditorArtwork = null;
mEditorArtwork = (Bitmap) metadata.getParcelable(
String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK));
if (mEditorArtwork != null) {
cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK);
}
mMetadataChanged = true; mMetadataChanged = true;
mArtworkChanged = true; mArtworkChanged = true;
mApplied = false; mApplied = false;
@@ -706,6 +767,7 @@ public final class RemoteController
// existing metadata, merge existing and new // existing metadata, merge existing and new
mMetadataEditor.mEditorMetadata.putAll(metadata); mMetadataEditor.mEditorMetadata.putAll(metadata);
} }
mMetadataEditor.putBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK, mMetadataEditor.putBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK,
(Bitmap)metadata.getParcelable( (Bitmap)metadata.getParcelable(
String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK))); String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK)));

View File

@@ -22,34 +22,133 @@
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:id="@+id/keyguard_transport_control"> android:id="@+id/keyguard_transport_control">
<!-- Use ImageView for its cropping features; otherwise could be android:background -->
<ImageView
android:id="@+id/albumart"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="fill"
android:scaleType="centerCrop"
android:adjustViewBounds="false"
android:contentDescription="@string/keygaurd_accessibility_media_controls" />
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom"> android:layout_gravity="top"
<TextView android:gravity="center">
android:id="@+id/title" <ImageView
android:id="@+id/badge"
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="fitCenter" />
<FrameLayout
android:id="@+id/info_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_marginTop="8dip" <LinearLayout
android:layout_marginStart="16dip" android:id="@+id/metadata_container"
android:layout_marginEnd="16dip" android:orientation="vertical"
android:gravity="center_horizontal" android:layout_width="match_parent"
android:singleLine="true" android:layout_height="wrap_content"
android:ellipsize="end" android:layout_gravity="center">
android:textAppearance="?android:attr/textAppearanceMedium" <TextView
/> android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dip"
android:layout_marginEnd="16dip"
android:gravity="center_horizontal"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceLarge"
android:fontFamily="sans-serif-light" />
<TextView
android:id="@+id/artist_album"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dip"
android:layout_marginEnd="16dip"
android:gravity="center_horizontal"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<RelativeLayout
android:id="@+id/transient_seek"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="invisible">
<SeekBar
android:id="@+id/transient_seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/transient_seek_time_elapsed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/transient_seek_bar"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="12dp" />
<TextView
android:id="@+id/transient_seek_time_remaining"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/transient_seek_bar"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="12dp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/transient_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="invisible">
<RatingBar
android:id="@+id/transient_rating_bar_stars"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/transient_rating_thumbs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageButton
android:id="@+id/btn_thumbs_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_media_previous"
android:background="?android:attr/selectableItemBackground"
android:minWidth="48dp"
android:minHeight="48dp"
android:contentDescription="@string/keyguard_accessibility_transport_thumbs_up_description"/>
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageButton
android:id="@+id/btn_thumbs_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_media_next"
android:background="?android:attr/selectableItemBackground"
android:minWidth="48dp"
android:minHeight="48dp"
android:contentDescription="@string/keyguard_accessibility_transport_thumbs_down_description"/>
</FrameLayout>
</LinearLayout>
<ToggleButton
android:id="@+id/transient_rating_heart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:minWidth="48dp"
android:minHeight="48dp"
android:contentDescription="@string/keyguard_accessibility_transport_heart_description" />
</LinearLayout>
</FrameLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -59,45 +158,45 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"> android:layout_weight="1">
<ImageView <ImageButton
android:id="@+id/btn_prev" android:id="@+id/btn_prev"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_media_previous" android:src="@drawable/ic_media_previous"
android:clickable="true"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:padding="10dip" android:minWidth="48dp"
android:minHeight="48dp"
android:contentDescription="@string/keyguard_accessibility_transport_prev_description"/> android:contentDescription="@string/keyguard_accessibility_transport_prev_description"/>
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"> android:layout_weight="1">
<ImageView <ImageButton
android:id="@+id/btn_play" android:id="@+id/btn_play"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:clickable="true"
android:src="@drawable/ic_media_play" android:src="@drawable/ic_media_play"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:padding="10dip" android:minWidth="48dp"
android:minHeight="48dp"
android:contentDescription="@string/keyguard_accessibility_transport_play_description"/> android:contentDescription="@string/keyguard_accessibility_transport_play_description"/>
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"> android:layout_weight="1">
<ImageView <ImageButton
android:id="@+id/btn_next" android:id="@+id/btn_next"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:clickable="true"
android:src="@drawable/ic_media_next" android:src="@drawable/ic_media_next"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:padding="10dip" android:minWidth="48dp"
android:minHeight="48dp"
android:contentDescription="@string/keyguard_accessibility_transport_next_description"/> android:contentDescription="@string/keyguard_accessibility_transport_next_description"/>
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>

View File

@@ -152,6 +152,13 @@
<string name="keyguard_accessibility_transport_play_description">Play button</string> <string name="keyguard_accessibility_transport_play_description">Play button</string>
<!-- Shown on transport control of lockscreen. Pressing button pauses playback --> <!-- Shown on transport control of lockscreen. Pressing button pauses playback -->
<string name="keyguard_accessibility_transport_stop_description">Stop button</string> <string name="keyguard_accessibility_transport_stop_description">Stop button</string>
<!-- Shown on transport control of lockscreen. Pressing button rates the track as "thumbs up." -->
<string name="keyguard_accessibility_transport_thumbs_up_description">Thumbs up</string>
<!-- Shown on transport control of lockscreen. Pressing button rates the track as "thumbs down." -->
<string name="keyguard_accessibility_transport_thumbs_down_description">Thumbs down</string>
<!-- Shown on transport control of lockscreen. Pressing button toggles the "heart" rating. -->
<string name="keyguard_accessibility_transport_heart_description">Heart</string>
<!-- Accessibility description for when the device prompts the user to dismiss keyguard <!-- Accessibility description for when the device prompts the user to dismiss keyguard
in order to complete an action. This will be followed by a message about the current in order to complete an action. This will be followed by a message about the current

View File

@@ -134,6 +134,10 @@ public class KeyguardHostView extends KeyguardViewBase {
void userActivity(); void userActivity();
} }
interface TransportControlCallback {
void userActivity();
}
/*package*/ interface OnDismissAction { /*package*/ interface OnDismissAction {
/* returns true if the dismiss should be deferred */ /* returns true if the dismiss should be deferred */
boolean onDismiss(); boolean onDismiss();
@@ -1222,6 +1226,11 @@ public class KeyguardHostView extends KeyguardViewBase {
LayoutInflater inflater = LayoutInflater.from(mContext); LayoutInflater inflater = LayoutInflater.from(mContext);
mTransportControl = (KeyguardTransportControlView) mTransportControl = (KeyguardTransportControlView)
inflater.inflate(R.layout.keyguard_transport_control_view, this, false); inflater.inflate(R.layout.keyguard_transport_control_view, this, false);
mTransportControl.setTransportControlCallback(new TransportControlCallback() {
public void userActivity() {
mViewMediatorCallback.userActivity();
}
});
} }
return mTransportControl; return mTransportControl;
} }

View File

@@ -16,191 +16,263 @@
package com.android.keyguard; package com.android.keyguard;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.IRemoteControlDisplay; import android.media.MediaMetadataEditor;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.media.RemoteControlClient; import android.media.RemoteControlClient;
import android.os.Bundle; import android.media.RemoteController;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.Spannable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ForegroundColorSpan; import android.text.format.DateFormat;
import android.transition.ChangeBounds;
import android.transition.ChangeText;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import java.lang.ref.WeakReference; import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/** /**
* This is the widget responsible for showing music controls in keyguard. * This is the widget responsible for showing music controls in keyguard.
*/ */
public class KeyguardTransportControlView extends FrameLayout implements OnClickListener { public class KeyguardTransportControlView extends FrameLayout {
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 DISPLAY_TIMEOUT_MS = 5000; // 5s private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s
private static final int RESET_TO_METADATA_DELAY = 5000;
protected static final boolean DEBUG = false; protected static final boolean DEBUG = false;
protected static final String TAG = "TransportControlView"; protected static final String TAG = "TransportControlView";
private ImageView mAlbumArt; private static final boolean ANIMATE_TRANSITIONS = false;
private ViewGroup mMetadataContainer;
private ViewGroup mInfoContainer;
private TextView mTrackTitle; private TextView mTrackTitle;
private TextView mTrackArtistAlbum;
private View mTransientSeek;
private SeekBar mTransientSeekBar;
private TextView mTransientSeekTimeElapsed;
private TextView mTransientSeekTimeRemaining;
private ImageView mBtnPrev; private ImageView mBtnPrev;
private ImageView mBtnPlay; private ImageView mBtnPlay;
private ImageView mBtnNext; private ImageView mBtnNext;
private int mClientGeneration;
private Metadata mMetadata = new Metadata(); private Metadata mMetadata = new Metadata();
private boolean mAttached;
private PendingIntent mClientIntent;
private int mTransportControlFlags; private int mTransportControlFlags;
private int mCurrentPlayState; private int mCurrentPlayState;
private AudioManager mAudioManager; private AudioManager mAudioManager;
private IRemoteControlDisplayWeak mIRCD; private RemoteController mRemoteController;
private ImageView mBadge;
private boolean mSeekEnabled;
private boolean mUserSeeking;
private java.text.DateFormat mFormat;
/** /**
* The metadata which should be populated into the view once we've been attached * The metadata which should be populated into the view once we've been attached
*/ */
private Bundle mPopulateMetadataWhenAttached = null; private RemoteController.MetadataEditor mPopulateMetadataWhenAttached = null;
// This handler is required to ensure messages from IRCD are handled in sequence and on private RemoteController.OnClientUpdateListener mRCClientUpdateListener =
// the UI thread. new RemoteController.OnClientUpdateListener() {
private Handler mHandler = new Handler() {
@Override @Override
public void handleMessage(Message msg) { public void onClientChange(boolean clearing) {
switch (msg.what) { if (clearing) {
case MSG_UPDATE_STATE: clearMetadata();
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;
KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground(
mMetadata.bitmap);
}
break;
case MSG_SET_GENERATION_ID:
if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2);
mClientGeneration = msg.arg1;
mClientIntent = (PendingIntent) msg.obj;
break;
} }
} }
};
/** @Override
* This class is required to have weak linkage to the current TransportControlView public void onClientPlaybackStateUpdate(int state) {
* because the remote process can hold a strong reference to this binder object and setSeekBarsEnabled(false);
* we can't predict when it will be GC'd in the remote process. Without this code, it updatePlayPauseState(state);
* 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<Handler> mLocalHandler;
IRemoteControlDisplayWeak(Handler handler) {
mLocalHandler = new WeakReference<Handler>(handler);
} }
public void setPlaybackState(int generationId, int state, long stateChangeTimeMs, @Override
public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
long currentPosMs, float speed) { long currentPosMs, float speed) {
Handler handler = mLocalHandler.get(); setSeekBarsEnabled(mMetadata != null && mMetadata.duration > 0);
if (handler != null) { updatePlayPauseState(state);
handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget(); if (DEBUG) Log.d(TAG, "onClientPlaybackStateUpdate(state=" + state +
} ", stateChangeTimeMs=" + stateChangeTimeMs + ", currentPosMs=" + currentPosMs +
", speed=" + speed + ")");
} }
public void setMetadata(int generationId, Bundle metadata) { @Override
Handler handler = mLocalHandler.get(); public void onClientTransportControlUpdate(int transportControlFlags) {
if (handler != null) { updateTransportControls(transportControlFlags);
handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
}
} }
public void setTransportControlInfo(int generationId, int flags, int posCapabilities) { @Override
Handler handler = mLocalHandler.get(); public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) {
if (handler != null) { updateMetadata(metadataEditor);
handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags)
.sendToTarget();
}
} }
};
public void setArtwork(int generationId, Bitmap bitmap) { private final Runnable mUpdateSeekBars = new Runnable() {
Handler handler = mLocalHandler.get(); public void run() {
if (handler != null) { if (updateSeekBars()) {
handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); postDelayed(this, 1000);
}
}
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, PendingIntent mediaIntent,
boolean clearing) throws RemoteException {
Handler handler = mLocalHandler.get();
if (handler != null) {
handler.obtainMessage(MSG_SET_GENERATION_ID,
clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget();
} }
} }
}; };
private final Runnable mResetToMetadata = new Runnable() {
public void run() {
resetToMetadata();
}
};
private final OnClickListener mTransportCommandListener = new OnClickListener() {
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);
}
}
};
private final OnLongClickListener mTransportShowSeekBarListener = new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (mSeekEnabled) {
return tryToggleSeekBar();
}
return false;
}
};
private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener =
new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
scrubTo(progress);
delayResetToMetadata();
}
updateSeekDisplay();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mUserSeeking = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mUserSeeking = false;
}
};
private static final int TRANSITION_DURATION = 200;
private final TransitionSet mMetadataChangeTransition;
KeyguardHostView.TransportControlCallback mTransportControlCallback;
public KeyguardTransportControlView(Context context, AttributeSet attrs) { public KeyguardTransportControlView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
if (DEBUG) Log.v(TAG, "Create TCV " + this); if (DEBUG) Log.v(TAG, "Create TCV " + this);
mAudioManager = new AudioManager(mContext); mAudioManager = new AudioManager(mContext);
mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback
mIRCD = new IRemoteControlDisplayWeak(mHandler); mRemoteController = new RemoteController(context);
mRemoteController.setOnClientUpdateListener(mRCClientUpdateListener);
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
final int dim = Math.max(dm.widthPixels, dm.heightPixels);
mRemoteController.setArtworkConfiguration(true, dim, dim);
final ChangeText tc = new ChangeText();
tc.setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN);
final TransitionSet inner = new TransitionSet();
inner.addTransition(tc).addTransition(new ChangeBounds());
final TransitionSet tg = new TransitionSet();
tg.addTransition(new Fade(Fade.OUT)).addTransition(inner).
addTransition(new Fade(Fade.IN));
tg.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
tg.setDuration(TRANSITION_DURATION);
mMetadataChangeTransition = tg;
} }
private void updateTransportControls(int transportControlFlags) { private void updateTransportControls(int transportControlFlags) {
mTransportControlFlags = transportControlFlags; mTransportControlFlags = transportControlFlags;
setSeekBarsEnabled(
(transportControlFlags & RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE) != 0);
}
void setSeekBarsEnabled(boolean enabled) {
if (enabled == mSeekEnabled) return;
mSeekEnabled = enabled;
if (mTransientSeek.getVisibility() == VISIBLE) {
mTransientSeek.setVisibility(INVISIBLE);
mMetadataContainer.setVisibility(VISIBLE);
mUserSeeking = false;
cancelResetToMetadata();
}
if (enabled) {
mUpdateSeekBars.run();
postDelayed(mUpdateSeekBars, 1000);
} else {
removeCallbacks(mUpdateSeekBars);
}
}
public void setTransportControlCallback(KeyguardHostView.TransportControlCallback
transportControlCallback) {
mTransportControlCallback = transportControlCallback;
} }
@Override @Override
public void onFinishInflate() { public void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
mInfoContainer = (ViewGroup) findViewById(R.id.info_container);
mMetadataContainer = (ViewGroup) findViewById(R.id.metadata_container);
mBadge = (ImageView) findViewById(R.id.badge);
mTrackTitle = (TextView) findViewById(R.id.title); mTrackTitle = (TextView) findViewById(R.id.title);
mTrackTitle.setSelected(true); // enable marquee mTrackTitle.setSelected(true); // enable marquee
mAlbumArt = (ImageView) findViewById(R.id.albumart); mTrackArtistAlbum = (TextView) findViewById(R.id.artist_album);
mTrackArtistAlbum.setSelected(true);
mTransientSeek = findViewById(R.id.transient_seek);
mTransientSeekBar = (SeekBar) findViewById(R.id.transient_seek_bar);
mTransientSeekBar.setOnSeekBarChangeListener(mOnSeekBarChangeListener);
mTransientSeekTimeElapsed = (TextView) findViewById(R.id.transient_seek_time_elapsed);
mTransientSeekTimeRemaining = (TextView) findViewById(R.id.transient_seek_time_remaining);
mBtnPrev = (ImageView) findViewById(R.id.btn_prev); mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
mBtnPlay = (ImageView) findViewById(R.id.btn_play); mBtnPlay = (ImageView) findViewById(R.id.btn_play);
mBtnNext = (ImageView) findViewById(R.id.btn_next); mBtnNext = (ImageView) findViewById(R.id.btn_next);
final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
for (View view : buttons) { for (View view : buttons) {
view.setOnClickListener(this); view.setOnClickListener(mTransportCommandListener);
view.setOnLongClickListener(mTransportShowSeekBarListener);
} }
} }
@@ -212,32 +284,34 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
updateMetadata(mPopulateMetadataWhenAttached); updateMetadata(mPopulateMetadataWhenAttached);
mPopulateMetadataWhenAttached = null; mPopulateMetadataWhenAttached = null;
} }
if (!mAttached) { if (DEBUG) Log.v(TAG, "Registering TCV " + this);
if (DEBUG) Log.v(TAG, "Registering TCV " + this); mAudioManager.registerRemoteController(mRemoteController);
mAudioManager.registerRemoteControlDisplay(mIRCD);
}
mAttached = true;
} }
@Override @Override
protected void onSizeChanged (int w, int h, int oldw, int oldh) { protected void onConfigurationChanged(Configuration newConfig) {
if (mAttached) { super.onConfigurationChanged(newConfig);
final DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); final DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
int dim = Math.max(dm.widthPixels, dm.heightPixels); final int dim = Math.max(dm.widthPixels, dm.heightPixels);
if (DEBUG) Log.v(TAG, "TCV uses bitmap size=" + dim); mRemoteController.setArtworkConfiguration(true, dim, dim);
mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim);
}
} }
@Override @Override
public void onDetachedFromWindow() { public void onDetachedFromWindow() {
if (DEBUG) Log.v(TAG, "onDetachFromWindow()"); if (DEBUG) Log.v(TAG, "onDetachFromWindow()");
super.onDetachedFromWindow(); super.onDetachedFromWindow();
if (mAttached) { if (DEBUG) Log.v(TAG, "Unregistering TCV " + this);
if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); mAudioManager.unregisterRemoteController(mRemoteController);
mAudioManager.unregisterRemoteControlDisplay(mIRCD); mUserSeeking = false;
} }
mAttached = false;
void setBadgeIcon(Drawable bmp) {
mBadge.setImageDrawable(bmp);
final ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
mBadge.setColorFilter(new ColorMatrixColorFilter(cm));
mBadge.setImageAlpha(0xef);
} }
class Metadata { class Metadata {
@@ -245,21 +319,39 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
private String trackTitle; private String trackTitle;
private String albumTitle; private String albumTitle;
private Bitmap bitmap; private Bitmap bitmap;
private long duration;
public void clear() {
artist = null;
trackTitle = null;
albumTitle = null;
bitmap = null;
duration = -1;
}
public String toString() { public String toString() {
return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]"; return "Metadata[artist=" + artist + " trackTitle=" + trackTitle +
" albumTitle=" + albumTitle + " duration=" + duration + "]";
} }
} }
private String getMdString(Bundle data, int id) { void clearMetadata() {
return data.getString(Integer.toString(id)); mPopulateMetadataWhenAttached = null;
mMetadata.clear();
populateMetadata();
} }
private void updateMetadata(Bundle data) { void updateMetadata(RemoteController.MetadataEditor data) {
if (mAttached) { if (isAttachedToWindow()) {
mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST); mMetadata.artist = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE); mMetadata.artist);
mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM); mMetadata.trackTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_TITLE,
mMetadata.trackTitle);
mMetadata.albumTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
mMetadata.albumTitle);
mMetadata.duration = data.getLong(MediaMetadataRetriever.METADATA_KEY_DURATION, -1);
mMetadata.bitmap = data.getBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK,
mMetadata.bitmap);
populateMetadata(); populateMetadata();
} else { } else {
mPopulateMetadataWhenAttached = data; mPopulateMetadataWhenAttached = data;
@@ -270,12 +362,22 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
* Populates the given metadata into the view * Populates the given metadata into the view
*/ */
private void populateMetadata() { private void populateMetadata() {
StringBuilder sb = new StringBuilder(); if (ANIMATE_TRANSITIONS && isLaidOut() && mMetadataContainer.getVisibility() == VISIBLE) {
int trackTitleLength = 0; TransitionManager.beginDelayedTransition(mMetadataContainer, mMetadataChangeTransition);
if (!TextUtils.isEmpty(mMetadata.trackTitle)) {
sb.append(mMetadata.trackTitle);
trackTitleLength = mMetadata.trackTitle.length();
} }
final String remoteClientPackage = mRemoteController.getRemoteControlClientPackageName();
Drawable badgeIcon = null;
try {
badgeIcon = getContext().getPackageManager().getApplicationIcon(remoteClientPackage);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Couldn't get remote control client package icon", e);
}
setBadgeIcon(badgeIcon);
if (!TextUtils.isEmpty(mMetadata.trackTitle)) {
mTrackTitle.setText(mMetadata.trackTitle);
}
StringBuilder sb = new StringBuilder();
if (!TextUtils.isEmpty(mMetadata.artist)) { if (!TextUtils.isEmpty(mMetadata.artist)) {
if (sb.length() != 0) { if (sb.length() != 0) {
sb.append(" - "); sb.append(" - ");
@@ -288,16 +390,27 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
} }
sb.append(mMetadata.albumTitle); sb.append(mMetadata.albumTitle);
} }
mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE); mTrackArtistAlbum.setText(sb.toString());
Spannable str = (Spannable) mTrackTitle.getText();
if (trackTitleLength != 0) { if (mMetadata.duration >= 0) {
str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength, setSeekBarsEnabled(true);
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); setSeekBarDuration(mMetadata.duration);
trackTitleLength++;
} final String skeleton;
if (sb.length() > trackTitleLength) {
str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(), if (mMetadata.duration >= 86400000) {
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); skeleton = "DDD kk mm ss";
} else if (mMetadata.duration >= 3600000) {
skeleton = "kk mm ss";
} else {
skeleton = "mm ss";
}
mFormat = new SimpleDateFormat(DateFormat.getBestDateTimePattern(
getContext().getResources().getConfiguration().locale,
skeleton));
mFormat.setTimeZone(TimeZone.getTimeZone("GMT+0"));
} else {
setSeekBarsEnabled(false);
} }
KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground( KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground(
@@ -314,6 +427,66 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
updatePlayPauseState(mCurrentPlayState); updatePlayPauseState(mCurrentPlayState);
} }
void updateSeekDisplay() {
if (mMetadata != null && mRemoteController != null && mFormat != null) {
final long timeElapsed = mRemoteController.getEstimatedMediaPosition();
final long duration = mMetadata.duration;
final long remaining = duration - timeElapsed;
mTransientSeekTimeElapsed.setText(mFormat.format(new Date(timeElapsed)));
mTransientSeekTimeRemaining.setText(mFormat.format(new Date(remaining)));
if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + timeElapsed +
" duration=" + duration + " remaining=" + remaining);
}
}
boolean tryToggleSeekBar() {
if (ANIMATE_TRANSITIONS) {
TransitionManager.beginDelayedTransition(mInfoContainer);
}
if (mTransientSeek.getVisibility() == VISIBLE) {
mTransientSeek.setVisibility(INVISIBLE);
mMetadataContainer.setVisibility(VISIBLE);
cancelResetToMetadata();
} else {
mTransientSeek.setVisibility(VISIBLE);
mMetadataContainer.setVisibility(INVISIBLE);
delayResetToMetadata();
}
mTransportControlCallback.userActivity();
return true;
}
void resetToMetadata() {
if (ANIMATE_TRANSITIONS) {
TransitionManager.beginDelayedTransition(mInfoContainer);
}
if (mTransientSeek.getVisibility() == VISIBLE) {
mTransientSeek.setVisibility(INVISIBLE);
mMetadataContainer.setVisibility(VISIBLE);
}
// TODO Also hide ratings, if applicable
}
void delayResetToMetadata() {
removeCallbacks(mResetToMetadata);
postDelayed(mResetToMetadata, RESET_TO_METADATA_DELAY);
}
void cancelResetToMetadata() {
removeCallbacks(mResetToMetadata);
}
void setSeekBarDuration(long duration) {
mTransientSeekBar.setMax((int) duration);
}
void scrubTo(int progress) {
mRemoteController.seekTo(progress);
mTransportControlCallback.userActivity();
}
private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
if ((flags & flag) != 0) { if ((flags & flag) != 0) {
view.setVisibility(View.VISIBLE); view.setVisibility(View.VISIBLE);
@@ -341,6 +514,9 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
case RemoteControlClient.PLAYSTATE_PLAYING: case RemoteControlClient.PLAYSTATE_PLAYING:
imageResId = R.drawable.ic_media_pause; imageResId = R.drawable.ic_media_pause;
imageDescId = R.string.keyguard_transport_pause_description; imageDescId = R.string.keyguard_transport_pause_description;
if (mSeekEnabled) {
postDelayed(mUpdateSeekBars, 1000);
}
break; break;
case RemoteControlClient.PLAYSTATE_BUFFERING: case RemoteControlClient.PLAYSTATE_BUFFERING:
@@ -354,11 +530,30 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
imageDescId = R.string.keyguard_transport_play_description; imageDescId = R.string.keyguard_transport_play_description;
break; break;
} }
if (state != RemoteControlClient.PLAYSTATE_PLAYING) {
removeCallbacks(mUpdateSeekBars);
updateSeekBars();
}
mBtnPlay.setImageResource(imageResId); mBtnPlay.setImageResource(imageResId);
mBtnPlay.setContentDescription(getResources().getString(imageDescId)); mBtnPlay.setContentDescription(getResources().getString(imageDescId));
mCurrentPlayState = state; mCurrentPlayState = state;
} }
boolean updateSeekBars() {
final int position = (int) mRemoteController.getEstimatedMediaPosition();
if (position >= 0) {
if (!mUserSeeking) {
mTransientSeekBar.setProgress(position);
}
return true;
}
Log.w(TAG, "Updating seek bars; received invalid estimated media position (" +
position + "). Disabling seek.");
setSeekBarsEnabled(false);
return false;
}
static class SavedState extends BaseSavedState { static class SavedState extends BaseSavedState {
boolean clientPresent; boolean clientPresent;
@@ -389,48 +584,13 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick
}; };
} }
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);
}
}
private void sendMediaButtonClick(int keyCode) { private void sendMediaButtonClick(int keyCode) {
if (mClientIntent == null) { // TODO We should think about sending these up/down events accurately with touch up/down
// Shouldn't be possible because this view should be hidden in this case. // on the buttons, but in the near term this will interfere with the long press behavior.
Log.e(TAG, "sendMediaButtonClick(): No client is currently registered"); mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
return; mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
}
// use the registered PendingIntent that will be processed by the registered
// media button event receiver, which is the component of mClientIntent
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
try {
mClientIntent.send(getContext(), 0, intent);
} catch (CanceledException e) {
Log.e(TAG, "Error sending intent for media button down: "+e);
e.printStackTrace();
}
keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); mTransportControlCallback.userActivity();
intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
try {
mClientIntent.send(getContext(), 0, intent);
} catch (CanceledException e) {
Log.e(TAG, "Error sending intent for media button up: "+e);
e.printStackTrace();
}
} }
public boolean providesClock() { public boolean providesClock() {