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