diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 39f5df636..9e6073c10 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -120,6 +120,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { BGManager.shared.receiveMessages(complete) } + + static func keepScreenOn(_ on: Bool) { + UIApplication.shared.isIdleTimerDisabled = on + } } class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { diff --git a/apps/ios/Shared/Model/AudioRecPlay.swift b/apps/ios/Shared/Model/AudioRecPlay.swift index 973d79ab3..78773f5ea 100644 --- a/apps/ios/Shared/Model/AudioRecPlay.swift +++ b/apps/ios/Shared/Model/AudioRecPlay.swift @@ -46,6 +46,7 @@ class AudioRecorder { audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH) await MainActor.run { + AppDelegate.keepScreenOn(true) recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in guard let time = self.audioRecorder?.currentTime else { return } self.onTimer?(time) @@ -57,6 +58,9 @@ class AudioRecorder { } return nil } catch let error { + await MainActor.run { + AppDelegate.keepScreenOn(false) + } logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)") return .error(error.localizedDescription) } @@ -71,6 +75,7 @@ class AudioRecorder { timer.invalidate() } recordingTimer = nil + AppDelegate.keepScreenOn(false) } private func checkPermission() async -> Bool { @@ -121,6 +126,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in if self.audioPlayer?.isPlaying ?? false { + AppDelegate.keepScreenOn(true) guard let time = self.audioPlayer?.currentTime else { return } self.onTimer?(time) } @@ -129,6 +135,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { func pause() { audioPlayer?.pause() + AppDelegate.keepScreenOn(false) } func play() { @@ -149,6 +156,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { func stop() { if let player = audioPlayer { player.stop() + AppDelegate.keepScreenOn(false) } audioPlayer = nil if let timer = playbackTimer { diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 393a370ee..ad9d90c38 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -39,6 +39,7 @@ struct ActiveCallView: View { } .onAppear { logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)") + AppDelegate.keepScreenOn(true) createWebRTCClient() dismissAllSheets() } @@ -48,6 +49,7 @@ struct ActiveCallView: View { } .onDisappear { logger.debug("ActiveCallView: disappear") + AppDelegate.keepScreenOn(false) client?.endCall() } .onChange(of: m.callCommand) { _ in sendCommandToClient()} diff --git a/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift b/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift index 5b982f5f0..416fa0c37 100644 --- a/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift +++ b/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift @@ -6,6 +6,7 @@ import Foundation import SwiftUI import AVKit +import Combine struct VideoPlayerView: UIViewRepresentable { @@ -37,6 +38,9 @@ struct VideoPlayerView: UIViewRepresentable { player.seek(to: CMTime.zero) player.play() } + context.coordinator.publisher = player.publisher(for: \.timeControlStatus).sink { status in + AppDelegate.keepScreenOn(status == .playing) + } return controller.view } @@ -50,11 +54,13 @@ struct VideoPlayerView: UIViewRepresentable { class Coordinator: NSObject { var controller: AVPlayerViewController? var timeObserver: Any? = nil + var publisher: AnyCancellable? = nil deinit { if let timeObserver = timeObserver { NotificationCenter.default.removeObserver(timeObserver) } + publisher?.cancel() } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index 8df99d15f..b89719b2e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -48,6 +48,7 @@ actual class RecorderNative: RecorderInterface { recStartedAt = System.currentTimeMillis() progressJob = CoroutineScope(Dispatchers.Default).launch { while(isActive) { + keepScreenOn(true) onProgressUpdate(progress(), false) delay(50) } @@ -84,6 +85,7 @@ actual class RecorderNative: RecorderInterface { progressJob = null filePath = null recorder = null + keepScreenOn(false) return (realDuration(path) ?: 0).also { recStartedAt = null } } @@ -170,6 +172,7 @@ actual object AudioPlayer: AudioPlayerInterface { progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && player.isPlaying) { + keepScreenOn(true) // Even when current position is equal to duration, the player has isPlaying == true for some time, // so help to make the playback stopped in UI immediately if (player.currentPosition == player.duration) { @@ -187,6 +190,7 @@ actual object AudioPlayer: AudioPlayerInterface { if (isActive) { onProgressUpdate(player.duration, TrackState.PAUSED) } + keepScreenOn(false) onProgressUpdate(null, TrackState.PAUSED) } return player.duration @@ -196,6 +200,7 @@ actual object AudioPlayer: AudioPlayerInterface { progressJob?.cancel() progressJob = null player.pause() + keepScreenOn(false) return player.currentPosition } @@ -203,6 +208,7 @@ actual object AudioPlayer: AudioPlayerInterface { if (currentlyPlaying.value == null) return player.stop() stopListener() + keepScreenOn(false) } override fun stop(item: ChatItem) = stop(item.file?.fileName) @@ -263,6 +269,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun pause(audioPlaying: MutableState, pro: MutableState) { pro.value = pause() audioPlaying.value = false + keepScreenOn(false) } override fun seekTo(ms: Int, pro: MutableState, filePath: String?) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt index 574b756bb..1d62efbb4 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt @@ -1,13 +1,10 @@ package chat.simplex.common.platform -import android.media.MediaMetadataRetriever import android.media.session.PlaybackState import android.net.Uri import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import chat.simplex.common.helpers.toUri import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.google.android.exoplayer2.* @@ -134,6 +131,7 @@ actual class VideoPlayer actual constructor( player.addListener(object: Player.Listener{ override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) + keepScreenOn(isPlaying) // Produce non-ideal transition from stopped to playing state while showing preview image in ChatView // videoPlaying.value = isPlaying } @@ -192,6 +190,7 @@ actual class VideoPlayer actual constructor( override fun release(remove: Boolean) { player.release() + keepScreenOn(false) if (remove) { VideoPlayerHolder.players.remove(uri to gallery) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 790345e97..5c7b430ab 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -196,12 +196,14 @@ actual fun ActiveCallView() { chatModel.activeCallViewIsVisible.value = true // After the first call, End command gets added to the list which prevents making another calls chatModel.callCommand.removeAll { it is WCallCommand.End } + keepScreenOn(true) onDispose { activity.volumeControlStream = prevVolumeControlStream // Unlock orientation activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED chatModel.activeCallViewIsVisible.value = false chatModel.callCommand.clear() + keepScreenOn(false) } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index 8b98b0542..f2c2f393a 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -11,6 +11,7 @@ import android.text.Spanned import android.text.SpannedString import android.text.style.* import android.util.Base64 +import android.view.WindowManager import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* @@ -43,6 +44,17 @@ fun Resources.getText(id: StringResource, vararg args: Any): CharSequence { return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) } +fun keepScreenOn(on: Boolean) { + val window = mainActivity.get()?.window ?: return + Handler(Looper.getMainLooper()).post { + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } +} + actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) }