| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.bluetooth.a2dp; |
| |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.media.AudioManager; |
| import android.media.IRemoteControlDisplay; |
| import android.media.MediaMetadataRetriever; |
| import android.media.RemoteControlClient; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.ParcelUuid; |
| import android.os.PowerManager; |
| import android.os.PowerManager.WakeLock; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.SystemClock; |
| import android.util.Log; |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.btservice.ProfileService; |
| import com.android.bluetooth.Utils; |
| import com.android.internal.util.IState; |
| import com.android.internal.util.State; |
| import com.android.internal.util.StateMachine; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * support Bluetooth AVRCP profile. |
| * support metadata, play status and event notification |
| */ |
| final class Avrcp { |
| private static final boolean DEBUG = false; |
| private static final String TAG = "Avrcp"; |
| |
| private Context mContext; |
| private final AudioManager mAudioManager; |
| private AvrcpMessageHandler mHandler; |
| private IRemoteControlDisplayWeak mRemoteControlDisplay; |
| private int mClientGeneration; |
| private Metadata mMetadata; |
| private int mTransportControlFlags; |
| private int mCurrentPlayState; |
| private int mPlayStatusChangedNT; |
| private int mTrackChangedNT; |
| private long mTrackNumber; |
| private long mCurrentPosMs; |
| private long mPlayStartTimeMs; |
| private long mSongLengthMs; |
| private long mPlaybackIntervalMs; |
| private int mPlayPosChangedNT; |
| private long mNextPosMs; |
| private long mPrevPosMs; |
| |
| private static final int MESSAGE_GET_PLAY_STATUS = 1; |
| private static final int MESSAGE_GET_ELEM_ATTRS = 2; |
| private static final int MESSAGE_REGISTER_NOTIFICATION = 3; |
| private static final int MESSAGE_PLAY_INTERVAL_TIMEOUT = 4; |
| private static final int MSG_UPDATE_STATE = 100; |
| private static final int MSG_SET_METADATA = 101; |
| private static final int MSG_SET_TRANSPORT_CONTROLS = 102; |
| private static final int MSG_SET_ARTWORK = 103; |
| private static final int MSG_SET_GENERATION_ID = 104; |
| |
| static { |
| classInitNative(); |
| } |
| |
| private Avrcp(Context context) { |
| mMetadata = new Metadata(); |
| mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback |
| mPlayStatusChangedNT = NOTIFICATION_TYPE_CHANGED; |
| mTrackChangedNT = NOTIFICATION_TYPE_CHANGED; |
| mTrackNumber = -1L; |
| mCurrentPosMs = RemoteControlClient.PLAYBACK_POSITION_INVALID; |
| mPlayStartTimeMs = -1L; |
| mSongLengthMs = 0L; |
| mPlaybackIntervalMs = 0L; |
| mPlayPosChangedNT = NOTIFICATION_TYPE_CHANGED; |
| |
| mContext = context; |
| |
| initNative(); |
| |
| mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| } |
| |
| private void start() { |
| HandlerThread thread = new HandlerThread("BluetoothAvrcpHandler"); |
| thread.start(); |
| Looper looper = thread.getLooper(); |
| mHandler = new AvrcpMessageHandler(looper); |
| mRemoteControlDisplay = new IRemoteControlDisplayWeak(mHandler); |
| mAudioManager.registerRemoteControlDisplay(mRemoteControlDisplay); |
| mAudioManager.remoteControlDisplayWantsPlaybackPositionSync( |
| mRemoteControlDisplay, true); |
| } |
| |
| static Avrcp make(Context context) { |
| if (DEBUG) Log.v(TAG, "make"); |
| Avrcp ar = new Avrcp(context); |
| ar.start(); |
| return ar; |
| } |
| |
| public void doQuit() { |
| mHandler.removeCallbacksAndMessages(null); |
| Looper looper = mHandler.getLooper(); |
| if (looper != null) { |
| looper.quit(); |
| } |
| mAudioManager.unregisterRemoteControlDisplay(mRemoteControlDisplay); |
| } |
| |
| public void cleanup() { |
| cleanupNative(); |
| } |
| |
| private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub { |
| private WeakReference<Handler> mLocalHandler; |
| IRemoteControlDisplayWeak(Handler handler) { |
| mLocalHandler = new WeakReference<Handler>(handler); |
| } |
| |
| @Override |
| public void setPlaybackState(int generationId, int state, long stateChangeTimeMs, |
| long currentPosMs, float speed) { |
| Handler handler = mLocalHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(MSG_UPDATE_STATE, generationId, state, |
| new Long(currentPosMs)).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void setMetadata(int generationId, Bundle metadata) { |
| Handler handler = mLocalHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void setTransportControlInfo(int generationId, int flags, int posCapabilities) { |
| Handler handler = mLocalHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags) |
| .sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void setArtwork(int generationId, Bitmap bitmap) { |
| } |
| |
| @Override |
| public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) { |
| Handler handler = mLocalHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); |
| handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent, |
| boolean clearing) throws RemoteException { |
| Handler handler = mLocalHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(MSG_SET_GENERATION_ID, |
| clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget(); |
| } |
| } |
| } |
| |
| /** Handles Avrcp messages. */ |
| private final class AvrcpMessageHandler extends Handler { |
| private AvrcpMessageHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_UPDATE_STATE: |
| if (mClientGeneration == msg.arg1) { |
| updatePlayPauseState(msg.arg2, ((Long)msg.obj).longValue()); |
| } |
| break; |
| |
| case MSG_SET_METADATA: |
| if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj); |
| break; |
| |
| case MSG_SET_TRANSPORT_CONTROLS: |
| if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2); |
| break; |
| |
| case MSG_SET_ARTWORK: |
| if (mClientGeneration == msg.arg1) { |
| } |
| break; |
| |
| case MSG_SET_GENERATION_ID: |
| if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2); |
| mClientGeneration = msg.arg1; |
| break; |
| |
| case MESSAGE_GET_PLAY_STATUS: |
| if (DEBUG) Log.v(TAG, "MESSAGE_GET_PLAY_STATUS"); |
| getPlayStatusRspNative(convertPlayStateToPlayStatus(mCurrentPlayState), |
| (int)mSongLengthMs, (int)getPlayPosition()); |
| break; |
| |
| case MESSAGE_GET_ELEM_ATTRS: |
| { |
| String[] textArray; |
| int[] attrIds; |
| byte numAttr = (byte) msg.arg1; |
| ArrayList<Integer> attrList = (ArrayList<Integer>) msg.obj; |
| if (DEBUG) Log.v(TAG, "MESSAGE_GET_ELEM_ATTRS:numAttr=" + numAttr); |
| attrIds = new int[numAttr]; |
| textArray = new String[numAttr]; |
| for (int i = 0; i < numAttr; ++i) { |
| attrIds[i] = attrList.get(i).intValue(); |
| textArray[i] = getAttributeString(attrIds[i]); |
| } |
| getElementAttrRspNative(numAttr, attrIds, textArray); |
| break; |
| } |
| case MESSAGE_REGISTER_NOTIFICATION: |
| if (DEBUG) Log.v(TAG, "MESSAGE_REGISTER_NOTIFICATION:event=" + msg.arg1 + |
| " param=" + msg.arg2); |
| processRegisterNotification(msg.arg1, msg.arg2); |
| break; |
| |
| case MESSAGE_PLAY_INTERVAL_TIMEOUT: |
| if (DEBUG) Log.v(TAG, "MESSAGE_PLAY_INTERVAL_TIMEOUT"); |
| mPlayPosChangedNT = NOTIFICATION_TYPE_CHANGED; |
| registerNotificationRspPlayPosNative(mPlayPosChangedNT, (int)getPlayPosition()); |
| break; |
| |
| } |
| } |
| } |
| |
| private void updatePlayPauseState(int state, long currentPosMs) { |
| if (DEBUG) Log.v(TAG, |
| "updatePlayPauseState, old=" + mCurrentPlayState + ", state=" + state); |
| boolean oldPosValid = (mCurrentPosMs != RemoteControlClient.PLAYBACK_POSITION_INVALID); |
| boolean newPosValid = (currentPosMs != RemoteControlClient.PLAYBACK_POSITION_INVALID); |
| int oldPlayStatus = convertPlayStateToPlayStatus(mCurrentPlayState); |
| int newPlayStatus = convertPlayStateToPlayStatus(state); |
| mCurrentPlayState = state; |
| mCurrentPosMs = currentPosMs; |
| if (state == RemoteControlClient.PLAYSTATE_PLAYING) { |
| mPlayStartTimeMs = SystemClock.elapsedRealtime(); |
| } |
| |
| mHandler.removeMessages(MESSAGE_PLAY_INTERVAL_TIMEOUT); |
| /* need send play position changed notification when play status is changed */ |
| if ((mPlayPosChangedNT == NOTIFICATION_TYPE_INTERIM) && |
| ((oldPlayStatus != newPlayStatus) || (oldPosValid != newPosValid) || |
| (newPosValid && ((mCurrentPosMs >= mNextPosMs) || (mCurrentPosMs <= mPrevPosMs))))) { |
| mPlayPosChangedNT = NOTIFICATION_TYPE_CHANGED; |
| registerNotificationRspPlayPosNative(mPlayPosChangedNT, (int)getPlayPosition()); |
| } |
| if ((mPlayPosChangedNT == NOTIFICATION_TYPE_INTERIM) && newPosValid && |
| (state == RemoteControlClient.PLAYSTATE_PLAYING)) { |
| Message msg = mHandler.obtainMessage(MESSAGE_PLAY_INTERVAL_TIMEOUT); |
| mHandler.sendMessageDelayed(msg, mNextPosMs - mCurrentPosMs); |
| } |
| |
| if ((mPlayStatusChangedNT == NOTIFICATION_TYPE_INTERIM) && (oldPlayStatus != newPlayStatus)) { |
| mPlayStatusChangedNT = NOTIFICATION_TYPE_CHANGED; |
| registerNotificationRspPlayStatusNative(mPlayStatusChangedNT, newPlayStatus); |
| } |
| } |
| |
| private void updateTransportControls(int transportControlFlags) { |
| mTransportControlFlags = transportControlFlags; |
| } |
| |
| class Metadata { |
| private String artist; |
| private String trackTitle; |
| private String albumTitle; |
| |
| public Metadata() { |
| artist = null; |
| trackTitle = null; |
| albumTitle = null; |
| } |
| |
| public String toString() { |
| return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + |
| albumTitle + "]"; |
| } |
| } |
| |
| private String getMdString(Bundle data, int id) { |
| return data.getString(Integer.toString(id)); |
| } |
| |
| private long getMdLong(Bundle data, int id) { |
| return data.getLong(Integer.toString(id)); |
| } |
| |
| private void updateMetadata(Bundle data) { |
| String oldMetadata = mMetadata.toString(); |
| mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST); |
| mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE); |
| mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM); |
| if (!oldMetadata.equals(mMetadata.toString())) { |
| mTrackNumber++; |
| if (mTrackChangedNT == NOTIFICATION_TYPE_INTERIM) { |
| mTrackChangedNT = NOTIFICATION_TYPE_CHANGED; |
| sendTrackChangedRsp(); |
| } |
| |
| if (mCurrentPosMs != RemoteControlClient.PLAYBACK_POSITION_INVALID) { |
| mCurrentPosMs = 0L; |
| if (mCurrentPlayState == RemoteControlClient.PLAYSTATE_PLAYING) { |
| mPlayStartTimeMs = SystemClock.elapsedRealtime(); |
| } |
| } |
| /* need send play position changed notification when track is changed */ |
| if (mPlayPosChangedNT == NOTIFICATION_TYPE_INTERIM) { |
| mPlayPosChangedNT = NOTIFICATION_TYPE_CHANGED; |
| registerNotificationRspPlayPosNative(mPlayPosChangedNT, |
| (int)getPlayPosition()); |
| mHandler.removeMessages(MESSAGE_PLAY_INTERVAL_TIMEOUT); |
| } |
| } |
| if (DEBUG) Log.v(TAG, "mMetadata=" + mMetadata.toString()); |
| |
| mSongLengthMs = getMdLong(data, MediaMetadataRetriever.METADATA_KEY_DURATION); |
| if (DEBUG) Log.v(TAG, "duration=" + mSongLengthMs); |
| } |
| |
| private void getPlayStatus() { |
| Message msg = mHandler.obtainMessage(MESSAGE_GET_PLAY_STATUS); |
| mHandler.sendMessage(msg); |
| } |
| |
| private void getElementAttr(byte numAttr, int[] attrs) { |
| int i; |
| ArrayList<Integer> attrList = new ArrayList<Integer>(); |
| for (i = 0; i < numAttr; ++i) { |
| attrList.add(attrs[i]); |
| } |
| Message msg = mHandler.obtainMessage(MESSAGE_GET_ELEM_ATTRS, (int)numAttr, 0, attrList); |
| mHandler.sendMessage(msg); |
| } |
| |
| private void registerNotification(int eventId, int param) { |
| Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_NOTIFICATION, eventId, param); |
| mHandler.sendMessage(msg); |
| } |
| |
| private void processRegisterNotification(int eventId, int param) { |
| switch (eventId) { |
| case EVT_PLAY_STATUS_CHANGED: |
| mPlayStatusChangedNT = NOTIFICATION_TYPE_INTERIM; |
| registerNotificationRspPlayStatusNative(mPlayStatusChangedNT, |
| convertPlayStateToPlayStatus(mCurrentPlayState)); |
| break; |
| |
| case EVT_TRACK_CHANGED: |
| mTrackChangedNT = NOTIFICATION_TYPE_INTERIM; |
| sendTrackChangedRsp(); |
| break; |
| |
| case EVT_PLAY_POS_CHANGED: |
| long songPosition = getPlayPosition(); |
| mPlayPosChangedNT = NOTIFICATION_TYPE_INTERIM; |
| mPlaybackIntervalMs = (long)param * 1000L; |
| if (mCurrentPosMs != RemoteControlClient.PLAYBACK_POSITION_INVALID) { |
| mNextPosMs = songPosition + mPlaybackIntervalMs; |
| mPrevPosMs = songPosition - mPlaybackIntervalMs; |
| if (mCurrentPlayState == RemoteControlClient.PLAYSTATE_PLAYING) { |
| Message msg = mHandler.obtainMessage(MESSAGE_PLAY_INTERVAL_TIMEOUT); |
| mHandler.sendMessageDelayed(msg, mPlaybackIntervalMs); |
| } |
| } |
| registerNotificationRspPlayPosNative(mPlayPosChangedNT, (int)songPosition); |
| break; |
| |
| } |
| } |
| |
| private void sendTrackChangedRsp() { |
| byte[] track = new byte[TRACK_ID_SIZE]; |
| /* track is stored in big endian format */ |
| for (int i = 0; i < TRACK_ID_SIZE; ++i) { |
| track[i] = (byte) (mTrackNumber >> (56 - 8 * i)); |
| } |
| registerNotificationRspTrackChangeNative(mTrackChangedNT, track); |
| } |
| |
| private long getPlayPosition() { |
| long songPosition = -1L; |
| if (mCurrentPosMs != RemoteControlClient.PLAYBACK_POSITION_INVALID) { |
| if (mCurrentPlayState == RemoteControlClient.PLAYSTATE_PLAYING) { |
| songPosition = SystemClock.elapsedRealtime() - |
| mPlayStartTimeMs + mCurrentPosMs; |
| } else { |
| songPosition = mCurrentPosMs; |
| } |
| } |
| if (DEBUG) Log.v(TAG, "position=" + songPosition); |
| return songPosition; |
| } |
| |
| private String getAttributeString(int attrId) { |
| String attrStr = null; |
| switch (attrId) { |
| case MEDIA_ATTR_TITLE: |
| attrStr = mMetadata.trackTitle; |
| break; |
| |
| case MEDIA_ATTR_ARTIST: |
| attrStr = mMetadata.artist; |
| break; |
| |
| case MEDIA_ATTR_ALBUM: |
| attrStr = mMetadata.albumTitle; |
| break; |
| |
| case MEDIA_ATTR_PLAYING_TIME: |
| if (mSongLengthMs != 0L) { |
| attrStr = Long.toString(mSongLengthMs); |
| } |
| break; |
| |
| } |
| if (attrStr == null) { |
| attrStr = new String(); |
| } |
| if (DEBUG) Log.v(TAG, "getAttributeString:attrId=" + attrId + " str=" + attrStr); |
| return attrStr; |
| } |
| |
| private int convertPlayStateToPlayStatus(int playState) { |
| int playStatus = PLAYSTATUS_ERROR; |
| switch (playState) { |
| case RemoteControlClient.PLAYSTATE_PLAYING: |
| case RemoteControlClient.PLAYSTATE_BUFFERING: |
| playStatus = PLAYSTATUS_PLAYING; |
| break; |
| |
| case RemoteControlClient.PLAYSTATE_STOPPED: |
| case RemoteControlClient.PLAYSTATE_NONE: |
| playStatus = PLAYSTATUS_STOPPED; |
| break; |
| |
| case RemoteControlClient.PLAYSTATE_PAUSED: |
| playStatus = PLAYSTATUS_PAUSED; |
| break; |
| |
| case RemoteControlClient.PLAYSTATE_FAST_FORWARDING: |
| case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS: |
| playStatus = PLAYSTATUS_FWD_SEEK; |
| break; |
| |
| case RemoteControlClient.PLAYSTATE_REWINDING: |
| case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS: |
| playStatus = PLAYSTATUS_REV_SEEK; |
| break; |
| |
| case RemoteControlClient.PLAYSTATE_ERROR: |
| playStatus = PLAYSTATUS_ERROR; |
| break; |
| |
| } |
| return playStatus; |
| } |
| |
| // Do not modify without updating the HAL bt_rc.h files. |
| |
| // match up with btrc_play_status_t enum of bt_rc.h |
| final static int PLAYSTATUS_STOPPED = 0; |
| final static int PLAYSTATUS_PLAYING = 1; |
| final static int PLAYSTATUS_PAUSED = 2; |
| final static int PLAYSTATUS_FWD_SEEK = 3; |
| final static int PLAYSTATUS_REV_SEEK = 4; |
| final static int PLAYSTATUS_ERROR = 255; |
| |
| // match up with btrc_media_attr_t enum of bt_rc.h |
| final static int MEDIA_ATTR_TITLE = 1; |
| final static int MEDIA_ATTR_ARTIST = 2; |
| final static int MEDIA_ATTR_ALBUM = 3; |
| final static int MEDIA_ATTR_TRACK_NUM = 4; |
| final static int MEDIA_ATTR_NUM_TRACKS = 5; |
| final static int MEDIA_ATTR_GENRE = 6; |
| final static int MEDIA_ATTR_PLAYING_TIME = 7; |
| |
| // match up with btrc_event_id_t enum of bt_rc.h |
| final static int EVT_PLAY_STATUS_CHANGED = 1; |
| final static int EVT_TRACK_CHANGED = 2; |
| final static int EVT_TRACK_REACHED_END = 3; |
| final static int EVT_TRACK_REACHED_START = 4; |
| final static int EVT_PLAY_POS_CHANGED = 5; |
| final static int EVT_BATT_STATUS_CHANGED = 6; |
| final static int EVT_SYSTEM_STATUS_CHANGED = 7; |
| final static int EVT_APP_SETTINGS_CHANGED = 8; |
| |
| // match up with btrc_notification_type_t enum of bt_rc.h |
| final static int NOTIFICATION_TYPE_INTERIM = 0; |
| final static int NOTIFICATION_TYPE_CHANGED = 1; |
| |
| // match up with BTRC_UID_SIZE of bt_rc.h |
| final static int TRACK_ID_SIZE = 8; |
| |
| private native static void classInitNative(); |
| private native void initNative(); |
| private native void cleanupNative(); |
| private native boolean getPlayStatusRspNative(int playStatus, int songLen, int songPos); |
| private native boolean getElementAttrRspNative(byte numAttr, int[] attrIds, String[] textArray); |
| private native boolean registerNotificationRspPlayStatusNative(int type, int playStatus); |
| private native boolean registerNotificationRspTrackChangeNative(int type, byte[] track); |
| private native boolean registerNotificationRspPlayPosNative(int type, int playPos); |
| } |