Added full screen intent permission for Android 14 + updated callbacks due to rework

This commit is contained in:
Sylvain Berfini 2023-11-30 11:04:09 +01:00
parent 3e7e2000d5
commit b80a86a366
9 changed files with 212 additions and 57 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> {
return arrayOf(
Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
)
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}
}

View file

@ -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<String> {
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
}
}
}

View file

@ -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,

View file

@ -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
}
}

View file

@ -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())
}
}

View file

@ -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()

View file

@ -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"
)

View file

@ -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
}
}