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            Handler handler = mLocalHandler.get();
148            if (handler != null) {
149                handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
150            }
151        }
152
153        public void setMetadata(int generationId, Bundle metadata) {
154            Handler handler = mLocalHandler.get();
155            if (handler != null) {
156                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
157            }
158        }
159
160        public void setTransportControlFlags(int generationId, int flags) {
161            Handler handler = mLocalHandler.get();
162            if (handler != null) {
163                handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags)
164                        .sendToTarget();
165            }
166        }
167
168        public void setArtwork(int generationId, Bitmap bitmap) {
169            Handler handler = mLocalHandler.get();
170            if (handler != null) {
171                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
172            }
173        }
174
175        public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) {
176            Handler handler = mLocalHandler.get();
177            if (handler != null) {
178                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
179                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
180            }
181        }
182
183        public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent,
184                boolean clearing) throws RemoteException {
185            Handler handler = mLocalHandler.get();
186            if (handler != null) {
187                handler.obtainMessage(MSG_SET_GENERATION_ID,
188                    clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget();
189            }
190        }
191    };
192
193    public TransportControlView(Context context, AttributeSet attrs) {
194        super(context, attrs);
195        if (DEBUG) Log.v(TAG, "Create TCV " + this);
196        mAudioManager = new AudioManager(mContext);
197        mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback
198        mIRCD = new IRemoteControlDisplayWeak(mHandler);
199    }
200
201    private void updateTransportControls(int transportControlFlags) {
202        mTransportControlFlags = transportControlFlags;
203    }
204
205    @Override
206    public void onFinishInflate() {
207        super.onFinishInflate();
208        mTrackTitle = (TextView) findViewById(R.id.title);
209        mTrackTitle.setSelected(true); // enable marquee
210        mAlbumArt = (ImageView) findViewById(R.id.albumart);
211        mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
212        mBtnPlay = (ImageView) findViewById(R.id.btn_play);
213        mBtnNext = (ImageView) findViewById(R.id.btn_next);
214        final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
215        for (View view : buttons) {
216            view.setOnClickListener(this);
217        }
218    }
219
220    @Override
221    public void onAttachedToWindow() {
222        super.onAttachedToWindow();
223        if (mPopulateMetadataWhenAttached != null) {
224            updateMetadata(mPopulateMetadataWhenAttached);
225            mPopulateMetadataWhenAttached = null;
226        }
227        if (!mAttached) {
228            if (DEBUG) Log.v(TAG, "Registering TCV " + this);
229            mAudioManager.registerRemoteControlDisplay(mIRCD);
230        }
231        mAttached = true;
232    }
233
234    @Override
235    public void onDetachedFromWindow() {
236        super.onDetachedFromWindow();
237        if (mAttached) {
238            if (DEBUG) Log.v(TAG, "Unregistering TCV " + this);
239            mAudioManager.unregisterRemoteControlDisplay(mIRCD);
240        }
241        mAttached = false;
242    }
243
244    @Override
245    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
246        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
247        int dim = Math.min(MAXDIM, Math.max(getWidth(), getHeight()));
248//        Log.v(TAG, "setting max bitmap size: " + dim + "x" + dim);
249//        mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim);
250    }
251
252    class Metadata {
253        private String artist;
254        private String trackTitle;
255        private String albumTitle;
256        private Bitmap bitmap;
257
258        public String toString() {
259            return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]";
260        }
261    }
262
263    private String getMdString(Bundle data, int id) {
264        return data.getString(Integer.toString(id));
265    }
266
267    private void updateMetadata(Bundle data) {
268        if (mAttached) {
269            mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
270            mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE);
271            mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM);
272            populateMetadata();
273        } else {
274            mPopulateMetadataWhenAttached = data;
275        }
276    }
277
278    /**
279     * Populates the given metadata into the view
280     */
281    private void populateMetadata() {
282        StringBuilder sb = new StringBuilder();
283        int trackTitleLength = 0;
284        if (!TextUtils.isEmpty(mMetadata.trackTitle)) {
285            sb.append(mMetadata.trackTitle);
286            trackTitleLength = mMetadata.trackTitle.length();
287        }
288        if (!TextUtils.isEmpty(mMetadata.artist)) {
289            if (sb.length() != 0) {
290                sb.append(" - ");
291            }
292            sb.append(mMetadata.artist);
293        }
294        if (!TextUtils.isEmpty(mMetadata.albumTitle)) {
295            if (sb.length() != 0) {
296                sb.append(" - ");
297            }
298            sb.append(mMetadata.albumTitle);
299        }
300        mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE);
301        Spannable str = (Spannable) mTrackTitle.getText();
302        if (trackTitleLength != 0) {
303            str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength,
304                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
305            trackTitleLength++;
306        }
307        if (sb.length() > trackTitleLength) {
308            str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(),
309                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
310        }
311
312        mAlbumArt.setImageBitmap(mMetadata.bitmap);
313        final int flags = mTransportControlFlags;
314        setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
315        setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
316        setVisibilityBasedOnFlag(mBtnPlay, flags,
317                RemoteControlClient.FLAG_KEY_MEDIA_PLAY
318                | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
319                | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
320                | RemoteControlClient.FLAG_KEY_MEDIA_STOP);
321
322        updatePlayPauseState(mCurrentPlayState);
323    }
324
325    private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
326        if ((flags & flag) != 0) {
327            view.setVisibility(View.VISIBLE);
328        } else {
329            view.setVisibility(View.GONE);
330        }
331    }
332
333    private void updatePlayPauseState(int state) {
334        if (DEBUG) Log.v(TAG,
335                "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state);
336        if (state == mCurrentPlayState) {
337            return;
338        }
339        final int imageResId;
340        final int imageDescId;
341        boolean showIfHidden = false;
342        switch (state) {
343            case RemoteControlClient.PLAYSTATE_ERROR:
344                imageResId = com.android.internal.R.drawable.stat_sys_warning;
345                // TODO use more specific image description string for warning, but here the "play"
346                //      message is still valid because this button triggers a play command.
347                imageDescId = com.android.internal.R.string.lockscreen_transport_play_description;
348                break;
349
350            case RemoteControlClient.PLAYSTATE_PLAYING:
351                imageResId = com.android.internal.R.drawable.ic_media_pause;
352                imageDescId = com.android.internal.R.string.lockscreen_transport_pause_description;
353                showIfHidden = true;
354                break;
355
356            case RemoteControlClient.PLAYSTATE_BUFFERING:
357                imageResId = com.android.internal.R.drawable.ic_media_stop;
358                imageDescId = com.android.internal.R.string.lockscreen_transport_stop_description;
359                showIfHidden = true;
360                break;
361
362            case RemoteControlClient.PLAYSTATE_PAUSED:
363            default:
364                imageResId = com.android.internal.R.drawable.ic_media_play;
365                imageDescId = com.android.internal.R.string.lockscreen_transport_play_description;
366                showIfHidden = false;
367                break;
368        }
369        mBtnPlay.setImageResource(imageResId);
370        mBtnPlay.setContentDescription(getResources().getString(imageDescId));
371        if (showIfHidden && mWidgetCallbacks != null && !mWidgetCallbacks.isVisible(this)) {
372            mWidgetCallbacks.requestShow(this);
373        }
374        mCurrentPlayState = state;
375    }
376
377    static class SavedState extends BaseSavedState {
378        boolean wasShowing;
379
380        SavedState(Parcelable superState) {
381            super(superState);
382        }
383
384        private SavedState(Parcel in) {
385            super(in);
386            this.wasShowing = in.readInt() != 0;
387        }
388
389        @Override
390        public void writeToParcel(Parcel out, int flags) {
391            super.writeToParcel(out, flags);
392            out.writeInt(this.wasShowing ? 1 : 0);
393        }
394
395        public static final Parcelable.Creator<SavedState> CREATOR
396                = new Parcelable.Creator<SavedState>() {
397            public SavedState createFromParcel(Parcel in) {
398                return new SavedState(in);
399            }
400
401            public SavedState[] newArray(int size) {
402                return new SavedState[size];
403            }
404        };
405    }
406
407    @Override
408    public Parcelable onSaveInstanceState() {
409        if (DEBUG) Log.v(TAG, "onSaveInstanceState()");
410        Parcelable superState = super.onSaveInstanceState();
411        SavedState ss = new SavedState(superState);
412        ss.wasShowing = mWidgetCallbacks != null && mWidgetCallbacks.isVisible(this);
413        return ss;
414    }
415
416    @Override
417    public void onRestoreInstanceState(Parcelable state) {
418        if (DEBUG) Log.v(TAG, "onRestoreInstanceState()");
419        if (!(state instanceof SavedState)) {
420            super.onRestoreInstanceState(state);
421            return;
422        }
423        SavedState ss = (SavedState) state;
424        super.onRestoreInstanceState(ss.getSuperState());
425        if (ss.wasShowing && mWidgetCallbacks != null) {
426            mWidgetCallbacks.requestShow(this);
427        }
428    }
429
430    public void onClick(View v) {
431        int keyCode = -1;
432        if (v == mBtnPrev) {
433            keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
434        } else if (v == mBtnNext) {
435            keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
436        } else if (v == mBtnPlay) {
437            keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
438
439        }
440        if (keyCode != -1) {
441            sendMediaButtonClick(keyCode);
442            if (mWidgetCallbacks != null) {
443                mWidgetCallbacks.userActivity(this);
444            }
445        }
446    }
447
448    private void sendMediaButtonClick(int keyCode) {
449        if (mClientIntent == null) {
450            // Shouldn't be possible because this view should be hidden in this case.
451            Log.e(TAG, "sendMediaButtonClick(): No client is currently registered");
452            return;
453        }
454        // use the registered PendingIntent that will be processed by the registered
455        //    media button event receiver, which is the component of mClientIntent
456        KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
457        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
458        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
459        try {
460            mClientIntent.send(getContext(), 0, intent);
461        } catch (CanceledException e) {
462            Log.e(TAG, "Error sending intent for media button down: "+e);
463            e.printStackTrace();
464        }
465
466        keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
467        intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
468        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
469        try {
470            mClientIntent.send(getContext(), 0, intent);
471        } catch (CanceledException e) {
472            Log.e(TAG, "Error sending intent for media button up: "+e);
473            e.printStackTrace();
474        }
475    }
476
477    public void setCallback(LockScreenWidgetCallback callback) {
478        mWidgetCallbacks = callback;
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