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