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