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