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