mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-14 21:15:37 +00:00
android: ability to hide active call (#3770)
* android: ability to hide active call * enhancements * fixed some problems and adapted to lock screen usage * change * reduce diff * dealing with disable PiP by user * fix back action * fix hidden information on view rotation while info collapsed * better info showing * status bar color and user icon * reorder * experiment * icon placement * enhancements * back button * invitation accepted state handling * awesome background work * better service interaction and UI * disabled call overlay when call ends and ability to accept a new call from the same contact while previous call is not ended * incomming call alert * enhancements * text * text2 * top area * faster ending call * a lot of enhancements * paddings * icon position * move icon --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
committed by
GitHub
parent
09bbaa1c94
commit
5da8aef794
@@ -103,11 +103,14 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
|
||||
<activity android:name=".views.call.IncomingCallActivity"
|
||||
<activity android:name=".views.call.CallActivity"
|
||||
android:showOnLockScreen="true"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask"/>
|
||||
android:launchMode="singleInstance"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@@ -133,6 +136,18 @@
|
||||
android:stopWithTask="false"></service>
|
||||
|
||||
<!-- SimplexService restart on reboot -->
|
||||
|
||||
<service
|
||||
android:name=".CallService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<receiver
|
||||
android:name=".CallService$CallActionReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".SimplexService$StartReceiver"
|
||||
android:enabled="true"
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.*
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.simplex.app.model.NtfManager.EndCallAction
|
||||
import chat.simplex.app.views.call.CallActivity
|
||||
import chat.simplex.common.model.NotificationPreviewMode
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.CallState
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
class CallService: Service() {
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var notificationManager: NotificationManager? = null
|
||||
private var serviceNotification: Notification? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "onStartCommand startId: $startId")
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
Log.d(TAG, "intent action $action")
|
||||
when (action) {
|
||||
Action.START.name -> startService()
|
||||
else -> Log.e(TAG, "No action in the intent")
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "null intent. Probably restarted by the system.")
|
||||
}
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Call service created")
|
||||
notificationManager = createNotificationChannel()
|
||||
updateNotification()
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "Call service destroyed")
|
||||
try {
|
||||
wakeLock?.let {
|
||||
while (it.isHeld) it.release() // release all, in case acquired more than once
|
||||
}
|
||||
wakeLock = null
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
Log.d(TAG, "CallService startService")
|
||||
if (wakeLock != null) return
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
|
||||
acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotification() {
|
||||
val call = chatModel.activeCall.value
|
||||
val previewMode = appPreferences.notificationPreviewMode.get()
|
||||
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
generalGetString(MR.strings.notification_preview_somebody)
|
||||
else
|
||||
call?.contact?.profile?.displayName ?: ""
|
||||
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
|
||||
val image = call?.contact?.image
|
||||
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
BitmapFactory.decodeResource(resources, R.drawable.icon)
|
||||
else
|
||||
base64ToBitmap(image).asAndroidBitmap()
|
||||
|
||||
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel(): NotificationManager? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel = NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, CALL_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
return notificationManager
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun createNotification(title: String, text: String, icon: Bitmap, connectedAt: Instant? = null): Notification {
|
||||
val pendingIntent: PendingIntent = Intent(this, CallActivity::class.java).let { notificationIntent ->
|
||||
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
val endCallPendingIntent: PendingIntent = Intent(this, CallActionReceiver::class.java).let { notificationIntent ->
|
||||
notificationIntent.setAction(EndCallAction)
|
||||
PendingIntent.getBroadcast(this, 1, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(this, CALL_NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ntf_icon)
|
||||
.setLargeIcon(icon)
|
||||
.setColor(0x88FFFF)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setSilent(true)
|
||||
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.call_service_notification_end_call), endCallPendingIntent)
|
||||
if (connectedAt != null) {
|
||||
builder.setUsesChronometer(true)
|
||||
builder.setWhen(connectedAt.epochSeconds * 1000)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return CallServiceBinder()
|
||||
}
|
||||
|
||||
inner class CallServiceBinder : Binder() {
|
||||
fun getService() = this@CallService
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
}
|
||||
|
||||
class CallActionReceiver: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
EndCallAction -> {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
withBGApi {
|
||||
chatModel.callManager.endCall(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Unknown action. Make sure you provided an action")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "CALL_SERVICE"
|
||||
const val CALL_NOTIFICATION_CHANNEL_ID = "chat.simplex.app.CALL_SERVICE_NOTIFICATION"
|
||||
const val CALL_NOTIFICATION_CHANNEL_NAME = "SimpleX Chat call service"
|
||||
const val CALL_SERVICE_ID = 6788
|
||||
const val WAKE_LOCK_TAG = "CallService::lock"
|
||||
|
||||
fun startService(): Intent {
|
||||
Log.d(TAG, "CallService start")
|
||||
return Intent(androidAppContext, CallService::class.java).also {
|
||||
it.action = Action.START.name
|
||||
ContextCompat.startForegroundService(androidAppContext, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopService() {
|
||||
androidAppContext.stopService(Intent(androidAppContext, CallService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.app.Application
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import chat.simplex.common.platform.Log
|
||||
import android.app.UiModeManager
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import androidx.lifecycle.*
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.model.NtfManager.AcceptCallAction
|
||||
import chat.simplex.app.views.call.CallActivity
|
||||
import chat.simplex.common.helpers.APPLICATION_ID
|
||||
import chat.simplex.common.helpers.requiresIgnoringBattery
|
||||
import chat.simplex.common.model.*
|
||||
@@ -18,6 +19,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.ui.theme.DefaultTheme
|
||||
import chat.simplex.common.views.call.RcvCallInvitation
|
||||
import chat.simplex.common.views.call.activeCallDestroyWebView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
@@ -184,6 +186,10 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
SimplexService.safeStopService()
|
||||
}
|
||||
|
||||
override fun androidCallServiceSafeStop() {
|
||||
CallService.stopService()
|
||||
}
|
||||
|
||||
override fun androidNotificationsModeChanged(mode: NotificationsMode) {
|
||||
if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) {
|
||||
appPrefs.backgroundServiceNoticeShown.set(false)
|
||||
@@ -254,6 +260,28 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
uiModeManager.setApplicationNightMode(mode)
|
||||
}
|
||||
|
||||
override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) {
|
||||
val context = mainActivity.get() ?: return
|
||||
val intent = Intent(context, CallActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
if (acceptCall) {
|
||||
intent.setAction(AcceptCallAction)
|
||||
.putExtra("remoteHostId", remoteHostId)
|
||||
.putExtra("chatId", chatId)
|
||||
}
|
||||
intent.flags += Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun androidPictureInPictureAllowed(): Boolean {
|
||||
val appOps = androidAppContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||
return appOps.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), packageName) == AppOpsManager.MODE_ALLOWED
|
||||
}
|
||||
|
||||
override fun androidCallEnded() {
|
||||
activeCallDestroyWebView()
|
||||
}
|
||||
|
||||
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
|
||||
if (SimplexService.isBackgroundRestricted()) {
|
||||
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
|
||||
|
||||
@@ -34,12 +34,13 @@ import kotlin.system.exitProcess
|
||||
|
||||
class SimplexService: Service() {
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var isStartingService = false
|
||||
private var isCheckingNewMessages = false
|
||||
private var notificationManager: NotificationManager? = null
|
||||
private var serviceNotification: Notification? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "onStartCommand startId: $startId")
|
||||
isServiceStarting = false
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
Log.d(TAG, "intent action $action")
|
||||
@@ -71,6 +72,7 @@ class SimplexService: Service() {
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
} else {
|
||||
isServiceStarting = false
|
||||
isServiceStarted = true
|
||||
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||
@@ -89,6 +91,7 @@ class SimplexService: Service() {
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
|
||||
}
|
||||
isServiceStarting = false
|
||||
isServiceStarted = false
|
||||
stopAfterStart = false
|
||||
saveServiceState(this, ServiceState.STOPPED)
|
||||
@@ -101,9 +104,9 @@ class SimplexService: Service() {
|
||||
|
||||
private fun startService() {
|
||||
Log.d(TAG, "SimplexService startService")
|
||||
if (wakeLock != null || isStartingService) return
|
||||
if (wakeLock != null || isCheckingNewMessages) return
|
||||
val self = this
|
||||
isStartingService = true
|
||||
isCheckingNewMessages = true
|
||||
withLongRunningApi {
|
||||
val chatController = ChatController
|
||||
waitDbMigrationEnds(chatController)
|
||||
@@ -123,7 +126,7 @@ class SimplexService: Service() {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isStartingService = false
|
||||
isCheckingNewMessages = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,6 +265,7 @@ class SimplexService: Service() {
|
||||
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
|
||||
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
|
||||
|
||||
var isServiceStarting = false
|
||||
var isServiceStarted = false
|
||||
private var stopAfterStart = false
|
||||
|
||||
@@ -281,7 +285,7 @@ class SimplexService: Service() {
|
||||
fun safeStopService() {
|
||||
if (isServiceStarted) {
|
||||
androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java))
|
||||
} else {
|
||||
} else if (isServiceStarting) {
|
||||
stopAfterStart = true
|
||||
}
|
||||
}
|
||||
@@ -291,6 +295,7 @@ class SimplexService: Service() {
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(androidAppContext, SimplexService::class.java).also {
|
||||
it.action = action.name
|
||||
isServiceStarting = true
|
||||
ContextCompat.startForegroundService(androidAppContext, it)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.core.app.*
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.views.call.IncomingCallActivity
|
||||
import chat.simplex.app.views.call.CallActivity
|
||||
import chat.simplex.app.views.call.getKeyguardManager
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
@@ -33,6 +33,7 @@ object NtfManager {
|
||||
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2"
|
||||
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
|
||||
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
||||
const val EndCallAction: String = "chat.simplex.app.END_CALL"
|
||||
const val CallNotificationId: Int = -1
|
||||
private const val UserIdKey: String = "userId"
|
||||
private const val ChatIdKey: String = "chatId"
|
||||
@@ -157,7 +158,7 @@ object NtfManager {
|
||||
val screenOff = displayManager.displays.all { it.state != Display.STATE_ON }
|
||||
var ntfBuilder =
|
||||
if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
|
||||
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
|
||||
val fullScreenIntent = Intent(context, CallActivity::class.java)
|
||||
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
NotificationCompat.Builder(context, CallChannel)
|
||||
.setFullScreenIntent(fullScreenPendingIntent, true)
|
||||
|
||||
+193
-35
@@ -1,17 +1,18 @@
|
||||
package chat.simplex.app.views.call
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import chat.simplex.common.platform.Log
|
||||
import android.view.WindowManager
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Rect
|
||||
import android.os.*
|
||||
import android.util.Rational
|
||||
import android.view.*
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.trackPipAnimationHintView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -22,33 +23,115 @@ import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.model.NtfManager.AcceptCallAction
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.app.model.NtfManager.OpenChatAction
|
||||
import chat.simplex.common.platform.ntfManager
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import java.lang.ref.WeakReference
|
||||
import chat.simplex.common.platform.chatModel as m
|
||||
|
||||
class IncomingCallActivity: ComponentActivity() {
|
||||
class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
|
||||
var boundService: CallService? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent { IncomingCallActivityView(ChatModel) }
|
||||
unlockForIncomingCall()
|
||||
callActivity = WeakReference(this)
|
||||
when (intent?.action) {
|
||||
AcceptCallAction -> {
|
||||
val remoteHostId = intent.getLongExtra("remoteHostId", -1).takeIf { it != -1L }
|
||||
val chatId = intent.getStringExtra("chatId")
|
||||
val invitation = (m.callInvitations.values + m.activeCallInvitation.value).lastOrNull {
|
||||
it?.remoteHostId == remoteHostId && it?.contact?.id == chatId
|
||||
}
|
||||
if (invitation != null) {
|
||||
m.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent { CallActivityView() }
|
||||
|
||||
if (isOnLockScreenNow()) {
|
||||
unlockForIncomingCall()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lockAfterIncomingCall()
|
||||
if (isOnLockScreenNow()) {
|
||||
lockAfterIncomingCall()
|
||||
}
|
||||
try {
|
||||
unbindService(this)
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Unable to unbind service: " + e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOnLockScreenNow() = getKeyguardManager(this).isKeyguardLocked
|
||||
|
||||
fun setPipParams(video: Boolean, sourceRectHint: Rect? = null, viewRatio: Rational? = null) {
|
||||
// By manually specifying source rect we exclude empty background while toggling PiP
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(viewRatio)
|
||||
.setSourceRectHint(sourceRectHint)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(video)
|
||||
}
|
||||
setPictureInPictureParams(builder.build())
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
m.activeCallViewIsCollapsed.value = isInPictureInPictureMode
|
||||
val layoutType = if (!isInPictureInPictureMode) {
|
||||
LayoutType.Default
|
||||
} else {
|
||||
LayoutType.RemoteVideo
|
||||
}
|
||||
m.callCommand.add(WCallCommand.Layout(layoutType))
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (isOnLockScreenNow()) {
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
m.activeCallViewIsCollapsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureRequested(): Boolean {
|
||||
Log.d(TAG, "Requested picture-in-picture from the system")
|
||||
return super.onPictureInPictureRequested()
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
// On Android 12+ PiP is enabled automatically when a user hides the app
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
m.activeCallViewIsCollapsed.value = false
|
||||
}
|
||||
|
||||
private fun unlockForIncomingCall() {
|
||||
@@ -72,6 +155,23 @@ class IncomingCallActivity: ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun startServiceAndBind() {
|
||||
/**
|
||||
* On Android 12 there is a bug that prevents starting activity after pressing back button
|
||||
* (the error says that it denies to start activity in background).
|
||||
* Workaround is to bind to a service
|
||||
* */
|
||||
bindService(CallService.startService(), this, 0)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
boundService = (service as CallService.CallServiceBinder).getService()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
boundService = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
|
||||
}
|
||||
@@ -80,38 +180,96 @@ class IncomingCallActivity: ComponentActivity() {
|
||||
fun getKeyguardManager(context: Context): KeyguardManager =
|
||||
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
|
||||
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
|
||||
|
||||
@Composable
|
||||
fun IncomingCallActivityView(m: ChatModel) {
|
||||
fun CallActivityView() {
|
||||
val switchingCall = m.switchingCall.value
|
||||
val invitation = m.activeCallInvitation.value
|
||||
val call = m.activeCall.value
|
||||
val call = remember { m.activeCall }.value
|
||||
val showCallView = m.showCallView.value
|
||||
val activity = LocalContext.current as Activity
|
||||
LaunchedEffect(invitation, call, switchingCall, showCallView) {
|
||||
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
|
||||
Log.d(TAG, "IncomingCallActivityView: finishing activity")
|
||||
activity.finish()
|
||||
}
|
||||
val activity = LocalContext.current as CallActivity
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { m.activeCallViewIsCollapsed.value }
|
||||
.collect { collapsed ->
|
||||
when {
|
||||
collapsed -> {
|
||||
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
|
||||
activity.moveTaskToBack(true)
|
||||
activity.startActivity(Intent(activity, MainActivity::class.java))
|
||||
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
|
||||
// User pressed back button, show MainActivity
|
||||
activity.startActivity(Intent(activity, MainActivity::class.java))
|
||||
activity.enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
|
||||
// PiP disabled by user
|
||||
platform.androidStartCallActivity(false)
|
||||
}
|
||||
activity.isInPictureInPictureMode -> {
|
||||
platform.androidStartCallActivity(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SimpleXTheme {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
if (showCallView) {
|
||||
Box {
|
||||
ActiveCallView()
|
||||
if (invitation != null) IncomingCallAlertView(invitation, m)
|
||||
var prevCall by remember { mutableStateOf(call) }
|
||||
KeyChangeEffect(m.activeCall.value) {
|
||||
if (m.activeCall.value != null) {
|
||||
prevCall = m.activeCall.value
|
||||
activity.boundService?.updateNotification()
|
||||
}
|
||||
}
|
||||
Box(Modifier.background(Color.Black)) {
|
||||
if (call != null) {
|
||||
val view = LocalView.current
|
||||
ActiveCallView()
|
||||
if (callSupportsVideo()) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
|
||||
activity.trackPipAnimationHintView(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (prevCall != null) {
|
||||
prevCall?.let { ActiveCallOverlayDisabled(it) }
|
||||
}
|
||||
if (invitation != null) {
|
||||
if (call == null) {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background,
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
IncomingCallLockScreenAlert(invitation, m)
|
||||
}
|
||||
} else {
|
||||
IncomingCallAlertView(invitation, m)
|
||||
}
|
||||
} else if (invitation != null) {
|
||||
IncomingCallLockScreenAlert(invitation, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(call == null) {
|
||||
if (call != null) {
|
||||
activity.startServiceAndBind()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(invitation, call, switchingCall, showCallView) {
|
||||
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
|
||||
Log.d(TAG, "CallActivityView: finishing activity")
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Related to lockscreen
|
||||
* */
|
||||
|
||||
@Composable
|
||||
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||
val cm = chatModel.callManager
|
||||
@@ -135,7 +293,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
|
||||
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
|
||||
openApp = {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
.setAction(OpenChatAction)
|
||||
.setAction(NtfManager.OpenChatAction)
|
||||
.putExtra("userId", invitation.user.userId)
|
||||
.putExtra("chatId", invitation.contact.id)
|
||||
context.startActivity(intent)
|
||||
Reference in New Issue
Block a user