Skip to content

Commit a324f56

Browse files
committed
Merge remote-tracking branch 'origin-tareko/fix-background-death-2' into issue-1688-pr-5660-fix-background-death
2 parents b20a058 + b65b985 commit a324f56

7 files changed

Lines changed: 254 additions & 12 deletions

File tree

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"java.configuration.updateBuildConfiguration": "interactive"
3+
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@
285285
<receiver android:name=".receivers.MarkAsReadReceiver" />
286286
<receiver android:name=".receivers.DismissRecordingAvailableReceiver" />
287287
<receiver android:name=".receivers.ShareRecordingToChatReceiver" />
288+
<receiver android:name=".receivers.EndCallReceiver" />
288289

289290
<service
290291
android:name=".utils.SyncService"

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

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ class CallActivity : CallBaseActivity() {
247247
private val cameraSwitchHandler = Handler()
248248

249249
private val callTimeHandler = Handler(Looper.getMainLooper())
250+
251+
// Track if we're intentionally leaving the call
252+
private var isIntentionallyLeavingCall = false
250253

251254
// push to talk
252255
private var isPushToTalkActive = false
@@ -312,12 +315,16 @@ class CallActivity : CallBaseActivity() {
312315
private var requestPermissionLauncher = registerForActivityResult(
313316
ActivityResultContracts.RequestMultiplePermissions()
314317
) { permissionMap: Map<String, Boolean> ->
318+
// Log permission results
319+
Log.d(TAG, "Permission request completed with results: $permissionMap")
320+
315321
val rationaleList: MutableList<String> = ArrayList()
316322
val audioPermission = permissionMap[Manifest.permission.RECORD_AUDIO]
317323
if (audioPermission != null) {
318324
if (java.lang.Boolean.TRUE == audioPermission) {
319325
Log.d(TAG, "Microphone permission was granted")
320326
} else {
327+
Log.d(TAG, "Microphone permission was denied")
321328
rationaleList.add(resources.getString(R.string.nc_microphone_permission_hint))
322329
}
323330
}
@@ -326,6 +333,7 @@ class CallActivity : CallBaseActivity() {
326333
if (java.lang.Boolean.TRUE == cameraPermission) {
327334
Log.d(TAG, "Camera permission was granted")
328335
} else {
336+
Log.d(TAG, "Camera permission was denied")
329337
rationaleList.add(resources.getString(R.string.nc_camera_permission_hint))
330338
}
331339
}
@@ -335,6 +343,7 @@ class CallActivity : CallBaseActivity() {
335343
if (java.lang.Boolean.TRUE == bluetoothPermission) {
336344
enableBluetoothManager()
337345
} else {
346+
Log.d(TAG, "Bluetooth permission was denied")
338347
// Only ask for bluetooth when already asking to grant microphone or camera access. Asking
339348
// for bluetooth solely is not important enough here and would most likely annoy the user.
340349
if (rationaleList.isNotEmpty()) {
@@ -343,11 +352,32 @@ class CallActivity : CallBaseActivity() {
343352
}
344353
}
345354
}
355+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
356+
val notificationPermission = permissionMap[Manifest.permission.POST_NOTIFICATIONS]
357+
if (notificationPermission != null) {
358+
if (java.lang.Boolean.TRUE == notificationPermission) {
359+
Log.d(TAG, "Notification permission was granted")
360+
} else {
361+
Log.w(TAG, "Notification permission was denied - this may cause call hang")
362+
rationaleList.add(resources.getString(R.string.nc_notification_permission_hint))
363+
}
364+
}
365+
}
346366
if (rationaleList.isNotEmpty()) {
347367
showRationaleDialogForSettings(rationaleList)
348368
}
349369

370+
// Check if we should proceed with call despite notification permission
371+
val notificationPermissionGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
372+
permissionMap[Manifest.permission.POST_NOTIFICATIONS] == true
373+
} else {
374+
true // Older Android versions have permission by default
375+
}
376+
377+
Log.d(TAG, "DEBUGNotification permission granted: $notificationPermissionGranted, isConnectionEstablished: $isConnectionEstablished")
378+
350379
if (!isConnectionEstablished) {
380+
Log.d(TAG, "Proceeding with prepareCall() despite notification permission status")
351381
prepareCall()
352382
}
353383
}
@@ -376,6 +406,21 @@ class CallActivity : CallBaseActivity() {
376406
Log.d(TAG, "onCreate")
377407
super.onCreate(savedInstanceState)
378408
sharedApplication!!.componentApplication.inject(this)
409+
410+
// Register broadcast receiver for ending call from notification
411+
val endCallFilter = IntentFilter("com.nextcloud.talk.END_CALL_FROM_NOTIFICATION")
412+
413+
// Use the proper utility function with ReceiverFlag for Android 14+ compatibility
414+
// This receiver is for internal app use only (notification actions), so it should NOT be exported
415+
registerPermissionHandlerBroadcastReceiver(
416+
endCallFromNotificationReceiver,
417+
endCallFilter,
418+
permissionUtil!!.privateBroadcastPermission,
419+
null,
420+
ReceiverFlag.NotExported
421+
)
422+
423+
Log.d(TAG, "Broadcast receiver registered successfully")
379424

380425
callViewModel = ViewModelProvider(this, viewModelFactory)[CallViewModel::class.java]
381426

@@ -762,6 +807,7 @@ class CallActivity : CallBaseActivity() {
762807
true
763808
}
764809
binding!!.hangupButton.setOnClickListener {
810+
isIntentionallyLeavingCall = true
765811
hangup(shutDownView = true, endCallForAll = true)
766812
}
767813
binding!!.endCallPopupMenu.setOnClickListener {
@@ -776,6 +822,7 @@ class CallActivity : CallBaseActivity() {
776822
}
777823
}
778824
binding!!.hangupButton.setOnClickListener {
825+
isIntentionallyLeavingCall = true
779826
hangup(shutDownView = true, endCallForAll = false)
780827
}
781828
binding!!.endCallPopupMenu.setOnClickListener {
@@ -1002,6 +1049,18 @@ class CallActivity : CallBaseActivity() {
10021049
permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT)
10031050
}
10041051
}
1052+
1053+
// Check notification permission for Android 13+ (API 33+)
1054+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1055+
if (permissionUtil!!.isPostNotificationsPermissionGranted()) {
1056+
Log.d(TAG, "Notification permission already granted")
1057+
} else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
1058+
permissionsToRequest.add(Manifest.permission.POST_NOTIFICATIONS)
1059+
rationaleList.add(resources.getString(R.string.nc_notification_permission_hint))
1060+
} else {
1061+
permissionsToRequest.add(Manifest.permission.POST_NOTIFICATIONS)
1062+
}
1063+
}
10051064

