KeyguardTransportControlView.java revision f8895248e2ac4dbb46622f3e04c7256f03175b4f
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.drawable.Drawable;
26import android.media.AudioManager;
27import android.media.MediaMetadataEditor;
28import android.media.MediaMetadataRetriever;
29import android.media.RemoteControlClient;
30import android.media.RemoteController;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.os.SystemClock;
34import android.text.TextUtils;
35import android.text.format.DateFormat;
36import android.transition.ChangeBounds;
37import android.transition.ChangeText;
38import android.transition.Fade;
39import android.transition.TransitionManager;
40import android.transition.TransitionSet;
41import android.util.AttributeSet;
42import android.util.DisplayMetrics;
43import android.util.Log;
44import android.view.KeyEvent;
45import android.view.View;
46import android.view.ViewGroup;
47import android.widget.FrameLayout;
48import android.widget.ImageView;
49import android.widget.SeekBar;
50import android.widget.TextView;
51
52import java.text.SimpleDateFormat;
53import java.util.Date;
54import java.util.TimeZone;
55
56/**
57 * This is the widget responsible for showing music controls in keyguard.
58 */
59public class KeyguardTransportControlView extends FrameLayout {
60
61    private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s
62    private static final int RESET_TO_METADATA_DELAY = 5000;
63    protected static final boolean DEBUG = false;
64    protected static final String TAG = "TransportControlView";
65
66    private static final boolean ANIMATE_TRANSITIONS = false;
67
68    private ViewGroup mMetadataContainer;
69    private ViewGroup mInfoContainer;
70    private TextView mTrackTitle;
71    private TextView mTrackArtistAlbum;
72
73    private View mTransientSeek;
74    private SeekBar mTransientSeekBar;
75    private TextView mTransientSeekTimeElapsed;
76    private TextView mTransientSeekTimeRemaining;
77
78    private ImageView mBtnPrev;
79    private ImageView mBtnPlay;
80    private ImageView mBtnNext;
81    private Metadata mMetadata = new Metadata();
82    private int mTransportControlFlags;
83    private int mCurrentPlayState;
84    private AudioManager mAudioManager;
85    private RemoteController mRemoteController;
86
87    private ImageView mBadge;
88
89    private boolean mSeekEnabled;
90    private boolean mUserSeeking;
91    private java.text.DateFormat mFormat;
92
93    /**
94     * The metadata which should be populated into the view once we've been attached
95     */
96    private RemoteController.MetadataEditor mPopulateMetadataWhenAttached = null;
97
98    private RemoteController.OnClientUpdateListener mRCClientUpdateListener =
99            new RemoteController.OnClientUpdateListener() {
100        @Override
101        public void onClientChange(boolean clearing) {
102            if (clearing) {
103                clearMetadata();
104            }
105        }
106
107        @Override
108        public void onClientPlaybackStateUpdate(int state) {
109            setSeekBarsEnabled(false);
110            updatePlayPauseState(state);
111        }
112
113        @Override
114        public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
115                long currentPosMs, float speed) {
116            setSeekBarsEnabled(mMetadata != null && mMetadata.duration > 0);
117            updatePlayPauseState(state);
118            if (DEBUG) Log.d(TAG, "onClientPlaybackStateUpdate(state=" + state +
119                    ", stateChangeTimeMs=" + stateChangeTimeMs + ", currentPosMs=" + currentPosMs +
120                    ", speed=" + speed + ")");
121        }
122
123        @Override
124        public void onClientTransportControlUpdate(int transportControlFlags) {
125            updateTransportControls(transportControlFlags);
126        }
127
128        @Override
129        public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) {
130            updateMetadata(metadataEditor);
131        }
132    };
133
134    private final Runnable mUpdateSeekBars = new Runnable() {
135        public void run() {
136            if (updateSeekBars()) {
137                postDelayed(this, 1000);
138            }
139        }
140    };
141
142    private final Runnable mResetToMetadata = new Runnable() {
143        public void run() {
144            resetToMetadata();
145        }
146    };
147
148    private final OnClickListener mTransportCommandListener = new OnClickListener() {
149        public void onClick(View v) {
150            int keyCode = -1;
151            if (v == mBtnPrev) {
152                keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
153            } else if (v == mBtnNext) {
154                keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
155            } else if (v == mBtnPlay) {
156                keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
157            }
158            if (keyCode != -1) {
159                sendMediaButtonClick(keyCode);
160            }
161        }
162    };
163
164    private final OnLongClickListener mTransportShowSeekBarListener = new OnLongClickListener() {
165        @Override
166        public boolean onLongClick(View v) {
167            if (mSeekEnabled) {
168                return tryToggleSeekBar();
169            }
170            return false;
171        }
172    };
173
174    private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener =
175            new SeekBar.OnSeekBarChangeListener() {
176        @Override
177        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
178            if (fromUser) {
179                scrubTo(progress);
180                delayResetToMetadata();
181            }
182            updateSeekDisplay();
183        }
184
185        @Override
186        public void onStartTrackingTouch(SeekBar seekBar) {
187            mUserSeeking = true;
188        }
189
190        @Override
191        public void onStopTrackingTouch(SeekBar seekBar) {
192            mUserSeeking = false;
193        }
194    };
195
196    private static final int TRANSITION_DURATION = 200;
197    private final TransitionSet mMetadataChangeTransition;
198
199    KeyguardHostView.TransportControlCallback mTransportControlCallback;
200
201    public KeyguardTransportControlView(Context context, AttributeSet attrs) {
202        super(context, attrs);
203        if (DEBUG) Log.v(TAG, "Create TCV " + this);
204        mAudioManager = new AudioManager(mContext);
205        mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback
206        mRemoteController = new RemoteController(context);
207        mRemoteController.setOnClientUpdateListener(mRCClientUpdateListener);
208
209        final DisplayMetrics dm = context.getResources().getDisplayMetrics();
210        final int dim = Math.max(dm.widthPixels, dm.heightPixels);
211        mRemoteController.setArtworkConfiguration(true, dim, dim);
212
213        final ChangeText tc = new ChangeText();
214        tc.setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN);
215        final TransitionSet inner = new TransitionSet();
216        inner.addTransition(tc).addTransition(new ChangeBounds());
217        final TransitionSet tg = new TransitionSet();
218        tg.addTransition(new Fade(Fade.OUT)).addTransition(inner).
219                addTransition(new Fade(Fade.IN));
220        tg.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
221        tg.setDuration(TRANSITION_DURATION);
222        mMetadataChangeTransition = tg;
223    }
224
225    private void updateTransportControls(int transportControlFlags) {
226        mTransportControlFlags = transportControlFlags;
227        setSeekBarsEnabled(
228                (transportControlFlags & RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE) != 0);
229    }
230
231    void setSeekBarsEnabled(boolean enabled) {
232        if (enabled == mSeekEnabled) return;
233
234        mSeekEnabled = enabled;
235        if (mTransientSeek.getVisibility() == VISIBLE) {
236            mTransientSeek.setVisibility(INVISIBLE);
237            mMetadataContainer.setVisibility(VISIBLE);
238            mUserSeeking = false;
239            cancelResetToMetadata();
240        }
241        if (enabled) {
242            mUpdateSeekBars.run();
243            postDelayed(mUpdateSeekBars, 1000);
244        } else {
245            removeCallbacks(mUpdateSeekBars);
246        }
247    }
248
249    public void setTransportControlCallback(KeyguardHostView.TransportControlCallback
250            transportControlCallback) {
251        mTransportControlCallback = transportControlCallback;
252    }
253
254    @Override
255    public void onFinishInflate() {
256        super.onFinishInflate();
257        mInfoContainer = (ViewGroup) findViewById(R.id.info_container);
258        mMetadataContainer = (ViewGroup) findViewById(R.id.metadata_container);
259        mBadge = (ImageView) findViewById(R.id.badge);
260        mTrackTitle = (TextView) findViewById(R.id.title);
261        mTrackTitle.setSelected(true); // enable marquee
262        mTrackArtistAlbum = (TextView) findViewById(R.id.artist_album);
263        mTrackArtistAlbum.setSelected(true);
264        mTransientSeek = findViewById(R.id.transient_seek);
265        mTransientSeekBar = (SeekBar) findViewById(R.id.transient_seek_bar);
266        mTransientSeekBar.setOnSeekBarChangeListener(mOnSeekBarChangeListener);
267        mTransientSeekTimeElapsed = (TextView) findViewById(R.id.transient_seek_time_elapsed);
268        mTransientSeekTimeRemaining = (TextView) findViewById(R.id.transient_seek_time_remaining);
269        mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
270        mBtnPlay = (ImageView) findViewById(R.id.btn_play);
271        mBtnNext = (ImageView) findViewById(R.id.btn_next);
272        final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
273        for (View view : buttons) {
274            view.setOnClickListener(mTransportCommandListener);
275            view.setOnLongClickListener(mTransportShowSeekBarListener);
276        }
277    }
278
279    @Override
280    public void onAttachedToWindow() {
281        super.onAttachedToWindow();
282        if (DEBUG) Log.v(TAG, "onAttachToWindow()");
283        if (mPopulateMetadataWhenAttached != null) {
284            updateMetadata(mPopulateMetadataWhenAttached);
285            mPopulateMetadataWhenAttached = null;
286        }
287        if (DEBUG) Log.v(TAG, "Registering TCV " + this);
288        mAudioManager.registerRemoteController(mRemoteController);
289    }
290
291    @Override
292    protected void onConfigurationChanged(Configuration newConfig) {
293        super.onConfigurationChanged(newConfig);
294        final DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
295        final int dim = Math.max(dm.widthPixels, dm.heightPixels);
296        mRemoteController.setArtworkConfiguration(true, dim, dim);
297    }
298
299    @Override
300    public void onDetachedFromWindow() {
301        if (DEBUG) Log.v(TAG, "onDetachFromWindow()");
302        super.onDetachedFromWindow();
303        if (DEBUG) Log.v(TAG, "Unregistering TCV " + this);
304        mAudioManager.unregisterRemoteController(mRemoteController);
305        mUserSeeking = false;
306    }
307
308    void setBadgeIcon(Drawable bmp) {
309        mBadge.setImageDrawable(bmp);
310
311        final ColorMatrix cm = new ColorMatrix();
312        cm.setSaturation(0);
313        mBadge.setColorFilter(new ColorMatrixColorFilter(cm));
314        mBadge.setImageAlpha(0xef);
315    }
316
317    class Metadata {
318        private String artist;
319        private String trackTitle;
320        private String albumTitle;
321        private Bitmap bitmap;
322        private long duration;
323
324        public void clear() {
325            artist = null;
326            trackTitle = null;
327            albumTitle = null;
328            bitmap = null;
329            duration = -1;
330        }
331
332        public String toString() {
333            return "Metadata[artist=" + artist + " trackTitle=" + trackTitle +
334                    " albumTitle=" + albumTitle + " duration=" + duration + "]";
335        }
336    }
337
338    void clearMetadata() {
339        mPopulateMetadataWhenAttached = null;
340        mMetadata.clear();
341        populateMetadata();
342    }
343
344    void updateMetadata(RemoteController.MetadataEditor data) {
345        if (isAttachedToWindow()) {
346            mMetadata.artist = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
347                    mMetadata.artist);
348            mMetadata.trackTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_TITLE,
349                    mMetadata.trackTitle);
350            mMetadata.albumTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUM,
351                    mMetadata.albumTitle);
352            mMetadata.duration = data.getLong(MediaMetadataRetriever.METADATA_KEY_DURATION, -1);
353            mMetadata.bitmap = data.getBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK,
354                    mMetadata.bitmap);
355            populateMetadata();
356        } else {
357            mPopulateMetadataWhenAttached = data;
358        }
359    }
360
361    /**
362     * Populates the given metadata into the view
363     */
364    private void populateMetadata() {
365        if (ANIMATE_TRANSITIONS && isLaidOut() && mMetadataContainer.getVisibility() == VISIBLE) {
366            TransitionManager.beginDelayedTransition(mMetadataContainer, mMetadataChangeTransition);
367        }
368
369        final String remoteClientPackage = mRemoteController.getRemoteControlClientPackageName();
370        Drawable badgeIcon = null;
371        try {
372            badgeIcon = getContext().getPackageManager().getApplicationIcon(remoteClientPackage);
373        } catch (PackageManager.NameNotFoundException e) {
374            Log.e(TAG, "Couldn't get remote control client package icon", e);
375        }
376        setBadgeIcon(badgeIcon);
377        if (!TextUtils.isEmpty(mMetadata.trackTitle)) {
378            mTrackTitle.setText(mMetadata.trackTitle);
379        }
380        StringBuilder sb = new StringBuilder();
381        if (!TextUtils.isEmpty(mMetadata.artist)) {
382            if (sb.length() != 0) {
383                sb.append(" - ");
384            }
385            sb.append(mMetadata.artist);
386        }
387        if (!TextUtils.isEmpty(mMetadata.albumTitle)) {
388            if (sb.length() != 0) {
389                sb.append(" - ");
390            }
391            sb.append(mMetadata.albumTitle);
392        }
393        mTrackArtistAlbum.setText(sb.toString());
394
395        if (mMetadata.duration >= 0) {
396            setSeekBarsEnabled(true);
397            setSeekBarDuration(mMetadata.duration);
398
399            final String skeleton;
400
401            if (mMetadata.duration >= 86400000) {
402                skeleton = "DDD kk mm ss";
403            } else if (mMetadata.duration >= 3600000) {
404                skeleton = "kk mm ss";
405            } else {
406                skeleton = "mm ss";
407            }
408            mFormat = new SimpleDateFormat(DateFormat.getBestDateTimePattern(
409                    getContext().getResources().getConfiguration().locale,
410                    skeleton));
411            mFormat.setTimeZone(TimeZone.getTimeZone("GMT+0"));
412        } else {
413            setSeekBarsEnabled(false);
414        }
415
416        KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground(
417                mMetadata.bitmap);
418        final int flags = mTransportControlFlags;
419        setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
420        setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
421        setVisibilityBasedOnFlag(mBtnPlay, flags,
422                RemoteControlClient.FLAG_KEY_MEDIA_PLAY
423                | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
424                | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
425                | RemoteControlClient.FLAG_KEY_MEDIA_STOP);
426
427        updatePlayPauseState(mCurrentPlayState);
428    }
429
430    void updateSeekDisplay() {
431        if (mMetadata != null && mRemoteController != null && mFormat != null) {
432            final long timeElapsed = mRemoteController.getEstimatedMediaPosition();
433            final long duration = mMetadata.duration;
434            final long remaining = duration - timeElapsed;
435
436            mTransientSeekTimeElapsed.setText(mFormat.format(new Date(timeElapsed)));
437            mTransientSeekTimeRemaining.setText(mFormat.format(new Date(remaining)));
438
439            if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + timeElapsed +
440                    " duration=" + duration + " remaining=" + remaining);
441        }
442    }
443
444    boolean tryToggleSeekBar() {
445        if (ANIMATE_TRANSITIONS) {
446            TransitionManager.beginDelayedTransition(mInfoContainer);
447        }
448        if (mTransientSeek.getVisibility() == VISIBLE) {
449            mTransientSeek.setVisibility(INVISIBLE);
450            mMetadataContainer.setVisibility(VISIBLE);
451            cancelResetToMetadata();
452        } else {
453            mTransientSeek.setVisibility(VISIBLE);
454            mMetadataContainer.setVisibility(INVISIBLE);
455            delayResetToMetadata();
456        }
457        mTransportControlCallback.userActivity();
458        return true;
459    }
460
461    void resetToMetadata() {
462        if (ANIMATE_TRANSITIONS) {
463            TransitionManager.beginDelayedTransition(mInfoContainer);
464        }
465        if (mTransientSeek.getVisibility() == VISIBLE) {
466            mTransientSeek.setVisibility(INVISIBLE);
467            mMetadataContainer.setVisibility(VISIBLE);
468        }
469        // TODO Also hide ratings, if applicable
470    }
471
472    void delayResetToMetadata() {
473        removeCallbacks(mResetToMetadata);
474        postDelayed(mResetToMetadata, RESET_TO_METADATA_DELAY);
475    }
476
477    void cancelResetToMetadata() {
478        removeCallbacks(mResetToMetadata);
479    }
480
481    void setSeekBarDuration(long duration) {
482        mTransientSeekBar.setMax((int) duration);
483    }
484
485    void scrubTo(int progress) {
486        mRemoteController.seekTo(progress);
487        mTransportControlCallback.userActivity();
488    }
489
490    private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
491        if ((flags & flag) != 0) {
492            view.setVisibility(View.VISIBLE);
493        } else {
494            view.setVisibility(View.GONE);
495        }
496    }
497
498    private void updatePlayPauseState(int state) {
499        if (DEBUG) Log.v(TAG,
500                "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state);
501        if (state == mCurrentPlayState) {
502            return;
503        }
504        final int imageResId;
505        final int imageDescId;
506        switch (state) {
507            case RemoteControlClient.PLAYSTATE_ERROR:
508                imageResId = R.drawable.stat_sys_warning;
509                // TODO use more specific image description string for warning, but here the "play"
510                //      message is still valid because this button triggers a play command.
511                imageDescId = R.string.keyguard_transport_play_description;
512                break;
513
514            case RemoteControlClient.PLAYSTATE_PLAYING:
515                imageResId = R.drawable.ic_media_pause;
516                imageDescId = R.string.keyguard_transport_pause_description;
517                if (mSeekEnabled) {
518                    postDelayed(mUpdateSeekBars, 1000);
519                }
520                break;
521
522            case RemoteControlClient.PLAYSTATE_BUFFERING:
523                imageResId = R.drawable.ic_media_stop;
524                imageDescId = R.string.keyguard_transport_stop_description;
525                break;
526
527            case RemoteControlClient.PLAYSTATE_PAUSED:
528            default:
529                imageResId = R.drawable.ic_media_play;
530                imageDescId = R.string.keyguard_transport_play_description;
531                break;
532        }
533
534        if (state != RemoteControlClient.PLAYSTATE_PLAYING) {
535            removeCallbacks(mUpdateSeekBars);
536            updateSeekBars();
537        }
538        mBtnPlay.setImageResource(imageResId);
539        mBtnPlay.setContentDescription(getResources().getString(imageDescId));
540        mCurrentPlayState = state;
541    }
542
543    boolean updateSeekBars() {
544        final int position = (int) mRemoteController.getEstimatedMediaPosition();
545        if (position >= 0) {
546            if (!mUserSeeking) {
547                mTransientSeekBar.setProgress(position);
548            }
549            return true;
550        }
551        Log.w(TAG, "Updating seek bars; received invalid estimated media position (" +
552                position + "). Disabling seek.");
553        setSeekBarsEnabled(false);
554        return false;
555    }
556
557    static class SavedState extends BaseSavedState {
558        boolean clientPresent;
559
560        SavedState(Parcelable superState) {
561            super(superState);
562        }
563
564        private SavedState(Parcel in) {
565            super(in);
566            this.clientPresent = in.readInt() != 0;
567        }
568
569        @Override
570        public void writeToParcel(Parcel out, int flags) {
571            super.writeToParcel(out, flags);
572            out.writeInt(this.clientPresent ? 1 : 0);
573        }
574
575        public static final Parcelable.Creator<SavedState> CREATOR
576                = new Parcelable.Creator<SavedState>() {
577            public SavedState createFromParcel(Parcel in) {
578                return new SavedState(in);
579            }
580
581            public SavedState[] newArray(int size) {
582                return new SavedState[size];
583            }
584        };
585    }
586
587    private void sendMediaButtonClick(int keyCode) {
588        // TODO We should think about sending these up/down events accurately with touch up/down
589        // on the buttons, but in the near term this will interfere with the long press behavior.
590        mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
591        mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
592
593        mTransportControlCallback.userActivity();
594    }
595
596    public boolean providesClock() {
597        return false;
598    }
599
600    private boolean wasPlayingRecently(int state, long stateChangeTimeMs) {
601        switch (state) {
602            case RemoteControlClient.PLAYSTATE_PLAYING:
603            case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
604            case RemoteControlClient.PLAYSTATE_REWINDING:
605            case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
606            case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
607            case RemoteControlClient.PLAYSTATE_BUFFERING:
608                // actively playing or about to play
609                return true;
610            case RemoteControlClient.PLAYSTATE_NONE:
611                return false;
612            case RemoteControlClient.PLAYSTATE_STOPPED:
613            case RemoteControlClient.PLAYSTATE_PAUSED:
614            case RemoteControlClient.PLAYSTATE_ERROR:
615                // we have stopped playing, check how long ago
616                if (DEBUG) {
617                    if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) {
618                        Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently");
619                    } else {
620                        Log.v(TAG, "wasPlayingRecently: time > TIMEOUT");
621                    }
622                }
623                return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS);
624            default:
625                Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()");
626                return false;
627        }
628    }
629}
630