KeyguardTransportControlView.java revision 43a372f38ad642f86047e8112e3d43edb7300439
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.DisplayMetrics; 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 48import java.lang.ref.WeakReference; 49/** 50 * This is the widget responsible for showing music controls in keyguard. 51 */ 52public class KeyguardTransportControlView extends FrameLayout implements OnClickListener { 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 DISPLAY_TIMEOUT_MS = 5000; // 5s 60 protected static final boolean DEBUG = false; 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 PendingIntent mClientIntent; 72 private int mTransportControlFlags; 73 private int mCurrentPlayState; 74 private AudioManager mAudioManager; 75 private IRemoteControlDisplayWeak mIRCD; 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 mMetadata.bitmap = (Bitmap) msg.obj; 103 KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground( 104 mMetadata.bitmap); 105 } 106 break; 107 108 case MSG_SET_GENERATION_ID: 109 if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2); 110 mClientGeneration = msg.arg1; 111 mClientIntent = (PendingIntent) msg.obj; 112 break; 113 114 } 115 } 116 }; 117 118 /** 119 * This class is required to have weak linkage to the current TransportControlView 120 * because the remote process can hold a strong reference to this binder object and 121 * we can't predict when it will be GC'd in the remote process. Without this code, it 122 * would allow a heavyweight object to be held on this side of the binder when there's 123 * no requirement to run a GC on the other side. 124 */ 125 private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub { 126 private WeakReference<Handler> mLocalHandler; 127 128 IRemoteControlDisplayWeak(Handler handler) { 129 mLocalHandler = new WeakReference<Handler>(handler); 130 } 131 132 public void setPlaybackState(int generationId, int state, long stateChangeTimeMs, 133 long currentPosMs, float speed) { 134 Handler handler = mLocalHandler.get(); 135 if (handler != null) { 136 handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget(); 137 } 138 } 139 140 public void setMetadata(int generationId, Bundle metadata) { 141 Handler handler = mLocalHandler.get(); 142 if (handler != null) { 143 handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); 144 } 145 } 146 147 public void setTransportControlInfo(int generationId, int flags, int posCapabilities) { 148 Handler handler = mLocalHandler.get(); 149 if (handler != null) { 150 handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags) 151 .sendToTarget(); 152 } 153 } 154 155 public void setArtwork(int generationId, Bitmap bitmap) { 156 Handler handler = mLocalHandler.get(); 157 if (handler != null) { 158 handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); 159 } 160 } 161 162 public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) { 163 Handler handler = mLocalHandler.get(); 164 if (handler != null) { 165 handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); 166 handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); 167 } 168 } 169 170 public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent, 171 boolean clearing) throws RemoteException { 172 Handler handler = mLocalHandler.get(); 173 if (handler != null) { 174 handler.obtainMessage(MSG_SET_GENERATION_ID, 175 clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget(); 176 } 177 } 178 }; 179 180 public KeyguardTransportControlView(Context context, AttributeSet attrs) { 181 super(context, attrs); 182 if (DEBUG) Log.v(TAG, "Create TCV " + this); 183 mAudioManager = new AudioManager(mContext); 184 mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback 185 mIRCD = new IRemoteControlDisplayWeak(mHandler); 186 } 187 188 private void updateTransportControls(int transportControlFlags) { 189 mTransportControlFlags = transportControlFlags; 190 } 191 192 @Override 193 public void onFinishInflate() { 194 super.onFinishInflate(); 195 mTrackTitle = (TextView) findViewById(R.id.title); 196 mTrackTitle.setSelected(true); // enable marquee 197 mAlbumArt = (ImageView) findViewById(R.id.albumart); 198 mBtnPrev = (ImageView) findViewById(R.id.btn_prev); 199 mBtnPlay = (ImageView) findViewById(R.id.btn_play); 200 mBtnNext = (ImageView) findViewById(R.id.btn_next); 201 final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; 202 for (View view : buttons) { 203 view.setOnClickListener(this); 204 } 205 } 206 207 @Override 208 public void onAttachedToWindow() { 209 super.onAttachedToWindow(); 210 if (DEBUG) Log.v(TAG, "onAttachToWindow()"); 211 if (mPopulateMetadataWhenAttached != null) { 212 updateMetadata(mPopulateMetadataWhenAttached); 213 mPopulateMetadataWhenAttached = null; 214 } 215 if (!mAttached) { 216 if (DEBUG) Log.v(TAG, "Registering TCV " + this); 217 mAudioManager.registerRemoteControlDisplay(mIRCD); 218 } 219 mAttached = true; 220 } 221 222 @Override 223 protected void onSizeChanged (int w, int h, int oldw, int oldh) { 224 if (mAttached) { 225 final DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); 226 int dim = Math.max(dm.widthPixels, dm.heightPixels); 227 if (DEBUG) Log.v(TAG, "TCV uses bitmap size=" + dim); 228 mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim); 229 } 230 } 231 232 @Override 233 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 243 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 KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground( 304 mMetadata.bitmap); 305 final int flags = mTransportControlFlags; 306 setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); 307 setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT); 308 setVisibilityBasedOnFlag(mBtnPlay, flags, 309 RemoteControlClient.FLAG_KEY_MEDIA_PLAY 310 | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE 311 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE 312 | RemoteControlClient.FLAG_KEY_MEDIA_STOP); 313 314 updatePlayPauseState(mCurrentPlayState); 315 } 316 317 private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { 318 if ((flags & flag) != 0) { 319 view.setVisibility(View.VISIBLE); 320 } else { 321 view.setVisibility(View.GONE); 322 } 323 } 324 325 private void updatePlayPauseState(int state) { 326 if (DEBUG) Log.v(TAG, 327 "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state); 328 if (state == mCurrentPlayState) { 329 return; 330 } 331 final int imageResId; 332 final int imageDescId; 333 switch (state) { 334 case RemoteControlClient.PLAYSTATE_ERROR: 335 imageResId = R.drawable.stat_sys_warning; 336 // TODO use more specific image description string for warning, but here the "play" 337 // message is still valid because this button triggers a play command. 338 imageDescId = R.string.keyguard_transport_play_description; 339 break; 340 341 case RemoteControlClient.PLAYSTATE_PLAYING: 342 imageResId = R.drawable.ic_media_pause; 343 imageDescId = R.string.keyguard_transport_pause_description; 344 break; 345 346 case RemoteControlClient.PLAYSTATE_BUFFERING: 347 imageResId = R.drawable.ic_media_stop; 348 imageDescId = R.string.keyguard_transport_stop_description; 349 break; 350 351 case RemoteControlClient.PLAYSTATE_PAUSED: 352 default: 353 imageResId = R.drawable.ic_media_play; 354 imageDescId = R.string.keyguard_transport_play_description; 355 break; 356 } 357 mBtnPlay.setImageResource(imageResId); 358 mBtnPlay.setContentDescription(getResources().getString(imageDescId)); 359 mCurrentPlayState = state; 360 } 361 362 static class SavedState extends BaseSavedState { 363 boolean clientPresent; 364 365 SavedState(Parcelable superState) { 366 super(superState); 367 } 368 369 private SavedState(Parcel in) { 370 super(in); 371 this.clientPresent = in.readInt() != 0; 372 } 373 374 @Override 375 public void writeToParcel(Parcel out, int flags) { 376 super.writeToParcel(out, flags); 377 out.writeInt(this.clientPresent ? 1 : 0); 378 } 379 380 public static final Parcelable.Creator<SavedState> CREATOR 381 = new Parcelable.Creator<SavedState>() { 382 public SavedState createFromParcel(Parcel in) { 383 return new SavedState(in); 384 } 385 386 public SavedState[] newArray(int size) { 387 return new SavedState[size]; 388 } 389 }; 390 } 391 392 public void onClick(View v) { 393 int keyCode = -1; 394 if (v == mBtnPrev) { 395 keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; 396 } else if (v == mBtnNext) { 397 keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; 398 } else if (v == mBtnPlay) { 399 keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; 400 401 } 402 if (keyCode != -1) { 403 sendMediaButtonClick(keyCode); 404 } 405 } 406 407 private void sendMediaButtonClick(int keyCode) { 408 if (mClientIntent == null) { 409 // Shouldn't be possible because this view should be hidden in this case. 410 Log.e(TAG, "sendMediaButtonClick(): No client is currently registered"); 411 return; 412 } 413 // use the registered PendingIntent that will be processed by the registered 414 // media button event receiver, which is the component of mClientIntent 415 KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); 416 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 417 intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); 418 try { 419 mClientIntent.send(getContext(), 0, intent); 420 } catch (CanceledException e) { 421 Log.e(TAG, "Error sending intent for media button down: "+e); 422 e.printStackTrace(); 423 } 424 425 keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); 426 intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 427 intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); 428 try { 429 mClientIntent.send(getContext(), 0, intent); 430 } catch (CanceledException e) { 431 Log.e(TAG, "Error sending intent for media button up: "+e); 432 e.printStackTrace(); 433 } 434 } 435 436 public boolean providesClock() { 437 return false; 438 } 439 440 private boolean wasPlayingRecently(int state, long stateChangeTimeMs) { 441 switch (state) { 442 case RemoteControlClient.PLAYSTATE_PLAYING: 443 case RemoteControlClient.PLAYSTATE_FAST_FORWARDING: 444 case RemoteControlClient.PLAYSTATE_REWINDING: 445 case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS: 446 case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS: 447 case RemoteControlClient.PLAYSTATE_BUFFERING: 448 // actively playing or about to play 449 return true; 450 case RemoteControlClient.PLAYSTATE_NONE: 451 return false; 452 case RemoteControlClient.PLAYSTATE_STOPPED: 453 case RemoteControlClient.PLAYSTATE_PAUSED: 454 case RemoteControlClient.PLAYSTATE_ERROR: 455 // we have stopped playing, check how long ago 456 if (DEBUG) { 457 if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) { 458 Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently"); 459 } else { 460 Log.v(TAG, "wasPlayingRecently: time > TIMEOUT"); 461 } 462 } 463 return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS); 464 default: 465 Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()"); 466 return false; 467 } 468 } 469} 470