blob: 3208aff56fcaaca1dcc147979c4801f7cfaea158 [file] [log] [blame]
Jim Millerff2aa0b2012-09-06 19:03:52 -07001/*
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
Jim Miller5ecd8112013-01-09 18:50:26 -080017package com.android.keyguard;
Jim Millerff2aa0b2012-09-06 19:03:52 -070018
Jim Millerff2aa0b2012-09-06 19:03:52 -070019import android.app.PendingIntent;
20import android.app.PendingIntent.CanceledException;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.media.AudioManager;
Michael Jurka1254f2f2012-10-25 11:44:31 -070025import android.media.IRemoteControlDisplay;
Jim Millerff2aa0b2012-09-06 19:03:52 -070026import android.media.MediaMetadataRetriever;
27import android.media.RemoteControlClient;
Jim Millerff2aa0b2012-09-06 19:03:52 -070028import 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;
Jim Millerbdca3c02012-10-29 19:11:50 -070043import android.widget.FrameLayout;
Jim Millerff2aa0b2012-09-06 19:03:52 -070044import android.widget.ImageView;
45import android.widget.TextView;
46
Michael Jurka1254f2f2012-10-25 11:44:31 -070047import java.lang.ref.WeakReference;
Jim Millerff2aa0b2012-09-06 19:03:52 -070048/**
49 * This is the widget responsible for showing music controls in keyguard.
50 */
Jim Millerbdca3c02012-10-29 19:11:50 -070051public class KeyguardTransportControlView extends FrameLayout implements OnClickListener {
Jim Millerff2aa0b2012-09-06 19:03:52 -070052
53 private static final int MSG_UPDATE_STATE = 100;
54 private static final int MSG_SET_METADATA = 101;
55 private static final int MSG_SET_TRANSPORT_CONTROLS = 102;
56 private static final int MSG_SET_ARTWORK = 103;
57 private static final int MSG_SET_GENERATION_ID = 104;
Jim Millerff2aa0b2012-09-06 19:03:52 -070058 private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s
Jim Miller71b3cd52012-10-10 19:02:40 -070059 protected static final boolean DEBUG = false;
Jim Millerff2aa0b2012-09-06 19:03:52 -070060 protected static final String TAG = "TransportControlView";
61
62 private ImageView mAlbumArt;
63 private TextView mTrackTitle;
64 private ImageView mBtnPrev;
65 private ImageView mBtnPlay;
66 private ImageView mBtnNext;
67 private int mClientGeneration;
68 private Metadata mMetadata = new Metadata();
69 private boolean mAttached;
70 private PendingIntent mClientIntent;
71 private int mTransportControlFlags;
72 private int mCurrentPlayState;
73 private AudioManager mAudioManager;
74 private IRemoteControlDisplayWeak mIRCD;
75
76 /**
77 * The metadata which should be populated into the view once we've been attached
78 */
79 private Bundle mPopulateMetadataWhenAttached = null;
80
81 // This handler is required to ensure messages from IRCD are handled in sequence and on
82 // the UI thread.
83 private Handler mHandler = new Handler() {
84 @Override
85 public void handleMessage(Message msg) {
86 switch (msg.what) {
87 case MSG_UPDATE_STATE:
88 if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2);
89 break;
90
91 case MSG_SET_METADATA:
92 if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj);
93 break;
94
95 case MSG_SET_TRANSPORT_CONTROLS:
96 if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2);
97 break;
98
99 case MSG_SET_ARTWORK:
100 if (mClientGeneration == msg.arg1) {
101 if (mMetadata.bitmap != null) {
102 mMetadata.bitmap.recycle();
103 }
104 mMetadata.bitmap = (Bitmap) msg.obj;
105 mAlbumArt.setImageBitmap(mMetadata.bitmap);
106 }
107 break;
108
109 case MSG_SET_GENERATION_ID:
Jim Millerff2aa0b2012-09-06 19:03:52 -0700110 if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2);
111 mClientGeneration = msg.arg1;
112 mClientIntent = (PendingIntent) msg.obj;
113 break;
114
115 }
116 }
117 };
Jim Millerff2aa0b2012-09-06 19:03:52 -0700118
119 /**
120 * This class is required to have weak linkage to the current TransportControlView
121 * because the remote process can hold a strong reference to this binder object and
122 * we can't predict when it will be GC'd in the remote process. Without this code, it
123 * would allow a heavyweight object to be held on this side of the binder when there's
124 * no requirement to run a GC on the other side.
125 */
126 private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub {
127 private WeakReference<Handler> mLocalHandler;
128
129 IRemoteControlDisplayWeak(Handler handler) {
130 mLocalHandler = new WeakReference<Handler>(handler);
131 }
132
Jean-Michel Trivibc43b4c2013-03-22 09:30:50 -0700133 public void setPlaybackState(int generationId, int state, long stateChangeTimeMs,
134 long currentPosMs, float speed) {
Jim Millerff2aa0b2012-09-06 19:03:52 -0700135 Handler handler = mLocalHandler.get();
136 if (handler != null) {
137 handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
138 }
139 }
140
141 public void setMetadata(int generationId, Bundle metadata) {
142 Handler handler = mLocalHandler.get();
143 if (handler != null) {
144 handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
145 }
146 }
147
Jean-Michel Trivi3261b532013-04-01 14:59:39 -0700148 public void setTransportControlInfo(int generationId, int flags, int posCapabilities) {
Jim Millerff2aa0b2012-09-06 19:03:52 -0700149 Handler handler = mLocalHandler.get();
150 if (handler != null) {
151 handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags)
152 .sendToTarget();
153 }
154 }
155
156 public void setArtwork(int generationId, Bitmap bitmap) {
157 Handler handler = mLocalHandler.get();
158 if (handler != null) {
159 handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
160 }
161 }
162
163 public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) {
164 Handler handler = mLocalHandler.get();
165 if (handler != null) {
166 handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
167 handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
168 }
169 }
170
171 public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent,
172 boolean clearing) throws RemoteException {
173 Handler handler = mLocalHandler.get();
174 if (handler != null) {
175 handler.obtainMessage(MSG_SET_GENERATION_ID,
176 clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget();
177 }
178 }
179 };
180
181 public KeyguardTransportControlView(Context context, AttributeSet attrs) {
182 super(context, attrs);
Dianne Hackborn40e9f292012-11-27 19:12:23 -0800183 if (DEBUG) Log.v(TAG, "Create TCV " + this);
Jim Millerff2aa0b2012-09-06 19:03:52 -0700184 mAudioManager = new AudioManager(mContext);
185 mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback
186 mIRCD = new IRemoteControlDisplayWeak(mHandler);
187 }
188
Jim Millerff2aa0b2012-09-06 19:03:52 -0700189 private void updateTransportControls(int transportControlFlags) {
190 mTransportControlFlags = transportControlFlags;
191 }
192
193 @Override
194 public void onFinishInflate() {
195 super.onFinishInflate();
196 mTrackTitle = (TextView) findViewById(R.id.title);
197 mTrackTitle.setSelected(true); // enable marquee
198 mAlbumArt = (ImageView) findViewById(R.id.albumart);
199 mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
200 mBtnPlay = (ImageView) findViewById(R.id.btn_play);
201 mBtnNext = (ImageView) findViewById(R.id.btn_next);
202 final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
203 for (View view : buttons) {
204 view.setOnClickListener(this);
205 }
206 }
207
208 @Override
209 public void onAttachedToWindow() {
210 super.onAttachedToWindow();
211 if (DEBUG) Log.v(TAG, "onAttachToWindow()");
212 if (mPopulateMetadataWhenAttached != null) {
213 updateMetadata(mPopulateMetadataWhenAttached);
214 mPopulateMetadataWhenAttached = null;
215 }
216 if (!mAttached) {
217 if (DEBUG) Log.v(TAG, "Registering TCV " + this);
218 mAudioManager.registerRemoteControlDisplay(mIRCD);
219 }
220 mAttached = true;
221 }
222
223 @Override
Jean-Michel Trivi9e589b92013-03-08 14:30:10 -0800224 protected void onSizeChanged (int w, int h, int oldw, int oldh) {
225 if (mAttached) {
226 int dim = Math.min(512, Math.max(w, h));
227 if (DEBUG) Log.v(TAG, "TCV uses bitmap size=" + dim);
228 mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim);
229 }
230 }
231
232 @Override
Jim Millerff2aa0b2012-09-06 19:03:52 -0700233 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
Jim Millerff2aa0b2012-09-06 19:03:52 -0700243 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 mAlbumArt.setImageBitmap(mMetadata.bitmap);
304 final int flags = mTransportControlFlags;
305 setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
306 setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
307 setVisibilityBasedOnFlag(mBtnPlay, flags,
308 RemoteControlClient.FLAG_KEY_MEDIA_PLAY
309 | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
310 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
311 | RemoteControlClient.FLAG_KEY_MEDIA_STOP);
312
313 updatePlayPauseState(mCurrentPlayState);
314 }
315
316 private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
317 if ((flags & flag) != 0) {
318 view.setVisibility(View.VISIBLE);
319 } else {
320 view.setVisibility(View.GONE);
321 }
322 }
323
324 private void updatePlayPauseState(int state) {
325 if (DEBUG) Log.v(TAG,
326 "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state);
327 if (state == mCurrentPlayState) {
328 return;
329 }
330 final int imageResId;
331 final int imageDescId;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700332 switch (state) {
333 case RemoteControlClient.PLAYSTATE_ERROR:
Jim Miller5ecd8112013-01-09 18:50:26 -0800334 imageResId = R.drawable.stat_sys_warning;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700335 // TODO use more specific image description string for warning, but here the "play"
336 // message is still valid because this button triggers a play command.
Jim Miller5ecd8112013-01-09 18:50:26 -0800337 imageDescId = R.string.keyguard_transport_play_description;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700338 break;
339
340 case RemoteControlClient.PLAYSTATE_PLAYING:
Jim Miller5ecd8112013-01-09 18:50:26 -0800341 imageResId = R.drawable.ic_media_pause;
342 imageDescId = R.string.keyguard_transport_pause_description;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700343 break;
344
345 case RemoteControlClient.PLAYSTATE_BUFFERING:
Jim Miller5ecd8112013-01-09 18:50:26 -0800346 imageResId = R.drawable.ic_media_stop;
347 imageDescId = R.string.keyguard_transport_stop_description;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700348 break;
349
350 case RemoteControlClient.PLAYSTATE_PAUSED:
351 default:
Jim Miller5ecd8112013-01-09 18:50:26 -0800352 imageResId = R.drawable.ic_media_play;
353 imageDescId = R.string.keyguard_transport_play_description;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700354 break;
355 }
356 mBtnPlay.setImageResource(imageResId);
357 mBtnPlay.setContentDescription(getResources().getString(imageDescId));
Jim Millerff2aa0b2012-09-06 19:03:52 -0700358 mCurrentPlayState = state;
359 }
360
361 static class SavedState extends BaseSavedState {
Jim Miller223ce5c2012-10-05 19:13:23 -0700362 boolean clientPresent;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700363
364 SavedState(Parcelable superState) {
365 super(superState);
366 }
367
368 private SavedState(Parcel in) {
369 super(in);
Jim Miller223ce5c2012-10-05 19:13:23 -0700370 this.clientPresent = in.readInt() != 0;
Jim Millerff2aa0b2012-09-06 19:03:52 -0700371 }
372
373 @Override
374 public void writeToParcel(Parcel out, int flags) {
375 super.writeToParcel(out, flags);
Jim Miller223ce5c2012-10-05 19:13:23 -0700376 out.writeInt(this.clientPresent ? 1 : 0);
Jim Millerff2aa0b2012-09-06 19:03:52 -0700377 }
378
379 public static final Parcelable.Creator<SavedState> CREATOR
380 = new Parcelable.Creator<SavedState>() {
381 public SavedState createFromParcel(Parcel in) {
382 return new SavedState(in);
383 }
384
385 public SavedState[] newArray(int size) {
386 return new SavedState[size];
387 }
388 };
389 }
390
Jim Millerff2aa0b2012-09-06 19:03:52 -0700391 public void onClick(View v) {
392 int keyCode = -1;
393 if (v == mBtnPrev) {
394 keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
395 } else if (v == mBtnNext) {
396 keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
397 } else if (v == mBtnPlay) {
398 keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
399
400 }
401 if (keyCode != -1) {
402 sendMediaButtonClick(keyCode);
Jim Millerff2aa0b2012-09-06 19:03:52 -0700403 }
404 }
405
406 private void sendMediaButtonClick(int keyCode) {
407 if (mClientIntent == null) {
408 // Shouldn't be possible because this view should be hidden in this case.
409 Log.e(TAG, "sendMediaButtonClick(): No client is currently registered");
410 return;
411 }
412 // use the registered PendingIntent that will be processed by the registered
413 // media button event receiver, which is the component of mClientIntent
414 KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
415 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
416 intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
417 try {
418 mClientIntent.send(getContext(), 0, intent);
419 } catch (CanceledException e) {
420 Log.e(TAG, "Error sending intent for media button down: "+e);
421 e.printStackTrace();
422 }
423
424 keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
425 intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
426 intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
427 try {
428 mClientIntent.send(getContext(), 0, intent);
429 } catch (CanceledException e) {
430 Log.e(TAG, "Error sending intent for media button up: "+e);
431 e.printStackTrace();
432 }
433 }
434
435 public boolean providesClock() {
436 return false;
437 }
438
439 private boolean wasPlayingRecently(int state, long stateChangeTimeMs) {
440 switch (state) {
441 case RemoteControlClient.PLAYSTATE_PLAYING:
442 case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
443 case RemoteControlClient.PLAYSTATE_REWINDING:
444 case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
445 case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
446 case RemoteControlClient.PLAYSTATE_BUFFERING:
447 // actively playing or about to play
448 return true;
449 case RemoteControlClient.PLAYSTATE_NONE:
450 return false;
451 case RemoteControlClient.PLAYSTATE_STOPPED:
452 case RemoteControlClient.PLAYSTATE_PAUSED:
453 case RemoteControlClient.PLAYSTATE_ERROR:
454 // we have stopped playing, check how long ago
455 if (DEBUG) {
456 if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) {
457 Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently");
458 } else {
459 Log.v(TAG, "wasPlayingRecently: time > TIMEOUT");
460 }
461 }
462 return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS);
463 default:
464 Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()");
465 return false;
466 }
467 }
Jim Millerff2aa0b2012-09-06 19:03:52 -0700468}