TransportControlView.java revision 054340d0a3f242efeaf898cca38625bdcb3b4b5a
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.content.ComponentName; 25import android.content.Context; 26import android.content.Intent; 27import android.graphics.Bitmap; 28import android.media.AudioManager; 29import android.media.MediaMetadataRetriever; 30import android.media.RemoteControlClient; 31import android.media.IRemoteControlDisplay; 32import android.os.Bundle; 33import android.os.Handler; 34import android.os.Message; 35import android.os.RemoteException; 36import android.text.Spannable; 37import android.text.TextUtils; 38import android.text.style.ForegroundColorSpan; 39import android.util.AttributeSet; 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 48 49import com.android.internal.R; 50 51public class TransportControlView extends FrameLayout implements OnClickListener, 52 LockScreenWidgetInterface { 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 MAXDIM = 512; 60 protected static final boolean DEBUG = true; 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 ComponentName mClientName; 72 private int mTransportControlFlags; 73 private int mPlayState; 74 private AudioManager mAudioManager; 75 private LockScreenWidgetCallback mWidgetCallbacks; 76 private IRemoteControlDisplayWeak mIRCD; 77 78 /** 79 * The metadata which should be populated into the view once we've been attached 80 */ 81 private Bundle mPopulateMetadataWhenAttached = null; 82 83 // This handler is required to ensure messages from IRCD are handled in sequence and on 84 // the UI thread. 85 private Handler mHandler = new Handler() { 86 @Override 87 public void handleMessage(Message msg) { 88 switch (msg.what) { 89 case MSG_UPDATE_STATE: 90 if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2); 91 break; 92 93 case MSG_SET_METADATA: 94 if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj); 95 break; 96 97 case MSG_SET_TRANSPORT_CONTROLS: 98 if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2); 99 break; 100 101 case MSG_SET_ARTWORK: 102 if (mClientGeneration == msg.arg1) { 103 mMetadata.bitmap = (Bitmap) msg.obj; 104 mAlbumArt.setImageBitmap(mMetadata.bitmap); 105 } 106 break; 107 108 case MSG_SET_GENERATION_ID: 109 if (mWidgetCallbacks != null) { 110 boolean clearing = msg.arg2 != 0; 111 if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + clearing); 112 if (!clearing) { 113 mWidgetCallbacks.requestShow(TransportControlView.this); 114 } else { 115 mWidgetCallbacks.requestHide(TransportControlView.this); 116 } 117 } 118 mClientGeneration = msg.arg1; 119 mClientName = (ComponentName) msg.obj; 120 break; 121 122 } 123 } 124 }; 125 126 /** 127 * This class is required to have weak linkage to the current TransportControlView 128 * because the remote process can hold a strong reference to this binder object and 129 * we can't predict when it will be GC'd in the remote process. Without this code, it 130 * would allow a heavyweight object to be held on this side of the binder when there's 131 * no requirement to run a GC on the other side. 132 */ 133 private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub { 134 private WeakReference<Handler> mLocalHandler; 135 136 IRemoteControlDisplayWeak(Handler handler) { 137 mLocalHandler = new WeakReference<Handler>(handler); 138 } 139 140 public void setPlaybackState(int generationId, int state) { 141 Handler handler = mLocalHandler.get(); 142 if (handler != null) { 143 handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget(); 144 } 145 } 146 147 public void setMetadata(int generationId, Bundle metadata) { 148 Handler handler = mLocalHandler.get(); 149 if (handler != null) { 150 handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); 151 } 152 } 153 154 public void setTransportControlFlags(int generationId, int flags) { 155 Handler handler = mLocalHandler.get(); 156 if (handler != null) { 157 handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags) 158 .sendToTarget(); 159 } 160 } 161 162 public void setArtwork(int generationId, Bitmap bitmap) { 163 Handler handler = mLocalHandler.get(); 164 if (handler != null) { 165 handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); 166 } 167 } 168 169 public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) { 170 Handler handler = mLocalHandler.get(); 171 if (handler != null) { 172 handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); 173 handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); 174 } 175 } 176 177 public void setCurrentClientId(int clientGeneration, ComponentName clientEventReceiver, 178 boolean clearing) throws RemoteException { 179 Handler handler = mLocalHandler.get(); 180 if (handler != null) { 181 handler.obtainMessage(MSG_SET_GENERATION_ID, 182 clientGeneration, (clearing ? 1 : 0), clientEventReceiver).sendToTarget(); 183 } 184 } 185 }; 186 187 public TransportControlView(Context context, AttributeSet attrs) { 188 super(context, attrs); 189 Log.v(TAG, "Create TCV " + this); 190 mAudioManager = new AudioManager(mContext); 191 mIRCD = new IRemoteControlDisplayWeak(mHandler); 192 } 193 194 private void updateTransportControls(int transportControlFlags) { 195 mTransportControlFlags = transportControlFlags; 196 } 197 198 @Override 199 public void onFinishInflate() { 200 super.onFinishInflate(); 201 mTrackTitle = (TextView) findViewById(R.id.title); 202 mTrackTitle.setSelected(true); // enable marquee 203 mAlbumArt = (ImageView) findViewById(R.id.albumart); 204 mBtnPrev = (ImageView) findViewById(R.id.btn_prev); 205 mBtnPlay = (ImageView) findViewById(R.id.btn_play); 206 mBtnNext = (ImageView) findViewById(R.id.btn_next); 207 final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; 208 for (View view : buttons) { 209 view.setOnClickListener(this); 210 } 211 } 212 213 @Override 214 public void onAttachedToWindow() { 215 super.onAttachedToWindow(); 216 if (mPopulateMetadataWhenAttached != null) { 217 updateMetadata(mPopulateMetadataWhenAttached); 218 mPopulateMetadataWhenAttached = null; 219 } 220 if (!mAttached) { 221 if (DEBUG) Log.v(TAG, "Registering TCV " + this); 222 mAudioManager.registerRemoteControlDisplay(mIRCD); 223 } 224 mAttached = true; 225 } 226 227 @Override 228 public void onDetachedFromWindow() { 229 super.onDetachedFromWindow(); 230 if (mAttached) { 231 if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); 232 mAudioManager.unregisterRemoteControlDisplay(mIRCD); 233 } 234 mAttached = false; 235 } 236 237 @Override 238 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 239 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 240 int dim = Math.min(MAXDIM, Math.max(getWidth(), getHeight())); 241// Log.v(TAG, "setting max bitmap size: " + dim + "x" + dim); 242// mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim); 243 } 244 245 class Metadata { 246 private String artist; 247 private String trackTitle; 248 private String albumTitle; 249 private Bitmap bitmap; 250 251 public String toString() { 252 return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]"; 253 } 254 } 255 256 private String getMdString(Bundle data, int id) { 257 return data.getString(Integer.toString(id)); 258 } 259 260 private void updateMetadata(Bundle data) { 261 if (mAttached) { 262 mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST); 263 mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE); 264 mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM); 265 populateMetadata(); 266 } else { 267 mPopulateMetadataWhenAttached = data; 268 } 269 } 270 271 /** 272 * Populates the given metadata into the view 273 */ 274 private void populateMetadata() { 275 StringBuilder sb = new StringBuilder(); 276 int trackTitleLength = 0; 277 if (!TextUtils.isEmpty(mMetadata.trackTitle)) { 278 sb.append(mMetadata.trackTitle); 279 trackTitleLength = mMetadata.trackTitle.length(); 280 } 281 if (!TextUtils.isEmpty(mMetadata.artist)) { 282 if (sb.length() != 0) { 283 sb.append(" - "); 284 } 285 sb.append(mMetadata.artist); 286 } 287 if (!TextUtils.isEmpty(mMetadata.albumTitle)) { 288 if (sb.length() != 0) { 289 sb.append(" - "); 290 } 291 sb.append(mMetadata.albumTitle); 292 } 293 mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE); 294 Spannable str = (Spannable) mTrackTitle.getText(); 295 if (trackTitleLength != 0) { 296 str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength, 297 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 298 trackTitleLength++; 299 } 300 if (sb.length() > trackTitleLength) { 301 str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(), 302 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 303 } 304 305 mAlbumArt.setImageBitmap(mMetadata.bitmap); 306 final int flags = mTransportControlFlags; 307 setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); 308 setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT); 309 setVisibilityBasedOnFlag(mBtnPrev, flags, 310 RemoteControlClient.FLAG_KEY_MEDIA_PLAY 311 | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE 312 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE 313 | RemoteControlClient.FLAG_KEY_MEDIA_STOP); 314 315 updatePlayPauseState(mPlayState); 316 } 317 318 private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { 319 if ((flags & flag) != 0) { 320 view.setVisibility(View.VISIBLE); 321 } else { 322 view.setVisibility(View.GONE); 323 } 324 } 325 326 private void updatePlayPauseState(int state) { 327 if (DEBUG) Log.v(TAG, 328 "updatePlayPauseState(), old=" + mPlayState + ", state=" + state); 329 if (state == mPlayState) { 330 return; 331 } 332 switch (state) { 333 case RemoteControlClient.PLAYSTATE_PLAYING: 334 mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_pause); 335 break; 336 337 case RemoteControlClient.PLAYSTATE_BUFFERING: 338 mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_stop); 339 break; 340 341 case RemoteControlClient.PLAYSTATE_PAUSED: 342 default: 343 mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_play); 344 break; 345 } 346 mPlayState = state; 347 } 348 349 public void onClick(View v) { 350 int keyCode = -1; 351 if (v == mBtnPrev) { 352 keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; 353 } else if (v == mBtnNext) { 354 keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; 355 } else if (v == mBtnPlay) { 356 keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; 357 358 } 359 if (keyCode != -1) { 360 sendMediaButtonClick(keyCode); 361 if (mWidgetCallbacks != null) { 362 mWidgetCallbacks.userActivity(this); 363 } 364 } 365 } 366 367 private void sendMediaButtonClick(int keyCode) { 368 // TODO: target to specific player based on mClientName 369 KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); 370 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 371 intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); 372 getContext().sendOrderedBroadcast(intent, null); 373 374 keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); 375 intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 376 intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); 377 getContext().sendOrderedBroadcast(intent, null); 378 } 379 380 public void setCallback(LockScreenWidgetCallback callback) { 381 mWidgetCallbacks = callback; 382 } 383 384 public boolean providesClock() { 385 return false; 386 } 387 388} 389