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