Skip to content

Commit 1f342d1

Browse files
authored
Merge pull request #6334 from nextcloud/bugfix/1688/fixCallBackgroundDeath
Bugfix/1688/fix call background death
2 parents 04c1821 + 83b383e commit 1f342d1

11 files changed

Lines changed: 1300 additions & 95 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ target/
2121
# Local configuration files (sdk path, etc)
2222
local.properties
2323
tests/local.properties
24+
.vscode/
2425

2526
# Mac .DS_Store files
2627
.DS_Store

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@
333333
<receiver
334334
android:name=".receivers.DeclineCallReceiver"
335335
android:exported="false" />
336+
<receiver
337+
android:name=".receivers.EndCallReceiver"
338+
android:exported="false" />
336339

337340
<service
338341
android:name=".utils.SyncService"

app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt

Lines changed: 246 additions & 53 deletions
Large diffs are not rendered by default.

app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import android.content.pm.PackageManager;
1414
import android.os.Build;
1515
import android.os.Bundle;
16-
import android.os.PowerManager;
1716
import android.util.Log;
1817
import android.util.Rational;
1918
import android.view.View;
@@ -30,14 +29,14 @@ public abstract class CallBaseActivity extends BaseActivity {
3029

3130
public PictureInPictureParams.Builder mPictureInPictureParamsBuilder;
3231
public Boolean isInPipMode = Boolean.FALSE;
33-
long onCreateTime;
3432

35-
36-
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
33+
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
3734
@Override
3835
public void handleOnBackPressed() {
3936
if (isPipModePossible()) {
4037
enterPipMode();
38+
} else {
39+
moveTaskToBack(true);
4140
}
4241
}
4342
};
@@ -47,21 +46,25 @@ public void handleOnBackPressed() {
4746
public void onCreate(Bundle savedInstanceState) {
4847
super.onCreate(savedInstanceState);
4948

50-
onCreateTime = System.currentTimeMillis();
51-
5249
requestWindowFeature(Window.FEATURE_NO_TITLE);
5350
dismissKeyguard();
5451
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
5552
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
5653

5754
if (isPipModePossible()) {
5855
mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
56+
Rational pipRatio = new Rational(300, 500);
57+
mPictureInPictureParamsBuilder.setAspectRatio(pipRatio);
58+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
59+
mPictureInPictureParamsBuilder.setAutoEnterEnabled(true);
60+
}
61+
setPictureInPictureParams(mPictureInPictureParamsBuilder.build());
5962
}
6063

6164
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
6265
}
6366

64-
public void hideNavigationIfNoPipAvailable(){
67+
public void hideNavigationIfNoPipAvailable() {
6568
if (!isPipModePossible()) {
6669
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
6770
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
@@ -91,39 +94,82 @@ void enableKeyguard() {
9194
}
9295
}
9396

97+
/**
98+
* On API 29+, fires BEFORE onPause while the window is still fully visible.
99+
*
100+
* On API 29-30: enter PIP immediately (no auto-enter available).
101+
*
102+
* On API 31+: auto-enter handles swipe-up/home gestures. Task switching
103+
* (left/right swipe) does NOT trigger auto-enter — we accept no PIP for
104+
* task switch since the call stays alive in the background via the ICE
105+
* failure guard in CallActivity.
106+
*/
107+
@Override
108+
public void onTopResumedActivityChanged(boolean isTopResumedActivity) {
109+
super.onTopResumedActivityChanged(isTopResumedActivity);
110+
Log.d(TAG, "onTopResumedActivityChanged: isTopResumedActivity=" + isTopResumedActivity
111+
+ " isInPictureInPictureMode=" + isInPictureInPictureMode());
112+
if (isTopResumedActivity || isInPictureInPictureMode()
113+
|| !isPipModePossible()
114+
|| isChangingConfigurations()
115+
|| isFinishing()) {
116+
return;
117+
}
118+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
119+
enterPipMode();
120+
}
121+
}
122+
123+
@Override
124+
public void onPause() {
125+
super.onPause();
126+
Log.d(TAG, "onPause: isInPipMode=" + isInPipMode
127+
+ " isInPictureInPictureMode=" + isInPictureInPictureMode());
128+
// Fallback for API 26-28 where onTopResumedActivityChanged doesn't exist.
129+
// On API 29+, onTopResumedActivityChanged already handled this.
130+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
131+
&& !isInPictureInPictureMode()
132+
&& isPipModePossible()
133+
&& !isChangingConfigurations()
134+
&& !isFinishing()) {
135+
enterPipMode();
136+
}
137+
}
138+
94139
@Override
95140
public void onStop() {
96141
super.onStop();
97-
if (shouldFinishOnStop()) {
98-
finish();
99-
}
142+
Log.d(TAG, "onStop: isInPipMode=" + isInPipMode + " isFinishing=" + isFinishing());
100143
}
101144

