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 
17 package com.android.keyguard;
18 
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 import android.content.res.Configuration;
22 import android.graphics.Bitmap;
23 import android.graphics.ColorMatrix;
24 import android.graphics.ColorMatrixColorFilter;
25 import android.graphics.PorterDuff;
26 import android.graphics.PorterDuffXfermode;
27 import android.graphics.drawable.Drawable;
28 import android.media.AudioManager;
29 import android.media.MediaMetadataEditor;
30 import android.media.MediaMetadataRetriever;
31 import android.media.RemoteControlClient;
32 import android.media.RemoteController;
33 import android.os.Parcel;
34 import android.os.Parcelable;
35 import android.text.TextUtils;
36 import android.text.format.DateFormat;
37 import android.transition.ChangeBounds;
38 import android.transition.ChangeText;
39 import android.transition.Fade;
40 import android.transition.TransitionManager;
41 import android.transition.TransitionSet;
42 import android.util.AttributeSet;
43 import android.util.DisplayMetrics;
44 import android.util.Log;
45 import android.view.KeyEvent;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import android.widget.FrameLayout;
49 import android.widget.ImageView;
50 import android.widget.SeekBar;
51 import android.widget.TextView;
52 
53 import java.text.SimpleDateFormat;
54 import java.util.Date;
55 import java.util.TimeZone;
56 
57 /**
58  * This is the widget responsible for showing music controls in keyguard.
59  */
60 public 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 {
run()144         public void run() {
145             boolean seekAble = updateOnce();
146             if (seekAble) {
147                 removeCallbacks(this);
148                 postDelayed(this, 1000);
149             }
150         }
updateOnce()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 
run()196         public void run() {
197             scrubTo(mProgress);
198             mPending = false;
199         }
200 
setProgress(int progress)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 :/
playbackPositionShouldMove(int playstate)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 
KeyguardTransportControlView(Context context, AttributeSet attrs)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 
updateTransportControls(int transportControlFlags)293     private void updateTransportControls(int transportControlFlags) {
294         mTransportControlFlags = transportControlFlags;
295         setSeekBarsEnabled(
296                 (transportControlFlags & RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE) != 0);
297     }
298 
setSeekBarsEnabled(boolean enabled)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 
setTransportControlCallback(KeyguardHostView.TransportControlCallback transportControlCallback)310     public void setTransportControlCallback(KeyguardHostView.TransportControlCallback
311             transportControlCallback) {
312         mTransportControlCallback = transportControlCallback;
313     }
314 
setEnableMarquee(boolean enabled)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
onFinishInflate()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
onAttachedToWindow()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
onConfigurationChanged(Configuration newConfig)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
onDetachedFromWindow()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
onSaveInstanceState()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
onRestoreInstanceState(Parcelable state)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 
setBadgeIcon(Drawable bmp)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 
clear()425         public void clear() {
426             artist = null;
427             trackTitle = null;
428             albumTitle = null;
429             bitmap = null;
430             duration = -1;
431         }
432 
toString()433         public String toString() {
434             return "Metadata[artist=" + artist + " trackTitle=" + trackTitle +
435                     " albumTitle=" + albumTitle + " duration=" + duration + "]";
436         }
437     }
438 
clearMetadata()439     void clearMetadata() {
440         mPopulateMetadataWhenAttached = null;
441         mMetadata.clear();
442         populateMetadata();
443     }
444 
updateMetadata(RemoteController.MetadataEditor data)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      */
populateMetadata()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 
updateSeekDisplay()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 
tryToggleSeekBar()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 
resetToMetadata()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 
delayResetToMetadata()579     void delayResetToMetadata() {
580         removeCallbacks(mResetToMetadata);
581         postDelayed(mResetToMetadata, RESET_TO_METADATA_DELAY);
582     }
583 
cancelResetToMetadata()584     void cancelResetToMetadata() {
585         removeCallbacks(mResetToMetadata);
586     }
587 
setSeekBarDuration(long duration)588     void setSeekBarDuration(long duration) {
589         mTransientSeekBar.setMax((int) duration);
590     }
591 
scrubTo(int progress)592     void scrubTo(int progress) {
593         mRemoteController.seekTo(progress);
594         mTransportControlCallback.userActivity();
595     }
596 
setVisibilityBasedOnFlag(View view, int flags, int flag)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 
updatePlayPauseState(int state)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 
updateSeekBars()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 
SavedState(Parcelable superState)667         SavedState(Parcelable superState) {
668             super(superState);
669         }
670 
SavedState(Parcel in)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
writeToParcel(Parcel out, int flags)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 
sendMediaButtonClick(int keyCode)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 
providesClock()713     public boolean providesClock() {
714         return false;
715     }
716 }
717