10061065
if (permissionsToRequest.isNotEmpty()) {
10071066
if (rationaleList.isNotEmpty()) {
@@ -1011,30 +1070,65 @@ class CallActivity : CallBaseActivity() {
10111070
}
10121071
} else if (!isConnectionEstablished) {
10131072
prepareCall()
1073+
} else {
1074+
// All permissions granted but connection not established
1075+
Log.d(TAG, "All permissions granted but connection not established, proceeding with prepareCall()")
1076+
prepareCall()
10141077
}
10151078
}
10161079

10171080
private fun prepareCall() {
1081+
Log.d(TAG, "prepareCall() started")
10181082
basicInitialization()
10191083
initViews()
10201084
// updateSelfVideoViewPosition(true)
10211085
checkRecordingConsentAndInitiateCall()
10221086

1087+
// Start foreground service only if we have notification permission (for Android 13+)
1088+
// or if we're on older Android versions where permission is automatically granted
10231089
if (permissionUtil!!.isMicrophonePermissionGranted()) {
1024-
CallForegroundService.start(applicationContext, conversationName, intent.extras)
1090+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1091+
// Android 13+ requires explicit notification permission
1092+
if (permissionUtil!!.isPostNotificationsPermissionGranted()) {
1093+
Log.d(TAG, "Starting foreground service with notification permission")
1094+
CallForegroundService.start(applicationContext, conversationName, intent.extras)
1095+
} else {
1096+
Log.w(TAG, "Notification permission not granted - call will work but without persistent notification")
1097+
// Show warning to user that notification permission is missing (10 seconds)
1098+
Snackbar.make(
1099+
binding!!.root,
1100+
resources.getString(R.string.nc_notification_permission_hint),
1101+
10000
1102+
).show()
1103+
}
1104+
} else {
1105+
// Android 12 and below - notification permission is automatically granted
1106+
Log.d(TAG, "Starting foreground service (Android 12-)")
1107+
CallForegroundService.start(applicationContext, conversationName, intent.extras)
1108+
}
1109+
10251110
if (!microphoneOn) {
10261111
onMicrophoneClick()
10271112
}
1113+
} else {
1114+
Log.w(TAG, "Microphone permission not granted - skipping foreground service start")
10281115
}
10291116