102145
@Override
103146
protected void onUserLeaveHint() {
104147
super.onUserLeaveHint();
105-
long onUserLeaveHintTime = System.currentTimeMillis();
106-
long diff = onUserLeaveHintTime - onCreateTime;
107-
Log.d(TAG, "onUserLeaveHintTime - onCreateTime: " + diff);
108-
109-
if (diff < 3000) {
110-
Log.d(TAG, "enterPipMode skipped");
111-
} else {
148+
Log.d(TAG, "onUserLeaveHint: isInPipMode=" + isInPipMode
149+
+ " isInPictureInPictureMode=" + isInPictureInPictureMode());
150+
// On API 26-30, enter PIP manually. On API 31+ auto-enter handles swipe-up/home, and plain
151+
// backgrounding (e.g. task switch) keeps the activity alive on its own. Deliberately no
152+
// moveTaskToBack here: onUserLeaveHint also fires when a transient overlay like the
153+
// permission dialog appears at call start, which would throw the call to the background.
154+
if (!isInPipMode
155+
&& isPipModePossible()
156+
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
112157
enterPipMode();
113158
}
114159
}
115160

116161
void enterPipMode() {
162+
Log.d(TAG, "enterPipMode: isPipModePossible=" + isPipModePossible() + " isInPipMode=" + isInPipMode);
117163
enableKeyguard();
118164
if (isPipModePossible()) {
119165
Rational pipRatio = new Rational(300, 500);
120166
mPictureInPictureParamsBuilder.setAspectRatio(pipRatio);
121-
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
167+
boolean entered = enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
168+
Log.d(TAG, "enterPictureInPictureMode returned: " + entered);
122169
} else {
123-
// we don't support other solutions than PIP to have a call in the background.
124-
// If PIP is not available the call is ended when user presses the home button.
125-
Log.d(TAG, "Activity was finished because PIP is not available.");
126-
finish();
170+
// If PIP is not available, move to background instead of finishing
171+
Log.d(TAG, "PIP is not available, moving call to background.");
172+
moveTaskToBack(true);
127173
}
128174
}
129175

@@ -138,19 +184,6 @@ boolean isPipModePossible() {
138184
return deviceHasPipFeature && isPipFeatureGranted;
139185
}
140186

141-
private boolean shouldFinishOnStop() {
142-
if (!isInPipMode) {
143-
return false;
144-
}
145-
146-
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
147-
if (powerManager == null) {
148-
return true;
149-
}
150-
151-
return powerManager.isInteractive();
152-
}
153-
154187
public abstract void updateUiForPipMode();
155188

156189
public abstract void updateUiForNormalMode();

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ class ChatActivity :
391391
private lateinit var path: String
392392

393393
var myFirstMessage: CharSequence? = null
394+
private var isLeavingRoom: Boolean = false
394395

395396
private var lastHandledHighlightNonce: Long? = null
396397
private var pendingHighlightedMessageId: Long? = null
@@ -514,6 +515,24 @@ class ChatActivity :
514515

515516
val typingParticipants = HashMap<String, TypingParticipant>()
516517

518+
private val leaveRoomObserver = androidx.lifecycle.Observer<ChatViewModel.ViewState> { state ->
519+
when (state) {
520+
is ChatViewModel.LeaveRoomSuccessState -> {
521+
logConversationInfos("leaveRoom#onNext")
522+
523+
isLeavingRoom = false
524+
525+
if (getRoomInfoTimerHandler != null) {
526+
getRoomInfoTimerHandler?.removeCallbacksAndMessages(null)
527+
}
528+
529+
ApplicationWideCurrentRoomHolder.getInstance().clear()
530+
}
531+
532+
else -> {}
533+
}
534+
}
535+
517536
private val localParticipantMessageListener = SignalingMessageReceiver.LocalParticipantMessageListener { token ->
518537
if (CallActivity.active) {
519538
Log.d(TAG, "CallActivity is running. Ignore to switch chat in ChatActivity...")
@@ -1682,6 +1701,8 @@ class ChatActivity :
16821701
}
16831702
}
16841703

1704+
chatViewModel.leaveRoomViewState.observeForever(leaveRoomObserver)
1705+
16851706
messageInputViewModel.sendChatMessageViewState.observe(this) { state ->
16861707
when (state) {
16871708
is MessageInputViewModel.SendChatMessageSuccessState -> {
@@ -2035,6 +2056,9 @@ class ChatActivity :
20352056

20362057
pullChatMessagesPending = false
20372058

2059+
// reset in case a previously started leave failed (success already resets this in leaveRoomObserver)
2060+
isLeavingRoom = false
2061+
20382062
webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
20392063
webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
20402064

@@ -2959,11 +2983,13 @@ class ChatActivity :
29592983
}
29602984

29612985
if (::conversationUser.isInitialized && isActivityNotChangingConfigurations() && isNotInCall()) {
2962-
ApplicationWideCurrentRoomHolder.getInstance().clear()
2963-
if (validSessionId()) {
2986+
if (isLeavingRoom) {
2987+
Log.d(TAG, "not leaving room (leave already in progress)")
2988+
} else if (validSessionId()) {
29642989
leaveRoom(null)
29652990
} else {
29662991
Log.d(TAG, "not leaving room (validSessionId is false)")
2992+
ApplicationWideCurrentRoomHolder.getInstance().clear()
29672993
}
29682994
} else {
29692995
Log.d(TAG, "not leaving room...")
@@ -3005,6 +3031,8 @@ class ChatActivity :
30053031
super.onDestroy()
30063032
logConversationInfos("onDestroy")
30073033

3034+
chatViewModel.leaveRoomViewState.removeObserver(leaveRoomObserver)
3035+
30083036
findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
30093037

30103038
if (actionBar != null) {
@@ -3040,6 +3068,7 @@ class ChatActivity :
30403068

30413069
fun leaveRoom(functionToCallAfterLeave: (() -> Unit)?) {
30423070
logConversationInfos("leaveRoom")
3071+
isLeavingRoom = true
30433072

30443073
// Send the HPB "leave room" immediately, before waiting for the backend DELETE to
30453074
// confirm. This minimises the window in which the HPB could still consider the user
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package com.nextcloud.talk.receivers
8+
9+
import android.content.BroadcastReceiver
10+
import android.content.Context
11+
import android.content.Intent
12+
import android.util.Log
13+
import com.nextcloud.talk.services.CallForegroundService
14+
15+
class EndCallReceiver : BroadcastReceiver() {
16+
companion object {
17+
private val TAG = EndCallReceiver::class.simpleName
18+
const val END_CALL_ACTION = "com.nextcloud.talk.END_CALL"
19+
const val END_CALL_FROM_NOTIFICATION = "com.nextcloud.talk.END_CALL_FROM_NOTIFICATION"
20+
}
21+
22+
override fun onReceive(context: Context?, intent: Intent?) {
23+
if (intent?.action == END_CALL_ACTION) {
24+
Log.i(TAG, "Received end call broadcast")
25+
26+
// Stop the foreground service
27+
context?.let {
28+
CallForegroundService.stop(it)
29+
30+
// Send broadcast to CallActivity to end the call
31+
val endCallIntent = Intent(END_CALL_FROM_NOTIFICATION)
32+
endCallIntent.setPackage(context.packageName)
33+
context.sendBroadcast(endCallIntent)
34+
}
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)