| /* |
| * Copyright (C) 2017 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.pmc; |
| |
| import android.app.AlarmManager; |
| import android.app.PendingIntent; |
| import android.bluetooth.BluetoothA2dp; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothCodecConfig; |
| import android.bluetooth.BluetoothCodecStatus; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.media.MediaPlayer; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Bluetooth A2DP Receiver functions for codec power testing. |
| */ |
| public class A2dpReceiver extends BroadcastReceiver { |
| public static final String TAG = "A2DPPOWER"; |
| public static final String A2DP_INTENT = "com.android.pmc.A2DP"; |
| public static final String A2DP_ALARM = "com.android.pmc.A2DP.Alarm"; |
| public static final int THOUSAND = 1000; |
| public static final int WAIT_SECONDS = 10; |
| public static final int ALARM_MESSAGE = 1; |
| |
| public static final float NORMAL_VOLUME = 0.3f; |
| public static final float ZERO_VOLUME = 0.0f; |
| |
| private final Context mContext; |
| private final AlarmManager mAlarmManager; |
| private final BluetoothAdapter mBluetoothAdapter; |
| |
| private MediaPlayer mPlayer; |
| private BluetoothA2dp mBluetoothA2dp; |
| |
| private PMCStatusLogger mPMCStatusLogger; |
| |
| /** |
| * BroadcastReceiver() to get status after calling setCodecConfigPreference() |
| * |
| */ |
| private BroadcastReceiver mBluetoothA2dpReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Log.d(TAG, "mBluetoothA2dpReceiver.onReceive() intent=" + intent); |
| String action = intent.getAction(); |
| |
| if (BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED.equals(action)) { |
| getCodecValue(true); |
| } |
| } |
| }; |
| |
| /** |
| * ServiceListener for A2DP connection/disconnection event |
| * |
| */ |
| private BluetoothProfile.ServiceListener mBluetoothA2dpServiceListener = |
| new BluetoothProfile.ServiceListener() { |
| public void onServiceConnected(int profile, |
| BluetoothProfile proxy) { |
| Log.d(TAG, "BluetoothA2dpServiceListener.onServiceConnected"); |
| mBluetoothA2dp = (BluetoothA2dp) proxy; |
| getCodecValue(true); |
| } |
| |
| public void onServiceDisconnected(int profile) { |
| Log.d(TAG, "BluetoothA2dpServiceListener.onServiceDisconnected"); |
| mBluetoothA2dp = null; |
| } |
| }; |
| |
| /** |
| * Constructor to be called by PMC |
| * |
| * @param context - PMC will provide a context |
| * @param alarmManager - PMC will provide alarmManager |
| */ |
| public A2dpReceiver(Context context, AlarmManager alarmManager) { |
| // Prepare for setting alarm service |
| mContext = context; |
| mAlarmManager = alarmManager; |
| |
| mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| if (mBluetoothAdapter == null) { |
| Log.e(TAG, "BluetoothAdapter is Null"); |
| return; |
| } else { |
| if (!mBluetoothAdapter.isEnabled()) { |
| Log.d(TAG, "BluetoothAdapter is NOT enabled, enable now"); |
| mBluetoothAdapter.enable(); |
| if (!mBluetoothAdapter.isEnabled()) { |
| Log.e(TAG, "Can't enable Bluetooth"); |
| return; |
| } |
| } |
| } |
| // Setup BroadcastReceiver for ACTION_CODEC_CONFIG_CHANGED |
| IntentFilter filter = new IntentFilter(); |
| if (mBluetoothAdapter != null) { |
| mBluetoothAdapter.getProfileProxy(mContext, |
| mBluetoothA2dpServiceListener, |
| BluetoothProfile.A2DP); |
| Log.d(TAG, "After getProfileProxy()"); |
| } |
| filter = new IntentFilter(); |
| filter.addAction(BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED); |
| mContext.registerReceiver(mBluetoothA2dpReceiver, filter); |
| |
| Log.d(TAG, "A2dpReceiver()"); |
| } |
| |
| /** |
| * initialize() to setup Bluetooth adapters and check if Bluetooth device is connected |
| * it is called when PMC command is received to start streaming |
| */ |
| private boolean initialize() { |
| Log.d(TAG, "Start initialize()"); |
| |
| // Check if any Bluetooth devices are connected |
| ArrayList<BluetoothDevice> results = new ArrayList<BluetoothDevice>(); |
| Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices(); |
| if (bondedDevices == null) { |
| Log.e(TAG, "Bonded devices list is null"); |
| return false; |
| } |
| for (BluetoothDevice bd : bondedDevices) { |
| if (bd.isConnected()) { |
| results.add(bd); |
| } |
| } |
| |
| if (results.isEmpty()) { |
| Log.e(TAG, "No device is connected"); |
| return false; |
| } |
| |
| Log.d(TAG, "Finish initialize()"); |
| |
| return true; |
| } |
| |
| /** |
| * Method to receive the broadcast from Python client or AlarmManager |
| * |
| * @param context - system will provide a context to this function |
| * @param intent - system will provide an intent to this function |
| */ |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (!intent.getAction().equals(A2DP_INTENT)) return; |
| boolean alarm = intent.hasExtra(A2DP_ALARM); |
| if (alarm) { |
| Log.v(TAG, "Alarm Message to Stop playing"); |
| mPMCStatusLogger.logStatus("SUCCEED"); |
| mPlayer.stop(); |
| // Release the Media Player |
| mPlayer.release(); |
| } else { |
| Log.d(TAG, "Received PMC command message"); |
| processParameters(intent); |
| } |
| } |
| |
| /** |
| * Method to process parameters from Python client |
| * |
| * @param intent - system will provide an intent to this function |
| */ |
| private void processParameters(Intent intent) { |
| int codecType = BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID; |
| int sampleRate = BluetoothCodecConfig.SAMPLE_RATE_NONE; |
| int bitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_NONE; |
| int channelMode = BluetoothCodecConfig.CHANNEL_MODE_STEREO; |
| // codecSpecific1 is for LDAC quality so far |
| // Other code specific values are not used now |
| long codecSpecific1 = 0, codecSpecific2 = 0, codecSpecific3 = 0, |
| codecSpecific4 = 0; |
| int playTime = 0; |
| String musicUrl; |
| String tmpStr; |
| |
| // Create the logger object |
| mPMCStatusLogger = new PMCStatusLogger(TAG + ".log", TAG); |
| |
| // For a baseline case when Blueooth is off but music is playing with speaker is muted |
| boolean bt_off_mute = false; |
| |
| Bundle extras = intent.getExtras(); |
| |
| if (extras == null) { |
| Log.e(TAG, "No parameters specified"); |
| return; |
| } |
| |
| if (extras.containsKey("BT_OFF_Mute")) { |
| Log.v(TAG, "Mute is specified for Bluetooth off baseline case"); |
| bt_off_mute = true; |
| } |
| |
| // initialize() if we are testing over Bluetooth, we do NOT test |
| // over bluetooth for the play music with Bluetooth off test case. |
| if (!bt_off_mute) { |
| if (!initialize()) { |
| mPMCStatusLogger.logStatus("initialize() Failed"); |
| return; |
| } |
| } |
| // Check if it is baseline Bluetooth is on but not stream |
| if (extras.containsKey("BT_ON_NotPlay")) { |
| Log.v(TAG, "NotPlay is specified for baseline case that only Bluetooth is on"); |
| // Do nothing further |
| mPMCStatusLogger.logStatus("READY"); |
| mPMCStatusLogger.logStatus("SUCCEED"); |
| return; |
| } |
| |
| if (!extras.containsKey("PlayTime")) { |
| Log.e(TAG, "No Play Time specified"); |
| return; |
| } |
| tmpStr = extras.getString("PlayTime"); |
| Log.d(TAG, "Play Time = " + tmpStr); |
| playTime = Integer.valueOf(tmpStr); |
| |
| if (!extras.containsKey("MusicURL")) { |
| Log.e(TAG, "No Music URL specified"); |
| return; |
| } |
| musicUrl = extras.getString("MusicURL"); |
| Log.d(TAG, "Music URL = " + musicUrl); |
| |
| // playTime and musicUrl are necessary |
| if (playTime == 0 || musicUrl.isEmpty() || musicUrl == null) { |
| Log.d(TAG, "Invalid paramters"); |
| return; |
| } |
| // Check if it is the baseline that Bluetooth is off but streaming with speakers muted |
| if (!bt_off_mute) { |
| if (!extras.containsKey("CodecType")) { |
| Log.e(TAG, "No Codec Type specified"); |
| return; |
| } |
| tmpStr = extras.getString("CodecType"); |
| Log.d(TAG, "Codec Type= " + tmpStr); |
| codecType = Integer.valueOf(tmpStr); |
| |
| if (!extras.containsKey("SampleRate")) { |
| Log.e(TAG, "No Sample Rate specified"); |
| return; |
| } |
| tmpStr = extras.getString("SampleRate"); |
| Log.d(TAG, "Sample Rate = " + tmpStr); |
| sampleRate = Integer.valueOf(tmpStr); |
| |
| if (!extras.containsKey("BitsPerSample")) { |
| Log.e(TAG, "No BitsPerSample specified"); |
| return; |
| } |
| tmpStr = extras.getString("BitsPerSample"); |
| Log.d(TAG, "BitsPerSample = " + tmpStr); |
| bitsPerSample = Integer.valueOf(tmpStr); |
| |
| if (extras.containsKey("ChannelMode")) { |
| tmpStr = extras.getString("ChannelMode"); |
| Log.d(TAG, "ChannelMode = " + tmpStr); |
| channelMode = Integer.valueOf(tmpStr); |
| } |
| |
| if (extras.containsKey("LdacPlaybackQuality")) { |
| tmpStr = extras.getString("LdacPlaybackQuality"); |
| Log.d(TAG, "LdacPlaybackQuality = " + tmpStr); |
| codecSpecific1 = Integer.valueOf(tmpStr); |
| } |
| |
| if (extras.containsKey("CodecSpecific2")) { |
| tmpStr = extras.getString("CodecSpecific2"); |
| Log.d(TAG, "CodecSpecific2 = " + tmpStr); |
| codecSpecific1 = Integer.valueOf(tmpStr); |
| } |
| |
| if (extras.containsKey("CodecSpecific3")) { |
| tmpStr = extras.getString("CodecSpecific3"); |
| Log.d(TAG, "CodecSpecific3 = " + tmpStr); |
| codecSpecific1 = Integer.valueOf(tmpStr); |
| } |
| |
| if (extras.containsKey("CodecSpecific4")) { |
| tmpStr = extras.getString("CodecSpecific4"); |
| Log.d(TAG, "CodecSpecific4 = " + tmpStr); |
| codecSpecific1 = Integer.valueOf(tmpStr); |
| } |
| |
| if (codecType == BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID |
| || sampleRate == BluetoothCodecConfig.SAMPLE_RATE_NONE |
| || bitsPerSample == BluetoothCodecConfig.BITS_PER_SAMPLE_NONE) { |
| Log.d(TAG, "Invalid parameters"); |
| return; |
| } |
| } |
| |
| if (playMusic(musicUrl, bt_off_mute)) { |
| // Set the requested Codecs on the device for normal codec cases |
| if (!bt_off_mute) { |
| if (!setCodecValue(codecType, sampleRate, bitsPerSample, channelMode, |
| codecSpecific1, codecSpecific2, codecSpecific3, codecSpecific4)) { |
| mPMCStatusLogger.logStatus("setCodecValue() Failed"); |
| } |
| } |
| mPMCStatusLogger.logStatus("READY"); |
| startAlarm(playTime); |
| } else { |
| mPMCStatusLogger.logStatus("playMusic() Failed"); |
| } |
| } |
| |
| |
| /** |
| * Function to setup MediaPlayer and play music |
| * |
| * @param musicURL - Music URL |
| * @param btOffMute - true is to mute speakers |
| * |
| */ |
| private boolean playMusic(String musicURL, boolean btOffMute) { |
| |
| mPlayer = MediaPlayer.create(mContext, Uri.parse(musicURL)); |
| if (mPlayer == null) { |
| Log.e(TAG, "Failed to create Media Player"); |
| return false; |
| } |
| Log.d(TAG, "Media Player created: " + musicURL); |
| |
| if (btOffMute) { |
| Log.v(TAG, "Mute Speakers for Bluetooth off baseline case"); |
| mPlayer.setVolume(ZERO_VOLUME, ZERO_VOLUME); |
| } else { |
| Log.d(TAG, "Set Normal Volume for speakers"); |
| mPlayer.setVolume(NORMAL_VOLUME, NORMAL_VOLUME); |
| } |
| // Play Music now and setup looping |
| mPlayer.start(); |
| mPlayer.setLooping(true); |
| if (!mPlayer.isPlaying()) { |
| Log.e(TAG, "Media Player is not playing"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Function to be called to start alarm |
| * |
| * @param alarmStartTime - time when the music needs to be started or stopped |
| */ |
| private void startAlarm(int alarmStartTime) { |
| |
| Intent alarmIntent = new Intent(A2DP_INTENT); |
| alarmIntent.putExtra(A2DP_ALARM, ALARM_MESSAGE); |
| |
| long triggerTime = SystemClock.elapsedRealtime() |
| + alarmStartTime * THOUSAND; |
| mAlarmManager.setExactAndAllowWhileIdle( |
| AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, |
| PendingIntent.getBroadcast(mContext, 0, |
| alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT)); |
| } |
| |
| /** |
| * Function to get current codec config |
| * @param printCapabilities - Flag to indicate if to print local and selectable capabilities |
| */ |
| private BluetoothCodecConfig getCodecValue(boolean printCapabilities) { |
| BluetoothCodecStatus codecStatus = null; |
| BluetoothCodecConfig codecConfig = null; |
| List<BluetoothCodecConfig> codecsLocalCapabilities = new ArrayList<>(); |
| List<BluetoothCodecConfig> codecsSelectableCapabilities = new ArrayList<>(); |
| |
| if (mBluetoothA2dp != null) { |
| BluetoothDevice activeDevice = getA2dpActiveDevice(); |
| if (activeDevice == null) { |
| Log.e(TAG, "getCodecValue: Active device is null"); |
| return null; |
| } |
| codecStatus = mBluetoothA2dp.getCodecStatus(activeDevice); |
| if (codecStatus != null) { |
| codecConfig = codecStatus.getCodecConfig(); |
| codecsLocalCapabilities = codecStatus.getCodecsLocalCapabilities(); |
| codecsSelectableCapabilities = codecStatus.getCodecsSelectableCapabilities(); |
| } |
| } |
| if (codecConfig == null) return null; |
| |
| Log.d(TAG, "GetCodecValue: " + codecConfig.toString()); |
| |
| if (printCapabilities) { |
| Log.d(TAG, "Local Codec Capabilities "); |
| for (BluetoothCodecConfig config : codecsLocalCapabilities) { |
| Log.d(TAG, config.toString()); |
| } |
| Log.d(TAG, "Codec Selectable Capabilities: "); |
| for (BluetoothCodecConfig config : codecsSelectableCapabilities) { |
| Log.d(TAG, config.toString()); |
| } |
| } |
| return codecConfig; |
| } |
| |
| /** |
| * Function to set new codec config |
| * |
| * @param codecType - Codec Type |
| * @param sampleRate - Sample Rate |
| * @param bitsPerSample - Bit Per Sample |
| * @param codecSpecific1 - LDAC playback quality |
| * @param codecSpecific2 - codecSpecific2 |
| * @param codecSpecific3 - codecSpecific3 |
| * @param codecSpecific4 - codecSpecific4 |
| */ |
| private boolean setCodecValue(int codecType, int sampleRate, int bitsPerSample, |
| int channelMode, long codecSpecific1, long codecSpecific2, |
| long codecSpecific3, long codecSpecific4) { |
| Log.d(TAG, "SetCodecValue: Codec Type: " + codecType + " sampleRate: " + sampleRate |
| + " bitsPerSample: " + bitsPerSample + " Channel Mode: " + channelMode |
| + " LDAC quality: " + codecSpecific1); |
| |
| BluetoothCodecConfig codecConfig = new BluetoothCodecConfig.Builder() |
| .setCodecType(codecType) |
| .setCodecPriority(BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST) |
| .setSampleRate(sampleRate) |
| .setBitsPerSample(bitsPerSample) |
| .setChannelMode(channelMode) |
| .setCodecSpecific1(codecSpecific1) |
| .setCodecSpecific2(codecSpecific2) |
| .setCodecSpecific3(codecSpecific3) |
| .setCodecSpecific4(codecSpecific4) |
| .build(); |
| |
| // Wait here to see if mBluetoothA2dp is set |
| for (int i = 0; i < WAIT_SECONDS; i++) { |
| Log.d(TAG, "Wait for BluetoothA2dp"); |
| if (mBluetoothA2dp != null) { |
| break; |
| } |
| |
| try { |
| Thread.sleep(THOUSAND); |
| } catch (InterruptedException e) { |
| Log.d(TAG, "Sleep is interrupted"); |
| } |
| } |
| |
| if (mBluetoothA2dp != null) { |
| BluetoothDevice activeDevice = getA2dpActiveDevice(); |
| if (activeDevice == null) { |
| Log.e(TAG, "setCodecValue: Active device is null. Codec is not set."); |
| return false; |
| } |
| Log.d(TAG, "setCodecConfigPreference()"); |
| mBluetoothA2dp.setCodecConfigPreference(getA2dpActiveDevice(), codecConfig); |
| } else { |
| Log.e(TAG, "mBluetoothA2dp is null. Codec is not set"); |
| return false; |
| } |
| // Wait here to see if the codec is changed to new value |
| for (int i = 0; i < WAIT_SECONDS; i++) { |
| if (verifyCodeConfig(codecType, sampleRate, |
| bitsPerSample, channelMode, codecSpecific1)) { |
| break; |
| } |
| try { |
| Thread.sleep(THOUSAND); |
| } catch (InterruptedException e) { |
| Log.d(TAG, "Sleep is interrupted"); |
| } |
| } |
| if (!verifyCodeConfig(codecType, sampleRate, |
| bitsPerSample, channelMode, codecSpecific1)) { |
| Log.e(TAG, "Codec config is NOT set correctly"); |
| return false; |
| } |
| return true; |
| } |
| |
| private BluetoothDevice getA2dpActiveDevice() { |
| if (mBluetoothAdapter == null) { |
| return null; |
| } |
| List<BluetoothDevice> activeDevices = |
| mBluetoothAdapter.getActiveDevices(BluetoothProfile.A2DP); |
| return (activeDevices.size() > 0) ? activeDevices.get(0) : null; |
| } |
| |
| /** |
| * Method to verify if the codec config values are changed |
| * |
| * @param codecType - Codec Type |
| * @param sampleRate - Sample Rate |
| * @param bitsPerSample - Bit Per Sample |
| * @param codecSpecific1 - LDAC playback quality |
| */ |
| private boolean verifyCodeConfig(int codecType, int sampleRate, int bitsPerSample, |
| int channelMode, long codecSpecific1) { |
| BluetoothCodecConfig codecConfig = null; |
| codecConfig = getCodecValue(false); |
| if (codecConfig == null) return false; |
| |
| if (codecType == BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC) { |
| if (codecConfig.getCodecType() == codecType |
| && codecConfig.getSampleRate() == sampleRate |
| && codecConfig.getBitsPerSample() == bitsPerSample |
| && codecConfig.getChannelMode() == channelMode |
| && codecConfig.getCodecSpecific1() == codecSpecific1) return true; |
| } else { |
| if (codecConfig.getCodecType() == codecType |
| && codecConfig.getSampleRate() == sampleRate |
| && codecConfig.getBitsPerSample() == bitsPerSample |
| && codecConfig.getChannelMode() == channelMode) return true; |
| } |
| |
| return false; |
| } |
| |
| } |