mirror of https://github.com/NekoX-Dev/NekoX.git
2029 lines
68 KiB
Java
Executable File
2029 lines
68 KiB
Java
Executable File
/*
|
|
* This is the source code of Telegram for Android v. 5.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 org.telegram.messenger.voip;
|
|
|
|
import android.Manifest;
|
|
import android.annotation.SuppressLint;
|
|
import android.annotation.TargetApi;
|
|
import android.app.Activity;
|
|
import android.app.KeyguardManager;
|
|
import android.app.Notification;
|
|
import android.app.PendingIntent;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.pm.PackageManager;
|
|
import android.media.AudioManager;
|
|
import android.media.audiofx.AcousticEchoCanceler;
|
|
import android.media.audiofx.NoiseSuppressor;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.IBinder;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import android.os.SystemClock;
|
|
import android.telecom.TelecomManager;
|
|
import android.text.TextUtils;
|
|
import android.view.KeyEvent;
|
|
import android.widget.Toast;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
import org.telegram.messenger.AccountInstance;
|
|
import org.telegram.messenger.AndroidUtilities;
|
|
import org.telegram.messenger.ApplicationLoader;
|
|
import org.telegram.messenger.BuildVars;
|
|
import org.telegram.messenger.ChatObject;
|
|
import org.telegram.messenger.ContactsController;
|
|
import org.telegram.messenger.FileLog;
|
|
import org.telegram.messenger.LocaleController;
|
|
import org.telegram.messenger.MessageObject;
|
|
import org.telegram.messenger.MessagesController;
|
|
import org.telegram.messenger.MessagesStorage;
|
|
import org.telegram.messenger.NotificationCenter;
|
|
import org.telegram.messenger.NotificationsController;
|
|
import org.telegram.messenger.R;
|
|
import org.telegram.messenger.UserConfig;
|
|
import org.telegram.messenger.Utilities;
|
|
import org.telegram.messenger.XiaomiUtilities;
|
|
import org.telegram.tgnet.ConnectionsManager;
|
|
import org.telegram.tgnet.TLRPC;
|
|
import org.telegram.ui.Components.JoinCallAlert;
|
|
import org.telegram.ui.Components.voip.VoIPHelper;
|
|
import org.telegram.ui.LaunchActivity;
|
|
import org.telegram.ui.VoIPFeedbackActivity;
|
|
import org.webrtc.VideoFrame;
|
|
import org.webrtc.VideoSink;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.math.BigInteger;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.Set;
|
|
|
|
@SuppressLint("NewApi")
|
|
public class VoIPService extends VoIPBaseService {
|
|
|
|
public static final int CALL_MIN_LAYER = 65;
|
|
|
|
public static final int STATE_HANGING_UP = 10;
|
|
public static final int STATE_EXCHANGING_KEYS = 12;
|
|
public static final int STATE_WAITING = 13;
|
|
public static final int STATE_REQUESTING = 14;
|
|
public static final int STATE_WAITING_INCOMING = 15;
|
|
public static final int STATE_RINGING = 16;
|
|
public static final int STATE_BUSY = 17;
|
|
|
|
private TLRPC.User user;
|
|
private int callReqId;
|
|
|
|
private byte[] g_a;
|
|
private byte[] a_or_b;
|
|
private byte[] g_a_hash;
|
|
private byte[] authKey;
|
|
private long keyFingerprint;
|
|
private boolean forceRating;
|
|
|
|
public static TLRPC.PhoneCall callIShouldHavePutIntoIntent;
|
|
|
|
public static NativeInstance.AudioLevelsCallback audioLevelsCallback;
|
|
|
|
private boolean needSendDebugLog;
|
|
private boolean needRateCall;
|
|
private long lastTypingTimeSend;
|
|
|
|
private boolean endCallAfterRequest;
|
|
private ArrayList<TLRPC.PhoneCall> pendingUpdates = new ArrayList<>();
|
|
private Runnable delayedStartOutgoingCall;
|
|
|
|
private boolean startedRinging;
|
|
|
|
private int classGuid;
|
|
|
|
private long currentStreamRequestTimestamp;
|
|
|
|
public boolean isFrontFaceCamera() {
|
|
return isFrontFaceCamera;
|
|
}
|
|
|
|
public void setMicMute(boolean mute, boolean hold, boolean send) {
|
|
if (micMute == mute) {
|
|
return;
|
|
}
|
|
micMute = mute;
|
|
if (groupCall != null) {
|
|
if (!send) {
|
|
TLRPC.TL_groupCallParticipant self = groupCall.participants.get(getSelfId());
|
|
if (self != null && self.muted && !self.can_self_unmute) {
|
|
send = true;
|
|
}
|
|
}
|
|
if (send) {
|
|
editCallMember(UserConfig.getInstance(currentAccount).getCurrentUser(), mute, -1, null);
|
|
Utilities.globalQueue.postRunnable(updateNotificationRunnable = () -> {
|
|
if (updateNotificationRunnable == null) {
|
|
return;
|
|
}
|
|
updateNotificationRunnable = null;
|
|
showNotification(chat.title, getRoundAvatarBitmap(chat));
|
|
});
|
|
}
|
|
}
|
|
unmutedByHold = !micMute && hold;
|
|
if (tgVoip != null) {
|
|
tgVoip.setMuteMicrophone(mute);
|
|
}
|
|
for (StateListener l : stateListeners) {
|
|
l.onAudioSettingsChanged();
|
|
}
|
|
}
|
|
|
|
public boolean mutedByAdmin() {
|
|
ChatObject.Call call = groupCall;
|
|
if (call != null) {
|
|
int selfId = getSelfId();
|
|
TLRPC.TL_groupCallParticipant participant = call.participants.get(selfId);
|
|
if (participant != null && !participant.can_self_unmute && participant.muted && !ChatObject.canManageCalls(chat)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static class ProxyVideoSink implements VideoSink {
|
|
private VideoSink target;
|
|
private VideoSink background;
|
|
|
|
@Override
|
|
synchronized public void onFrame(VideoFrame frame) {
|
|
if (target == null) {
|
|
return;
|
|
}
|
|
|
|
target.onFrame(frame);
|
|
if (background != null) {
|
|
background.onFrame(frame);
|
|
}
|
|
}
|
|
|
|
synchronized public void setTarget(VideoSink target) {
|
|
this.target = target;
|
|
}
|
|
|
|
synchronized public void setBackground(VideoSink background) {
|
|
this.background = background;
|
|
}
|
|
|
|
synchronized public void swap() {
|
|
if (target != null && background != null) {
|
|
target = background;
|
|
background = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private ProxyVideoSink localSink;
|
|
private ProxyVideoSink remoteSink;
|
|
|
|
@Nullable
|
|
@Override
|
|
public IBinder onBind(Intent intent) {
|
|
return null;
|
|
}
|
|
|
|
@SuppressLint({"MissingPermission", "InlinedApi"})
|
|
@Override
|
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
if (sharedInstance != null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("Tried to start the VoIP service when it's already started");
|
|
}
|
|
return START_NOT_STICKY;
|
|
}
|
|
|
|
currentAccount = intent.getIntExtra("account", -1);
|
|
if (currentAccount == -1) {
|
|
throw new IllegalStateException("No account specified when starting VoIP service");
|
|
}
|
|
classGuid = ConnectionsManager.generateClassGuid();
|
|
int userID = intent.getIntExtra("user_id", 0);
|
|
int chatID = intent.getIntExtra("chat_id", 0);
|
|
createGroupCall = intent.getBooleanExtra("createGroupCall", false);
|
|
hasFewPeers = intent.getBooleanExtra("hasFewPeers", false);
|
|
joinHash = intent.getStringExtra("hash");
|
|
int peerChannelId = intent.getIntExtra("peerChannelId", 0);
|
|
int peerChatId = intent.getIntExtra("peerChatId", 0);
|
|
int peerUserId = intent.getIntExtra("peerUserId", 0);
|
|
if (peerChatId != 0) {
|
|
groupCallPeer = new TLRPC.TL_inputPeerChat();
|
|
groupCallPeer.chat_id = peerChatId;
|
|
groupCallPeer.access_hash = intent.getLongExtra("peerAccessHash", 0);
|
|
} else if (peerChannelId != 0) {
|
|
groupCallPeer = new TLRPC.TL_inputPeerChannel();
|
|
groupCallPeer.channel_id = peerChannelId;
|
|
groupCallPeer.access_hash = intent.getLongExtra("peerAccessHash", 0);
|
|
} else if (peerUserId != 0) {
|
|
groupCallPeer = new TLRPC.TL_inputPeerUser();
|
|
groupCallPeer.user_id = peerUserId;
|
|
groupCallPeer.access_hash = intent.getLongExtra("peerAccessHash", 0);
|
|
}
|
|
scheduleDate = intent.getIntExtra("scheduleDate", 0);
|
|
|
|
isOutgoing = intent.getBooleanExtra("is_outgoing", false);
|
|
videoCall = intent.getBooleanExtra("video_call", false);
|
|
isVideoAvailable = intent.getBooleanExtra("can_video_call", false);
|
|
notificationsDisabled = intent.getBooleanExtra("notifications_disabled", false);
|
|
if (userID != 0) {
|
|
user = MessagesController.getInstance(currentAccount).getUser(userID);
|
|
}
|
|
if (chatID != 0) {
|
|
chat = MessagesController.getInstance(currentAccount).getChat(chatID);
|
|
if (ChatObject.isChannel(chat)) {
|
|
MessagesController.getInstance(currentAccount).startShortPoll(chat, classGuid, false);
|
|
}
|
|
}
|
|
loadResources();
|
|
localSink = new ProxyVideoSink();
|
|
remoteSink = new ProxyVideoSink();
|
|
try {
|
|
AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
|
|
isHeadsetPlugged = am.isWiredHeadsetOn();
|
|
} catch (Exception e) {
|
|
FileLog.e(e);
|
|
}
|
|
if (chat != null && !createGroupCall) {
|
|
ChatObject.Call call = MessagesController.getInstance(currentAccount).getGroupCall(chat.id, false);
|
|
if (call == null) {
|
|
FileLog.w("VoIPService: trying to open group call without call " + chat.id);
|
|
stopSelf();
|
|
return START_NOT_STICKY;
|
|
}
|
|
}
|
|
|
|
if (videoCall) {
|
|
videoCapturer = NativeInstance.createVideoCapturer(localSink, isFrontFaceCamera);
|
|
videoState = Instance.VIDEO_STATE_ACTIVE;
|
|
if (!isBtHeadsetConnected && !isHeadsetPlugged) {
|
|
setAudioOutput(0);
|
|
}
|
|
}
|
|
|
|
if (user == null && chat == null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("VoIPService: user == null AND chat == null");
|
|
}
|
|
stopSelf();
|
|
return START_NOT_STICKY;
|
|
}
|
|
sharedInstance = this;
|
|
synchronized (sync) {
|
|
if (setModeRunnable != null) {
|
|
Utilities.globalQueue.cancelRunnable(setModeRunnable);
|
|
setModeRunnable = null;
|
|
}
|
|
}
|
|
|
|
if (isOutgoing) {
|
|
if (user != null) {
|
|
dispatchStateChanged(STATE_REQUESTING);
|
|
if (USE_CONNECTION_SERVICE) {
|
|
TelecomManager tm = (TelecomManager) getSystemService(TELECOM_SERVICE);
|
|
Bundle extras = new Bundle();
|
|
Bundle myExtras = new Bundle();
|
|
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, addAccountToTelecomManager());
|
|
myExtras.putInt("call_type", 1);
|
|
extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, myExtras);
|
|
ContactsController.getInstance(currentAccount).createOrUpdateConnectionServiceContact(user.id, user.first_name, user.last_name);
|
|
tm.placeCall(Uri.fromParts("tel", "+99084" + user.id, null), extras);
|
|
} else {
|
|
delayedStartOutgoingCall = () -> {
|
|
delayedStartOutgoingCall = null;
|
|
startOutgoingCall();
|
|
};
|
|
AndroidUtilities.runOnUIThread(delayedStartOutgoingCall, 2000);
|
|
}
|
|
} else {
|
|
micMute = true;
|
|
startGroupCall(0, null, false);
|
|
if (!isBtHeadsetConnected && !isHeadsetPlugged) {
|
|
setAudioOutput(0);
|
|
}
|
|
}
|
|
if (intent.getBooleanExtra("start_incall_activity", false)) {
|
|
Intent intent1 = new Intent(this, LaunchActivity.class).setAction(user != null ? "voip" : "voip_chat").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
if (chat != null) {
|
|
intent1.putExtra("currentAccount", currentAccount);
|
|
}
|
|
startActivity(intent1);
|
|
}
|
|
} else {
|
|
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.closeInCallActivity);
|
|
privateCall = callIShouldHavePutIntoIntent;
|
|
videoCall = privateCall != null && privateCall.video;
|
|
if (videoCall) {
|
|
isVideoAvailable = true;
|
|
}
|
|
if (videoCall && (Build.VERSION.SDK_INT < 23 || checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)) {
|
|
videoCapturer = NativeInstance.createVideoCapturer(localSink, isFrontFaceCamera);
|
|
videoState = Instance.VIDEO_STATE_ACTIVE;
|
|
} else {
|
|
videoState = Instance.VIDEO_STATE_INACTIVE;
|
|
}
|
|
if (videoCall && !isBtHeadsetConnected && !isHeadsetPlugged) {
|
|
setAudioOutput(0);
|
|
}
|
|
callIShouldHavePutIntoIntent = null;
|
|
if (USE_CONNECTION_SERVICE) {
|
|
acknowledgeCall(false);
|
|
showNotification();
|
|
} else {
|
|
acknowledgeCall(true);
|
|
}
|
|
}
|
|
initializeAccountRelatedThings();
|
|
AndroidUtilities.runOnUIThread(() -> NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.voipServiceCreated));
|
|
return START_NOT_STICKY;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
if (callIShouldHavePutIntoIntent != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
NotificationsController.checkOtherNotificationsChannel();
|
|
Notification.Builder bldr = new Notification.Builder(this, NotificationsController.OTHER_NOTIFICATIONS_CHANNEL)
|
|
.setContentTitle(LocaleController.getString("VoipOutgoingCall", R.string.VoipOutgoingCall))
|
|
.setShowWhen(false);
|
|
if (groupCall != null) {
|
|
bldr.setSmallIcon(isMicMute() ? R.drawable.voicechat_muted : R.drawable.voicechat_active);
|
|
} else {
|
|
bldr.setSmallIcon(R.drawable.notification);
|
|
}
|
|
startForeground(ID_ONGOING_CALL_NOTIFICATION, bldr.build());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void updateServerConfig() {
|
|
final SharedPreferences preferences = MessagesController.getMainSettings(currentAccount);
|
|
Instance.setGlobalServerConfig(preferences.getString("voip_server_config", "{}"));
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(new TLRPC.TL_phone_getCallConfig(), (response, error) -> {
|
|
if (error == null) {
|
|
String data = ((TLRPC.TL_dataJSON) response).data;
|
|
Instance.setGlobalServerConfig(data);
|
|
preferences.edit().putString("voip_server_config", data).commit();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
protected void onTgVoipPreStop() {
|
|
/*if(BuildConfig.DEBUG){
|
|
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(currentAccount).sendRequest(req, new RequestDelegate(){
|
|
@Override
|
|
public void run(TLObject response, TLRPC.TL_error error){
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("Sent debug logs, response=" + response);
|
|
}
|
|
}
|
|
});
|
|
}*/
|
|
}
|
|
|
|
@Override
|
|
protected void onTgVoipStop(Instance.FinalState finalState) {
|
|
if (user == null) {
|
|
return;
|
|
}
|
|
if (needRateCall || forceRating || finalState.isRatingSuggested) {
|
|
startRatingActivity();
|
|
needRateCall = false;
|
|
}
|
|
if (needSendDebugLog && finalState.debugLog != null) {
|
|
TLRPC.TL_phone_saveCallDebug req = new TLRPC.TL_phone_saveCallDebug();
|
|
req.debug = new TLRPC.TL_dataJSON();
|
|
req.debug.data = finalState.debugLog;
|
|
req.peer = new TLRPC.TL_inputPhoneCall();
|
|
req.peer.access_hash = privateCall.access_hash;
|
|
req.peer.id = privateCall.id;
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("Sent debug logs, response = " + response);
|
|
}
|
|
});
|
|
needSendDebugLog = false;
|
|
}
|
|
}
|
|
|
|
public static VoIPService getSharedInstance() {
|
|
return sharedInstance instanceof VoIPService ? ((VoIPService) sharedInstance) : null;
|
|
}
|
|
|
|
public TLRPC.User getUser() {
|
|
return user;
|
|
}
|
|
|
|
public TLRPC.Chat getChat() {
|
|
return chat;
|
|
}
|
|
|
|
public void setGroupCallHash(String hash) {
|
|
if (!currentGroupModeStreaming || TextUtils.isEmpty(hash) || hash.equals(joinHash)) {
|
|
return;
|
|
}
|
|
joinHash = hash;
|
|
createGroupInstance(false);
|
|
}
|
|
|
|
public int getCallerId() {
|
|
if (user != null) {
|
|
return user.id;
|
|
} else {
|
|
return -chat.id;
|
|
}
|
|
}
|
|
|
|
public void hangUp() {
|
|
hangUp(0, null);
|
|
}
|
|
|
|
public void hangUp(int discard) {
|
|
hangUp(discard, null);
|
|
}
|
|
|
|
public void hangUp(Runnable onDone) {
|
|
hangUp(0, onDone);
|
|
}
|
|
|
|
public void hangUp(int discard, Runnable onDone) {
|
|
declineIncomingCall(currentState == STATE_RINGING || (currentState == STATE_WAITING && isOutgoing) ? DISCARD_REASON_MISSED : DISCARD_REASON_HANGUP, onDone);
|
|
if (groupCall != null) {
|
|
if (discard == 2) {
|
|
return;
|
|
}
|
|
if (discard == 1) {
|
|
TLRPC.ChatFull chatFull = MessagesController.getInstance(currentAccount).getChatFull(chat.id);
|
|
if (chatFull != null) {
|
|
chatFull.flags &=~ 2097152;
|
|
chatFull.call = null;
|
|
NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.groupCallUpdated, chat.id, groupCall.call.id, false);
|
|
}
|
|
TLRPC.TL_phone_discardGroupCall req = new TLRPC.TL_phone_discardGroupCall();
|
|
req.call = groupCall.getInputGroupCall();
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
if (response instanceof TLRPC.TL_updates) {
|
|
TLRPC.TL_updates updates = (TLRPC.TL_updates) response;
|
|
MessagesController.getInstance(currentAccount).processUpdates(updates, false);
|
|
}
|
|
});
|
|
} else {
|
|
TLRPC.TL_phone_leaveGroupCall req = new TLRPC.TL_phone_leaveGroupCall();
|
|
req.call = groupCall.getInputGroupCall();
|
|
req.source = mySource;
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
if (response instanceof TLRPC.TL_updates) {
|
|
TLRPC.TL_updates updates = (TLRPC.TL_updates) response;
|
|
MessagesController.getInstance(currentAccount).processUpdates(updates, false);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void startOutgoingCall() {
|
|
if (USE_CONNECTION_SERVICE && systemCallConnection != null) {
|
|
systemCallConnection.setDialing();
|
|
}
|
|
configureDeviceForCall();
|
|
showNotification();
|
|
startConnectingSound();
|
|
dispatchStateChanged(STATE_REQUESTING);
|
|
AndroidUtilities.runOnUIThread(() -> NotificationCenter.getGlobalInstance().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;
|
|
final MessagesStorage messagesStorage = MessagesStorage.getInstance(currentAccount);
|
|
req.version = messagesStorage.getLastSecretVersion();
|
|
callReqId = ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
callReqId = 0;
|
|
if (endCallAfterRequest) {
|
|
callEnded();
|
|
return;
|
|
}
|
|
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.setSecretPBytes(res.p);
|
|
messagesStorage.setSecretG(res.g);
|
|
messagesStorage.setLastSecretVersion(res.version);
|
|
messagesStorage.saveSecretParams(messagesStorage.getLastSecretVersion(), messagesStorage.getSecretG(), messagesStorage.getSecretPBytes());
|
|
}
|
|
final byte[] salt1 = new byte[256];
|
|
for (int a = 0; a < 256; a++) {
|
|
salt1[a] = (byte) ((byte) (Utilities.random.nextDouble() * 256) ^ res.random[a]);
|
|
}
|
|
|
|
BigInteger i_g_a = BigInteger.valueOf(messagesStorage.getSecretG());
|
|
i_g_a = i_g_a.modPow(new BigInteger(1, salt1), new BigInteger(1, messagesStorage.getSecretPBytes()));
|
|
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.getInstance(currentAccount).getInputUser(user);
|
|
reqCall.protocol = new TLRPC.TL_phoneCallProtocol();
|
|
reqCall.video = videoCall;
|
|
reqCall.protocol.udp_p2p = true;
|
|
reqCall.protocol.udp_reflector = true;
|
|
reqCall.protocol.min_layer = CALL_MIN_LAYER;
|
|
reqCall.protocol.max_layer = Instance.getConnectionMaxLayer();
|
|
reqCall.protocol.library_versions.addAll(Instance.AVAILABLE_VERSIONS);
|
|
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(currentAccount).sendRequest(reqCall, (response12, error12) -> AndroidUtilities.runOnUIThread(() -> {
|
|
if (error12 == null) {
|
|
privateCall = ((TLRPC.TL_phone_phoneCall) response12).phone_call;
|
|
a_or_b = salt1;
|
|
dispatchStateChanged(STATE_WAITING);
|
|
if (endCallAfterRequest) {
|
|
hangUp();
|
|
return;
|
|
}
|
|
if (pendingUpdates.size() > 0 && privateCall != null) {
|
|
for (TLRPC.PhoneCall call : pendingUpdates) {
|
|
onCallUpdated(call);
|
|
}
|
|
pendingUpdates.clear();
|
|
}
|
|
timeoutRunnable = () -> {
|
|
timeoutRunnable = null;
|
|
TLRPC.TL_phone_discardCall req1 = new TLRPC.TL_phone_discardCall();
|
|
req1.peer = new TLRPC.TL_inputPhoneCall();
|
|
req1.peer.access_hash = privateCall.access_hash;
|
|
req1.peer.id = privateCall.id;
|
|
req1.reason = new TLRPC.TL_phoneCallDiscardReasonMissed();
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req1, (response1, error1) -> {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
if (error1 != null) {
|
|
FileLog.e("error on phone.discardCall: " + error1);
|
|
} else {
|
|
FileLog.d("phone.discardCall " + response1);
|
|
}
|
|
}
|
|
AndroidUtilities.runOnUIThread(VoIPService.this::callFailed);
|
|
}, ConnectionsManager.RequestFlagFailOnServerErrors);
|
|
};
|
|
AndroidUtilities.runOnUIThread(timeoutRunnable, MessagesController.getInstance(currentAccount).callReceiveTimeout);
|
|
} else {
|
|
if (error12.code == 400 && "PARTICIPANT_VERSION_OUTDATED".equals(error12.text)) {
|
|
callFailed(Instance.ERROR_PEER_OUTDATED);
|
|
} else if (error12.code == 403) {
|
|
callFailed(Instance.ERROR_PRIVACY);
|
|
} else if (error12.code == 406) {
|
|
callFailed(Instance.ERROR_LOCALIZED);
|
|
} else {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("Error on phone.requestCall: " + error12);
|
|
}
|
|
callFailed();
|
|
}
|
|
}
|
|
}), ConnectionsManager.RequestFlagFailOnServerErrors);
|
|
} else {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("Error on getDhConfig " + error);
|
|
}
|
|
callFailed();
|
|
}
|
|
}, ConnectionsManager.RequestFlagFailOnServerErrors);
|
|
}
|
|
|
|
private void acknowledgeCall(final boolean startRinging) {
|
|
if (privateCall instanceof TLRPC.TL_phoneCallDiscarded) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("Call " + privateCall.id + " was discarded before the service started, stopping");
|
|
}
|
|
stopSelf();
|
|
return;
|
|
}
|
|
if (Build.VERSION.SDK_INT >= 19 && XiaomiUtilities.isMIUI() && !XiaomiUtilities.isCustomPermissionGranted(XiaomiUtilities.OP_SHOW_WHEN_LOCKED)) {
|
|
if (((KeyguardManager) getSystemService(KEYGUARD_SERVICE)).inKeyguardRestrictedInputMode()) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("MIUI: no permission to show when locked but the screen is locked. ¯\\_(ツ)_/¯");
|
|
}
|
|
stopSelf();
|
|
return;
|
|
}
|
|
}
|
|
TLRPC.TL_phone_receivedCall req = new TLRPC.TL_phone_receivedCall();
|
|
req.peer = new TLRPC.TL_inputPhoneCall();
|
|
req.peer.id = privateCall.id;
|
|
req.peer.access_hash = privateCall.access_hash;
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> {
|
|
if (sharedInstance == null) {
|
|
return;
|
|
}
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("receivedCall response = " + response);
|
|
}
|
|
if (error != null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("error on receivedCall: " + error);
|
|
}
|
|
stopSelf();
|
|
} else {
|
|
if (USE_CONNECTION_SERVICE) {
|
|
ContactsController.getInstance(currentAccount).createOrUpdateConnectionServiceContact(user.id, user.first_name, user.last_name);
|
|
TelecomManager tm = (TelecomManager) getSystemService(TELECOM_SERVICE);
|
|
Bundle extras = new Bundle();
|
|
extras.putInt("call_type", 1);
|
|
tm.addNewIncomingCall(addAccountToTelecomManager(), extras);
|
|
}
|
|
if (startRinging) {
|
|
startRinging();
|
|
}
|
|
}
|
|
}), ConnectionsManager.RequestFlagFailOnServerErrors);
|
|
}
|
|
|
|
protected void startRinging() {
|
|
if (currentState == STATE_WAITING_INCOMING) {
|
|
return;
|
|
}
|
|
if (USE_CONNECTION_SERVICE && systemCallConnection != null) {
|
|
systemCallConnection.setRinging();
|
|
}
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("starting ringing for call " + privateCall.id);
|
|
}
|
|
dispatchStateChanged(STATE_WAITING_INCOMING);
|
|
if (!notificationsDisabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
showIncomingNotification(ContactsController.formatName(user.first_name, user.last_name), null, user, privateCall.video, 0);
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("Showing incoming call notification");
|
|
}
|
|
} else {
|
|
startRingtoneAndVibration(user.id);
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("Starting incall activity for incoming call");
|
|
}
|
|
try {
|
|
PendingIntent.getActivity(VoIPService.this, 12345, new Intent(VoIPService.this, LaunchActivity.class).setAction("voip"), 0).send();
|
|
} catch (Exception x) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("Error starting incall activity", x);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void startRingtoneAndVibration() {
|
|
if (!startedRinging) {
|
|
startRingtoneAndVibration(user.id);
|
|
startedRinging = true;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean isRinging() {
|
|
return currentState == STATE_WAITING_INCOMING;
|
|
}
|
|
|
|
public boolean isJoined() {
|
|
return currentState != STATE_WAIT_INIT && currentState != STATE_CREATING;
|
|
}
|
|
|
|
public void acceptIncomingCall() {
|
|
MessagesController.getInstance(currentAccount).ignoreSetOnline = false;
|
|
stopRinging();
|
|
showNotification();
|
|
configureDeviceForCall();
|
|
startConnectingSound();
|
|
dispatchStateChanged(STATE_EXCHANGING_KEYS);
|
|
AndroidUtilities.runOnUIThread(() -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.didStartedCall));
|
|
final MessagesStorage messagesStorage = MessagesStorage.getInstance(currentAccount);
|
|
TLRPC.TL_messages_getDhConfig req = new TLRPC.TL_messages_getDhConfig();
|
|
req.random_length = 256;
|
|
req.version = messagesStorage.getLastSecretVersion();
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, 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)) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("stopping VoIP service, bad prime");
|
|
}
|
|
callFailed();
|
|
return;
|
|
}
|
|
|
|
messagesStorage.setSecretPBytes(res.p);
|
|
messagesStorage.setSecretG(res.g);
|
|
messagesStorage.setLastSecretVersion(res.version);
|
|
MessagesStorage.getInstance(currentAccount).saveSecretParams(messagesStorage.getLastSecretVersion(), messagesStorage.getSecretG(), messagesStorage.getSecretPBytes());
|
|
}
|
|
byte[] salt = new byte[256];
|
|
for (int a = 0; a < 256; a++) {
|
|
salt[a] = (byte) ((byte) (Utilities.random.nextDouble() * 256) ^ res.random[a]);
|
|
}
|
|
if (privateCall == null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("call is null");
|
|
}
|
|
callFailed();
|
|
return;
|
|
}
|
|
a_or_b = salt;
|
|
BigInteger g_b = BigInteger.valueOf(messagesStorage.getSecretG());
|
|
BigInteger p = new BigInteger(1, messagesStorage.getSecretPBytes());
|
|
g_b = g_b.modPow(new BigInteger(1, salt), p);
|
|
g_a_hash = privateCall.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 req1 = new TLRPC.TL_phone_acceptCall();
|
|
req1.g_b = g_b_bytes;
|
|
req1.peer = new TLRPC.TL_inputPhoneCall();
|
|
req1.peer.id = privateCall.id;
|
|
req1.peer.access_hash = privateCall.access_hash;
|
|
req1.protocol = new TLRPC.TL_phoneCallProtocol();
|
|
req1.protocol.udp_p2p = req1.protocol.udp_reflector = true;
|
|
req1.protocol.min_layer = CALL_MIN_LAYER;
|
|
req1.protocol.max_layer = Instance.getConnectionMaxLayer();
|
|
req1.protocol.library_versions.addAll(Instance.AVAILABLE_VERSIONS);
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req1, (response1, error1) -> AndroidUtilities.runOnUIThread(() -> {
|
|
if (error1 == null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("accept call ok! " + response1);
|
|
}
|
|
privateCall = ((TLRPC.TL_phone_phoneCall) response1).phone_call;
|
|
if (privateCall instanceof TLRPC.TL_phoneCallDiscarded) {
|
|
onCallUpdated(privateCall);
|
|
}
|
|
} else {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("Error on phone.acceptCall: " + error1);
|
|
}
|
|
callFailed();
|
|
}
|
|
}), ConnectionsManager.RequestFlagFailOnServerErrors);
|
|
} else {
|
|
callFailed();
|
|
}
|
|
});
|
|
}
|
|
|
|
public void declineIncomingCall() {
|
|
declineIncomingCall(DISCARD_REASON_HANGUP, null);
|
|
}
|
|
|
|
public void requestVideoCall() {
|
|
if (tgVoip == null) {
|
|
return;
|
|
}
|
|
tgVoip.setupOutgoingVideo(localSink, isFrontFaceCamera);
|
|
}
|
|
|
|
public void switchCamera() {
|
|
if (tgVoip == null || switchingCamera) {
|
|
if (videoCapturer != 0 && !switchingCamera) {
|
|
NativeInstance.switchCameraCapturer(videoCapturer, !isFrontFaceCamera);
|
|
}
|
|
return;
|
|
}
|
|
switchingCamera = true;
|
|
tgVoip.switchCamera(!isFrontFaceCamera);
|
|
}
|
|
|
|
public void setVideoState(int videoState) {
|
|
if (tgVoip == null) {
|
|
if (videoCapturer != 0) {
|
|
this.videoState = videoState;
|
|
NativeInstance.setVideoStateCapturer(videoCapturer, videoState);
|
|
} else if (videoState == Instance.VIDEO_STATE_ACTIVE && currentState != STATE_BUSY && currentState != STATE_ENDED) {
|
|
videoCapturer = NativeInstance.createVideoCapturer(localSink, isFrontFaceCamera);
|
|
this.videoState = Instance.VIDEO_STATE_ACTIVE;
|
|
}
|
|
return;
|
|
}
|
|
this.videoState = videoState;
|
|
tgVoip.setVideoState(videoState);
|
|
checkIsNear();
|
|
}
|
|
|
|
public int getVideoState() {
|
|
return videoState;
|
|
}
|
|
|
|
public void setSinks(VideoSink local, VideoSink remote) {
|
|
localSink.setTarget(local);
|
|
remoteSink.setTarget(remote);
|
|
}
|
|
|
|
public void setBackgroundSinks(VideoSink local, VideoSink remote) {
|
|
localSink.setBackground(local);
|
|
remoteSink.setBackground(remote);
|
|
}
|
|
|
|
public void swapSinks() {
|
|
localSink.swap();
|
|
remoteSink.swap();
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
setSinks(null, null);
|
|
if (onDestroyRunnable != null) {
|
|
onDestroyRunnable.run();
|
|
}
|
|
if (ChatObject.isChannel(chat)) {
|
|
MessagesController.getInstance(currentAccount).startShortPoll(chat, classGuid, true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected Class<? extends Activity> getUIActivityClass() {
|
|
return LaunchActivity.class;
|
|
}
|
|
|
|
public boolean isHangingUp() {
|
|
return currentState == STATE_HANGING_UP;
|
|
}
|
|
|
|
public void declineIncomingCall(int reason, final Runnable onDone) {
|
|
stopRinging();
|
|
callDiscardReason = reason;
|
|
if (currentState == STATE_REQUESTING) {
|
|
if (delayedStartOutgoingCall != null) {
|
|
AndroidUtilities.cancelRunOnUIThread(delayedStartOutgoingCall);
|
|
callEnded();
|
|
} else {
|
|
dispatchStateChanged(STATE_HANGING_UP);
|
|
endCallAfterRequest = true;
|
|
AndroidUtilities.runOnUIThread(() -> {
|
|
if (currentState == STATE_HANGING_UP) {
|
|
callEnded();
|
|
}
|
|
}, 5000);
|
|
}
|
|
return;
|
|
}
|
|
if (currentState == STATE_HANGING_UP || currentState == STATE_ENDED) {
|
|
return;
|
|
}
|
|
dispatchStateChanged(STATE_HANGING_UP);
|
|
if (privateCall == null) {
|
|
onDestroyRunnable = onDone;
|
|
callEnded();
|
|
if (callReqId != 0) {
|
|
ConnectionsManager.getInstance(currentAccount).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 = privateCall.access_hash;
|
|
req.peer.id = privateCall.id;
|
|
req.duration = (int) (getCallDuration() / 1000);
|
|
req.connection_id = tgVoip != null ? tgVoip.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;
|
|
}
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
if (error != null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("error on phone.discardCall: " + error);
|
|
}
|
|
} else {
|
|
if (response instanceof TLRPC.TL_updates) {
|
|
TLRPC.TL_updates updates = (TLRPC.TL_updates) response;
|
|
MessagesController.getInstance(currentAccount).processUpdates(updates, false);
|
|
}
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("phone.discardCall " + response);
|
|
}
|
|
}
|
|
}, ConnectionsManager.RequestFlagFailOnServerErrors);
|
|
onDestroyRunnable = onDone;
|
|
callEnded();
|
|
}
|
|
|
|
public void onSignalingData(TLRPC.TL_updatePhoneCallSignalingData data) {
|
|
if (user == null || tgVoip == null || tgVoip.isGroup() || getCallID() != data.phone_call_id) {
|
|
return;
|
|
}
|
|
tgVoip.onSignalingDataReceive(data.data);
|
|
}
|
|
|
|
public int getSelfId() {
|
|
if (groupCallPeer == null) {
|
|
return UserConfig.getInstance(currentAccount).clientUserId;
|
|
}
|
|
if (groupCallPeer instanceof TLRPC.TL_inputPeerUser) {
|
|
return groupCallPeer.user_id;
|
|
} else if (groupCallPeer instanceof TLRPC.TL_inputPeerChannel) {
|
|
return -groupCallPeer.channel_id;
|
|
} else {
|
|
return -groupCallPeer.chat_id;
|
|
}
|
|
}
|
|
|
|
public void onGroupCallParticipantsUpdate(TLRPC.TL_updateGroupCallParticipants update) {
|
|
if (chat == null || groupCall == null || groupCall.call.id != update.call.id) {
|
|
return;
|
|
}
|
|
int selfId = getSelfId();
|
|
NativeInstance instance = tgVoip;
|
|
for (int a = 0, N = update.participants.size(); a < N; a++) {
|
|
TLRPC.TL_groupCallParticipant participant = update.participants.get(a);
|
|
if (participant.left) {
|
|
if (participant.source != 0) {
|
|
if (participant.source == mySource) {
|
|
int selfCount = 0;
|
|
for (int b = 0; b < N; b++) {
|
|
TLRPC.TL_groupCallParticipant p = update.participants.get(b);
|
|
if (p.self || p.source == mySource) {
|
|
selfCount++;
|
|
}
|
|
}
|
|
if (selfCount > 1) {
|
|
hangUp(2);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} else if (MessageObject.getPeerId(participant.peer) == selfId) {
|
|
if (participant.source != mySource && mySource != 0 && participant.source != 0) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("source mismatch my = " + mySource + " psrc = " + participant.source);
|
|
}
|
|
hangUp(2);
|
|
return;
|
|
} else if (ChatObject.isChannel(chat) && currentGroupModeStreaming && participant.can_self_unmute) {
|
|
switchingStream = true;
|
|
createGroupInstance(false);
|
|
}
|
|
if (participant.muted) {
|
|
setMicMute(true, false, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onGroupCallUpdated(TLRPC.GroupCall call) {
|
|
if (chat == null) {
|
|
return;
|
|
}
|
|
if (groupCall == null || groupCall.call.id != call.id) {
|
|
return;
|
|
}
|
|
if (groupCall.call instanceof TLRPC.TL_groupCallDiscarded) {
|
|
hangUp(2);
|
|
return;
|
|
}
|
|
boolean newModeStreaming = false;
|
|
JSONObject object = null;
|
|
if (call.params != null) {
|
|
try {
|
|
TLRPC.TL_dataJSON json = call.params;
|
|
object = new JSONObject(json.data);
|
|
newModeStreaming = object.optBoolean("stream");
|
|
} catch (Exception e) {
|
|
FileLog.e(e);
|
|
}
|
|
}
|
|
if ((currentState == STATE_WAIT_INIT || newModeStreaming != currentGroupModeStreaming) && call.params != null) {
|
|
if (playedConnectedSound && newModeStreaming != currentGroupModeStreaming) {
|
|
switchingStream = true;
|
|
}
|
|
currentGroupModeStreaming = newModeStreaming;
|
|
try {
|
|
if (newModeStreaming) {
|
|
tgVoip.prepareForStream();
|
|
} else {
|
|
object = object.getJSONObject("transport");
|
|
String ufrag = object.getString("ufrag");
|
|
String pwd = object.getString("pwd");
|
|
JSONArray array = object.getJSONArray("fingerprints");
|
|
Instance.Fingerprint[] fingerprints = new Instance.Fingerprint[array.length()];
|
|
for (int a = 0; a < fingerprints.length; a++) {
|
|
JSONObject item = array.getJSONObject(a);
|
|
fingerprints[a] = new Instance.Fingerprint(
|
|
item.getString("hash"),
|
|
item.getString("setup"),
|
|
item.getString("fingerprint")
|
|
);
|
|
}
|
|
array = object.getJSONArray("candidates");
|
|
Instance.Candidate[] candidates = new Instance.Candidate[array.length()];
|
|
for (int a = 0; a < candidates.length; a++) {
|
|
JSONObject item = array.getJSONObject(a);
|
|
candidates[a] = new Instance.Candidate(
|
|
item.optString("port", ""),
|
|
item.optString("protocol", ""),
|
|
item.optString("network", ""),
|
|
item.optString("generation", ""),
|
|
item.optString("id", ""),
|
|
item.optString("component", ""),
|
|
item.optString("foundation", ""),
|
|
item.optString("priority", ""),
|
|
item.optString("ip", ""),
|
|
item.optString("type", ""),
|
|
item.optString("tcpType", ""),
|
|
item.optString("relAddr", ""),
|
|
item.optString("relPort", "")
|
|
);
|
|
}
|
|
tgVoip.setJoinResponsePayload(ufrag, pwd, fingerprints, candidates);
|
|
}
|
|
dispatchStateChanged(STATE_WAIT_INIT_ACK);
|
|
} catch (Exception e) {
|
|
FileLog.e(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onCallUpdated(TLRPC.PhoneCall phoneCall) {
|
|
if (user == null) {
|
|
return;
|
|
}
|
|
if (privateCall == null) {
|
|
pendingUpdates.add(phoneCall);
|
|
return;
|
|
}
|
|
if (phoneCall == null) {
|
|
return;
|
|
}
|
|
if (phoneCall.id != privateCall.id) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("onCallUpdated called with wrong call id (got " + phoneCall.id + ", expected " + this.privateCall.id + ")");
|
|
}
|
|
return;
|
|
}
|
|
if (phoneCall.access_hash == 0) {
|
|
phoneCall.access_hash = this.privateCall.access_hash;
|
|
}
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("Call updated: " + phoneCall);
|
|
}
|
|
privateCall = phoneCall;
|
|
if (phoneCall instanceof TLRPC.TL_phoneCallDiscarded) {
|
|
needSendDebugLog = phoneCall.need_debug;
|
|
needRateCall = phoneCall.need_rating;
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("call discarded, stopping service");
|
|
}
|
|
if (phoneCall.reason instanceof TLRPC.TL_phoneCallDiscardReasonBusy) {
|
|
dispatchStateChanged(STATE_BUSY);
|
|
playingSound = true;
|
|
Utilities.globalQueue.postRunnable(() -> soundPool.play(spBusyId, 1, 1, 0, -1, 1));
|
|
AndroidUtilities.runOnUIThread(afterSoundRunnable, 1500);
|
|
endConnectionServiceCall(1500);
|
|
stopSelf();
|
|
} else {
|
|
callEnded();
|
|
}
|
|
} else if (phoneCall instanceof TLRPC.TL_phoneCall && authKey == null) {
|
|
if (phoneCall.g_a_or_b == null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("stopping VoIP service, Ga == null");
|
|
}
|
|
callFailed();
|
|
return;
|
|
}
|
|
if (!Arrays.equals(g_a_hash, Utilities.computeSHA256(phoneCall.g_a_or_b, 0, phoneCall.g_a_or_b.length))) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("stopping VoIP service, Ga hash doesn't match");
|
|
}
|
|
callFailed();
|
|
return;
|
|
}
|
|
g_a = phoneCall.g_a_or_b;
|
|
BigInteger g_a = new BigInteger(1, phoneCall.g_a_or_b);
|
|
BigInteger p = new BigInteger(1, MessagesStorage.getInstance(currentAccount).getSecretPBytes());
|
|
|
|
if (!Utilities.isGoodGaAndGb(g_a, p)) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
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++) {
|
|
correctedAuth[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 != phoneCall.key_fingerprint) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.w("key fingerprints don't match");
|
|
}
|
|
callFailed();
|
|
return;
|
|
}
|
|
|
|
initiateActualEncryptedCall();
|
|
} else if (phoneCall instanceof TLRPC.TL_phoneCallAccepted && authKey == null) {
|
|
processAcceptedCall();
|
|
} else {
|
|
if (currentState == STATE_WAITING && phoneCall.receive_date != 0) {
|
|
dispatchStateChanged(STATE_RINGING);
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("!!!!!! CALL RECEIVED");
|
|
}
|
|
if (connectingSoundRunnable != null) {
|
|
AndroidUtilities.cancelRunOnUIThread(connectingSoundRunnable);
|
|
connectingSoundRunnable = null;
|
|
}
|
|
Utilities.globalQueue.postRunnable(() -> {
|
|
if (spPlayId != 0) {
|
|
soundPool.stop(spPlayId);
|
|
}
|
|
spPlayId = soundPool.play(spRingbackID, 1, 1, 0, -1, 1);
|
|
});
|
|
if (timeoutRunnable != null) {
|
|
AndroidUtilities.cancelRunOnUIThread(timeoutRunnable);
|
|
timeoutRunnable = null;
|
|
}
|
|
timeoutRunnable = () -> {
|
|
timeoutRunnable = null;
|
|
declineIncomingCall(DISCARD_REASON_MISSED, null);
|
|
};
|
|
AndroidUtilities.runOnUIThread(timeoutRunnable, MessagesController.getInstance(currentAccount).callRingTimeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void startRatingActivity() {
|
|
try {
|
|
PendingIntent.getActivity(VoIPService.this, 0, new Intent(VoIPService.this, VoIPFeedbackActivity.class)
|
|
.putExtra("call_id", privateCall.id)
|
|
.putExtra("call_access_hash", privateCall.access_hash)
|
|
.putExtra("call_video", privateCall.video)
|
|
.putExtra("account", currentAccount)
|
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP), 0).send();
|
|
} catch (Exception x) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("Error starting incall activity", x);
|
|
}
|
|
}
|
|
}
|
|
|
|
public byte[] getEncryptionKey() {
|
|
return authKey;
|
|
}
|
|
|
|
private void processAcceptedCall() {
|
|
dispatchStateChanged(STATE_EXCHANGING_KEYS);
|
|
BigInteger p = new BigInteger(1, MessagesStorage.getInstance(currentAccount).getSecretPBytes());
|
|
BigInteger i_authKey = new BigInteger(1, privateCall.g_b);
|
|
|
|
if (!Utilities.isGoodGaAndGb(i_authKey, p)) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
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++) {
|
|
correctedAuth[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 = privateCall.id;
|
|
req.peer.access_hash = privateCall.access_hash;
|
|
req.protocol = new TLRPC.TL_phoneCallProtocol();
|
|
req.protocol.max_layer = Instance.getConnectionMaxLayer();
|
|
req.protocol.min_layer = CALL_MIN_LAYER;
|
|
req.protocol.udp_p2p = req.protocol.udp_reflector = true;
|
|
req.protocol.library_versions.addAll(Instance.AVAILABLE_VERSIONS);
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> {
|
|
if (error != null) {
|
|
callFailed();
|
|
} else {
|
|
privateCall = ((TLRPC.TL_phone_phoneCall) response).phone_call;
|
|
initiateActualEncryptedCall();
|
|
}
|
|
}));
|
|
}
|
|
|
|
private int convertDataSavingMode(int mode) {
|
|
if (mode != Instance.DATA_SAVING_ROAMING) {
|
|
return mode;
|
|
}
|
|
return ApplicationLoader.isRoaming() ? Instance.DATA_SAVING_MOBILE : Instance.DATA_SAVING_NEVER;
|
|
}
|
|
|
|
public void migrateToChat(TLRPC.Chat newChat) {
|
|
chat = newChat;
|
|
}
|
|
|
|
public void setGroupCallPeer(TLRPC.InputPeer peer) {
|
|
if (groupCall == null) {
|
|
return;
|
|
}
|
|
groupCallPeer = peer;
|
|
groupCall.setSelfPeer(groupCallPeer);
|
|
createGroupInstance(true);
|
|
}
|
|
|
|
private void startGroupCall(int ssrc, String json, boolean create) {
|
|
if (sharedInstance != this) {
|
|
return;
|
|
}
|
|
if (createGroupCall) {
|
|
groupCall = new ChatObject.Call();
|
|
groupCall.call = new TLRPC.TL_groupCall();
|
|
groupCall.call.participants_count = 0;
|
|
groupCall.call.version = 1;
|
|
groupCall.call.can_change_join_muted = true;
|
|
groupCall.chatId = chat.id;
|
|
groupCall.currentAccount = AccountInstance.getInstance(currentAccount);
|
|
groupCall.setSelfPeer(groupCallPeer);
|
|
|
|
dispatchStateChanged(STATE_CREATING);
|
|
TLRPC.TL_phone_createGroupCall req = new TLRPC.TL_phone_createGroupCall();
|
|
req.peer = MessagesController.getInputPeer(chat);
|
|
req.random_id = Utilities.random.nextInt();
|
|
if (scheduleDate != 0) {
|
|
req.schedule_date = scheduleDate;
|
|
req.flags |= 2;
|
|
}
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
if (response != null) {
|
|
TLRPC.Updates updates = (TLRPC.Updates) response;
|
|
for (int a = 0; a < updates.updates.size(); a++) {
|
|
TLRPC.Update update = updates.updates.get(a);
|
|
if (update instanceof TLRPC.TL_updateGroupCall) {
|
|
TLRPC.TL_updateGroupCall updateGroupCall = (TLRPC.TL_updateGroupCall) update;
|
|
AndroidUtilities.runOnUIThread(() -> {
|
|
if (sharedInstance == null) {
|
|
return;
|
|
}
|
|
groupCall.call.access_hash = updateGroupCall.call.access_hash;
|
|
groupCall.call.id = updateGroupCall.call.id;
|
|
MessagesController.getInstance(currentAccount).putGroupCall(groupCall.chatId, groupCall);
|
|
startGroupCall(0, null, false);
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
MessagesController.getInstance(currentAccount).processUpdates(updates, false);
|
|
} else {
|
|
AndroidUtilities.runOnUIThread(() -> {
|
|
NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.needShowAlert, 6, error.text);
|
|
hangUp(0);
|
|
});
|
|
}
|
|
}, ConnectionsManager.RequestFlagFailOnServerErrors);
|
|
createGroupCall = false;
|
|
return;
|
|
}
|
|
|
|
if (json == null) {
|
|
if (groupCall == null) {
|
|
groupCall = MessagesController.getInstance(currentAccount).getGroupCall(chat.id, false);
|
|
if (groupCall != null) {
|
|
groupCall.setSelfPeer(groupCallPeer);
|
|
}
|
|
}
|
|
configureDeviceForCall();
|
|
showNotification();
|
|
AndroidUtilities.runOnUIThread(() -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.didStartedCall));
|
|
createGroupInstance(false);
|
|
} else {
|
|
if (getSharedInstance() == null || groupCall == null) {
|
|
return;
|
|
}
|
|
dispatchStateChanged(STATE_WAIT_INIT);
|
|
mySource = ssrc;
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("initital source = " + mySource);
|
|
}
|
|
myJson = json;
|
|
TLRPC.TL_phone_joinGroupCall req = new TLRPC.TL_phone_joinGroupCall();
|
|
req.muted = true;
|
|
req.call = groupCall.getInputGroupCall();
|
|
req.params = new TLRPC.TL_dataJSON();
|
|
req.params.data = json;
|
|
if (!TextUtils.isEmpty(joinHash)) {
|
|
req.invite_hash = joinHash;
|
|
req.flags |= 2;
|
|
}
|
|
if (groupCallPeer != null) {
|
|
req.join_as = groupCallPeer;
|
|
} else {
|
|
req.join_as = new TLRPC.TL_inputPeerUser();
|
|
req.join_as.user_id = AccountInstance.getInstance(currentAccount).getUserConfig().getClientUserId();
|
|
}
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
if (response != null) {
|
|
TLRPC.Updates updates = (TLRPC.Updates) response;
|
|
int selfId = getSelfId();
|
|
for (int a = 0, N = updates.updates.size(); a < N; a++) {
|
|
TLRPC.Update update = updates.updates.get(a);
|
|
if (update instanceof TLRPC.TL_updateGroupCallParticipants) {
|
|
TLRPC.TL_updateGroupCallParticipants updateGroupCallParticipants = (TLRPC.TL_updateGroupCallParticipants) update;
|
|
for (int b = 0, N2 = updateGroupCallParticipants.participants.size(); b < N2; b++) {
|
|
TLRPC.TL_groupCallParticipant participant = updateGroupCallParticipants.participants.get(b);
|
|
if (MessageObject.getPeerId(participant.peer) == selfId) {
|
|
mySource = participant.source;
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("join source = " + mySource);
|
|
}
|
|
a = N;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MessagesController.getInstance(currentAccount).processUpdates(updates, false);
|
|
AndroidUtilities.runOnUIThread(() -> groupCall.loadMembers(create));
|
|
startGroupCheckShortpoll();
|
|
} else {
|
|
AndroidUtilities.runOnUIThread(() -> {
|
|
if ("JOIN_AS_PEER_INVALID".equals(error.text)) {
|
|
TLRPC.ChatFull chatFull = MessagesController.getInstance(currentAccount).getChatFull(chat.id);
|
|
if (chatFull != null) {
|
|
if (chatFull instanceof TLRPC.TL_chatFull) {
|
|
chatFull.flags &=~ 32768;
|
|
} else {
|
|
chatFull.flags &=~ 67108864;
|
|
}
|
|
chatFull.groupcall_default_join_as = null;
|
|
JoinCallAlert.resetCache();
|
|
}
|
|
hangUp(2);
|
|
} else if ("GROUPCALL_SSRC_DUPLICATE_MUCH".equals(error.text)) {
|
|
createGroupInstance(false);
|
|
} else {
|
|
if ("GROUPCALL_INVALID".equals(error.text)) {
|
|
MessagesController.getInstance(currentAccount).loadFullChat(chat.id, 0, true);
|
|
}
|
|
NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.needShowAlert, 6, error.text);
|
|
hangUp(0);
|
|
}
|
|
});
|
|
}
|
|
}, BuildVars.DEBUG_PRIVATE_VERSION ? ConnectionsManager.RequestFlagFailOnServerErrors : 0);
|
|
}
|
|
}
|
|
|
|
private Runnable shortPollRunnable;
|
|
private int checkRequestId;
|
|
|
|
private void startGroupCheckShortpoll() {
|
|
if (shortPollRunnable != null || sharedInstance == null || groupCall == null || mySource == 0) {
|
|
return;
|
|
}
|
|
AndroidUtilities.runOnUIThread(shortPollRunnable = () -> {
|
|
if (shortPollRunnable == null || sharedInstance == null || groupCall == null || mySource == 0) {
|
|
return;
|
|
}
|
|
TLRPC.TL_phone_checkGroupCall req = new TLRPC.TL_phone_checkGroupCall();
|
|
req.call = groupCall.getInputGroupCall();
|
|
req.source = mySource;
|
|
checkRequestId = ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> {
|
|
if (shortPollRunnable == null || sharedInstance == null || groupCall == null) {
|
|
return;
|
|
}
|
|
shortPollRunnable = null;
|
|
checkRequestId = 0;
|
|
if (response instanceof TLRPC.TL_boolFalse || error != null && error.code == 400) {
|
|
createGroupInstance(false);
|
|
} else {
|
|
startGroupCheckShortpoll();
|
|
}
|
|
}));
|
|
}, 4000);
|
|
}
|
|
|
|
private void cancelGroupCheckShortPoll() {
|
|
if (checkRequestId != 0) {
|
|
ConnectionsManager.getInstance(currentAccount).cancelRequest(checkRequestId, false);
|
|
checkRequestId = 0;
|
|
}
|
|
if (shortPollRunnable != null) {
|
|
AndroidUtilities.cancelRunOnUIThread(shortPollRunnable);
|
|
shortPollRunnable = null;
|
|
}
|
|
}
|
|
|
|
private void broadcastUnknownParticipants(int[] unknown, ArrayList<Integer> uids) {
|
|
if (groupCall == null || tgVoip == null) {
|
|
return;
|
|
}
|
|
int selfId = getSelfId();
|
|
ArrayList<TLRPC.TL_groupCallParticipant> participants = null;
|
|
for (int a = 0, N = unknown != null ? unknown.length : uids.size(); a < N; a++) {
|
|
int ssrc;
|
|
if (unknown != null) {
|
|
ssrc = unknown[a];
|
|
} else {
|
|
ssrc = uids.get(a);
|
|
}
|
|
TLRPC.TL_groupCallParticipant p = groupCall.participantsBySources.get(ssrc);
|
|
if (p == null || MessageObject.getPeerId(p.peer) == selfId || p.source == 0) {
|
|
continue;
|
|
}
|
|
//if (p.params != null && !TextUtils.isEmpty(p.params.data)) {
|
|
if (participants == null) {
|
|
participants = new ArrayList<>();
|
|
}
|
|
participants.add(p);
|
|
//}
|
|
}
|
|
if (participants != null) {
|
|
String[] jsonArray = new String[participants.size()];
|
|
int[] ssrcs = new int[participants.size()];
|
|
for (int a = 0, N = participants.size(); a < N; a++) {
|
|
TLRPC.TL_groupCallParticipant p = participants.get(a);
|
|
jsonArray[a] = null;//p.params.data;
|
|
ssrcs[a] = p.source;
|
|
}
|
|
tgVoip.addParticipants(ssrcs, jsonArray);
|
|
|
|
for (int a = 0, N = participants.size(); a < N; a++) {
|
|
TLRPC.TL_groupCallParticipant p = participants.get(a);
|
|
if (p.muted_by_you) {
|
|
tgVoip.setVolume(p.source, 0);
|
|
} else {
|
|
tgVoip.setVolume(p.source, ChatObject.getParticipantVolume(p) / 10000.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void createGroupInstance(boolean switchingAccount) {
|
|
cancelGroupCheckShortPoll();
|
|
wasConnected = false;
|
|
if (switchingAccount) {
|
|
mySource = 0;
|
|
tgVoip.stopGroup();
|
|
tgVoip = null;
|
|
}
|
|
if (tgVoip == null) {
|
|
final String logFilePath = BuildVars.DEBUG_VERSION ? VoIPHelper.getLogFilePath("voip" + groupCall.call.id) : VoIPHelper.getLogFilePath(groupCall.call.id, false);
|
|
tgVoip = NativeInstance.makeGroup(logFilePath, (ssrc, json) -> startGroupCall(ssrc, json, true), (uids, levels, voice) -> {
|
|
if (sharedInstance == null || groupCall == null) {
|
|
return;
|
|
}
|
|
groupCall.processVoiceLevelsUpdate(uids, levels, voice);
|
|
float maxAmplitude = 0;
|
|
boolean hasOther = false;
|
|
for (int a = 0; a < uids.length; a++) {
|
|
if (uids[a] == 0) {
|
|
if (lastTypingTimeSend < SystemClock.uptimeMillis() - 5000 && levels[a] > 0.1f && voice[a]) {
|
|
lastTypingTimeSend = SystemClock.uptimeMillis();
|
|
TLRPC.TL_messages_setTyping req = new TLRPC.TL_messages_setTyping();
|
|
req.action = new TLRPC.TL_speakingInGroupCallAction();
|
|
req.peer = MessagesController.getInputPeer(chat);
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
|
|
});
|
|
}
|
|
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.webRtcMicAmplitudeEvent, levels[a]);
|
|
continue;
|
|
}
|
|
hasOther = true;
|
|
maxAmplitude = Math.max(maxAmplitude, levels[a]);
|
|
}
|
|
if (hasOther) {
|
|
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.webRtcSpeakerAmplitudeEvent, maxAmplitude);
|
|
if (audioLevelsCallback != null) {
|
|
audioLevelsCallback.run(uids, levels, voice);
|
|
}
|
|
}
|
|
}, ssrcs -> {
|
|
|
|
}, unknown -> {
|
|
if (sharedInstance == null || groupCall == null) {
|
|
return;
|
|
}
|
|
groupCall.processUnknownVideoParticipants(unknown, (ssrcs) -> {
|
|
if (sharedInstance == null || groupCall == null) {
|
|
return;
|
|
}
|
|
broadcastUnknownParticipants(null, ssrcs);
|
|
});
|
|
broadcastUnknownParticipants(unknown, null);
|
|
}, (timestamp, duration) -> {
|
|
TLRPC.TL_upload_getFile req = new TLRPC.TL_upload_getFile();
|
|
req.limit = 128 * 1024;
|
|
TLRPC.TL_inputGroupCallStream inputGroupCallStream = new TLRPC.TL_inputGroupCallStream();
|
|
inputGroupCallStream.call = groupCall.getInputGroupCall();
|
|
inputGroupCallStream.time_ms = timestamp;
|
|
if (duration == 500) {
|
|
inputGroupCallStream.scale = 1;
|
|
}
|
|
req.location = inputGroupCallStream;
|
|
currentStreamRequestTimestamp = timestamp;
|
|
currentStreamRequestId = AccountInstance.getInstance(currentAccount).getConnectionsManager().sendRequest(req, (response, error, responseTime) -> {
|
|
if (tgVoip == null) {
|
|
return;
|
|
}
|
|
if (response != null) {
|
|
TLRPC.TL_upload_file res = (TLRPC.TL_upload_file) response;
|
|
tgVoip.onStreamPartAvailable(timestamp, res.bytes.buffer, res.bytes.limit(), responseTime);
|
|
} else {
|
|
if ("GROUPCALL_JOIN_MISSING".equals(error.text)) {
|
|
AndroidUtilities.runOnUIThread(() -> createGroupInstance(false));
|
|
} else {
|
|
int status;
|
|
if ("TIME_TOO_BIG".equals(error.text) || error.text.startsWith("FLOOD_WAIT")) {
|
|
status = 0;
|
|
} else {
|
|
status = -1;
|
|
}
|
|
tgVoip.onStreamPartAvailable(timestamp, null, status, responseTime);
|
|
}
|
|
}
|
|
}, ConnectionsManager.RequestFlagFailOnServerErrors, ConnectionsManager.ConnectionTypeDownload, groupCall.call.stream_dc_id);
|
|
}, (timestamp, duration) -> {
|
|
if (currentStreamRequestTimestamp == timestamp) {
|
|
AccountInstance.getInstance(currentAccount).getConnectionsManager().cancelRequest(currentStreamRequestId, true);
|
|
currentStreamRequestId = 0;
|
|
}
|
|
});
|
|
tgVoip.setOnStateUpdatedListener(this::updateConnectionState);
|
|
}
|
|
tgVoip.resetGroupInstance(false);
|
|
dispatchStateChanged(STATE_WAIT_INIT);
|
|
}
|
|
|
|
private void updateConnectionState(int state, boolean inTransition) {
|
|
dispatchStateChanged(state == 1 || switchingStream ? STATE_ESTABLISHED : STATE_RECONNECTING);
|
|
if (switchingStream && (state == 0 || state == 1 && inTransition)) {
|
|
AndroidUtilities.runOnUIThread(switchingStreamTimeoutRunnable = () -> {
|
|
if (switchingStreamTimeoutRunnable == null) {
|
|
return;
|
|
}
|
|
switchingStream = false;
|
|
updateConnectionState(0, true);
|
|
switchingStreamTimeoutRunnable = null;
|
|
}, 3000);
|
|
}
|
|
if (state == 0) {
|
|
startGroupCheckShortpoll();
|
|
if (playedConnectedSound && spPlayId == 0 && !switchingStream) {
|
|
Utilities.globalQueue.postRunnable(() -> {
|
|
if (spPlayId != 0) {
|
|
soundPool.stop(spPlayId);
|
|
}
|
|
spPlayId = soundPool.play(spVoiceChatConnecting, 1.0f, 1.0f, 0, -1, 1);
|
|
});
|
|
}
|
|
} else {
|
|
cancelGroupCheckShortPoll();
|
|
if (!inTransition) {
|
|
switchingStream = false;
|
|
}
|
|
if (switchingStreamTimeoutRunnable != null) {
|
|
AndroidUtilities.cancelRunOnUIThread(switchingStreamTimeoutRunnable);
|
|
switchingStreamTimeoutRunnable = null;
|
|
}
|
|
if (playedConnectedSound) {
|
|
Utilities.globalQueue.postRunnable(() -> {
|
|
if (spPlayId != 0) {
|
|
soundPool.stop(spPlayId);
|
|
spPlayId = 0;
|
|
}
|
|
});
|
|
if (connectingSoundRunnable != null) {
|
|
AndroidUtilities.cancelRunOnUIThread(connectingSoundRunnable);
|
|
connectingSoundRunnable = null;
|
|
}
|
|
} else {
|
|
playConnectedSound();
|
|
}
|
|
if (!wasConnected) {
|
|
wasConnected = true;
|
|
NativeInstance instance = tgVoip;
|
|
if (instance != null) {
|
|
if (!micMute) {
|
|
tgVoip.setMuteMicrophone(false);
|
|
}
|
|
}
|
|
setParticipantsVolume();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setParticipantsVolume() {
|
|
NativeInstance instance = tgVoip;
|
|
if (instance != null) {
|
|
for (int a = 0, N = groupCall.participants.size(); a < N; a++) {
|
|
TLRPC.TL_groupCallParticipant participant = groupCall.participants.valueAt(a);
|
|
if (participant.self || participant.source == 0 || !participant.can_self_unmute && participant.muted) {
|
|
continue;
|
|
}
|
|
if (participant.muted_by_you) {
|
|
instance.setVolume(participant.source, 0);
|
|
} else {
|
|
instance.setVolume(participant.source, ChatObject.getParticipantVolume(participant) / 10000.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setParticipantVolume(int ssrc, int volume) {
|
|
tgVoip.setVolume(ssrc, volume / 10000.0);
|
|
}
|
|
|
|
public boolean isSwitchingStream() {
|
|
return switchingStream;
|
|
}
|
|
|
|
private void initiateActualEncryptedCall() {
|
|
if (timeoutRunnable != null) {
|
|
AndroidUtilities.cancelRunOnUIThread(timeoutRunnable);
|
|
timeoutRunnable = null;
|
|
}
|
|
try {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("InitCall: keyID=" + keyFingerprint);
|
|
}
|
|
SharedPreferences nprefs = MessagesController.getNotificationsSettings(currentAccount);
|
|
Set<String> set = nprefs.getStringSet("calls_access_hashes", null);
|
|
HashSet<String> hashes;
|
|
if (set != null) {
|
|
hashes = new HashSet<>(set);
|
|
} else {
|
|
hashes = new HashSet<>();
|
|
}
|
|
hashes.add(privateCall.id + " " + privateCall.access_hash + " " + System.currentTimeMillis());
|
|
while (hashes.size() > 20) {
|
|
String oldest = null;
|
|
long oldestTime = Long.MAX_VALUE;
|
|
Iterator<String> itr = hashes.iterator();
|
|
while (itr.hasNext()) {
|
|
String item = itr.next();
|
|
String[] s = item.split(" ");
|
|
if (s.length < 2) {
|
|
itr.remove();
|
|
} else {
|
|
try {
|
|
long t = Long.parseLong(s[2]);
|
|
if (t < oldestTime) {
|
|
oldestTime = t;
|
|
oldest = item;
|
|
}
|
|
} catch (Exception x) {
|
|
itr.remove();
|
|
}
|
|
}
|
|
}
|
|
if (oldest != null) {
|
|
hashes.remove(oldest);
|
|
}
|
|
}
|
|
nprefs.edit().putStringSet("calls_access_hashes", hashes).commit();
|
|
|
|
boolean sysAecAvailable = false, sysNsAvailable = false;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
|
try {
|
|
sysAecAvailable = AcousticEchoCanceler.isAvailable();
|
|
} catch (Exception ignored) {
|
|
}
|
|
try {
|
|
sysNsAvailable = NoiseSuppressor.isAvailable();
|
|
} catch (Exception ignored) {
|
|
}
|
|
}
|
|
|
|
final SharedPreferences preferences = MessagesController.getGlobalMainSettings();
|
|
|
|
// config
|
|
final MessagesController messagesController = MessagesController.getInstance(currentAccount);
|
|
final double initializationTimeout = messagesController.callConnectTimeout / 1000.0;
|
|
final double receiveTimeout = messagesController.callPacketTimeout / 1000.0;
|
|
final int voipDataSaving = convertDataSavingMode(preferences.getInt("VoipDataSaving", VoIPHelper.getDataSavingDefault()));
|
|
final Instance.ServerConfig serverConfig = Instance.getGlobalServerConfig();
|
|
final boolean enableAec = !(sysAecAvailable && serverConfig.useSystemAec);
|
|
final boolean enableNs = !(sysNsAvailable && serverConfig.useSystemNs);
|
|
final String logFilePath = BuildVars.DEBUG_VERSION ? VoIPHelper.getLogFilePath("voip" + privateCall.id) : VoIPHelper.getLogFilePath(privateCall.id, false);
|
|
final String statisLogFilePath = "";
|
|
final Instance.Config config = new Instance.Config(initializationTimeout, receiveTimeout, voipDataSaving, privateCall.p2p_allowed, enableAec, enableNs, true, false, serverConfig.enableStunMarking, logFilePath, statisLogFilePath, privateCall.protocol.max_layer);
|
|
|
|
// persistent state
|
|
final String persistentStateFilePath = new File(ApplicationLoader.applicationContext.getFilesDir(), "voip_persistent_state.json").getAbsolutePath();
|
|
|
|
// endpoints
|
|
final boolean forceTcp = preferences.getBoolean("dbg_force_tcp_in_calls", false);
|
|
final int endpointType = forceTcp ? Instance.ENDPOINT_TYPE_TCP_RELAY : Instance.ENDPOINT_TYPE_UDP_RELAY;
|
|
final Instance.Endpoint[] endpoints = new Instance.Endpoint[privateCall.connections.size()];
|
|
for (int i = 0; i < endpoints.length; i++) {
|
|
final TLRPC.PhoneConnection connection = privateCall.connections.get(i);
|
|
endpoints[i] = new Instance.Endpoint(connection instanceof TLRPC.TL_phoneConnectionWebrtc, connection.id, connection.ip, connection.ipv6, connection.port, endpointType, connection.peer_tag, connection.turn, connection.stun, connection.username, connection.password);
|
|
}
|
|
if (forceTcp) {
|
|
AndroidUtilities.runOnUIThread(() -> Toast.makeText(VoIPService.this, "This call uses TCP which will degrade its quality.", Toast.LENGTH_SHORT).show());
|
|
}
|
|
|
|
// proxy
|
|
Instance.Proxy proxy = null;
|
|
if (preferences.getBoolean("proxy_enabled", false) && preferences.getBoolean("proxy_enabled_calls", false)) {
|
|
final String server = preferences.getString("proxy_ip", null);
|
|
final String secret = preferences.getString("proxy_secret", null);
|
|
if (!TextUtils.isEmpty(server) && TextUtils.isEmpty(secret)) {
|
|
proxy = new Instance.Proxy(server, preferences.getInt("proxy_port", 0), preferences.getString("proxy_user", null), preferences.getString("proxy_pass", null));
|
|
}
|
|
}
|
|
|
|
// encryption key
|
|
final Instance.EncryptionKey encryptionKey = new Instance.EncryptionKey(authKey, isOutgoing);
|
|
|
|
boolean newAvailable = "2.7.7".compareTo(privateCall.protocol.library_versions.get(0)) <= 0;
|
|
if (videoCapturer != 0 && !newAvailable) {
|
|
NativeInstance.destroyVideoCapturer(videoCapturer);
|
|
videoCapturer = 0;
|
|
videoState = Instance.VIDEO_STATE_INACTIVE;
|
|
}
|
|
// init
|
|
tgVoip = Instance.makeInstance(privateCall.protocol.library_versions.get(0), config, persistentStateFilePath, endpoints, proxy, getNetworkType(), encryptionKey, remoteSink, videoCapturer, (uids, levels, voice) -> {
|
|
if (sharedInstance == null || privateCall == null) {
|
|
return;
|
|
}
|
|
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.webRtcMicAmplitudeEvent, levels[0]);
|
|
});
|
|
tgVoip.setOnStateUpdatedListener(this::onConnectionStateChanged);
|
|
tgVoip.setOnSignalBarsUpdatedListener(this::onSignalBarCountChanged);
|
|
tgVoip.setOnSignalDataListener(this::onSignalingData);
|
|
tgVoip.setOnRemoteMediaStateUpdatedListener(this::onMediaStateUpdated);
|
|
tgVoip.setMuteMicrophone(micMute);
|
|
|
|
if (newAvailable != isVideoAvailable) {
|
|
isVideoAvailable = newAvailable;
|
|
for (int a = 0; a < stateListeners.size(); a++) {
|
|
StateListener l = stateListeners.get(a);
|
|
l.onVideoAvailableChange(isVideoAvailable);
|
|
}
|
|
}
|
|
videoCapturer = 0;
|
|
|
|
AndroidUtilities.runOnUIThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (tgVoip != null) {
|
|
updateTrafficStats(null);
|
|
AndroidUtilities.runOnUIThread(this, 5000);
|
|
}
|
|
}
|
|
}, 5000);
|
|
} catch (Exception x) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("error starting call", x);
|
|
}
|
|
callFailed();
|
|
}
|
|
}
|
|
|
|
protected void showNotification() {
|
|
if (user != null) {
|
|
showNotification(ContactsController.formatName(user.first_name, user.last_name), getRoundAvatarBitmap(user));
|
|
} else {
|
|
showNotification(chat.title, getRoundAvatarBitmap(chat));
|
|
}
|
|
}
|
|
|
|
public void playConnectedSound() {
|
|
Utilities.globalQueue.postRunnable(() -> soundPool.play(spVoiceChatStartId, 1.0f, 1.0f, 0, 0, 1));
|
|
playedConnectedSound = true;
|
|
}
|
|
|
|
private void startConnectingSound() {
|
|
Utilities.globalQueue.postRunnable(() -> {
|
|
if (spPlayId != 0) {
|
|
soundPool.stop(spPlayId);
|
|
}
|
|
spPlayId = soundPool.play(spConnectingId, 1, 1, 0, -1, 1);
|
|
if (spPlayId == 0) {
|
|
AndroidUtilities.runOnUIThread(connectingSoundRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (sharedInstance == null) {
|
|
return;
|
|
}
|
|
Utilities.globalQueue.postRunnable(() -> {
|
|
if (spPlayId == 0) {
|
|
spPlayId = soundPool.play(spConnectingId, 1, 1, 0, -1, 1);
|
|
}
|
|
if (spPlayId == 0) {
|
|
AndroidUtilities.runOnUIThread(this, 100);
|
|
} else {
|
|
connectingSoundRunnable = null;
|
|
}
|
|
});
|
|
}
|
|
}, 100);
|
|
}
|
|
});
|
|
}
|
|
|
|
public void onSignalingData(byte[] data) {
|
|
if (privateCall == null) {
|
|
return;
|
|
}
|
|
TLRPC.TL_phone_sendSignalingData req = new TLRPC.TL_phone_sendSignalingData();
|
|
req.peer = new TLRPC.TL_inputPhoneCall();
|
|
req.peer.access_hash = privateCall.access_hash;
|
|
req.peer.id = privateCall.id;
|
|
req.data = data;
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> {
|
|
|
|
});
|
|
}
|
|
|
|
protected void callFailed(String error) {
|
|
if (privateCall != null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
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 = privateCall.access_hash;
|
|
req.peer.id = privateCall.id;
|
|
req.duration = (int) (getCallDuration() / 1000);
|
|
req.connection_id = tgVoip != null ? tgVoip.getPreferredRelayId() : 0;
|
|
req.reason = new TLRPC.TL_phoneCallDiscardReasonDisconnect();
|
|
ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error1) -> {
|
|
if (error1 != null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.e("error on phone.discardCall: " + error1);
|
|
}
|
|
} else {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("phone.discardCall " + response);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
super.callFailed(error);
|
|
}
|
|
|
|
@Override
|
|
public long getCallID() {
|
|
return privateCall != null ? privateCall.id : 0;
|
|
}
|
|
|
|
public boolean isVideoAvailable() {
|
|
return isVideoAvailable;
|
|
}
|
|
|
|
void onMediaButtonEvent(KeyEvent ev) {
|
|
if (ev.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK || ev.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE || ev.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
|
|
if (ev.getAction() == KeyEvent.ACTION_UP) {
|
|
if (currentState == STATE_WAITING_INCOMING) {
|
|
acceptIncomingCall();
|
|
} else {
|
|
setMicMute(!isMicMute(), false, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public byte[] getGA() {
|
|
return g_a;
|
|
}
|
|
|
|
@Override
|
|
public void didReceivedNotification(int id, int account, Object... args) {
|
|
if (id == NotificationCenter.appDidLogout) {
|
|
callEnded();
|
|
}
|
|
}
|
|
|
|
public void forceRating() {
|
|
forceRating = true;
|
|
}
|
|
|
|
private String[] getEmoji() {
|
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
try {
|
|
os.write(authKey);
|
|
os.write(g_a);
|
|
} catch (IOException ignore) {
|
|
}
|
|
return EncryptionKeyEmojifier.emojifyForCall(Utilities.computeSHA256(os.toByteArray(), 0, os.size()));
|
|
}
|
|
|
|
@Override
|
|
public void onConnectionStateChanged(int newState, boolean inTransition) {
|
|
AndroidUtilities.runOnUIThread(() -> {
|
|
if (newState == STATE_ESTABLISHED) {
|
|
if (callStartTime == 0) {
|
|
callStartTime = SystemClock.elapsedRealtime();
|
|
}
|
|
//peerCapabilities = tgVoip.getPeerCapabilities();
|
|
}
|
|
super.onConnectionStateChanged(newState, inTransition);
|
|
});
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.O)
|
|
@Override
|
|
public CallConnection getConnectionAndStartCall() {
|
|
if (systemCallConnection == null) {
|
|
if (BuildVars.LOGS_ENABLED) {
|
|
FileLog.d("creating call connection");
|
|
}
|
|
systemCallConnection = new CallConnection();
|
|
systemCallConnection.setInitializing();
|
|
if (isOutgoing) {
|
|
delayedStartOutgoingCall = () -> {
|
|
delayedStartOutgoingCall = null;
|
|
startOutgoingCall();
|
|
};
|
|
AndroidUtilities.runOnUIThread(delayedStartOutgoingCall, 2000);
|
|
}
|
|
systemCallConnection.setAddress(Uri.fromParts("tel", "+99084" + user.id, null), TelecomManager.PRESENTATION_ALLOWED);
|
|
systemCallConnection.setCallerDisplayName(ContactsController.formatName(user.first_name, user.last_name), TelecomManager.PRESENTATION_ALLOWED);
|
|
}
|
|
return systemCallConnection;
|
|
}
|
|
}
|