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