1117+
// The call should not hang just because notification permission was denied
1118+
// Always proceed with call setup regardless of notification permission
1119+
Log.d(TAG, "Ensuring call proceeds even without notification permission")
1120+
10301121
if (isVoiceOnlyCall) {
10311122
binding!!.selfVideoViewWrapper.visibility = View.GONE
10321123
} else if (permissionUtil!!.isCameraPermissionGranted()) {
1124+
Log.d(TAG, "Camera permission granted, showing video")
10331125
binding!!.selfVideoViewWrapper.visibility = View.VISIBLE
10341126
onCameraClick()
10351127
if (cameraEnumerator!!.deviceNames.isEmpty()) {
10361128
binding!!.cameraButton.visibility = View.GONE
10371129
}
1130+
} else {
1131+
Log.w(TAG, "Camera permission not granted, hiding video")
10381132
}
10391133
}
10401134

@@ -1051,13 +1145,31 @@ class CallActivity : CallBaseActivity() {
10511145
for (rationale in rationaleList) {
10521146
rationalesWithLineBreaks.append(rationale).append("\n\n")
10531147
}
1148+
1149+
// Log when permission rationale dialog is shown
1150+
Log.d(TAG, "Showing permission rationale dialog for permissions: $permissionsToRequest")
1151+
Log.d(TAG, "Rationale includes notification permission: ${permissionsToRequest.contains(Manifest.permission.POST_NOTIFICATIONS)}")
1152+
10541153
val dialogBuilder = MaterialAlertDialogBuilder(this)
10551154
.setTitle(R.string.nc_permissions_rationale_dialog_title)
10561155
.setMessage(rationalesWithLineBreaks)
10571156
.setPositiveButton(R.string.nc_permissions_ask) { _, _ ->
1157+
Log.d(TAG, "User clicked 'Ask' for permissions")
10581158
requestPermissionLauncher.launch(permissionsToRequest.toTypedArray())
10591159
}
1060-
.setNegativeButton(R.string.nc_common_dismiss, null)
1160+
.setNegativeButton(R.string.nc_common_dismiss) { _, _ ->
1161+
// Log when user dismisses permission request
1162+
Log.w(TAG, "User dismissed permission request for: $permissionsToRequest")
1163+
if (permissionsToRequest.contains(Manifest.permission.POST_NOTIFICATIONS)) {
1164+
Log.w(TAG, "Notification permission specifically dismissed - proceeding with call anyway")
1165+
}
1166+
1167+
// Proceed with call even when notification permission is dismissed
1168+
if (!isConnectionEstablished) {
1169+
Log.d(TAG, "Proceeding with prepareCall() after dismissing notification permission")
1170+
prepareCall()
1171+
}
1172+
}
10611173
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
10621174
dialogBuilder.show()
10631175
}
@@ -1327,6 +1439,10 @@ class CallActivity : CallBaseActivity() {
13271439
}
13281440

13291441
public override fun onDestroy() {
1442+
Log.d(TAG, "onDestroy called")
1443+
Log.d(TAG, "onDestroy: isIntentionallyLeavingCall=$isIntentionallyLeavingCall")
1444+
Log.d(TAG, "onDestroy: currentCallStatus=$currentCallStatus")
1445+
13301446
if (signalingMessageReceiver != null) {
13311447
signalingMessageReceiver!!.removeListener(localParticipantMessageListener)
13321448
signalingMessageReceiver!!.removeListener(offerMessageListener)
@@ -1339,10 +1455,29 @@ class CallActivity : CallBaseActivity() {
13391455
Log.d(TAG, "localStream is null")
13401456
}
13411457
if (currentCallStatus !== CallStatus.LEAVING) {
1342-
hangup(true, false)
1458+
// Only hangup if we're intentionally leaving
1459+
if (isIntentionallyLeavingCall) {
1460+
hangup(true, false)
1461+
}
1462+
}
1463+
// Only stop the foreground service if we're actually leaving the call
1464+
if (isIntentionallyLeavingCall || currentCallStatus === CallStatus.LEAVING) {
1465+
CallForegroundService.stop(applicationContext)
13431466
}
1344-
CallForegroundService.stop(applicationContext)
1467+
1468+
Log.d(TAG, "onDestroy: Releasing proximity sensor - updating to IDLE state")
13451469
powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.IDLE)
1470+
Log.d(TAG, "onDestroy: Proximity sensor released")
1471+
1472+
// Unregister receiver
1473+
try {
1474+
Log.d(TAG, "Unregistering endCallFromNotificationReceiver...")
1475+
unregisterReceiver(endCallFromNotificationReceiver)
1476+
Log.d(TAG, "endCallFromNotificationReceiver unregistered successfully")
1477+
} catch (e: Exception) {
1478+
Log.w(TAG, "Failed to unregister endCallFromNotificationReceiver", e)
1479+
}
1480+
13461481
super.onDestroy()
13471482
}
13481483

