diff options
35 files changed, 1099 insertions, 375 deletions
diff --git a/Android.mk b/Android.mk index a34d2a34f..1f73091ad 100644 --- a/Android.mk +++ b/Android.mk @@ -29,6 +29,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ LOCAL_STATIC_ANDROID_LIBRARIES := \ androidx.core_core \ + androidx.legacy_legacy-support-v4 \ androidx.lifecycle_lifecycle-livedata \ androidx.room_room-runtime \ diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8ce4707dc..6de611475 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -320,6 +320,16 @@ <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service> + + <activity + android:name=".BluetoothPrefs" + android:exported="@bool/profile_supported_a2dp_sink" + android:enabled="@bool/profile_supported_a2dp_sink"> + <intent-filter> + <action android:name="android.intent.action.APPLICATION_PREFERENCES"/> + </intent-filter> + </activity> + <service android:process="@string/process" android:name = ".avrcp.AvrcpTargetService" diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 1525a71f5..a82501057 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -26,10 +26,10 @@ <string name="airplane_error_title" msgid="2683839635115739939">"وضع الطائرة"</string> <string name="airplane_error_msg" msgid="8698965595254137230">"لا يمكنك استخدام البلوتوث في وضع الطائرة."</string> <string name="bt_enable_title" msgid="8657832550503456572"></string> - <string name="bt_enable_line1" msgid="7203551583048149">"لإستخدام خدمات البلوتوث، يجب تشغيل البلوتوث أولاً."</string> - <string name="bt_enable_line2" msgid="4341936569415937994">"هل تريد تشغيل البلوتوث الآن؟"</string> + <string name="bt_enable_line1" msgid="7203551583048149">"لإستخدام خدمات البلوتوث، يجب تفعيل البلوتوث أولاً."</string> + <string name="bt_enable_line2" msgid="4341936569415937994">"هل تريد تفعيل البلوتوث الآن؟"</string> <string name="bt_enable_cancel" msgid="1988832367505151727">"إلغاء"</string> - <string name="bt_enable_ok" msgid="3432462749994538265">"تشغيل"</string> + <string name="bt_enable_ok" msgid="3432462749994538265">"تفعيل"</string> <string name="incoming_file_confirm_title" msgid="8139874248612182627">"نقل الملف"</string> <string name="incoming_file_confirm_content" msgid="2752605552743148036">"هل تقبل الملف الوارد؟"</string> <string name="incoming_file_confirm_cancel" msgid="2973321832477704805">"رفض"</string> @@ -77,7 +77,7 @@ <string name="not_exist_file" msgid="3489434189599716133">"ليست هناك أي ملفات"</string> <string name="not_exist_file_desc" msgid="4059531573790529229">"الملف غير موجود. \n"</string> <string name="enabling_progress_title" msgid="436157952334723406">"يرجى الانتظار…"</string> - <string name="enabling_progress_content" msgid="4601542238119927904">"جارٍ تشغيل البلوتوث..."</string> + <string name="enabling_progress_content" msgid="4601542238119927904">"جارٍ تفعيل البلوتوث..."</string> <string name="bt_toast_1" msgid="972182708034353383">"سيتم استلام الملف. تحقق من التقدم في لوحة الإشعارات."</string> <string name="bt_toast_2" msgid="8602553334099066582">"لا يمكن تلقي الملف."</string> <string name="bt_toast_3" msgid="6707884165086862518">"تم إيقاف استلام الملف من \"<xliff:g id="SENDER">%1$s</xliff:g>\""</string> diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index a9e448c41..7d371e0df 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -122,7 +122,7 @@ <string name="transfer_menu_open" msgid="3368984869083107200">"Abrir"</string> <string name="transfer_menu_clear" msgid="5854038118831427492">"Borrar de la lista"</string> <string name="transfer_clear_dlg_title" msgid="2953444575556460386">"Borrar"</string> - <string name="bluetooth_a2dp_sink_queue_name" msgid="6864149958708669766">"Está Sonando"</string> + <string name="bluetooth_a2dp_sink_queue_name" msgid="6864149958708669766">"Reproduciendo"</string> <string name="bluetooth_map_settings_save" msgid="7635491847388074606">"Guardar"</string> <string name="bluetooth_map_settings_cancel" msgid="9205350798049865699">"Cancelar"</string> <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"Selecciona las cuentas que quieras compartir por Bluetooth. Tendrás que aceptar cualquier acceso a las cuentas al establecer conexión."</string> diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml index f4b7e7a5c..c9b6335fb 100644 --- a/res/values-km/strings.xml +++ b/res/values-km/strings.xml @@ -122,7 +122,7 @@ <string name="transfer_menu_open" msgid="3368984869083107200">"បើក"</string> <string name="transfer_menu_clear" msgid="5854038118831427492">"សម្អាតពីបញ្ជី"</string> <string name="transfer_clear_dlg_title" msgid="2953444575556460386">"សម្អាត"</string> - <string name="bluetooth_a2dp_sink_queue_name" msgid="6864149958708669766">"Now Playing"</string> + <string name="bluetooth_a2dp_sink_queue_name" msgid="6864149958708669766">"ឥឡូវកំពុងចាក់"</string> <string name="bluetooth_map_settings_save" msgid="7635491847388074606">"រក្សាទុក"</string> <string name="bluetooth_map_settings_cancel" msgid="9205350798049865699">"បោះបង់"</string> <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"ជ្រើសគណនីដែលអ្នកចង់ចែករំលែកតាមរយៈប៊្លូធូស។ អ្នកនៅតែត្រូវទទួលយកលទ្ធភាពចូលដំណើរការទាំងឡាយទៅកាន់គណនីនេះដដែល នៅពេលភ្ជាប់។"</string> diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml index 0ed4aa9bd..a0fa232fc 100644 --- a/res/values-ky/strings.xml +++ b/res/values-ky/strings.xml @@ -78,7 +78,7 @@ <string name="not_exist_file_desc" msgid="4059531573790529229">"Мындай файл жок. \n"</string> <string name="enabling_progress_title" msgid="436157952334723406">"Күтө туруңуз…"</string> <string name="enabling_progress_content" msgid="4601542238119927904">"Bluetooth жандырылууда…"</string> - <string name="bt_toast_1" msgid="972182708034353383">"Файл алынат. Эскертмелер тактасынан жүрүшүн байкап турсаңыз болот."</string> + <string name="bt_toast_1" msgid="972182708034353383">"Файл алынат. Билдирмелер тактасынан жүрүшүн байкап турсаңыз болот."</string> <string name="bt_toast_2" msgid="8602553334099066582">"Файлды алуу мүмкүн эмес."</string> <string name="bt_toast_3" msgid="6707884165086862518">"\"<xliff:g id="SENDER">%1$s</xliff:g>\" жөнөткөн файлды алуу токтотулду"</string> <string name="bt_toast_4" msgid="4678812947604395649">"Кийинкиге файл жөнөтүлүүдө: \"<xliff:g id="RECIPIENT">%1$s</xliff:g>\""</string> @@ -125,7 +125,7 @@ <string name="bluetooth_a2dp_sink_queue_name" msgid="6864149958708669766">"Азыр эмне ойноп жатат?"</string> <string name="bluetooth_map_settings_save" msgid="7635491847388074606">"Сактоо"</string> <string name="bluetooth_map_settings_cancel" msgid="9205350798049865699">"Жокко чыгаруу"</string> - <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"Bluetooth аркылуу бөлүшө турган каттоо эсептерин тандаңыз. Туташкан сайын каттоо эсептерине кирүү мүмкүнчүлүгүн ырастап турушуңуз керек."</string> + <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"Bluetooth аркылуу бөлүшө турган каттоо эсептерин тандаңыз. Туташкан сайын аккаунттарына кирүү мүмкүнчүлүгүн ырастап турушуңуз керек."</string> <string name="bluetooth_map_settings_count" msgid="4557473074937024833">"Калган көзөнөктөр:"</string> <string name="bluetooth_map_settings_app_icon" msgid="7105805610929114707">"Колдонмонун сүрөтчөсү"</string> <string name="bluetooth_map_settings_title" msgid="7420332483392851321">"Bluetooth билдирүү бөлүшүү жөндөөлөрү"</string> diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml index 08e907f4f..5198b8762 100644 --- a/res/values-ml/strings.xml +++ b/res/values-ml/strings.xml @@ -20,7 +20,7 @@ <string name="permdesc_bluetoothShareManager" msgid="8930572979123190223">"BluetoothShare മാനേജർ ആക്സസ്സുചെയ്യാനും ഫയലുകൾ കൈമാറാൻ അത് ഉപയോഗിക്കാനും അപ്ലിക്കേഷനെ അനുവദിക്കുന്നു."</string> <string name="permlab_bluetoothWhitelist" msgid="7091552898592306386">"വൈറ്റ്ലിസ്റ്റ് ബ്ലൂടൂത്ത് ഉപകരണ ആക്സസ്സ്."</string> <string name="permdesc_bluetoothWhitelist" msgid="5494513855192170109">"ഒരു ബ്ലൂടൂത്ത് ഉപകരണം താൽക്കാലികമായി വൈറ്റ്ലിസ്റ്റുചെയ്യാൻ അപ്ലിക്കേഷനെ അനുവദിക്കുന്നു, അത് ഉപയോക്താവിന്റെ സ്ഥിരീകരണമില്ലാതെ ഈ ഉപകരണത്തിലേക്ക് ഫയലുകൾ അയയ്ക്കാൻ ആ ഉപകരണത്തെ അനുവദിക്കുന്നു."</string> - <string name="bt_share_picker_label" msgid="6268100924487046932">"ബ്ലൂടൂത്ത്"</string> + <string name="bt_share_picker_label" msgid="6268100924487046932">"Bluetooth"</string> <string name="unknown_device" msgid="9221903979877041009">"അജ്ഞാത ഉപകരണം"</string> <string name="unknownNumber" msgid="4994750948072751566">"അറിയില്ല"</string> <string name="airplane_error_title" msgid="2683839635115739939">"ഫ്ലൈറ്റ് മോഡ്"</string> diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml index 4a40dfe12..c60c8dec7 100644 --- a/res/values-vi/strings.xml +++ b/res/values-vi/strings.xml @@ -128,7 +128,7 @@ <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"Chọn tài khoản mà bạn muốn chia sẻ qua Bluetooth. Bạn vẫn phải chấp nhận mọi quyền truy cập vào tài khoản khi kết nối."</string> <string name="bluetooth_map_settings_count" msgid="4557473074937024833">"Số khe cắm còn lại:"</string> <string name="bluetooth_map_settings_app_icon" msgid="7105805610929114707">"Biểu tượng ứng dụng"</string> - <string name="bluetooth_map_settings_title" msgid="7420332483392851321">"Cài đặt chia sẻ thư qua Bluetooth"</string> + <string name="bluetooth_map_settings_title" msgid="7420332483392851321">"Cài đặt cách chia sẻ thư qua Bluetooth"</string> <string name="bluetooth_map_settings_no_account_slots_left" msgid="1796029082612965251">"Không chọn được tài khoản. Còn lại 0 khe cắm"</string> <string name="bluetooth_connected" msgid="6718623220072656906">"Đã kết nối âm thanh Bluetooth"</string> <string name="bluetooth_disconnected" msgid="3318303728981478873">"Đã ngắt kết nối âm thanh Bluetooth"</string> diff --git a/res/values/strings.xml b/res/values/strings.xml index 1389e0d35..0204f9830 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -249,4 +249,5 @@ <string name="bluetooth_disconnected">Bluetooth audio disconnected"</string> <string name="a2dp_sink_mbs_label">Bluetooth Audio</string> <string name="bluetooth_opp_file_limit_exceeded">Files bigger than 4GB cannot be transferred</string> + <string name="bluetooth_connect_action">Connect to Bluetooth</string> </resources> diff --git a/res/xml/authenticator.xml b/res/xml/authenticator.xml index b719fec4f..ab08a6103 100644 --- a/res/xml/authenticator.xml +++ b/res/xml/authenticator.xml @@ -17,5 +17,4 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:icon="@mipmap/bt_share" android:smallIcon="@mipmap/bt_share" - android:accountType="@string/pbap_account_type" - android:label="@string/pbap_account_type" /> + android:accountType="@string/pbap_account_type" /> 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/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/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); + } } } } diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java index c759a8a1f..b8fbbb9ef 100644 --- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java +++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java @@ -195,6 +195,21 @@ public class A2dpSinkStreamHandlerTest { } @Test + public void testFocusRerequest() { + // Focus was lost transiently, expect streaming to stop. + testSnkPlay(); + mStreamHandler.handleMessage( + mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE, + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)); + verify(mMockAudioManager, times(0)).abandonAudioFocus(any()); + verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0); + verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0); + mStreamHandler.handleMessage( + mStreamHandler.obtainMessage(A2dpSinkStreamHandler.REQUEST_FOCUS, true)); + verify(mMockAudioManager, times(2)).requestAudioFocus(any()); + } + + @Test public void testFocusGainTransient() { // Focus was lost then regained. testSnkPlay(); diff --git a/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java index 4fedc889a..907f0dcdc 100644 --- a/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java +++ b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java @@ -23,9 +23,11 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.media.AudioManager; -import android.media.session.MediaController; import android.os.Looper; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.PlaybackStateCompat; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; @@ -34,6 +36,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; +import com.android.bluetooth.a2dpsink.A2dpSinkService; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; @@ -66,15 +69,23 @@ public class AvrcpControllerStateMachineTest { private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class); private byte[] mTestAddress = new byte[]{00, 01, 02, 03, 04, 05}; - @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule(); + @Rule public final ServiceTestRule mAvrcpServiceRule = new ServiceTestRule(); + @Rule public final ServiceTestRule mA2dpServiceRule = new ServiceTestRule(); @Mock - private AdapterService mAdapterService; + private AdapterService mAvrcpAdapterService; + + @Mock + private AdapterService mA2dpAdapterService; + @Mock private AudioManager mAudioManager; @Mock private AvrcpControllerService mAvrcpControllerService; + @Mock + private Resources mMockResources; + AvrcpControllerStateMachine mAvrcpStateMachine; @Before @@ -90,9 +101,14 @@ public class AvrcpControllerStateMachineTest { // Setup mocks and test assets MockitoAnnotations.initMocks(this); - TestUtils.setAdapterService(mAdapterService); - TestUtils.startService(mServiceRule, AvrcpControllerService.class); - doReturn(mTargetContext.getResources()).when(mAvrcpControllerService).getResources(); + TestUtils.setAdapterService(mAvrcpAdapterService); + TestUtils.startService(mAvrcpServiceRule, AvrcpControllerService.class); + TestUtils.clearAdapterService(mAvrcpAdapterService); + TestUtils.setAdapterService(mA2dpAdapterService); + TestUtils.startService(mA2dpServiceRule, A2dpSinkService.class); + when(mMockResources.getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus)) + .thenReturn(true); + doReturn(mMockResources).when(mAvrcpControllerService).getResources(); doReturn(15).when(mAudioManager).getStreamMaxVolume(anyInt()); doReturn(8).when(mAudioManager).getStreamVolume(anyInt()); doReturn(true).when(mAudioManager).isVolumeFixed(); @@ -113,7 +129,7 @@ public class AvrcpControllerStateMachineTest { if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_avrcp_controller)) { return; } - TestUtils.clearAdapterService(mAdapterService); + TestUtils.clearAdapterService(mA2dpAdapterService); } /** @@ -141,6 +157,10 @@ public class AvrcpControllerStateMachineTest { IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Disconnected.class)); Assert.assertEquals(mAvrcpStateMachine.getState(), BluetoothProfile.STATE_DISCONNECTED); verify(mAvrcpControllerService).removeStateMachine(eq(mAvrcpStateMachine)); + MediaControllerCompat.TransportControls transportControls = + BluetoothMediaBrowserService.getTransportControls(); + Assert.assertEquals(PlaybackStateCompat.STATE_ERROR, + BluetoothMediaBrowserService.getPlaybackState()); } /** @@ -149,6 +169,11 @@ public class AvrcpControllerStateMachineTest { @Test public void testControlOnly() { int numBroadcastsSent = setUpConnectedState(true, false); + MediaControllerCompat.TransportControls transportControls = + BluetoothMediaBrowserService.getTransportControls(); + Assert.assertNotNull(transportControls); + Assert.assertEquals(PlaybackStateCompat.STATE_NONE, + BluetoothMediaBrowserService.getPlaybackState()); StackEvent event = StackEvent.connectionStateChanged(false, false); mAvrcpStateMachine.disconnect(); @@ -166,6 +191,8 @@ public class AvrcpControllerStateMachineTest { IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Disconnected.class)); Assert.assertEquals(mAvrcpStateMachine.getState(), BluetoothProfile.STATE_DISCONNECTED); verify(mAvrcpControllerService).removeStateMachine(eq(mAvrcpStateMachine)); + Assert.assertEquals(PlaybackStateCompat.STATE_ERROR, + BluetoothMediaBrowserService.getPlaybackState()); } /** @@ -176,6 +203,8 @@ public class AvrcpControllerStateMachineTest { Assert.assertEquals(0, mAvrcpControllerService.sBrowseTree.mRootNode.getChildrenCount()); int numBroadcastsSent = setUpConnectedState(false, true); Assert.assertEquals(1, mAvrcpControllerService.sBrowseTree.mRootNode.getChildrenCount()); + Assert.assertEquals(PlaybackStateCompat.STATE_NONE, + BluetoothMediaBrowserService.getPlaybackState()); StackEvent event = StackEvent.connectionStateChanged(false, false); mAvrcpStateMachine.disconnect(); @@ -193,6 +222,10 @@ public class AvrcpControllerStateMachineTest { IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Disconnected.class)); Assert.assertEquals(mAvrcpStateMachine.getState(), BluetoothProfile.STATE_DISCONNECTED); verify(mAvrcpControllerService).removeStateMachine(eq(mAvrcpStateMachine)); + MediaControllerCompat.TransportControls transportControls = + BluetoothMediaBrowserService.getTransportControls(); + Assert.assertEquals(PlaybackStateCompat.STATE_ERROR, + BluetoothMediaBrowserService.getPlaybackState()); } /** @@ -221,7 +254,7 @@ public class AvrcpControllerStateMachineTest { @Test public void testPlay() throws Exception { setUpConnectedState(true, true); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); //Play @@ -240,7 +273,7 @@ public class AvrcpControllerStateMachineTest { @Test public void testPause() throws Exception { setUpConnectedState(true, true); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); //Pause @@ -259,7 +292,7 @@ public class AvrcpControllerStateMachineTest { @Test public void testStop() throws Exception { setUpConnectedState(true, true); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); //Stop @@ -278,7 +311,7 @@ public class AvrcpControllerStateMachineTest { @Test public void testNext() throws Exception { setUpConnectedState(true, true); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); //Next @@ -298,7 +331,7 @@ public class AvrcpControllerStateMachineTest { @Test public void testPrevious() throws Exception { setUpConnectedState(true, true); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); //Previous @@ -318,7 +351,7 @@ public class AvrcpControllerStateMachineTest { @Test public void testFastForward() throws Exception { setUpConnectedState(true, true); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); //FastForward @@ -339,7 +372,7 @@ public class AvrcpControllerStateMachineTest { @Test public void testRewind() throws Exception { setUpConnectedState(true, true); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); //Rewind @@ -355,6 +388,69 @@ public class AvrcpControllerStateMachineTest { } /** + * Test media browser skip to queue item + */ + @Test + public void testSkipToQueueInvalid() throws Exception { + byte scope = 1; + int minSize = 0; + int maxSize = 255; + setUpConnectedState(true, true); + MediaControllerCompat.TransportControls transportControls = + BluetoothMediaBrowserService.getTransportControls(); + + //Play an invalid item below start + transportControls.skipToQueueItem(minSize - 1); + verify(mAvrcpControllerService, + timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(0)).playItemNative( + eq(mTestAddress), eq(scope), anyLong(), anyInt()); + + //Play an invalid item beyond end + transportControls.skipToQueueItem(maxSize + 1); + verify(mAvrcpControllerService, + timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(0)).playItemNative( + eq(mTestAddress), eq(scope), anyLong(), anyInt()); + } + + /** + * Test media browser shuffle command + */ + @Test + public void testShuffle() throws Exception { + byte[] shuffleSetting = new byte[]{3}; + byte[] shuffleMode = new byte[]{2}; + + setUpConnectedState(true, true); + MediaControllerCompat.TransportControls transportControls = + BluetoothMediaBrowserService.getTransportControls(); + + //Shuffle + transportControls.setShuffleMode(1); + verify(mAvrcpControllerService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) + .setPlayerApplicationSettingValuesNative( + eq(mTestAddress), eq((byte) 1), eq(shuffleSetting), eq(shuffleMode)); + } + + /** + * Test media browser repeat command + */ + @Test + public void testRepeat() throws Exception { + byte[] repeatSetting = new byte[]{2}; + byte[] repeatMode = new byte[]{3}; + + setUpConnectedState(true, true); + MediaControllerCompat.TransportControls transportControls = + BluetoothMediaBrowserService.getTransportControls(); + + //Shuffle + transportControls.setRepeatMode(2); + verify(mAvrcpControllerService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) + .setPlayerApplicationSettingValuesNative( + eq(mTestAddress), eq((byte) 1), eq(repeatSetting), eq(repeatMode)); + } + + /** * Test media browsing * Verify that a browse tree is created with the proper root * Verify that a player can be fetched and added to the browse tree @@ -483,7 +579,7 @@ public class AvrcpControllerStateMachineTest { BrowseTree.BrowseNode playerNodes = mAvrcpStateMachine.findNode(results.getID()); mAvrcpStateMachine.requestContents(results); - MediaController.TransportControls transportControls = + MediaControllerCompat.TransportControls transportControls = BluetoothMediaBrowserService.getTransportControls(); transportControls.play(); verify(mAvrcpControllerService, @@ -507,6 +603,45 @@ public class AvrcpControllerStateMachineTest { } /** + * Test playback does not request focus when another app is playing music. + */ + @Test + public void testPlaybackWhileMusicPlaying() { + when(mMockResources.getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus)) + .thenReturn(false); + Assert.assertEquals(AudioManager.AUDIOFOCUS_NONE, A2dpSinkService.getFocusState()); + doReturn(true).when(mAudioManager).isMusicActive(); + setUpConnectedState(true, true); + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_STATUS_CHANGED, + PlaybackStateCompat.STATE_PLAYING); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + verify(mAvrcpControllerService, + timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative( + eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE), eq(KEY_DOWN)); + TestUtils.waitForLooperToFinishScheduledTask( + A2dpSinkService.getA2dpSinkService().getMainLooper()); + Assert.assertEquals(AudioManager.AUDIOFOCUS_NONE, A2dpSinkService.getFocusState()); + } + + /** + * Test playback requests focus while nothing is playing music. + */ + @Test + public void testPlaybackWhileIdle() { + Assert.assertEquals(AudioManager.AUDIOFOCUS_NONE, A2dpSinkService.getFocusState()); + doReturn(false).when(mAudioManager).isMusicActive(); + setUpConnectedState(true, true); + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_STATUS_CHANGED, + PlaybackStateCompat.STATE_PLAYING); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + TestUtils.waitForLooperToFinishScheduledTask( + A2dpSinkService.getA2dpSinkService().getMainLooper()); + Assert.assertEquals(AudioManager.AUDIOFOCUS_GAIN, A2dpSinkService.getFocusState()); + } + + /** * Setup Connected State * * @return number of times mAvrcpControllerService.sendBroadcastAsUser() has been invoked diff --git a/tests/unit/src/com/android/bluetooth/mapclient/BmessageTest.java b/tests/unit/src/com/android/bluetooth/mapclient/BmessageTest.java new file mode 100644 index 000000000..acd05ed02 --- /dev/null +++ b/tests/unit/src/com/android/bluetooth/mapclient/BmessageTest.java @@ -0,0 +1,96 @@ +/* + * 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.mapclient; + +import static org.mockito.Mockito.*; + +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class BmessageTest { + private static final String TAG = BmessageTest.class.getSimpleName(); + private static final String SIMPLE_MMS_MESSAGE = + "BEGIN:BMSG\r\nVERSION:1.0\r\nSTATUS:READ\r\nTYPE:MMS\r\nFOLDER:null\r\nBEGIN:BENV\r\n" + + "BEGIN:VCARD\r\nVERSION:2.1\r\nN:null;;;;\r\nTEL:555-5555\r\nEND:VCARD\r\n" + + "BEGIN:BBODY\r\nLENGTH:39\r\nBEGIN:MSG\r\nThis is a new msg\r\nEND:MSG\r\n" + + "END:BBODY\r\nEND:BENV\r\nEND:BMSG\r\n"; + + private static final String NO_END_MESSAGE = + "BEGIN:BMSG\r\nVERSION:1.0\r\nSTATUS:READ\r\nTYPE:MMS\r\nFOLDER:null\r\nBEGIN:BENV\r\n" + + "BEGIN:VCARD\r\nVERSION:2.1\r\nN:null;;;;\r\nTEL:555-5555\r\nEND:VCARD\r\n" + + "BEGIN:BBODY\r\nLENGTH:39\r\nBEGIN:MSG\r\nThis is a new msg\r\n"; + + private static final String WRONG_LENGTH_MESSAGE = + "BEGIN:BMSG\r\nVERSION:1.0\r\nSTATUS:READ\r\nTYPE:MMS\r\nFOLDER:null\r\nBEGIN:BENV\r\n" + + "BEGIN:VCARD\r\nVERSION:2.1\r\nN:null;;;;\r\nTEL:555-5555\r\nEND:VCARD\r\n" + + "BEGIN:BBODY\r\nLENGTH:200\r\nBEGIN:MSG\r\nThis is a new msg\r\nEND:MSG\r\n" + + "END:BBODY\r\nEND:BENV\r\nEND:BMSG\r\n"; + + private static final String NO_BODY_MESSAGE = + "BEGIN:BMSG\r\nVERSION:1.0\r\nSTATUS:READ\r\nTYPE:MMS\r\nFOLDER:null\r\nBEGIN:BENV\r\n" + + "BEGIN:VCARD\r\nVERSION:2.1\r\nN:null;;;;\r\nTEL:555-5555\r\nEND:VCARD\r\n" + + "BEGIN:BBODY\r\nLENGTH:\r\n"; + + private static final String NEGATIVE_LENGTH_MESSAGE = + "BEGIN:BMSG\r\nVERSION:1.0\r\nSTATUS:READ\r\nTYPE:MMS\r\nFOLDER:null\r\nBEGIN:BENV\r\n" + + "BEGIN:VCARD\r\nVERSION:2.1\r\nN:null;;;;\r\nTEL:555-5555\r\nEND:VCARD\r\n" + + "BEGIN:BBODY\r\nLENGTH:-1\r\nBEGIN:MSG\r\nThis is a new msg\r\nEND:MSG\r\n" + + "END:BBODY\r\nEND:BENV\r\nEND:BMSG\r\n"; + + @Test + public void testNormalMessages() { + Bmessage message = BmessageParser.createBmessage(SIMPLE_MMS_MESSAGE); + Assert.assertNotNull(message); + } + + @Test + public void testParseWrongLengthMessage() { + Bmessage message = BmessageParser.createBmessage(WRONG_LENGTH_MESSAGE); + Assert.assertNull(message); + } + + @Test + public void testParseNoEndMessage() { + Bmessage message = BmessageParser.createBmessage(NO_END_MESSAGE); + Assert.assertNull(message); + } + + @Test + public void testParseReallyLongMessage() { + String testMessage = new String(new char[68048]).replace('\0', 'A'); + Bmessage message = BmessageParser.createBmessage(testMessage); + Assert.assertNull(message); + } + + @Test + public void testNoBodyMessage() { + Bmessage message = BmessageParser.createBmessage(NO_BODY_MESSAGE); + Assert.assertNull(message); + } + + @Test + public void testNegativeLengthMessage() { + Bmessage message = BmessageParser.createBmessage(NEGATIVE_LENGTH_MESSAGE); + Assert.assertNull(message); + } +} diff --git a/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java b/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java index 70854d8d1..ccfd9f810 100644 --- a/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java +++ b/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java @@ -150,6 +150,36 @@ public class MapClientStateMachineTest { Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState()); } + /** + * Test transition from STATE_CONNECTING --> (receive MSG_MAS_CONNECTED) --> STATE_CONNECTED + * --> (receive MSG_MAS_DISCONNECTED) --> STATE_DISCONNECTED + */ + @Test + public void testStateTransitionFromConnectedWithMasDisconnected() { + Log.i(TAG, "in testStateTransitionFromConnectedWithMasDisconnected"); + + setupSdpRecordReceipt(); + Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED); + mMceStateMachine.sendMessage(msg); + + // Wait until the message is processed and a broadcast request is sent to + // to MapClientService to change + // state from STATE_CONNECTING to STATE_CONNECTED + verify(mMockMapClientService, + timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast( + mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM)); + Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState()); + + msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_DISCONNECTED); + mMceStateMachine.sendMessage(msg); + verify(mMockMapClientService, + timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(4)).sendBroadcast( + mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM)); + + Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, mMceStateMachine.getState()); + } + + /** * Test receiving an empty event report */ diff --git a/tests/unit/src/com/android/bluetooth/mapclient/ObexTimeTest.java b/tests/unit/src/com/android/bluetooth/mapclient/ObexTimeTest.java new file mode 100644 index 000000000..5ef2d455b --- /dev/null +++ b/tests/unit/src/com/android/bluetooth/mapclient/ObexTimeTest.java @@ -0,0 +1,114 @@ +/* + * 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.mapclient; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Date; +import java.util.TimeZone; + +@RunWith(AndroidJUnit4.class) +public class ObexTimeTest { + private static final String TAG = ObexTimeTest.class.getSimpleName(); + + private static final String VALID_TIME_STRING = "20190101T121314"; + private static final String VALID_TIME_STRING_WITH_OFFSET_POS = "20190101T121314+0130"; + private static final String VALID_TIME_STRING_WITH_OFFSET_NEG = "20190101T121314-0130"; + + private static final String INVALID_TIME_STRING_OFFSET_EXTRA_DIGITS = "20190101T121314-99999"; + private static final String INVALID_TIME_STRING_BAD_DELIMITER = "20190101Q121314"; + + // MAP message listing times, per spec, use "local time basis" if UTC offset isn't given. The + // ObexTime class parses using the current default timezone (assumed to be the "local timezone") + // in the case that UTC isn't provided. However, the Date class assumes UTC ALWAYS when + // initializing off of a long value. We have to take that into account when determining our + // expected results for time strings that don't have an offset. + private static final long LOCAL_TIMEZONE_OFFSET = TimeZone.getDefault().getRawOffset(); + + // If you are a positive offset from GMT then GMT is in the "past" and you need to subtract that + // offset from the time. If you are negative then GMT is in the future and you need to add that + // offset to the time. + private static final long VALID_TS = 1546344794000L; // Jan 01, 2019 at 12:13:14 GMT + private static final long TS_OFFSET = 5400000L; // 1 Hour, 30 minutes -> milliseconds + private static final long VALID_TS_LOCAL_TZ = VALID_TS - LOCAL_TIMEZONE_OFFSET; + private static final long VALID_TS_OFFSET_POS = VALID_TS - TS_OFFSET; + private static final long VALID_TS_OFFSET_NEG = VALID_TS + TS_OFFSET; + + private static final Date VALID_DATE_LOCAL_TZ = new Date(VALID_TS_LOCAL_TZ); + private static final Date VALID_DATE_WITH_OFFSET_POS = new Date(VALID_TS_OFFSET_POS); + private static final Date VALID_DATE_WITH_OFFSET_NEG = new Date(VALID_TS_OFFSET_NEG); + + @Test + public void createWithValidDateTimeString_TimestampCorrect() { + ObexTime timestamp = new ObexTime(VALID_TIME_STRING); + Assert.assertEquals("Parsed timestamp must match expected", VALID_DATE_LOCAL_TZ, + timestamp.getTime()); + } + + @Test + public void createWithValidDateTimeStringWithPosOffset_TimestampCorrect() { + ObexTime timestamp = new ObexTime(VALID_TIME_STRING_WITH_OFFSET_POS); + Assert.assertEquals("Parsed timestamp must match expected", VALID_DATE_WITH_OFFSET_POS, + timestamp.getTime()); + } + + @Test + public void createWithValidDateTimeStringWithNegOffset_TimestampCorrect() { + ObexTime timestamp = new ObexTime(VALID_TIME_STRING_WITH_OFFSET_NEG); + Assert.assertEquals("Parsed timestamp must match expected", VALID_DATE_WITH_OFFSET_NEG, + timestamp.getTime()); + } + + @Test + public void createWithValidDate_TimestampCorrect() { + ObexTime timestamp = new ObexTime(VALID_DATE_LOCAL_TZ); + Assert.assertEquals("ObexTime created with a date must return the same date", + VALID_DATE_LOCAL_TZ, timestamp.getTime()); + } + + @Test + public void printValidTime_TimestampMatchesInput() { + ObexTime timestamp = new ObexTime(VALID_TIME_STRING); + Assert.assertEquals("Timestamp as a string must match the input string", VALID_TIME_STRING, + timestamp.toString()); + } + + @Test + public void createWithInvalidDelimiterString_TimestampIsNull() { + ObexTime timestamp = new ObexTime(INVALID_TIME_STRING_BAD_DELIMITER); + Assert.assertEquals("Parsed timestamp was invalid and must result in a null object", null, + timestamp.getTime()); + } + + @Test + public void createWithInvalidOffsetString_TimestampIsNull() { + ObexTime timestamp = new ObexTime(INVALID_TIME_STRING_OFFSET_EXTRA_DIGITS); + Assert.assertEquals("Parsed timestamp was invalid and must result in a null object", null, + timestamp.getTime()); + } + + @Test + public void printInvalidTime_ReturnsNull() { + ObexTime timestamp = new ObexTime(INVALID_TIME_STRING_BAD_DELIMITER); + Assert.assertEquals("Invalid timestamps must return null for toString()", null, + timestamp.toString()); + } +} |