KeyguardTransportControlView.java revision 5ecd81154fa039961f65bb4e36d18ac555b0d1d6
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.app.PendingIntent;
20import android.app.PendingIntent.CanceledException;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.media.AudioManager;
25import android.media.IRemoteControlDisplay;
26import android.media.MediaMetadataRetriever;
27import android.media.RemoteControlClient;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.Message;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.os.RemoteException;
34import android.os.SystemClock;
35import android.text.Spannable;
36import android.text.TextUtils;
37import android.text.style.ForegroundColorSpan;
38import android.util.AttributeSet;
39import android.util.Log;
40import android.view.KeyEvent;
41import android.view.View;
42import android.view.View.OnClickListener;
43import android.widget.FrameLayout;
44import android.widget.ImageView;
45import android.widget.TextView;
46
47import java.lang.ref.WeakReference;
48/**
49 * This is the widget responsible for showing music controls in keyguard.
50 */
51public class KeyguardTransportControlView extends FrameLayout implements OnClickListener {
52
53    private static final int MSG_UPDATE_STATE = 100;
54    private static final int MSG_SET_METADATA = 101;
55    private static final int MSG_SET_TRANSPORT_CONTROLS = 102;
56    private static final int MSG_SET_ARTWORK = 103;
57    private static final int MSG_SET_GENERATION_ID = 104;
58    private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s
59    protected static final boolean DEBUG = false;
60    protected static final String TAG = "TransportControlView";
61
62    private ImageView mAlbumArt;
63    private TextView mTrackTitle;
64    private ImageView mBtnPrev;
65    private ImageView mBtnPlay;
66    private ImageView mBtnNext;
67    private int mClientGeneration;
68    private Metadata mMetadata = new Metadata();
69    private boolean mAttached;
70    private PendingIntent mClientIntent;
71    private int mTransportControlFlags;
72    private int mCurrentPlayState;
73    private AudioManager mAudioManager;
74    private IRemoteControlDisplayWeak mIRCD;
75    private boolean mMusicClientPresent = true;
76
77    /**
78     * The metadata which should be populated into the view once we've been attached
79     */
80    private Bundle mPopulateMetadataWhenAttached = null;
81
82    // This handler is required to ensure messages from IRCD are handled in sequence and on
83    // the UI thread.
84    private Handler mHandler = new Handler() {
85        @Override
86        public void handleMessage(Message msg) {
87            switch (msg.what) {
88            case MSG_UPDATE_STATE:
89                if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2);
90                break;
91
92            case MSG_SET_METADATA:
93                if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj);
94                break;
95
96            case MSG_SET_TRANSPORT_CONTROLS:
97                if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2);
98                break;
99
100            case MSG_SET_ARTWORK:
101                if (mClientGeneration == msg.arg1) {
102                    if (mMetadata.bitmap != null) {
103                        mMetadata.bitmap.recycle();
104                    }
105                    mMetadata.bitmap = (Bitmap) msg.obj;
106                    mAlbumArt.setImageBitmap(mMetadata.bitmap);
107                }
108                break;
109
110            case MSG_SET_GENERATION_ID:
111                if (msg.arg2 != 0) {
112                    // This means nobody is currently registered. Hide the view.
113                    onListenerDetached();
114                } else {
115                    onListenerAttached();
116                }
117                if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2);
118                mClientGeneration = msg.arg1;
119                mClientIntent = (PendingIntent) msg.obj;
120                break;
121
122            }
123        }
124    };
125    private KeyguardHostView.TransportCallback mTransportCallback;
126
127    /**
128     * This class is required to have weak linkage to the current TransportControlView
129     * because the remote process can hold a strong reference to this binder object and
130     * we can't predict when it will be GC'd in the remote process. Without this code, it
131     * would allow a heavyweight object to be held on this side of the binder when there's
132     * no requirement to run a GC on the other side.
133     */
134    private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub {
135        private WeakReference<Handler> mLocalHandler;
136
137        IRemoteControlDisplayWeak(Handler handler) {
138            mLocalHandler = new WeakReference<Handler>(handler);
139        }
140
141        public void setPlaybackState(int generationId, int state, long stateChangeTimeMs) {
142            Handler handler = mLocalHandler.get();
143            if (handler != null) {
144                handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
145            }
146        }
147
148        public void setMetadata(int generationId, Bundle metadata) {
149            Handler handler = mLocalHandler.get();
150            if (handler != null) {
151                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
152            }
153        }
154
155        public void setTransportControlFlags(int generationId, int flags) {
156            Handler handler = mLocalHandler.get();
157            if (handler != null) {
158                handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags)
159                        .sendToTarget();
160            }
161        }
162
163        public void setArtwork(int generationId, Bitmap bitmap) {
164            Handler handler = mLocalHandler.get();
165            if (handler != null) {
166                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
167            }
168        }
169
170        public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) {
171            Handler handler = mLocalHandler.get();
172            if (handler != null) {
173                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
174                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
175            }
176        }
177
178        public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent,
179                boolean clearing) throws RemoteException {
180            Handler handler = mLocalHandler.get();
181            if (handler != null) {
182                handler.obtainMessage(MSG_SET_GENERATION_ID,
183                    clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget();
184            }
185        }
186    };
187
188    public KeyguardTransportControlView(Context context, AttributeSet attrs) {
189        super(context, attrs);
190        if (DEBUG) Log.v(TAG, "Create TCV " + this);
191        mAudioManager = new AudioManager(mContext);
192        mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback
193        mIRCD = new IRemoteControlDisplayWeak(mHandler);
194    }
195
196    protected void onListenerDetached() {
197        mMusicClientPresent = false;
198        if (DEBUG) Log.v(TAG, "onListenerDetached()");
199        if (mTransportCallback != null) {
200            mTransportCallback.onListenerDetached();
201        } else {
202            Log.w(TAG, "onListenerDetached: no callback");
203        }
204    }
205
206    private void onListenerAttached() {
207        mMusicClientPresent = true;
208        if (DEBUG) Log.v(TAG, "onListenerAttached()");
209        if (mTransportCallback != null) {
210            mTransportCallback.onListenerAttached();
211        } else {
212            Log.w(TAG, "onListenerAttached(): no callback");
213        }
214    }
215
216    private void updateTransportControls(int transportControlFlags) {
217        mTransportControlFlags = transportControlFlags;
218    }
219
220    @Override
221    public void onFinishInflate() {
222        super.onFinishInflate();
223        mTrackTitle = (TextView) findViewById(R.id.title);
224        mTrackTitle.setSelected(true); // enable marquee
225        mAlbumArt = (ImageView) findViewById(R.id.albumart);
226        mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
227        mBtnPlay = (ImageView) findViewById(R.id.btn_play);
228        mBtnNext = (ImageView) findViewById(R.id.btn_next);
229        final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
230        for (View view : buttons) {
231            view.setOnClickListener(this);
232        }
233    }
234
235    @Override
236    public void onAttachedToWindow() {
237        super.onAttachedToWindow();
238        if (DEBUG) Log.v(TAG, "onAttachToWindow()");
239        if (mPopulateMetadataWhenAttached != null) {
240            updateMetadata(mPopulateMetadataWhenAttached);
241            mPopulateMetadataWhenAttached = null;
242        }
243        if (!mAttached) {
244            if (DEBUG) Log.v(TAG, "Registering TCV " + this);
245            mAudioManager.registerRemoteControlDisplay(mIRCD);
246        }
247        mAttached = true;
248    }
249
250    @Override
251    public void onDetachedFromWindow() {
252        if (DEBUG) Log.v(TAG, "onDetachFromWindow()");
253        super.onDetachedFromWindow();
254        if (mAttached) {
255            if (DEBUG) Log.v(TAG, "Unregistering TCV " + this);
256            mAudioManager.unregisterRemoteControlDisplay(mIRCD);
257        }
258        mAttached = false;
259    }
260
261    class Metadata {
262        private String artist;
263        private String trackTitle;
264        private String albumTitle;
265        private Bitmap bitmap;
266
267        public String toString() {
268            return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]";
269        }
270    }
271
272    private String getMdString(Bundle data, int id) {
273        return data.getString(Integer.toString(id));
274    }
275
276    private void updateMetadata(Bundle data) {
277        if (mAttached) {
278            mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
279            mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE);
280            mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM);
281            populateMetadata();
282        } else {
283            mPopulateMetadataWhenAttached = data;
284        }
285    }
286
287    /**
288     * Populates the given metadata into the view
289     */
290    private void populateMetadata() {
291        StringBuilder sb = new StringBuilder();
292        int trackTitleLength = 0;
293        if (!TextUtils.isEmpty(mMetadata.trackTitle)) {
294            sb.append(mMetadata.trackTitle);
295            trackTitleLength = mMetadata.trackTitle.length();
296        }
297        if (!TextUtils.isEmpty(mMetadata.artist)) {
298            if (sb.length() != 0) {
299                sb.append(" - ");
300            }
301            sb.append(mMetadata.artist);
302        }
303        if (!TextUtils.isEmpty(mMetadata.albumTitle)) {
304            if (sb.length() != 0) {
305                sb.append(" - ");
306            }
307            sb.append(mMetadata.albumTitle);
308        }
309        mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE);
310        Spannable str = (Spannable) mTrackTitle.getText();
311        if (trackTitleLength != 0) {
312            str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength,
313                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
314            trackTitleLength++;
315        }
316        if (sb.length() > trackTitleLength) {
317            str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(),
318                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
319        }
320
321        mAlbumArt.setImageBitmap(mMetadata.bitmap);
322        final int flags = mTransportControlFlags;
323        setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
324        setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
325        setVisibilityBasedOnFlag(mBtnPlay, flags,
326                RemoteControlClient.FLAG_KEY_MEDIA_PLAY
327                | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
328                | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
329                | RemoteControlClient.FLAG_KEY_MEDIA_STOP);
330
331        updatePlayPauseState(mCurrentPlayState);
332    }
333
334    public boolean isMusicPlaying() {
335       return mCurrentPlayState == RemoteControlClient.PLAYSTATE_PLAYING
336               || mCurrentPlayState == RemoteControlClient.PLAYSTATE_BUFFERING;
337    }
338
339    private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
340        if ((flags & flag) != 0) {
341            view.setVisibility(View.VISIBLE);
342        } else {
343            view.setVisibility(View.GONE);
344        }
345    }
346
347    private void updatePlayPauseState(int state) {
348        if (DEBUG) Log.v(TAG,
349                "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state);
350        if (state == mCurrentPlayState) {
351            return;
352        }
353        final int imageResId;
354        final int imageDescId;
355        switch (state) {
356            case RemoteControlClient.PLAYSTATE_ERROR:
357                imageResId = R.drawable.stat_sys_warning;
358                // TODO use more specific image description string for warning, but here the "play"
359                //      message is still valid because this button triggers a play command.
360                imageDescId = R.string.keyguard_transport_play_description;
361                break;
362
363            case RemoteControlClient.PLAYSTATE_PLAYING:
364                imageResId = R.drawable.ic_media_pause;
365                imageDescId = R.string.keyguard_transport_pause_description;
366                break;
367
368            case RemoteControlClient.PLAYSTATE_BUFFERING:
369                imageResId = R.drawable.ic_media_stop;
370                imageDescId = R.string.keyguard_transport_stop_description;
371                break;
372
373            case RemoteControlClient.PLAYSTATE_PAUSED:
374            default:
375                imageResId = R.drawable.ic_media_play;
376                imageDescId = R.string.keyguard_transport_play_description;
377                break;
378        }
379        mBtnPlay.setImageResource(imageResId);
380        mBtnPlay.setContentDescription(getResources().getString(imageDescId));
381        mCurrentPlayState = state;
382        mTransportCallback.onPlayStateChanged();
383    }
384
385    static class SavedState extends BaseSavedState {
386        boolean clientPresent;
387
388        SavedState(Parcelable superState) {
389            super(superState);
390        }
391
392        private SavedState(Parcel in) {
393            super(in);
394            this.clientPresent = in.readInt() != 0;
395        }
396
397        @Override
398        public void writeToParcel(Parcel out, int flags) {
399            super.writeToParcel(out, flags);
400            out.writeInt(this.clientPresent ? 1 : 0);
401        }
402
403        public static final Parcelable.Creator<SavedState> CREATOR
404                = new Parcelable.Creator<SavedState>() {
405            public SavedState createFromParcel(Parcel in) {
406                return new SavedState(in);
407            }
408
409            public SavedState[] newArray(int size) {
410                return new SavedState[size];
411            }
412        };
413    }
414
415    @Override
416    public Parcelable onSaveInstanceState() {
417        Parcelable superState = super.onSaveInstanceState();
418        SavedState ss = new SavedState(superState);
419        ss.clientPresent = mMusicClientPresent;
420        return ss;
421    }
422
423    @Override
424    public void onRestoreInstanceState(Parcelable state) {
425        if (!(state instanceof SavedState)) {
426            super.onRestoreInstanceState(state);
427            return;
428        }
429        SavedState ss = (SavedState) state;
430        super.onRestoreInstanceState(ss.getSuperState());
431        if (ss.clientPresent) {
432            if (DEBUG) Log.v(TAG, "Reattaching client because it was attached");
433            onListenerAttached();
434        }
435    }
436
437    public void onClick(View v) {
438        int keyCode = -1;
439        if (v == mBtnPrev) {
440            keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
441        } else if (v == mBtnNext) {
442            keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
443        } else if (v == mBtnPlay) {
444            keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
445
446        }
447        if (keyCode != -1) {
448            sendMediaButtonClick(keyCode);
449        }
450    }
451
452    private void sendMediaButtonClick(int keyCode) {
453        if (mClientIntent == null) {
454            // Shouldn't be possible because this view should be hidden in this case.
455            Log.e(TAG, "sendMediaButtonClick(): No client is currently registered");
456            return;
457        }
458        // use the registered PendingIntent that will be processed by the registered
459        //    media button event receiver, which is the component of mClientIntent
460        KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
461        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
462        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
463        try {
464            mClientIntent.send(getContext(), 0, intent);
465        } catch (CanceledException e) {
466            Log.e(TAG, "Error sending intent for media button down: "+e);
467            e.printStackTrace();
468        }
469
470        keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
471        intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
472        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
473        try {
474            mClientIntent.send(getContext(), 0, intent);
475        } catch (CanceledException e) {
476            Log.e(TAG, "Error sending intent for media button up: "+e);
477            e.printStackTrace();
478        }
479    }
480
481    public boolean providesClock() {
482        return false;
483    }
484
485    private boolean wasPlayingRecently(int state, long stateChangeTimeMs) {
486        switch (state) {
487            case RemoteControlClient.PLAYSTATE_PLAYING:
488            case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
489            case RemoteControlClient.PLAYSTATE_REWINDING:
490            case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
491            case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
492            case RemoteControlClient.PLAYSTATE_BUFFERING:
493                // actively playing or about to play
494                return true;
495            case RemoteControlClient.PLAYSTATE_NONE:
496                return false;
497            case RemoteControlClient.PLAYSTATE_STOPPED:
498            case RemoteControlClient.PLAYSTATE_PAUSED:
499            case RemoteControlClient.PLAYSTATE_ERROR:
500                // we have stopped playing, check how long ago
501                if (DEBUG) {
502                    if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) {
503                        Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently");
504                    } else {
505                        Log.v(TAG, "wasPlayingRecently: time > TIMEOUT");
506                    }
507                }
508                return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS);
509            default:
510                Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()");
511                return false;
512        }
513    }
514
515    public void setKeyguardCallback(KeyguardHostView.TransportCallback transportCallback) {
516        mTransportCallback = transportCallback;
517    }
518}
519