@@ -15,6 +15,8 @@ import android.animation.Animator
1515import android.animation.AnimatorListenerAdapter
1616import android.annotation.SuppressLint
1717import android.app.KeyguardManager
18+ import android.app.NotificationManager
19+ import android.content.ActivityNotFoundException
1820import android.content.Context
1921import android.content.DialogInterface
2022import android.content.Intent
@@ -144,12 +146,16 @@ class SettingsActivity :
144146 private var dbQueryDisposable: Disposable ? = null
145147 private var openedByNotificationWarning: Boolean = false
146148 private var focusBubbleSettings: Boolean = false
149+ private var isUpdatingBubbleSwitchState: Boolean = false
150+ private var pendingBubbleEnableAfterSystemChange: Boolean = false
147151 private var isOnline: MutableState <Boolean > = mutableStateOf(false )
148152
149153 @SuppressLint(" StringFormatInvalid" )
150154 override fun onCreate (savedInstanceState : Bundle ? ) {
151155 super .onCreate(savedInstanceState)
152156 NextcloudTalkApplication .sharedApplication!! .componentApplication.inject(this )
157+ pendingBubbleEnableAfterSystemChange =
158+ savedInstanceState?.getBoolean(STATE_PENDING_ENABLE_BUBBLES ) ? : false
153159 networkMonitor.isOnlineLiveData.observe(this ) { online ->
154160 isOnline.value = online
155161 handleNetworkChange(isOnline.value)
@@ -201,6 +207,11 @@ class SettingsActivity :
201207 focusBubbleSettings = extras?.getBoolean(KEY_FOCUS_BUBBLE_SETTINGS ) ? : false
202208 }
203209
210+ override fun onSaveInstanceState (outState : Bundle ) {
211+ super .onSaveInstanceState(outState)
212+ outState.putBoolean(STATE_PENDING_ENABLE_BUBBLES , pendingBubbleEnableAfterSystemChange)
213+ }
214+
204215 override fun onResume () {
205216 super .onResume()
206217 supportActionBar?.show()
@@ -431,24 +442,29 @@ class SettingsActivity :
431442 binding.settingsBubbles.visibility = View .VISIBLE
432443 binding.settingsBubblesForce.visibility = View .VISIBLE
433444
434- val bubblesEnabled = appPreferences.areBubblesEnabled()
435- binding.settingsBubblesSwitch.isChecked = bubblesEnabled
445+ val systemAllowsAllConversations = isSystemBubblePreferenceAll()
446+ updateBubbleSummary(systemAllowsAllConversations)
447+
448+ var bubblesEnabled = appPreferences.areBubblesEnabled()
449+ if (bubblesEnabled && ! systemAllowsAllConversations) {
450+ appPreferences.setBubblesEnabled(false )
451+ bubblesEnabled = false
452+ }
453+
454+ setGlobalBubbleSwitchState(bubblesEnabled)
436455 binding.settingsBubblesForceSwitch.isChecked = appPreferences.areBubblesForced()
437456
438457 updateBubbleForceRowState(bubblesEnabled)
439458
440- binding.settingsBubbles.setOnClickListener {
441- val newValue = ! binding.settingsBubblesSwitch.isChecked
442- binding.settingsBubblesSwitch.isChecked = newValue
443- appPreferences.setBubblesEnabled(newValue)
444- updateBubbleForceRowState(newValue)
445-
446- // Dismiss all active bubbles when disabling globally
447- if (! newValue) {
448- currentUser?.let { user ->
449- NotificationUtils .dismissAllBubbles(this , user)
450- }
459+ binding.settingsBubblesSwitch.setOnCheckedChangeListener { _, isChecked ->
460+ if (isUpdatingBubbleSwitchState) {
461+ return @setOnCheckedChangeListener
451462 }
463+ handleGlobalBubblePreferenceChange(isChecked)
464+ }
465+
466+ binding.settingsBubbles.setOnClickListener {
467+ binding.settingsBubblesSwitch.performClick()
452468 }
453469
454470 binding.settingsBubblesForce.setOnClickListener {
@@ -467,6 +483,8 @@ class SettingsActivity :
467483 )
468484 }
469485 }
486+
487+ maybeEnableBubblesAfterSystemChange(systemAllowsAllConversations)
470488 }
471489
472490 private fun updateBubbleForceRowState (globalEnabled : Boolean ) {
@@ -475,6 +493,141 @@ class SettingsActivity :
475493 binding.settingsBubblesForceSwitch.isEnabled = globalEnabled
476494 }
477495
496+ private fun handleGlobalBubblePreferenceChange (enabled : Boolean ) {
497+ val systemAllowsAllConversations = isSystemBubblePreferenceAll()
498+
499+ if (enabled) {
500+ if (! systemAllowsAllConversations) {
501+ pendingBubbleEnableAfterSystemChange = true
502+ showSystemBubblesDisabledFeedback()
503+ updateBubbleSummary(systemAllowsAllConversations)
504+ setGlobalBubbleSwitchState(false )
505+ navigateToSystemBubbleSettings()
506+ return
507+ }
508+
509+ pendingBubbleEnableAfterSystemChange = false
510+ appPreferences.setBubblesEnabled(true )
511+ updateBubbleForceRowState(true )
512+ updateBubbleSummary(true )
513+ } else {
514+ pendingBubbleEnableAfterSystemChange = false
515+ appPreferences.setBubblesEnabled(false )
516+ updateBubbleForceRowState(false )
517+ updateBubbleSummary(systemAllowsAllConversations)
518+ currentUser?.let { user ->
519+ NotificationUtils .dismissAllBubbles(this , user)
520+ }
521+ }
522+ }
523+
524+ private fun setGlobalBubbleSwitchState (checked : Boolean ) {
525+ isUpdatingBubbleSwitchState = true
526+ binding.settingsBubblesSwitch.isChecked = checked
527+ isUpdatingBubbleSwitchState = false
528+ }
529+
530+ private fun updateBubbleSummary (systemAllowsAllConversations : Boolean ) {
531+ val summaryText = if (systemAllowsAllConversations) {
532+ R .string.nc_notification_settings_bubbles_desc
533+ } else {
534+ R .string.nc_notification_settings_bubbles_system_disabled
535+ }
536+ binding.settingsBubblesSummary.setText(summaryText)
537+ }
538+
539+ private fun showSystemBubblesDisabledFeedback () {
540+ Toast .makeText(
541+ this ,
542+ R .string.nc_notification_settings_bubbles_system_disabled_toast,
543+ Toast .LENGTH_LONG
544+ ).show()
545+ }
546+
547+ private fun maybeEnableBubblesAfterSystemChange (systemAllowsAllConversations : Boolean ) {
548+ if (! pendingBubbleEnableAfterSystemChange || ! systemAllowsAllConversations) {
549+ return
550+ }
551+
552+ pendingBubbleEnableAfterSystemChange = false
553+ appPreferences.setBubblesEnabled(true )
554+ setGlobalBubbleSwitchState(true )
555+ updateBubbleForceRowState(true )
556+ updateBubbleSummary(true )
557+ }
558+
559+ private fun isSystemBubblePreferenceAll (): Boolean {
560+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .R ) {
561+ return false
562+ }
563+
564+ val notificationManager = getSystemService(NotificationManager ::class .java)
565+ return notificationManager?.bubblePreference == NotificationManager .BUBBLE_PREFERENCE_ALL
566+ }
567+
568+ private fun navigateToSystemBubbleSettings () {
569+ val targetPackage = packageName
570+ val targetUid = applicationInfo?.uid ? : - 1
571+ val candidateIntents = mutableListOf<Intent >()
572+
573+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
574+ candidateIntents + = Intent (Settings .ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS )
575+ .withNotificationExtras(targetPackage, targetUid)
576+ .apply {
577+ putExtra(
578+ Settings .EXTRA_CHANNEL_ID ,
579+ NotificationUtils .NotificationChannels .NOTIFICATION_CHANNEL_MESSAGES_V4 .name
580+ )
581+ }
582+
583+ candidateIntents + = Intent (" android.settings.APP_NOTIFICATION_BUBBLE_SETTINGS" )
584+ .withNotificationExtras(targetPackage, targetUid)
585+
586+ val explicitBubbleComponents = listOf (
587+ " com.android.settings.Settings\$ AppBubbleNotificationSettingsActivity" ,
588+ " com.android.settings.Settings\$ BubbleNotificationSettingsActivity"
589+ )
590+
591+ explicitBubbleComponents.forEach { componentName ->
592+ candidateIntents + = Intent (Intent .ACTION_MAIN )
593+ .withNotificationExtras(targetPackage, targetUid)
594+ .setClassName(" com.android.settings" , componentName)
595+ }
596+ }
597+
598+ candidateIntents + = Intent (Settings .ACTION_APP_NOTIFICATION_SETTINGS )
599+ .withNotificationExtras(targetPackage, targetUid)
600+
601+ candidateIntents + = Intent (Settings .ACTION_APPLICATION_DETAILS_SETTINGS )
602+ .withNotificationExtras(targetPackage, targetUid)
603+
604+ candidateIntents.firstOrNull { launchIntentSafely(it) } ? : Toast .makeText(
605+ this ,
606+ R .string.nc_notification_settings_bubbles_open_failed,
607+ Toast .LENGTH_LONG
608+ ).show()
609+ }
610+
611+ private fun Intent.withNotificationExtras (targetPackage : String , targetUid : Int ): Intent {
612+ data = Uri .fromParts(" package" , targetPackage, null )
613+ putExtra(Settings .EXTRA_APP_PACKAGE , targetPackage)
614+ putExtra(" app_uid" , targetUid)
615+ return this
616+ }
617+
618+ private fun launchIntentSafely (intent : Intent ): Boolean {
619+ return try {
620+ if (intent.resolveActivity(packageManager) != null ) {
621+ startActivity(intent)
622+ true
623+ } else {
624+ false
625+ }
626+ } catch (activityNotFoundException: ActivityNotFoundException ) {
627+ false
628+ }
629+ }
630+
478631 private fun setupNotificationSoundsSettings () {
479632 if (NotificationUtils .isCallsNotificationChannelEnabled(this )) {
480633 val callRingtoneUri = getCallRingtoneUri(context, (appPreferences))
@@ -1547,6 +1700,7 @@ class SettingsActivity :
15471700 private const val DISABLED_ALPHA : Float = 0.38f
15481701 private const val ENABLED_ALPHA : Float = 1.0f
15491702 private const val LINEBREAK = " \n "
1703+ private const val STATE_PENDING_ENABLE_BUBBLES = " statePendingEnableBubbles"
15501704 const val HTTP_CODE_OK : Int = 200
15511705 const val HTTP_ERROR_CODE_BAD_REQUEST : Int = 400
15521706 const val NO_NOTIFICATION_REMINDER_WANTED = 0L
0 commit comments