KeyguardTransportControlView.java revision f8895248e2ac4dbb46622f3e04c7256f03175b4f
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.content.Context; 20import android.content.pm.PackageManager; 21import android.content.res.Configuration; 22import android.graphics.Bitmap; 23import android.graphics.ColorMatrix; 24import android.graphics.ColorMatrixColorFilter; 25import android.graphics.drawable.Drawable; 26import android.media.AudioManager; 27import android.media.MediaMetadataEditor; 28import android.media.MediaMetadataRetriever; 29import android.media.RemoteControlClient; 30import android.media.RemoteController; 31import android.os.Parcel; 32import android.os.Parcelable; 33import android.os.SystemClock; 34import android.text.TextUtils; 35import android.text.format.DateFormat; 36import android.transition.ChangeBounds; 37import android.transition.ChangeText; 38import android.transition.Fade; 39import android.transition.TransitionManager; 40import android.transition.TransitionSet; 41import android.util.AttributeSet; 42import android.util.DisplayMetrics; 43import android.util.Log; 44import android.view.KeyEvent; 45import android.view.View; 46import android.view.ViewGroup; 47import android.widget.FrameLayout; 48import android.widget.ImageView; 49import android.widget.SeekBar; 50import android.widget.TextView; 51 52import java.text.SimpleDateFormat; 53import java.util.Date; 54import java.util.TimeZone; 55 56/** 57 * This is the widget responsible for showing music controls in keyguard. 58 */ 59public class KeyguardTransportControlView extends FrameLayout { 60 61 private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s 62 private static final int RESET_TO_METADATA_DELAY = 5000; 63 protected static final boolean DEBUG = false; 64 protected static final String TAG = "TransportControlView"; 65 66 private static final boolean ANIMATE_TRANSITIONS = false; 67 68 private ViewGroup mMetadataContainer; 69 private ViewGroup mInfoContainer; 70 private TextView mTrackTitle; 71 private TextView mTrackArtistAlbum; 72 73 private View mTransientSeek; 74 private SeekBar mTransientSeekBar; 75 private TextView mTransientSeekTimeElapsed; 76 private TextView mTransientSeekTimeRemaining; 77 78 private ImageView mBtnPrev; 79 private ImageView mBtnPlay; 80 private ImageView mBtnNext; 81 private Metadata mMetadata = new Metadata(); 82 private int mTransportControlFlags; 83 private int mCurrentPlayState; 84 private AudioManager mAudioManager; 85 private RemoteController mRemoteController; 86 87 private ImageView mBadge; 88 89 private boolean mSeekEnabled; 90 private boolean mUserSeeking; 91 private java.text.DateFormat mFormat; 92 93 /** 94 * The metadata which should be populated into the view once we've been attached 95 */ 96 private RemoteController.MetadataEditor mPopulateMetadataWhenAttached = null; 97 98 private RemoteController.OnClientUpdateListener mRCClientUpdateListener = 99 new RemoteController.OnClientUpdateListener() { 100 @Override 101 public void onClientChange(boolean clearing) { 102 if (clearing) { 103 clearMetadata(); 104 } 105 } 106 107 @Override 108 public void onClientPlaybackStateUpdate(int state) { 109 setSeekBarsEnabled(false); 110 updatePlayPauseState(state); 111 } 112 113 @Override 114 public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, 115 long currentPosMs, float speed) { 116 setSeekBarsEnabled(mMetadata != null && mMetadata.duration > 0); 117 updatePlayPauseState(state); 118 if (DEBUG) Log.d(TAG, "onClientPlaybackStateUpdate(state=" + state + 119 ", stateChangeTimeMs=" + stateChangeTimeMs + ", currentPosMs=" + currentPosMs + 120 ", speed=" + speed + ")"); 121 } 122 123 @Override 124 public void onClientTransportControlUpdate(int transportControlFlags) { 125 updateTransportControls(transportControlFlags); 126 } 127 128 @Override 129 public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) { 130 updateMetadata(metadataEditor); 131 } 132 }; 133 134 private final Runnable mUpdateSeekBars = new Runnable() { 135 public void run() { 136 if (updateSeekBars()) { 137 postDelayed(this, 1000); 138 } 139 } 140 }; 141 142 private final Runnable mResetToMetadata = new Runnable() { 143 public void run() { 144 resetToMetadata(); 145 } 146 }; 147 148 private final OnClickListener mTransportCommandListener = new OnClickListener() { 149 public void onClick(View v) { 150 int keyCode = -1; 151 if (v == mBtnPrev) { 152 keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; 153 } else if (v == mBtnNext) { 154 keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; 155 } else if (v == mBtnPlay) { 156 keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; 157 } 158 if (keyCode != -1) { 159 sendMediaButtonClick(keyCode); 160 } 161 } 162 }; 163 164 private final OnLongClickListener mTransportShowSeekBarListener = new OnLongClickListener() { 165 @Override 166 public boolean onLongClick(View v) { 167 if (mSeekEnabled) { 168 return tryToggleSeekBar(); 169 } 170 return false; 171 } 172 }; 173 174 private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = 175 new SeekBar.OnSeekBarChangeListener() { 176 @Override 177 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 178 if (fromUser) { 179 scrubTo(progress); 180 delayResetToMetadata(); 181 } 182 updateSeekDisplay(); 183 } 184 185 @Override 186 public void onStartTrackingTouch(SeekBar seekBar) { 187 mUserSeeking = true; 188 } 189 190 @Override 191 public void onStopTrackingTouch(SeekBar seekBar) { 192 mUserSeeking = false; 193 } 194 }; 195 196 private static final int TRANSITION_DURATION = 200; 197 private final TransitionSet mMetadataChangeTransition; 198 199 KeyguardHostView.TransportControlCallback mTransportControlCallback; 200 201 public KeyguardTransportControlView(Context context, AttributeSet attrs) { 202 super(context, attrs); 203 if (DEBUG) Log.v(TAG, "Create TCV " + this); 204 mAudioManager = new AudioManager(mContext); 205 mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback 206 mRemoteController = new RemoteController(context); 207 mRemoteController.setOnClientUpdateListener(mRCClientUpdateListener); 208 209 final DisplayMetrics dm = context.getResources().getDisplayMetrics(); 210 final int dim = Math.max(dm.widthPixels, dm.heightPixels); 211 mRemoteController.setArtworkConfiguration(true, dim, dim); 212 213 final ChangeText tc = new ChangeText(); 214 tc.setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN); 215 final TransitionSet inner = new TransitionSet(); 216 inner.addTransition(tc).addTransition(new ChangeBounds()); 217 final TransitionSet tg = new TransitionSet(); 218 tg.addTransition(new Fade(Fade.OUT)).addTransition(inner). 219 addTransition(new Fade(Fade.IN)); 220 tg.setOrdering(TransitionSet.ORDERING_SEQUENTIAL); 221 tg.setDuration(TRANSITION_DURATION); 222 mMetadataChangeTransition = tg; 223 } 224 225 private void updateTransportControls(int transportControlFlags) { 226 mTransportControlFlags = transportControlFlags; 227 setSeekBarsEnabled( 228 (transportControlFlags & RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE) != 0); 229 } 230 231 void setSeekBarsEnabled(boolean enabled) { 232 if (enabled == mSeekEnabled) return; 233 234 mSeekEnabled = enabled; 235 if (mTransientSeek.getVisibility() == VISIBLE) { 236 mTransientSeek.setVisibility(INVISIBLE); 237 mMetadataContainer.setVisibility(VISIBLE); 238 mUserSeeking = false; 239 cancelResetToMetadata(); 240 } 241 if (enabled) { 242 mUpdateSeekBars.run(); 243 postDelayed(mUpdateSeekBars, 1000); 244 } else { 245 removeCallbacks(mUpdateSeekBars); 246 } 247 } 248 249 public void setTransportControlCallback(KeyguardHostView.TransportControlCallback 250 transportControlCallback) { 251 mTransportControlCallback = transportControlCallback; 252 } 253 254 @Override 255 public void onFinishInflate() { 256 super.onFinishInflate(); 257 mInfoContainer = (ViewGroup) findViewById(R.id.info_container); 258 mMetadataContainer = (ViewGroup) findViewById(R.id.metadata_container); 259 mBadge = (ImageView) findViewById(R.id.badge); 260 mTrackTitle = (TextView) findViewById(R.id.title); 261 mTrackTitle.setSelected(true); // enable marquee 262 mTrackArtistAlbum = (TextView) findViewById(R.id.artist_album); 263 mTrackArtistAlbum.setSelected(true); 264 mTransientSeek = findViewById(R.id.transient_seek); 265 mTransientSeekBar = (SeekBar) findViewById(R.id.transient_seek_bar); 266 mTransientSeekBar.setOnSeekBarChangeListener(mOnSeekBarChangeListener); 267 mTransientSeekTimeElapsed = (TextView) findViewById(R.id.transient_seek_time_elapsed); 268 mTransientSeekTimeRemaining = (TextView) findViewById(R.id.transient_seek_time_remaining); 269 mBtnPrev = (ImageView) findViewById(R.id.btn_prev); 270 mBtnPlay = (ImageView) findViewById(R.id.btn_play); 271 mBtnNext = (ImageView) findViewById(R.id.btn_next); 272 final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; 273 for (View view : buttons) { 274 view.setOnClickListener(mTransportCommandListener); 275 view.setOnLongClickListener(mTransportShowSeekBarListener); 276 } 277 } 278 279 @Override 280 public void onAttachedToWindow() { 281 super.onAttachedToWindow(); 282 if (DEBUG) Log.v(TAG, "onAttachToWindow()"); 283 if (mPopulateMetadataWhenAttached != null) { 284 updateMetadata(mPopulateMetadataWhenAttached); 285 mPopulateMetadataWhenAttached = null; 286 } 287 if (DEBUG) Log.v(TAG, "Registering TCV " + this); 288 mAudioManager.registerRemoteController(mRemoteController); 289 } 290 291 @Override 292 protected void onConfigurationChanged(Configuration newConfig) { 293 super.onConfigurationChanged(newConfig); 294 final DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); 295 final int dim = Math.max(dm.widthPixels, dm.heightPixels); 296 mRemoteController.setArtworkConfiguration(true, dim, dim); 297 } 298 299 @Override 300 public void onDetachedFromWindow() { 301 if (DEBUG) Log.v(TAG, "onDetachFromWindow()"); 302 super.onDetachedFromWindow(); 303 if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); 304 mAudioManager.unregisterRemoteController(mRemoteController); 305 mUserSeeking = false; 306 } 307 308 void setBadgeIcon(Drawable bmp) { 309 mBadge.setImageDrawable(bmp); 310 311 final ColorMatrix cm = new ColorMatrix(); 312 cm.setSaturation(0); 313 mBadge.setColorFilter(new ColorMatrixColorFilter(cm)); 314 mBadge.setImageAlpha(0xef); 315 } 316 317 class Metadata { 318 private String artist; 319 private String trackTitle; 320 private String albumTitle; 321 private Bitmap bitmap; 322 private long duration; 323 324 public void clear() { 325 artist = null; 326 trackTitle = null; 327 albumTitle = null; 328 bitmap = null; 329 duration = -1; 330 } 331 332 public String toString() { 333 return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + 334 " albumTitle=" + albumTitle + " duration=" + duration + "]"; 335 } 336 } 337 338 void clearMetadata() { 339 mPopulateMetadataWhenAttached = null; 340 mMetadata.clear(); 341 populateMetadata(); 342 } 343 344 void updateMetadata(RemoteController.MetadataEditor data) { 345 if (isAttachedToWindow()) { 346 mMetadata.artist = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, 347 mMetadata.artist); 348 mMetadata.trackTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_TITLE, 349 mMetadata.trackTitle); 350 mMetadata.albumTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUM, 351 mMetadata.albumTitle); 352 mMetadata.duration = data.getLong(MediaMetadataRetriever.METADATA_KEY_DURATION, -1); 353 mMetadata.bitmap = data.getBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK, 354 mMetadata.bitmap); 355 populateMetadata(); 356 } else { 357 mPopulateMetadataWhenAttached = data; 358 } 359 } 360 361 /** 362 * Populates the given metadata into the view 363 */ 364 private void populateMetadata() { 365 if (ANIMATE_TRANSITIONS && isLaidOut() && mMetadataContainer.getVisibility() == VISIBLE) { 366 TransitionManager.beginDelayedTransition(mMetadataContainer, mMetadataChangeTransition); 367 } 368 369 final String remoteClientPackage = mRemoteController.getRemoteControlClientPackageName(); 370 Drawable badgeIcon = null; 371 try { 372 badgeIcon = getContext().getPackageManager().getApplicationIcon(remoteClientPackage); 373 } catch (PackageManager.NameNotFoundException e) { 374 Log.e(TAG, "Couldn't get remote control client package icon", e); 375 } 376 setBadgeIcon(badgeIcon); 377 if (!TextUtils.isEmpty(mMetadata.trackTitle)) { 378 mTrackTitle.setText(mMetadata.trackTitle); 379 } 380 StringBuilder sb = new StringBuilder(); 381 if (!TextUtils.isEmpty(mMetadata.artist)) { 382 if (sb.length() != 0) { 383 sb.append(" - "); 384 } 385 sb.append(mMetadata.artist); 386 } 387 if (!TextUtils.isEmpty(mMetadata.albumTitle)) { 388 if (sb.length() != 0) { 389 sb.append(" - "); 390 } 391 sb.append(mMetadata.albumTitle); 392 } 393 mTrackArtistAlbum.setText(sb.toString()); 394 395 if (mMetadata.duration >= 0) { 396 setSeekBarsEnabled(true); 397 setSeekBarDuration(mMetadata.duration); 398 399 final String skeleton; 400 401 if (mMetadata.duration >= 86400000) { 402 skeleton = "DDD kk mm ss"; 403 } else if (mMetadata.duration >= 3600000) { 404 skeleton = "kk mm ss"; 405 } else { 406 skeleton = "mm ss"; 407 } 408 mFormat = new SimpleDateFormat(DateFormat.getBestDateTimePattern( 409 getContext().getResources().getConfiguration().locale, 410 skeleton)); 411 mFormat.setTimeZone(TimeZone.getTimeZone("GMT+0")); 412 } else { 413 setSeekBarsEnabled(false); 414 } 415 416 KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground( 417 mMetadata.bitmap); 418 final int flags = mTransportControlFlags; 419 setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); 420 setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT); 421 setVisibilityBasedOnFlag(mBtnPlay, flags, 422 RemoteControlClient.FLAG_KEY_MEDIA_PLAY 423 | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE 424 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE 425 | RemoteControlClient.FLAG_KEY_MEDIA_STOP); 426 427 updatePlayPauseState(mCurrentPlayState); 428 } 429 430 void updateSeekDisplay() { 431 if (mMetadata != null && mRemoteController != null && mFormat != null) { 432 final long timeElapsed = mRemoteController.getEstimatedMediaPosition(); 433 final long duration = mMetadata.duration; 434 final long remaining = duration - timeElapsed; 435 436 mTransientSeekTimeElapsed.setText(mFormat.format(new Date(timeElapsed))); 437 mTransientSeekTimeRemaining.setText(mFormat.format(new Date(remaining))); 438 439 if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + timeElapsed + 440 " duration=" + duration + " remaining=" + remaining); 441 } 442 } 443 444 boolean tryToggleSeekBar() { 445 if (ANIMATE_TRANSITIONS) { 446 TransitionManager.beginDelayedTransition(mInfoContainer); 447 } 448 if (mTransientSeek.getVisibility() == VISIBLE) { 449 mTransientSeek.setVisibility(INVISIBLE); 450 mMetadataContainer.setVisibility(VISIBLE); 451 cancelResetToMetadata(); 452 } else { 453 mTransientSeek.setVisibility(VISIBLE); 454 mMetadataContainer.setVisibility(INVISIBLE); 455 delayResetToMetadata(); 456 } 457 mTransportControlCallback.userActivity(); 458 return true; 459 } 460 461 void resetToMetadata() { 462 if (ANIMATE_TRANSITIONS) { 463 TransitionManager.beginDelayedTransition(mInfoContainer); 464 } 465 if (mTransientSeek.getVisibility() == VISIBLE) { 466 mTransientSeek.setVisibility(INVISIBLE); 467 mMetadataContainer.setVisibility(VISIBLE); 468 } 469 // TODO Also hide ratings, if applicable 470 } 471 472 void delayResetToMetadata() { 473 removeCallbacks(mResetToMetadata); 474 postDelayed(mResetToMetadata, RESET_TO_METADATA_DELAY); 475 } 476 477 void cancelResetToMetadata() { 478 removeCallbacks(mResetToMetadata); 479 } 480 481 void setSeekBarDuration(long duration) { 482 mTransientSeekBar.setMax((int) duration); 483 } 484 485 void scrubTo(int progress) { 486 mRemoteController.seekTo(progress); 487 mTransportControlCallback.userActivity(); 488 } 489 490 private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { 491 if ((flags & flag) != 0) { 492 view.setVisibility(View.VISIBLE); 493 } else { 494 view.setVisibility(View.GONE); 495 } 496 } 497 498 private void updatePlayPauseState(int state) { 499 if (DEBUG) Log.v(TAG, 500 "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state); 501 if (state == mCurrentPlayState) { 502 return; 503 } 504 final int imageResId; 505 final int imageDescId; 506 switch (state) { 507 case RemoteControlClient.PLAYSTATE_ERROR: 508 imageResId = R.drawable.stat_sys_warning; 509 // TODO use more specific image description string for warning, but here the "play" 510 // message is still valid because this button triggers a play command. 511 imageDescId = R.string.keyguard_transport_play_description; 512 break; 513 514 case RemoteControlClient.PLAYSTATE_PLAYING: 515 imageResId = R.drawable.ic_media_pause; 516 imageDescId = R.string.keyguard_transport_pause_description; 517 if (mSeekEnabled) { 518 postDelayed(mUpdateSeekBars, 1000); 519 } 520 break; 521 522 case RemoteControlClient.PLAYSTATE_BUFFERING: 523 imageResId = R.drawable.ic_media_stop; 524 imageDescId = R.string.keyguard_transport_stop_description; 525 break; 526 527 case RemoteControlClient.PLAYSTATE_PAUSED: 528 default: 529 imageResId = R.drawable.ic_media_play; 530 imageDescId = R.string.keyguard_transport_play_description; 531 break; 532 } 533 534 if (state != RemoteControlClient.PLAYSTATE_PLAYING) { 535 removeCallbacks(mUpdateSeekBars); 536 updateSeekBars(); 537 } 538 mBtnPlay.setImageResource(imageResId); 539 mBtnPlay.setContentDescription(getResources().getString(imageDescId)); 540 mCurrentPlayState = state; 541 } 542 543 boolean updateSeekBars() { 544 final int position = (int) mRemoteController.getEstimatedMediaPosition(); 545 if (position >= 0) { 546 if (!mUserSeeking) { 547 mTransientSeekBar.setProgress(position); 548 } 549 return true; 550 } 551 Log.w(TAG, "Updating seek bars; received invalid estimated media position (" + 552 position + "). Disabling seek."); 553 setSeekBarsEnabled(false); 554 return false; 555 } 556 557 static class SavedState extends BaseSavedState { 558 boolean clientPresent; 559 560 SavedState(Parcelable superState) { 561 super(superState); 562 } 563 564 private SavedState(Parcel in) { 565 super(in); 566 this.clientPresent = in.readInt() != 0; 567 } 568 569 @Override 570 public void writeToParcel(Parcel out, int flags) { 571 super.writeToParcel(out, flags); 572 out.writeInt(this.clientPresent ? 1 : 0); 573 } 574 575 public static final Parcelable.Creator<SavedState> CREATOR 576 = new Parcelable.Creator<SavedState>() { 577 public SavedState createFromParcel(Parcel in) { 578 return new SavedState(in); 579 } 580 581 public SavedState[] newArray(int size) { 582 return new SavedState[size]; 583 } 584 }; 585 } 586 587 private void sendMediaButtonClick(int keyCode) { 588 // TODO We should think about sending these up/down events accurately with touch up/down 589 // on the buttons, but in the near term this will interfere with the long press behavior. 590 mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); 591 mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); 592 593 mTransportControlCallback.userActivity(); 594 } 595 596 public boolean providesClock() { 597 return false; 598 } 599 600 private boolean wasPlayingRecently(int state, long stateChangeTimeMs) { 601 switch (state) { 602 case RemoteControlClient.PLAYSTATE_PLAYING: 603 case RemoteControlClient.PLAYSTATE_FAST_FORWARDING: 604 case RemoteControlClient.PLAYSTATE_REWINDING: 605 case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS: 606 case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS: 607 case RemoteControlClient.PLAYSTATE_BUFFERING: 608 // actively playing or about to play 609 return true; 610 case RemoteControlClient.PLAYSTATE_NONE: 611 return false; 612 case RemoteControlClient.PLAYSTATE_STOPPED: 613 case RemoteControlClient.PLAYSTATE_PAUSED: 614 case RemoteControlClient.PLAYSTATE_ERROR: 615 // we have stopped playing, check how long ago 616 if (DEBUG) { 617 if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) { 618 Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently"); 619 } else { 620 Log.v(TAG, "wasPlayingRecently: time > TIMEOUT"); 621 } 622 } 623 return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS); 624 default: 625 Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()"); 626 return false; 627 } 628 } 629} 630