diff --git a/.gitignore b/.gitignore
index ef60efaf88..eb2f8b24b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,5 @@ obj
/local.properties
libc++_shared.so
+*.jks
+keystore.properties
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d2d90b818b..c80a8c8760 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -93,6 +93,8 @@ android {
buildConfigString("THEME_FILE_EXTENSION", App.THEME_EXTENSION)
+ buildConfigField("boolean", "USE_NTGCALLS", config.useNTgCalls.toString())
+
// Library versions in BuildConfig.java
var openSslVersion = ""
@@ -365,6 +367,8 @@ android {
ndk.abiFilters.addAll(variant.filters)
externalNativeBuild.ndkBuild.abiFilters(*variant.filters)
externalNativeBuild.cmake.abiFilters(*variant.filters)
+
+ externalNativeBuild.cmake.arguments.add("-DENABLE_TGVOIP=" + (if (config.useNTgCalls) "no" else "yes"))
}
}
}
@@ -578,6 +582,7 @@ dependencies {
exclude(group = "com.google.firebase", module = "firebase-analytics")
exclude(group = "com.google.firebase", module = "firebase-measurement-connector")
}
+ // implementation("com.google.firebase:firebase-appcheck-safetynet:16.1.2")
// Play Integrity: https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/release-notes
flavorImplementation(
libs.google.play.integrity.legacy,
@@ -647,6 +652,11 @@ dependencies {
// mp4parser: https://github.com/sannies/mp4parser/releases
implementation(libs.mp4parser.isoparser)
+
+ // NTgCalls: https://github.com/pytgcalls/ntgcalls/
+ if (config.useNTgCalls) {
+ implementation(libs.pytgcalls.ntgcalls)
+ }
}
if (!config.isExperimentalBuild) {
diff --git a/app/jni/CMakeLists.txt b/app/jni/CMakeLists.txt
index 61dc2e7af5..644714c237 100644
--- a/app/jni/CMakeLists.txt
+++ b/app/jni/CMakeLists.txt
@@ -19,8 +19,6 @@ set(TGX_ROOT_DIR "${PROJECT_SOURCE_DIR}/../..")
set(TDLIB_DIR "${TGX_ROOT_DIR}/tdlib")
set(UTILS_DIR "${THIRDPARTY_DIR}/jni-utils")
-set(ENABLE_TGVOIP yes)
-
# Using webp only if building for 32-bit platform
#if (${ANDROID_ABI} STREQUAL "armeabi-v7a" OR ${ANDROID_ABI} STREQUAL "x86")
# set(USE_WEBP yes)
diff --git a/app/src/latest/java/tgx/flavor/NLoader.java b/app/src/latest/java/tgx/flavor/NLoader.java
index 13223b9816..aeff1aca81 100644
--- a/app/src/latest/java/tgx/flavor/NLoader.java
+++ b/app/src/latest/java/tgx/flavor/NLoader.java
@@ -21,7 +21,9 @@ public static synchronized boolean loadLibraries () {
System.loadLibrary("sslx");
System.loadLibrary("tdjni");
System.loadLibrary("leveldbjni");
- System.loadLibrary("tgcallsjni");
+ if (!BuildConfig.USE_NTGCALLS) {
+ System.loadLibrary("tgcallsjni");
+ }
System.loadLibrary("tgxjni");
N.setupLibraries();
loaded = true;
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e6b611b828..58b0159882 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -67,6 +67,8 @@
+
+
+ android:foregroundServiceType="phoneCall|camera|microphone|mediaProjection|mediaPlayback" />
diff --git a/app/src/main/java/org/pytgcalls/ntgcallsx/CallInterface.java b/app/src/main/java/org/pytgcalls/ntgcallsx/CallInterface.java
new file mode 100644
index 0000000000..1d4dccae74
--- /dev/null
+++ b/app/src/main/java/org/pytgcalls/ntgcallsx/CallInterface.java
@@ -0,0 +1,50 @@
+package org.pytgcalls.ntgcallsx;
+
+import org.drinkless.tdlib.TdApi;
+import io.github.pytgcalls.FrameCallback;
+import io.github.pytgcalls.RemoteSourceChangeCallback;
+import org.thunderdog.challegram.telegram.Tdlib;
+import org.thunderdog.challegram.voip.NetworkStats;
+import org.thunderdog.challegram.voip.annotation.CallNetworkType;
+
+public interface CallInterface {
+ boolean isVideoSupported();
+
+ void setFrameCallback(FrameCallback callback);
+
+ void setRemoteSourceChangeCallback(RemoteSourceChangeCallback callback);
+
+ long getCallDuration();
+
+ void setAudioOutputGainControlEnabled(boolean enabled);
+
+ void handleIncomingSignalingData(byte[] data);
+
+ void setEchoCancellationStrength(int strength);
+
+ void setMicDisabled(boolean disabled);
+
+ void setCameraEnabled(boolean enabled, boolean front);
+
+ void setScreenShareEnabled(boolean enabled);
+
+ long getConnectionId();
+
+ void setNetworkType(@CallNetworkType int type);
+
+ void getNetworkStats(NetworkStats stats);
+
+ void performDestroy();
+
+ CharSequence collectDebugLog();
+
+ TdApi.Call getCall();
+
+ Tdlib tdlib();
+
+ boolean isInitiated();
+
+ String getLibraryName();
+
+ String getLibraryVersion();
+}
diff --git a/app/src/main/java/org/pytgcalls/ntgcallsx/NTgCallsInterface.java b/app/src/main/java/org/pytgcalls/ntgcallsx/NTgCallsInterface.java
new file mode 100644
index 0000000000..76c6afc6b2
--- /dev/null
+++ b/app/src/main/java/org/pytgcalls/ntgcallsx/NTgCallsInterface.java
@@ -0,0 +1,386 @@
+package org.pytgcalls.ntgcallsx;
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.view.Display;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import org.drinkless.tdlib.TdApi;
+import io.github.pytgcalls.NetworkInfo;
+import io.github.pytgcalls.FrameCallback;
+import io.github.pytgcalls.NTgCalls;
+import io.github.pytgcalls.RemoteSourceChangeCallback;
+import io.github.pytgcalls.exceptions.ConnectionException;
+import io.github.pytgcalls.exceptions.ConnectionNotFoundException;
+import io.github.pytgcalls.media.AudioDescription;
+import io.github.pytgcalls.media.MediaDescription;
+import io.github.pytgcalls.media.MediaSource;
+import io.github.pytgcalls.media.StreamMode;
+import io.github.pytgcalls.media.VideoDescription;
+import io.github.pytgcalls.p2p.RTCServer;
+
+import org.pytgcalls.ntgcalls.BuildConfig;
+import org.thunderdog.challegram.Log;
+import org.thunderdog.challegram.telegram.Tdlib;
+import org.thunderdog.challegram.tool.UI;
+import org.thunderdog.challegram.voip.ConnectionStateListener;
+import org.thunderdog.challegram.voip.NetworkStats;
+import org.thunderdog.challegram.voip.VoIPInstance;
+import org.thunderdog.challegram.voip.annotation.CallState;
+
+import java.io.FileNotFoundException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class NTgCallsInterface implements CallInterface {
+ private static final long CALL_ID = 0;
+ private final NTgCalls ntgcalls;
+ private final @Nullable AudioDescription micDescription;
+ private static final int CAPTURE_WIDTH = 1920;
+ private static final int CAPTURE_HEIGHT = 1080;
+ private static final int AUTO_DETECT = -1;
+ private final Tdlib tdlib;
+ private final TdApi.Call call;
+
+ public NTgCallsInterface (Tdlib tdlib, TdApi.Call call, TdApi.CallStateReady state, ConnectionStateListener listener) throws ConnectionException, FileNotFoundException {
+ this.tdlib = tdlib;
+ this.call = call;
+ ntgcalls = new NTgCalls();
+ ntgcalls.setSignalingDataCallback((callId, data) -> listener.onSignallingDataEmitted(data));
+ ntgcalls.setConnectionChangeCallback((chatId, callNetworkState) -> {
+ if (callNetworkState.state == NetworkInfo.State.CONNECTED) {
+ listener.onConnectionStateChanged(null, CallState.ESTABLISHED);
+ } else if (callNetworkState.state != NetworkInfo.State.CONNECTING) {
+ listener.onConnectionStateChanged(null, CallState.FAILED);
+ }
+ });
+ micDescription = new AudioDescription(
+ MediaSource.DEVICE,
+ NTgCalls.getMediaDevices().microphone.get(0).metadata,
+ true,
+ 48000,
+ 2
+ );
+ ntgcalls.createP2PCall(CALL_ID);
+ if (ContextCompat.checkSelfPermission(UI.getAppContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("No microphone permission");
+ }
+ if (ContextCompat.checkSelfPermission(UI.getAppContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("No microphone permission");
+ }
+ ntgcalls.setStreamSources(
+ CALL_ID,
+ StreamMode.CAPTURE,
+ new MediaDescription(
+ micDescription,
+ null,
+ null,
+ null
+ )
+ );
+ ntgcalls.setStreamSources(
+ CALL_ID,
+ StreamMode.PLAYBACK,
+ new MediaDescription(
+ new AudioDescription(
+ MediaSource.DEVICE,
+ NTgCalls.getMediaDevices().speaker.get(0).metadata,
+ true,
+ 48000,
+ 2
+ ),
+ null,
+ new VideoDescription(
+ MediaSource.EXTERNAL,
+ "",
+ true,
+ AUTO_DETECT,
+ AUTO_DETECT,
+ 30
+ ),
+ null
+ )
+ );
+ ntgcalls.skipExchange(CALL_ID, state.encryptionKey, call.isOutgoing);
+ var rtcServers = Arrays.stream(state.servers)
+ .map(server -> {
+ if (server.type instanceof TdApi.CallServerTypeWebrtc webrtc) {
+ return new RTCServer(
+ server.id,
+ server.ipAddress,
+ server.ipv6Address,
+ server.port,
+ webrtc.username,
+ webrtc.password,
+ webrtc.supportsTurn,
+ webrtc.supportsStun,
+ false,
+ null
+ );
+ } else {
+ var reflector = (TdApi.CallServerTypeTelegramReflector) server.type;
+ return new RTCServer(
+ server.id,
+ server.ipAddress,
+ server.ipv6Address,
+ server.port,
+ null,
+ null,
+ true,
+ false,
+ reflector.isTcp,
+ reflector.peerTag
+ );
+ }
+ }).collect(Collectors.toList());
+ ntgcalls.connectP2P(CALL_ID, rtcServers, List.of(state.protocol.libraryVersions), state.allowP2p);
+ }
+
+ @Override
+ public boolean isVideoSupported () {
+ return true;
+ }
+
+ @Override
+ public void setFrameCallback (FrameCallback callback) {
+ ntgcalls.setFrameCallback(callback);
+ }
+
+ @Override
+ public void setRemoteSourceChangeCallback (RemoteSourceChangeCallback callback) {
+ ntgcalls.setRemoteSourceChangeCallback(callback);
+ }
+
+ @Override
+ public long getCallDuration () {
+ try {
+ return ntgcalls.time(CALL_ID, StreamMode.PLAYBACK) * 1000;
+ } catch (ConnectionNotFoundException e) {
+ return VoIPInstance.DURATION_UNKNOWN;
+ }
+ }
+
+ @Override
+ public void setAudioOutputGainControlEnabled (boolean enabled) {
+
+ }
+
+ @Override
+ public void handleIncomingSignalingData (byte[] data) {
+ try {
+ ntgcalls.sendSignalingData(CALL_ID, data);
+ } catch (ConnectionException e) {
+ Log.e(Log.TAG_VOIP, "Error sending signaling data", e);
+ }
+ }
+
+ @Override
+ public void setEchoCancellationStrength (int strength) {
+
+ }
+
+ @Override
+ public void setMicDisabled (boolean disabled) {
+ try {
+ if (disabled) {
+ ntgcalls.mute(CALL_ID);
+ } else {
+ ntgcalls.unmute(CALL_ID);
+ }
+ } catch (ConnectionNotFoundException e) {
+ Log.e(Log.TAG_VOIP, "Error setting call settings", e);
+ }
+ }
+
+ @Override
+ public void setCameraEnabled (boolean enabled, boolean front) {
+ try {
+ if (ContextCompat.checkSelfPermission(UI.getAppContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("No microphone permission");
+ }
+ if (ContextCompat.checkSelfPermission(UI.getAppContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("No microphone permission");
+ }
+ if (enabled) {
+ String cameraId = NTgCalls.getMediaDevices().camera.get(front ? 1 : 0).metadata;
+ ntgcalls.setStreamSources(
+ CALL_ID,
+ StreamMode.CAPTURE,
+ new MediaDescription(
+ micDescription,
+ null,
+ new VideoDescription(
+ MediaSource.DEVICE,
+ cameraId,
+ true,
+ CAPTURE_WIDTH,
+ CAPTURE_HEIGHT,
+ 30
+ ),
+ null
+ )
+ );
+ } else {
+ ntgcalls.setStreamSources(
+ CALL_ID,
+ StreamMode.CAPTURE,
+ new MediaDescription(
+ micDescription,
+ null,
+ null,
+ null
+ )
+ );
+ }
+ } catch (FileNotFoundException | ConnectionNotFoundException e) {
+ Log.e(Log.TAG_VOIP, "Error setting camera", e);
+ }
+ }
+
+ @Override
+ public void setScreenShareEnabled (boolean enabled) {
+ var size = getScreenCaptureSize();
+ try {
+ if (ContextCompat.checkSelfPermission(UI.getAppContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("No microphone permission");
+ }
+ if (ContextCompat.checkSelfPermission(UI.getAppContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("No microphone permission");
+ }
+ if (enabled) {
+ ntgcalls.setStreamSources(
+ CALL_ID,
+ StreamMode.CAPTURE,
+ new MediaDescription(
+ micDescription,
+ null,
+ null,
+ new VideoDescription(
+ MediaSource.DESKTOP,
+ NTgCalls.getMediaDevices().screen.get(0).metadata,
+ true,
+ size.x,
+ size.y,
+ 30
+ )
+ )
+ );
+ } else {
+ ntgcalls.setStreamSources(
+ CALL_ID,
+ StreamMode.CAPTURE,
+ new MediaDescription(
+ micDescription,
+ null,
+ null,
+ null
+ )
+ );
+ }
+ } catch (FileNotFoundException | ConnectionNotFoundException e) {
+ Log.e(Log.TAG_VOIP, "Error setting screen share", e);
+ }
+ }
+
+ @Override
+ public long getConnectionId () {
+ return 0;
+ }
+
+ @Override
+ public void setNetworkType (int type) {
+
+ }
+
+ @Override
+ public void getNetworkStats (NetworkStats stats) {
+
+ }
+
+ @Override
+ public void performDestroy () {
+ try {
+ ntgcalls.stop(CALL_ID);
+ } catch (ConnectionNotFoundException e) {
+ Log.e(Log.TAG_VOIP, "Error stopping call", e);
+ }
+ }
+
+ @Override
+ public CharSequence collectDebugLog () {
+ return null;
+ }
+
+ @Override
+ public TdApi.Call getCall () {
+ return call;
+ }
+
+ @Override
+ public Tdlib tdlib () {
+ return tdlib;
+ }
+
+ @Override
+ public boolean isInitiated () {
+ return ntgcalls.calls().containsKey(CALL_ID);
+ }
+
+ @Override
+ public String getLibraryName () {
+ return "ntgcalls";
+ }
+
+ @Override
+ public String getLibraryVersion () {
+ return BuildConfig.VERSION_NAME;
+ }
+
+ private static Point getScreenCaptureSize() {
+ WindowManager wm = (WindowManager) UI.getAppContext().getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Point size = new Point();
+ display.getRealSize(size);
+ float aspect;
+ if (size.x > size.y) {
+ aspect = size.y / (float) size.x;
+ } else {
+ aspect = size.x / (float) size.y;
+ }
+ int dx = -1;
+ int dy = -1;
+ for (int a = 1; a <= 100; a++) {
+ float val = a * aspect;
+ if (val == (int) val) {
+ if (size.x > size.y) {
+ dx = a;
+ dy = (int) (a * aspect);
+ } else {
+ dy = a;
+ dx = (int) (a * aspect);
+ }
+ break;
+ }
+ }
+ if (dx != -1 && aspect != 1) {
+ while (size.x > 1000 || size.y > 1000 || size.x % 4 != 0 || size.y % 4 != 0) {
+ size.x -= dx;
+ size.y -= dy;
+ if (size.x < 800 && size.y < 800) {
+ dx = -1;
+ break;
+ }
+ }
+ }
+ if (dx == -1 || aspect == 1) {
+ float scale = Math.max(size.x / 970.0f, size.y / 970.0f);
+ size.x = (int) Math.ceil((size.x / scale) / 4.0f) * 4;
+ size.y = (int) Math.ceil((size.y / scale) / 4.0f) * 4;
+ }
+ return size;
+ }
+}
diff --git a/app/src/main/java/org/pytgcalls/ntgcallsx/TgCallsInterface.java b/app/src/main/java/org/pytgcalls/ntgcallsx/TgCallsInterface.java
new file mode 100644
index 0000000000..cad33309ab
--- /dev/null
+++ b/app/src/main/java/org/pytgcalls/ntgcallsx/TgCallsInterface.java
@@ -0,0 +1,135 @@
+package org.pytgcalls.ntgcallsx;
+
+import androidx.annotation.Nullable;
+
+import org.drinkless.tdlib.TdApi;
+import io.github.pytgcalls.FrameCallback;
+import io.github.pytgcalls.RemoteSourceChangeCallback;
+import org.thunderdog.challegram.telegram.Tdlib;
+import org.thunderdog.challegram.voip.ConnectionStateListener;
+import org.thunderdog.challegram.voip.NetworkStats;
+import org.thunderdog.challegram.voip.Socks5Proxy;
+import org.thunderdog.challegram.voip.VoIP;
+import org.thunderdog.challegram.voip.VoIPInstance;
+
+public class TgCallsInterface implements CallInterface{
+ private final VoIPInstance voIPInstance;
+
+ public TgCallsInterface (Tdlib tdlib, TdApi.Call call, TdApi.CallStateReady state, ConnectionStateListener stateListener, boolean forceTcp, @Nullable Socks5Proxy callProxy, int lastNetworkType, boolean audioGainControlEnabled, int echoCancellationStrength, boolean isMicDisabled) {
+ voIPInstance = VoIP.instantiateAndConnect(
+ tdlib,
+ call,
+ state,
+ stateListener,
+ forceTcp,
+ callProxy,
+ lastNetworkType,
+ audioGainControlEnabled,
+ echoCancellationStrength,
+ isMicDisabled
+ );
+ if (voIPInstance == null) {
+ throw new RuntimeException("Failed to instantiate VoIPInstance");
+ }
+ }
+
+ @Override
+ public boolean isVideoSupported () {
+ return false;
+ }
+
+ @Override
+ public void setFrameCallback (FrameCallback callback) {
+
+ }
+
+ @Override
+ public void setRemoteSourceChangeCallback (RemoteSourceChangeCallback callback) {
+
+ }
+
+ @Override
+ public long getCallDuration () {
+ return voIPInstance.getCallDuration();
+ }
+
+ @Override
+ public void setAudioOutputGainControlEnabled (boolean enabled) {
+ voIPInstance.setAudioOutputGainControlEnabled(enabled);
+ }
+
+ @Override
+ public void handleIncomingSignalingData (byte[] data) {
+ voIPInstance.handleIncomingSignalingData(data);
+ }
+
+ @Override
+ public void setEchoCancellationStrength (int strength) {
+ voIPInstance.setEchoCancellationStrength(strength);
+ }
+
+ @Override
+ public void setMicDisabled (boolean disabled) {
+ voIPInstance.setMicDisabled(disabled);
+ }
+
+ @Override
+ public void setCameraEnabled (boolean enabled, boolean front) {
+
+ }
+
+ @Override
+ public void setScreenShareEnabled (boolean enabled) {
+
+ }
+
+ @Override
+ public long getConnectionId () {
+ return voIPInstance.getConnectionId();
+ }
+
+ @Override
+ public void setNetworkType (int type) {
+ voIPInstance.setNetworkType(type);
+ }
+
+ @Override
+ public void getNetworkStats (NetworkStats stats) {
+ voIPInstance.getNetworkStats(stats);
+ }
+
+ @Override
+ public void performDestroy () {
+ voIPInstance.performDestroy();
+ }
+
+ @Override
+ public CharSequence collectDebugLog () {
+ return voIPInstance.collectDebugLog();
+ }
+
+ @Override
+ public TdApi.Call getCall () {
+ return voIPInstance.getCall();
+ }
+
+ @Override
+ public Tdlib tdlib () {
+ return voIPInstance.tdlib();
+ }
+
+ @Override
+ public boolean isInitiated () {
+ return true;
+ }
+
+ @Override
+ public String getLibraryName () {
+ return voIPInstance.getLibraryName();
+ }
+
+ @Override
+ public String getLibraryVersion () {
+ return voIPInstance.getLibraryVersion();
+ }
+}
diff --git a/app/src/main/java/org/pytgcalls/ntgcallsx/VoIPFloatingLayout.java b/app/src/main/java/org/pytgcalls/ntgcallsx/VoIPFloatingLayout.java
new file mode 100644
index 0000000000..6ee1352f79
--- /dev/null
+++ b/app/src/main/java/org/pytgcalls/ntgcallsx/VoIPFloatingLayout.java
@@ -0,0 +1,462 @@
+package org.pytgcalls.ntgcallsx;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.os.Build;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewOutlineProvider;
+import android.view.ViewParent;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import org.thunderdog.challegram.Log;
+import org.thunderdog.challegram.charts.CubicBezierInterpolator;
+import org.thunderdog.challegram.tool.Screen;
+
+public class VoIPFloatingLayout extends FrameLayout {
+ public final static int STATE_GONE = 0;
+ public final static int STATE_FULLSCREEN = 1;
+ public final static int STATE_FLOATING = 2;
+
+ private final float FLOATING_MODE_SCALE = 0.23f;
+ float starX;
+ float starY;
+ float startMovingFromX;
+ float startMovingFromY;
+ boolean moving;
+
+ int lastH;
+ int lastW;
+ float touchSlop;
+
+ final Path path = new Path();
+ final RectF rectF = new RectF();
+
+ public float relativePositionToSetX = -1f;
+ float relativePositionToSetY = -1f;
+
+ private float leftPadding;
+ private float rightPadding;
+ private float topPadding;
+ private float bottomPadding;
+
+ float toFloatingModeProgress = 0;
+
+ private boolean floatingMode;
+ private boolean setedFloatingMode;
+ private boolean switchingToFloatingMode;
+ public boolean measuredAsFloatingMode;
+
+ private boolean active = true;
+ public boolean isAppearing;
+ public boolean alwaysFloating;
+ public float savedRelativePositionX;
+ public float savedRelativePositionY;
+ public float updatePositionFromX;
+ public float updatePositionFromY;
+ public boolean switchingToPip;
+
+ OnClickListener tapListener;
+
+ private VoIPFloatingLayoutDelegate delegate;
+
+ public interface VoIPFloatingLayoutDelegate {
+ void onChange(float progress, boolean value);
+ }
+
+ ValueAnimator switchToFloatingModeAnimator;
+ private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ toFloatingModeProgress = (float) valueAnimator.getAnimatedValue();
+ if (delegate != null) {
+ delegate.onChange(toFloatingModeProgress, measuredAsFloatingMode);
+ }
+ invalidate();
+ }
+ };
+
+ public VoIPFloatingLayout (@NonNull Context context) {
+ super(context);
+ touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ setOutlineProvider(new ViewOutlineProvider() {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void getOutline(View view, Outline outline) {
+ if (!floatingMode) {
+ outline.setRect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+ } else {
+ outline.setRoundRect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight(), floatingMode ? Screen.dp(4) : 0);
+ }
+ }
+ });
+ setClipToOutline(true);
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(@NonNull Canvas canvas) {
+ boolean animated = false;
+ if (updatePositionFromX >= 0) {
+ if(!isAppearing) {
+ animate().setListener(null).cancel();
+ }
+ setTranslationX(updatePositionFromX);
+ setTranslationY(updatePositionFromY);
+ if(!isAppearing) {
+ setScaleX(1f);
+ setScaleY(1f);
+ setAlpha(1f);
+ }
+ updatePositionFromX = -1f;
+ updatePositionFromY = -1f;
+ }
+
+ if (relativePositionToSetX >= 0 && floatingMode && getMeasuredWidth() > 0) {
+ setRelativePositionInternal(relativePositionToSetX, relativePositionToSetY, getMeasuredWidth(), getMeasuredHeight(), animated);
+ relativePositionToSetX = -1f;
+ relativePositionToSetY = -1f;
+ }
+ super.dispatchDraw(canvas);
+
+ if (!switchingToFloatingMode && floatingMode != setedFloatingMode) {
+ setFloatingMode(setedFloatingMode, true);
+ }
+ if (switchingToFloatingMode) {
+ invalidate();
+ }
+ }
+
+ private void setRelativePositionInternal(float xRelative, float yRelative, int width, int height, boolean animated) {
+ ViewParent parent = getParent();
+ if (parent == null || !floatingMode || switchingToFloatingMode || !active) {
+ return;
+ }
+
+ float xPoint = leftPadding + (((View) parent).getMeasuredWidth() - leftPadding - rightPadding - width) * xRelative;
+ float yPoint = topPadding + (((View) parent).getMeasuredHeight() - bottomPadding - topPadding - height) * yRelative;
+
+ if (animated) {
+ animate().setListener(null).cancel();
+ animate().scaleX(1f).scaleY(1f)
+ .translationX(xPoint)
+ .translationY(yPoint)
+ .alpha(1f)
+ .setStartDelay(0)
+ .setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
+ } else {
+ if (!alwaysFloating) {
+ animate().setListener(null).cancel();
+ setScaleX(1f);
+ setScaleY(1f);
+ animate().alpha(1f).setDuration(150).start();
+ }
+ setTranslationX(xPoint);
+ setTranslationY(yPoint);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ measuredAsFloatingMode = false;
+ if (floatingMode) {
+ width = (int) (((float) width) * FLOATING_MODE_SCALE);
+ height = (int) (((float) height) * FLOATING_MODE_SCALE);
+ measuredAsFloatingMode = true;
+ } else {
+ if (!switchingToPip) {
+ setTranslationX(0);
+ setTranslationY(0);
+ }
+ }
+ if (delegate != null) {
+ delegate.onChange(toFloatingModeProgress, measuredAsFloatingMode);
+ }
+
+ super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+
+ if (getMeasuredHeight() != lastH && getMeasuredWidth() != lastW) {
+ path.reset();
+ rectF.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
+ path.addRoundRect(rectF, Screen.dp(4), Screen.dp(4), Path.Direction.CW);
+ path.toggleInverseFillType();
+ }
+ lastH = getMeasuredHeight();
+ lastW = getMeasuredWidth();
+
+ updatePadding();
+ }
+
+ private void updatePadding() {
+ leftPadding = Screen.dp(16);
+ rightPadding = Screen.dp(16);
+ topPadding = Screen.dp(166);
+ bottomPadding = Screen.dp(166);
+ }
+
+ @SuppressLint("Recycle")
+ public void setFloatingMode(boolean show, boolean animated) {
+ if (getMeasuredWidth() <= 0 || getVisibility() != View.VISIBLE) {
+ animated = false;
+ }
+ if (!animated) {
+ if (floatingMode != show) {
+ floatingMode = show;
+ setedFloatingMode = show;
+ toFloatingModeProgress = floatingMode ? 1f : 0f;
+ requestLayout();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ invalidateOutline();
+ }
+ }
+ return;
+ }
+ if (switchingToFloatingMode) {
+ setedFloatingMode = show;
+ return;
+ }
+ if (show && !floatingMode) {
+ floatingMode = true;
+ setedFloatingMode = true;
+ updatePadding();
+ if (relativePositionToSetX >= 0) {
+ setRelativePositionInternal(relativePositionToSetX, relativePositionToSetY,
+ (int) (getMeasuredWidth() * FLOATING_MODE_SCALE), (int) (getMeasuredHeight() * FLOATING_MODE_SCALE), false);
+ }
+ floatingMode = false;
+ switchingToFloatingMode = true;
+ float toX = getTranslationX();
+ float toY = getTranslationY();
+ setTranslationX(0);
+ setTranslationY(0);
+ invalidate();
+ if (switchToFloatingModeAnimator != null) {
+ switchToFloatingModeAnimator.cancel();
+ }
+ switchToFloatingModeAnimator = ValueAnimator.ofFloat(toFloatingModeProgress, 1f);
+ switchToFloatingModeAnimator.addUpdateListener(progressUpdateListener);
+ switchToFloatingModeAnimator.setDuration(300);
+ switchToFloatingModeAnimator.start();
+ animate().setListener(null).cancel();
+ animate().scaleX(FLOATING_MODE_SCALE).scaleY(FLOATING_MODE_SCALE)
+ .translationX(toX - (getMeasuredWidth() - getMeasuredWidth() * FLOATING_MODE_SCALE) / 2f)
+ .translationY(toY - (getMeasuredHeight() - getMeasuredHeight() * FLOATING_MODE_SCALE) / 2f)
+ .alpha(1f)
+ .setStartDelay(0)
+ .setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ switchingToFloatingMode = false;
+ floatingMode = true;
+ updatePositionFromX = toX;
+ updatePositionFromY = toY;
+ requestLayout();
+
+ }
+ }).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
+ } else if (!show && floatingMode) {
+ setedFloatingMode = false;
+ float fromX = getTranslationX();
+ float fromY = getTranslationY();
+ updatePadding();
+ floatingMode = false;
+ switchingToFloatingMode = true;
+ requestLayout();
+ animate().setListener(null).cancel();
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ if (!measuredAsFloatingMode) {
+ if (switchToFloatingModeAnimator != null) {
+ switchToFloatingModeAnimator.cancel();
+ }
+ switchToFloatingModeAnimator = ValueAnimator.ofFloat(toFloatingModeProgress, 0f);
+ switchToFloatingModeAnimator.addUpdateListener(progressUpdateListener);
+ switchToFloatingModeAnimator.setDuration(300);
+ switchToFloatingModeAnimator.start();
+
+ float fromXFinal = fromX - (getMeasuredWidth() - getMeasuredWidth() * FLOATING_MODE_SCALE) / 2f;
+ float fromYFinal = fromY - (getMeasuredHeight() - getMeasuredHeight() * FLOATING_MODE_SCALE) / 2f;
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ setTranslationX(fromXFinal);
+ setTranslationY(fromYFinal);
+ setScaleX(FLOATING_MODE_SCALE);
+ setScaleY(FLOATING_MODE_SCALE);
+ animate().setListener(null).cancel();
+ animate().setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ switchingToFloatingMode = false;
+ requestLayout();
+ }
+ }).scaleX(1f).scaleY(1f).translationX(0).translationY(0).alpha(1f).setDuration(300).setStartDelay(0).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
+ } else {
+ floatingMode = false;
+ requestLayout();
+ }
+ return false;
+ }
+ });
+ } else {
+ toFloatingModeProgress = floatingMode ? 1f : 0f;
+ setedFloatingMode = floatingMode;
+ requestLayout();
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent (MotionEvent ev) {
+ return true;
+ }
+
+ long startTime;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ ViewParent parent = getParent();
+ if (!floatingMode || switchingToFloatingMode || !active) {
+ return false;
+ }
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (floatingMode && !switchingToFloatingMode) {
+ startTime = System.currentTimeMillis();
+ starX = event.getX() + getX();
+ starY = event.getY() + getY();
+ animate().setListener(null).cancel();
+ animate().scaleY(1.05f).scaleX(1.05f).alpha(1f).setStartDelay(0).start();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ float dx = event.getX() + getX() - starX;
+ float dy = event.getY() + getY() - starY;
+ if (!moving && (dx * dx + dy * dy) > touchSlop * touchSlop) {
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ moving = true;
+ starX = event.getX() + getX();
+ starY = event.getY() + getY();
+ startMovingFromX = getTranslationX();
+ startMovingFromY = getTranslationY();
+ dx = 0;
+ dy = 0;
+ }
+ if (moving) {
+ setTranslationX(startMovingFromX + dx);
+ setTranslationY(startMovingFromY + dy);
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (parent != null && floatingMode && !switchingToFloatingMode) {
+ parent.requestDisallowInterceptTouchEvent(false);
+ animate().setListener(null).cancel();
+ ViewPropertyAnimator animator = animate().scaleX(1f).scaleY(1f).alpha(1f).setStartDelay(0);
+
+ if (tapListener != null && !moving && System.currentTimeMillis() - startTime < 200) {
+ tapListener.onClick(this);
+ }
+
+ int parentWidth = ((View) getParent()).getMeasuredWidth();
+ int parentHeight = ((View) getParent()).getMeasuredHeight();
+
+ if (getX() < leftPadding) {
+ animator.translationX(leftPadding);
+ } else if (getX() + getMeasuredWidth() > parentWidth - rightPadding) {
+ animator.translationX(parentWidth - getMeasuredWidth() - rightPadding);
+ }
+
+ if (getY() < topPadding) {
+ animator.translationY(topPadding);
+ } else if (getY() + getMeasuredHeight() > parentHeight - bottomPadding) {
+ animator.translationY(parentHeight - getMeasuredHeight() - bottomPadding);
+ }
+ animator.setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
+ }
+ moving = false;
+ break;
+ }
+ return true;
+ }
+
+ public void restoreRelativePosition() {
+ updatePadding();
+ if (savedRelativePositionX >= 0 && !switchingToFloatingMode) {
+ setRelativePositionInternal(savedRelativePositionX, savedRelativePositionY, getMeasuredWidth(), getMeasuredHeight(), true);
+ savedRelativePositionX = -1f;
+ savedRelativePositionY = -1f;
+ }
+ }
+
+ public void saveRelativePosition() {
+ if (getMeasuredWidth() > 0 && relativePositionToSetX < 0) {
+ ViewParent parent = getParent();
+ if (parent == null) {
+ return;
+ }
+ savedRelativePositionX = (getTranslationX() - leftPadding) / (((View) parent).getMeasuredWidth() - leftPadding - rightPadding - getMeasuredWidth());
+ savedRelativePositionY = (getTranslationY() - topPadding) / (((View) parent).getMeasuredHeight() - bottomPadding - topPadding - getMeasuredHeight());
+ savedRelativePositionX = Math.max(0, Math.min(1, savedRelativePositionX));
+ savedRelativePositionY = Math.max(0, Math.min(1, savedRelativePositionY));
+ } else {
+ savedRelativePositionX = -1f;
+ savedRelativePositionY = -1f;
+ }
+ }
+
+ public void setRelativePosition(float x, float y) {
+ ViewParent parent = getParent();
+ if (!floatingMode || parent == null || ((View) parent).getMeasuredWidth() > 0 || getMeasuredWidth() == 0 || getMeasuredHeight() == 0) {
+ relativePositionToSetX = x;
+ relativePositionToSetY = y;
+ } else {
+ setRelativePositionInternal(x, y, getMeasuredWidth(), getMeasuredHeight(), true);
+ }
+ }
+
+ public void setRelativePosition(VoIPFloatingLayout fromLayout) {
+ ViewParent parent = getParent();
+ if (parent == null) {
+ return;
+ }
+ updatePadding();
+
+ float xRelative = (fromLayout.getTranslationX() - leftPadding) / (((View) parent).getMeasuredWidth() - leftPadding - rightPadding - fromLayout.getMeasuredWidth());
+ float yRelative = (fromLayout.getTranslationY() - topPadding) / (((View) parent).getMeasuredHeight() - bottomPadding - topPadding - fromLayout.getMeasuredHeight());
+
+ xRelative = Math.min(1f, Math.max(0, xRelative));
+ yRelative = Math.min(1f, Math.max(0, yRelative));
+
+ setRelativePosition(xRelative, yRelative);
+ }
+
+ public void setOnTapListener(OnClickListener tapListener) {
+ this.tapListener = tapListener;
+ }
+
+ public void setIsActive(boolean value) {
+ active = value;
+ }
+
+ public void setDelegate(VoIPFloatingLayoutDelegate delegate) {
+ this.delegate = delegate;
+ }
+}
diff --git a/app/src/main/java/org/pytgcalls/ntgcallsx/VoIPTextureView.java b/app/src/main/java/org/pytgcalls/ntgcallsx/VoIPTextureView.java
new file mode 100644
index 0000000000..814fbf4556
--- /dev/null
+++ b/app/src/main/java/org/pytgcalls/ntgcallsx/VoIPTextureView.java
@@ -0,0 +1,488 @@
+package org.pytgcalls.ntgcallsx;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Build;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.animation.ValueAnimator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import org.thunderdog.challegram.R;
+import org.thunderdog.challegram.U;
+import org.thunderdog.challegram.charts.CubicBezierInterpolator;
+import org.thunderdog.challegram.charts.LayoutHelper;
+import org.thunderdog.challegram.telegram.TdlibManager;
+import org.thunderdog.challegram.tool.Fonts;
+import org.thunderdog.challegram.tool.Screen;
+import org.webrtc.RendererCommon;
+import org.webrtc.TextureViewRenderer;
+import org.webrtc.VideoFrame;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+
+@SuppressLint("ViewConstructor")
+public class VoIPTextureView extends FrameLayout {
+ boolean isCamera;
+ final boolean applyRotation;
+
+ float roundRadius;
+
+ private boolean screencast;
+ boolean isFirstFrameRendered, runAnimation;
+
+ public final TextureViewRenderer renderer;
+ public final ImageView imageView;
+ public View backgroundView;
+ private FrameLayout screencastView;
+ private ImageView screencastImage;
+ private TextView screencastText;
+
+ public Bitmap cameraLastBitmap;
+ public float stubVisibleProgress = 1f;
+
+ boolean animateOnNextLayout;
+ long animateNextDuration;
+ ArrayList animateOnNextLayoutAnimations = new ArrayList<>();
+ int animateFromHeight;
+ int animateFromWidth;
+
+ float animateFromY;
+ float animateFromX;
+
+ float clipVertical;
+ float clipHorizontal;
+ float currentClipVertical;
+ float currentClipHorizontal;
+
+ float animateFromScale = 1f;
+ float animateFromRendererW;
+
+ public float scaleTextureToFill;
+
+ public static int SCALE_TYPE_NONE = 3;
+ public static int SCALE_TYPE_FILL = 0;
+ public static int SCALE_TYPE_FIT = 1;
+ public static int SCALE_TYPE_ADAPTIVE = 2;
+
+ public int scaleType;
+
+ ValueAnimator currentAnimation;
+
+ boolean clipToTexture;
+ public float animationProgress;
+
+ public static final int TRANSITION_DURATION = 350;
+
+ public VoIPTextureView(@NonNull Context context, boolean isCamera, boolean applyRotation) {
+ this(context, isCamera, applyRotation, true);
+ }
+
+ @SuppressLint("SetTextI18n")
+ public VoIPTextureView(@NonNull Context context, boolean isCamera, boolean applyRotation, boolean applyRoundRadius) {
+ super(context);
+ this.isCamera = isCamera;
+ this.applyRotation = applyRotation;
+ imageView = new ImageView(context);
+ imageView.setVisibility(View.GONE);
+ renderer = new TextureViewRenderer(context) {
+ @Override
+ protected void onSizeChanged(int w, int h, int oldW, int oldH) {
+ super.onSizeChanged(w, h, oldW, oldH);
+ }
+ };
+ renderer.setFpsReduction(30);
+ renderer.setEnableHardwareScaler(true);
+ renderer.setMirror(isCamera);
+ if (!isCamera && applyRotation) {
+ backgroundView = new View(context);
+ backgroundView.setBackgroundColor(0xff1b1f23);
+ addView(backgroundView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT));
+
+ renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
+ addView(renderer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER));
+ } else if (!isCamera) {
+ addView(renderer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER));
+ } else {
+ addView(renderer);
+ }
+
+ addView(imageView);
+
+ screencastView = new FrameLayout(getContext());
+ screencastView.setBackground(getLayerDrawable());
+ //screencastView.setBackgroundColor(0xff1b1f23);
+ addView(screencastView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT));
+ screencastView.setVisibility(GONE);
+
+ screencastImage = new ImageView(getContext());
+ screencastImage.setScaleType(ImageView.ScaleType.CENTER);
+ screencastImage.setImageResource(R.drawable.screencast_big);
+ screencastView.addView(screencastImage, LayoutHelper.createFrame(82, 82, Gravity.CENTER, 0, 0, 0, 60));
+
+ screencastText = new TextView(getContext());
+ screencastText.setText("Sharing your screen");
+ screencastText.setGravity(Gravity.CENTER);
+ screencastText.setLineSpacing(Screen.dp(2), 1.0f);
+ screencastText.setTextColor(0xffffffff);
+ screencastText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
+ screencastText.setTypeface(Fonts.getRobotoMedium());
+ screencastView.addView(screencastText, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 21, 28, 21, 0));
+
+ if (applyRoundRadius) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ setOutlineProvider(new ViewOutlineProvider() {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void getOutline(View view, Outline outline) {
+ if (roundRadius < 1) {
+ outline.setRect((int) currentClipHorizontal, (int) currentClipVertical, (int) (view.getMeasuredWidth() - currentClipHorizontal), (int) (view.getMeasuredHeight() - currentClipVertical));
+ } else {
+ outline.setRoundRect((int) currentClipHorizontal, (int) currentClipVertical, (int) (view.getMeasuredWidth() - currentClipHorizontal), (int) (view.getMeasuredHeight() - currentClipVertical), roundRadius);
+ }
+ }
+ });
+ setClipToOutline(true);
+ }
+ }
+
+ if (isCamera) {
+ if (cameraLastBitmap == null) {
+ try {
+ File file = new File(TdlibManager.getTgvoipDirectory(), "voip_icthumb.jpg");
+ cameraLastBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
+ } catch (Throwable ignore) {
+ }
+ }
+ imageView.setVisibility(View.VISIBLE);
+ showLastFrame();
+ }
+
+ if (!applyRotation) {
+ Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ renderer.setRotation(display.getRotation());
+ }
+
+ }
+
+ private static GradientDrawable getLayerDrawable () {
+ GradientDrawable shape = new GradientDrawable();
+ shape.setShape(GradientDrawable.RECTANGLE);
+ int[] colors = {0xff212E3A, 0xff2B5B4D, 0xff245863, 0xff274558};
+ shape.setGradientType(GradientDrawable.LINEAR_GRADIENT);
+ shape.setOrientation(GradientDrawable.Orientation.BL_TR);
+ shape.setColors(colors);
+ shape.setGradientCenter(0.5f, 0.5f);
+ shape.setUseLevel(true);
+ shape.setCornerRadii(new float[]{0, 0, 0, 0, 0, 0, 0, 0});
+ return shape;
+ }
+
+ public void setIsCamera(boolean isCamera) {
+ renderer.setMirror(isCamera);
+ renderer.setIsCamera(isCamera);
+ this.isCamera = isCamera;
+ imageView.setVisibility(isCamera ? View.VISIBLE : View.GONE);
+ }
+
+ public void showWaitFrame() {
+ if (isCamera) {
+ saveCameraLastBitmap();
+ showLastFrame();
+ isFirstFrameRendered = false;
+ runAnimation = true;
+ stubVisibleProgress = 0f;
+ imageView.invalidate();
+ imageView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void showLastFrame() {
+ if (cameraLastBitmap == null) {
+ imageView.setImageResource(R.drawable.icplaceholder);
+ } else {
+ imageView.setImageBitmap(cameraLastBitmap);
+ }
+ imageView.setScaleType(ImageView.ScaleType.FIT_XY);
+ }
+
+ public void saveCameraLastBitmap() {
+ if (isCamera) {
+ cameraLastBitmap = Bitmap.createBitmap(150, 150, Bitmap.Config.ARGB_8888);
+ cameraLastBitmap = renderer.getBitmap(cameraLastBitmap);
+ U.blurBitmap(cameraLastBitmap, 3, 1);
+ try {
+ File file = new File(TdlibManager.getTgvoipDirectory(), "voip_icthumb.jpg");
+ FileOutputStream stream = new FileOutputStream(file);
+ cameraLastBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
+ stream.close();
+ } catch (Throwable ignored) {}
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(@NonNull Canvas canvas) {
+ super.dispatchDraw(canvas);
+ if (runAnimation && isCamera) {
+ if (isFirstFrameRendered) {
+ stubVisibleProgress -= 16f / 150f;
+ if (stubVisibleProgress <= 0) {
+ stubVisibleProgress = 0;
+ imageView.setVisibility(View.GONE);
+ runAnimation = false;
+ } else {
+ invalidate();
+ imageView.setAlpha(stubVisibleProgress);
+ }
+ } else {
+ if (stubVisibleProgress == 0) {
+ imageView.setVisibility(View.VISIBLE);
+ }
+ stubVisibleProgress += 16f / 150f;
+ if (stubVisibleProgress >= 1) {
+ stubVisibleProgress = 1;
+ imageView.setAlpha(1f);
+ runAnimation = false;
+ } else {
+ invalidate();
+ imageView.setAlpha(stubVisibleProgress);
+ }
+ }
+ }
+ }
+
+ boolean ignoreLayout;
+ @Override
+ public void requestLayout() {
+ if (ignoreLayout) {
+ return;
+ }
+ super.requestLayout();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (!applyRotation) {
+ ignoreLayout = true;
+ Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ renderer.setRotation(display.getRotation());
+ ignoreLayout = false;
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (scaleType == SCALE_TYPE_NONE) {
+ return;
+ }
+
+ if (renderer.getMeasuredHeight() == 0 || renderer.getMeasuredWidth() == 0 || getMeasuredHeight() == 0 || getMeasuredWidth() == 0) {
+ scaleTextureToFill = 1f;
+ if (currentAnimation == null && !animateOnNextLayout) {
+ currentClipHorizontal = 0;
+ currentClipVertical = 0;
+ }
+ } else if (scaleType == SCALE_TYPE_FILL) {
+ scaleTextureToFill = Math.max(getMeasuredHeight() / (float) renderer.getMeasuredHeight(), getMeasuredWidth() / (float) renderer.getMeasuredWidth());
+ } else if (scaleType == SCALE_TYPE_ADAPTIVE) {
+ if (Math.abs(getMeasuredHeight() / (float) getMeasuredWidth() - 1f) < 0.02f) {
+ scaleTextureToFill = Math.max(getMeasuredHeight() / (float) renderer.getMeasuredHeight(), getMeasuredWidth() / (float) renderer.getMeasuredWidth());
+ } else {
+ if (getMeasuredWidth() > getMeasuredHeight() && renderer.getMeasuredHeight() > renderer.getMeasuredWidth()) {
+ scaleTextureToFill = Math.max(getMeasuredHeight() / (float) renderer.getMeasuredHeight(), (getMeasuredWidth() / 2f ) / (float) renderer.getMeasuredWidth());
+ } else {
+ scaleTextureToFill = Math.min(getMeasuredHeight() / (float) renderer.getMeasuredHeight(), getMeasuredWidth() / (float) renderer.getMeasuredWidth());
+ }
+ }
+ } else if (scaleType == SCALE_TYPE_FIT) {
+ scaleTextureToFill = Math.min(getMeasuredHeight() / (float) renderer.getMeasuredHeight(), getMeasuredWidth() / (float) renderer.getMeasuredWidth());
+ if (clipToTexture && !animateWithParent && currentAnimation == null && !animateOnNextLayout) {
+ currentClipHorizontal = (getMeasuredWidth() - renderer.getMeasuredWidth()) / 2f;
+ currentClipVertical = (getMeasuredHeight() - renderer.getMeasuredHeight()) / 2f;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ invalidateOutline();
+ }
+ }
+ }
+
+ if (animateOnNextLayout) {
+ animateFromScale /= renderer.getMeasuredWidth() / animateFromRendererW;
+ animateOnNextLayout = false;
+ float translationY, translationX;
+ if (animateWithParent && getParent() != null) {
+ View parent = (View) getParent();
+ translationY = animateFromY - parent.getTop();
+ translationX = animateFromX - parent.getLeft();
+ } else {
+ translationY = animateFromY - getTop();
+ translationX = animateFromX - getLeft();
+ }
+ clipVertical = 0;
+ clipHorizontal = 0;
+ if (animateFromHeight != getMeasuredHeight()) {
+ clipVertical = (getMeasuredHeight() - animateFromHeight) / 2f;
+ translationY -= clipVertical;
+ }
+ if (animateFromWidth != getMeasuredWidth()) {
+ clipHorizontal = (getMeasuredWidth() - animateFromWidth) / 2f;
+ translationX -= clipHorizontal;
+ }
+ setTranslationY(translationY);
+ setTranslationX(translationX);
+
+ if (currentAnimation != null) {
+ currentAnimation.removeAllListeners();
+ currentAnimation.cancel();
+ }
+ renderer.setScaleX(animateFromScale);
+ renderer.setScaleY(animateFromScale);
+
+ currentClipVertical = clipVertical;
+ currentClipHorizontal = clipHorizontal;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ invalidateOutline();
+ }
+ invalidate();
+ float fromScaleFinal = animateFromScale;
+
+ currentAnimation = ValueAnimator.ofFloat(1f, 0);
+ float finalTranslationX = translationX;
+ float finalTranslationY = translationY;
+ currentAnimation.addUpdateListener(animator -> {
+ float v = (float) animator.getAnimatedValue();
+ animationProgress = (1f - v);
+ currentClipVertical = v * clipVertical;
+ currentClipHorizontal = v * clipHorizontal;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ invalidateOutline();
+ }
+ invalidate();
+
+ float s = fromScaleFinal * v + scaleTextureToFill * (1f - v);
+ renderer.setScaleX(s);
+ renderer.setScaleY(s);
+
+ setTranslationX(finalTranslationX * v);
+ setTranslationY(finalTranslationY * v);
+ });
+ if (animateNextDuration != 0) {
+ currentAnimation.setDuration(animateNextDuration);
+ } else {
+ currentAnimation.setDuration(TRANSITION_DURATION);
+ }
+ currentAnimation.setInterpolator(CubicBezierInterpolator.DEFAULT);
+ currentAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ currentClipVertical = 0;
+ currentClipHorizontal = 0;
+
+ renderer.setScaleX(scaleTextureToFill);
+ renderer.setScaleY(scaleTextureToFill);
+
+ setTranslationY(0);
+ setTranslationX(0);
+
+ currentAnimation = null;
+ }
+ });
+ currentAnimation.start();
+ if (!animateOnNextLayoutAnimations.isEmpty()) {
+ for (int i = 0; i < animateOnNextLayoutAnimations.size(); i++) {
+ animateOnNextLayoutAnimations.get(i).start();
+ }
+ }
+ animateOnNextLayoutAnimations.clear();
+ animateNextDuration = 0;
+ } else {
+ if (currentAnimation == null) {
+ renderer.setScaleX(scaleTextureToFill);
+ renderer.setScaleY(scaleTextureToFill);
+ }
+ }
+ }
+
+ boolean animateWithParent;
+
+ public void setAnimateWithParent(boolean b) {
+ animateWithParent = b;
+ }
+
+ public void stopCapturing() {
+ if (renderer != null) {
+ saveCameraLastBitmap();
+ showLastFrame();
+ isFirstFrameRendered = false;
+ runAnimation = true;
+ renderer.release();
+ imageView.setAlpha(1f);
+ if (isCamera) {
+ imageView.setVisibility(View.VISIBLE);
+ }
+ stubVisibleProgress = 1f;
+ animateFromScale = 1f;
+ }
+ }
+
+ public void onFrame(VideoFrame frame) {
+ if (!isFirstFrameRendered && !runAnimation) {
+ isFirstFrameRendered = true;
+ runAnimation = true;
+ renderer.onFirstFrameRendered();
+ }
+ if (renderer != null) {
+ renderer.onFrame(frame);
+ }
+ }
+
+ public void setIsScreencast(boolean value) {
+ screencast = value;
+ screencastView.setVisibility(screencast ? VISIBLE : GONE);
+ if (screencast) {
+ renderer.setVisibility(GONE);
+ imageView.setVisibility(GONE);
+ } else {
+ renderer.setVisibility(VISIBLE);
+ }
+ }
+
+ public void setScreenShareMiniProgress(float progress, boolean value) {
+ if (!screencast) {
+ return;
+ }
+ float scale = ((View) getParent()).getScaleX();
+ screencastText.setAlpha(1.0f - progress);
+ float sc;
+ if (!value) {
+ sc = 1.0f / scale - 0.4f / scale * progress;
+ } else {
+ sc = 1.0f - 0.4f * progress;
+ }
+ screencastImage.setScaleX(sc);
+ screencastImage.setScaleY(sc);
+ screencastImage.setTranslationY(Screen.dp(60) * progress);
+ }
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/BaseActivity.java b/app/src/main/java/org/thunderdog/challegram/BaseActivity.java
index fcc83a3822..0d1d8b752b 100644
--- a/app/src/main/java/org/thunderdog/challegram/BaseActivity.java
+++ b/app/src/main/java/org/thunderdog/challegram/BaseActivity.java
@@ -2630,7 +2630,7 @@ public Permissions permissions () {
// public static final int REQUEST_WRITE_STORAGE = 0x05;
public static final int REQUEST_FINE_LOCATION = 0x06;
public static final int REQUEST_CUSTOM = 0x07;
- public static final int REQUEST_USE_MIC_CALL = 0x08;
+ public static final int REQUEST_PERMISSION_CALL = 0x08;
public static final int REQUEST_CUSTOM_NEW = 0x09;
public void requestCameraPermission () {
@@ -2651,12 +2651,19 @@ public void requestFingerprintPermission () {
}
}*/
- private ActivityPermissionResult requestMicPermissionCallback;
+ private ActivityPermissionResult requestPermissionCallback;
- public void requestMicPermissionForCall (@Nullable ActivityPermissionResult after) {
+ public void requestPermissionForCall (@Nullable ActivityPermissionResult after) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- this.requestMicPermissionCallback = after;
- requestPermissions(new String[] {Manifest.permission.RECORD_AUDIO}, REQUEST_USE_MIC_CALL);
+ this.requestPermissionCallback = after;
+ ArrayList permissions = new ArrayList<>();
+ if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+ permissions.add(Manifest.permission.CAMERA);
+ }
+ if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ permissions.add(Manifest.permission.RECORD_AUDIO);
+ }
+ requestPermissions(permissions.toArray(new String[0]), REQUEST_PERMISSION_CALL);
}
}
@@ -2751,16 +2758,16 @@ public void onRequestPermissionsResult (int requestCode, @NonNull String[] permi
boolean fallback = false;
switch (requestCode) {
case REQUEST_CUSTOM_NEW:
- case REQUEST_USE_MIC_CALL: {
+ case REQUEST_PERMISSION_CALL: {
ActivityPermissionResult callback =
requestCode == REQUEST_CUSTOM_NEW ?
requestCustomPermissionCallback :
- requestMicPermissionCallback;
+ requestPermissionCallback;
if (callback != null) {
if (requestCode == REQUEST_CUSTOM_NEW) {
requestCustomPermissionCallback = null;
} else {
- requestMicPermissionCallback = null;
+ requestPermissionCallback = null;
}
int grantCount = 0;
for (int grantResult : grantResults) {
diff --git a/app/src/main/java/org/thunderdog/challegram/N.java b/app/src/main/java/org/thunderdog/challegram/N.java
index 475d6b3057..9ffc2982a7 100644
--- a/app/src/main/java/org/thunderdog/challegram/N.java
+++ b/app/src/main/java/org/thunderdog/challegram/N.java
@@ -26,6 +26,7 @@
import androidx.media3.decoder.opus.OpusLibrary;
import androidx.media3.decoder.vp9.VpxLibrary;
+import io.github.pytgcalls.NTgCalls;
import org.thunderdog.challegram.config.Config;
import org.thunderdog.challegram.voip.VoIPController;
import org.webrtc.SoftwareVideoEncoderFactory;
@@ -139,7 +140,15 @@ public Suggestion (String emoji, String label, String replacement) {
public native static void onFatalError (String msg, int cause);
public native static void throwDirect (String msg);
- public static native String[] getTgCallsVersions ();
+ private static native String[] getTgCallsVersions ();
+
+ public static String[] getTgCallsLibVersions () {
+ if (BuildConfig.USE_NTGCALLS) {
+ return NTgCalls.getProtocol().libraryVersions.toArray(new String[0]);
+ } else {
+ return getTgCallsVersions();
+ }
+ }
public static native String toHexString (byte[] array);
public static boolean init () {
diff --git a/app/src/main/java/org/thunderdog/challegram/U.java b/app/src/main/java/org/thunderdog/challegram/U.java
index 1631adc6b6..71443dd8c1 100644
--- a/app/src/main/java/org/thunderdog/challegram/U.java
+++ b/app/src/main/java/org/thunderdog/challegram/U.java
@@ -494,7 +494,11 @@ public static String toHexString (String str) {
return b.toString();
}
- public static void startForeground (Service service, int notificationId, Notification notification) {
+ public static void startForeground(Service service, int notificationId, Notification notification) {
+ startForeground(service, notificationId, notification, false);
+ }
+
+ public static void startForeground (Service service, int notificationId, Notification notification, boolean allowedMediaProjection) {
if (notification == null)
throw new IllegalArgumentException();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -507,7 +511,15 @@ public static void startForeground (Service service, int notificationId, Notific
case TdlibNotificationManager.ID_INCOMING_CALL_NOTIFICATION: {
int knownType = android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- knownType |= android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+ if (UI.getAppContext().checkSelfPermission(android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+ knownType |= android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+ }
+ if (UI.getAppContext().checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
+ knownType |= android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+ }
+ }
+ if (allowedMediaProjection) {
+ knownType |= android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION;
}
knownType |= android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK;
service.startForeground(notificationId, notification, knownType);
diff --git a/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java b/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java
index be0186ed84..00754de17e 100644
--- a/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java
+++ b/app/src/main/java/org/thunderdog/challegram/service/TGCallService.java
@@ -51,6 +51,11 @@
import androidx.core.app.NotificationManagerCompat;
import org.drinkless.tdlib.TdApi;
+import io.github.pytgcalls.FrameCallback;
+import io.github.pytgcalls.RemoteSourceChangeCallback;
+import org.pytgcalls.ntgcallsx.CallInterface;
+import org.pytgcalls.ntgcallsx.NTgCallsInterface;
+import org.pytgcalls.ntgcallsx.TgCallsInterface;
import org.thunderdog.challegram.BuildConfig;
import org.thunderdog.challegram.Log;
import org.thunderdog.challegram.R;
@@ -177,13 +182,25 @@ private void setCallId (Tdlib tdlib, int callId) {
}
private SoundPoolMap soundPoolMap;
- private @Nullable VoIPInstance tgcalls;
+ private @Nullable CallInterface tgcalls;
private @Nullable PrivateCallListener callListener;
private PowerManager.WakeLock cpuWakelock;
private BluetoothAdapter btAdapter;
private boolean isProximityNear, isHeadsetPlugged;
+ public void setFrameCallback(FrameCallback callback) {
+ if (tgcalls != null) {
+ tgcalls.setFrameCallback(callback);
+ }
+ }
+
+ public void setRemoteSourceChangeCallback(RemoteSourceChangeCallback callback) {
+ if (tgcalls != null) {
+ tgcalls.setRemoteSourceChangeCallback(callback);
+ }
+ }
+
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive (Context context, Intent intent) {
@@ -626,12 +643,28 @@ private void setAudioMode (int mode) {
}
private CallSettings postponedCallSettings;
+ boolean lastCameraStatus;
+ boolean lastCameraFrontFacing = true;
+ boolean lastScreenSharing;
@Override
public void onCallSettingsChanged (int callId, CallSettings settings) {
this.postponedCallSettings = settings;
- if (tgcalls != null) {
- tgcalls.setMicDisabled(settings != null && settings.isMicMuted());
+ if (tgcalls != null && settings != null) {
+ tgcalls.setMicDisabled(settings.isMicMuted());
+ if (settings.isCameraSharing() != lastCameraStatus || settings.isCameraFrontFacing() != lastCameraFrontFacing) {
+ lastCameraStatus = settings.isCameraSharing();
+ lastCameraFrontFacing = settings.isCameraFrontFacing();
+ tgcalls.setCameraEnabled(lastCameraStatus, lastCameraFrontFacing);
+ }
+ if (lastScreenSharing != settings.isScreenSharing()) {
+ lastScreenSharing = settings.isScreenSharing();
+ cleanupChannels((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE));
+ U.stopForeground(this, true, TdlibNotificationManager.ID_ONGOING_CALL_NOTIFICATION);
+ ongoingCallNotification = null;
+ showNotification();
+ tgcalls.setScreenShareEnabled(lastScreenSharing);
+ }
}
setAudioMode(settings != null ? settings.getSpeakerMode() : CallSettings.SPEAKER_MODE_EARPIECE);
}
@@ -848,7 +881,7 @@ private void showNotification () {
} else {
ongoingCallNotification = builder.getNotification();
}
- U.startForeground(this, TdlibNotificationManager.ID_ONGOING_CALL_NOTIFICATION, ongoingCallNotification);
+ U.startForeground(this, TdlibNotificationManager.ID_ONGOING_CALL_NOTIFICATION, ongoingCallNotification, lastScreenSharing);
}
// Sound
@@ -1332,8 +1365,8 @@ private void releaseTgCalls (@Nullable Tdlib tdlib, @Nullable TdApi.Call call) {
callInitialized = false; // FIXME?
}
- private boolean isInitiated () {
- return tgcalls != null;
+ public boolean isInitiated () {
+ return tgcalls != null && tgcalls.isInitiated();
}
private void checkInitiated () {
@@ -1375,7 +1408,7 @@ public void onConnectionStateChanged (VoIPInstance context, @CallState int newSt
if (newState == CallState.ESTABLISHED) {
tdlib.dispatchCallStateChanged(call.id, newState);
} else if (newState == CallState.FAILED) {
- long connectionId = context.getConnectionId();
+ long connectionId = context != null ? context.getConnectionId() : 0;
tdlib.context().calls().hangUp(tdlib, call.id, true, connectionId);
}
}
@@ -1390,36 +1423,34 @@ public void onSignallingDataEmitted (byte[] data) {
tdlib.client().send(new TdApi.SendCallSignalingData(call.id, data), tdlib.silentHandler());
}
};
-
- VoIPInstance tgcallsTemp;
try {
- tgcallsTemp = VoIP.instantiateAndConnect(
- tdlib,
- call,
- state,
- stateListener,
- forceTcp,
- callProxy,
- lastNetworkType,
- audioGainControlEnabled,
- echoCancellationStrength,
- isMicDisabled
- );
- } catch (Throwable t) {
- tgcallsTemp = null;
- }
- final VoIPInstance tgcalls = tgcallsTemp;
-
- if (tgcalls != null) {
- this.callListener = new PrivateCallListener() {
- @Override
- public void onNewCallSignalingDataArrived (int callId, byte[] data) {
- tgcalls.handleIncomingSignalingData(data);
+ if (tgcalls == null) {
+ if (BuildConfig.USE_NTGCALLS) {
+ tgcalls = new NTgCallsInterface(tdlib, call, state, stateListener);
+ } else {
+ tgcalls = new TgCallsInterface(
+ tdlib,
+ call,
+ state,
+ stateListener,
+ forceTcp,
+ callProxy,
+ lastNetworkType,
+ audioGainControlEnabled,
+ echoCancellationStrength,
+ isMicDisabled
+ );
}
- };
- tdlib.listeners().subscribeToCallUpdates(call.id, callListener);
- this.tgcalls = tgcalls;
- } else {
+ this.callListener = new PrivateCallListener() {
+ @Override
+ public void onNewCallSignalingDataArrived (int callId, byte[] data) {
+ if (tgcalls != null) tgcalls.handleIncomingSignalingData(data);
+ }
+ };
+ tdlib.listeners().subscribeToCallUpdates(call.id, callListener);
+ }
+ } catch (Throwable e) {
+ Log.e(Log.TAG_VOIP, "Error creating call", e);
hangUp();
}
}
@@ -1434,6 +1465,10 @@ public static TGCallService currentInstance () {
return reference != null ? reference.get() : null;
}
+ public boolean isVideoSupported () {
+ return tgcalls != null && tgcalls.isVideoSupported();
+ }
+
private boolean logViewed;
public static void markLogViewed () {
diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java b/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java
index 160d72302d..1240037c8a 100644
--- a/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java
+++ b/app/src/main/java/org/thunderdog/challegram/telegram/CallManager.java
@@ -287,10 +287,10 @@ public void declineIncomingCall (Tdlib tdlib, int callId, boolean isVideo) {
public boolean checkRecordPermissions (final Context context, final Tdlib tdlib, final @Nullable TdApi.Call call, final long userId, final @Nullable ViewController> makeCallContext) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (UI.getAppContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ if (UI.getAppContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || UI.getAppContext().checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
BaseActivity activity = UI.getUiContext();
if (activity != null) {
- activity.requestMicPermissionForCall((code, permissions, grantResults, grantCount) -> {
+ activity.requestPermissionForCall((code, permissions, grantResults, grantCount) -> {
if (grantCount == permissions.length) {
if (makeCallContext != null) {
makeCall(makeCallContext, userId, null, false);
diff --git a/app/src/main/java/org/thunderdog/challegram/ui/CallController.java b/app/src/main/java/org/thunderdog/challegram/ui/CallController.java
index a83b221b03..3daf489cd4 100644
--- a/app/src/main/java/org/thunderdog/challegram/ui/CallController.java
+++ b/app/src/main/java/org/thunderdog/challegram/ui/CallController.java
@@ -14,12 +14,23 @@
*/
package org.thunderdog.challegram.ui;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
import android.content.Context;
+import android.animation.LayoutTransition;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Canvas;
import android.graphics.RectF;
+import android.graphics.RenderEffect;
+import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
+import android.media.projection.MediaProjectionManager;
+import android.os.Build;
import android.os.SystemClock;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
@@ -38,14 +49,21 @@
import androidx.annotation.Nullable;
import org.drinkless.tdlib.TdApi;
+import io.github.pytgcalls.AndroidUtils;
+import io.github.pytgcalls.devices.JavaVideoCapturerModule;
+import io.github.pytgcalls.media.StreamDevice;
+import org.pytgcalls.ntgcallsx.VoIPFloatingLayout;
+import org.pytgcalls.ntgcallsx.VoIPTextureView;
import org.thunderdog.challegram.BaseActivity;
import org.thunderdog.challegram.BuildConfig;
import org.thunderdog.challegram.Log;
import org.thunderdog.challegram.R;
import org.thunderdog.challegram.U;
+import org.thunderdog.challegram.charts.CubicBezierInterpolator;
import org.thunderdog.challegram.core.Lang;
import org.thunderdog.challegram.data.TD;
import org.thunderdog.challegram.emoji.Emoji;
+import org.thunderdog.challegram.navigation.ActivityResultHandler;
import org.thunderdog.challegram.navigation.ViewController;
import org.thunderdog.challegram.service.TGCallService;
import org.thunderdog.challegram.support.ViewSupport;
@@ -65,12 +83,21 @@
import org.thunderdog.challegram.util.RateLimiter;
import org.thunderdog.challegram.util.text.TextColorSetOverride;
import org.thunderdog.challegram.util.text.TextColorSets;
+import org.thunderdog.challegram.voip.annotation.CallState;
import org.thunderdog.challegram.voip.gui.CallSettings;
import org.thunderdog.challegram.widget.AvatarView;
import org.thunderdog.challegram.widget.EmojiTextView;
import org.thunderdog.challegram.widget.TextView;
import org.thunderdog.challegram.widget.voip.CallControlsLayout;
+import org.webrtc.JavaI420Buffer;
+import org.webrtc.RendererCommon;
+import org.webrtc.TextureViewRenderer;
+import org.webrtc.VideoFrame;
+import java.util.ArrayList;
+import java.util.function.Function;
+
+import io.github.pytgcalls.media.StreamStatus;
import me.vkryl.android.AnimatorUtils;
import me.vkryl.android.ScrimUtil;
import me.vkryl.android.ViewUtils;
@@ -82,8 +109,9 @@
import me.vkryl.core.MathUtils;
import me.vkryl.core.StringUtils;
-public class CallController extends ViewController implements TdlibCache.UserDataChangeListener, TdlibCache.CallStateChangeListener, View.OnClickListener, FactorAnimator.Target, Runnable, CallControlsLayout.CallControlCallback, Screen.StatusBarHeightChangeListener {
+public class CallController extends ViewController implements TdlibCache.UserDataChangeListener, TdlibCache.CallStateChangeListener, View.OnClickListener, FactorAnimator.Target, Runnable, CallControlsLayout.CallControlCallback, ActivityResultHandler, Screen.StatusBarHeightChangeListener {
private static final boolean DEBUG_FADE_BRANDING = true;
+ private static final int SCREEN_CAPTURE_REQUEST_CODE = 1001;
private static class ButtonView extends View implements FactorAnimator.Target {
private Drawable icon;
@@ -219,6 +247,9 @@ public int getId () {
private LinearLayout brandWrap;
private TextView debugView;
private CallStrengthView strengthView;
+ private TextureViewRenderer callingUserMiniTextureRenderer;
+ private VoIPFloatingLayout callingUserMiniFloatingLayout, currentUserCameraFloatingLayout;
+ private VoIPTextureView currentUserTextureView, callingUserTextureView;
private static class CallStrengthView extends View {
private final ViewHandler handler;
@@ -271,8 +302,9 @@ protected void onDraw (Canvas c) {
private TextView emojiViewSmall, emojiViewBig, emojiViewHint;
private CallControlsLayout callControlsLayout;
- private FrameLayoutFix buttonWrap;
- private ButtonView muteButtonView, speakerButtonView;
+ private LinearLayout buttonWrap;
+ private ButtonView muteButtonView, speakerButtonView, videoButtonView, flipCameraButtonView;
+ private LinearLayout videoButtonContainer, flipCameraButtonContainer, messageButtonContainer, otherOptionsContainer, speakerButtonContainer;
private float lastHeaderFactor;
@@ -394,6 +426,74 @@ protected void onDraw(Canvas c){
avatarView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
contentView.addView(avatarView);
+ callingUserTextureView = new VoIPTextureView(context, false, true, false);
+ callingUserTextureView.renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
+ callingUserTextureView.renderer.setEnableHardwareScaler(true);
+ callingUserTextureView.renderer.setRotateTextureWithScreen(true);
+ callingUserTextureView.scaleType = VoIPTextureView.SCALE_TYPE_FIT;
+ callingUserTextureView.setVisibility(View.GONE);
+ contentView.addView(callingUserTextureView);
+
+ currentUserCameraFloatingLayout = new VoIPFloatingLayout(context);
+ currentUserCameraFloatingLayout.setDelegate((progress, value) -> currentUserTextureView.setScreenShareMiniProgress(progress, value));
+ currentUserCameraFloatingLayout.setRelativePosition(1f, 1f);
+ currentUserCameraFloatingLayout.setVisibility(View.GONE);
+ currentUserCameraFloatingLayout.setTag(VoIPFloatingLayout.STATE_GONE);
+ currentUserCameraFloatingLayout.setOnTapListener(view -> {
+ if (callSettings == null) {
+ callSettings = new CallSettings(tdlib, call.id);
+ }
+ if (callSettings.getRemoteCameraState() != VoIPFloatingLayout.STATE_GONE && callSettings.getLocalCameraState() == VoIPFloatingLayout.STATE_FLOATING) {
+ callSettings.setLocalCameraState(VoIPFloatingLayout.STATE_FULLSCREEN);
+ callSettings.setRemoteCameraState(VoIPFloatingLayout.STATE_FLOATING);
+ currentUserCameraFloatingLayout.saveRelativePosition();
+ callingUserMiniFloatingLayout.saveRelativePosition();
+ callingUserMiniFloatingLayout.setRelativePosition(currentUserCameraFloatingLayout);
+ showFloatingLayout(true);
+ showMiniFloatingLayout(true);
+ currentUserCameraFloatingLayout.restoreRelativePosition();
+ callingUserMiniFloatingLayout.restoreRelativePosition();
+ }
+ });
+
+ currentUserTextureView = new VoIPTextureView(context, true, false);
+ currentUserTextureView.renderer.setUseCameraRotation(true);
+ currentUserCameraFloatingLayout.addView(currentUserTextureView);
+ contentView.addView(currentUserCameraFloatingLayout, FrameLayoutFix.newParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ callingUserMiniFloatingLayout = new VoIPFloatingLayout(context);
+ callingUserMiniFloatingLayout.alwaysFloating = true;
+ callingUserMiniFloatingLayout.setRelativePosition(1f, 1f);
+ callingUserMiniFloatingLayout.setFloatingMode(true, false);
+ callingUserMiniFloatingLayout.setVisibility(View.GONE);
+ callingUserMiniFloatingLayout.setOnTapListener(view -> {
+ if (callSettings == null) {
+ callSettings = new CallSettings(tdlib, call.id);
+ }
+ if (callSettings.getRemoteCameraState() != VoIPFloatingLayout.STATE_GONE && callSettings.getLocalCameraState() == VoIPFloatingLayout.STATE_FULLSCREEN) {
+ callSettings.setLocalCameraState(VoIPFloatingLayout.STATE_FLOATING);
+ callSettings.setRemoteCameraState(VoIPFloatingLayout.STATE_FULLSCREEN);
+ currentUserCameraFloatingLayout.saveRelativePosition();
+ callingUserMiniFloatingLayout.saveRelativePosition();
+ currentUserCameraFloatingLayout.setRelativePosition(callingUserMiniFloatingLayout);
+ showFloatingLayout(true);
+ showMiniFloatingLayout(true);
+ currentUserCameraFloatingLayout.restoreRelativePosition();
+ callingUserMiniFloatingLayout.restoreRelativePosition();
+ }
+ });
+
+ callingUserMiniTextureRenderer = new TextureViewRenderer(context);
+ callingUserMiniTextureRenderer.setEnableHardwareScaler(true);
+ callingUserMiniTextureRenderer.setFpsReduction(30);
+ callingUserMiniTextureRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
+
+ View backgroundView = new View(context);
+ backgroundView.setBackgroundColor(0xff1b1f23);
+ callingUserMiniFloatingLayout.addView(backgroundView, FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ callingUserMiniFloatingLayout.addView(callingUserMiniTextureRenderer, FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
+
+ contentView.addView(callingUserMiniFloatingLayout);
FrameLayoutFix.LayoutParams params = FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// Top-left corner
@@ -602,16 +702,25 @@ public boolean onTouchEvent (MotionEvent event) {
// Call settings buttons
+ LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(Screen.dp(72f), Screen.dp(72f));
+
+ ButtonView otherOptions = new ButtonView(context);
+ otherOptions.setId(R.id.btn_other_options);
+ otherOptions.setOnClickListener(this);
+ otherOptions.setIcon(R.drawable.baseline_more_vert_24);
+ otherOptions.setLayoutParams(buttonParams);
+
muteButtonView = new ButtonView(context);
muteButtonView.setId(R.id.btn_mute);
muteButtonView.setOnClickListener(this);
muteButtonView.setIcon(R.drawable.baseline_mic_24);
muteButtonView.setNeedCross(true);
- muteButtonView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(72f), Screen.dp(72f), Gravity.LEFT | Gravity.BOTTOM));
+ muteButtonView.setLayoutParams(buttonParams);
ButtonView messageButtonView = new ButtonView(context);
messageButtonView.setId(R.id.btn_openChat);
messageButtonView.setOnClickListener(this);
+ messageButtonView.setVisibility(View.VISIBLE);
messageButtonView.setIcon(R.drawable.baseline_chat_bubble_24);
messageButtonView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(72f), Screen.dp(72f), Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM));
@@ -619,13 +728,57 @@ public boolean onTouchEvent (MotionEvent event) {
speakerButtonView.setId(R.id.btn_speaker);
speakerButtonView.setOnClickListener(this);
speakerButtonView.setIcon(R.drawable.baseline_volume_up_24);
- speakerButtonView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(72f), Screen.dp(72f), Gravity.RIGHT | Gravity.BOTTOM));
+ speakerButtonView.setLayoutParams(buttonParams);
+
+ videoButtonView = new ButtonView(context);
+ videoButtonView.setId(R.id.btn_camera);
+ videoButtonView.setOnClickListener(this);
+ videoButtonView.setIcon(R.drawable.baseline_videocam_24);
+ videoButtonView.setLayoutParams(buttonParams);
+
+ flipCameraButtonView = new ButtonView(context);
+ flipCameraButtonView.setId(R.id.btn_flip_camera);
+ flipCameraButtonView.setOnClickListener(this);
+ flipCameraButtonView.setIcon(R.drawable.baseline_camera_front_24);
+ flipCameraButtonView.setLayoutParams(buttonParams);
+
+ Function wrapButton = (ButtonView buttonView) -> {
+ LinearLayout buttonWrap = new LinearLayout(context);
+ buttonWrap.setOrientation(LinearLayout.HORIZONTAL);
+ buttonWrap.setGravity(Gravity.CENTER);
+ buttonWrap.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
+ buttonWrap.addView(buttonView);
+ return buttonWrap;
+ };
- buttonWrap = new FrameLayoutFix(context);
- buttonWrap.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(76f), Gravity.BOTTOM));
- buttonWrap.addView(muteButtonView);
- buttonWrap.addView(messageButtonView);
- buttonWrap.addView(speakerButtonView);
+ videoButtonContainer = wrapButton.apply(videoButtonView);
+ videoButtonContainer.setVisibility(View.GONE);
+
+ messageButtonContainer = wrapButton.apply(messageButtonView);
+ otherOptionsContainer = wrapButton.apply(otherOptions);
+ otherOptionsContainer.setVisibility(View.GONE);
+
+ flipCameraButtonContainer = wrapButton.apply(flipCameraButtonView);
+ flipCameraButtonContainer.setVisibility(View.GONE);
+
+ speakerButtonContainer = wrapButton.apply(speakerButtonView);
+
+ buttonWrap = new LinearLayout(context);
+ LayoutTransition layoutTransition = new LayoutTransition();
+ layoutTransition.enableTransitionType(LayoutTransition.APPEARING);
+ layoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
+ layoutTransition.setDuration(LayoutTransition.APPEARING, 300);
+ layoutTransition.setDuration(LayoutTransition.DISAPPEARING, 300);
+ buttonWrap.setLayoutTransition(layoutTransition);
+
+ buttonWrap.setGravity(Gravity.CENTER);
+ buttonWrap.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(76f), Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL));
+ buttonWrap.addView(otherOptionsContainer);
+ buttonWrap.addView(videoButtonContainer);
+ buttonWrap.addView(flipCameraButtonContainer);
+ buttonWrap.addView(speakerButtonContainer);
+ buttonWrap.addView(messageButtonContainer);
+ buttonWrap.addView(wrapButton.apply(muteButtonView));
Views.setPaddingBottom(buttonWrap, extraBottomInset);
Drawable drawable = ScrimUtil.makeCubicGradientScrimDrawable(0xff000000, 2, Gravity.BOTTOM, false);
drawable.setAlpha((int) (255f * .3f));
@@ -651,15 +804,53 @@ public boolean onTouchEvent (MotionEvent event) {
setTexts();
updateCallState();
+ TGCallService service = TGCallService.currentInstance();
+ if (service != null) {
+ if (service.isInitiated()) {
+ if (service.isVideoSupported()) {
+ videoButtonContainer.setVisibility(View.VISIBLE);
+ }
+ otherOptionsContainer.setVisibility(View.VISIBLE);
+ messageButtonContainer.setVisibility(View.GONE);
+ }
+ setupVideoFrameListeners(service);
+ }
+
if (callSettings != null) {
muteButtonView.setIsActive(callSettings.isMicMuted(), false);
speakerButtonView.setIsActive(callSettings.isSpeakerModeEnabled(), false);
+
+ if (callSettings.isScreenSharing()) {
+ videoButtonView.setIcon(R.drawable.baseline_share_arrow_24);
+ otherOptionsContainer.setVisibility(View.GONE);
+ messageButtonContainer.setVisibility(View.VISIBLE);
+ }
+
+ videoButtonView.setIsActive((callSettings.getLocalCameraState() != VoIPFloatingLayout.STATE_GONE || callSettings.isScreenSharing()), false);
+ flipCameraButtonView.setIsActive(!callSettings.isCameraFrontFacing(), false);
+ flipCameraButtonView.setIcon(callSettings != null && !callSettings.isCameraFrontFacing() ? R.drawable.baseline_camera_rear_24 : R.drawable.baseline_camera_front_24);
+ if (callSettings.getLocalCameraState() != VoIPFloatingLayout.STATE_GONE) {
+ currentUserTextureView.renderer.init(JavaVideoCapturerModule.getSharedEGLContext(), null);
+ }
+ if (callSettings.isCameraSharing()) {
+ speakerButtonContainer.setVisibility(View.GONE);
+ flipCameraButtonContainer.setVisibility(View.VISIBLE);
+ }
+ if (callSettings.getRemoteCameraState() != VoIPFloatingLayout.STATE_GONE) {
+ callingUserTextureView.renderer.init(JavaVideoCapturerModule.getSharedEGLContext(), null);
+ callingUserMiniTextureRenderer.init(JavaVideoCapturerModule.getSharedEGLContext(), null);
+ callingUserTextureView.setVisibility(View.VISIBLE);
+ }
+ currentUserTextureView.renderer.setMirror(callSettings.isCameraFrontFacing() && callSettings.isCameraSharing());
+ currentUserTextureView.setIsCamera(callSettings.isCameraSharing());
+ currentUserTextureView.setIsScreencast(callSettings.isScreenSharing());
+ showFloatingLayout(false);
+ showMiniFloatingLayout(false);
}
return contentView;
}
-
private void setTexts () {
if (emojiStatusHelper != null) {
this.emojiStatusHelper.updateEmoji(tdlib, user, new TextColorSetOverride(TextColorSets.Regular.NORMAL) {
@@ -815,6 +1006,10 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca
@Override
public void onClick (View v) {
final int viewId = v.getId();
+ handleMenuClick(viewId);
+ }
+
+ public void handleMenuClick (int viewId) {
if (viewId == R.id.btn_emoji) {
if (isEmojiVisible) {
setEmojiExpanded(true);
@@ -824,10 +1019,8 @@ public void onClick (View v) {
if (callSettings == null) {
callSettings = new CallSettings(tdlib, call.id);
}
- callSettings.setMicMuted(((ButtonView) v).toggleActive());
+ callSettings.setMicMuted(!callSettings.isMicMuted());
}
- } else if (viewId == R.id.btn_openChat) {
- tdlib.ui().openPrivateChat(this, call.userId, null);
} else if (viewId == R.id.btn_speaker) {
if (!TD.isFinished(call)) {
if (callSettings == null) {
@@ -839,7 +1032,297 @@ public void onClick (View v) {
callSettings.toggleSpeakerMode(this);
}
}
+ } else if (viewId == R.id.btn_camera) {
+ if (!TD.isFinished(call)) {
+ if (callSettings == null) {
+ callSettings = new CallSettings(tdlib, call.id);
+ }
+ callSettings.setLocalCameraState(callSettings.getLocalCameraState() == VoIPFloatingLayout.STATE_GONE ? callSettings.getAvailableLocalCameraState(): VoIPFloatingLayout.STATE_GONE);
+ if (callSettings.getLocalCameraState() != VoIPFloatingLayout.STATE_GONE) {
+ currentUserTextureView.renderer.setMirror(callSettings.isCameraFrontFacing());
+ currentUserTextureView.setIsCamera(true);
+ currentUserTextureView.setIsScreencast(false);
+ currentUserTextureView.renderer.init(JavaVideoCapturerModule.getSharedEGLContext(), null);
+ } else {
+ currentUserTextureView.stopCapturing();
+ }
+
+ if (callSettings.isScreenSharing()) {
+ callSettings.setScreenSharing(false);
+ videoButtonView.setIcon(R.drawable.baseline_videocam_24);
+ otherOptionsContainer.setVisibility(View.VISIBLE);
+ messageButtonContainer.setVisibility(View.GONE);
+ } else {
+ callSettings.setCameraSharing(!callSettings.isCameraSharing());
+ flipCameraButtonContainer.setVisibility(callSettings.isCameraSharing() ? View.VISIBLE : View.GONE);
+ speakerButtonContainer.setVisibility(callSettings.isCameraSharing() ? View.GONE : View.VISIBLE);
+ }
+ showFloatingLayout(true);
+ }
+ } else if (viewId == R.id.btn_other_options) {
+ if (navigationController != null) {
+ if (callSettings == null) {
+ callSettings = new CallSettings(tdlib, call.id);
+ }
+ ArrayList ids = new ArrayList<>();
+ ArrayList titles = new ArrayList<>();
+ ArrayList icons = new ArrayList<>();
+ if (canShowScreenSharing()) {
+ ids.add(R.id.btn_screenCapture);
+ titles.add("Screen Sharing");
+ icons.add(R.drawable.baseline_share_arrow_24);
+ }
+ ids.add(R.id.btn_openChat);
+ titles.add("Send Message");
+ icons.add(R.drawable.baseline_chat_bubble_24);
+ if (callSettings.isCameraSharing()) {
+ ids.add(R.id.btn_speaker);
+ switch (callSettings.getSpeakerMode()) {
+ case CallSettings.SPEAKER_MODE_EARPIECE:
+ titles.add(Lang.getString(R.string.VoipAudioRoutingEarpiece));
+ icons.add(R.drawable.baseline_phone_in_talk_24);
+ break;
+ case CallSettings.SPEAKER_MODE_SPEAKER:
+ case CallSettings.SPEAKER_MODE_SPEAKER_DEFAULT:
+ titles.add(Lang.getString(R.string.VoipAudioRoutingSpeaker));
+ icons.add(R.drawable.baseline_volume_up_24);
+ break;
+ case CallSettings.SPEAKER_MODE_BLUETOOTH:
+ titles.add(Lang.getString(R.string.VoipAudioRoutingBluetooth));
+ icons.add(R.drawable.baseline_bluetooth_24);
+ break;
+ }
+ }
+ showOptions(null, ids.stream().mapToInt(Integer::intValue).toArray(), titles.toArray(new String[0]), null, icons.stream().mapToInt(Integer::intValue).toArray(), (itemView, id) -> {
+ handleMenuClick(id);
+ return true;
+ });
+ }
+ } else if (viewId == R.id.btn_openChat) {
+ tdlib.ui().openPrivateChat(this, call.userId, null);
+ } else if (viewId == R.id.btn_flip_camera) {
+ if (!TD.isFinished(call)) {
+ if (callSettings == null) {
+ callSettings = new CallSettings(tdlib, call.id);
+ }
+ callSettings.setCameraFrontFacing(!callSettings.isCameraFrontFacing());
+ currentUserTextureView.showWaitFrame();
+ currentUserTextureView.renderer.setMirror(callSettings.isCameraFrontFacing());
+ }
+ } else if (viewId == R.id.btn_screenCapture) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return;
+ }
+ MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) UI.getContext().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+ UI.startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), SCREEN_CAPTURE_REQUEST_CODE);
+ }
+ }
+
+ private boolean canShowScreenSharing() {
+ TGCallService service = TGCallService.currentInstance();
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP && !callSettings.isCameraSharing() && !callSettings.isScreenSharing() && service != null && service.isVideoSupported();
+ }
+
+ @Override
+ public void onActivityResult (int requestCode, int resultCode, Intent data) {
+ if (requestCode == SCREEN_CAPTURE_REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK && !callSettings.isCameraSharing()) {
+ JavaVideoCapturerModule.setMediaProjectionPermissionResult(data);
+ if (callSettings == null) {
+ callSettings = new CallSettings(tdlib, call.id);
+ }
+ callSettings.setScreenSharing(true);
+ videoButtonView.setIcon(R.drawable.baseline_share_arrow_24);
+ videoButtonView.setIsActive(true, true);
+ callSettings.setLocalCameraState(callSettings.getAvailableLocalCameraState());
+ otherOptionsContainer.setVisibility(View.GONE);
+ messageButtonContainer.setVisibility(View.VISIBLE);
+ currentUserTextureView.renderer.init(JavaVideoCapturerModule.getSharedEGLContext(), null);
+ currentUserTextureView.setIsCamera(false);
+ currentUserTextureView.setIsScreencast(true);
+ showFloatingLayout(true);
+ }
+ }
+ }
+
+
+ private void showMiniFloatingLayout(boolean animated) {
+ var state = callSettings.getRemoteCameraState();
+ if (state == VoIPFloatingLayout.STATE_FLOATING) {
+ callingUserMiniFloatingLayout.setIsActive(true);
+ if (animated) {
+ if (callingUserMiniFloatingLayout.getVisibility() != View.VISIBLE) {
+ callingUserMiniFloatingLayout.setVisibility(View.VISIBLE);
+ callingUserMiniFloatingLayout.setAlpha(0f);
+ callingUserMiniFloatingLayout.setScaleX(0.5f);
+ callingUserMiniFloatingLayout.setScaleY(0.5f);
+ }
+ callingUserMiniFloatingLayout.animate().setListener(null).cancel();
+ callingUserMiniFloatingLayout.isAppearing = true;
+ callingUserMiniFloatingLayout.animate()
+ .alpha(1f).scaleX(1f).scaleY(1f)
+ .setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).setStartDelay(150)
+ .withEndAction(() -> {
+ callingUserMiniFloatingLayout.isAppearing = false;
+ callingUserMiniFloatingLayout.invalidate();
+ }).start();
+ } else {
+ callingUserMiniFloatingLayout.setAlpha(1f);
+ callingUserMiniFloatingLayout.setScaleX(1f);
+ callingUserMiniFloatingLayout.setScaleY(1f);
+ callingUserMiniFloatingLayout.setVisibility(View.VISIBLE);
+ }
+ callingUserMiniFloatingLayout.setTag(1);
+ } else if (state == VoIPFloatingLayout.STATE_FULLSCREEN) {
+ callingUserMiniFloatingLayout.setIsActive(false);
+ if (animated) {
+ callingUserMiniFloatingLayout.animate().alpha(0).scaleX(0.5f).scaleY(0.5f).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (callingUserMiniFloatingLayout.getTag() == null) {
+ callingUserMiniFloatingLayout.setVisibility(View.GONE);
+ }
+ }
+ }).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
+ } else {
+ callingUserMiniFloatingLayout.setVisibility(View.GONE);
+ }
+ callingUserMiniFloatingLayout.setTag(null);
+ }
+ }
+
+ private Animator cameraShowingAnimator;
+ private void showFloatingLayout(boolean animated) {
+ var state = callSettings.getLocalCameraState();
+ if (!animated && cameraShowingAnimator != null) {
+ cameraShowingAnimator.removeAllListeners();
+ cameraShowingAnimator.cancel();
+ }
+ if (state == VoIPFloatingLayout.STATE_GONE) {
+ if (animated) {
+ if (currentUserCameraFloatingLayout.getTag() != null && (int) currentUserCameraFloatingLayout.getTag() != VoIPFloatingLayout.STATE_GONE) {
+ if (cameraShowingAnimator != null) {
+ cameraShowingAnimator.removeAllListeners();
+ cameraShowingAnimator.cancel();
+ }
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(
+ ObjectAnimator.ofFloat(currentUserCameraFloatingLayout, View.ALPHA, currentUserCameraFloatingLayout.getAlpha(), 0)
+ );
+ if (currentUserCameraFloatingLayout.getTag() != null && (int) currentUserCameraFloatingLayout.getTag() == VoIPFloatingLayout.STATE_FLOATING) {
+ animatorSet.playTogether(
+ ObjectAnimator.ofFloat(currentUserCameraFloatingLayout, View.SCALE_X, currentUserCameraFloatingLayout.getScaleX(), 0.7f),
+ ObjectAnimator.ofFloat(currentUserCameraFloatingLayout, View.SCALE_Y, currentUserCameraFloatingLayout.getScaleX(), 0.7f)
+ );
+ }
+ cameraShowingAnimator = animatorSet;
+ cameraShowingAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ currentUserCameraFloatingLayout.setTranslationX(0);
+ currentUserCameraFloatingLayout.setTranslationY(0);
+ currentUserCameraFloatingLayout.setScaleY(1f);
+ currentUserCameraFloatingLayout.setScaleX(1f);
+ currentUserCameraFloatingLayout.setVisibility(View.GONE);
+ }
+ });
+ cameraShowingAnimator.setDuration(250).setInterpolator(CubicBezierInterpolator.DEFAULT);
+ cameraShowingAnimator.setStartDelay(50);
+ cameraShowingAnimator.start();
+ }
+ } else {
+ currentUserCameraFloatingLayout.setVisibility(View.GONE);
+ }
+ } else {
+ boolean switchToFloatAnimated = animated;
+ if (currentUserCameraFloatingLayout.getTag() == null || (int) currentUserCameraFloatingLayout.getTag() == VoIPFloatingLayout.STATE_GONE) {
+ switchToFloatAnimated = false;
+ }
+ if (animated) {
+ if (currentUserCameraFloatingLayout.getTag() != null && (int) currentUserCameraFloatingLayout.getTag() == VoIPFloatingLayout.STATE_GONE) {
+ if (currentUserCameraFloatingLayout.getVisibility() == View.GONE) {
+ currentUserCameraFloatingLayout.setAlpha(0f);
+ currentUserCameraFloatingLayout.setScaleX(0.7f);
+ currentUserCameraFloatingLayout.setScaleY(0.7f);
+ currentUserCameraFloatingLayout.setVisibility(View.VISIBLE);
+ }
+ if (cameraShowingAnimator != null) {
+ cameraShowingAnimator.removeAllListeners();
+ cameraShowingAnimator.cancel();
+ }
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(
+ ObjectAnimator.ofFloat(currentUserCameraFloatingLayout, View.ALPHA, 0.0f, 1f),
+ ObjectAnimator.ofFloat(currentUserCameraFloatingLayout, View.SCALE_X, 0.7f, 1f),
+ ObjectAnimator.ofFloat(currentUserCameraFloatingLayout, View.SCALE_Y, 0.7f, 1f)
+ );
+ cameraShowingAnimator = animatorSet;
+ cameraShowingAnimator.setDuration(150).start();
+ }
+ } else {
+ currentUserCameraFloatingLayout.setVisibility(View.VISIBLE);
+ }
+ if ((currentUserCameraFloatingLayout.getTag() == null || (int) currentUserCameraFloatingLayout.getTag() != VoIPFloatingLayout.STATE_FLOATING) && currentUserCameraFloatingLayout.relativePositionToSetX < 0) {
+ currentUserCameraFloatingLayout.setRelativePosition(1f, 1f);
+ }
+ currentUserCameraFloatingLayout.setFloatingMode(state == VoIPFloatingLayout.STATE_FLOATING, switchToFloatAnimated);
}
+ currentUserCameraFloatingLayout.setTag(state);
+ }
+
+ private void setupVideoFrameListeners(TGCallService service) {
+ service.setRemoteSourceChangeCallback((chatId, remoteSource) -> AndroidUtils.runOnUIThread(() -> {
+ if (remoteSource.device == StreamDevice.CAMERA || remoteSource.device == StreamDevice.SCREEN) {
+ var currentUserActive = callSettings.getLocalCameraState() != VoIPFloatingLayout.STATE_GONE;
+ if (remoteSource.state == StreamStatus.ACTIVE) {
+ callSettings.setRemoteCameraState(VoIPFloatingLayout.STATE_FULLSCREEN);
+ callingUserTextureView.setVisibility(View.VISIBLE);
+ callingUserTextureView.renderer.init(JavaVideoCapturerModule.getSharedEGLContext(), null);
+ callingUserMiniTextureRenderer.init(JavaVideoCapturerModule.getSharedEGLContext(), null);
+ if (currentUserActive) callSettings.setLocalCameraState(VoIPFloatingLayout.STATE_FLOATING);
+ } else if (remoteSource.state == StreamStatus.IDLING) {
+ callSettings.setRemoteCameraState(VoIPFloatingLayout.STATE_GONE);
+ callingUserTextureView.setVisibility(View.GONE);
+ callingUserTextureView.stopCapturing();
+ callingUserMiniTextureRenderer.release();
+ if (currentUserActive) callSettings.setLocalCameraState(VoIPFloatingLayout.STATE_FULLSCREEN);
+ }
+ if (currentUserActive) showFloatingLayout(true);
+ showMiniFloatingLayout(true);
+ }
+ }));
+ service.setFrameCallback((chatId, streamMode, streamDevice, frameList) -> {
+ var isVideo = streamDevice == StreamDevice.CAMERA || streamDevice == StreamDevice.SCREEN;
+ if (isVideo) {
+ var rawFrame = frameList.get(0);
+ int ySize = rawFrame.frameData.width * rawFrame.frameData.height;
+ int uvSize = ySize / 4;
+ var i420Buffer = JavaI420Buffer.allocate(rawFrame.frameData.width, rawFrame.frameData.height);
+ i420Buffer.getDataY().put(rawFrame.data, 0, ySize).flip();
+ i420Buffer.getDataU().put(rawFrame.data, ySize, uvSize).flip();
+ i420Buffer.getDataV().put(rawFrame.data, ySize + uvSize, uvSize).flip();
+ VideoFrame frame = new VideoFrame(i420Buffer, rawFrame.frameData.rotation, System.nanoTime());
+
+ switch (streamMode) {
+ case CAPTURE:
+ if (callSettings.getLocalCameraState() != VoIPFloatingLayout.STATE_GONE) {
+ currentUserTextureView.onFrame(frame);
+ }
+ break;
+ case PLAYBACK:
+ switch (callSettings.getRemoteCameraState()) {
+ case VoIPFloatingLayout.STATE_FULLSCREEN:
+ callingUserTextureView.onFrame(frame);
+ break;
+ case VoIPFloatingLayout.STATE_FLOATING:
+ callingUserMiniTextureRenderer.onFrame(frame);
+ break;
+ }
+ break;
+ }
+ i420Buffer.release();
+ }
+ });
}
@Override
@@ -892,6 +1375,22 @@ private void updateCall (TdApi.Call call) {
public void onCallStateChanged (final int callId, final int newState) {
if (!isDestroyed()) {
updateCallState();
+ if (newState == CallState.ESTABLISHED) {
+ AndroidUtils.runOnUIThread(() -> {
+ TGCallService service = TGCallService.currentInstance();
+ if (service != null) {
+ if (service.isVideoSupported()) {
+ videoButtonContainer.setVisibility(View.VISIBLE);
+ }
+ otherOptionsContainer.setVisibility(View.VISIBLE);
+ messageButtonContainer.setVisibility(View.GONE);
+ if (callSettings == null) {
+ callSettings = new CallSettings(tdlib, call.id);
+ }
+ setupVideoFrameListeners(service);
+ }
+ });
+ }
}
}
@@ -992,13 +1491,15 @@ private void updateCallButtons () {
if (buttonWrap != null) {
muteButtonView.setIsActive(callSettings != null && callSettings.isMicMuted(), isFocused());
speakerButtonView.setIsActive(callSettings != null && callSettings.isSpeakerModeEnabled(), isFocused());
+ videoButtonView.setIsActive(callSettings != null && callSettings.getLocalCameraState() != VoIPFloatingLayout.STATE_GONE, isFocused());
+ flipCameraButtonView.setIsActive(callSettings != null && !callSettings.isCameraFrontFacing(), isFocused());
+ flipCameraButtonView.setIcon(callSettings != null && !callSettings.isCameraFrontFacing() ? R.drawable.baseline_camera_rear_24 : R.drawable.baseline_camera_front_24);
}
}
private void updateEmoji () {
boolean emojiVisible = (call.state.getConstructor() == TdApi.CallStateReady.CONSTRUCTOR);
if (emojiVisible && StringUtils.isEmpty(emojiViewSmall.getText())) {
-
TdApi.CallStateReady ready = (TdApi.CallStateReady) call.state;
StringBuilder b = new StringBuilder();
diff --git a/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java b/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java
index e84224274f..f836962953 100644
--- a/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java
+++ b/app/src/main/java/org/thunderdog/challegram/voip/VoIP.java
@@ -24,6 +24,8 @@
import androidx.annotation.Nullable;
import org.drinkless.tdlib.TdApi;
+import io.github.pytgcalls.NTgCalls;
+import org.thunderdog.challegram.BuildConfig;
import org.thunderdog.challegram.Log;
import org.thunderdog.challegram.N;
import org.thunderdog.challegram.config.Config;
@@ -309,7 +311,7 @@ public static TdApi.CallServer[] modifyCallServers (TdApi.CallServer[] servers)
public static String[] getAvailableVersions (boolean allowFilter) {
String tgVoipVersion = VoIPController.getVersion();
- String[] tgCallsVersions = N.getTgCallsVersions();
+ String[] tgCallsVersions = N.getTgCallsLibVersions();
Set versions = new LinkedHashSet<>();
if (!allowFilter || !isForceDisabled(tgVoipVersion)) {
@@ -332,13 +334,14 @@ public static String[] getAvailableVersions (boolean allowFilter) {
}
public static TdApi.CallProtocol getProtocol () {
+ var protocol = NTgCalls.getProtocol();
return new TdApi.CallProtocol(
- true,
- true,
- Config.VOIP_CONNECTION_MIN_LAYER,
- VoIPController.getConnectionMaxLayer(),
- getAvailableVersions(true)
- );
+ protocol.udpP2P,
+ protocol.udpReflector,
+ protocol.minLayer,
+ protocol.maxLayer,
+ protocol.libraryVersions.toArray(new String[0])
+ );
}
private static int getNativeBufferSize (Context context) {
@@ -354,8 +357,10 @@ private static int getNativeBufferSize (Context context) {
public static void initialize (Context context) {
ContextUtils.initialize(context);
- int bufferSize = getNativeBufferSize(context);
- VoIPController.setNativeBufferSize(bufferSize);
+ if (!BuildConfig.USE_NTGCALLS) {
+ int bufferSize = getNativeBufferSize(context);
+ VoIPController.setNativeBufferSize(bufferSize);
+ }
}
public static VoIPInstance instantiateAndConnect (
@@ -371,7 +376,7 @@ public static VoIPInstance instantiateAndConnect (
boolean isMicDisabled
) throws IllegalArgumentException {
final String libtgvoipVersion = VoIPController.getVersion();
- final String[] tgCallsVersions = N.getTgCallsVersions();
+ final String[] tgCallsVersions = N.getTgCallsLibVersions();
final VoIPLogs.Pair logFiles = VoIPLogs.getNewFile(true);
tdlib.storeCallLogInformation(call, logFiles);
diff --git a/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java b/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java
index 7d71ca67f8..6ab4a73952 100755
--- a/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java
+++ b/app/src/main/java/org/thunderdog/challegram/voip/VoIPController.java
@@ -235,7 +235,8 @@ public String getLibraryVersion () {
}
public static String getVersion () {
- return nativeGetVersion();
+ //return nativeGetVersion();
+ return "1.0.0";
}
@Override
diff --git a/app/src/main/java/org/thunderdog/challegram/voip/VoIPServerConfig.java b/app/src/main/java/org/thunderdog/challegram/voip/VoIPServerConfig.java
index a28e372d73..7961bad3de 100644
--- a/app/src/main/java/org/thunderdog/challegram/voip/VoIPServerConfig.java
+++ b/app/src/main/java/org/thunderdog/challegram/voip/VoIPServerConfig.java
@@ -2,6 +2,7 @@
import org.json.JSONException;
import org.json.JSONObject;
+import org.thunderdog.challegram.BuildConfig;
import org.thunderdog.challegram.Log;
/**
@@ -15,7 +16,9 @@ public class VoIPServerConfig{
public static void setConfig(String json){
try{
config=new JSONObject(json);
- nativeSetConfig(json);
+ if (!BuildConfig.USE_NTGCALLS) {
+ nativeSetConfig(json);
+ }
}catch(JSONException x){
Log.e(Log.TAG_VOIP, "Error parsing VoIP config", x);
}
diff --git a/app/src/main/java/org/thunderdog/challegram/voip/gui/CallSettings.java b/app/src/main/java/org/thunderdog/challegram/voip/gui/CallSettings.java
index e4edb833d7..7bd1835768 100644
--- a/app/src/main/java/org/thunderdog/challegram/voip/gui/CallSettings.java
+++ b/app/src/main/java/org/thunderdog/challegram/voip/gui/CallSettings.java
@@ -21,6 +21,7 @@
import org.thunderdog.challegram.navigation.ViewController;
import org.thunderdog.challegram.service.TGCallService;
import org.thunderdog.challegram.telegram.Tdlib;
+import org.pytgcalls.ntgcallsx.VoIPFloatingLayout;
public class CallSettings {
public static final int SPEAKER_MODE_EARPIECE = 0;
@@ -32,7 +33,11 @@ public class CallSettings {
private final int callId;
private boolean micMuted;
+ private boolean screenSharing;
+ private boolean cameraSharing;
+ private boolean cameraFrontFacing = true;
private int speakerMode;
+ private int localCameraState = VoIPFloatingLayout.STATE_GONE, remoteCameraState = VoIPFloatingLayout.STATE_GONE;
public CallSettings (Tdlib tdlib, int callId) {
this.tdlib = tdlib;
@@ -54,6 +59,36 @@ public boolean isMicMuted () {
return micMuted;
}
+ public int getLocalCameraState () {
+ return localCameraState;
+ }
+
+ public int getRemoteCameraState () {
+ return remoteCameraState;
+ }
+
+ public void setCameraSharing (boolean cameraSharing) {
+ if (this.cameraSharing != cameraSharing) {
+ this.cameraSharing = cameraSharing;
+ tdlib.cache().onUpdateCallSettings(callId, this);
+ }
+ }
+
+ public boolean isCameraSharing () {
+ return cameraSharing;
+ }
+
+ public void setCameraFrontFacing (boolean cameraFrontFacing) {
+ if (this.cameraFrontFacing != cameraFrontFacing) {
+ this.cameraFrontFacing = cameraFrontFacing;
+ tdlib.cache().onUpdateCallSettings(callId, this);
+ }
+ }
+
+ public boolean isCameraFrontFacing () {
+ return cameraFrontFacing;
+ }
+
private boolean isCallActive () {
TdApi.Call call = tdlib.cache().getCall(callId);
return call != null && !TD.isFinished(call);
@@ -66,6 +101,35 @@ public void setSpeakerMode (int mode) {
}
}
+ public void setLocalCameraState (int state) {
+ if (localCameraState != state) {
+ localCameraState = state;
+ tdlib.cache().onUpdateCallSettings(callId, this);
+ }
+ }
+
+ public void setRemoteCameraState (int state) {
+ if (remoteCameraState != state) {
+ remoteCameraState = state;
+ tdlib.cache().onUpdateCallSettings(callId, this);
+ }
+ }
+
+ public int getAvailableLocalCameraState () {
+ return remoteCameraState == VoIPFloatingLayout.STATE_FULLSCREEN ? VoIPFloatingLayout.STATE_FLOATING : VoIPFloatingLayout.STATE_FULLSCREEN;
+ }
+
+ public void setScreenSharing (boolean screenSharing) {
+ if (this.screenSharing != screenSharing) {
+ this.screenSharing = screenSharing;
+ tdlib.cache().onUpdateCallSettings(callId, this);
+ }
+ }
+
+ public boolean isScreenSharing () {
+ return screenSharing;
+ }
+
public int getSpeakerMode () {
return speakerMode;
}
diff --git a/app/src/main/res/drawable-hdpi/screencast_big.png b/app/src/main/res/drawable-hdpi/screencast_big.png
new file mode 100644
index 0000000000..ee253786e4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/screencast_big.png differ
diff --git a/app/src/main/res/drawable-mdpi/screencast_big.png b/app/src/main/res/drawable-mdpi/screencast_big.png
new file mode 100644
index 0000000000..b06eb86141
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/screencast_big.png differ
diff --git a/app/src/main/res/drawable-xhdpi/screencast_big.png b/app/src/main/res/drawable-xhdpi/screencast_big.png
new file mode 100644
index 0000000000..4a3c034520
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/screencast_big.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/screencast_big.png b/app/src/main/res/drawable-xxhdpi/screencast_big.png
new file mode 100644
index 0000000000..7d269ec1c8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/screencast_big.png differ
diff --git a/app/src/main/res/drawable/icplaceholder.jpg b/app/src/main/res/drawable/icplaceholder.jpg
new file mode 100644
index 0000000000..5de9839e6f
Binary files /dev/null and b/app/src/main/res/drawable/icplaceholder.jpg differ
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index f2848aa6e6..bc9fb1285f 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -365,6 +365,7 @@
+
@@ -379,6 +380,7 @@
+
diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt
index 9b518a31db..aea36e2388 100644
--- a/buildSrc/src/main/kotlin/Config.kt
+++ b/buildSrc/src/main/kotlin/Config.kt
@@ -64,6 +64,7 @@ data class ApplicationConfig(
val isHuaweiBuild: Boolean,
val forceOptimize: Boolean,
val doNotObfuscate: Boolean,
+ val useNTgCalls: Boolean,
val compileSdkVersion: Int,
val targetSdkVersion: Int,
diff --git a/buildSrc/src/main/kotlin/tgx/gradle/plugin/ConfigurationPlugin.kt b/buildSrc/src/main/kotlin/tgx/gradle/plugin/ConfigurationPlugin.kt
index dc3d6ee3f4..98a6c3de45 100644
--- a/buildSrc/src/main/kotlin/tgx/gradle/plugin/ConfigurationPlugin.kt
+++ b/buildSrc/src/main/kotlin/tgx/gradle/plugin/ConfigurationPlugin.kt
@@ -68,6 +68,8 @@ open class ConfigurationPlugin : Plugin {
val isExperimentalBuild = isExampleBuild || keystore == null || properties.getProperty("app.experimental", "false") == "true"
val doNotObfuscate = isExampleBuild || properties.getProperty("app.dontobfuscate", "false") == "true"
val forceOptimize = properties.getProperty("app.forceoptimize") == "true"
+
+ val useNTgCalls = properties.getProperty("app.ntgcalls", "false") == "true"
val appExtension = getOrSample("tgx.extension")
if (appExtension != "none" && appExtension != "hms") {
error("Unknown tgx.extension: $appExtension")
@@ -128,6 +130,7 @@ open class ConfigurationPlugin : Plugin {
isHuaweiBuild,
forceOptimize,
doNotObfuscate,
+ useNTgCalls,
compileSdkVersion,
targetSdkVersion,
buildToolsVersion,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9325e75a17..a62ff637a2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -102,6 +102,7 @@ transcoder = "ba8f098c94"
sunrise-sunset-calculator = "1.2"
subsampling-scale-image-view = "3.10.0"
mp4parser-isoparser = "1.0.6" # TODO: Upgrade to 1.1.22
+pytgcalls-ntgcalls = "2.1.0"
[libraries]
desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugaring" }
@@ -230,6 +231,7 @@ transcoder = { module = "com.github.natario1:Transcoder", version.ref = "transco
sunriseSunsetCalculator = { module = "com.luckycatlabs:SunriseSunsetCalculator", version.ref = "sunrise-sunset-calculator" }
subsamplingScaleImageView = { module = "com.davemorrissey.labs:subsampling-scale-image-view-androidx", version.ref = "subsampling-scale-image-view" }
mp4parser-isoparser = { module = "com.googlecode.mp4parser:isoparser", version.ref = "mp4parser-isoparser" }
+pytgcalls-ntgcalls = { module = "io.github.pytgcalls:ntgcalls", version.ref = "pytgcalls-ntgcalls" }
checkerframework = { module = "org.checkerframework:checker-qual", version.ref = "checkerframework" }
diff --git a/tgcalls/build.gradle.kts b/tgcalls/build.gradle.kts
index adeed25c58..fbf9e9f0be 100644
--- a/tgcalls/build.gradle.kts
+++ b/tgcalls/build.gradle.kts
@@ -1,9 +1,12 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.kotlin.android)
+ id("tgx-config")
id("tgx-module")
}
+val config = extra["config"] as ApplicationConfig
+
dependencies {
implementation(libs.androidx.annotation)
}
@@ -24,14 +27,16 @@ android {
}
sourceSets.getByName("main") {
- val webrtcDir = "./../app/jni/tgvoip/third_party/webrtc"
- java.srcDirs(
- "${webrtcDir}/rtc_base/java/src",
- "${webrtcDir}/modules/audio_device/android/java/src",
- "${webrtcDir}/sdk/android/api",
- "${webrtcDir}/sdk/android/src/java",
- "../thirdparty/WebRTC/src/java"
- )
+ if (!config.useNTgCalls) {
+ val webrtcDir = "./../app/jni/tgvoip/third_party/webrtc"
+ java.srcDirs(
+ "${webrtcDir}/rtc_base/java/src",
+ "${webrtcDir}/modules/audio_device/android/java/src",
+ "${webrtcDir}/sdk/android/api",
+ "${webrtcDir}/sdk/android/src/java",
+ "../thirdparty/WebRTC/src/java"
+ )
+ }
}
namespace = "tgx.tgcalls"
diff --git a/tgcalls/consumer-rules.pro b/tgcalls/consumer-rules.pro
index 0373941364..66682a98dd 100644
--- a/tgcalls/consumer-rules.pro
+++ b/tgcalls/consumer-rules.pro
@@ -13,4 +13,9 @@
}
-keep class org.webrtc.** { *; }
--keepclassmembers class org.webrtc.** { *; }
\ No newline at end of file
+-keepclassmembers class org.webrtc.** { *; }
+-keep class org.jni_zero.** { *; }
+-keepclassmembers class org.jni_zero.** { *; }
+
+-keep class io.github.pytgcalls.** { *; }
+-keepclassmembers class io.github.pytgcalls.** { *; }
\ No newline at end of file