@@ -1916,7 +2051,10 @@ class CallActivity : CallBaseActivity() {
19162051
}
19172052

19182053
private fun hangup(shutDownView: Boolean, endCallForAll: Boolean) {
1919-
Log.d(TAG, "hangup! shutDownView=$shutDownView")
2054+
Log.d(TAG, "hangup! shutDownView=$shutDownView, endCallForAll=$endCallForAll")
2055+
Log.d(TAG, "hangup! isIntentionallyLeavingCall=$isIntentionallyLeavingCall")
2056+
Log.d(TAG, "hangup! powerManagerUtils state before cleanup: ${powerManagerUtils != null}")
2057+
19202058
if (shutDownView) {
19212059
setCallState(CallStatus.LEAVING)
19222060
}
@@ -3085,4 +3223,18 @@ class CallActivity : CallBaseActivity() {
30853223

30863224
private const val SESSION_ID_PREFFIX_END: Int = 4
30873225
}
3226+
3227+
// Broadcast receiver to handle end call from notification
3228+
private val endCallFromNotificationReceiver = object : BroadcastReceiver() {
3229+
override fun onReceive(context: Context, intent: Intent) {
3230+
if (intent.action == "com.nextcloud.talk.END_CALL_FROM_NOTIFICATION") {
3231+
Log.d(TAG, "Received end call from notification broadcast")
3232+
Log.d(TAG, "endCallFromNotificationReceiver: Setting isIntentionallyLeavingCall=true")
3233+
isIntentionallyLeavingCall = true
3234+
Log.d(TAG, "endCallFromNotificationReceiver: Releasing proximity sensor before hangup")
3235+
powerManagerUtils?.updatePhoneState(PowerManagerUtils.PhoneState.IDLE)
3236+
hangup(shutDownView = true, endCallForAll = false)
3237+
}
3238+
}
3239+
}
30883240
}

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public abstract class CallBaseActivity extends BaseActivity {
3939
public void handleOnBackPressed() {
4040
if (isPipModePossible()) {
4141
enterPipMode();
42+
} else {
43+
// Move the task to background instead of finishing
44+
moveTaskToBack(true);
4245
}
4346
}
4447
};
@@ -98,8 +101,13 @@ void enableKeyguard() {
98101
@Override
99102
public void onStop() {
100103
super.onStop();
101-
if (shouldFinishOnStop()) {
102-
finish();
104+
// Don't automatically finish when going to background
105+
// Only finish if explicitly leaving the call
106+
if (shouldFinishOnStop() && !isChangingConfigurations()) {
107+
// Check if we're really leaving the call or just backgrounding
108+
if (isFinishing()) {
109+
finish();
110+
}
103111
}
104112
}
105113

@@ -124,10 +132,9 @@ void enterPipMode() {
124132
mPictureInPictureParamsBuilder.setAspectRatio(pipRatio);
125133
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
126134
} else {
127-
// we don't support other solutions than PIP to have a call in the background.
128-
// If PIP is not available the call is ended when user presses the home button.
129-
Log.d(TAG, "Activity was finished because PIP is not available.");
130-
finish();
135+
// If PIP is not available, move to background instead of finishing
136+
Log.d(TAG, "PIP is not available, moving call to background.");
137+
moveTaskToBack(true);
131138
}
132139
}
133140

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.activities.CallActivity
14+
import com.nextcloud.talk.services.CallForegroundService
15+
16+
class EndCallReceiver : BroadcastReceiver() {
17+
companion object {
18+
private const val TAG = "EndCallReceiver"
19+
}
20+
21+
override fun onReceive(context: Context?, intent: Intent?) {
22+
if (intent?.action == "com.nextcloud.talk.END_CALL") {
23+
Log.d(TAG, "Received end call broadcast")
24+
25+
// Stop the foreground service
26+
context?.let {
27+
CallForegroundService.stop(it)
28+
29+
// Send broadcast to CallActivity to end the call
30+
val endCallIntent = Intent("com.nextcloud.talk.END_CALL_FROM_NOTIFICATION")
31+
endCallIntent.setPackage(context.packageName)
32+
context.sendBroadcast(endCallIntent)
33+
}
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)