/*
 * 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 org.telegram.messenger.voip;

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.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationManagerCompat;
import android.telecom.Connection;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.widget.Toast;

import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.ApplicationLoader;
import org.telegram.messenger.BuildConfig;
import org.telegram.messenger.BuildVars;
import org.telegram.messenger.ContactsController;
import org.telegram.messenger.FileLog;
import org.telegram.messenger.LocaleController;
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.tgnet.ConnectionsManager;
import org.telegram.tgnet.RequestDelegate;
import org.telegram.tgnet.TLObject;
import org.telegram.tgnet.TLRPC;
import org.telegram.ui.Components.voip.VoIPHelper;
import org.telegram.ui.VoIPActivity;
import org.telegram.ui.VoIPFeedbackActivity;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

public class VoIPService extends VoIPBaseService{

	public static final int CALL_MIN_LAYER = 65;
	public static final int CALL_MAX_LAYER = 74;

	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 TLRPC.PhoneCall call;
	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;

	private boolean needSendDebugLog=false;
	private boolean endCallAfterRequest=false;
	private ArrayList<TLRPC.PhoneCall> pendingUpdates=new ArrayList<>();
	private Runnable delayedStartOutgoingCall;
	private int peerCapabilities;

	private byte[] groupCallEncryptionKey;
	private long groupCallKeyFingerprint;
	private List<Integer> groupUsersToAdd=new ArrayList<>();
	private boolean upgrading;
	private boolean joiningGroupCall;

	@Nullable
	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}


	@SuppressLint("MissingPermission")
	@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");
		int userID=intent.getIntExtra("user_id", 0);
		isOutgoing = intent.getBooleanExtra("is_outgoing", false);
		user = MessagesController.getInstance(currentAccount).getUser(userID);

		if(user==null){
            if (BuildVars.LOGS_ENABLED) {
                FileLog.w("VoIPService: user==null");
            }
			stopSelf();
			return START_NOT_STICKY;
		}
		sharedInstance = this;

		if (isOutgoing) {
			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);
				tm.placeCall(Uri.fromParts("sip", UserConfig.getInstance(currentAccount).getClientUserId()+";user="+user.id, null), extras);
			}else{
				delayedStartOutgoingCall=new Runnable(){
					@Override
					public void run(){
						delayedStartOutgoingCall=null;
						startOutgoingCall();
					}
				};
				AndroidUtilities.runOnUIThread(delayedStartOutgoingCall, 2000);
			}
			if (intent.getBooleanExtra("start_incall_activity", false)) {
				startActivity(new Intent(this, VoIPActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
			}
		} else {
			NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.closeInCallActivity);
			call = callIShouldHavePutIntoIntent;
			callIShouldHavePutIntoIntent = null;
			if(USE_CONNECTION_SERVICE){
				acknowledgeCall(false);
				showNotification();
			}else{
				acknowledgeCall(true);
			}
		}
		initializeAccountRelatedThings();

		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)
					.setSmallIcon(R.drawable.notification)
					.setContentTitle(LocaleController.getString("VoipOutgoingCall", R.string.VoipOutgoingCall))
					.setShowWhen(false);
			startForeground(ID_ONGOING_CALL_NOTIFICATION, bldr.build());
		}
	}

	@Override
	protected void updateServerConfig(){
		final SharedPreferences preferences = MessagesController.getMainSettings(currentAccount);
		VoIPServerConfig.setConfig(preferences.getString("voip_server_config", "{}"));
		ConnectionsManager.getInstance(currentAccount).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).commit();
				}
			}
		});
	}

	@Override
	protected void onControllerPreRelease(){
		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(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);
                    }
				}
			});
		}
	}

	public static VoIPService getSharedInstance() {
		return sharedInstance instanceof VoIPService ? ((VoIPService)sharedInstance) : null;
	}

	public TLRPC.User getUser() {
		return user;
	}

	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);
	}

	private void startOutgoingCall() {
		if(USE_CONNECTION_SERVICE && systemCallConnection!=null)
			systemCallConnection.setDialing();
		configureDeviceForCall();
		showNotification();
		startConnectingSound();
		dispatchStateChanged(STATE_REQUESTING);
		AndroidUtilities.runOnUIThread(new Runnable(){
			@Override
			public void run(){
				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, 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.setSecretPBytes(res.p);
						messagesStorage.setSecretG(res.g);
						messagesStorage.setLastSecretVersion(res.version);
						messagesStorage.saveSecretParams(messagesStorage.getLastSecretVersion(), messagesStorage.getSecretG(), messagesStorage.getSecretPBytes());
					}
					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.getSecretG());
					i_g_a = i_g_a.modPow(new BigInteger(1, salt), 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.protocol.udp_p2p = true;
					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(currentAccount).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(currentAccount).sendRequest(req, new RequestDelegate() {
													@Override
													public void run(TLObject response, TLRPC.TL_error error) {
                                                        if (BuildVars.LOGS_ENABLED) {
                                                            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(currentAccount).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 {
                                            if (BuildVars.LOGS_ENABLED) {
                                                FileLog.e("Error on phone.requestCall: " + error);
                                            }
											callFailed();
										}
									}
								}
							});
						}
					}, ConnectionsManager.RequestFlagFailOnServerErrors);
				} else {
                    if (BuildVars.LOGS_ENABLED) {
                        FileLog.e("Error on getDhConfig " + error);
                    }
					callFailed();
				}
			}
		}, ConnectionsManager.RequestFlagFailOnServerErrors);
	}

	private void acknowledgeCall(final boolean startRinging){
		if(call instanceof TLRPC.TL_phoneCallDiscarded){
            if (BuildVars.LOGS_ENABLED) {
                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(currentAccount).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;
                        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){
								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 " + call.id);
        }
		dispatchStateChanged(STATE_WAITING_INCOMING);
		startRingtoneAndVibration(user.id);
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !((KeyguardManager) getSystemService(KEYGUARD_SERVICE)).inKeyguardRestrictedInputMode() && NotificationManagerCompat.from(this).areNotificationsEnabled()) {
			showIncomingNotification(ContactsController.formatName(user.first_name, user.last_name), null, user, null, 0, VoIPActivity.class);
            if (BuildVars.LOGS_ENABLED) {
                FileLog.d("Showing incoming call notification");
            }
		} else {
            if (BuildVars.LOGS_ENABLED) {
                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) {
                if (BuildVars.LOGS_ENABLED) {
                    FileLog.e("Error starting incall activity", x);
                }
			}
			if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
				showNotification();
			}
		}
	}

	@Override
	protected boolean isRinging(){
		return currentState==STATE_WAITING_INCOMING;
	}

	public void acceptIncomingCall() {
		stopRinging();
		showNotification();
		configureDeviceForCall();
		startConnectingSound();
		dispatchStateChanged(STATE_EXCHANGING_KEYS);
		AndroidUtilities.runOnUIThread(new Runnable(){
			@Override
			public void run(){
				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, 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);*/
                            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(call==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=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(currentAccount).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) {
                                        if (BuildVars.LOGS_ENABLED) {
                                            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 {
                                        if (BuildVars.LOGS_ENABLED) {
                                            FileLog.e("Error on phone.acceptCall: " + error);
                                        }
										callFailed();
									}
								}
							});
						}
					}, ConnectionsManager.RequestFlagFailOnServerErrors);
				} else {
					//acceptingChats.remove(encryptedChat.id);
					callFailed();
				}
			}
		});
	}

	public void declineIncomingCall() {
		declineIncomingCall(DISCARD_REASON_HANGUP, null);
	}

	@Override
	protected Class<? extends Activity> getUIActivityClass(){
		return VoIPActivity.class;
	}

	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;
			}
			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(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 = 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(currentAccount).getConnectionState()!=ConnectionsManager.ConnectionStateConnected;
		final Runnable stopper;
		if(wasNotConnected){
			if (onDone != null)
				onDone.run();
			callEnded();
			stopper=null;
		}else{
			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(currentAccount).sendRequest(req, new RequestDelegate() {
			@Override
			public void run(TLObject response, TLRPC.TL_error 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);
                    }
				}
				if (!wasNotConnected){
					AndroidUtilities.cancelRunOnUIThread(stopper);
					if(onDone!=null)
						onDone.run();
				}
			}
		}, ConnectionsManager.RequestFlagFailOnServerErrors);
	}

	private void dumpCallObject() {
        try {
            if (BuildVars.LOGS_ENABLED) {
                Field[] flds = TLRPC.PhoneCall.class.getFields();
                for (Field f : flds) {
                    FileLog.d(f.getName() + " = " + f.get(call));
                }
            }
        } catch (Exception x) {
            if (BuildVars.LOGS_ENABLED) {
                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.LOGS_ENABLED) {
                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.LOGS_ENABLED) {
            FileLog.d("Call updated: " + call);
            dumpCallObject();
        }
		this.call = call;
		if (call instanceof TLRPC.TL_phoneCallDiscarded) {
			needSendDebugLog=call.need_debug;
            if (BuildVars.LOGS_ENABLED) {
                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(afterSoundRunnable, 1500);
				stopSelf();
			} else {
				callEnded();
			}
			if (call.need_rating || forceRating) {
				startRatingActivity();
			}
		} else if (call instanceof TLRPC.TL_phoneCall && authKey == null){
			if(call.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(call.g_a_or_b, 0, call.g_a_or_b.length))){
                if (BuildVars.LOGS_ENABLED) {
                    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.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!=call.key_fingerprint){
                if (BuildVars.LOGS_ENABLED) {
                    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);
                if (BuildVars.LOGS_ENABLED) {
                    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(currentAccount).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)
					.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, call.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=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(currentAccount).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 {
            if (BuildVars.LOGS_ENABLED) {
                FileLog.d("InitCall: keyID=" + keyFingerprint);
            }
			SharedPreferences nprefs=MessagesController.getNotificationsSettings(currentAccount);
			HashSet<String> hashes=new HashSet<>(nprefs.getStringSet("calls_access_hashes", Collections.EMPTY_SET));
			hashes.add(call.id+" "+call.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();
			final SharedPreferences preferences = MessagesController.getGlobalMainSettings();
			controller.setConfig(MessagesController.getInstance(currentAccount).callPacketTimeout / 1000.0, MessagesController.getInstance(currentAccount).callConnectTimeout / 1000.0,
					preferences.getInt("VoipDataSaving", VoIPController.DATA_SAVING_NEVER), call.id);
			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);

			SharedPreferences prefs=MessagesController.getGlobalMainSettings();
			VoIPHelper.upgradeP2pSetting(currentAccount);
			boolean allowP2p=true;
			switch(MessagesController.getMainSettings(currentAccount).getInt("calls_p2p_new", MessagesController.getInstance(currentAccount).defaultP2pContacts ? 1 : 0)){
				case 0:
					allowP2p=true;
					break;
				case 2:
					allowP2p=false;
					break;
				case 1:
					allowP2p=ContactsController.getInstance(currentAccount).contactsDict.get(user.id)!=null;
					break;
			}

			controller.setRemoteEndpoints(endpoints, call.protocol.udp_p2p && allowP2p, BuildVars.DEBUG_VERSION && prefs.getBoolean("dbg_force_tcp_in_calls", false), call.protocol.max_layer);
			if(prefs.getBoolean("dbg_force_tcp_in_calls", false)){
				AndroidUtilities.runOnUIThread(new Runnable(){
					@Override
					public void run(){
						Toast.makeText(VoIPService.this, "This call uses TCP which will degrade its quality.", Toast.LENGTH_SHORT).show();
					}
				});
			}
			if(prefs.getBoolean("proxy_enabled", false) && prefs.getBoolean("proxy_enabled_calls", false)) {
				String server = prefs.getString("proxy_ip", null);
				String secret = prefs.getString("proxy_secret", null);
				if (!TextUtils.isEmpty(server) && TextUtils.isEmpty(secret)) {
					controller.setProxy(server, prefs.getInt("proxy_port", 0), prefs.getString("proxy_user", null), prefs.getString("proxy_pass", null));
				}
			}
			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) {
            if (BuildVars.LOGS_ENABLED) {
                FileLog.e("error starting call", x);
            }
			callFailed();
		}
	}

	protected void showNotification(){
		showNotification(ContactsController.formatName(user.first_name, user.last_name), user.photo!=null ? user.photo.photo_small : null, VoIPActivity.class);
	}

	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);
		}
	}

	protected void callFailed(int errorCode) {
		if (call != 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 = 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(currentAccount).sendRequest(req, new RequestDelegate() {
				@Override
				public void run(TLObject response, TLRPC.TL_error error) {
					if (error != null) {
                        if (BuildVars.LOGS_ENABLED) {
                            FileLog.e("error on phone.discardCall: " + error);
                        }
					} else {
                        if (BuildVars.LOGS_ENABLED) {
                            FileLog.d("phone.discardCall " + response);
                        }
					}
				}
			});
		}
		super.callFailed(errorCode);
	}

	@Override
	public long getCallID(){
		return call!=null ? call.id : 0;
	}

	public void onUIForegroundStateChanged(boolean isForeground) {
		if (currentState == STATE_WAITING_INCOMING) {
			if (isForeground) {
				stopForeground(true);
			} else {
				if (!((KeyguardManager) getSystemService(KEYGUARD_SERVICE)).inKeyguardRestrictedInputMode()) {
					if(NotificationManagerCompat.from(this).areNotificationsEnabled())
						showIncomingNotification(ContactsController.formatName(user.first_name, user.last_name), null, user, null, 0, VoIPActivity.class);
					else
						declineIncomingCall(DISCARD_REASON_LINE_BUSY, null);
				} 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) {
                                if (BuildVars.LOGS_ENABLED) {
                                    FileLog.e("error restarting activity", e);
                                }
								declineIncomingCall(DISCARD_REASON_LINE_BUSY, null);
							}
							if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
								showNotification();
							}
						}
					}, 500);
				}
			}
		}
	}

	/*package*/ 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());
					for (StateListener l : stateListeners)
						l.onAudioSettingsChanged();
				}
			}
		}
	}

	public void debugCtl(int request, int param) {
		if (controller != null)
			controller.debugCtl(request, param);
	}

	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()));
	}

	public boolean canUpgrate(){
		return (peerCapabilities & VoIPController.PEER_CAP_GROUP_CALLS)==VoIPController.PEER_CAP_GROUP_CALLS;
	}

	public void upgradeToGroupCall(List<Integer> usersToAdd){
		if(upgrading)
			return;
		groupUsersToAdd=usersToAdd;
		if(!isOutgoing){
			controller.requestCallUpgrade();
			return;
		}
		upgrading=true;
		groupCallEncryptionKey=new byte[256];
		Utilities.random.nextBytes(groupCallEncryptionKey);
		groupCallEncryptionKey[0]&=0x7F;
		byte[] authKeyHash = Utilities.computeSHA1(groupCallEncryptionKey);
		byte[] authKeyId = new byte[8];
		System.arraycopy(authKeyHash, authKeyHash.length - 8, authKeyId, 0, 8);
		groupCallKeyFingerprint=Utilities.bytesToLong(authKeyId);

		controller.sendGroupCallKey(groupCallEncryptionKey);
	}

	/*public void upgradedToGroupCall(TLRPC.TL_updateGroupCall update){
		if(upgrading){
			FileLog.w("Received an update about call upgrade but we're upgrading it ourselves; ignoring update");
			return;
		}
		VoIPGroupService.waitingToStart=true;
		TLRPC.TL_phone_getGroupCall req=new TLRPC.TL_phone_getGroupCall();
		req.call=new TLRPC.TL_inputGroupCall();
		req.call.id=update.call.id;
		req.call.access_hash=update.call.access_hash;
		ConnectionsManager.getInstance(currentAccount).sendRequest(req, new RequestDelegate(){
			@Override
			public void run(TLObject response, TLRPC.TL_error error){
				if(response!=null){
					TLRPC.TL_phone_groupCall call=(TLRPC.TL_phone_groupCall) response;
					if((call.call.flags & 2)==1 || call.call.key_fingerprint!=groupCallKeyFingerprint){
						callFailed(VoIPController.ERROR_INSECURE_UPGRADE);
						return;
					}
					stopSelf();
					VoIPGroupService.callToStartFor=call;
					VoIPGroupService.secretCallEncryptionKey=groupCallEncryptionKey;
					Intent intent=new Intent(ApplicationLoader.applicationContext, VoIPGroupService.class);
					intent.putExtra("account", currentAccount);
					intent.putExtra("use_existing_call", true);
					intent.putExtra("start_incall_activity", true);
					intent.putExtra("need_update_self_streams", true);
					intent.putExtra("private_key_emoji", getEmoji());
					//intent.putExtra("forced_admin_id", user.id);
					int[] uids=new int[groupUsersToAdd.size()];
					for(int i=0;i<uids.length;i++)
						uids[i]=groupUsersToAdd.get(i);
					intent.putExtra("invite_users", uids);
					ApplicationLoader.applicationContext.startService(intent);
				}else{
					VoIPGroupService.waitingToStart=false;
					callFailed();
				}
			}
		});
	}*/

	@Override
	public void onConnectionStateChanged(int newState){
		if(newState==STATE_ESTABLISHED){
			peerCapabilities=controller.getPeerCapabilities();
		}
		super.onConnectionStateChanged(newState);
	}

	@Override
	public void onGroupCallKeyReceived(byte[] key){
		joiningGroupCall=true;
		groupCallEncryptionKey=key;
		byte[] authKeyHash = Utilities.computeSHA1(groupCallEncryptionKey);
		byte[] authKeyId = new byte[8];
		System.arraycopy(authKeyHash, authKeyHash.length - 8, authKeyId, 0, 8);
		groupCallKeyFingerprint=Utilities.bytesToLong(authKeyId);
	}

	@Override
	public void onGroupCallKeySent(){
		if(isOutgoing){
			//actuallyUpgradeToGroupCall();
		}
	}

	@Override
	public void onCallUpgradeRequestReceived(){
		upgradeToGroupCall(new ArrayList<Integer>());
	}

	@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=new Runnable(){
					@Override
					public void run(){
						delayedStartOutgoingCall=null;
						startOutgoingCall();
					}
				};
				AndroidUtilities.runOnUIThread(delayedStartOutgoingCall, 2000);
			}
			systemCallConnection.setCallerDisplayName(ContactsController.formatName(user.first_name, user.last_name), TelecomManager.PRESENTATION_ALLOWED);
		}
		return systemCallConnection;
	}

	/*private void actuallyUpgradeToGroupCall(){
		TLRPC.TL_phone_upgradePhoneCall req=new TLRPC.TL_phone_upgradePhoneCall();
		req.peer=new TLRPC.TL_inputPhoneCall();
		req.peer.id=call.id;
		req.peer.access_hash=call.access_hash;
		req.key_fingerprint=groupCallKeyFingerprint;
		req.streams=VoIPGroupController.getInitialStreams();
		upgrading=true;
		ConnectionsManager.getInstance(currentAccount).sendRequest(req, new RequestDelegate(){
			@Override
			public void run(TLObject response, TLRPC.TL_error error){
				FileLog.d("upgrade call response = "+response);
				if(error!=null){
					FileLog.e("Failed to upgrade call, error: "+error.code+" "+error.text);
					callFailed();
					return;
				}
				stopSelf();
				TLRPC.TL_phone_groupCall call=(TLRPC.TL_phone_groupCall) response;
				VoIPGroupService.callToStartFor=call;
				VoIPGroupService.secretCallEncryptionKey=groupCallEncryptionKey;
				VoIPGroupService.waitingToStart=true;
				Intent intent=new Intent(ApplicationLoader.applicationContext, VoIPGroupService.class);
				intent.putExtra("account", currentAccount);
				intent.putExtra("use_existing_call", true);
				intent.putExtra("start_incall_activity", true);
				intent.putExtra("forced_admin_id", user.id);
				intent.putExtra("private_key_emoji", getEmoji());
				int[] uids=new int[groupUsersToAdd.size()];
				for(int i=0;i<uids.length;i++)
					uids[i]=groupUsersToAdd.get(i);
				intent.putExtra("invite_users", uids);
				ApplicationLoader.applicationContext.startService(intent);
			}
		});
	}*/
}
