/* LinphoneManager.java Copyright (C) 2010 Belledonne Communications, Grenoble, France 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.linphone; import static android.media.AudioManager.MODE_RINGTONE; import static android.media.AudioManager.STREAM_RING; import static android.media.AudioManager.STREAM_VOICE_CALL; import static org.linphone.core.LinphoneCall.State.CallEnd; import static org.linphone.core.LinphoneCall.State.Error; import static org.linphone.core.LinphoneCall.State.IncomingReceived; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import org.linphone.LinphoneSimpleListener.ConnectivityChangedListener; import org.linphone.LinphoneSimpleListener.LinphoneOnAudioChangedListener; import org.linphone.LinphoneSimpleListener.LinphoneOnAudioChangedListener.AudioState; import org.linphone.LinphoneSimpleListener.LinphoneOnDTMFReceivedListener; import org.linphone.LinphoneSimpleListener.LinphoneOnMessageReceivedListener; import org.linphone.LinphoneSimpleListener.LinphoneServiceListener; import org.linphone.compatibility.Compatibility; import org.linphone.core.CallDirection; import org.linphone.core.LinphoneAddress; import org.linphone.core.LinphoneCall; import org.linphone.core.LinphoneCall.State; import org.linphone.core.LinphoneCallParams; import org.linphone.core.LinphoneCallStats; import org.linphone.core.LinphoneChatMessage; import org.linphone.core.LinphoneChatRoom; import org.linphone.core.LinphoneContent; import org.linphone.core.LinphoneCore; import org.linphone.core.LinphoneCore.EcCalibratorStatus; import org.linphone.core.LinphoneCore.GlobalState; import org.linphone.core.LinphoneCore.RegistrationState; import org.linphone.core.LinphoneCoreException; import org.linphone.core.LinphoneCoreFactory; import org.linphone.core.LinphoneCoreListener; import org.linphone.core.LinphoneEvent; import org.linphone.core.LinphoneFriend; import org.linphone.core.LinphoneInfoMessage; import org.linphone.core.LinphoneProxyConfig; import org.linphone.core.PayloadType; import org.linphone.core.PublishState; import org.linphone.core.SubscriptionState; import org.linphone.mediastream.Log; import org.linphone.mediastream.Version; import org.linphone.mediastream.video.capture.AndroidVideoApi5JniWrapper; import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration; import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration.AndroidCamera; import org.linphone.mediastream.video.capture.hwconf.Hacks; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.Vibrator; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.telephony.TelephonyManager; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.Toast; /** * * Manager of the low level LibLinphone stuff.
* Including: * * Add Service Listener to react to Linphone state changes. * * @author Guillaume Beraudo * */ public class LinphoneManager implements LinphoneCoreListener { private static LinphoneManager instance; private Context mServiceContext; private AudioManager mAudioManager; private PowerManager mPowerManager; private Resources mR; private LinphonePreferences mPrefs; private LinphoneCore mLc; private String lastLcStatusMessage; private String basePath; private static boolean sExited; private boolean mAudioFocused; private int mLastNetworkType=-1; private ConnectivityManager mConnectivityManager; private WakeLock mIncallWakeLock; private BluetoothAdapter mBluetoothAdapter; private BluetoothHeadset mBluetoothHeadset; private BluetoothProfile.ServiceListener mProfileListener; private BroadcastReceiver bluetoothReiceiver = new BluetoothManager(); public boolean isBluetoothScoConnected; public boolean isUsingBluetoothAudioRoute; private boolean mBluetoothStarted; private static List simpleListeners = new ArrayList(); public static void addListener(LinphoneSimpleListener listener) { if (!simpleListeners.contains(listener)) { simpleListeners.add(listener); } } public static void removeListener(LinphoneSimpleListener listener) { simpleListeners.remove(listener); } protected LinphoneManager(final Context c, LinphoneServiceListener listener) { sExited=false; mServiceContext = c; mListenerDispatcher = new ListenerDispatcher(listener); basePath = c.getFilesDir().getAbsolutePath(); mLPConfigXsd = basePath + "/lpconfig.xsd"; mLinphoneFactoryConfigFile = basePath + "/linphonerc"; mLinphoneConfigFile = basePath + "/.linphonerc"; mLinphoneRootCaFile = basePath + "/rootca.pem"; mRingSoundFile = basePath + "/oldphone_mono.wav"; mRingbackSoundFile = basePath + "/ringback.wav"; mPauseSoundFile = basePath + "/toy_mono.wav"; mChatDatabaseFile = basePath + "/linphone-history.db"; mPrefs = LinphonePreferences.instance(); mAudioManager = ((AudioManager) c.getSystemService(Context.AUDIO_SERVICE)); mVibrator = (Vibrator) c.getSystemService(Context.VIBRATOR_SERVICE); mPowerManager = (PowerManager) c.getSystemService(Context.POWER_SERVICE); mConnectivityManager = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); mR = c.getResources(); } private static final int LINPHONE_VOLUME_STREAM = STREAM_VOICE_CALL; private static final int dbStep = 4; /** Called when the activity is first created. */ private final String mLPConfigXsd; private final String mLinphoneFactoryConfigFile; private final String mLinphoneRootCaFile; public final String mLinphoneConfigFile; private final String mRingSoundFile; private final String mRingbackSoundFile; private final String mPauseSoundFile; private final String mChatDatabaseFile; private Timer mTimer; private BroadcastReceiver mKeepAliveReceiver = new KeepAliveReceiver(); private void routeAudioToSpeakerHelper(boolean speakerOn) { isUsingBluetoothAudioRoute = false; if (mAudioManager != null && mBluetoothStarted) { //Compatibility.setAudioManagerInCallMode(mAudioManager); mAudioManager.stopBluetoothSco(); mAudioManager.setBluetoothScoOn(false); mBluetoothStarted = false; } if (!speakerOn) { mLc.enableSpeaker(false); } else { mLc.enableSpeaker(true); } for (LinphoneOnAudioChangedListener listener : getSimpleListeners(LinphoneOnAudioChangedListener.class)) { listener.onAudioStateChanged(speakerOn ? AudioState.SPEAKER : AudioState.EARPIECE); } } public void routeAudioToSpeaker() { routeAudioToSpeakerHelper(true); } public String getUserAgent() throws NameNotFoundException { StringBuilder userAgent = new StringBuilder(); userAgent.append("LinphoneAndroid/" + mServiceContext.getPackageManager().getPackageInfo(mServiceContext.getPackageName(),0).versionCode); userAgent.append(" ("); userAgent.append("Linphone/" + LinphoneManager.getLc().getVersion() + "; "); userAgent.append(Build.DEVICE + " " + Build.MODEL + " Android/" + Build.VERSION.SDK_INT); userAgent.append(")"); return userAgent.toString(); } /** * */ public void routeAudioToReceiver() { routeAudioToSpeakerHelper(false); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SuppressWarnings("deprecation") public void startBluetooth() { mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter.isEnabled()) { if (Version.sdkAboveOrEqual(Version.API11_HONEYCOMB_30)) { mProfileListener = new BluetoothProfile.ServiceListener() { @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void onServiceConnected(int profile, BluetoothProfile proxy) { if (profile == BluetoothProfile.HEADSET) { mBluetoothHeadset = (BluetoothHeadset) proxy; isBluetoothScoConnected = true; Log.d("Bluetooth headset connected"); } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void onServiceDisconnected(int profile) { if (profile == BluetoothProfile.HEADSET) { mBluetoothHeadset = null; isBluetoothScoConnected = false; Log.d("Bluetooth headset disconnected"); routeAudioToReceiver(); } } }; mBluetoothAdapter.getProfileProxy(mServiceContext, mProfileListener, BluetoothProfile.HEADSET); } else { try { mServiceContext.unregisterReceiver(bluetoothReiceiver); } catch (Exception e) {} Intent currentValue = mServiceContext.registerReceiver(bluetoothReiceiver, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED)); int state = currentValue == null ? 0 : currentValue.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, 0); if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) { isBluetoothScoConnected = true; } } } else { isBluetoothScoConnected = false; scoDisconnected(); routeAudioToReceiver(); } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public boolean routeAudioToBluetooth() { BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter.isEnabled() && mAudioManager.isBluetoothScoAvailableOffCall()) { mAudioManager.setBluetoothScoOn(true); mAudioManager.startBluetoothSco(); mBluetoothStarted=true; if (Version.sdkAboveOrEqual(Version.API11_HONEYCOMB_30)) { isUsingBluetoothAudioRoute = false; if (mBluetoothHeadset != null) { List devices = mBluetoothHeadset.getConnectedDevices(); for (final BluetoothDevice dev : devices) { isUsingBluetoothAudioRoute |= mBluetoothHeadset.getConnectionState(dev) == BluetoothHeadset.STATE_CONNECTED; } } if (!isUsingBluetoothAudioRoute) { Log.d("No bluetooth device available"); scoDisconnected(); } else { //Why is this for: //mAudioManager.setMode(AudioManager.MODE_IN_CALL); for (LinphoneOnAudioChangedListener listener : getSimpleListeners(LinphoneOnAudioChangedListener.class)) { listener.onAudioStateChanged(AudioState.SPEAKER); } } } return isUsingBluetoothAudioRoute; } return false; } public void scoConnected() { Log.i("Bluetooth sco connected!"); isBluetoothScoConnected = true; } public void scoDisconnected() { Log.w("Bluetooth sco disconnected!"); isUsingBluetoothAudioRoute = false; isBluetoothScoConnected = false; if (mAudioManager != null) { //why is this for ? //mAudioManager.setMode(AudioManager.MODE_NORMAL); mAudioManager.stopBluetoothSco(); mAudioManager.setBluetoothScoOn(false); } } public synchronized static final LinphoneManager createAndStart( Context c, LinphoneServiceListener listener) { if (instance != null) throw new RuntimeException("Linphone Manager is already initialized"); instance = new LinphoneManager(c, listener); instance.startLibLinphone(c); TelephonyManager tm = (TelephonyManager) c.getSystemService(Context.TELEPHONY_SERVICE); boolean gsmIdle = tm.getCallState() == TelephonyManager.CALL_STATE_IDLE; setGsmIdle(gsmIdle); if (Version.isVideoCapable() && getLc().isVideoSupported()) AndroidVideoApi5JniWrapper.setAndroidSdkVersion(Version.sdk()); return instance; } public static synchronized final LinphoneManager getInstance() { if (instance != null) return instance; if (sExited) { throw new RuntimeException("Linphone Manager was already destroyed. " + "Better use getLcIfManagerNotDestroyed and check returned value"); } throw new RuntimeException("Linphone Manager should be created before accessed"); } public static synchronized final LinphoneCore getLc() { return getInstance().mLc; } public String getLPConfigXsdPath() { return mLPConfigXsd; } public void newOutgoingCall(AddressType address) { String to = address.getText().toString(); newOutgoingCall(to, address.getDisplayedName()); } public void newOutgoingCall(String to, String displayName) { // if (mLc.isIncall()) { // listenerDispatcher.tryingNewOutgoingCallButAlreadyInCall(); // return; // } LinphoneAddress lAddress; try { lAddress = mLc.interpretUrl(to); if (mServiceContext.getResources().getBoolean(R.bool.override_domain_using_default_one)) { lAddress.setDomain(mServiceContext.getString(R.string.default_domain)); } LinphoneProxyConfig lpc = mLc.getDefaultProxyConfig(); if (mR.getBoolean(R.bool.forbid_self_call) && lpc!=null && lAddress.asStringUriOnly().equals(lpc.getIdentity())) { mListenerDispatcher.tryingNewOutgoingCallButWrongDestinationAddress(); return; } } catch (LinphoneCoreException e) { mListenerDispatcher.tryingNewOutgoingCallButWrongDestinationAddress(); return; } lAddress.setDisplayName(displayName); boolean isLowBandwidthConnection = !LinphoneUtils.isHightBandwidthConnection(LinphoneService.instance().getApplicationContext()); if (mLc.isNetworkReachable()) { try { if (Version.isVideoCapable()) { boolean prefVideoEnable = mPrefs.isVideoEnabled(); boolean prefInitiateWithVideo = mPrefs.shouldInitiateVideoCall(); CallManager.getInstance().inviteAddress(lAddress, prefVideoEnable && prefInitiateWithVideo, isLowBandwidthConnection); } else { CallManager.getInstance().inviteAddress(lAddress, false, isLowBandwidthConnection); } } catch (LinphoneCoreException e) { mListenerDispatcher.tryingNewOutgoingCallButCannotGetCallParameters(); return; } } else if (LinphoneActivity.isInstanciated()) { LinphoneActivity.instance().displayCustomToast(getString(R.string.error_network_unreachable), Toast.LENGTH_LONG); } else { Log.e("Error: " + getString(R.string.error_network_unreachable)); } } private void resetCameraFromPreferences() { boolean useFrontCam = mPrefs.useFrontCam(); int camId = 0; AndroidCamera[] cameras = AndroidCameraConfiguration.retrieveCameras(); for (AndroidCamera androidCamera : cameras) { if (androidCamera.frontFacing == useFrontCam) camId = androidCamera.id; } LinphoneManager.getLc().setVideoDevice(camId); } public static interface AddressType { void setText(CharSequence s); CharSequence getText(); void setDisplayedName(String s); String getDisplayedName(); } public static interface NewOutgoingCallUiListener { public void onWrongDestinationAddress(); public void onCannotGetCallParameters(); public void onAlreadyInCall(); } public boolean toggleEnableCamera() { if (mLc.isIncall()) { boolean enabled = !mLc.getCurrentCall().cameraEnabled(); enableCamera(mLc.getCurrentCall(), enabled); return enabled; } return false; } public void enableCamera(LinphoneCall call, boolean enable) { if (call != null) { call.enableCamera(enable); if (mServiceContext.getResources().getBoolean(R.bool.enable_call_notification)) LinphoneService.instance().refreshIncallIcon(mLc.getCurrentCall()); } } public void sendStaticImage(boolean send) { if (mLc.isIncall()) { enableCamera(mLc.getCurrentCall(), !send); } } public void playDtmf(ContentResolver r, char dtmf) { try { if (Settings.System.getInt(r, Settings.System.DTMF_TONE_WHEN_DIALING) == 0) { // audible touch disabled: don't play on speaker, only send in outgoing stream return; } } catch (SettingNotFoundException e) {} getLc().playDtmf(dtmf, -1); } public void terminateCall() { if (mLc.isIncall()) { mLc.terminateCall(mLc.getCurrentCall()); } } public void initTunnelFromConf() { if (!mLc.isTunnelAvailable()) return; NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); mLc.tunnelCleanServers(); String host = mPrefs.getTunnelHost(); if (host != null) { int port = mPrefs.getTunnelPort(); mLc.tunnelAddServerAndMirror(host, port, 12345, 500); manageTunnelServer(info); } } private boolean isTunnelNeeded(NetworkInfo info) { if (info == null) { Log.i("No connectivity: tunnel should be disabled"); return false; } String pref = mPrefs.getTunnelMode(); if (getString(R.string.tunnel_mode_entry_value_always).equals(pref)) { return true; } if (info.getType() != ConnectivityManager.TYPE_WIFI && getString(R.string.tunnel_mode_entry_value_3G_only).equals(pref)) { Log.i("need tunnel: 'no wifi' connection"); return true; } return false; } private void manageTunnelServer(NetworkInfo info) { if (mLc == null) return; if (!mLc.isTunnelAvailable()) return; Log.i("Managing tunnel"); if (isTunnelNeeded(info)) { Log.i("Tunnel need to be activated"); mLc.tunnelEnable(true); } else { Log.i("Tunnel should not be used"); String pref = mPrefs.getTunnelMode(); mLc.tunnelEnable(false); if (getString(R.string.tunnel_mode_entry_value_auto).equals(pref)) { mLc.tunnelAutoDetect(); } } } private synchronized void startLibLinphone(Context c) { try { copyAssetsFromPackage(); //traces alway start with traces enable to not missed first initialization boolean isDebugLogEnabled = !(mR.getBoolean(R.bool.disable_every_log)); LinphoneCoreFactory.instance().setDebugMode(isDebugLogEnabled, getString(R.string.app_name)); // Try to get remote provisioning // First check if there is a remote provisioning url in the old preferences API String remote_provisioning = mPrefs.getRemoteProvisioningUrl(); if(remote_provisioning != null && remote_provisioning.length() > 0 && RemoteProvisioning.isAvailable()) { RemoteProvisioning.download(remote_provisioning, mLinphoneConfigFile); } initLiblinphone(c); PreferencesMigrator prefMigrator = new PreferencesMigrator(mServiceContext); if (prefMigrator.isMigrationNeeded()) { prefMigrator.doMigration(); } if (mServiceContext.getResources().getBoolean(R.bool.enable_push_id)) { Compatibility.initPushNotificationService(mServiceContext); } IntentFilter lFilter = new IntentFilter(Intent.ACTION_SCREEN_ON); lFilter.addAction(Intent.ACTION_SCREEN_OFF); mServiceContext.registerReceiver(mKeepAliveReceiver, lFilter); updateNetworkReachability(); startBluetooth(); resetCameraFromPreferences(); } catch (Exception e) { Log.e(e, "Cannot start linphone"); } } public synchronized void initLiblinphone(Context c) throws LinphoneCoreException { boolean isDebugLogEnabled = !(mR.getBoolean(R.bool.disable_every_log)) && mPrefs.isDebugEnabled(); LinphoneCoreFactory.instance().setDebugMode(isDebugLogEnabled, getString(R.string.app_name)); mLc = LinphoneCoreFactory.instance().createLinphoneCore(this, mLinphoneConfigFile, mLinphoneFactoryConfigFile, null); mLc.setContext(c); try { String versionName = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName; if (versionName == null) { versionName = String.valueOf(c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionCode); } mLc.setUserAgent("LinphoneAndroid", versionName); } catch (NameNotFoundException e) { Log.e(e, "cannot get version name"); } mLc.setZrtpSecretsCache(basePath + "/zrtp_secrets"); mLc.setRing(null); mLc.setRootCA(mLinphoneRootCaFile); mLc.setPlayFile(mPauseSoundFile); mLc.setChatDatabasePath(mChatDatabaseFile); int availableCores = Runtime.getRuntime().availableProcessors(); Log.w("MediaStreamer : " + availableCores + " cores detected and configured"); mLc.setCpuCount(availableCores); int camId = 0; AndroidCamera[] cameras = AndroidCameraConfiguration.retrieveCameras(); for (AndroidCamera androidCamera : cameras) { if (androidCamera.frontFacing == mPrefs.useFrontCam()) camId = androidCamera.id; } LinphoneManager.getLc().setVideoDevice(camId); initTunnelFromConf(); TimerTask lTask = new TimerTask() { @Override public void run() { mLc.iterate(); } }; /*use schedule instead of scheduleAtFixedRate to avoid iterate from being call in burst after cpu wake up*/ mTimer = new Timer("Linphone scheduler"); mTimer.schedule(lTask, 0, 20); } private void copyAssetsFromPackage() throws IOException { copyIfNotExist(R.raw.oldphone_mono, mRingSoundFile); copyIfNotExist(R.raw.ringback, mRingbackSoundFile); copyIfNotExist(R.raw.toy_mono, mPauseSoundFile); copyIfNotExist(R.raw.linphonerc_default, mLinphoneConfigFile); copyFromPackage(R.raw.linphonerc_factory, new File(mLinphoneFactoryConfigFile).getName()); copyIfNotExist(R.raw.lpconfig, mLPConfigXsd); copyIfNotExist(R.raw.rootca, mLinphoneRootCaFile); } private void copyIfNotExist(int ressourceId,String target) throws IOException { File lFileToCopy = new File(target); if (!lFileToCopy.exists()) { copyFromPackage(ressourceId,lFileToCopy.getName()); } } private void copyFromPackage(int ressourceId,String target) throws IOException{ FileOutputStream lOutputStream = mServiceContext.openFileOutput (target, 0); InputStream lInputStream = mR.openRawResource(ressourceId); int readByte; byte[] buff = new byte[8048]; while (( readByte = lInputStream.read(buff)) != -1) { lOutputStream.write(buff,0, readByte); } lOutputStream.flush(); lOutputStream.close(); lInputStream.close(); } public boolean detectVideoCodec(String mime) { for (PayloadType videoCodec : mLc.getVideoCodecs()) { if (mime.equals(videoCodec.getMime())) return true; } return false; } public boolean detectAudioCodec(String mime){ for (PayloadType audioCodec : mLc.getAudioCodecs()) { if (mime.equals(audioCodec.getMime())) return true; } return false; } public void updateNetworkReachability() { ConnectivityManager cm = (ConnectivityManager) mServiceContext.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo eventInfo = cm.getActiveNetworkInfo(); if (eventInfo == null || eventInfo.getState() == NetworkInfo.State.DISCONNECTED) { Log.i("No connectivity: setting network unreachable"); mLc.setNetworkReachable(false); } else if (eventInfo.getState() == NetworkInfo.State.CONNECTED){ manageTunnelServer(eventInfo); boolean wifiOnly = LinphonePreferences.instance().isWifiOnlyEnabled(); if (wifiOnly){ if (eventInfo.getType()==ConnectivityManager.TYPE_WIFI) mLc.setNetworkReachable(true); else { Log.i("Wifi-only mode, setting network not reachable"); mLc.setNetworkReachable(false); } }else{ int curtype=eventInfo.getType(); if (curtype!=mLastNetworkType){ //if kind of network has changed, we need to notify network_reachable(false) to make sure all current connections are destroyed. //they will be re-created during setNetworkReachable(true). Log.i("Connectivity has changed."); mLc.setNetworkReachable(false); } mLc.setNetworkReachable(true); mLastNetworkType=curtype; } } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void doDestroy() { ChatStorage.getInstance().close(); try { mServiceContext.unregisterReceiver(bluetoothReiceiver); } catch (Exception e) {} try { if (Version.sdkAboveOrEqual(Version.API11_HONEYCOMB_30)) mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset); } catch (Exception e) {} try { mTimer.cancel(); mLc.destroy(); } catch (RuntimeException e) { e.printStackTrace(); } finally { mServiceContext.unregisterReceiver(instance.mKeepAliveReceiver); mLc = null; instance = null; } } public static synchronized void destroy() { if (instance == null) return; sExited=true; instance.doDestroy(); } private String getString(int key) { return mR.getString(key); } /* Simple implementation as Android way seems very complicate: For example: with wifi and mobile actives; when pulling mobile down: I/Linphone( 8397): WIFI connected: setting network reachable I/Linphone( 8397): new state [RegistrationProgress] I/Linphone( 8397): mobile disconnected: setting network unreachable I/Linphone( 8397): Managing tunnel I/Linphone( 8397): WIFI connected: setting network reachable */ public void connectivityChanged(ConnectivityManager cm, boolean noConnectivity) { NetworkInfo eventInfo = cm.getActiveNetworkInfo(); updateNetworkReachability(); if (connectivityListener != null) { connectivityListener.onConnectivityChanged(mServiceContext, eventInfo, cm); } } private ConnectivityChangedListener connectivityListener; public void addConnectivityChangedListener(ConnectivityChangedListener l) { connectivityListener = l; } public interface EcCalibrationListener { void onEcCalibrationStatus(EcCalibratorStatus status, int delayMs); } private ListenerDispatcher mListenerDispatcher; private LinphoneCall ringingCall; private MediaPlayer mRingerPlayer; private Vibrator mVibrator; public void displayWarning(LinphoneCore lc, String message) {} public void authInfoRequested(LinphoneCore lc, String realm, String username) {} public void byeReceived(LinphoneCore lc, String from) {} public void displayMessage(LinphoneCore lc, String message) {} public void show(LinphoneCore lc) {} public void newSubscriptionRequest(LinphoneCore lc, LinphoneFriend lf, String url) { for (LinphoneSimpleListener listener : getSimpleListeners(LinphoneActivity.class)) { ((LinphoneActivity) listener).onNewSubscriptionRequestReceived(lf, url); } } public void notifyPresenceReceived(LinphoneCore lc, LinphoneFriend lf) { for (LinphoneSimpleListener listener : getSimpleListeners(LinphoneActivity.class)) { ((LinphoneActivity) listener).onNotifyPresenceReceived(lf); } } public void textReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneAddress from, String message) { //deprecated } @Override public void dtmfReceived(LinphoneCore lc, LinphoneCall call, int dtmf) { Log.d("DTMF received: " + dtmf); if (dtmfReceivedListener != null) dtmfReceivedListener.onDTMFReceived(call, dtmf); } private LinphoneOnDTMFReceivedListener dtmfReceivedListener; public void setOnDTMFReceivedListener(LinphoneOnDTMFReceivedListener listener) { dtmfReceivedListener = listener; } @Override public void messageReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneChatMessage message) { if (mServiceContext.getResources().getBoolean(R.bool.disable_chat)) { return; } LinphoneAddress from = message.getFrom(); String textMessage = message.getText(); String url = message.getExternalBodyUrl(); int id = -1; if (textMessage != null && textMessage.length() > 0) { id = ChatStorage.getInstance().saveTextMessage(from.asStringUriOnly(), "", textMessage, message.getTime()); } else if (url != null && url.length() > 0) { //Bitmap bm = ChatFragment.downloadImage(url); id = ChatStorage.getInstance().saveImageMessage(from.asStringUriOnly(), "", null, message.getExternalBodyUrl(), message.getTime()); } try { LinphoneUtils.findUriPictureOfContactAndSetDisplayName(from, mServiceContext.getContentResolver()); if (!mServiceContext.getResources().getBoolean(R.bool.disable_chat__message_notification)) { LinphoneService.instance().displayMessageNotification(from.asStringUriOnly(), from.getDisplayName(), textMessage); } } catch (Exception e) { } for (LinphoneSimpleListener listener : getSimpleListeners(LinphoneOnMessageReceivedListener.class)) { ((LinphoneOnMessageReceivedListener) listener).onMessageReceived(from, message, id); } } public String getLastLcStatusMessage() { return lastLcStatusMessage; } public void displayStatus(final LinphoneCore lc, final String message) { Log.i(message); lastLcStatusMessage=message; mListenerDispatcher.onDisplayStatus(message); } public void globalState(final LinphoneCore lc, final LinphoneCore.GlobalState state, final String message) { Log.i("new state [",state,"]"); mListenerDispatcher.onGlobalStateChanged(state, message); } public void registrationState(final LinphoneCore lc, final LinphoneProxyConfig cfg,final LinphoneCore.RegistrationState state,final String message) { Log.i("new state ["+state+"]"); mListenerDispatcher.onRegistrationStateChanged(state, message); } private int savedMaxCallWhileGsmIncall; private synchronized void preventSIPCalls() { if (savedMaxCallWhileGsmIncall != 0) { Log.w("SIP calls are already blocked due to GSM call running"); return; } savedMaxCallWhileGsmIncall = mLc.getMaxCalls(); mLc.setMaxCalls(0); } private synchronized void allowSIPCalls() { if (savedMaxCallWhileGsmIncall == 0) { Log.w("SIP calls are already allowed as no GSM call knowned to be running"); return; } mLc.setMaxCalls(savedMaxCallWhileGsmIncall); savedMaxCallWhileGsmIncall = 0; } public static void setGsmIdle(boolean gsmIdle) { LinphoneManager mThis = instance; if (mThis == null) return; if (gsmIdle) { mThis.allowSIPCalls(); } else { mThis.preventSIPCalls(); } } public Context getContext() { try { if (LinphoneActivity.isInstanciated()) return LinphoneActivity.instance(); else if (InCallActivity.isInstanciated()) return InCallActivity.instance(); else if (IncomingCallActivity.isInstanciated()) return IncomingCallActivity.instance(); else if (mServiceContext != null) return mServiceContext; else if (LinphoneService.isReady()) return LinphoneService.instance().getApplicationContext(); } catch (Exception e) { e.printStackTrace(); } return null; } @SuppressLint("Wakelock") public void callState(final LinphoneCore lc,final LinphoneCall call, final State state, final String message) { Log.i("new state [",state,"]"); if (state == IncomingReceived && !call.equals(lc.getCurrentCall())) { if (call.getReplacedCall()!=null){ // attended transfer // it will be accepted automatically. return; } } if (state == IncomingReceived && mR.getBoolean(R.bool.auto_answer_calls)) { try { mLc.acceptCall(call); } catch (LinphoneCoreException e) { e.printStackTrace(); } } else if (state == IncomingReceived || (state == State.CallIncomingEarlyMedia && mR.getBoolean(R.bool.allow_ringing_while_early_media))) { // Brighten screen for at least 10 seconds if (mLc.getCallsNb() == 1) { ringingCall = call; startRinging(); // otherwise there is the beep } } else if (call == ringingCall && isRinging) { //previous state was ringing, so stop ringing stopRinging(); } if (state == LinphoneCall.State.Connected) { if (mLc.getCallsNb() == 1) { requestAudioFocus(); Compatibility.setAudioManagerInCallMode(mAudioManager); } if (Hacks.needSoftvolume()) { adjustVolume(0); // Synchronize } } if (state == CallEnd || state == Error) { if (mLc.getCallsNb() == 0) { if (mAudioFocused){ int res=mAudioManager.abandonAudioFocus(null); Log.d("Audio focus released a bit later: " + (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED ? "Granted" : "Denied")); mAudioFocused=false; } Context activity = getContext(); if (activity != null) { TelephonyManager tm = (TelephonyManager) activity.getSystemService(Context.TELEPHONY_SERVICE); if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) { mAudioManager.setMode(AudioManager.MODE_NORMAL); Log.d("---AudioManager: back to MODE_NORMAL"); } } } } if (state == CallEnd) { if (mLc.getCallsNb() == 0) { if (mIncallWakeLock != null && mIncallWakeLock.isHeld()) { mIncallWakeLock.release(); Log.i("Last call ended: releasing incall (CPU only) wake lock"); } else { Log.i("Last call ended: no incall (CPU only) wake lock were held"); } } } if (state == State.StreamsRunning) { if (mIncallWakeLock == null) { mIncallWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "incall"); } if (!mIncallWakeLock.isHeld()) { Log.i("New call active : acquiring incall (CPU only) wake lock"); mIncallWakeLock.acquire(); } else { Log.i("New call active while incall (CPU only) wake lock already active"); } } mListenerDispatcher.onCallStateChanged(call, state, message); } public void callStatsUpdated(final LinphoneCore lc, final LinphoneCall call, final LinphoneCallStats stats) {} public void callEncryptionChanged(LinphoneCore lc, LinphoneCall call, boolean encrypted, String authenticationToken) { mListenerDispatcher.onCallEncryptionChanged(call, encrypted, authenticationToken); } public void ecCalibrationStatus(final LinphoneCore lc,final EcCalibratorStatus status, final int delayMs, final Object data) { EcCalibrationListener listener = (EcCalibrationListener) data; listener.onEcCalibrationStatus(status, delayMs); } public void startEcCalibration(EcCalibrationListener l) throws LinphoneCoreException { int oldVolume = mAudioManager.getStreamVolume(STREAM_VOICE_CALL); int maxVolume = mAudioManager.getStreamMaxVolume(STREAM_VOICE_CALL); mAudioManager.setStreamVolume(STREAM_VOICE_CALL, maxVolume, 0); mLc.startEchoCalibration(l); mAudioManager.setStreamVolume(STREAM_VOICE_CALL, oldVolume, 0); } private boolean isRinging; private boolean disableRinging = false; public void disableRinging() { disableRinging = true; } private void requestAudioFocus(){ if (!mAudioFocused){ int res = mAudioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT ); Log.d("Audio focus requested: " + (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED ? "Granted" : "Denied")); if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) mAudioFocused=true; } } private synchronized void startRinging() { if (disableRinging) { return; } if (Hacks.needGalaxySAudioHack()) { mAudioManager.setMode(MODE_RINGTONE); } try { if ((mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE || mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL) && mVibrator != null) { long[] patern = {0,1000,1000}; mVibrator.vibrate(patern, 1); } if (mRingerPlayer == null) { requestAudioFocus(); mRingerPlayer = new MediaPlayer(); mRingerPlayer.setAudioStreamType(STREAM_RING); String ringtone = LinphonePreferences.instance().getRingtone(android.provider.Settings.System.DEFAULT_RINGTONE_URI.toString()); try { if (ringtone.startsWith("content://")) { mRingerPlayer.setDataSource(mServiceContext, Uri.parse(ringtone)); } else { FileInputStream fis = new FileInputStream(ringtone); mRingerPlayer.setDataSource(fis.getFD()); fis.close(); } } catch (IOException e) { Log.e(e, "Cannot set ringtone"); } mRingerPlayer.prepare(); mRingerPlayer.setLooping(true); mRingerPlayer.start(); } else { Log.w("already ringing"); } } catch (Exception e) { Log.e(e,"cannot handle incoming call"); } isRinging = true; } private synchronized void stopRinging() { if (mRingerPlayer != null) { mRingerPlayer.stop(); mRingerPlayer.release(); mRingerPlayer = null; } if (mVibrator != null) { mVibrator.cancel(); } if (Hacks.needGalaxySAudioHack()) mAudioManager.setMode(AudioManager.MODE_NORMAL); isRinging = false; // You may need to call galaxys audio hack after this method routeAudioToReceiver(); } public static String extractADisplayName(Resources r, LinphoneAddress address) { if (address == null) return r.getString(R.string.unknown_incoming_call_name); final String displayName = address.getDisplayName(); if (displayName!=null) { return displayName; } else if (address.getUserName() != null){ return address.getUserName(); } else { String rms = address.toString(); if (rms != null && rms.length() > 1) return rms; return r.getString(R.string.unknown_incoming_call_name); } } public static boolean reinviteWithVideo() { return CallManager.getInstance().reinviteWithVideo(); } /** * * @return false if already in video call. */ public boolean addVideo() { LinphoneCall call = mLc.getCurrentCall(); enableCamera(call, true); return reinviteWithVideo(); } public boolean acceptCallIfIncomingPending() throws LinphoneCoreException { if (mLc.isInComingInvitePending()) { mLc.acceptCall(mLc.getCurrentCall()); return true; } return false; } public boolean acceptCall(LinphoneCall call) { try { mLc.acceptCall(call); return true; } catch (LinphoneCoreException e) { Log.i(e, "Accept call failed"); } return false; } public boolean acceptCallWithParams(LinphoneCall call, LinphoneCallParams params) { try { mLc.acceptCallWithParams(call, params); return true; } catch (LinphoneCoreException e) { Log.i(e, "Accept call failed"); } return false; } public static String extractIncomingRemoteName(Resources r, LinphoneAddress linphoneAddress) { return extractADisplayName(r, linphoneAddress); } public void adjustVolume(int i) { if (Build.VERSION.SDK_INT<15) { int oldVolume = mAudioManager.getStreamVolume(LINPHONE_VOLUME_STREAM); int maxVolume = mAudioManager.getStreamMaxVolume(LINPHONE_VOLUME_STREAM); int nextVolume = oldVolume +i; if (nextVolume > maxVolume) nextVolume = maxVolume; if (nextVolume < 0) nextVolume = 0; mLc.setPlaybackGain((nextVolume - maxVolume)* dbStep); } else // starting from ICS, volume must be adjusted by the application, at least for STREAM_VOICE_CALL volume stream mAudioManager.adjustStreamVolume(LINPHONE_VOLUME_STREAM, i<0?AudioManager.ADJUST_LOWER:AudioManager.ADJUST_RAISE, 0); } public static Boolean isProximitySensorNearby(final SensorEvent event) { float threshold = 4.001f; // <= 4 cm is near final float distanceInCm = event.values[0]; final float maxDistance = event.sensor.getMaximumRange(); Log.d("Proximity sensor report [",distanceInCm,"] , for max range [",maxDistance,"]"); if (maxDistance <= threshold) { // Case binary 0/1 and short sensors threshold = maxDistance; } return distanceInCm < threshold; } private static boolean sLastProximitySensorValueNearby; private static Set sProximityDependentActivities = new HashSet(); private static SensorEventListener sProximitySensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { if (event.timestamp == 0) return; //just ignoring for nexus 1 sLastProximitySensorValueNearby = isProximitySensorNearby(event); proximityNearbyChanged(); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} }; private static void simulateProximitySensorNearby(Activity activity, boolean nearby) { final Window window = activity.getWindow(); WindowManager.LayoutParams params = window.getAttributes(); View view = ((ViewGroup) window.getDecorView().findViewById(android.R.id.content)).getChildAt(0); if (nearby) { params.screenBrightness = 0.1f; view.setVisibility(View.INVISIBLE); Compatibility.hideNavigationBar(activity); } else { params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; view.setVisibility(View.VISIBLE); Compatibility.showNavigationBar(activity); } window.setAttributes(params); } private static void proximityNearbyChanged() { boolean nearby = sLastProximitySensorValueNearby; for (Activity activity : sProximityDependentActivities) { simulateProximitySensorNearby(activity, nearby); } } public static synchronized void startProximitySensorForActivity(Activity activity) { if (sProximityDependentActivities.contains(activity)) { Log.i("proximity sensor already active for " + activity.getLocalClassName()); return; } if (sProximityDependentActivities.isEmpty()) { SensorManager sm = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE); Sensor s = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); if (s != null) { sm.registerListener(sProximitySensorListener,s,SensorManager.SENSOR_DELAY_UI); Log.i("Proximity sensor detected, registering"); } } else if (sLastProximitySensorValueNearby){ simulateProximitySensorNearby(activity, true); } sProximityDependentActivities.add(activity); } public static synchronized void stopProximitySensorForActivity(Activity activity) { sProximityDependentActivities.remove(activity); simulateProximitySensorNearby(activity, false); if (sProximityDependentActivities.isEmpty()) { SensorManager sm = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE); sm.unregisterListener(sProximitySensorListener); sLastProximitySensorValueNearby = false; } } public static synchronized LinphoneCore getLcIfManagerNotDestroyedOrNull() { if (sExited) { // Can occur if the UI thread play a posted event but in the meantime the LinphoneManager was destroyed // Ex: stop call and quickly terminate application. Log.w("Trying to get linphone core while LinphoneManager already destroyed"); return null; } return getLc(); } @SuppressWarnings("unchecked") private List getSimpleListeners(Class clazz) { List list = new ArrayList(); for (LinphoneSimpleListener l : simpleListeners) { if (clazz.isInstance(l)) list.add((T) l); } return list; } private class ListenerDispatcher implements LinphoneServiceListener { private LinphoneServiceListener serviceListener; public ListenerDispatcher(LinphoneServiceListener listener) { this.serviceListener = listener; } public void onCallEncryptionChanged(LinphoneCall call, boolean encrypted, String authenticationToken) { if (serviceListener != null) { serviceListener.onCallEncryptionChanged(call, encrypted, authenticationToken); } for (LinphoneOnCallEncryptionChangedListener l : getSimpleListeners(LinphoneOnCallEncryptionChangedListener.class)) { l.onCallEncryptionChanged(call, encrypted, authenticationToken); } } public void onCallStateChanged(LinphoneCall call, State state, String message) { if (state == State.OutgoingInit || state == State.IncomingReceived) { boolean sendCamera = mLc.getConferenceSize() == 0; enableCamera(call, sendCamera); } Context activity = getContext(); if (activity != null) { TelephonyManager tm = (TelephonyManager) activity.getSystemService(Context.TELEPHONY_SERVICE); if (state == State.CallEnd && mLc.getCallsNb() == 0 && tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) { routeAudioToReceiver(); } } if (serviceListener != null) serviceListener.onCallStateChanged(call, state, message); for (LinphoneOnCallStateChangedListener l : getSimpleListeners(LinphoneOnCallStateChangedListener.class)) { l.onCallStateChanged(call, state, message); } } public void onDisplayStatus(String message) { if (serviceListener != null) serviceListener.onDisplayStatus(message); } public void onGlobalStateChanged(GlobalState state, String message) { if (serviceListener != null) serviceListener.onGlobalStateChanged( state, message); } public void onRegistrationStateChanged(RegistrationState state, String message) { if (serviceListener != null) serviceListener.onRegistrationStateChanged(state, message); for (LinphoneOnRegistrationStateChangedListener listener : getSimpleListeners(LinphoneOnRegistrationStateChangedListener.class)) { listener.onRegistrationStateChanged(state); } } public void tryingNewOutgoingCallButAlreadyInCall() { if (serviceListener != null) serviceListener.tryingNewOutgoingCallButAlreadyInCall(); } public void tryingNewOutgoingCallButCannotGetCallParameters() { if (serviceListener != null) serviceListener.tryingNewOutgoingCallButCannotGetCallParameters(); } public void tryingNewOutgoingCallButWrongDestinationAddress() { if (serviceListener != null) serviceListener.tryingNewOutgoingCallButWrongDestinationAddress(); } } public static final boolean isInstanciated() { return instance != null; } public synchronized LinphoneCall getPendingIncomingCall() { LinphoneCall currentCall = mLc.getCurrentCall(); if (currentCall == null) return null; LinphoneCall.State state = currentCall.getState(); boolean incomingPending = currentCall.getDirection() == CallDirection.Incoming && (state == State.IncomingReceived || state == State.CallIncomingEarlyMedia); return incomingPending ? currentCall : null; } @SuppressWarnings("serial") public static class LinphoneConfigException extends LinphoneException { public LinphoneConfigException() { super(); } public LinphoneConfigException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } public LinphoneConfigException(String detailMessage) { super(detailMessage); } public LinphoneConfigException(Throwable throwable) { super(throwable); } } @Override public void notifyReceived(LinphoneCore lc, LinphoneCall call, LinphoneAddress from, byte[] event) { } @Override public void transferState(LinphoneCore lc, LinphoneCall call, State new_call_state) { } @Override public void infoReceived(LinphoneCore lc, LinphoneCall call, LinphoneInfoMessage info) { Log.d("Info message received from "+call.getRemoteAddress().asString()); LinphoneContent ct=info.getContent(); if (ct!=null){ Log.d("Info received with body with mime type "+ct.getType()+"/"+ct.getSubtype()+" and data ["+ct.getDataAsString()+"]"); } } @Override public void subscriptionStateChanged(LinphoneCore lc, LinphoneEvent ev, SubscriptionState state) { Log.d("Subscription state changed to "+state+" event name is "+ev.getEventName()); } @Override public void notifyReceived(LinphoneCore lc, LinphoneEvent ev, String eventName, LinphoneContent content) { Log.d("Notify received for event "+eventName); if (content!=null) Log.d("with content "+content.getType()+"/"+content.getSubtype()+" data:"+content.getDataAsString()); } @Override public void publishStateChanged(LinphoneCore lc, LinphoneEvent ev, PublishState state) { // TODO Auto-generated method stub } @Override public void isComposingReceived(LinphoneCore lc, LinphoneChatRoom cr) { // TODO Auto-generated method stub } }