Java tutorial
/* * This is the source code of Telegram for Android v. 3.x.x. * It is licensed under GNU GPL v. 2 or later. * You should have received a copy of the license in this archive (see LICENSE). * * Copyright Grishka, 2013-2016. */ package com.ferdi2005.secondgram.voip; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.KeyguardManager; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioTrack; import android.media.MediaPlayer; import android.media.RingtoneManager; import android.media.SoundPool; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.os.PowerManager; import android.os.Vibrator; import android.support.annotation.Nullable; import android.support.v4.app.NotificationManagerCompat; import android.telephony.TelephonyManager; import android.text.Html; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.KeyEvent; import com.ferdi2005.secondgram.AndroidUtilities; import com.ferdi2005.secondgram.ApplicationLoader; import com.ferdi2005.secondgram.BuildConfig; import com.ferdi2005.secondgram.BuildVars; import com.ferdi2005.secondgram.ContactsController; import com.ferdi2005.secondgram.FileLoader; import com.ferdi2005.secondgram.FileLog; import com.ferdi2005.secondgram.ImageLoader; import com.ferdi2005.secondgram.LocaleController; import com.ferdi2005.secondgram.MessagesController; import com.ferdi2005.secondgram.MessagesStorage; import com.ferdi2005.secondgram.NotificationCenter; import com.ferdi2005.secondgram.R; import com.ferdi2005.secondgram.StatsController; import com.ferdi2005.secondgram.Utilities; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.ui.VoIPActivity; import org.telegram.ui.VoIPFeedbackActivity; import org.telegram.ui.VoIPPermissionActivity; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; public class VoIPService extends Service implements VoIPController.ConnectionStateListener, SensorEventListener, AudioManager.OnAudioFocusChangeListener, NotificationCenter.NotificationCenterDelegate { private static final int ID_ONGOING_CALL_NOTIFICATION = 201; private static final int ID_INCOMING_CALL_NOTIFICATION = 202; private static final int CALL_MIN_LAYER = 65; private static final int CALL_MAX_LAYER = 65; public static final int STATE_WAIT_INIT = 1; public static final int STATE_WAIT_INIT_ACK = 2; public static final int STATE_ESTABLISHED = 3; public static final int STATE_FAILED = 4; public static final int STATE_HANGING_UP = 5; public static final int STATE_ENDED = 6; public static final int STATE_EXCHANGING_KEYS = 7; public static final int STATE_WAITING = 8; public static final int STATE_REQUESTING = 9; public static final int STATE_WAITING_INCOMING = 10; public static final int STATE_RINGING = 11; public static final int STATE_BUSY = 12; public static final int DISCARD_REASON_HANGUP = 1; public static final int DISCARD_REASON_DISCONNECT = 2; public static final int DISCARD_REASON_MISSED = 3; public static final int DISCARD_REASON_LINE_BUSY = 4; private static final String TAG = "tg-voip-service"; private static VoIPService sharedInstance; private int userID; private TLRPC.User user; private boolean isOutgoing; private TLRPC.PhoneCall call; private Notification ongoingCallNotification; private VoIPController controller; private int currentState = 0; private int endHash; private int callReqId; private int lastError; private byte[] g_a; private byte[] a_or_b; private byte[] g_a_hash; private byte[] authKey; private long keyFingerprint; public static TLRPC.PhoneCall callIShouldHavePutIntoIntent; private static final int PROXIMITY_SCREEN_OFF_WAKE_LOCK = 32; private PowerManager.WakeLock proximityWakelock; private PowerManager.WakeLock cpuWakelock; private boolean isProximityNear, isHeadsetPlugged; private ArrayList<StateListener> stateListeners = new ArrayList<>(); private MediaPlayer ringtonePlayer; private Vibrator vibrator; private SoundPool soundPool; private int spRingbackID, spFailedID, spEndId, spBusyId, spConnectingId; private int spPlayID; private boolean needPlayEndSound; private Runnable timeoutRunnable; private boolean haveAudioFocus; private boolean playingSound; private boolean micMute; private boolean controllerStarted; private long lastKnownDuration = 0; private VoIPController.Stats stats = new VoIPController.Stats(), prevStats = new VoIPController.Stats(); private NetworkInfo lastNetInfo; private Boolean mHasEarpiece = null; private BluetoothAdapter btAdapter; private boolean isBtHeadsetConnected; private boolean needSendDebugLog = false; private boolean endCallAfterRequest = false; private ArrayList<TLRPC.PhoneCall> pendingUpdates = new ArrayList<>(); public static final String ACTION_HEADSET_PLUG = "android.intent.action.HEADSET_PLUG"; private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (ACTION_HEADSET_PLUG.equals(intent.getAction())) { isHeadsetPlugged = intent.getIntExtra("state", 0) == 1; if (isHeadsetPlugged && proximityWakelock != null && proximityWakelock.isHeld()) { proximityWakelock.release(); } isProximityNear = false; } else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { updateNetworkType(); } else if ((getPackageName() + ".END_CALL").equals(intent.getAction())) { if (intent.getIntExtra("end_hash", 0) == endHash) { stopForeground(true); hangUp(); } } else if ((getPackageName() + ".DECLINE_CALL").equals(intent.getAction())) { if (intent.getIntExtra("end_hash", 0) == endHash) { stopForeground(true); declineIncomingCall(DISCARD_REASON_LINE_BUSY, null); } } else if ((getPackageName() + ".ANSWER_CALL").equals(intent.getAction())) { if (intent.getIntExtra("end_hash", 0) == endHash) { showNotification(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission( Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { try { PendingIntent.getActivity(VoIPService.this, 0, new Intent(VoIPService.this, VoIPPermissionActivity.class) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0).send(); } catch (Exception x) { FileLog.e("Error starting permission activity", x); } return; } acceptIncomingCall(); try { PendingIntent .getActivity(VoIPService.this, 0, new Intent(VoIPService.this, VoIPActivity.class).addFlags( Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP), 0) .send(); } catch (Exception x) { FileLog.e("Error starting incall activity", x); } } } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { //FileLog.e("bt headset state = "+intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0)); updateBluetoothHeadsetState( intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0) == BluetoothProfile.STATE_CONNECTED); } else if (AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED.equals(intent.getAction())) { for (StateListener l : stateListeners) l.onAudioSettingsChanged(); } else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(intent.getAction())) { String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(state)) { hangUp(); } } } }; @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (sharedInstance != null) { FileLog.e("Tried to start the VoIP service when it's already started"); return START_NOT_STICKY; } userID = intent.getIntExtra("user_id", 0); isOutgoing = intent.getBooleanExtra("is_outgoing", false); user = MessagesController.getInstance().getUser(userID); if (user == null) { FileLog.w("VoIPService: user==null"); stopSelf(); return START_NOT_STICKY; } if (isOutgoing) { startOutgoingCall(); if (intent.getBooleanExtra("start_incall_activity", false)) { startActivity(new Intent(this, VoIPActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } } else { NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeInCallActivity); call = callIShouldHavePutIntoIntent; callIShouldHavePutIntoIntent = null; acknowledgeCallAndStartRinging(); } sharedInstance = this; return START_NOT_STICKY; } @Override public void onCreate() { super.onCreate(); FileLog.d("=============== VoIPService STARTING ==============="); AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1 && am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER) != null) { int outFramesPerBuffer = Integer .parseInt(am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)); VoIPController.setNativeBufferSize(outFramesPerBuffer); } else { VoIPController.setNativeBufferSize( AudioTrack.getMinBufferSize(48000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT) / 2); } final SharedPreferences preferences = getSharedPreferences("mainconfig", MODE_PRIVATE); VoIPServerConfig.setConfig(preferences.getString("voip_server_config", "{}")); if (System.currentTimeMillis() - preferences.getLong("voip_server_config_updated", 0) > 24 * 3600000) { ConnectionsManager.getInstance().sendRequest(new TLRPC.TL_phone_getCallConfig(), new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { String data = ((TLRPC.TL_dataJSON) response).data; VoIPServerConfig.setConfig(data); preferences.edit().putString("voip_server_config", data) .putLong("voip_server_config_updated", BuildConfig.DEBUG ? 0 : System.currentTimeMillis()) .apply(); } } }); } try { controller = new VoIPController(); controller.setConnectionStateListener(this); controller.setConfig(MessagesController.getInstance().callPacketTimeout / 1000.0, MessagesController.getInstance().callConnectTimeout / 1000.0, preferences.getInt("VoipDataSaving", VoIPController.DATA_SAVING_NEVER)); cpuWakelock = ((PowerManager) getSystemService(POWER_SERVICE)) .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "telegram-voip"); cpuWakelock.acquire(); btAdapter = am.isBluetoothScoAvailableOffCall() ? BluetoothAdapter.getDefaultAdapter() : null; IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(ACTION_HEADSET_PLUG); if (btAdapter != null) { filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); } filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); filter.addAction(getPackageName() + ".END_CALL"); filter.addAction(getPackageName() + ".DECLINE_CALL"); filter.addAction(getPackageName() + ".ANSWER_CALL"); registerReceiver(receiver, filter); ConnectionsManager.getInstance().setAppPaused(false, false); soundPool = new SoundPool(1, AudioManager.STREAM_VOICE_CALL, 0); spConnectingId = soundPool.load(this, R.raw.voip_connecting, 1); spRingbackID = soundPool.load(this, R.raw.voip_ringback, 1); spFailedID = soundPool.load(this, R.raw.voip_failed, 1); spEndId = soundPool.load(this, R.raw.voip_end, 1); spBusyId = soundPool.load(this, R.raw.voip_busy, 1); am.registerMediaButtonEventReceiver(new ComponentName(this, VoIPMediaButtonReceiver.class)); if (btAdapter != null && btAdapter.isEnabled()) { int headsetState = btAdapter.getProfileConnectionState(BluetoothProfile.HEADSET); updateBluetoothHeadsetState(headsetState == BluetoothProfile.STATE_CONNECTED); if (headsetState == BluetoothProfile.STATE_CONNECTED) am.setBluetoothScoOn(true); for (StateListener l : stateListeners) l.onAudioSettingsChanged(); } NotificationCenter.getInstance().addObserver(this, NotificationCenter.appDidLogout); } catch (Exception x) { FileLog.e("error initializing voip controller", x); callFailed(); } } @Override public void onDestroy() { FileLog.d("=============== VoIPService STOPPING ==============="); stopForeground(true); stopRinging(); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.appDidLogout); SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); if (proximity != null) { sm.unregisterListener(this); } if (proximityWakelock != null && proximityWakelock.isHeld()) { proximityWakelock.release(); } unregisterReceiver(receiver); if (timeoutRunnable != null) { AndroidUtilities.cancelRunOnUIThread(timeoutRunnable); timeoutRunnable = null; } super.onDestroy(); sharedInstance = null; AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { NotificationCenter.getInstance().postNotificationName(NotificationCenter.didEndedCall); } }); if (controller != null && controllerStarted) { lastKnownDuration = controller.getCallDuration(); updateStats(); StatsController.getInstance().incrementTotalCallsTime(getStatsNetworkType(), (int) (lastKnownDuration / 1000) % 5); if (needSendDebugLog) { String debugLog = controller.getDebugLog(); TLRPC.TL_phone_saveCallDebug req = new TLRPC.TL_phone_saveCallDebug(); req.debug = new TLRPC.TL_dataJSON(); req.debug.data = debugLog; req.peer = new TLRPC.TL_inputPhoneCall(); req.peer.access_hash = call.access_hash; req.peer.id = call.id; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { FileLog.d("Sent debug logs, response=" + response); } }); } controller.release(); controller = null; } cpuWakelock.release(); AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); if (isBtHeadsetConnected && !playingSound) am.stopBluetoothSco(); am.setMode(AudioManager.MODE_NORMAL); am.unregisterMediaButtonEventReceiver(new ComponentName(this, VoIPMediaButtonReceiver.class)); if (haveAudioFocus) am.abandonAudioFocus(this); if (!playingSound) soundPool.release(); ConnectionsManager.getInstance().setAppPaused(true, false); } public static VoIPService getSharedInstance() { return sharedInstance; } public TLRPC.User getUser() { return user; } public void setMicMute(boolean mute) { controller.setMicMute(micMute = mute); } public boolean isMicMute() { return micMute; } public String getDebugString() { return controller.getDebugString(); } public long getCallDuration() { if (!controllerStarted || controller == null) return lastKnownDuration; return lastKnownDuration = controller.getCallDuration(); } public void hangUp() { declineIncomingCall(currentState == STATE_RINGING || (currentState == STATE_WAITING && isOutgoing) ? DISCARD_REASON_MISSED : DISCARD_REASON_HANGUP, null); } public void hangUp(Runnable onDone) { declineIncomingCall(currentState == STATE_RINGING || (currentState == STATE_WAITING && isOutgoing) ? DISCARD_REASON_MISSED : DISCARD_REASON_HANGUP, onDone); } public void registerStateListener(StateListener l) { stateListeners.add(l); if (currentState != 0) l.onStateChanged(currentState); } public void unregisterStateListener(StateListener l) { stateListeners.remove(l); } private void startOutgoingCall() { configureDeviceForCall(); showNotification(); startConnectingSound(); dispatchStateChanged(STATE_REQUESTING); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { NotificationCenter.getInstance().postNotificationName(NotificationCenter.didStartedCall); } }); final byte[] salt = new byte[256]; Utilities.random.nextBytes(salt); TLRPC.TL_messages_getDhConfig req = new TLRPC.TL_messages_getDhConfig(); req.random_length = 256; req.version = MessagesStorage.lastSecretVersion; callReqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { callReqId = 0; if (error == null) { TLRPC.messages_DhConfig res = (TLRPC.messages_DhConfig) response; if (response instanceof TLRPC.TL_messages_dhConfig) { if (!Utilities.isGoodPrime(res.p, res.g)) { callFailed(); return; } MessagesStorage.secretPBytes = res.p; MessagesStorage.secretG = res.g; MessagesStorage.lastSecretVersion = res.version; MessagesStorage.getInstance().saveSecretParams(MessagesStorage.lastSecretVersion, MessagesStorage.secretG, MessagesStorage.secretPBytes); } final byte[] salt = new byte[256]; for (int a = 0; a < 256; a++) { salt[a] = (byte) ((byte) (Utilities.random.nextDouble() * 256) ^ res.random[a]); } BigInteger i_g_a = BigInteger.valueOf(MessagesStorage.secretG); i_g_a = i_g_a.modPow(new BigInteger(1, salt), new BigInteger(1, MessagesStorage.secretPBytes)); byte[] g_a = i_g_a.toByteArray(); if (g_a.length > 256) { byte[] correctedAuth = new byte[256]; System.arraycopy(g_a, 1, correctedAuth, 0, 256); g_a = correctedAuth; } TLRPC.TL_phone_requestCall reqCall = new TLRPC.TL_phone_requestCall(); reqCall.user_id = MessagesController.getInputUser(user); reqCall.protocol = new TLRPC.TL_phoneCallProtocol(); reqCall.protocol.udp_p2p = reqCall.protocol.udp_reflector = true; reqCall.protocol.min_layer = CALL_MIN_LAYER; reqCall.protocol.max_layer = CALL_MAX_LAYER; VoIPService.this.g_a = g_a; reqCall.g_a_hash = Utilities.computeSHA256(g_a, 0, g_a.length); reqCall.random_id = Utilities.random.nextInt(); ConnectionsManager.getInstance().sendRequest(reqCall, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (error == null) { call = ((TLRPC.TL_phone_phoneCall) response).phone_call; a_or_b = salt; dispatchStateChanged(STATE_WAITING); if (endCallAfterRequest) { hangUp(); return; } if (pendingUpdates.size() > 0 && call != null) { for (TLRPC.PhoneCall call : pendingUpdates) { onCallUpdated(call); } pendingUpdates.clear(); } timeoutRunnable = new Runnable() { @Override public void run() { timeoutRunnable = null; TLRPC.TL_phone_discardCall req = new TLRPC.TL_phone_discardCall(); req.peer = new TLRPC.TL_inputPhoneCall(); req.peer.access_hash = call.access_hash; req.peer.id = call.id; req.reason = new TLRPC.TL_phoneCallDiscardReasonMissed(); ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error != null) { FileLog.e( "error on phone.discardCall: " + error); } else { FileLog.d("phone.discardCall " + response); } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { callFailed(); } }); } }, ConnectionsManager.RequestFlagFailOnServerErrors); } }; AndroidUtilities.runOnUIThread(timeoutRunnable, MessagesController.getInstance().callReceiveTimeout); } else { if (error.code == 400 && "PARTICIPANT_VERSION_OUTDATED".equals(error.text)) { callFailed(VoIPController.ERROR_PEER_OUTDATED); } else if (error.code == 403 && "USER_PRIVACY_RESTRICTED".equals(error.text)) { callFailed(VoIPController.ERROR_PRIVACY); } else if (error.code == 406) { callFailed(VoIPController.ERROR_LOCALIZED); } else { FileLog.e("Error on phone.requestCall: " + error); callFailed(); } } } }); } }, ConnectionsManager.RequestFlagFailOnServerErrors); } else { FileLog.e("Error on getDhConfig " + error); callFailed(); } } }, ConnectionsManager.RequestFlagFailOnServerErrors); } private void acknowledgeCallAndStartRinging() { if (call instanceof TLRPC.TL_phoneCallDiscarded) { FileLog.w("Call " + call.id + " was discarded before the service started, stopping"); stopSelf(); return; } TLRPC.TL_phone_receivedCall req = new TLRPC.TL_phone_receivedCall(); req.peer = new TLRPC.TL_inputPhoneCall(); req.peer.id = call.id; req.peer.access_hash = call.access_hash; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (sharedInstance == null) return; FileLog.w("receivedCall response = " + response); if (error != null) { FileLog.e("error on receivedCall: " + error); stopSelf(); } else { startRinging(); } } }); } }, ConnectionsManager.RequestFlagFailOnServerErrors); } private void startRinging() { FileLog.d("starting ringing for call " + call.id); dispatchStateChanged(STATE_WAITING_INCOMING); //ringtone=RingtoneManager.getRingtone(this, Settings.System.DEFAULT_RINGTONE_URI); //ringtone.play(); SharedPreferences prefs = getSharedPreferences("Notifications", MODE_PRIVATE); ringtonePlayer = new MediaPlayer(); ringtonePlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { ringtonePlayer.start(); } }); ringtonePlayer.setLooping(true); ringtonePlayer.setAudioStreamType(AudioManager.STREAM_RING); try { String notificationUri; if (prefs.getBoolean("custom_" + user.id, false)) notificationUri = prefs.getString("ringtone_path_" + user.id, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE).toString()); else notificationUri = prefs.getString("CallsRingtonePath", RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE).toString()); ringtonePlayer.setDataSource(this, Uri.parse(notificationUri)); ringtonePlayer.prepareAsync(); } catch (Exception e) { FileLog.e(e); if (ringtonePlayer != null) { ringtonePlayer.release(); ringtonePlayer = null; } } AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); int vibrate; if (prefs.getBoolean("custom_" + user.id, false)) vibrate = prefs.getInt("calls_vibrate_" + user.id, 0); else vibrate = prefs.getInt("vibrate_calls", 0); if ((vibrate != 2 && vibrate != 4 && (am.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE || am.getRingerMode() == AudioManager.RINGER_MODE_NORMAL)) || (vibrate == 4 && am.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE)) { vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); long duration = 700; if (vibrate == 1) duration /= 2; else if (vibrate == 3) duration *= 2; vibrator.vibrate(new long[] { 0, duration, 500 }, 0); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !((KeyguardManager) getSystemService(KEYGUARD_SERVICE)).inKeyguardRestrictedInputMode() && NotificationManagerCompat.from(this).areNotificationsEnabled()) { showIncomingNotification(); FileLog.d("Showing incoming call notification"); } else { FileLog.d("Starting incall activity for incoming call"); try { PendingIntent.getActivity(VoIPService.this, 12345, new Intent(VoIPService.this, VoIPActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0) .send(); } catch (Exception x) { FileLog.e("Error starting incall activity", x); } } } public void acceptIncomingCall() { stopRinging(); showNotification(); configureDeviceForCall(); startConnectingSound(); dispatchStateChanged(STATE_EXCHANGING_KEYS); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { NotificationCenter.getInstance().postNotificationName(NotificationCenter.didStartedCall); } }); TLRPC.TL_messages_getDhConfig req = new TLRPC.TL_messages_getDhConfig(); req.random_length = 256; req.version = MessagesStorage.lastSecretVersion; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { TLRPC.messages_DhConfig res = (TLRPC.messages_DhConfig) response; if (response instanceof TLRPC.TL_messages_dhConfig) { if (!Utilities.isGoodPrime(res.p, res.g)) { /*acceptingChats.remove(encryptedChat.id); declineSecretChat(encryptedChat.id);*/ FileLog.e("stopping VoIP service, bad prime"); callFailed(); return; } MessagesStorage.secretPBytes = res.p; MessagesStorage.secretG = res.g; MessagesStorage.lastSecretVersion = res.version; MessagesStorage.getInstance().saveSecretParams(MessagesStorage.lastSecretVersion, MessagesStorage.secretG, MessagesStorage.secretPBytes); } byte[] salt = new byte[256]; for (int a = 0; a < 256; a++) { salt[a] = (byte) ((byte) (Utilities.random.nextDouble() * 256) ^ res.random[a]); } a_or_b = salt; BigInteger g_b = BigInteger.valueOf(MessagesStorage.secretG); BigInteger p = new BigInteger(1, MessagesStorage.secretPBytes); g_b = g_b.modPow(new BigInteger(1, salt), p); g_a_hash = call.g_a_hash; byte[] g_b_bytes = g_b.toByteArray(); if (g_b_bytes.length > 256) { byte[] correctedAuth = new byte[256]; System.arraycopy(g_b_bytes, 1, correctedAuth, 0, 256); g_b_bytes = correctedAuth; } TLRPC.TL_phone_acceptCall req = new TLRPC.TL_phone_acceptCall(); req.g_b = g_b_bytes; //req.key_fingerprint = Utilities.bytesToLong(authKeyId); req.peer = new TLRPC.TL_inputPhoneCall(); req.peer.id = call.id; req.peer.access_hash = call.access_hash; req.protocol = new TLRPC.TL_phoneCallProtocol(); req.protocol.udp_p2p = req.protocol.udp_reflector = true; req.protocol.min_layer = CALL_MIN_LAYER; req.protocol.max_layer = CALL_MAX_LAYER; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (error == null) { FileLog.w("accept call ok! " + response); call = ((TLRPC.TL_phone_phoneCall) response).phone_call; if (call instanceof TLRPC.TL_phoneCallDiscarded) { onCallUpdated(call); } /*else{ initiateActualEncryptedCall(); }*/ } else { FileLog.e("Error on phone.acceptCall: " + error); callFailed(); } } }); } }, ConnectionsManager.RequestFlagFailOnServerErrors); } else { //acceptingChats.remove(encryptedChat.id); callFailed(); } } }); } public void declineIncomingCall() { declineIncomingCall(DISCARD_REASON_HANGUP, null); } public void declineIncomingCall(int reason, final Runnable onDone) { if (currentState == STATE_REQUESTING) { endCallAfterRequest = true; return; } if (currentState == STATE_HANGING_UP || currentState == STATE_ENDED) return; dispatchStateChanged(STATE_HANGING_UP); if (call == null) { if (onDone != null) onDone.run(); callEnded(); if (callReqId != 0) { ConnectionsManager.getInstance().cancelRequest(callReqId, false); callReqId = 0; } return; } TLRPC.TL_phone_discardCall req = new TLRPC.TL_phone_discardCall(); req.peer = new TLRPC.TL_inputPhoneCall(); req.peer.access_hash = call.access_hash; req.peer.id = call.id; req.duration = controller != null && controllerStarted ? (int) (controller.getCallDuration() / 1000) : 0; req.connection_id = controller != null && controllerStarted ? controller.getPreferredRelayID() : 0; switch (reason) { case DISCARD_REASON_DISCONNECT: req.reason = new TLRPC.TL_phoneCallDiscardReasonDisconnect(); break; case DISCARD_REASON_MISSED: req.reason = new TLRPC.TL_phoneCallDiscardReasonMissed(); break; case DISCARD_REASON_LINE_BUSY: req.reason = new TLRPC.TL_phoneCallDiscardReasonBusy(); break; case DISCARD_REASON_HANGUP: default: req.reason = new TLRPC.TL_phoneCallDiscardReasonHangup(); break; } final boolean wasNotConnected = ConnectionsManager.getInstance() .getConnectionState() != ConnectionsManager.ConnectionStateConnected; if (wasNotConnected) { if (onDone != null) onDone.run(); callEnded(); } final Runnable stopper = new Runnable() { private boolean done = false; @Override public void run() { if (done) return; done = true; if (onDone != null) onDone.run(); callEnded(); } }; AndroidUtilities.runOnUIThread(stopper, (int) (VoIPServerConfig.getDouble("hangup_ui_timeout", 5) * 1000)); ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error != null) { FileLog.e("error on phone.discardCall: " + error); } else { if (response instanceof TLRPC.TL_updates) { TLRPC.TL_updates updates = (TLRPC.TL_updates) response; MessagesController.getInstance().processUpdates(updates, false); } FileLog.d("phone.discardCall " + response); } if (!wasNotConnected) { AndroidUtilities.cancelRunOnUIThread(stopper); if (onDone != null) onDone.run(); } } }, ConnectionsManager.RequestFlagFailOnServerErrors); } private void dumpCallObject() { try { Field[] flds = TLRPC.PhoneCall.class.getFields(); for (Field f : flds) { FileLog.d(f.getName() + " = " + f.get(call)); } } catch (Exception x) { FileLog.e(x); } } public void onCallUpdated(TLRPC.PhoneCall call) { if (this.call == null) { pendingUpdates.add(call); return; } if (call == null) return; if (call.id != this.call.id) { if (BuildVars.DEBUG_VERSION) FileLog.w("onCallUpdated called with wrong call id (got " + call.id + ", expected " + this.call.id + ")"); return; } if (call.access_hash == 0) call.access_hash = this.call.access_hash; if (BuildVars.DEBUG_VERSION) { FileLog.d("Call updated: " + call); dumpCallObject(); } this.call = call; if (call instanceof TLRPC.TL_phoneCallDiscarded) { needSendDebugLog = call.need_debug; FileLog.d("call discarded, stopping service"); if (call.reason instanceof TLRPC.TL_phoneCallDiscardReasonBusy) { dispatchStateChanged(STATE_BUSY); playingSound = true; soundPool.play(spBusyId, 1, 1, 0, -1, 1); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { soundPool.release(); if (isBtHeadsetConnected) ((AudioManager) ApplicationLoader.applicationContext.getSystemService(AUDIO_SERVICE)) .stopBluetoothSco(); } }, 2500); stopSelf(); } else { callEnded(); } if (call.need_rating) { startRatingActivity(); } } else if (call instanceof TLRPC.TL_phoneCall && authKey == null) { if (call.g_a_or_b == null) { FileLog.w("stopping VoIP service, Ga == null"); callFailed(); return; } if (!Arrays.equals(g_a_hash, Utilities.computeSHA256(call.g_a_or_b, 0, call.g_a_or_b.length))) { FileLog.w("stopping VoIP service, Ga hash doesn't match"); callFailed(); return; } g_a = call.g_a_or_b; BigInteger g_a = new BigInteger(1, call.g_a_or_b); BigInteger p = new BigInteger(1, MessagesStorage.secretPBytes); if (!Utilities.isGoodGaAndGb(g_a, p)) { FileLog.w("stopping VoIP service, bad Ga and Gb (accepting)"); callFailed(); return; } g_a = g_a.modPow(new BigInteger(1, a_or_b), p); byte[] authKey = g_a.toByteArray(); if (authKey.length > 256) { byte[] correctedAuth = new byte[256]; System.arraycopy(authKey, authKey.length - 256, correctedAuth, 0, 256); authKey = correctedAuth; } else if (authKey.length < 256) { byte[] correctedAuth = new byte[256]; System.arraycopy(authKey, 0, correctedAuth, 256 - authKey.length, authKey.length); for (int a = 0; a < 256 - authKey.length; a++) { authKey[a] = 0; } authKey = correctedAuth; } byte[] authKeyHash = Utilities.computeSHA1(authKey); byte[] authKeyId = new byte[8]; System.arraycopy(authKeyHash, authKeyHash.length - 8, authKeyId, 0, 8); VoIPService.this.authKey = authKey; keyFingerprint = Utilities.bytesToLong(authKeyId); if (keyFingerprint != call.key_fingerprint) { FileLog.w("key fingerprints don't match"); callFailed(); return; } initiateActualEncryptedCall(); } else if (call instanceof TLRPC.TL_phoneCallAccepted && authKey == null) { processAcceptedCall(); } else { if (currentState == STATE_WAITING && call.receive_date != 0) { dispatchStateChanged(STATE_RINGING); FileLog.d("!!!!!! CALL RECEIVED"); if (spPlayID != 0) soundPool.stop(spPlayID); spPlayID = soundPool.play(spRingbackID, 1, 1, 0, -1, 1); if (timeoutRunnable != null) { AndroidUtilities.cancelRunOnUIThread(timeoutRunnable); timeoutRunnable = null; } timeoutRunnable = new Runnable() { @Override public void run() { timeoutRunnable = null; declineIncomingCall(DISCARD_REASON_MISSED, null); } }; AndroidUtilities.runOnUIThread(timeoutRunnable, MessagesController.getInstance().callRingTimeout); } } } private void startRatingActivity() { try { PendingIntent .getActivity(VoIPService.this, 0, new Intent(VoIPService.this, VoIPFeedbackActivity.class).putExtra("call_id", call.id) .putExtra("call_access_hash", call.access_hash) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP), 0) .send(); } catch (Exception x) { FileLog.e("Error starting incall activity", x); } } public void stopRinging() { if (ringtonePlayer != null) { ringtonePlayer.stop(); ringtonePlayer.release(); ringtonePlayer = null; } if (vibrator != null) { vibrator.cancel(); vibrator = null; } } public byte[] getEncryptionKey() { return authKey; } private void processAcceptedCall() { dispatchStateChanged(STATE_EXCHANGING_KEYS); BigInteger p = new BigInteger(1, MessagesStorage.secretPBytes); BigInteger i_authKey = new BigInteger(1, call.g_b); if (!Utilities.isGoodGaAndGb(i_authKey, p)) { FileLog.w("stopping VoIP service, bad Ga and Gb"); callFailed(); return; } i_authKey = i_authKey.modPow(new BigInteger(1, a_or_b), p); byte[] authKey = i_authKey.toByteArray(); if (authKey.length > 256) { byte[] correctedAuth = new byte[256]; System.arraycopy(authKey, authKey.length - 256, correctedAuth, 0, 256); authKey = correctedAuth; } else if (authKey.length < 256) { byte[] correctedAuth = new byte[256]; System.arraycopy(authKey, 0, correctedAuth, 256 - authKey.length, authKey.length); for (int a = 0; a < 256 - authKey.length; a++) { authKey[a] = 0; } authKey = correctedAuth; } byte[] authKeyHash = Utilities.computeSHA1(authKey); byte[] authKeyId = new byte[8]; System.arraycopy(authKeyHash, authKeyHash.length - 8, authKeyId, 0, 8); long fingerprint = Utilities.bytesToLong(authKeyId); this.authKey = authKey; keyFingerprint = fingerprint; TLRPC.TL_phone_confirmCall req = new TLRPC.TL_phone_confirmCall(); req.g_a = g_a; req.key_fingerprint = fingerprint; req.peer = new TLRPC.TL_inputPhoneCall(); req.peer.id = call.id; req.peer.access_hash = call.access_hash; req.protocol = new TLRPC.TL_phoneCallProtocol(); req.protocol.max_layer = CALL_MAX_LAYER; req.protocol.min_layer = CALL_MIN_LAYER; req.protocol.udp_p2p = req.protocol.udp_reflector = true; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (error != null) { callFailed(); } else { call = ((TLRPC.TL_phone_phoneCall) response).phone_call; initiateActualEncryptedCall(); } } }); } }); } private void initiateActualEncryptedCall() { if (timeoutRunnable != null) { AndroidUtilities.cancelRunOnUIThread(timeoutRunnable); timeoutRunnable = null; } try { FileLog.d("InitCall: keyID=" + keyFingerprint); controller.setEncryptionKey(authKey, isOutgoing); TLRPC.TL_phoneConnection[] endpoints = new TLRPC.TL_phoneConnection[1 + call.alternative_connections.size()]; endpoints[0] = call.connection; for (int i = 0; i < call.alternative_connections.size(); i++) endpoints[i + 1] = call.alternative_connections.get(i); controller.setRemoteEndpoints(endpoints, call.protocol.udp_p2p); controller.start(); updateNetworkType(); controller.connect(); controllerStarted = true; AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (controller == null) return; updateStats(); AndroidUtilities.runOnUIThread(this, 5000); } }, 5000); } catch (Exception x) { FileLog.e("error starting call", x); callFailed(); } } private void showNotification() { Intent intent = new Intent(this, VoIPActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); Notification.Builder builder = new Notification.Builder(this) .setContentTitle(LocaleController.getString("VoipOutgoingCall", R.string.VoipOutgoingCall)) .setContentText(ContactsController.formatName(user.first_name, user.last_name)) .setSmallIcon(R.drawable.notification) .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { Intent endIntent = new Intent(); endIntent.setAction(getPackageName() + ".END_CALL"); endIntent.putExtra("end_hash", endHash = Utilities.random.nextInt()); builder.addAction(R.drawable.ic_call_end_white_24dp, LocaleController.getString("VoipEndCall", R.string.VoipEndCall), PendingIntent.getBroadcast(this, 0, endIntent, PendingIntent.FLAG_UPDATE_CURRENT)); builder.setPriority(Notification.PRIORITY_MAX); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { builder.setShowWhen(false); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setColor(0xff2ca5e0); } if (user.photo != null) { TLRPC.FileLocation photoPath = user.photo.photo_small; if (photoPath != null) { BitmapDrawable img = ImageLoader.getInstance().getImageFromMemory(photoPath, null, "50_50"); if (img != null) { builder.setLargeIcon(img.getBitmap()); } else { try { float scaleFactor = 160.0f / AndroidUtilities.dp(50); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = scaleFactor < 1 ? 1 : (int) scaleFactor; Bitmap bitmap = BitmapFactory .decodeFile(FileLoader.getPathToAttach(photoPath, true).toString(), options); if (bitmap != null) { builder.setLargeIcon(bitmap); } } catch (Throwable e) { FileLog.e(e); } } } } ongoingCallNotification = builder.getNotification(); startForeground(ID_ONGOING_CALL_NOTIFICATION, ongoingCallNotification); } private void showIncomingNotification() { Intent intent = new Intent(this, VoIPActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); Notification.Builder builder = new Notification.Builder(this) .setContentTitle(LocaleController.getString("VoipInCallBranding", R.string.VoipInCallBranding)) .setContentText(ContactsController.formatName(user.first_name, user.last_name)) .setSmallIcon(R.drawable.notification) .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { endHash = Utilities.random.nextInt(); Intent endIntent = new Intent(); endIntent.setAction(getPackageName() + ".DECLINE_CALL"); endIntent.putExtra("end_hash", endHash); CharSequence endTitle = LocaleController.getString("VoipDeclineCall", R.string.VoipDeclineCall); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { endTitle = new SpannableString(endTitle); ((SpannableString) endTitle).setSpan(new ForegroundColorSpan(0xFFF44336), 0, endTitle.length(), 0); } builder.addAction(R.drawable.ic_call_end_white_24dp, endTitle, PendingIntent.getBroadcast(this, 0, endIntent, PendingIntent.FLAG_UPDATE_CURRENT)); Intent answerIntent = new Intent(); answerIntent.setAction(getPackageName() + ".ANSWER_CALL"); answerIntent.putExtra("end_hash", endHash); CharSequence answerTitle = LocaleController.getString("VoipAnswerCall", R.string.VoipAnswerCall); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { answerTitle = new SpannableString(answerTitle); ((SpannableString) answerTitle).setSpan(new ForegroundColorSpan(0xFF00AA00), 0, answerTitle.length(), 0); } builder.addAction(R.drawable.ic_call_white_24dp, answerTitle, PendingIntent.getBroadcast(this, 0, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT)); builder.setPriority(Notification.PRIORITY_MAX); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { builder.setShowWhen(false); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setColor(0xff2ca5e0); builder.setVibrate(new long[0]); builder.setCategory(Notification.CATEGORY_CALL); builder.setFullScreenIntent(PendingIntent.getActivity(this, 0, intent, 0), true); } if (user.photo != null) { TLRPC.FileLocation photoPath = user.photo.photo_small; if (photoPath != null) { BitmapDrawable img = ImageLoader.getInstance().getImageFromMemory(photoPath, null, "50_50"); if (img != null) { builder.setLargeIcon(img.getBitmap()); } else { try { float scaleFactor = 160.0f / AndroidUtilities.dp(50); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = scaleFactor < 1 ? 1 : (int) scaleFactor; Bitmap bitmap = BitmapFactory .decodeFile(FileLoader.getPathToAttach(photoPath, true).toString(), options); if (bitmap != null) { builder.setLargeIcon(bitmap); } } catch (Throwable e) { FileLog.e(e); } } } } Notification incomingNotification = builder.getNotification(); startForeground(ID_INCOMING_CALL_NOTIFICATION, incomingNotification); } private void startConnectingSound() { if (spPlayID != 0) soundPool.stop(spPlayID); spPlayID = soundPool.play(spConnectingId, 1, 1, 0, -1, 1); if (spPlayID == 0) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (sharedInstance == null) return; if (spPlayID == 0) spPlayID = soundPool.play(spConnectingId, 1, 1, 0, -1, 1); if (spPlayID == 0) AndroidUtilities.runOnUIThread(this, 100); } }, 100); } } private void callFailed() { callFailed( controller != null && controllerStarted ? controller.getLastError() : VoIPController.ERROR_UNKNOWN); } private void callFailed(int errorCode) { try { throw new Exception("Call " + (call != null ? call.id : 0) + " failed with error code " + errorCode); } catch (Exception x) { FileLog.e(x); } lastError = errorCode; if (call != null) { FileLog.d("Discarding failed call"); TLRPC.TL_phone_discardCall req = new TLRPC.TL_phone_discardCall(); req.peer = new TLRPC.TL_inputPhoneCall(); req.peer.access_hash = call.access_hash; req.peer.id = call.id; req.duration = controller != null && controllerStarted ? (int) (controller.getCallDuration() / 1000) : 0; req.connection_id = controller != null && controllerStarted ? controller.getPreferredRelayID() : 0; req.reason = new TLRPC.TL_phoneCallDiscardReasonDisconnect(); ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error != null) { FileLog.e("error on phone.discardCall: " + error); } else { FileLog.d("phone.discardCall " + response); } } }); } dispatchStateChanged(STATE_FAILED); if (errorCode != VoIPController.ERROR_LOCALIZED && soundPool != null) { playingSound = true; soundPool.play(spFailedID, 1, 1, 0, 0, 1); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { soundPool.release(); if (isBtHeadsetConnected) ((AudioManager) ApplicationLoader.applicationContext.getSystemService(AUDIO_SERVICE)) .stopBluetoothSco(); } }, 1000); } stopSelf(); } private void callEnded() { FileLog.d("Call " + (call != null ? call.id : 0) + " ended"); dispatchStateChanged(STATE_ENDED); if (needPlayEndSound) { playingSound = true; soundPool.play(spEndId, 1, 1, 0, 0, 1); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { soundPool.release(); if (isBtHeadsetConnected) ((AudioManager) ApplicationLoader.applicationContext.getSystemService(AUDIO_SERVICE)) .stopBluetoothSco(); } }, 1000); } if (timeoutRunnable != null) { AndroidUtilities.cancelRunOnUIThread(timeoutRunnable); timeoutRunnable = null; } stopSelf(); } @Override public void onConnectionStateChanged(int newState) { if (newState == STATE_FAILED) { callFailed(); return; } if (newState == STATE_ESTABLISHED) { if (spPlayID != 0) { soundPool.stop(spPlayID); spPlayID = 0; } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (controller == null) return; int netType = StatsController.TYPE_WIFI; if (lastNetInfo != null) { if (lastNetInfo.getType() == ConnectivityManager.TYPE_MOBILE) netType = lastNetInfo.isRoaming() ? StatsController.TYPE_ROAMING : StatsController.TYPE_MOBILE; } StatsController.getInstance().incrementTotalCallsTime(netType, 5); AndroidUtilities.runOnUIThread(this, 5000); } }, 5000); if (isOutgoing) StatsController.getInstance().incrementSentItemsCount(getStatsNetworkType(), StatsController.TYPE_CALLS, 1); else StatsController.getInstance().incrementReceivedItemsCount(getStatsNetworkType(), StatsController.TYPE_CALLS, 1); } dispatchStateChanged(newState); } public boolean isOutgoing() { return isOutgoing; } @SuppressLint("NewApi") @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); if (isHeadsetPlugged || am.isSpeakerphoneOn() || (isBluetoothHeadsetConnected() && am.isBluetoothScoOn())) { return; } boolean newIsNear = event.values[0] < Math.min(event.sensor.getMaximumRange(), 3); if (newIsNear != isProximityNear) { FileLog.d("proximity " + newIsNear); isProximityNear = newIsNear; try { if (isProximityNear) { proximityWakelock.acquire(); } else { proximityWakelock.release(1); // this is non-public API before L } } catch (Exception x) { FileLog.e(x); } } } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } private void updateNetworkType() { ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); NetworkInfo info = cm.getActiveNetworkInfo(); lastNetInfo = info; int type = VoIPController.NET_TYPE_UNKNOWN; if (info != null) { switch (info.getType()) { case ConnectivityManager.TYPE_MOBILE: switch (info.getSubtype()) { case TelephonyManager.NETWORK_TYPE_GPRS: type = VoIPController.NET_TYPE_GPRS; break; case TelephonyManager.NETWORK_TYPE_EDGE: case TelephonyManager.NETWORK_TYPE_1xRTT: type = VoIPController.NET_TYPE_EDGE; break; case TelephonyManager.NETWORK_TYPE_UMTS: case TelephonyManager.NETWORK_TYPE_EVDO_0: type = VoIPController.NET_TYPE_3G; break; case TelephonyManager.NETWORK_TYPE_HSDPA: case TelephonyManager.NETWORK_TYPE_HSPA: case TelephonyManager.NETWORK_TYPE_HSPAP: case TelephonyManager.NETWORK_TYPE_HSUPA: case TelephonyManager.NETWORK_TYPE_EVDO_A: case TelephonyManager.NETWORK_TYPE_EVDO_B: type = VoIPController.NET_TYPE_HSPA; break; case TelephonyManager.NETWORK_TYPE_LTE: type = VoIPController.NET_TYPE_LTE; break; default: type = VoIPController.NET_TYPE_OTHER_MOBILE; break; } break; case ConnectivityManager.TYPE_WIFI: type = VoIPController.NET_TYPE_WIFI; break; case ConnectivityManager.TYPE_ETHERNET: type = VoIPController.NET_TYPE_ETHERNET; break; } } if (controller != null) { controller.setNetworkType(type); } } private void updateBluetoothHeadsetState(boolean connected) { if (connected == isBtHeadsetConnected) return; isBtHeadsetConnected = connected; AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); if (connected) am.startBluetoothSco(); else am.stopBluetoothSco(); for (StateListener l : stateListeners) l.onAudioSettingsChanged(); } public boolean isBluetoothHeadsetConnected() { return isBtHeadsetConnected; } private void configureDeviceForCall() { needPlayEndSound = true; AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); am.setMode(AudioManager.MODE_IN_COMMUNICATION); am.setSpeakerphoneOn(false); am.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN); SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); try { if (proximity != null) { proximityWakelock = ((PowerManager) getSystemService(Context.POWER_SERVICE)) .newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, "telegram-voip-prx"); sm.registerListener(this, proximity, SensorManager.SENSOR_DELAY_NORMAL); } } catch (Exception x) { FileLog.e("Error initializing proximity sensor", x); } } private void dispatchStateChanged(int state) { FileLog.d("== Call " + (call != null ? call.id : 0) + " state changed to " + state + " =="); currentState = state; for (int a = 0; a < stateListeners.size(); a++) { StateListener l = stateListeners.get(a); l.onStateChanged(state); } } @Override public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { haveAudioFocus = true; } else { haveAudioFocus = false; } } public void onUIForegroundStateChanged(boolean isForeground) { if (currentState == STATE_WAITING_INCOMING) { if (isForeground) { stopForeground(true); } else { if (!((KeyguardManager) getSystemService(KEYGUARD_SERVICE)).inKeyguardRestrictedInputMode()) { showIncomingNotification(); } else { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { Intent intent = new Intent(VoIPService.this, VoIPActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); try { PendingIntent.getActivity(VoIPService.this, 0, intent, 0).send(); } catch (PendingIntent.CanceledException e) { FileLog.e("error restarting activity", e); } } }, 500); } } } } /*package*/ void onMediaButtonEvent(KeyEvent ev) { if (ev.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) { if (ev.getAction() == KeyEvent.ACTION_UP) { if (currentState == STATE_WAITING_INCOMING) { acceptIncomingCall(); } else { setMicMute(!isMicMute()); for (StateListener l : stateListeners) l.onAudioSettingsChanged(); } } } } public void debugCtl(int request, int param) { if (controller != null) controller.debugCtl(request, param); } public int getLastError() { return lastError; } private void updateStats() { controller.getStats(stats); long wifiSentDiff = stats.bytesSentWifi - prevStats.bytesSentWifi; long wifiRecvdDiff = stats.bytesRecvdWifi - prevStats.bytesRecvdWifi; long mobileSentDiff = stats.bytesSentMobile - prevStats.bytesSentMobile; long mobileRecvdDiff = stats.bytesRecvdMobile - prevStats.bytesRecvdMobile; VoIPController.Stats tmp = stats; stats = prevStats; prevStats = tmp; if (wifiSentDiff > 0) StatsController.getInstance().incrementSentBytesCount(StatsController.TYPE_WIFI, StatsController.TYPE_CALLS, wifiSentDiff); if (wifiRecvdDiff > 0) StatsController.getInstance().incrementReceivedBytesCount(StatsController.TYPE_WIFI, StatsController.TYPE_CALLS, wifiRecvdDiff); if (mobileSentDiff > 0) StatsController.getInstance().incrementSentBytesCount( lastNetInfo != null && lastNetInfo.isRoaming() ? StatsController.TYPE_ROAMING : StatsController.TYPE_MOBILE, StatsController.TYPE_CALLS, mobileSentDiff); if (mobileRecvdDiff > 0) StatsController.getInstance().incrementReceivedBytesCount( lastNetInfo != null && lastNetInfo.isRoaming() ? StatsController.TYPE_ROAMING : StatsController.TYPE_MOBILE, StatsController.TYPE_CALLS, mobileRecvdDiff); } private int getStatsNetworkType() { int netType = StatsController.TYPE_WIFI; if (lastNetInfo != null) { if (lastNetInfo.getType() == ConnectivityManager.TYPE_MOBILE) netType = lastNetInfo.isRoaming() ? StatsController.TYPE_ROAMING : StatsController.TYPE_MOBILE; } return netType; } public boolean hasEarpiece() { if (((TelephonyManager) getSystemService(TELEPHONY_SERVICE)) .getPhoneType() != TelephonyManager.PHONE_TYPE_NONE) return true; if (mHasEarpiece != null) { return mHasEarpiece; } // not calculated yet, do it now try { AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); Method method = AudioManager.class.getMethod("getDevicesForStream", Integer.TYPE); Field field = AudioManager.class.getField("DEVICE_OUT_EARPIECE"); int earpieceFlag = field.getInt(null); int bitmaskResult = (int) method.invoke(am, AudioManager.STREAM_VOICE_CALL); // check if masked by the earpiece flag if ((bitmaskResult & earpieceFlag) == earpieceFlag) { mHasEarpiece = Boolean.TRUE; } else { mHasEarpiece = Boolean.FALSE; } } catch (Throwable error) { FileLog.e("Error while checking earpiece! ", error); mHasEarpiece = Boolean.TRUE; } return mHasEarpiece; } public int getCallState() { return currentState; } public byte[] getGA() { return g_a; } @Override public void didReceivedNotification(int id, Object... args) { if (id == NotificationCenter.appDidLogout) { callEnded(); } } public interface StateListener { void onStateChanged(int state); void onAudioSettingsChanged(); } }