/* * Copyright (c) 2010-2019 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.linphone.call; import static android.media.AudioManager.MODE_RINGTONE; import static android.media.AudioManager.STREAM_RING; import static android.media.AudioManager.STREAM_VOICE_CALL; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.os.Vibrator; import android.provider.Settings; import android.telephony.TelephonyManager; import android.view.KeyEvent; import java.io.FileInputStream; import java.io.IOException; import java.util.List; import org.linphone.LinphoneContext; import org.linphone.LinphoneManager; import org.linphone.R; import org.linphone.compatibility.Compatibility; import org.linphone.core.Address; import org.linphone.core.Call; import org.linphone.core.Core; import org.linphone.core.CoreListenerStub; import org.linphone.core.EcCalibratorStatus; import org.linphone.core.tools.Log; import org.linphone.receivers.BluetoothReceiver; import org.linphone.receivers.HeadsetReceiver; import org.linphone.settings.LinphonePreferences; public class AndroidAudioManager { private Context mContext; private AudioManager mAudioManager; private Call mRingingCall; private MediaPlayer mRingerPlayer; private final Vibrator mVibrator; private BluetoothAdapter mBluetoothAdapter; private BluetoothHeadset mBluetoothHeadset; private BluetoothReceiver mBluetoothReceiver; private HeadsetReceiver mHeadsetReceiver; private boolean mHeadsetReceiverRegistered; private boolean mIsRinging; private boolean mAudioFocused; private boolean mEchoTesterIsRunning; private boolean mIsBluetoothHeadsetConnected; private boolean mIsBluetoothHeadsetScoConnected; private CoreListenerStub mListener; public AndroidAudioManager(Context context) { mContext = context; mAudioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mEchoTesterIsRunning = false; mHeadsetReceiverRegistered = false; startBluetooth(); mListener = new CoreListenerStub() { @Override public void onCallStateChanged( final Core core, final Call call, final Call.State state, final String message) { if (state == Call.State.IncomingReceived || (state == Call.State.IncomingEarlyMedia && mContext.getResources() .getBoolean( R.bool.allow_ringing_while_early_media))) { // Brighten screen for at least 10 seconds if (core.getCallsNb() == 1) { requestAudioFocus(STREAM_RING); mRingingCall = call; startRinging(call.getRemoteAddress()); // otherwise there is the beep } } else if (call == mRingingCall && mIsRinging) { // previous state was ringing, so stop ringing stopRinging(); } if (state == Call.State.Connected) { if (core.getCallsNb() == 1) { // It is for incoming calls, because outgoing calls enter // MODE_IN_COMMUNICATION immediately when they start. // However, incoming call first use the MODE_RINGING to play the // local ring. if (call.getDir() == Call.Dir.Incoming) { setAudioManagerInCallMode(); // mAudioManager.abandonAudioFocus(null); requestAudioFocus(STREAM_VOICE_CALL); } if (!mIsBluetoothHeadsetConnected) { if (mContext.getResources().getBoolean(R.bool.isTablet)) { routeAudioToSpeaker(); } else { // Only force earpiece audio route for incoming audio calls, // outgoing calls may have manually enabled speaker if (call.getDir() == Call.Dir.Incoming) { routeAudioToEarPiece(); } } } // Only register this one when a call is active enableHeadsetReceiver(); } } else if (state == Call.State.End || state == Call.State.Error) { if (core.getCallsNb() == 0) { if (mAudioFocused) { int res = mAudioManager.abandonAudioFocus(null); Log.d( "[Audio Manager] Audio focus released a bit later: " + (res == AudioManager .AUDIOFOCUS_REQUEST_GRANTED ? "Granted" : "Denied")); mAudioFocused = false; } // Only register this one when a call is active if (mHeadsetReceiver != null && mHeadsetReceiverRegistered) { Log.i("[Audio Manager] Unregistering headset receiver"); mContext.unregisterReceiver(mHeadsetReceiver); mHeadsetReceiverRegistered = false; } TelephonyManager tm = (TelephonyManager) mContext.getSystemService( Context.TELEPHONY_SERVICE); if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) { Log.d( "[Audio Manager] ---AndroidAudioManager: back to MODE_NORMAL"); mAudioManager.setMode(AudioManager.MODE_NORMAL); Log.d( "[Audio Manager] All call terminated, routing back to earpiece"); routeAudioToEarPiece(); } } } if (state == Call.State.OutgoingInit) { // Enter the MODE_IN_COMMUNICATION mode as soon as possible, so that // ringback is heard normally in earpiece or bluetooth receiver. setAudioManagerInCallMode(); requestAudioFocus(STREAM_VOICE_CALL); if (mIsBluetoothHeadsetConnected) { routeAudioToBluetooth(); } } if (state == Call.State.StreamsRunning) { setAudioManagerInCallMode(); if (mIsBluetoothHeadsetConnected) { routeAudioToBluetooth(); } } } @Override public void onEcCalibrationResult( Core core, EcCalibratorStatus status, int delay_ms) { mAudioManager.setMode(AudioManager.MODE_NORMAL); mAudioManager.abandonAudioFocus(null); Log.i("[Audio Manager] Set audio mode on 'Normal'"); } }; Core core = LinphoneManager.getCore(); if (core != null) { core.addListener(mListener); } } public void destroy() { if (mBluetoothAdapter != null && mBluetoothHeadset != null) { Log.i("[Audio Manager] [Bluetooth] Closing HEADSET profile proxy"); mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset); } Log.i("[Audio Manager] [Bluetooth] Unegistering bluetooth receiver"); if (mBluetoothReceiver != null) { mContext.unregisterReceiver(mBluetoothReceiver); } Core core = LinphoneManager.getCore(); if (core != null) { core.removeListener(mListener); } } /* Audio routing */ public void setAudioManagerModeNormal() { mAudioManager.setMode(AudioManager.MODE_NORMAL); } public void routeAudioToEarPiece() { routeAudioToSpeakerHelper(false); } public void routeAudioToSpeaker() { routeAudioToSpeakerHelper(true); } public boolean isAudioRoutedToSpeaker() { return mAudioManager.isSpeakerphoneOn() && !isUsingBluetoothAudioRoute(); } public boolean isAudioRoutedToEarpiece() { return !mAudioManager.isSpeakerphoneOn() && !isUsingBluetoothAudioRoute(); } /* Echo cancellation */ public void startEcCalibration() { Core core = LinphoneManager.getCore(); if (core == null) { return; } routeAudioToSpeaker(); setAudioManagerInCallMode(); Log.i("[Audio Manager] Set audio mode on 'Voice Communication'"); requestAudioFocus(STREAM_VOICE_CALL); int oldVolume = mAudioManager.getStreamVolume(STREAM_VOICE_CALL); int maxVolume = mAudioManager.getStreamMaxVolume(STREAM_VOICE_CALL); mAudioManager.setStreamVolume(STREAM_VOICE_CALL, maxVolume, 0); core.startEchoCancellerCalibration(); mAudioManager.setStreamVolume(STREAM_VOICE_CALL, oldVolume, 0); } public void startEchoTester() { Core core = LinphoneManager.getCore(); if (core == null) { return; } routeAudioToSpeaker(); setAudioManagerInCallMode(); Log.i("[Audio Manager] Set audio mode on 'Voice Communication'"); requestAudioFocus(STREAM_VOICE_CALL); int maxVolume = mAudioManager.getStreamMaxVolume(STREAM_VOICE_CALL); int sampleRate; mAudioManager.setStreamVolume(STREAM_VOICE_CALL, maxVolume, 0); String sampleRateProperty = mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); sampleRate = Integer.parseInt(sampleRateProperty); core.startEchoTester(sampleRate); mEchoTesterIsRunning = true; } public void stopEchoTester() { Core core = LinphoneManager.getCore(); if (core == null) { return; } mEchoTesterIsRunning = false; core.stopEchoTester(); routeAudioToEarPiece(); mAudioManager.setMode(AudioManager.MODE_NORMAL); Log.i("[Audio Manager] Set audio mode on 'Normal'"); } public boolean getEchoTesterStatus() { return mEchoTesterIsRunning; } public boolean onKeyVolumeAdjust(int keyCode) { if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { adjustVolume(1); return true; } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { adjustVolume(-1); return true; } return false; } private void setAudioManagerInCallMode() { if (mAudioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION) { Log.w("[Audio Manager] already in MODE_IN_COMMUNICATION, skipping..."); return; } Log.d("[Audio Manager] Mode: MODE_IN_COMMUNICATION"); mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); } private void requestAudioFocus(int stream) { if (!mAudioFocused) { int res = mAudioManager.requestAudioFocus( null, stream, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE); Log.d( "[Audio Manager] Audio focus requested: " + (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED ? "Granted" : "Denied")); if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) mAudioFocused = true; } } private synchronized void startRinging(Address remoteAddress) { if (!LinphonePreferences.instance().isDeviceRingtoneEnabled()) { // Enable speaker audio route, linphone library will do the ringing itself automatically routeAudioToSpeaker(); return; } boolean doNotDisturbPolicyAllowsRinging = Compatibility.isDoNotDisturbPolicyAllowingRinging(mContext, remoteAddress); if (!doNotDisturbPolicyAllowsRinging) { Log.e("[Audio Manager] Do not ring as Android Do Not Disturb Policy forbids it"); return; } routeAudioToSpeaker(); mAudioManager.setMode(MODE_RINGTONE); try { if ((mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE || mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL) && mVibrator != null && LinphonePreferences.instance().isIncomingCallVibrationEnabled()) { Compatibility.vibrate(mVibrator); } if (mRingerPlayer == null) { requestAudioFocus(STREAM_RING); mRingerPlayer = new MediaPlayer(); mRingerPlayer.setAudioStreamType(STREAM_RING); String ringtone = LinphonePreferences.instance() .getRingtone(Settings.System.DEFAULT_RINGTONE_URI.toString()); try { if (ringtone.startsWith("content://")) { mRingerPlayer.setDataSource(mContext, Uri.parse(ringtone)); } else { FileInputStream fis = new FileInputStream(ringtone); mRingerPlayer.setDataSource(fis.getFD()); fis.close(); } } catch (IOException e) { Log.e(e, "[Audio Manager] Cannot set ringtone"); } mRingerPlayer.prepare(); mRingerPlayer.setLooping(true); mRingerPlayer.start(); } else { Log.w("[Audio Manager] Already ringing"); } } catch (Exception e) { Log.e(e, "[Audio Manager] Cannot handle incoming call"); } mIsRinging = true; } private synchronized void stopRinging() { if (mRingerPlayer != null) { mRingerPlayer.stop(); mRingerPlayer.release(); mRingerPlayer = null; } if (mVibrator != null) { mVibrator.cancel(); } mIsRinging = false; } private void routeAudioToSpeakerHelper(boolean speakerOn) { Log.w("[Audio Manager] Routing audio to " + (speakerOn ? "speaker" : "earpiece")); if (mIsBluetoothHeadsetScoConnected) { Log.w("[Audio Manager] [Bluetooth] Disabling bluetooth audio route"); changeBluetoothSco(false); } mAudioManager.setSpeakerphoneOn(speakerOn); } private void adjustVolume(int i) { if (mAudioManager.isVolumeFixed()) { Log.e("[Audio Manager] Can't adjust volume, device has it fixed..."); // Keep going just in case... } int stream = STREAM_VOICE_CALL; if (mIsBluetoothHeadsetScoConnected) { Log.i( "[Audio Manager] Bluetooth is connected, try to change the volume on STREAM_BLUETOOTH_SCO"); stream = 6; // STREAM_BLUETOOTH_SCO, it's hidden... } // starting from ICS, volume must be adjusted by the application, // at least for STREAM_VOICE_CALL volume stream mAudioManager.adjustStreamVolume( stream, i < 0 ? AudioManager.ADJUST_LOWER : AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI); } // Bluetooth public synchronized void bluetoothHeadetConnectionChanged(boolean connected) { mIsBluetoothHeadsetConnected = connected; mAudioManager.setBluetoothScoOn(connected); if (LinphoneContext.isReady()) LinphoneManager.getCallManager().refreshInCallActions(); } public synchronized void bluetoothHeadetAudioConnectionChanged(boolean connected) { mIsBluetoothHeadsetScoConnected = connected; mAudioManager.setBluetoothScoOn(connected); } public synchronized boolean isBluetoothHeadsetConnected() { return mIsBluetoothHeadsetConnected; } public synchronized void bluetoothHeadetScoConnectionChanged(boolean connected) { mIsBluetoothHeadsetScoConnected = connected; if (LinphoneContext.isReady()) LinphoneManager.getCallManager().refreshInCallActions(); } public synchronized boolean isUsingBluetoothAudioRoute() { return mIsBluetoothHeadsetScoConnected; } public synchronized void routeAudioToBluetooth() { if (!isBluetoothHeadsetConnected()) { Log.w("[Audio Manager] [Bluetooth] No headset connected"); return; } if (mAudioManager.getMode() != AudioManager.MODE_IN_COMMUNICATION) { Log.w( "[Audio Manager] [Bluetooth] Changing audio mode to MODE_IN_COMMUNICATION and requesting STREAM_VOICE_CALL focus"); mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); requestAudioFocus(STREAM_VOICE_CALL); } changeBluetoothSco(true); } private synchronized void changeBluetoothSco(final boolean enable) { // IT WILL TAKE A CERTAIN NUMBER OF CALLS TO EITHER START/STOP BLUETOOTH SCO FOR IT TO WORK if (enable && mIsBluetoothHeadsetScoConnected) { Log.i("[Audio Manager] [Bluetooth] SCO already enabled, skipping"); return; } else if (!enable && !mIsBluetoothHeadsetScoConnected) { Log.i("[Audio Manager] [Bluetooth] SCO already disabled, skipping"); return; } new Thread() { @Override public void run() { Log.i("[Audio Manager] [Bluetooth] SCO start/stop thread started"); boolean resultAcknowledged; int retries = 0; do { try { Thread.sleep(200); } catch (InterruptedException e) { Log.e(e); } synchronized (AndroidAudioManager.this) { if (enable) { Log.i( "[Audio Manager] [Bluetooth] Starting SCO: try number " + retries); mAudioManager.startBluetoothSco(); } else { Log.i( "[Audio Manager] [Bluetooth] Stopping SCO: try number " + retries); mAudioManager.stopBluetoothSco(); } resultAcknowledged = isUsingBluetoothAudioRoute() == enable; retries++; } } while (!resultAcknowledged && retries < 10); } }.start(); } public void bluetoothAdapterStateChanged() { if (mBluetoothAdapter.isEnabled()) { Log.i("[Audio Manager] [Bluetooth] Adapter enabled"); mIsBluetoothHeadsetConnected = false; mIsBluetoothHeadsetScoConnected = false; BluetoothProfile.ServiceListener bluetoothServiceListener = new BluetoothProfile.ServiceListener() { public void onServiceConnected(int profile, BluetoothProfile proxy) { if (profile == BluetoothProfile.HEADSET) { Log.i("[Audio Manager] [Bluetooth] HEADSET profile connected"); mBluetoothHeadset = (BluetoothHeadset) proxy; List<BluetoothDevice> devices = mBluetoothHeadset.getConnectedDevices(); if (devices.size() > 0) { Log.i( "[Audio Manager] [Bluetooth] A device is already connected"); bluetoothHeadetConnectionChanged(true); } Log.i("[Audio Manager] [Bluetooth] Registering bluetooth receiver"); IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); filter.addAction( BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT); Intent sticky = mContext.registerReceiver(mBluetoothReceiver, filter); Log.i("[Audio Manager] [Bluetooth] Bluetooth receiver registered"); int state = sticky.getIntExtra( AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_DISCONNECTED); if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) { Log.i( "[Audio Manager] [Bluetooth] Bluetooth headset SCO connected"); bluetoothHeadetScoConnectionChanged(true); } else if (state == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) { Log.i( "[Audio Manager] [Bluetooth] Bluetooth headset SCO disconnected"); bluetoothHeadetScoConnectionChanged(false); } else if (state == AudioManager.SCO_AUDIO_STATE_CONNECTING) { Log.i( "[Audio Manager] [Bluetooth] Bluetooth headset SCO connecting"); } else if (state == AudioManager.SCO_AUDIO_STATE_ERROR) { Log.i( "[Audio Manager] [Bluetooth] Bluetooth headset SCO connection error"); } else { Log.w( "[Audio Manager] [Bluetooth] Bluetooth headset unknown SCO state changed: " + state); } } } public void onServiceDisconnected(int profile) { if (profile == BluetoothProfile.HEADSET) { Log.i("[Audio Manager] [Bluetooth] HEADSET profile disconnected"); mBluetoothHeadset = null; mIsBluetoothHeadsetConnected = false; mIsBluetoothHeadsetScoConnected = false; } } }; mBluetoothAdapter.getProfileProxy( mContext, bluetoothServiceListener, BluetoothProfile.HEADSET); } else { Log.w("[Audio Manager] [Bluetooth] Adapter disabled"); } } private void startBluetooth() { mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter != null) { Log.i("[Audio Manager] [Bluetooth] Adapter found"); if (mAudioManager.isBluetoothScoAvailableOffCall()) { Log.i("[Audio Manager] [Bluetooth] SCO available off call, continue"); } else { Log.w("[Audio Manager] [Bluetooth] SCO not available off call !"); } mBluetoothReceiver = new BluetoothReceiver(); IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.registerReceiver(mBluetoothReceiver, filter); bluetoothAdapterStateChanged(); } } // HEADSET private void enableHeadsetReceiver() { mHeadsetReceiver = new HeadsetReceiver(); Log.i("[Audio Manager] Registering headset receiver"); mContext.registerReceiver( mHeadsetReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); mContext.registerReceiver( mHeadsetReceiver, new IntentFilter(AudioManager.ACTION_HEADSET_PLUG)); mHeadsetReceiverRegistered = true; } }