/*
* 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 .
*/
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 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;
}
}