diff options
Diffstat (limited to 'src/com')
22 files changed, 710 insertions, 348 deletions
diff --git a/src/com/android/bluetooth/BluetoothPrefs.java b/src/com/android/bluetooth/BluetoothPrefs.java new file mode 100644 index 000000000..2c7c87aaa --- /dev/null +++ b/src/com/android/bluetooth/BluetoothPrefs.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +/** + * Activity that routes to Bluetooth settings when launched + */ +public class BluetoothPrefs extends Activity { + + public static final String BLUETOOTH_SETTING_ACTION = "android.settings.BLUETOOTH_SETTINGS"; + public static final String BLUETOOTH_SETTING_CATEGORY = "android.intent.category.DEFAULT"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent launchIntent = new Intent(); + launchIntent.setAction(BLUETOOTH_SETTING_ACTION); + launchIntent.addCategory(BLUETOOTH_SETTING_CATEGORY); + startActivity(launchIntent); + finish(); + } +} diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java index e41def075..5271cc791 100644 --- a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java +++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java @@ -413,7 +413,8 @@ public class A2dpSinkService extends ProfileService { if (state == StackEvent.AUDIO_STATE_STARTED) { mA2dpSinkStreamHandler.obtainMessage( A2dpSinkStreamHandler.SRC_STR_START).sendToTarget(); - } else if (state == StackEvent.AUDIO_STATE_STOPPED) { + } else if (state == StackEvent.AUDIO_STATE_STOPPED + || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) { mA2dpSinkStreamHandler.obtainMessage( A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget(); } diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java index 5e3b3567c..5aa3cbbd5 100644 --- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java +++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java @@ -17,6 +17,7 @@ package com.android.bluetooth.a2dpsink; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadsetClientCall; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; @@ -24,10 +25,9 @@ import android.media.AudioFocusRequest; import android.media.AudioManager; import android.media.AudioManager.OnAudioFocusChangeListener; import android.media.MediaPlayer; -import android.media.session.PlaybackState; - import android.os.Handler; import android.os.Message; +import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; import com.android.bluetooth.R; @@ -183,8 +183,9 @@ public class A2dpSinkStreamHandler extends Handler { break; case AUDIO_FOCUS_CHANGE: + mAudioFocus = (int) message.obj; // message.obj is the newly granted audio focus. - switch ((int) message.obj) { + switch (mAudioFocus) { case AudioManager.AUDIOFOCUS_GAIN: removeMessages(DELAYED_PAUSE); // Begin playing audio, if we paused the remote, send a play now. @@ -228,7 +229,7 @@ public class A2dpSinkStreamHandler extends Handler { case DELAYED_PAUSE: if (BluetoothMediaBrowserService.getPlaybackState() - == PlaybackState.STATE_PLAYING && !inCallFromStreamingDevice()) { + == PlaybackStateCompat.STATE_PLAYING && !inCallFromStreamingDevice()) { sendAvrcpPause(); mSentPause = true; mStreamAvailable = false; @@ -245,12 +246,9 @@ public class A2dpSinkStreamHandler extends Handler { */ private void requestAudioFocusIfNone() { if (DBG) Log.d(TAG, "requestAudioFocusIfNone()"); - if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) { + if (mAudioFocus != AudioManager.AUDIOFOCUS_GAIN) { requestAudioFocus(); } - // On the off change mMediaPlayer errors out and dies, we want to make sure we retry this. - // This function immediately exits if we have a MediaPlayer object. - requestMediaKeyFocus(); } private synchronized int requestAudioFocus() { @@ -277,8 +275,11 @@ public class A2dpSinkStreamHandler extends Handler { } /** - * Creates a MediaPlayer that plays a silent audio sample so that MediaSessionService will be - * aware of the fact that Bluetooth is playing audio. + * Plays a silent audio sample so that MediaSessionService will be aware of the fact that + * Bluetooth is playing audio. + * + * Creates a new MediaPlayer if one does not already exist. Repeat calls to this function are + * safe and will result in the silent audio sample again. * * This allows the MediaSession in AVRCP Controller to be routed media key events, if we've * chosen to use it. @@ -286,25 +287,25 @@ public class A2dpSinkStreamHandler extends Handler { private synchronized void requestMediaKeyFocus() { if (DBG) Log.d(TAG, "requestMediaKeyFocus()"); - if (mMediaPlayer != null) return; - - AudioAttributes attrs = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .build(); - - mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent, attrs, - mAudioManager.generateAudioSessionId()); if (mMediaPlayer == null) { - Log.e(TAG, "Failed to initialize media player. You may not get media key events"); - return; - } + AudioAttributes attrs = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + + mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent, attrs, + mAudioManager.generateAudioSessionId()); + if (mMediaPlayer == null) { + Log.e(TAG, "Failed to initialize media player. You may not get media key events"); + return; + } - mMediaPlayer.setLooping(false); - mMediaPlayer.setOnErrorListener((mp, what, extra) -> { - Log.e(TAG, "Silent media player error: " + what + ", " + extra); - releaseMediaKeyFocus(); - return false; - }); + mMediaPlayer.setLooping(false); + mMediaPlayer.setOnErrorListener((mp, what, extra) -> { + Log.e(TAG, "Silent media player error: " + what + ", " + extra); + releaseMediaKeyFocus(); + return false; + }); + } mMediaPlayer.start(); BluetoothMediaBrowserService.setActive(true); @@ -313,7 +314,6 @@ public class A2dpSinkStreamHandler extends Handler { private synchronized void abandonAudioFocus() { if (DBG) Log.d(TAG, "abandonAudioFocus()"); stopFluorideStreaming(); - releaseMediaKeyFocus(); mAudioManager.abandonAudioFocus(mAudioFocusListener); mAudioFocus = AudioManager.AUDIOFOCUS_NONE; } @@ -336,9 +336,11 @@ public class A2dpSinkStreamHandler extends Handler { private void startFluorideStreaming() { mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_GRANTED); mA2dpSinkService.informAudioTrackGainNative(1.0f); + requestMediaKeyFocus(); } private void stopFluorideStreaming() { + releaseMediaKeyFocus(); mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_LOST); } @@ -362,7 +364,10 @@ public class A2dpSinkStreamHandler extends Handler { } HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService(); if (targetDevice != null && headsetClientService != null) { - return headsetClientService.getCurrentCalls(targetDevice).size() > 0; + List<BluetoothHeadsetClientCall> currentCalls = + headsetClientService.getCurrentCalls(targetDevice); + if (currentCalls == null) return false; + return currentCalls.size() > 0; } return false; } diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java index 64e63df00..56684cd7c 100644 --- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java +++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java @@ -172,9 +172,11 @@ public class AvrcpControllerService extends ProfileService { } } + // If we don't find a node in the tree then do not have any way to browse for the contents. + // Return an empty list instead. if (requestedNode == null) { if (DBG) Log.d(TAG, "Didn't find a node"); - return null; + return new ArrayList(0); } else { if (!requestedNode.isCached()) { if (DBG) Log.d(TAG, "node is not cached"); @@ -412,9 +414,10 @@ public class AvrcpControllerService extends ProfileService { if (stateMachine != null) { PlayerApplicationSettings supportedSettings = PlayerApplicationSettings.makeSupportedSettings(playerAttribRsp); + stateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS, + supportedSettings); } - /* Do nothing */ - } private synchronized void onPlayerAppSettingChanged(byte[] address, byte[] playerAttribRsp, @@ -426,10 +429,12 @@ public class AvrcpControllerService extends ProfileService { AvrcpControllerStateMachine stateMachine = getStateMachine(device); if (stateMachine != null) { - PlayerApplicationSettings desiredSettings = + PlayerApplicationSettings currentSettings = PlayerApplicationSettings.makeSettings(playerAttribRsp); + stateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS, + currentSettings); } - /* Do nothing */ } // Browsing related JNI callbacks. @@ -711,7 +716,7 @@ public class AvrcpControllerService extends ProfileService { /** * Send button press commands to addressed device * - * @param keyCode key code as defined in AVRCP specification + * @param keyCode key code as defined in AVRCP specification * @param keyState 0 = key pressed, 1 = key released * @return command was sent */ @@ -720,7 +725,7 @@ public class AvrcpControllerService extends ProfileService { /** * Send group navigation commands * - * @param keyCode next/previous + * @param keyCode next/previous * @param keyState state * @return command was sent */ @@ -741,7 +746,7 @@ public class AvrcpControllerService extends ProfileService { * Send response to set absolute volume * * @param absVol new volume - * @param label label + * @param label label */ public native void sendAbsVolRspNative(byte[] address, int absVol, int label); @@ -749,8 +754,8 @@ public class AvrcpControllerService extends ProfileService { * Register for any volume level changes * * @param rspType type of response - * @param absVol current volume - * @param label label + * @param absVol current volume + * @param label label */ public native void sendRegisterAbsVolRspNative(byte[] address, byte rspType, int absVol, int label); @@ -764,7 +769,7 @@ public class AvrcpControllerService extends ProfileService { * Fetch the current now playing list * * @param start first index to retrieve - * @param end last index to retrieve + * @param end last index to retrieve */ public native void getNowPlayingListNative(byte[] address, int start, int end); @@ -772,7 +777,7 @@ public class AvrcpControllerService extends ProfileService { * Fetch the current folder's listing * * @param start first index to retrieve - * @param end last index to retrieve + * @param end last index to retrieve */ public native void getFolderListNative(byte[] address, int start, int end); @@ -780,7 +785,7 @@ public class AvrcpControllerService extends ProfileService { * Fetch the listing of players * * @param start first index to retrieve - * @param end last index to retrieve + * @param end last index to retrieve */ public native void getPlayerListNative(byte[] address, int start, int end); @@ -788,15 +793,15 @@ public class AvrcpControllerService extends ProfileService { * Change the current browsed folder * * @param direction up/down - * @param uid folder unique id + * @param uid folder unique id */ public native void changeFolderPathNative(byte[] address, byte direction, long uid); /** * Play item with provided uid * - * @param scope scope of item to played - * @param uid song unique id + * @param scope scope of item to played + * @param uid song unique id * @param uidCounter counter */ public native void playItemNative(byte[] address, byte scope, long uid, int uidCounter); diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java index 66571c4e7..c319364c1 100644 --- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java +++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java @@ -24,10 +24,10 @@ import android.content.Intent; import android.media.AudioManager; import android.media.MediaMetadata; import android.media.browse.MediaBrowser.MediaItem; -import android.media.session.MediaSession; -import android.media.session.PlaybackState; import android.os.Bundle; import android.os.Message; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; import android.util.SparseArray; @@ -42,6 +42,7 @@ import com.android.internal.util.StateMachine; import java.util.ArrayList; import java.util.List; + /** * Provides Bluetooth AVRCP Controller State Machine responsible for all remote control connections * and interactions with a remote controlable device. @@ -76,11 +77,15 @@ class AvrcpControllerStateMachine extends StateMachine { static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 214; static final int MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED = 215; static final int MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED = 216; + static final int MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS = 217; + static final int MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS = 218; //300->399 Events for Browsing static final int MESSAGE_GET_FOLDER_ITEMS = 300; static final int MESSAGE_PLAY_ITEM = 301; static final int MSG_AVRCP_PASSTHRU = 302; + static final int MSG_AVRCP_SET_SHUFFLE = 303; + static final int MSG_AVRCP_SET_REPEAT = 304; static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 404; @@ -218,26 +223,19 @@ class AvrcpControllerStateMachine extends StateMachine { mService.sBrowseTree.mRootNode.addChild(mBrowseTree.mRootNode); BluetoothMediaBrowserService.notifyChanged(mService .sBrowseTree.mRootNode); - BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState()); mBrowsingConnected = true; } synchronized void onBrowsingDisconnected() { if (!mBrowsingConnected) return; - mAddressedPlayer.setPlayStatus(PlaybackState.STATE_ERROR); + mAddressedPlayer.setPlayStatus(PlaybackStateCompat.STATE_ERROR); mAddressedPlayer.updateCurrentTrack(null); mBrowseTree.mNowPlayingNode.setCached(false); BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode); - PlaybackState.Builder pbb = new PlaybackState.Builder(); - pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN, - 1.0f).setActions(0); - pbb.setErrorMessage(mService.getString(R.string.bluetooth_disconnected)); - BluetoothMediaBrowserService.notifyChanged(pbb.build()); mService.sBrowseTree.mRootNode.removeChild( mBrowseTree.mRootNode); BluetoothMediaBrowserService.notifyChanged(mService .sBrowseTree.mRootNode); - BluetoothMediaBrowserService.trackChanged(null); mBrowsingConnected = false; } @@ -298,8 +296,9 @@ class AvrcpControllerStateMachine extends StateMachine { @Override public void enter() { if (mMostRecentState == BluetoothProfile.STATE_CONNECTING) { - broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTED); BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks); + BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState()); + broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTED); } else { logD("ReEnteringConnected"); } @@ -315,14 +314,14 @@ class AvrcpControllerStateMachine extends StateMachine { removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT); sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT, ABS_VOL_TIMEOUT_MILLIS); - setAbsVolume(msg.arg1, msg.arg2); + handleAbsVolumeRequest(msg.arg1, msg.arg2); return true; case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: mVolumeNotificationLabel = msg.arg1; mService.sendRegisterAbsVolRspNative(mDeviceAddress, NOTIFICATION_RSP_TYPE_INTERIM, - getAbsVolumeResponse(), mVolumeNotificationLabel); + getAbsVolume(), mVolumeNotificationLabel); return true; case MESSAGE_GET_FOLDER_ITEMS: @@ -338,6 +337,14 @@ class AvrcpControllerStateMachine extends StateMachine { passThru(msg.arg1); return true; + case MSG_AVRCP_SET_REPEAT: + setRepeat(msg.arg1); + return true; + + case MSG_AVRCP_SET_SHUFFLE: + setShuffle(msg.arg1); + return true; + case MESSAGE_PROCESS_TRACK_CHANGED: mAddressedPlayer.updateCurrentTrack((MediaMetadata) msg.obj); BluetoothMediaBrowserService.trackChanged((MediaMetadata) msg.obj); @@ -347,11 +354,14 @@ class AvrcpControllerStateMachine extends StateMachine { mAddressedPlayer.setPlayStatus(msg.arg1); BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState()); if (mAddressedPlayer.getPlaybackState().getState() - == PlaybackState.STATE_PLAYING - && A2dpSinkService.getFocusState() == AudioManager.AUDIOFOCUS_NONE - && !shouldRequestFocus()) { + == PlaybackStateCompat.STATE_PLAYING + && A2dpSinkService.getFocusState() == AudioManager.AUDIOFOCUS_NONE) { + if (shouldRequestFocus()) { + mSessionCallbacks.onPrepare(); + } else { sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE); + } } return true; @@ -378,6 +388,18 @@ class AvrcpControllerStateMachine extends StateMachine { } return true; + case MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS: + mAddressedPlayer.setSupportedPlayerApplicationSettings( + (PlayerApplicationSettings) msg.obj); + BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState()); + return true; + + case MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS: + mAddressedPlayer.setCurrentPlayerApplicationSettings( + (PlayerApplicationSettings) msg.obj); + BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState()); + return true; + case DISCONNECT: transitionTo(mDisconnecting); return true; @@ -435,6 +457,20 @@ class AvrcpControllerStateMachine extends StateMachine { return (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_REWIND) || (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_FF); } + + private void setRepeat(int repeatMode) { + mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1, + new byte[]{PlayerApplicationSettings.REPEAT_STATUS}, new byte[]{ + PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal( + PlayerApplicationSettings.REPEAT_STATUS, repeatMode)}); + } + + private void setShuffle(int shuffleMode) { + mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1, + new byte[]{PlayerApplicationSettings.SHUFFLE_STATUS}, new byte[]{ + PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal( + PlayerApplicationSettings.SHUFFLE_STATUS, shuffleMode)}); + } } // Handle the get folder listing action @@ -554,7 +590,7 @@ class AvrcpControllerStateMachine extends StateMachine { case MESSAGE_GET_FOLDER_ITEMS: if (!mBrowseNode.equals(msg.obj)) { if (shouldAbort(mBrowseNode.getScope(), - ((BrowseTree.BrowseNode) msg.obj).getScope())) { + ((BrowseTree.BrowseNode) msg.obj).getScope())) { mAbort = true; } deferMessage(msg); @@ -576,8 +612,8 @@ class AvrcpControllerStateMachine extends StateMachine { * necessary. * * @return true: a new folder in the same scope - * a new player while fetching contents of a folder - * false: other cases, specifically Now Playing while fetching a folder + * a new player while fetching contents of a folder + * false: other cases, specifically Now Playing while fetching a folder */ private boolean shouldAbort(int currentScope, int fetchScope) { if ((currentScope == fetchScope) @@ -674,31 +710,60 @@ class AvrcpControllerStateMachine extends StateMachine { @Override public void enter() { onBrowsingDisconnected(); + BluetoothMediaBrowserService.trackChanged(null); + BluetoothMediaBrowserService.addressedPlayerChanged(null); broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING); transitionTo(mDisconnected); } } + /** + * Handle a request to align our local volume with the volume of a remote device. If + * we're assuming the source volume is fixed then a response of ABS_VOL_MAX will always be + * sent and no volume adjustment action will be taken on the sink side. + * + * @param absVol A volume level based on a domain of [0, ABS_VOL_MAX] + * @param label Volume notification label + */ + private void handleAbsVolumeRequest(int absVol, int label) { + logD("handleAbsVolumeRequest: absVol = " + absVol + ", label = " + label); + if (mIsVolumeFixed) { + logD("Source volume is assumed to be fixed, responding with max volume"); + absVol = ABS_VOL_BASE; + } else { + mVolumeChangedNotificationsToIgnore++; + removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT); + sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT, + ABS_VOL_TIMEOUT_MILLIS); + setAbsVolume(absVol); + } + mService.sendAbsVolRspNative(mDeviceAddress, absVol, label); + } + + /** + * Align our volume with a requested absolute volume level + * + * @param absVol A volume level based on a domain of [0, ABS_VOL_MAX] + */ + private void setAbsVolume(int absVol) { + int maxLocalVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int curLocalVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + int reqLocalVolume = (maxLocalVolume * absVol) / ABS_VOL_BASE; + logD("setAbsVolme: absVol = " + absVol + ", reqLocal = " + reqLocalVolume + + ", curLocal = " + curLocalVolume + ", maxLocal = " + maxLocalVolume); - private void setAbsVolume(int absVol, int label) { - int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); - int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); - int newIndex = (maxVolume * absVol) / ABS_VOL_BASE; - logD(" setAbsVolume =" + absVol + " maxVol = " + maxVolume - + " cur = " + currIndex + " new = " + newIndex); /* * In some cases change in percentage is not sufficient enough to warrant * change in index values which are in range of 0-15. For such cases * no action is required */ - if (newIndex != currIndex) { - mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex, + if (reqLocalVolume != curLocalVolume) { + mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, reqLocalVolume, AudioManager.FLAG_SHOW_UI); } - mService.sendAbsVolRspNative(mDeviceAddress, getAbsVolumeResponse(), label); } - private int getAbsVolumeResponse() { + private int getAbsVolume() { if (mIsVolumeFixed) { return ABS_VOL_BASE; } @@ -708,7 +773,7 @@ class AvrcpControllerStateMachine extends StateMachine { return newIndex; } - MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() { + MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() { @Override public void onPlay() { logD("onPlay"); @@ -781,6 +846,19 @@ class AvrcpControllerStateMachine extends StateMachine { BrowseTree.BrowseNode node = mBrowseTree.findBrowseNodeByID(mediaId); sendMessage(MESSAGE_PLAY_ITEM, node); } + + @Override + public void onSetRepeatMode(int repeatMode) { + logD("onSetRepeatMode"); + sendMessage(MSG_AVRCP_SET_REPEAT, repeatMode); + } + + @Override + public void onSetShuffleMode(int shuffleMode) { + logD("onSetShuffleMode"); + sendMessage(MSG_AVRCP_SET_SHUFFLE, shuffleMode); + + } }; protected void broadcastConnectionStateChanged(int currentState) { diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java index bed38d905..4736acffa 100644 --- a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java +++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java @@ -17,8 +17,9 @@ package com.android.bluetooth.avrcpcontroller; import android.media.MediaMetadata; -import android.media.session.PlaybackState; import android.os.SystemClock; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; import java.util.Arrays; @@ -41,27 +42,31 @@ class AvrcpPlayer { public static final int FEATURE_PREVIOUS = 48; public static final int FEATURE_BROWSING = 59; - private int mPlayStatus = PlaybackState.STATE_NONE; - private long mPlayTime = PlaybackState.PLAYBACK_POSITION_UNKNOWN; + private int mPlayStatus = PlaybackStateCompat.STATE_NONE; + private long mPlayTime = PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN; private long mPlayTimeUpdate = 0; private float mPlaySpeed = 1; private int mId; private String mName = ""; private int mPlayerType; - private byte[] mPlayerFeatures; - private long mAvailableActions; + private byte[] mPlayerFeatures = new byte[16]; + private long mAvailableActions = PlaybackStateCompat.ACTION_PREPARE; private MediaMetadata mCurrentTrack; - private PlaybackState mPlaybackState; + private PlaybackStateCompat mPlaybackStateCompat; + private PlayerApplicationSettings mSupportedPlayerApplicationSettings = + new PlayerApplicationSettings(); + private PlayerApplicationSettings mCurrentPlayerApplicationSettings; AvrcpPlayer() { mId = INVALID_ID; //Set Default Actions in case Player data isn't available. - mAvailableActions = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY - | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS - | PlaybackState.ACTION_STOP; - PlaybackState.Builder playbackStateBuilder = new PlaybackState.Builder() + mAvailableActions = PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PREPARE; + PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder() .setActions(mAvailableActions); - mPlaybackState = playbackStateBuilder.build(); + mPlaybackStateCompat = playbackStateBuilder.build(); } AvrcpPlayer(int id, String name, byte[] playerFeatures, int playStatus, int playerType) { @@ -70,10 +75,10 @@ class AvrcpPlayer { mPlayStatus = playStatus; mPlayerType = playerType; mPlayerFeatures = Arrays.copyOf(playerFeatures, playerFeatures.length); - updateAvailableActions(); - PlaybackState.Builder playbackStateBuilder = new PlaybackState.Builder() + PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder() .setActions(mAvailableActions); - mPlaybackState = playbackStateBuilder.build(); + mPlaybackStateCompat = playbackStateBuilder.build(); + updateAvailableActions(); } public int getId() { @@ -87,7 +92,8 @@ class AvrcpPlayer { public void setPlayTime(int playTime) { mPlayTime = playTime; mPlayTimeUpdate = SystemClock.elapsedRealtime(); - mPlaybackState = new PlaybackState.Builder(mPlaybackState).setState(mPlayStatus, mPlayTime, + mPlaybackStateCompat = new PlaybackStateCompat.Builder(mPlaybackStateCompat).setState( + mPlayStatus, mPlayTime, mPlaySpeed).build(); } @@ -97,30 +103,48 @@ class AvrcpPlayer { public void setPlayStatus(int playStatus) { mPlayTime += mPlaySpeed * (SystemClock.elapsedRealtime() - - mPlaybackState.getLastPositionUpdateTime()); + - mPlaybackStateCompat.getLastPositionUpdateTime()); mPlayStatus = playStatus; switch (mPlayStatus) { - case PlaybackState.STATE_STOPPED: + case PlaybackStateCompat.STATE_STOPPED: mPlaySpeed = 0; break; - case PlaybackState.STATE_PLAYING: + case PlaybackStateCompat.STATE_PLAYING: mPlaySpeed = 1; break; - case PlaybackState.STATE_PAUSED: + case PlaybackStateCompat.STATE_PAUSED: mPlaySpeed = 0; break; - case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackStateCompat.STATE_FAST_FORWARDING: mPlaySpeed = 3; break; - case PlaybackState.STATE_REWINDING: + case PlaybackStateCompat.STATE_REWINDING: mPlaySpeed = -3; break; } - mPlaybackState = new PlaybackState.Builder(mPlaybackState).setState(mPlayStatus, mPlayTime, + mPlaybackStateCompat = new PlaybackStateCompat.Builder(mPlaybackStateCompat).setState( + mPlayStatus, mPlayTime, mPlaySpeed).build(); } + public void setSupportedPlayerApplicationSettings( + PlayerApplicationSettings playerApplicationSettings) { + mSupportedPlayerApplicationSettings = playerApplicationSettings; + updateAvailableActions(); + } + + public void setCurrentPlayerApplicationSettings( + PlayerApplicationSettings playerApplicationSettings) { + Log.d(TAG, "Settings changed"); + mCurrentPlayerApplicationSettings = playerApplicationSettings; + MediaSessionCompat session = BluetoothMediaBrowserService.getSession(); + session.setRepeatMode(mCurrentPlayerApplicationSettings.getSetting( + PlayerApplicationSettings.REPEAT_STATUS)); + session.setShuffleMode(mCurrentPlayerApplicationSettings.getSetting( + PlayerApplicationSettings.SHUFFLE_STATUS)); + } + public int getPlayStatus() { return mPlayStatus; } @@ -131,17 +155,22 @@ class AvrcpPlayer { return (mPlayerFeatures[byteNumber] & bitMask) == bitMask; } - public PlaybackState getPlaybackState() { + public boolean supportsSetting(int settingType, int settingValue) { + return mSupportedPlayerApplicationSettings.supportsSetting(settingType, settingValue); + } + + public PlaybackStateCompat getPlaybackState() { if (DBG) { Log.d(TAG, "getPlayBackState state " + mPlayStatus + " time " + mPlayTime); } - return mPlaybackState; + return mPlaybackStateCompat; } public synchronized void updateCurrentTrack(MediaMetadata update) { if (update != null) { long trackNumber = update.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); - mPlaybackState = new PlaybackState.Builder(mPlaybackState).setActiveQueueItemId( + mPlaybackStateCompat = new PlaybackStateCompat.Builder( + mPlaybackStateCompat).setActiveQueueItemId( trackNumber - 1).build(); } mCurrentTrack = update; @@ -153,26 +182,37 @@ class AvrcpPlayer { private void updateAvailableActions() { if (supportsFeature(FEATURE_PLAY)) { - mAvailableActions = mAvailableActions | PlaybackState.ACTION_PLAY; + mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_PLAY; } if (supportsFeature(FEATURE_STOP)) { - mAvailableActions = mAvailableActions | PlaybackState.ACTION_STOP; + mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_STOP; } if (supportsFeature(FEATURE_PAUSE)) { - mAvailableActions = mAvailableActions | PlaybackState.ACTION_PAUSE; + mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_PAUSE; } if (supportsFeature(FEATURE_REWIND)) { - mAvailableActions = mAvailableActions | PlaybackState.ACTION_REWIND; + mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_REWIND; } if (supportsFeature(FEATURE_FAST_FORWARD)) { - mAvailableActions = mAvailableActions | PlaybackState.ACTION_FAST_FORWARD; + mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_FAST_FORWARD; } if (supportsFeature(FEATURE_FORWARD)) { - mAvailableActions = mAvailableActions | PlaybackState.ACTION_SKIP_TO_NEXT; + mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_SKIP_TO_NEXT; } if (supportsFeature(FEATURE_PREVIOUS)) { - mAvailableActions = mAvailableActions | PlaybackState.ACTION_SKIP_TO_PREVIOUS; + mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; } + if (mSupportedPlayerApplicationSettings.supportsSetting( + PlayerApplicationSettings.REPEAT_STATUS)) { + mAvailableActions |= PlaybackStateCompat.ACTION_SET_REPEAT_MODE; + } + if (mSupportedPlayerApplicationSettings.supportsSetting( + PlayerApplicationSettings.SHUFFLE_STATUS)) { + mAvailableActions |= PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE; + } + mPlaybackStateCompat = new PlaybackStateCompat.Builder(mPlaybackStateCompat) + .setActions(mAvailableActions).build(); + if (DBG) Log.d(TAG, "Supported Actions = " + mAvailableActions); } } diff --git a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java index 304d5a2c9..a0b1224ee 100644 --- a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java +++ b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java @@ -16,15 +16,22 @@ package com.android.bluetooth.avrcpcontroller; +import android.app.PendingIntent; +import android.content.Intent; import android.media.MediaMetadata; import android.media.browse.MediaBrowser.MediaItem; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.media.session.PlaybackState; import android.os.Bundle; -import android.service.media.MediaBrowserService; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; +import androidx.media.MediaBrowserServiceCompat; + +import com.android.bluetooth.BluetoothPrefs; import com.android.bluetooth.R; import java.util.ArrayList; @@ -37,45 +44,48 @@ import java.util.List; * The applications are expected to use MediaBrowser (see API) and all the music * browsing/playback/metadata can be controlled via MediaBrowser and MediaController. * - * The current behavior of MediaSession exposed by this service is as follows: - * 1. MediaSession is active (i.e. SystemUI and other overview UIs can see updates) when device is - * connected and first starts playing. Before it starts playing we do not active the session. + * The current behavior of MediaSessionCompat exposed by this service is as follows: + * 1. MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when + * device is connected and first starts playing. Before it starts playing we do not activate the + * session. * 1.1 The session is active throughout the duration of connection. * 2. The session is de-activated when the device disconnects. It will be connected again when (1) * happens. */ -public class BluetoothMediaBrowserService extends MediaBrowserService { +public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat { private static final String TAG = "BluetoothMediaBrowserService"; private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private static BluetoothMediaBrowserService sBluetoothMediaBrowserService; - private MediaSession mSession; + private MediaSessionCompat mSession; // Browsing related structures. - private List<MediaSession.QueueItem> mMediaQueue = new ArrayList<>(); + private List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>(); + + // Error messaging extras + public static final String ERROR_RESOLUTION_ACTION_INTENT = + "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; + public static final String ERROR_RESOLUTION_ACTION_LABEL = + "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; /** - * Initialize this BluetoothMediaBrowserService, creating our MediaSession, MediaPlayer and - * MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService. + * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer + * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService. */ @Override public void onCreate() { if (DBG) Log.d(TAG, "onCreate"); super.onCreate(); - // Create and configure the MediaSession - mSession = new MediaSession(this, TAG); + // Create and configure the MediaSessionCompat + mSession = new MediaSessionCompat(this, TAG); setSessionToken(mSession.getSessionToken()); - mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name)); mSession.setQueue(mMediaQueue); - PlaybackState.Builder playbackStateBuilder = new PlaybackState.Builder(); - playbackStateBuilder.setState(PlaybackState.STATE_ERROR, - PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f).setActions(0); - playbackStateBuilder.setErrorMessage(getString(R.string.bluetooth_disconnected)); - mSession.setPlaybackState(playbackStateBuilder.build()); + setErrorPlaybackState(); sBluetoothMediaBrowserService = this; } @@ -89,11 +99,30 @@ public class BluetoothMediaBrowserService extends MediaBrowserService { } } + private void setErrorPlaybackState() { + Bundle extras = new Bundle(); + extras.putString(ERROR_RESOLUTION_ACTION_LABEL, + getString(R.string.bluetooth_connect_action)); + Intent launchIntent = new Intent(); + launchIntent.setAction(BluetoothPrefs.BLUETOOTH_SETTING_ACTION); + launchIntent.addCategory(BluetoothPrefs.BLUETOOTH_SETTING_CATEGORY); + PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, + launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + extras.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); + PlaybackStateCompat errorState = new PlaybackStateCompat.Builder() + .setErrorMessage(getString(R.string.bluetooth_disconnected)) + .setExtras(extras) + .setState(PlaybackStateCompat.STATE_ERROR, 0, 0) + .build(); + mSession.setPlaybackState(errorState); + } + @Override public synchronized void onLoadChildren(final String parentMediaId, - final Result<List<MediaItem>> result) { + final Result<List<MediaBrowserCompat.MediaItem>> result) { if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId); - List<MediaItem> contents = getContents(parentMediaId); + List<MediaBrowserCompat.MediaItem> contents = + MediaBrowserCompat.MediaItem.fromMediaItemList(getContents(parentMediaId)); if (contents == null) { result.detach(); } else { @@ -112,7 +141,8 @@ public class BluetoothMediaBrowserService extends MediaBrowserService { mMediaQueue.clear(); if (songList != null) { for (MediaItem song : songList) { - mMediaQueue.add(new MediaSession.QueueItem(song.getDescription(), + mMediaQueue.add(new MediaSessionCompat.QueueItem( + MediaDescriptionCompat.fromMediaDescription(song.getDescription()), mMediaQueue.size())); } } @@ -129,8 +159,11 @@ public class BluetoothMediaBrowserService extends MediaBrowserService { } } - static synchronized void addressedPlayerChanged(MediaSession.Callback callback) { + static synchronized void addressedPlayerChanged(MediaSessionCompat.Callback callback) { if (sBluetoothMediaBrowserService != null) { + if (callback == null) { + sBluetoothMediaBrowserService.setErrorPlaybackState(); + } sBluetoothMediaBrowserService.mSession.setCallback(callback); } else { Log.w(TAG, "addressedPlayerChanged Unavailable"); @@ -139,13 +172,14 @@ public class BluetoothMediaBrowserService extends MediaBrowserService { static synchronized void trackChanged(MediaMetadata mediaMetadata) { if (sBluetoothMediaBrowserService != null) { - sBluetoothMediaBrowserService.mSession.setMetadata(mediaMetadata); + sBluetoothMediaBrowserService.mSession.setMetadata( + MediaMetadataCompat.fromMediaMetadata(mediaMetadata)); } else { Log.w(TAG, "trackChanged Unavailable"); } } - static synchronized void notifyChanged(PlaybackState playbackState) { + static synchronized void notifyChanged(PlaybackStateCompat playbackState) { Log.d(TAG, "notifyChanged PlaybackState" + playbackState); if (sBluetoothMediaBrowserService != null) { sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState); @@ -181,19 +215,19 @@ public class BluetoothMediaBrowserService extends MediaBrowserService { */ public static synchronized int getPlaybackState() { if (sBluetoothMediaBrowserService != null) { - PlaybackState currentPlaybackState = + PlaybackStateCompat currentPlaybackState = sBluetoothMediaBrowserService.mSession.getController().getPlaybackState(); if (currentPlaybackState != null) { return currentPlaybackState.getState(); } } - return PlaybackState.STATE_ERROR; + return PlaybackStateCompat.STATE_ERROR; } /** * Get object for controlling playback */ - public static synchronized MediaController.TransportControls getTransportControls() { + public static synchronized MediaControllerCompat.TransportControls getTransportControls() { if (sBluetoothMediaBrowserService != null) { return sBluetoothMediaBrowserService.mSession.getController().getTransportControls(); } else { @@ -212,4 +246,16 @@ public class BluetoothMediaBrowserService extends MediaBrowserService { Log.w(TAG, "setActive Unavailable"); } } + + /** + * Get Media session for updating state + */ + public static synchronized MediaSessionCompat getSession() { + if (sBluetoothMediaBrowserService != null) { + return sBluetoothMediaBrowserService.mSession; + } else { + Log.w(TAG, "getSession Unavailable"); + return null; + } + } } diff --git a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java index accea2a70..923282d34 100644 --- a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java +++ b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java @@ -100,7 +100,7 @@ public class BrowseTree { } BrowseNode getTrackFromNowPlayingList(int trackNumber) { - return mNowPlayingNode.mChildren.get(trackNumber); + return mNowPlayingNode.getChild(trackNumber); } // Each node of the tree is represented by Folder ID, Folder Name and the children. @@ -218,6 +218,13 @@ public class BrowseTree { return mChildren; } + synchronized BrowseNode getChild(int index) { + if (index < 0 || index >= mChildren.size()) { + return null; + } + return mChildren.get(index); + } + synchronized BrowseNode getParent() { return mParent; } diff --git a/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java b/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java index c34a2d7d0..362548e5f 100644 --- a/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java +++ b/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java @@ -16,12 +16,11 @@ package com.android.bluetooth.avrcpcontroller; -import android.bluetooth.BluetoothAvrcpPlayerSettings; +import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; +import android.util.SparseArray; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; /* * Contains information Player Application Setting extended from BluetootAvrcpPlayerSettings @@ -32,10 +31,10 @@ class PlayerApplicationSettings { /* * Values for SetPlayerApplicationSettings from AVRCP Spec V1.6 Appendix F. */ - private static final byte JNI_ATTRIB_EQUALIZER_STATUS = 0x01; - private static final byte JNI_ATTRIB_REPEAT_STATUS = 0x02; - private static final byte JNI_ATTRIB_SHUFFLE_STATUS = 0x03; - private static final byte JNI_ATTRIB_SCAN_STATUS = 0x04; + static final byte EQUALIZER_STATUS = 0x01; + static final byte REPEAT_STATUS = 0x02; + static final byte SHUFFLE_STATUS = 0x03; + static final byte SCAN_STATUS = 0x04; private static final byte JNI_EQUALIZER_STATUS_OFF = 0x01; private static final byte JNI_EQUALIZER_STATUS_ON = 0x02; @@ -55,18 +54,17 @@ class PlayerApplicationSettings { private static final byte JNI_STATUS_INVALID = -1; - /* * Hash map of current settings. */ - private Map<Integer, Integer> mSettings = new HashMap<Integer, Integer>(); + private SparseArray<Integer> mSettings = new SparseArray<>(); /* * Hash map of supported values, a setting should be supported by the remote in order to enable * in mSettings. */ - private Map<Integer, ArrayList<Integer>> mSupportedValues = - new HashMap<Integer, ArrayList<Integer>>(); + private SparseArray<ArrayList<Integer>> mSupportedValues = + new SparseArray<ArrayList<Integer>>(); /* Convert from JNI array to Java classes. */ static PlayerApplicationSettings makeSupportedSettings(byte[] btAvrcpAttributeList) { @@ -82,8 +80,7 @@ class PlayerApplicationSettings { supportedValues.add( mapAttribIdValtoAvrcpPlayerSetting(attrId, btAvrcpAttributeList[i++])); } - newObj.mSupportedValues.put(mapBTAttribIdToAvrcpPlayerSettings(attrId), - supportedValues); + newObj.mSupportedValues.put(attrId, supportedValues); } } catch (ArrayIndexOutOfBoundsException exception) { Log.e(TAG, "makeSupportedSettings attributeList index error."); @@ -91,25 +88,13 @@ class PlayerApplicationSettings { return newObj; } - public BluetoothAvrcpPlayerSettings getAvrcpSettings() { - int supportedSettings = 0; - for (Integer setting : mSettings.keySet()) { - supportedSettings |= setting; - } - BluetoothAvrcpPlayerSettings result = new BluetoothAvrcpPlayerSettings(supportedSettings); - for (Integer setting : mSettings.keySet()) { - result.addSettingValue(setting, mSettings.get(setting)); - } - return result; - } - static PlayerApplicationSettings makeSettings(byte[] btAvrcpAttributeList) { PlayerApplicationSettings newObj = new PlayerApplicationSettings(); try { for (int i = 0; i < btAvrcpAttributeList.length; ) { byte attrId = btAvrcpAttributeList[i++]; - newObj.mSettings.put(mapBTAttribIdToAvrcpPlayerSettings(attrId), + newObj.mSettings.put(attrId, mapAttribIdValtoAvrcpPlayerSetting(attrId, btAvrcpAttributeList[i++])); } } catch (ArrayIndexOutOfBoundsException exception) { @@ -123,177 +108,69 @@ class PlayerApplicationSettings { mSupportedValues = updates.mSupportedValues; } - public void setValues(BluetoothAvrcpPlayerSettings updates) { - int supportedSettings = updates.getSettings(); - for (int i = 1; i <= BluetoothAvrcpPlayerSettings.SETTING_SCAN; i++) { - if ((i & supportedSettings) > 0) { - mSettings.put(i, updates.getSettingValue(i)); - } - } + public boolean supportsSetting(int settingType, int settingValue) { + if (null == mSupportedValues.get(settingType)) return false; + return mSupportedValues.valueAt(settingType).contains(settingValue); } - /* - * Check through all settings to ensure that they are all available to be set and then check - * that the desired value is in fact supported by our remote player. - */ - public boolean supportsSettings(BluetoothAvrcpPlayerSettings settingsToCheck) { - int settingSubset = settingsToCheck.getSettings(); - int supportedSettings = 0; - for (Integer setting : mSupportedValues.keySet()) { - supportedSettings |= setting; - } - try { - if ((supportedSettings & settingSubset) == settingSubset) { - for (Integer settingId : mSettings.keySet()) { - // The setting is in both settings to check and supported settings but the - // value is not supported. - if ((settingId & settingSubset) == settingId && (!mSupportedValues.get( - settingId).contains(settingsToCheck.getSettingValue(settingId)))) { - return false; - } - } - return true; - } - } catch (NullPointerException e) { - Log.e(TAG, - "supportsSettings received a supported setting that has no supported values."); - } - return false; + public boolean supportsSetting(int settingType) { + return (null != mSupportedValues.get(settingType)); } - // Convert currently desired settings into an attribute array to pass to the native layer to - // enable them. - public ArrayList<Byte> getNativeSettings() { - int i = 0; - ArrayList<Byte> attribArray = new ArrayList<Byte>(); - for (Integer settingId : mSettings.keySet()) { - switch (settingId) { - case BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER: - attribArray.add(JNI_ATTRIB_EQUALIZER_STATUS); - attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId, - mSettings.get(settingId))); - break; - case BluetoothAvrcpPlayerSettings.SETTING_REPEAT: - attribArray.add(JNI_ATTRIB_REPEAT_STATUS); - attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId, - mSettings.get(settingId))); - break; - case BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE: - attribArray.add(JNI_ATTRIB_SHUFFLE_STATUS); - attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId, - mSettings.get(settingId))); - break; - case BluetoothAvrcpPlayerSettings.SETTING_SCAN: - attribArray.add(JNI_ATTRIB_SCAN_STATUS); - attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId, - mSettings.get(settingId))); - break; - default: - Log.w(TAG, "Unknown setting found in getNativeSettings: " + settingId); - } - } - return attribArray; + public int getSetting(int settingType) { + if (null == mSettings.get(settingType)) return -1; + return mSettings.get(settingType); } // Convert a native Attribute Id/Value pair into the AVRCP equivalent value. private static int mapAttribIdValtoAvrcpPlayerSetting(byte attribId, byte attribVal) { - if (attribId == JNI_ATTRIB_EQUALIZER_STATUS) { - switch (attribVal) { - case JNI_EQUALIZER_STATUS_OFF: - return BluetoothAvrcpPlayerSettings.STATE_OFF; - case JNI_EQUALIZER_STATUS_ON: - return BluetoothAvrcpPlayerSettings.STATE_ON; - } - } else if (attribId == JNI_ATTRIB_REPEAT_STATUS) { + if (attribId == REPEAT_STATUS) { switch (attribVal) { case JNI_REPEAT_STATUS_ALL_TRACK_REPEAT: - return BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK; + return PlaybackStateCompat.REPEAT_MODE_ALL; case JNI_REPEAT_STATUS_GROUP_REPEAT: - return BluetoothAvrcpPlayerSettings.STATE_GROUP; + return PlaybackStateCompat.REPEAT_MODE_GROUP; case JNI_REPEAT_STATUS_OFF: - return BluetoothAvrcpPlayerSettings.STATE_OFF; + return PlaybackStateCompat.REPEAT_MODE_NONE; case JNI_REPEAT_STATUS_SINGLE_TRACK_REPEAT: - return BluetoothAvrcpPlayerSettings.STATE_SINGLE_TRACK; - } - } else if (attribId == JNI_ATTRIB_SCAN_STATUS) { - switch (attribVal) { - case JNI_SCAN_STATUS_ALL_TRACK_SCAN: - return BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK; - case JNI_SCAN_STATUS_GROUP_SCAN: - return BluetoothAvrcpPlayerSettings.STATE_GROUP; - case JNI_SCAN_STATUS_OFF: - return BluetoothAvrcpPlayerSettings.STATE_OFF; + return PlaybackStateCompat.REPEAT_MODE_ONE; } - } else if (attribId == JNI_ATTRIB_SHUFFLE_STATUS) { + } else if (attribId == SHUFFLE_STATUS) { switch (attribVal) { case JNI_SHUFFLE_STATUS_ALL_TRACK_SHUFFLE: - return BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK; + return PlaybackStateCompat.SHUFFLE_MODE_ALL; case JNI_SHUFFLE_STATUS_GROUP_SHUFFLE: - return BluetoothAvrcpPlayerSettings.STATE_GROUP; + return PlaybackStateCompat.SHUFFLE_MODE_GROUP; case JNI_SHUFFLE_STATUS_OFF: - return BluetoothAvrcpPlayerSettings.STATE_OFF; + return PlaybackStateCompat.SHUFFLE_MODE_NONE; } } - return BluetoothAvrcpPlayerSettings.STATE_INVALID; + return JNI_STATUS_INVALID; } // Convert an AVRCP Setting/Value pair into the native equivalent value; - private static byte mapAvrcpPlayerSettingstoBTattribVal(int mSetting, int mSettingVal) { - if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER) { - switch (mSettingVal) { - case BluetoothAvrcpPlayerSettings.STATE_OFF: - return JNI_EQUALIZER_STATUS_OFF; - case BluetoothAvrcpPlayerSettings.STATE_ON: - return JNI_EQUALIZER_STATUS_ON; - } - } else if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_REPEAT) { + static byte mapAvrcpPlayerSettingstoBTattribVal(int mSetting, int mSettingVal) { + if (mSetting == REPEAT_STATUS) { switch (mSettingVal) { - case BluetoothAvrcpPlayerSettings.STATE_OFF: + case PlaybackStateCompat.REPEAT_MODE_NONE: return JNI_REPEAT_STATUS_OFF; - case BluetoothAvrcpPlayerSettings.STATE_SINGLE_TRACK: + case PlaybackStateCompat.REPEAT_MODE_ONE: return JNI_REPEAT_STATUS_SINGLE_TRACK_REPEAT; - case BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK: + case PlaybackStateCompat.REPEAT_MODE_ALL: return JNI_REPEAT_STATUS_ALL_TRACK_REPEAT; - case BluetoothAvrcpPlayerSettings.STATE_GROUP: + case PlaybackStateCompat.REPEAT_MODE_GROUP: return JNI_REPEAT_STATUS_GROUP_REPEAT; } - } else if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE) { + } else if (mSetting == SHUFFLE_STATUS) { switch (mSettingVal) { - case BluetoothAvrcpPlayerSettings.STATE_OFF: + case PlaybackStateCompat.SHUFFLE_MODE_NONE: return JNI_SHUFFLE_STATUS_OFF; - case BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK: + case PlaybackStateCompat.SHUFFLE_MODE_ALL: return JNI_SHUFFLE_STATUS_ALL_TRACK_SHUFFLE; - case BluetoothAvrcpPlayerSettings.STATE_GROUP: + case PlaybackStateCompat.SHUFFLE_MODE_GROUP: return JNI_SHUFFLE_STATUS_GROUP_SHUFFLE; } - } else if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_SCAN) { - switch (mSettingVal) { - case BluetoothAvrcpPlayerSettings.STATE_OFF: - return JNI_SCAN_STATUS_OFF; - case BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK: - return JNI_SCAN_STATUS_ALL_TRACK_SCAN; - case BluetoothAvrcpPlayerSettings.STATE_GROUP: - return JNI_SCAN_STATUS_GROUP_SCAN; - } } return JNI_STATUS_INVALID; } - - // convert a native Attribute Id into the AVRCP Setting equivalent value; - private static int mapBTAttribIdToAvrcpPlayerSettings(byte attribId) { - switch (attribId) { - case JNI_ATTRIB_EQUALIZER_STATUS: - return BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER; - case JNI_ATTRIB_REPEAT_STATUS: - return BluetoothAvrcpPlayerSettings.SETTING_REPEAT; - case JNI_ATTRIB_SHUFFLE_STATUS: - return BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE; - case JNI_ATTRIB_SCAN_STATUS: - return BluetoothAvrcpPlayerSettings.SETTING_SCAN; - default: - return BluetoothAvrcpPlayerSettings.STATE_INVALID; - } - } - } - diff --git a/src/com/android/bluetooth/btservice/AdapterService.java b/src/com/android/bluetooth/btservice/AdapterService.java index 009e42cef..a65bcfdf9 100644 --- a/src/com/android/bluetooth/btservice/AdapterService.java +++ b/src/com/android/bluetooth/btservice/AdapterService.java @@ -1559,7 +1559,6 @@ public class AdapterService extends Service { if (service == null) { return false; } - service.disable(); return service.factoryReset(); } diff --git a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java index 989043a7d..e92688f8c 100644 --- a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java +++ b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java @@ -1196,6 +1196,21 @@ public class HeadsetStateMachine extends StateMachine { } } + class MyAudioServerStateCallback extends AudioManager.AudioServerStateCallback { + @Override + public void onAudioServerDown() { + logi("onAudioServerDown"); + } + + @Override + public void onAudioServerUp() { + logi("onAudioServerUp restoring audio parameters"); + setAudioParameters(); + } + } + + MyAudioServerStateCallback mAudioServerStateCallback = new MyAudioServerStateCallback(); + class AudioOn extends ConnectedBase { @Override int getAudioStateInt() { @@ -1214,10 +1229,21 @@ public class HeadsetStateMachine extends StateMachine { mHeadsetService.setActiveDevice(mDevice); } setAudioParameters(); + + mSystemInterface.getAudioManager().setAudioServerStateCallback( + mHeadsetService.getMainExecutor(), mAudioServerStateCallback); + broadcastStateTransitions(); } @Override + public void exit() { + super.exit(); + + mSystemInterface.getAudioManager().clearAudioServerStateCallback(); + } + + @Override public boolean processMessage(Message message) { switch (message.what) { case CONNECT: { diff --git a/src/com/android/bluetooth/hfpclient/connserv/HfpClientDeviceBlock.java b/src/com/android/bluetooth/hfpclient/connserv/HfpClientDeviceBlock.java index 05af73e08..b567371f1 100644 --- a/src/com/android/bluetooth/hfpclient/connserv/HfpClientDeviceBlock.java +++ b/src/com/android/bluetooth/hfpclient/connserv/HfpClientDeviceBlock.java @@ -224,7 +224,7 @@ public class HfpClientDeviceBlock { if (DBG) { Log.d(mTAG, "prevConn " + prevConn.isClosing() + " new call " + newCall.getState()); } - if (prevConn.isClosing() + if (prevConn.isClosing() && prevConn.getCall().getState() != newCall.getState() && newCall.getState() != BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) { return true; } diff --git a/src/com/android/bluetooth/mapclient/MceStateMachine.java b/src/com/android/bluetooth/mapclient/MceStateMachine.java index 0a428b418..9b86aaef1 100644 --- a/src/com/android/bluetooth/mapclient/MceStateMachine.java +++ b/src/com/android/bluetooth/mapclient/MceStateMachine.java @@ -42,7 +42,6 @@ package com.android.bluetooth.mapclient; import android.app.Activity; import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothMapClient; import android.bluetooth.BluetoothProfile; @@ -59,6 +58,7 @@ import android.util.Log; import com.android.bluetooth.BluetoothMetricsProto; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; +import com.android.bluetooth.map.BluetoothMapbMessageMime; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IState; import com.android.internal.util.State; @@ -69,7 +69,6 @@ import com.android.vcard.VCardProperty; import java.util.ArrayList; import java.util.Calendar; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -347,7 +346,9 @@ final class MceStateMachine extends StateMachine { void setDefaultMessageType(SdpMasRecord sdpMasRecord) { int supportedMessageTypes = sdpMasRecord.getSupportedMessageTypes(); synchronized (mDefaultMessageType) { - if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_CDMA) > 0) { + if ((supportedMessageTypes & SdpMasRecord.MessageType.MMS) > 0) { + mDefaultMessageType = Bmessage.Type.MMS; + } else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_CDMA) > 0) { mDefaultMessageType = Bmessage.Type.SMS_CDMA; } else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_GSM) > 0) { mDefaultMessageType = Bmessage.Type.SMS_GSM; @@ -473,6 +474,11 @@ final class MceStateMachine extends StateMachine { } break; + case MSG_MAS_DISCONNECTED: + deferMessage(message); + transitionTo(mDisconnecting); + break; + case MSG_OUTBOUND_MESSAGE: mMasClient.makeRequest( new RequestPushMessage(FOLDER_OUTBOX, (Bmessage) message.obj, null, @@ -625,9 +631,12 @@ final class MceStateMachine extends StateMachine { } ArrayList<com.android.bluetooth.mapclient.Message> messageListing = request.getList(); if (messageListing != null) { - for (com.android.bluetooth.mapclient.Message msg : messageListing) { + // Message listings by spec arrive ordered newest first but we wish to broadcast as + // oldest first. Iterate in reverse order so we initiate requests oldest first. + for (int i = messageListing.size() - 1; i >= 0; i--) { + com.android.bluetooth.mapclient.Message msg = messageListing.get(i); if (DBG) { - Log.d(TAG, "getting message "); + Log.d(TAG, "getting message for handle " + msg.getHandle()); } // A message listing coming from the server should always have up to date data mMessages.put(msg.getHandle(), new MessageMetadata(msg.getHandle(), @@ -665,6 +674,7 @@ final class MceStateMachine extends StateMachine { switch (message.getType()) { case SMS_CDMA: case SMS_GSM: + case MMS: if (DBG) { Log.d(TAG, "Body: " + message.getBodyContent()); } @@ -705,6 +715,12 @@ final class MceStateMachine extends StateMachine { intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME, originator.getDisplayName()); } + if (message.getType() == Bmessage.Type.MMS) { + BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime(); + mmsBmessage.parseMsgPart(message.getBodyContent()); + intent.putExtra(android.content.Intent.EXTRA_TEXT, + mmsBmessage.getMessageAsText()); + } // Only send to the current default SMS app if one exists String defaultMessagingPackage = Telephony.Sms.getDefaultSmsPackage(mService); if (defaultMessagingPackage != null) { @@ -712,8 +728,6 @@ final class MceStateMachine extends StateMachine { } mService.sendBroadcast(intent, android.Manifest.permission.RECEIVE_SMS); break; - - case MMS: case EMAIL: default: Log.e(TAG, "Received unhandled type" + message.getType().toString()); diff --git a/src/com/android/bluetooth/mapclient/MnsObexServer.java b/src/com/android/bluetooth/mapclient/MnsObexServer.java index 53cd79bb2..33ba1ea74 100644 --- a/src/com/android/bluetooth/mapclient/MnsObexServer.java +++ b/src/com/android/bluetooth/mapclient/MnsObexServer.java @@ -90,6 +90,10 @@ class MnsObexServer extends ServerRequestHandler { if (VDBG) { Log.v(TAG, "onDisconnect"); } + MceStateMachine currentStateMachine = mStateMachineReference.get(); + if (currentStateMachine != null) { + currentStateMachine.disconnect(); + } } @Override diff --git a/src/com/android/bluetooth/mapclient/MnsService.java b/src/com/android/bluetooth/mapclient/MnsService.java index c1ab39e00..b3317df90 100644 --- a/src/com/android/bluetooth/mapclient/MnsService.java +++ b/src/com/android/bluetooth/mapclient/MnsService.java @@ -17,6 +17,7 @@ package com.android.bluetooth.mapclient; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.os.Handler; @@ -129,6 +130,11 @@ public class MnsService { Log.e(TAG, "Error: NO statemachine for device: " + device.getAddress() + " (name: " + device.getName()); return false; + } else if (stateMachine.getState() != BluetoothProfile.STATE_CONNECTED) { + Log.e(TAG, "Error: statemachine for device: " + device.getAddress() + + " (name: " + device.getName() + ") is not currently CONNECTED : " + + stateMachine.getCurrentState()); + return false; } MnsObexServer srv = new MnsObexServer(stateMachine, sServerSockets); BluetoothObexTransport transport = new BluetoothObexTransport(socket); diff --git a/src/com/android/bluetooth/mapclient/obex/BmessageParser.java b/src/com/android/bluetooth/mapclient/obex/BmessageParser.java index 2705e3429..5b844dce5 100644 --- a/src/com/android/bluetooth/mapclient/obex/BmessageParser.java +++ b/src/com/android/bluetooth/mapclient/obex/BmessageParser.java @@ -309,6 +309,12 @@ class BmessageParser { String remng = mParser.remaining(); byte[] data = remng.getBytes(); + if (offset < 0 || offset > data.length) { + /* Handle possible exception for incorrect LENGTH value + * from MSE while parsing end of props */ + throw new ParseException("Invalid LENGTH value", mParser.pos()); + } + /* restart parsing from after 'message'<CRLF> */ mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos); diff --git a/src/com/android/bluetooth/mapclient/obex/ObexTime.java b/src/com/android/bluetooth/mapclient/obex/ObexTime.java index 42a32c10f..cc58a5144 100644 --- a/src/com/android/bluetooth/mapclient/obex/ObexTime.java +++ b/src/com/android/bluetooth/mapclient/obex/ObexTime.java @@ -29,8 +29,17 @@ public final class ObexTime { public ObexTime(String time) { /* - * match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset - * +/-hhmm + * Match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset +/-hhmm + * + * Matched groups are numberes as follows: + * + * YYYY MM DD T HH MM SS + hh mm + * ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^ + * 1 2 3 4 5 6 8 9 10 + * |---7---| + * + * All groups are guaranteed to be numeric so conversion will always succeed (except group 8 + * which is either + or -) */ Pattern p = Pattern.compile( "(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})(([+-])(\\d{2})(\\d{2})" + ")?"); @@ -39,20 +48,26 @@ public final class ObexTime { if (m.matches()) { /* - * matched groups are numberes as follows: YYYY MM DD T HH MM SS + - * hh mm ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^ 1 2 3 4 5 6 8 9 10 all groups - * are guaranteed to be numeric so conversion will always succeed - * (except group 8 which is either + or -) + * MAP spec says to default to "Local Time basis" for a message listing timestamp. We'll + * use the system default timezone and assume it knows best what our local timezone is. + * The builder defaults to the default locale and timezone if none is provided. */ + Calendar.Builder builder = new Calendar.Builder(); - Calendar cal = Calendar.getInstance(); - cal.set(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)) - 1, - Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)), - Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6))); + /* Note that Calendar months are zero-based */ + builder.setDate(Integer.parseInt(m.group(1)), /* year */ + Integer.parseInt(m.group(2)) - 1, /* month */ + Integer.parseInt(m.group(3))); /* day of month */ + + /* Note the MAP timestamp doesn't have milliseconds and we're explicitly setting to 0 */ + builder.setTimeOfDay(Integer.parseInt(m.group(4)), /* hours */ + Integer.parseInt(m.group(5)), /* minutes */ + Integer.parseInt(m.group(6)), /* seconds */ + 0); /* milliseconds */ /* - * if 7th group is matched then we have UTC offset information - * included + * If 7th group is matched then we're no longer using "Local Time basis" and instead + * have a UTC based timestamp and offset information included */ if (m.group(7) != null) { int ohh = Integer.parseInt(m.group(9)); @@ -68,10 +83,10 @@ public final class ObexTime { TimeZone tz = TimeZone.getTimeZone("UTC"); tz.setRawOffset(offset); - cal.setTimeZone(tz); + builder.setTimeZone(tz); } - mDate = cal.getTime(); + mDate = builder.build().getTime(); } } diff --git a/src/com/android/bluetooth/pan/PanService.java b/src/com/android/bluetooth/pan/PanService.java index aa5a1fd31..92eab7787 100644 --- a/src/com/android/bluetooth/pan/PanService.java +++ b/src/com/android/bluetooth/pan/PanService.java @@ -40,6 +40,7 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; +import com.android.internal.annotations.VisibleForTesting; import java.net.InetAddress; import java.util.ArrayList; @@ -67,6 +68,9 @@ public class PanService extends ProfileService { private String mNapIfaceAddr; private boolean mNativeAvailable; + @VisibleForTesting + UserManager mUserManager; + private static final int MESSAGE_CONNECT = 1; private static final int MESSAGE_DISCONNECT = 2; private static final int MESSAGE_CONNECT_STATE_CHANGED = 11; @@ -116,6 +120,8 @@ public class PanService extends ProfileService { initializeNative(); mNativeAvailable = true; + mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); + mNetworkFactory = new BluetoothTetheringNetworkFactory(getBaseContext(), getMainLooper(), this); setPanService(this); @@ -137,6 +143,9 @@ public class PanService extends ProfileService { cleanupNative(); mNativeAvailable = false; } + + mUserManager = null; + if (mPanDevices != null) { int[] desiredStates = {BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTING}; @@ -319,6 +328,10 @@ public class PanService extends ProfileService { public boolean connect(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (mUserManager.isGuestUser()) { + Log.w(TAG, "Guest user does not have the permission to change the WiFi network"); + return false; + } if (getConnectionState(device) != BluetoothProfile.STATE_DISCONNECTED) { Log.e(TAG, "Pan Device not disconnected: " + device); return false; diff --git a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java index 34ab7d52c..a5b3fbfea 100644 --- a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java +++ b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java @@ -73,7 +73,7 @@ final class BluetoothPbapRequestPullPhoneBook extends BluetoothPbapRequest { oap.add(OAP_TAGID_FORMAT, format); /* - * maxListCount is a special case which is handled in + * maxListCount == 0 is a special case which is handled in * BluetoothPbapRequestPullPhoneBookSize */ if (maxListCount > 0) { diff --git a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBookSize.java b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBookSize.java new file mode 100644 index 000000000..f0706610f --- /dev/null +++ b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBookSize.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 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.pbapclient; + +import android.util.Log; + +import javax.obex.HeaderSet; + +final class BluetoothPbapRequestPullPhoneBookSize extends BluetoothPbapRequest { + + private static final boolean VDBG = Utils.VDBG; + + private static final String TAG = "BtPbapReqPullPhoneBookSize"; + + private static final String TYPE = "x-bt/phonebook"; + + private int mSize; + + BluetoothPbapRequestPullPhoneBookSize(String pbName, long filter) { + mHeaderSet.setHeader(HeaderSet.NAME, pbName); + + mHeaderSet.setHeader(HeaderSet.TYPE, TYPE); + + ObexAppParameters oap = new ObexAppParameters(); + // Set MaxListCount in the request to 0 to get PhonebookSize in the response. + // If a vCardSelector is present in the request, then the result shall + // contain the number of items that satisfy the selector’s criteria. + // See PBAP v1.2.3, Sec. 5.1.4.5. + oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 0); + if (filter != 0) { + oap.add(OAP_TAGID_FILTER, filter); + } + oap.addToHeaderSet(mHeaderSet); + } + + @Override + protected void readResponseHeaders(HeaderSet headerset) { + if (VDBG) { + Log.v(TAG, "readResponseHeaders"); + } + + ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset); + + if (oap.exists(OAP_TAGID_PHONEBOOK_SIZE)) { + mSize = oap.getShort(OAP_TAGID_PHONEBOOK_SIZE); + } + } + + public int getSize() { + return mSize; + } +} diff --git a/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java b/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java index 914b5b163..4e4a240f1 100644 --- a/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java +++ b/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java @@ -32,8 +32,10 @@ import android.util.Log; import com.android.bluetooth.BluetoothObexTransport; import com.android.bluetooth.R; +import com.android.vcard.VCardEntry; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import javax.obex.ClientSession; @@ -46,6 +48,15 @@ import javax.obex.ResponseCodes; * controlling state machine. */ class PbapClientConnectionHandler extends Handler { + // Tradeoff: larger BATCH_SIZE leads to faster download rates, while smaller + // BATCH_SIZE is less prone to IO Exceptions if there is a download in + // progress when Bluetooth stack is torn down. + private static final int DEFAULT_BATCH_SIZE = 250; + + // Upper limit on the indices of the vcf cards/entries, inclusive, + // i.e., valid indices are [0, 1, ... , UPPER_LIMIT] + private static final int UPPER_LIMIT = 65535; + static final String TAG = "PbapClientConnHandler"; static final boolean DBG = Utils.DBG; static final boolean VDBG = Utils.VDBG; @@ -88,16 +99,26 @@ class PbapClientConnectionHandler extends Handler { private static final long PBAP_FILTER_NICKNAME = 1 << 23; private static final int PBAP_SUPPORTED_FEATURE = - PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_BROWSING | PBAP_FEATURE_DOWNLOADING; + PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_DOWNLOADING; private static final long PBAP_REQUESTED_FIELDS = PBAP_FILTER_VERSION | PBAP_FILTER_FN | PBAP_FILTER_N | PBAP_FILTER_PHOTO | PBAP_FILTER_ADR | PBAP_FILTER_EMAIL | PBAP_FILTER_TEL | PBAP_FILTER_NICKNAME; private static final int L2CAP_INVALID_PSM = -1; public static final String PB_PATH = "telecom/pb.vcf"; + public static final String FAV_PATH = "telecom/fav.vcf"; public static final String MCH_PATH = "telecom/mch.vcf"; public static final String ICH_PATH = "telecom/ich.vcf"; public static final String OCH_PATH = "telecom/och.vcf"; + public static final String SIM_PB_PATH = "SIM1/telecom/pb.vcf"; + public static final String SIM_MCH_PATH = "SIM1/telecom/mch.vcf"; + public static final String SIM_ICH_PATH = "SIM1/telecom/ich.vcf"; + public static final String SIM_OCH_PATH = "SIM1/telecom/och.vcf"; + + // PBAP v1.2.3 Sec. 7.1.2 + private static final int SUPPORTED_REPOSITORIES_LOCALPHONEBOOK = 1 << 0; + private static final int SUPPORTED_REPOSITORIES_SIMCARD = 1 << 1; + private static final int SUPPORTED_REPOSITORIES_FAVORITES = 1 << 3; public static final int PBAP_V1_2 = 0x0102; public static final byte VCARD_TYPE_21 = 0; @@ -239,29 +260,25 @@ class PbapClientConnectionHandler extends Handler { break; case MSG_DOWNLOAD: - try { - mAccountCreated = addAccount(mAccount); - if (!mAccountCreated) { - Log.e(TAG, "Account creation failed."); - return; - } - // Start at contact 1 to exclued Owner Card PBAP 1.1 sec 3.1.5.2 - BluetoothPbapRequestPullPhoneBook request = - new BluetoothPbapRequestPullPhoneBook(PB_PATH, mAccount, - PBAP_REQUESTED_FIELDS, VCARD_TYPE_30, 0, 1); - request.execute(mObexSession); - PhonebookPullRequest processor = - new PhonebookPullRequest(mPbapClientStateMachine.getContext(), - mAccount); - processor.setResults(request.getList()); - processor.onPullComplete(); - HashMap<String, Integer> callCounter = new HashMap<>(); - downloadCallLog(MCH_PATH, callCounter); - downloadCallLog(ICH_PATH, callCounter); - downloadCallLog(OCH_PATH, callCounter); - } catch (IOException e) { - Log.w(TAG, "DOWNLOAD_CONTACTS Failure" + e.toString()); + mAccountCreated = addAccount(mAccount); + if (!mAccountCreated) { + Log.e(TAG, "Account creation failed."); + return; } + if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) { + downloadContacts(FAV_PATH); + } + if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) { + downloadContacts(PB_PATH); + } + if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) { + downloadContacts(SIM_PB_PATH); + } + + HashMap<String, Integer> callCounter = new HashMap<>(); + downloadCallLog(MCH_PATH, callCounter); + downloadCallLog(ICH_PATH, callCounter); + downloadCallLog(OCH_PATH, callCounter); break; default: @@ -369,6 +386,59 @@ class PbapClientConnectionHandler extends Handler { } } + void downloadContacts(String path) { + try { + PhonebookPullRequest processor = + new PhonebookPullRequest(mPbapClientStateMachine.getContext(), + mAccount); + + // Download contacts in batches of size DEFAULT_BATCH_SIZE + BluetoothPbapRequestPullPhoneBookSize requestPbSize = + new BluetoothPbapRequestPullPhoneBookSize(path, + PBAP_REQUESTED_FIELDS); + requestPbSize.execute(mObexSession); + + int numberOfContactsRemaining = requestPbSize.getSize(); + int startOffset = 0; + if (PB_PATH.equals(path)) { + // PBAP v1.2.3, Sec 3.1.5. The first contact in pb is owner card 0.vcf, which we + // do not want to download. The other phonebook objects (e.g., fav) don't have an + // owner card, so they don't need an offset. + startOffset = 1; + // "-1" because Owner Card 0.vcf is also included in /pb, but not in /fav. + numberOfContactsRemaining -= 1; + } + + while ((numberOfContactsRemaining > 0) && (startOffset <= UPPER_LIMIT)) { + int numberOfContactsToDownload = + Math.min(Math.min(DEFAULT_BATCH_SIZE, numberOfContactsRemaining), + UPPER_LIMIT - startOffset + 1); + BluetoothPbapRequestPullPhoneBook request = + new BluetoothPbapRequestPullPhoneBook(path, mAccount, + PBAP_REQUESTED_FIELDS, VCARD_TYPE_30, + numberOfContactsToDownload, startOffset); + request.execute(mObexSession); + ArrayList<VCardEntry> vcards = request.getList(); + if (path == FAV_PATH) { + // mark each vcard as a favorite + for (VCardEntry v : vcards) { + v.setStarred(true); + } + } + processor.setResults(vcards); + processor.onPullComplete(); + + startOffset += numberOfContactsToDownload; + numberOfContactsRemaining -= numberOfContactsToDownload; + } + if ((startOffset > UPPER_LIMIT) && (numberOfContactsRemaining > 0)) { + Log.w(TAG, "Download contacts incomplete, index exceeded upper limit."); + } + } catch (IOException e) { + Log.w(TAG, "Download contacts failure" + e.toString()); + } + } + void downloadCallLog(String path, HashMap<String, Integer> callCounter) { try { BluetoothPbapRequestPullPhoneBook request = @@ -419,4 +489,12 @@ class PbapClientConnectionHandler extends Handler { Log.d(TAG, "Call Logs could not be deleted, they may not exist yet."); } } + + private boolean isRepositorySupported(int mask) { + if (mPseRec == null) { + if (VDBG) Log.v(TAG, "No PBAP Server SDP Record"); + return false; + } + return (mask & mPseRec.getSupportedRepositories()) != 0; + } } diff --git a/src/com/android/bluetooth/pbapclient/PbapClientService.java b/src/com/android/bluetooth/pbapclient/PbapClientService.java index f150cddec..02b1e7a51 100644 --- a/src/com/android/bluetooth/pbapclient/PbapClientService.java +++ b/src/com/android/bluetooth/pbapclient/PbapClientService.java @@ -19,9 +19,11 @@ package com.android.bluetooth.pbapclient; import android.accounts.Account; import android.accounts.AccountManager; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothPbapClient; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -31,6 +33,7 @@ import android.util.Log; import com.android.bluetooth.R; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; +import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService; import com.android.bluetooth.sdp.SdpManager; import java.util.ArrayList; @@ -71,6 +74,9 @@ public class PbapClientService extends ProfileService { filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); // delay initial download until after the user is unlocked to add an account. filter.addAction(Intent.ACTION_USER_UNLOCKED); + // To remove call logs when PBAP was never connected while calls were made, + // we also listen for HFP to become disconnected. + filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); try { registerReceiver(mPbapBroadcastReceiver, filter); } catch (Exception e) { @@ -128,6 +134,21 @@ public class PbapClientService extends ProfileService { } } + private void removeHfpCallLog(String accountName, Context context) { + if (DBG) Log.d(TAG, "Removing call logs from " + accountName); + // Delete call logs belonging to accountName==BD_ADDR that also match + // component name "hfpclient". + ComponentName componentName = new ComponentName(context, HfpClientConnectionService.class); + String selectionFilter = CallLog.Calls.PHONE_ACCOUNT_ID + "=? AND " + + CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=?"; + String[] selectionArgs = new String[]{accountName, componentName.flattenToString()}; + try { + getContentResolver().delete(CallLog.Calls.CONTENT_URI, selectionFilter, selectionArgs); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Call Logs could not be deleted, they may not exist yet."); + } + } + private void registerSdpRecord() { SdpManager sdpManager = SdpManager.getDefaultManager(); if (sdpManager == null) { @@ -171,6 +192,21 @@ public class PbapClientService extends ProfileService { for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) { stateMachine.resumeDownload(); } + } else if (action.equals(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED)) { + // PbapClientConnectionHandler has code to remove calllogs when PBAP disconnects. + // However, if PBAP was never connected/enabled in the first place, and calls are + // made over HFP, these calllogs will not be removed when the device disconnects. + // This code ensures callogs are still removed in this case. + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); + + if (newState == BluetoothProfile.STATE_DISCONNECTED) { + if (DBG) { + Log.d(TAG, "Received intent to disconnect HFP with " + device); + } + // HFP client stores entries in calllog.db by BD_ADDR and component name + removeHfpCallLog(device.getAddress(), context); + } } } } |