1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.keyguard;
18
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.res.Configuration;
22import android.graphics.Bitmap;
23import android.graphics.ColorMatrix;
24import android.graphics.ColorMatrixColorFilter;
25import android.graphics.PorterDuff;
26import android.graphics.PorterDuffXfermode;
27import android.graphics.drawable.Drawable;
28import android.media.AudioManager;
29import android.media.MediaMetadataEditor;
30import android.media.MediaMetadataRetriever;
31import android.media.RemoteControlClient;
32import android.media.RemoteController;
33import android.os.Parcel;
34import android.os.Parcelable;
35import android.text.TextUtils;
36import android.text.format.DateFormat;
37import android.transition.ChangeBounds;
38import android.transition.ChangeText;
39import android.transition.Fade;
40import android.transition.TransitionManager;
41import android.transition.TransitionSet;
42import android.util.AttributeSet;
43import android.util.DisplayMetrics;
44import android.util.Log;
45import android.view.KeyEvent;
46import android.view.View;
47import android.view.ViewGroup;
48import android.widget.FrameLayout;
49import android.widget.ImageView;
50import android.widget.SeekBar;
51import android.widget.TextView;
52
53import java.text.SimpleDateFormat;
54import java.util.Date;
55import java.util.TimeZone;
56
57/**
58 * This is the widget responsible for showing music controls in keyguard.
59 */
60public class KeyguardTransportControlView extends FrameLayout {
61
62    private static final int RESET_TO_METADATA_DELAY = 5000;
63    protected static final boolean DEBUG = KeyguardConstants.DEBUG;
64    protected static final String TAG = "TransportControlView";
65
66    private static final boolean ANIMATE_TRANSITIONS = true;
67    protected static final long QUIESCENT_PLAYBACK_FACTOR = 1000;
68
69    private ViewGroup mMetadataContainer;
70    private ViewGroup mInfoContainer;
71    private TextView mTrackTitle;
72    private TextView mTrackArtistAlbum;
73
74    private View mTransientSeek;
75    private SeekBar mTransientSeekBar;
76    private TextView mTransientSeekTimeElapsed;
77    private TextView mTransientSeekTimeTotal;
78
79    private ImageView mBtnPrev;
80    private ImageView mBtnPlay;
81    private ImageView mBtnNext;
82    private Metadata mMetadata = new Metadata();
83    private int mTransportControlFlags;
84    private int mCurrentPlayState;
85    private AudioManager mAudioManager;
86    private RemoteController mRemoteController;
87
88    private ImageView mBadge;
89
90    private boolean mSeekEnabled;
91    private java.text.DateFormat mFormat;
92
93    private Date mTempDate = new Date();
94
95    /**
96     * The metadata which should be populated into the view once we've been attached
97     */
98    private RemoteController.MetadataEditor mPopulateMetadataWhenAttached = null;
99
100    private RemoteController.OnClientUpdateListener mRCClientUpdateListener =
101            new RemoteController.OnClientUpdateListener() {
102        @Override
103        public void onClientChange(boolean clearing) {
104            if (clearing) {
105                clearMetadata();
106            }
107        }
108
109        @Override
110        public void onClientPlaybackStateUpdate(int state) {
111            updatePlayPauseState(state);
112        }
113
114        @Override
115        public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
116                long currentPosMs, float speed) {
117            updatePlayPauseState(state);
118            if (DEBUG) Log.d(TAG, "onClientPlaybackStateUpdate(state=" + state +
119                    ", stateChangeTimeMs=" + stateChangeTimeMs + ", currentPosMs=" + currentPosMs +
120                    ", speed=" + speed + ")");
121
122            removeCallbacks(mUpdateSeekBars);
123            // Since the music client may be responding to historical events that cause the
124            // playback state to change dramatically, wait until things become quiescent before
125            // resuming automatic scrub position update.
126            if (mTransientSeek.getVisibility() == View.VISIBLE
127                    && playbackPositionShouldMove(mCurrentPlayState)) {
128                postDelayed(mUpdateSeekBars, QUIESCENT_PLAYBACK_FACTOR);
129            }
130        }
131
132        @Override
133        public void onClientTransportControlUpdate(int transportControlFlags) {
134            updateTransportControls(transportControlFlags);
135        }
136
137        @Override
138        public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) {
139            updateMetadata(metadataEditor);
140        }
141    };
142
143    private class UpdateSeekBarRunnable implements  Runnable {
144        public void run() {
145            boolean seekAble = updateOnce();
146            if (seekAble) {
147                removeCallbacks(this);
148                postDelayed(this, 1000);
149            }
150        }
151        public boolean updateOnce() {
152            return updateSeekBars();
153        }
154    };
155
156    private final UpdateSeekBarRunnable mUpdateSeekBars = new UpdateSeekBarRunnable();
157
158    private final Runnable mResetToMetadata = new Runnable() {
159        public void run() {
160            resetToMetadata();
161        }
162    };
163
164    private final OnClickListener mTransportCommandListener = new OnClickListener() {
165        public void onClick(View v) {
166            int keyCode = -1;
167            if (v == mBtnPrev) {
168                keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
169            } else if (v == mBtnNext) {
170                keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
171            } else if (v == mBtnPlay) {
172                keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
173            }
174            if (keyCode != -1) {
175                sendMediaButtonClick(keyCode);
176                delayResetToMetadata(); // if the scrub bar is showing, keep showing it.
177            }
178        }
179    };
180
181    private final OnLongClickListener mTransportShowSeekBarListener = new OnLongClickListener() {
182        @Override
183        public boolean onLongClick(View v) {
184            if (mSeekEnabled) {
185                return tryToggleSeekBar();
186            }
187            return false;
188        }
189    };
190
191    // This class is here to throttle scrub position updates to the music client
192    class FutureSeekRunnable implements Runnable {
193        private int mProgress;
194        private boolean mPending;
195
196        public void run() {
197            scrubTo(mProgress);
198            mPending = false;
199        }
200
201        void setProgress(int progress) {
202            mProgress = progress;
203            if (!mPending) {
204                mPending = true;
205                postDelayed(this, 30);
206            }
207        }
208    };
209
210    // This is here because RemoteControlClient's method isn't visible :/
211    private final static boolean playbackPositionShouldMove(int playstate) {
212        switch(playstate) {
213            case RemoteControlClient.PLAYSTATE_STOPPED:
214            case RemoteControlClient.PLAYSTATE_PAUSED:
215            case RemoteControlClient.PLAYSTATE_BUFFERING:
216            case RemoteControlClient.PLAYSTATE_ERROR:
217            case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
218            case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
219                return false;
220            case RemoteControlClient.PLAYSTATE_PLAYING:
221            case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
222            case RemoteControlClient.PLAYSTATE_REWINDING:
223            default:
224                return true;
225        }
226    }
227
228    private final FutureSeekRunnable mFutureSeekRunnable = new FutureSeekRunnable();
229
230    private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener =
231            new SeekBar.OnSeekBarChangeListener() {
232        @Override
233        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
234            if (fromUser) {
235                mFutureSeekRunnable.setProgress(progress);
236                delayResetToMetadata();
237                mTempDate.setTime(progress);
238                mTransientSeekTimeElapsed.setText(mFormat.format(mTempDate));
239            } else {
240                updateSeekDisplay();
241            }
242        }
243
244        @Override
245        public void onStartTrackingTouch(SeekBar seekBar) {
246            delayResetToMetadata();
247            removeCallbacks(mUpdateSeekBars); // don't update during user interaction
248        }
249
250        @Override
251        public void onStopTrackingTouch(SeekBar seekBar) {
252        }
253    };
254
255    private static final int TRANSITION_DURATION = 200;
256    private final TransitionSet mMetadataChangeTransition;
257
258    KeyguardHostView.TransportControlCallback mTransportControlCallback;
259
260    private final KeyguardUpdateMonitorCallback mUpdateMonitor
261            = new KeyguardUpdateMonitorCallback() {
262        public void onScreenTurnedOff(int why) {
263            setEnableMarquee(false);
264        }
265        public void onScreenTurnedOn() {
266            setEnableMarquee(true);
267        }
268    };
269
270    public KeyguardTransportControlView(Context context, AttributeSet attrs) {
271        super(context, attrs);
272        if (DEBUG) Log.v(TAG, "Create TCV " + this);
273        mAudioManager = new AudioManager(mContext);
274        mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback
275        mRemoteController = new RemoteController(context, mRCClientUpdateListener);
276
277        final DisplayMetrics dm = context.getResources().getDisplayMetrics();
278        final int dim = Math.max(dm.widthPixels, dm.heightPixels);
279        mRemoteController.setArtworkConfiguration(true, dim, dim);
280
281        final ChangeText tc = new ChangeText();
282        tc.setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN);
283        final TransitionSet inner = new TransitionSet();
284        inner.addTransition(tc).addTransition(new ChangeBounds());
285        final TransitionSet tg = new TransitionSet();
286        tg.addTransition(new Fade(Fade.OUT)).addTransition(inner).
287                addTransition(new Fade(Fade.IN));
288        tg.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
289        tg.setDuration(TRANSITION_DURATION);
290        mMetadataChangeTransition = tg;
291    }
292
293    private void updateTransportControls(int transportControlFlags) {
294        mTransportControlFlags = transportControlFlags;
295        setSeekBarsEnabled(
296                (transportControlFlags & RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE) != 0);
297    }
298
299    void setSeekBarsEnabled(boolean enabled) {
300        if (enabled == mSeekEnabled) return;
301
302        mSeekEnabled = enabled;
303        if (mTransientSeek.getVisibility() == VISIBLE && !enabled) {
304            mTransientSeek.setVisibility(INVISIBLE);
305            mMetadataContainer.setVisibility(VISIBLE);
306            cancelResetToMetadata();
307        }
308    }
309
310    public void setTransportControlCallback(KeyguardHostView.TransportControlCallback
311            transportControlCallback) {
312        mTransportControlCallback = transportControlCallback;
313    }
314
315    private void setEnableMarquee(boolean enabled) {
316        if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee");
317        if (mTrackTitle != null) mTrackTitle.setSelected(enabled);
318        if (mTrackArtistAlbum != null) mTrackTitle.setSelected(enabled);
319    }
320
321    @Override
322    public void onFinishInflate() {
323        super.onFinishInflate();
324        mInfoContainer = (ViewGroup) findViewById(R.id.info_container);
325        mMetadataContainer = (ViewGroup) findViewById(R.id.metadata_container);
326        mBadge = (ImageView) findViewById(R.id.badge);
327        mTrackTitle = (TextView) findViewById(R.id.title);
328        mTrackArtistAlbum = (TextView) findViewById(R.id.artist_album);
329        mTransientSeek = findViewById(R.id.transient_seek);
330        mTransientSeekBar = (SeekBar) findViewById(R.id.transient_seek_bar);
331        mTransientSeekBar.setOnSeekBarChangeListener(mOnSeekBarChangeListener);
332        mTransientSeekTimeElapsed = (TextView) findViewById(R.id.transient_seek_time_elapsed);
333        mTransientSeekTimeTotal = (TextView) findViewById(R.id.transient_seek_time_remaining);
334        mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
335        mBtnPlay = (ImageView) findViewById(R.id.btn_play);
336        mBtnNext = (ImageView) findViewById(R.id.btn_next);
337        final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
338        for (View view : buttons) {
339            view.setOnClickListener(mTransportCommandListener);
340            view.setOnLongClickListener(mTransportShowSeekBarListener);
341        }
342        final boolean screenOn = KeyguardUpdateMonitor.getInstance(mContext).isScreenOn();
343        setEnableMarquee(screenOn);
344        // Allow long-press anywhere else in this view to show the seek bar
345        setOnLongClickListener(mTransportShowSeekBarListener);
346    }
347
348    @Override
349    public void onAttachedToWindow() {
350        super.onAttachedToWindow();
351        if (DEBUG) Log.v(TAG, "onAttachToWindow()");
352        if (mPopulateMetadataWhenAttached != null) {
353            updateMetadata(mPopulateMetadataWhenAttached);
354            mPopulateMetadataWhenAttached = null;
355        }
356        if (DEBUG) Log.v(TAG, "Registering TCV " + this);
357        mMetadata.clear();
358        mAudioManager.registerRemoteController(mRemoteController);
359        KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitor);
360    }
361
362    @Override
363    protected void onConfigurationChanged(Configuration newConfig) {
364        super.onConfigurationChanged(newConfig);
365        final DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
366        final int dim = Math.max(dm.widthPixels, dm.heightPixels);
367        mRemoteController.setArtworkConfiguration(true, dim, dim);
368    }
369
370    @Override
371    public void onDetachedFromWindow() {
372        if (DEBUG) Log.v(TAG, "onDetachFromWindow()");
373        super.onDetachedFromWindow();
374        if (DEBUG) Log.v(TAG, "Unregistering TCV " + this);
375        mAudioManager.unregisterRemoteController(mRemoteController);
376        KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitor);
377        mMetadata.clear();
378        removeCallbacks(mUpdateSeekBars);
379    }
380
381    @Override
382    protected Parcelable onSaveInstanceState() {
383        SavedState ss = new SavedState(super.onSaveInstanceState());
384        ss.artist = mMetadata.artist;
385        ss.trackTitle = mMetadata.trackTitle;
386        ss.albumTitle = mMetadata.albumTitle;
387        ss.duration = mMetadata.duration;
388        ss.bitmap = mMetadata.bitmap;
389        return ss;
390    }
391
392    @Override
393    protected void onRestoreInstanceState(Parcelable state) {
394        if (!(state instanceof SavedState)) {
395            super.onRestoreInstanceState(state);
396            return;
397        }
398        SavedState ss = (SavedState) state;
399        super.onRestoreInstanceState(ss.getSuperState());
400        mMetadata.artist = ss.artist;
401        mMetadata.trackTitle = ss.trackTitle;
402        mMetadata.albumTitle = ss.albumTitle;
403        mMetadata.duration = ss.duration;
404        mMetadata.bitmap = ss.bitmap;
405        populateMetadata();
406    }
407
408    void setBadgeIcon(Drawable bmp) {
409        mBadge.setImageDrawable(bmp);
410
411        final ColorMatrix cm = new ColorMatrix();
412        cm.setSaturation(0);
413        mBadge.setColorFilter(new ColorMatrixColorFilter(cm));
414        mBadge.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));
415        mBadge.setImageAlpha(0xef);
416    }
417
418    class Metadata {
419        private String artist;
420        private String trackTitle;
421        private String albumTitle;
422        private Bitmap bitmap;
423        private long duration;
424
425        public void clear() {
426            artist = null;
427            trackTitle = null;
428            albumTitle = null;
429            bitmap = null;
430            duration = -1;
431        }
432
433        public String toString() {
434            return "Metadata[artist=" + artist + " trackTitle=" + trackTitle +
435                    " albumTitle=" + albumTitle + " duration=" + duration + "]";
436        }
437    }
438
439    void clearMetadata() {
440        mPopulateMetadataWhenAttached = null;
441        mMetadata.clear();
442        populateMetadata();
443    }
444
445    void updateMetadata(RemoteController.MetadataEditor data) {
446        if (isAttachedToWindow()) {
447            mMetadata.artist = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
448                    mMetadata.artist);
449            mMetadata.trackTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_TITLE,
450                    mMetadata.trackTitle);
451            mMetadata.albumTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
452                    mMetadata.albumTitle);
453            mMetadata.duration = data.getLong(MediaMetadataRetriever.METADATA_KEY_DURATION, -1);
454            mMetadata.bitmap = data.getBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK,
455                    mMetadata.bitmap);
456            populateMetadata();
457        } else {
458            mPopulateMetadataWhenAttached = data;
459        }
460    }
461
462    /**
463     * Populates the given metadata into the view
464     */
465    private void populateMetadata() {
466        if (ANIMATE_TRANSITIONS && isLaidOut() && mMetadataContainer.getVisibility() == VISIBLE) {
467            TransitionManager.beginDelayedTransition(mMetadataContainer, mMetadataChangeTransition);
468        }
469
470        final String remoteClientPackage = mRemoteController.getRemoteControlClientPackageName();
471        Drawable badgeIcon = null;
472        try {
473            badgeIcon = getContext().getPackageManager().getApplicationIcon(remoteClientPackage);
474        } catch (PackageManager.NameNotFoundException e) {
475            Log.e(TAG, "Couldn't get remote control client package icon", e);
476        }
477        setBadgeIcon(badgeIcon);
478        mTrackTitle.setText(!TextUtils.isEmpty(mMetadata.trackTitle)
479                ? mMetadata.trackTitle : null);
480
481        final StringBuilder sb = new StringBuilder();
482        if (!TextUtils.isEmpty(mMetadata.artist)) {
483            if (sb.length() != 0) {
484                sb.append(" - ");
485            }
486            sb.append(mMetadata.artist);
487        }
488        if (!TextUtils.isEmpty(mMetadata.albumTitle)) {
489            if (sb.length() != 0) {
490                sb.append(" - ");
491            }
492            sb.append(mMetadata.albumTitle);
493        }
494
495        final String trackArtistAlbum = sb.toString();
496        mTrackArtistAlbum.setText(!TextUtils.isEmpty(trackArtistAlbum) ?
497                trackArtistAlbum : null);
498
499        if (mMetadata.duration >= 0) {
500            setSeekBarsEnabled(true);
501            setSeekBarDuration(mMetadata.duration);
502
503            final String skeleton;
504
505            if (mMetadata.duration >= 86400000) {
506                skeleton = "DDD kk mm ss";
507            } else if (mMetadata.duration >= 3600000) {
508                skeleton = "kk mm ss";
509            } else {
510                skeleton = "mm ss";
511            }
512            mFormat = new SimpleDateFormat(DateFormat.getBestDateTimePattern(
513                    getContext().getResources().getConfiguration().locale,
514                    skeleton));
515            mFormat.setTimeZone(TimeZone.getTimeZone("GMT+0"));
516        } else {
517            setSeekBarsEnabled(false);
518        }
519
520        KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground(mMetadata.bitmap);
521        final int flags = mTransportControlFlags;
522        setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
523        setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
524        setVisibilityBasedOnFlag(mBtnPlay, flags,
525                RemoteControlClient.FLAG_KEY_MEDIA_PLAY
526                | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
527                | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
528                | RemoteControlClient.FLAG_KEY_MEDIA_STOP);
529
530        updatePlayPauseState(mCurrentPlayState);
531    }
532
533    void updateSeekDisplay() {
534        if (mMetadata != null && mRemoteController != null && mFormat != null) {
535            mTempDate.setTime(mRemoteController.getEstimatedMediaPosition());
536            mTransientSeekTimeElapsed.setText(mFormat.format(mTempDate));
537            mTempDate.setTime(mMetadata.duration);
538            mTransientSeekTimeTotal.setText(mFormat.format(mTempDate));
539
540            if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + mTempDate +
541                    " duration=" + mMetadata.duration);
542        }
543    }
544
545    boolean tryToggleSeekBar() {
546        if (ANIMATE_TRANSITIONS) {
547            TransitionManager.beginDelayedTransition(mInfoContainer);
548        }
549        if (mTransientSeek.getVisibility() == VISIBLE) {
550            mTransientSeek.setVisibility(INVISIBLE);
551            mMetadataContainer.setVisibility(VISIBLE);
552            cancelResetToMetadata();
553            removeCallbacks(mUpdateSeekBars); // don't update if scrubber isn't visible
554        } else {
555            mTransientSeek.setVisibility(VISIBLE);
556            mMetadataContainer.setVisibility(INVISIBLE);
557            delayResetToMetadata();
558            if (playbackPositionShouldMove(mCurrentPlayState)) {
559                mUpdateSeekBars.run();
560            } else {
561                mUpdateSeekBars.updateOnce();
562            }
563        }
564        mTransportControlCallback.userActivity();
565        return true;
566    }
567
568    void resetToMetadata() {
569        if (ANIMATE_TRANSITIONS) {
570            TransitionManager.beginDelayedTransition(mInfoContainer);
571        }
572        if (mTransientSeek.getVisibility() == VISIBLE) {
573            mTransientSeek.setVisibility(INVISIBLE);
574            mMetadataContainer.setVisibility(VISIBLE);
575        }
576        // TODO Also hide ratings, if applicable
577    }
578
579    void delayResetToMetadata() {
580        removeCallbacks(mResetToMetadata);
581        postDelayed(mResetToMetadata, RESET_TO_METADATA_DELAY);
582    }
583
584    void cancelResetToMetadata() {
585        removeCallbacks(mResetToMetadata);
586    }
587
588    void setSeekBarDuration(long duration) {
589        mTransientSeekBar.setMax((int) duration);
590    }
591
592    void scrubTo(int progress) {
593        mRemoteController.seekTo(progress);
594        mTransportControlCallback.userActivity();
595    }
596
597    private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
598        if ((flags & flag) != 0) {
599            view.setVisibility(View.VISIBLE);
600        } else {
601            view.setVisibility(View.INVISIBLE);
602        }
603    }
604
605    private void updatePlayPauseState(int state) {
606        if (DEBUG) Log.v(TAG,
607                "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state);
608        if (state == mCurrentPlayState) {
609            return;
610        }
611        final int imageResId;
612        final int imageDescId;
613        switch (state) {
614            case RemoteControlClient.PLAYSTATE_ERROR:
615                imageResId = R.drawable.stat_sys_warning;
616                // TODO use more specific image description string for warning, but here the "play"
617                //      message is still valid because this button triggers a play command.
618                imageDescId = R.string.keyguard_transport_play_description;
619                break;
620
621            case RemoteControlClient.PLAYSTATE_PLAYING:
622                imageResId = R.drawable.ic_media_pause;
623                imageDescId = R.string.keyguard_transport_pause_description;
624                break;
625
626            case RemoteControlClient.PLAYSTATE_BUFFERING:
627                imageResId = R.drawable.ic_media_stop;
628                imageDescId = R.string.keyguard_transport_stop_description;
629                break;
630
631            case RemoteControlClient.PLAYSTATE_PAUSED:
632            default:
633                imageResId = R.drawable.ic_media_play;
634                imageDescId = R.string.keyguard_transport_play_description;
635                break;
636        }
637
638        boolean clientSupportsSeek = mMetadata != null && mMetadata.duration > 0;
639        setSeekBarsEnabled(clientSupportsSeek);
640
641        mBtnPlay.setImageResource(imageResId);
642        mBtnPlay.setContentDescription(getResources().getString(imageDescId));
643        mCurrentPlayState = state;
644    }
645
646    boolean updateSeekBars() {
647        final int position = (int) mRemoteController.getEstimatedMediaPosition();
648        if (DEBUG) Log.v(TAG, "Estimated time:" + position);
649        if (position >= 0) {
650            mTransientSeekBar.setProgress(position);
651            return true;
652        }
653        Log.w(TAG, "Updating seek bars; received invalid estimated media position (" +
654                position + "). Disabling seek.");
655        setSeekBarsEnabled(false);
656        return false;
657    }
658
659    static class SavedState extends BaseSavedState {
660        boolean clientPresent;
661        String artist;
662        String trackTitle;
663        String albumTitle;
664        long duration;
665        Bitmap bitmap;
666
667        SavedState(Parcelable superState) {
668            super(superState);
669        }
670
671        private SavedState(Parcel in) {
672            super(in);
673            clientPresent = in.readInt() != 0;
674            artist = in.readString();
675            trackTitle = in.readString();
676            albumTitle = in.readString();
677            duration = in.readLong();
678            bitmap = Bitmap.CREATOR.createFromParcel(in);
679        }
680
681        @Override
682        public void writeToParcel(Parcel out, int flags) {
683            super.writeToParcel(out, flags);
684            out.writeInt(clientPresent ? 1 : 0);
685            out.writeString(artist);
686            out.writeString(trackTitle);
687            out.writeString(albumTitle);
688            out.writeLong(duration);
689            bitmap.writeToParcel(out, flags);
690        }
691
692        public static final Parcelable.Creator<SavedState> CREATOR
693                = new Parcelable.Creator<SavedState>() {
694            public SavedState createFromParcel(Parcel in) {
695                return new SavedState(in);
696            }
697
698            public SavedState[] newArray(int size) {
699                return new SavedState[size];
700            }
701        };
702    }
703
704    private void sendMediaButtonClick(int keyCode) {
705        // TODO We should think about sending these up/down events accurately with touch up/down
706        // on the buttons, but in the near term this will interfere with the long press behavior.
707        mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
708        mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
709
710        mTransportControlCallback.userActivity();
711    }
712
713    public boolean providesClock() {
714        return false;
715    }
716}
717