@@ -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}
0 commit comments