diff --git a/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt new file mode 100644 index 000000000..0f2ee06fd --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.compatibility + +import android.Manifest +import android.os.Build +import androidx.annotation.RequiresApi + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +class Api33Compatibility { + companion object { + private const val TAG = "[API 33 Compatibility]" + + fun getAllRequiredPermissionsArray(): Array { + return arrayOf( + Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.READ_CONTACTS, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA + ) + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt new file mode 100644 index 000000000..89f6ccf27 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.compatibility + +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import org.linphone.core.tools.Log + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +class Api34Compatibility { + companion object { + private const val TAG = "[API 34 Compatibility]" + + fun hasFullScreenIntentPermission(context: Context): Boolean { + val notificationManager = context.getSystemService(NotificationManager::class.java) as NotificationManager + // See https://developer.android.com/reference/android/app/NotificationManager#canUseFullScreenIntent%28%29 + val granted = notificationManager.canUseFullScreenIntent() + if (granted) { + Log.i("$TAG Full screen intent permission is granted") + } else { + Log.w("$TAG Full screen intent permission isn't granted yet!") + } + return granted + } + + fun requestFullScreenIntentPermission(context: Context) { + val intent = Intent() + // See https://developer.android.com/reference/android/provider/Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT + intent.action = Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT + intent.data = Uri.parse("package:${context.packageName}") + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + Log.i("$TAG Starting ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT") + ContextCompat.startActivity(context, intent, null) + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index 83c111a05..2be436622 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -19,9 +19,11 @@ */ package org.linphone.compatibility +import android.Manifest import android.annotation.SuppressLint import android.app.Notification import android.app.Service +import android.content.Context import android.net.Uri import android.view.View import org.linphone.mediastream.Version @@ -76,5 +78,31 @@ class Compatibility { Api28Compatibility.getMediaCollectionUri(isImage, isVideo, isAudio) } } + + fun getAllRequiredPermissionsArray(): Array { + if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) { + return Api33Compatibility.getAllRequiredPermissionsArray() + } + return arrayOf( + Manifest.permission.READ_CONTACTS, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA + ) + } + + fun hasFullScreenIntentPermission(context: Context): Boolean { + if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + return Api34Compatibility.hasFullScreenIntentPermission(context) + } + return true + } + + fun requestFullScreenIntentPermission(context: Context): Boolean { + if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + Api34Compatibility.requestFullScreenIntentPermission(context) + return true + } + return false + } } } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 60226b85a..1c33fff42 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -779,6 +779,11 @@ class NotificationsManager @MainThread constructor(private val context: Context) } val style = if (isIncoming) { + if (!Compatibility.hasFullScreenIntentPermission(context)) { + Log.e( + "$TAG Android >= 14 & full screen intent permission wasn't granted, incoming call may not be visible!" + ) + } NotificationCompat.CallStyle.forIncomingCall( caller, declineIntent, diff --git a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt index 7ed845391..fcd7f692d 100644 --- a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt +++ b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt @@ -19,20 +19,20 @@ */ package org.linphone.ui.assistant -import android.Manifest import android.content.pm.PackageManager import android.os.Bundle -import android.os.PersistableBundle import android.view.ViewGroup import androidx.activity.addCallback import androidx.annotation.DrawableRes import androidx.annotation.UiThread import androidx.core.content.ContextCompat +import androidx.core.view.doOnPreDraw import androidx.databinding.DataBindingUtil import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log import org.linphone.databinding.AssistantActivityBinding import org.linphone.ui.GenericActivity @@ -44,13 +44,6 @@ import org.linphone.utils.slideInToastFromTopForDuration class AssistantActivity : GenericActivity() { companion object { private const val TAG = "[Assistant Activity]" - - val PERMISSIONS = arrayOf( - Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.READ_CONTACTS, - Manifest.permission.RECORD_AUDIO, - Manifest.permission.CAMERA - ) } private lateinit var binding: AssistantActivityBinding @@ -70,15 +63,13 @@ class AssistantActivity : GenericActivity() { } } } - } - override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { - super.onPostCreate(savedInstanceState, persistentState) - - if (!areAllPermissionsGranted()) { - Log.w("$TAG Not all required permissions are granted, showing Permissions fragment") - val action = PermissionsFragmentDirections.actionGlobalPermissionsFragment() - binding.assistantNavContainer.findNavController().navigate(action) + (binding.root as? ViewGroup)?.doOnPreDraw { + if (!areAllPermissionsGranted()) { + Log.w("$TAG Not all required permissions are granted, showing Permissions fragment") + val action = PermissionsFragmentDirections.actionGlobalPermissionsFragment() + binding.assistantNavContainer.findNavController().navigate(action) + } } } @@ -103,11 +94,17 @@ class AssistantActivity : GenericActivity() { } private fun areAllPermissionsGranted(): Boolean { - for (permission in PERMISSIONS) { + for (permission in Compatibility.getAllRequiredPermissionsArray()) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + Log.w("$TAG Permission [$permission] hasn't been granted yet!") return false } } - return true + + val granted = Compatibility.hasFullScreenIntentPermission(this) + if (granted) { + Log.i("$TAG All permissions have been granted!") + } + return granted } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt index b17a6bf0a..1136261eb 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt @@ -30,9 +30,9 @@ import androidx.annotation.UiThread import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log import org.linphone.databinding.AssistantPermissionsFragmentBinding -import org.linphone.ui.assistant.AssistantActivity @UiThread class PermissionsFragment : Fragment() { @@ -71,11 +71,6 @@ class PermissionsFragment : Fragment() { binding.lifecycleOwner = viewLifecycleOwner - if (areAllPermissionsGranted()) { - Log.i("$TAG All permissions have been granted, skipping") - goToLoginFragment() - } - binding.setBackClickListener { findNavController().popBackStack() } @@ -88,7 +83,7 @@ class PermissionsFragment : Fragment() { binding.setGrantAllClickListener { Log.i("$TAG Requesting all permissions") requestPermissionLauncher.launch( - AssistantActivity.PERMISSIONS + Compatibility.getAllRequiredPermissionsArray() ) } @@ -99,6 +94,22 @@ class PermissionsFragment : Fragment() { ) { requestPermissionLauncher.launch(arrayOf(Manifest.permission.MANAGE_OWN_CALLS)) } + + if (!Compatibility.hasFullScreenIntentPermission(requireContext())) { + Log.w( + "$TAG Android 14 or newer detected & full screen intent permission hasn't been granted!" + ) + Compatibility.requestFullScreenIntentPermission(requireContext()) + } + } + + override fun onResume() { + super.onResume() + + if (areAllPermissionsGranted()) { + Log.i("$TAG All permissions have been granted, skipping") + goToLoginFragment() + } } private fun goToLoginFragment() { @@ -107,11 +118,12 @@ class PermissionsFragment : Fragment() { } private fun areAllPermissionsGranted(): Boolean { - for (permission in AssistantActivity.PERMISSIONS) { + for (permission in Compatibility.getAllRequiredPermissionsArray()) { if (ContextCompat.checkSelfPermission(requireContext(), permission) != PackageManager.PERMISSION_GRANTED) { + Log.w("$TAG Permission [$permission] hasn't been granted yet!") return false } } - return true + return Compatibility.hasFullScreenIntentPermission(requireContext()) } } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt index 9e86b644b..4529c4792 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractTopBarViewModel.kt @@ -119,13 +119,18 @@ open class AbstractTopBarViewModel @UiThread constructor() : ViewModel() { } @WorkerThread - override fun onDefaultAccountChanged(core: Core, defaultAccount: Account) { - Log.i( - "$TAG Default account has changed [${defaultAccount.params.identityAddress?.asStringUriOnly()}]" - ) - + override fun onDefaultAccountChanged(core: Core, defaultAccount: Account?) { account.value?.destroy() - account.postValue(AccountModel(defaultAccount)) + + if (defaultAccount == null) { + Log.w("$TAG Default account is now null!") + } else { + Log.i( + "$TAG Default account has changed [${defaultAccount.params.identityAddress?.asStringUriOnly()}]" + ) + + account.postValue(AccountModel(defaultAccount)) + } updateUnreadMessagesCount() updateMissedCallsCount() diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt index 493691b35..8f7acdf69 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt @@ -57,22 +57,26 @@ class DrawerMenuViewModel @UiThread constructor() : ViewModel() { private val coreListener = object : CoreListenerStub() { @WorkerThread - override fun onDefaultAccountChanged(core: Core, account: Account) { - Log.i( - "$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been set as default" - ) - for (model in accounts.value.orEmpty()) { - if (model.account != account) { - model.isDefault.postValue(false) + override fun onDefaultAccountChanged(core: Core, account: Account?) { + if (account == null) { + Log.w("$TAG Default account is now null!") + } else { + Log.i( + "$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been set as default" + ) + for (model in accounts.value.orEmpty()) { + if (model.account != account) { + model.isDefault.postValue(false) + } } + defaultAccountChangedEvent.postValue( + Event(account.params.identityAddress?.asStringUriOnly() ?: "") + ) } - defaultAccountChangedEvent.postValue( - Event(account.params.identityAddress?.asStringUriOnly() ?: "") - ) } @WorkerThread - override fun onNewAccountAdded(core: Core, account: Account) { + override fun onAccountAdded(core: Core, account: Account) { Log.i( "$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been added to the Core" ) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index dda8f3548..85ec90ddb 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -172,23 +172,28 @@ class MainViewModel @UiThread constructor() : ViewModel() { } @WorkerThread - override fun onDefaultAccountChanged(core: Core, account: Account) { - Log.i( - "$TAG Default account changed, now is [${account.params.identityAddress?.asStringUriOnly()}]" - ) - removeAlert(NON_DEFAULT_ACCOUNT_NOTIFICATIONS) - - if (defaultAccountRegistrationFailed && account.state != RegistrationState.Failed) { + override fun onDefaultAccountChanged(core: Core, account: Account?) { + if (account == null) { + Log.w("$TAG Default account is now null!") + } else { Log.i( - "$TAG Newly set default account isn't in failed registration state, clearing alert" + "$TAG Default account changed, now is [${account.params.identityAddress?.asStringUriOnly()}]" ) - defaultAccountRegistrationFailed = false - defaultAccountRegistrationErrorEvent.postValue(Event(false)) - // Refresh REGISTER to re-compute alerts regarding accounts registration state - core.refreshRegisters() + if (defaultAccountRegistrationFailed && account.state != RegistrationState.Failed) { + Log.i( + "$TAG Newly set default account isn't in failed registration state, clearing alert" + ) + defaultAccountRegistrationFailed = false + defaultAccountRegistrationErrorEvent.postValue(Event(false)) + + // Refresh REGISTER to re-compute alerts regarding accounts registration state + core.refreshRegisters() + } } + removeAlert(NON_DEFAULT_ACCOUNT_NOTIFICATIONS) + // TODO: compute other calls notifications count } }