TransportControlView.java revision 054340d0a3f242efeaf898cca38625bdcb3b4b5a
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.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.graphics.Bitmap;
28import android.media.AudioManager;
29import android.media.MediaMetadataRetriever;
30import android.media.RemoteControlClient;
31import android.media.IRemoteControlDisplay;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Message;
35import android.os.RemoteException;
36import android.text.Spannable;
37import android.text.TextUtils;
38import android.text.style.ForegroundColorSpan;
39import android.util.AttributeSet;
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
48
49import com.android.internal.R;
50
51public class TransportControlView extends FrameLayout implements OnClickListener,
52        LockScreenWidgetInterface {
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 MAXDIM = 512;
60    protected static final boolean DEBUG = true;
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 ComponentName mClientName;
72    private int mTransportControlFlags;
73    private int mPlayState;
74    private AudioManager mAudioManager;
75    private LockScreenWidgetCallback mWidgetCallbacks;
76    private IRemoteControlDisplayWeak mIRCD;
77
78    /**
79     * The metadata which should be populated into the view once we've been attached
80     */
81    private Bundle mPopulateMetadataWhenAttached = null;
82
83    // This handler is required to ensure messages from IRCD are handled in sequence and on
84    // the UI thread.
85    private Handler mHandler = new Handler() {
86        @Override
87        public void handleMessage(Message msg) {
88            switch (msg.what) {
89            case MSG_UPDATE_STATE:
90                if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2);
91                break;
92
93            case MSG_SET_METADATA:
94                if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj);
95                break;
96
97            case MSG_SET_TRANSPORT_CONTROLS:
98                if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2);
99                break;
100
101            case MSG_SET_ARTWORK:
102                if (mClientGeneration == msg.arg1) {
103                    mMetadata.bitmap = (Bitmap) msg.obj;
104                    mAlbumArt.setImageBitmap(mMetadata.bitmap);
105                }
106                break;
107
108            case MSG_SET_GENERATION_ID:
109                if (mWidgetCallbacks != null) {
110                    boolean clearing = msg.arg2 != 0;
111                    if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + clearing);
112                    if (!clearing) {
113                        mWidgetCallbacks.requestShow(TransportControlView.this);
114                    } else {
115                        mWidgetCallbacks.requestHide(TransportControlView.this);
116                    }
117                }
118                mClientGeneration = msg.arg1;
119                mClientName = (ComponentName) msg.obj;
120                break;
121
122            }
123        }
124    };
125
126    /**
127     * This class is required to have weak linkage to the current TransportControlView
128     * because the remote process can hold a strong reference to this binder object and
129     * we can't predict when it will be GC'd in the remote process. Without this code, it
130     * would allow a heavyweight object to be held on this side of the binder when there's
131     * no requirement to run a GC on the other side.
132     */
133    private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub {
134        private WeakReference<Handler> mLocalHandler;
135
136        IRemoteControlDisplayWeak(Handler handler) {
137            mLocalHandler = new WeakReference<Handler>(handler);
138        }
139
140        public void setPlaybackState(int generationId, int state) {
141            Handler handler = mLocalHandler.get();
142            if (handler != null) {
143                handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
144            }
145        }
146
147        public void setMetadata(int generationId, Bundle metadata) {
148            Handler handler = mLocalHandler.get();
149            if (handler != null) {
150                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
151            }
152        }
153
154        public void setTransportControlFlags(int generationId, int flags) {
155            Handler handler = mLocalHandler.get();
156            if (handler != null) {
157                handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags)
158                        .sendToTarget();
159            }
160        }
161
162        public void setArtwork(int generationId, Bitmap bitmap) {
163            Handler handler = mLocalHandler.get();
164            if (handler != null) {
165                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
166            }
167        }
168
169        public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) {
170            Handler handler = mLocalHandler.get();
171            if (handler != null) {
172                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
173                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
174            }
175        }
176
177        public void setCurrentClientId(int clientGeneration, ComponentName clientEventReceiver,
178                boolean clearing) throws RemoteException {
179            Handler handler = mLocalHandler.get();
180            if (handler != null) {
181                handler.obtainMessage(MSG_SET_GENERATION_ID,
182                    clientGeneration, (clearing ? 1 : 0), clientEventReceiver).sendToTarget();
183            }
184        }
185    };
186
187    public TransportControlView(Context context, AttributeSet attrs) {
188        super(context, attrs);
189        Log.v(TAG, "Create TCV " + this);
190        mAudioManager = new AudioManager(mContext);
191        mIRCD = new IRemoteControlDisplayWeak(mHandler);
192    }
193
194    private void updateTransportControls(int transportControlFlags) {
195        mTransportControlFlags = transportControlFlags;
196    }
197
198    @Override
199    public void onFinishInflate() {
200        super.onFinishInflate();
201        mTrackTitle = (TextView) findViewById(R.id.title);
202        mTrackTitle.setSelected(true); // enable marquee
203        mAlbumArt = (ImageView) findViewById(R.id.albumart);
204        mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
205        mBtnPlay = (ImageView) findViewById(R.id.btn_play);
206        mBtnNext = (ImageView) findViewById(R.id.btn_next);
207        final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
208        for (View view : buttons) {
209            view.setOnClickListener(this);
210        }
211    }
212
213    @Override
214    public void onAttachedToWindow() {
215        super.onAttachedToWindow();
216        if (mPopulateMetadataWhenAttached != null) {
217            updateMetadata(mPopulateMetadataWhenAttached);
218            mPopulateMetadataWhenAttached = null;
219        }
220        if (!mAttached) {
221            if (DEBUG) Log.v(TAG, "Registering TCV " + this);
222            mAudioManager.registerRemoteControlDisplay(mIRCD);
223        }
224        mAttached = true;
225    }
226
227    @Override
228    public void onDetachedFromWindow() {
229        super.onDetachedFromWindow();
230        if (mAttached) {
231            if (DEBUG) Log.v(TAG, "Unregistering TCV " + this);
232            mAudioManager.unregisterRemoteControlDisplay(mIRCD);
233        }
234        mAttached = false;
235    }
236
237    @Override
238    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
239        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
240        int dim = Math.min(MAXDIM, Math.max(getWidth(), getHeight()));
241//        Log.v(TAG, "setting max bitmap size: " + dim + "x" + dim);
242//        mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim);
243    }
244
245    class Metadata {
246        private String artist;
247        private String trackTitle;
248        private String albumTitle;
249        private Bitmap bitmap;
250
251        public String toString() {
252            return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]";
253        }
254    }
255
256    private String getMdString(Bundle data, int id) {
257        return data.getString(Integer.toString(id));
258    }
259
260    private void updateMetadata(Bundle data) {
261        if (mAttached) {
262            mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
263            mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE);
264            mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM);
265            populateMetadata();
266        } else {
267            mPopulateMetadataWhenAttached = data;
268        }
269    }
270
271    /**
272     * Populates the given metadata into the view
273     */
274    private void populateMetadata() {
275        StringBuilder sb = new StringBuilder();
276        int trackTitleLength = 0;
277        if (!TextUtils.isEmpty(mMetadata.trackTitle)) {
278            sb.append(mMetadata.trackTitle);
279            trackTitleLength = mMetadata.trackTitle.length();
280        }
281        if (!TextUtils.isEmpty(mMetadata.artist)) {
282            if (sb.length() != 0) {
283                sb.append(" - ");
284            }
285            sb.append(mMetadata.artist);
286        }
287        if (!TextUtils.isEmpty(mMetadata.albumTitle)) {
288            if (sb.length() != 0) {
289                sb.append(" - ");
290            }
291            sb.append(mMetadata.albumTitle);
292        }
293        mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE);
294        Spannable str = (Spannable) mTrackTitle.getText();
295        if (trackTitleLength != 0) {
296            str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength,
297                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
298            trackTitleLength++;
299        }
300        if (sb.length() > trackTitleLength) {
301            str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(),
302                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
303        }
304
305        mAlbumArt.setImageBitmap(mMetadata.bitmap);
306        final int flags = mTransportControlFlags;
307        setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
308        setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
309        setVisibilityBasedOnFlag(mBtnPrev, flags,
310                RemoteControlClient.FLAG_KEY_MEDIA_PLAY
311                | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
312                | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
313                | RemoteControlClient.FLAG_KEY_MEDIA_STOP);
314
315        updatePlayPauseState(mPlayState);
316    }
317
318    private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
319        if ((flags & flag) != 0) {
320            view.setVisibility(View.VISIBLE);
321        } else {
322            view.setVisibility(View.GONE);
323        }
324    }
325
326    private void updatePlayPauseState(int state) {
327        if (DEBUG) Log.v(TAG,
328                "updatePlayPauseState(), old=" + mPlayState + ", state=" + state);
329        if (state == mPlayState) {
330            return;
331        }
332        switch (state) {
333            case RemoteControlClient.PLAYSTATE_PLAYING:
334                mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_pause);
335                break;
336
337            case RemoteControlClient.PLAYSTATE_BUFFERING:
338                mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_stop);
339                break;
340
341            case RemoteControlClient.PLAYSTATE_PAUSED:
342            default:
343                mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_play);
344                break;
345        }
346        mPlayState = state;
347    }
348
349    public void onClick(View v) {
350        int keyCode = -1;
351        if (v == mBtnPrev) {
352            keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
353        } else if (v == mBtnNext) {
354            keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
355        } else if (v == mBtnPlay) {
356            keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
357
358        }
359        if (keyCode != -1) {
360            sendMediaButtonClick(keyCode);
361            if (mWidgetCallbacks != null) {
362                mWidgetCallbacks.userActivity(this);
363            }
364        }
365    }
366
367    private void sendMediaButtonClick(int keyCode) {
368        // TODO: target to specific player based on mClientName
369        KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
370        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
371        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
372        getContext().sendOrderedBroadcast(intent, null);
373
374        keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
375        intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
376        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
377        getContext().sendOrderedBroadcast(intent, null);
378    }
379
380    public void setCallback(LockScreenWidgetCallback callback) {
381        mWidgetCallbacks = callback;
382    }
383
384    public boolean providesClock() {
385        return false;
386    }
387
388}
389