/* * 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; import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import java.io.File; import java.sql.Timestamp; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import org.linphone.assistant.PhoneAccountLinkingAssistantActivity; import org.linphone.call.AndroidAudioManager; import org.linphone.call.CallManager; import org.linphone.contacts.ContactsManager; import org.linphone.core.AccountCreator; import org.linphone.core.AccountCreatorListenerStub; import org.linphone.core.Call; import org.linphone.core.Call.State; import org.linphone.core.Core; import org.linphone.core.CoreListener; import org.linphone.core.CoreListenerStub; import org.linphone.core.Factory; import org.linphone.core.FriendList; import org.linphone.core.PresenceActivity; import org.linphone.core.PresenceBasicStatus; import org.linphone.core.PresenceModel; import org.linphone.core.ProxyConfig; import org.linphone.core.Reason; import org.linphone.core.Tunnel; import org.linphone.core.TunnelConfig; import org.linphone.core.tools.Log; import org.linphone.settings.LinphonePreferences; import org.linphone.utils.LinphoneUtils; import org.linphone.utils.MediaScanner; import org.linphone.utils.PushNotificationUtils; /** Handles Linphone's Core lifecycle */ public class LinphoneManager implements SensorEventListener { private final String mBasePath; private final String mRingSoundFile; private final String mCallLogDatabaseFile; private final String mFriendsDatabaseFile; private final String mUserCertsPath; private final Context mContext; private AndroidAudioManager mAudioManager; private CallManager mCallManager; private final PowerManager mPowerManager; private final ConnectivityManager mConnectivityManager; private TelephonyManager mTelephonyManager; private PhoneStateListener mPhoneStateListener; private WakeLock mProximityWakelock; private final SensorManager mSensorManager; private final Sensor mProximity; private final MediaScanner mMediaScanner; private Timer mTimer; private final LinphonePreferences mPrefs; private Core mCore; private CoreListenerStub mCoreListener; private AccountCreator mAccountCreator; private AccountCreatorListenerStub mAccountCreatorListener; private boolean mExited; private boolean mCallGsmON; private boolean mProximitySensingEnabled; private boolean mHasLastCallSasBeenRejected; private Runnable mIterateRunnable; public LinphoneManager(Context c) { mExited = false; mContext = c; mBasePath = c.getFilesDir().getAbsolutePath(); mCallLogDatabaseFile = mBasePath + "/linphone-log-history.db"; mFriendsDatabaseFile = mBasePath + "/linphone-friends.db"; mRingSoundFile = mBasePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv"; mUserCertsPath = mBasePath + "/user-certs"; mPrefs = LinphonePreferences.instance(); mPowerManager = (PowerManager) c.getSystemService(Context.POWER_SERVICE); mConnectivityManager = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); mSensorManager = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE); mProximity = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); mTelephonyManager = (TelephonyManager) c.getSystemService(Context.TELEPHONY_SERVICE); mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String phoneNumber) { switch (state) { case TelephonyManager.CALL_STATE_OFFHOOK: Log.i("[Manager] Phone state is off hook"); setCallGsmON(true); break; case TelephonyManager.CALL_STATE_RINGING: Log.i("[Manager] Phone state is ringing"); setCallGsmON(true); break; case TelephonyManager.CALL_STATE_IDLE: Log.i("[Manager] Phone state is idle"); setCallGsmON(false); break; } } }; Log.i("[Manager] Registering phone state listener"); mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); mHasLastCallSasBeenRejected = false; mCallManager = new CallManager(c); File f = new File(mUserCertsPath); if (!f.exists()) { if (!f.mkdir()) { Log.e("[Manager] " + mUserCertsPath + " can't be created."); } } mMediaScanner = new MediaScanner(c); mCoreListener = new CoreListenerStub() { @SuppressLint("Wakelock") @Override public void onCallStateChanged( final Core core, final Call call, final State state, final String message) { Log.i("[Manager] Call state is [", state, "]"); if (state == State.IncomingReceived && !call.equals(core.getCurrentCall())) { if (call.getReplacedCall() != null) { // attended transfer will be accepted automatically. return; } } if ((state == State.IncomingReceived || state == State.IncomingEarlyMedia) && getCallGsmON()) { if (mCore != null) { call.decline(Reason.Busy); } } else if (state == State.IncomingReceived && (LinphonePreferences.instance().isAutoAnswerEnabled()) && !getCallGsmON()) { LinphoneUtils.dispatchOnUIThreadAfter( new Runnable() { @Override public void run() { if (mCore != null) { if (mCore.getCallsNb() > 0) { mCallManager.acceptCall(call); mAudioManager.routeAudioToEarPiece(); } } } }, mPrefs.getAutoAnswerTime()); } else if (state == State.End || state == State.Error) { if (mCore.getCallsNb() == 0) { // Disabling proximity sensor enableProximitySensing(false); } } else if (state == State.UpdatedByRemote) { // If the correspondent proposes video while audio call boolean remoteVideo = call.getRemoteParams().videoEnabled(); boolean localVideo = call.getCurrentParams().videoEnabled(); boolean autoAcceptCameraPolicy = LinphonePreferences.instance() .shouldAutomaticallyAcceptVideoRequests(); if (remoteVideo && !localVideo && !autoAcceptCameraPolicy && mCore.getConference() == null) { call.deferUpdate(); } } } @Override public void onFriendListCreated(Core core, FriendList list) { if (LinphoneContext.isReady()) { list.addListener(ContactsManager.getInstance()); } } @Override public void onFriendListRemoved(Core core, FriendList list) { list.removeListener(ContactsManager.getInstance()); } }; mAccountCreatorListener = new AccountCreatorListenerStub() { @Override public void onIsAccountExist( AccountCreator accountCreator, AccountCreator.Status status, String resp) { if (status.equals(AccountCreator.Status.AccountExist)) { accountCreator.isAccountLinked(); } } @Override public void onLinkAccount( AccountCreator accountCreator, AccountCreator.Status status, String resp) { if (status.equals(AccountCreator.Status.AccountNotLinked)) { askLinkWithPhoneNumber(); } } @Override public void onIsAccountLinked( AccountCreator accountCreator, AccountCreator.Status status, String resp) { if (status.equals(AccountCreator.Status.AccountNotLinked)) { askLinkWithPhoneNumber(); } } }; } public static synchronized LinphoneManager getInstance() { LinphoneManager manager = LinphoneContext.instance().getLinphoneManager(); if (manager == null) { throw new RuntimeException( "[Manager] Linphone Manager should be created before accessed"); } if (manager.mExited) { throw new RuntimeException( "[Manager] Linphone Manager was already destroyed. " + "Better use getCore and check returned value"); } return manager; } public static synchronized AndroidAudioManager getAudioManager() { return getInstance().mAudioManager; } public static synchronized CallManager getCallManager() { return getInstance().mCallManager; } public static synchronized Core getCore() { if (!LinphoneContext.isReady()) return null; if (getInstance().mExited) { // 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. return null; } return getInstance().mCore; } /* End of static */ public MediaScanner getMediaScanner() { return mMediaScanner; } public synchronized void destroy() { destroyManager(); // Wait for Manager to destroy everything before setting mExited to true // Otherwise some objects might crash during their own destroy if they try to call // LinphoneManager.getCore(), for example to unregister a listener mExited = true; } public void restartCore() { Log.w("[Manager] Restarting Core"); mCore.stop(); mCore.start(); } private void destroyCore() { Log.w("[Manager] Destroying Core"); if (LinphonePreferences.instance() != null) { // We set network reachable at false before destroying the Core // to not send a register with expires at 0 if (LinphonePreferences.instance().isPushNotificationEnabled()) { Log.w( "[Manager] Setting network reachability to False to prevent unregister and allow incoming push notifications"); mCore.setNetworkReachable(false); } } mCore.stop(); mCore.removeListener(mCoreListener); } private synchronized void destroyManager() { Log.w("[Manager] Destroying Manager"); changeStatusToOffline(); if (mTelephonyManager != null) { Log.i("[Manager] Unregistering phone state listener"); mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); } if (mCallManager != null) mCallManager.destroy(); if (mMediaScanner != null) mMediaScanner.destroy(); if (mAudioManager != null) mAudioManager.destroy(); if (mTimer != null) mTimer.cancel(); if (mCore != null) { destroyCore(); mCore = null; } } public synchronized void startLibLinphone(boolean isPush, CoreListener listener) { try { mCore = Factory.instance() .createCore( mPrefs.getLinphoneDefaultConfig(), mPrefs.getLinphoneFactoryConfig(), mContext); mCore.addListener(listener); mCore.addListener(mCoreListener); if (isPush) { Log.w( "[Manager] We are here because of a received push notification, enter background mode before starting the Core"); mCore.enterBackground(); } mCore.start(); mIterateRunnable = new Runnable() { @Override public void run() { if (mCore != null) { mCore.iterate(); } } }; TimerTask lTask = new TimerTask() { @Override public void run() { LinphoneUtils.dispatchOnUIThread(mIterateRunnable); } }; /*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); configureCore(); } catch (Exception e) { Log.e(e, "[Manager] Cannot start linphone"); } } private synchronized void configureCore() { Log.i("[Manager] Configuring Core"); mAudioManager = new AndroidAudioManager(mContext); mCore.setZrtpSecretsFile(mBasePath + "/zrtp_secrets"); String deviceName = mPrefs.getDeviceName(mContext); String appName = mContext.getResources().getString(R.string.user_agent); String androidVersion = org.linphone.BuildConfig.VERSION_NAME; String userAgent = appName + "/" + androidVersion + " (" + deviceName + ") LinphoneSDK"; mCore.setUserAgent( userAgent, getString(R.string.linphone_sdk_version) + " (" + getString(R.string.linphone_sdk_branch) + ")"); // mCore.setChatDatabasePath(mChatDatabaseFile); mCore.setCallLogsDatabasePath(mCallLogDatabaseFile); mCore.setFriendsDatabasePath(mFriendsDatabaseFile); mCore.setUserCertificatesPath(mUserCertsPath); // mCore.setCallErrorTone(Reason.NotFound, mErrorToneFile); enableDeviceRingtone(mPrefs.isDeviceRingtoneEnabled()); int availableCores = Runtime.getRuntime().availableProcessors(); Log.w("[Manager] MediaStreamer : " + availableCores + " cores detected and configured"); mCore.migrateLogsFromRcToDb(); // Migrate existing linphone accounts to have conference factory uri and LIME X3Dh url set String uri = getString(R.string.default_conference_factory_uri); for (ProxyConfig lpc : mCore.getProxyConfigList()) { if (lpc.getIdentityAddress().getDomain().equals(getString(R.string.default_domain))) { if (lpc.getConferenceFactoryUri() == null) { lpc.edit(); Log.i( "[Manager] Setting conference factory on proxy config " + lpc.getIdentityAddress().asString() + " to default value: " + uri); lpc.setConferenceFactoryUri(uri); lpc.done(); } if (mCore.limeX3DhAvailable()) { String url = mCore.getLimeX3DhServerUrl(); if (url == null || url.isEmpty()) { url = getString(R.string.default_lime_x3dh_server_url); Log.i("[Manager] Setting LIME X3Dh server url to default value: " + url); mCore.setLimeX3DhServerUrl(url); } } } } if (mContext.getResources().getBoolean(R.bool.enable_push_id)) { PushNotificationUtils.init(mContext); } mProximityWakelock = mPowerManager.newWakeLock( PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, mContext.getPackageName() + ";manager_proximity_sensor"); resetCameraFromPreferences(); mAccountCreator = mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); mAccountCreator.setListener(mAccountCreatorListener); mCallGsmON = false; Log.i("[Manager] Core configured"); } public void resetCameraFromPreferences() { Core core = getCore(); if (core == null) return; boolean useFrontCam = LinphonePreferences.instance().useFrontCam(); String firstDevice = null; for (String camera : core.getVideoDevicesList()) { if (firstDevice == null) { firstDevice = camera; } if (useFrontCam) { if (camera.contains("Front")) { Log.i("[Manager] Found front facing camera: " + camera); core.setVideoDevice(camera); return; } } } Log.i("[Manager] Using first camera available: " + firstDevice); core.setVideoDevice(firstDevice); } /* Account linking */ public AccountCreator getAccountCreator() { if (mAccountCreator == null) { Log.w("[Manager] Account creator shouldn't be null !"); mAccountCreator = mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); mAccountCreator.setListener(mAccountCreatorListener); } return mAccountCreator; } public void isAccountWithAlias() { if (mCore.getDefaultProxyConfig() != null) { long now = new Timestamp(new Date().getTime()).getTime(); AccountCreator accountCreator = getAccountCreator(); if (LinphonePreferences.instance().getLinkPopupTime() == null || Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) < now) { accountCreator.reset(); accountCreator.setUsername( LinphonePreferences.instance() .getAccountUsername( LinphonePreferences.instance().getDefaultAccountIndex())); accountCreator.isAccountExist(); } } else { LinphonePreferences.instance().setLinkPopupTime(null); } } private void askLinkWithPhoneNumber() { if (!LinphonePreferences.instance().isLinkPopupEnabled()) return; long now = new Timestamp(new Date().getTime()).getTime(); if (LinphonePreferences.instance().getLinkPopupTime() != null && Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) >= now) return; ProxyConfig proxyConfig = mCore.getDefaultProxyConfig(); if (proxyConfig == null) return; if (!proxyConfig.getDomain().equals(getString(R.string.default_domain))) return; long future = new Timestamp( mContext.getResources() .getInteger( R.integer.phone_number_linking_popup_time_interval)) .getTime(); long newDate = now + future; LinphonePreferences.instance().setLinkPopupTime(String.valueOf(newDate)); final Dialog dialog = LinphoneUtils.getDialog( mContext, String.format( getString(R.string.link_account_popup), proxyConfig.getIdentityAddress().asStringUriOnly())); Button delete = dialog.findViewById(R.id.dialog_delete_button); delete.setVisibility(View.GONE); Button ok = dialog.findViewById(R.id.dialog_ok_button); ok.setText(getString(R.string.link)); ok.setVisibility(View.VISIBLE); Button cancel = dialog.findViewById(R.id.dialog_cancel_button); cancel.setText(getString(R.string.maybe_later)); dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE); final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); dialog.findViewById(R.id.doNotAskAgainLabel) .setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); } }); ok.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { Intent assistant = new Intent(); assistant.setClass(mContext, PhoneAccountLinkingAssistantActivity.class); mContext.startActivity(assistant); dialog.dismiss(); } }); cancel.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { if (doNotAskAgain.isChecked()) { LinphonePreferences.instance().enableLinkPopup(false); } dialog.dismiss(); } }); dialog.show(); } /* Presence stuff */ private boolean isPresenceModelActivitySet() { if (mCore != null) { return mCore.getPresenceModel() != null && mCore.getPresenceModel().getActivity() != null; } return false; } public void changeStatusToOnline() { if (mCore == null) return; PresenceModel model = mCore.createPresenceModel(); model.setBasicStatus(PresenceBasicStatus.Open); mCore.setPresenceModel(model); } public void changeStatusToOnThePhone() { if (mCore == null) return; if (isPresenceModelActivitySet() && mCore.getPresenceModel().getActivity().getType() != PresenceActivity.Type.OnThePhone) { mCore.getPresenceModel().getActivity().setType(PresenceActivity.Type.OnThePhone); } else if (!isPresenceModelActivitySet()) { PresenceModel model = mCore.createPresenceModelWithActivity(PresenceActivity.Type.OnThePhone, null); mCore.setPresenceModel(model); } } private void changeStatusToOffline() { if (mCore != null) { PresenceModel model = mCore.getPresenceModel(); model.setBasicStatus(PresenceBasicStatus.Closed); mCore.setPresenceModel(model); } } /* Tunnel stuff */ public void initTunnelFromConf() { if (!mCore.tunnelAvailable()) return; NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); Tunnel tunnel = mCore.getTunnel(); tunnel.cleanServers(); TunnelConfig config = mPrefs.getTunnelConfig(); if (config.getHost() != null) { tunnel.addServer(config); manageTunnelServer(info); } } private boolean isTunnelNeeded(NetworkInfo info) { if (info == null) { Log.i("[Manager] 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("[Manager] Need tunnel: 'no wifi' connection"); return true; } return false; } private void manageTunnelServer(NetworkInfo info) { if (mCore == null) return; if (!mCore.tunnelAvailable()) return; Tunnel tunnel = mCore.getTunnel(); Log.i("[Manager] Managing tunnel"); if (isTunnelNeeded(info)) { Log.i("[Manager] Tunnel need to be activated"); tunnel.setMode(Tunnel.Mode.Enable); } else { Log.i("[Manager] Tunnel should not be used"); String pref = mPrefs.getTunnelMode(); tunnel.setMode(Tunnel.Mode.Disable); if (getString(R.string.tunnel_mode_entry_value_auto).equals(pref)) { tunnel.setMode(Tunnel.Mode.Auto); } } } /* Proximity sensor stuff */ public void enableProximitySensing(boolean enable) { if (enable) { if (!mProximitySensingEnabled) { mSensorManager.registerListener( this, mProximity, SensorManager.SENSOR_DELAY_NORMAL); mProximitySensingEnabled = true; } } else { if (mProximitySensingEnabled) { mSensorManager.unregisterListener(this); mProximitySensingEnabled = false; // Don't forgeting to release wakelock if held if (mProximityWakelock.isHeld()) { mProximityWakelock.release(); } } } } private 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( "[Manager] Proximity sensor report [" + distanceInCm + "] , for max range [" + maxDistance + "]"); if (maxDistance <= threshold) { // Case binary 0/1 and short sensors threshold = maxDistance; } return distanceInCm < threshold; } @Override public void onSensorChanged(SensorEvent event) { if (event.timestamp == 0) return; if (isProximitySensorNearby(event)) { if (!mProximityWakelock.isHeld()) { mProximityWakelock.acquire(); } } else { if (mProximityWakelock.isHeld()) { mProximityWakelock.release(); } } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} /* Other stuff */ public void enableDeviceRingtone(boolean use) { if (use) { mCore.setRing(null); } else { mCore.setRing(mRingSoundFile); } } public boolean getCallGsmON() { return mCallGsmON; } public void setCallGsmON(boolean on) { mCallGsmON = on; if (on && mCore != null) { mCore.pauseAllCalls(); } } private String getString(int key) { return mContext.getString(key); } public boolean hasLastCallSasBeenRejected() { return mHasLastCallSasBeenRejected; } public void lastCallSasRejected(boolean rejected) { mHasLastCallSasBeenRejected = rejected; } }