diff --git a/changelog.d/3667.feature b/changelog.d/3667.feature new file mode 100644 index 0000000000..439a890dd0 --- /dev/null +++ b/changelog.d/3667.feature @@ -0,0 +1 @@ +Better management of permission requests \ No newline at end of file diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml index b97384099f..8ffcec6bc1 100644 --- a/vector/src/debug/AndroidManifest.xml +++ b/vector/src/debug/AndroidManifest.xml @@ -4,6 +4,7 @@ <application> <activity android:name=".features.debug.TestLinkifyActivity" /> + <activity android:name=".features.debug.DebugPermissionActivity" /> <activity android:name=".features.debug.sas.DebugSasEmojiActivity" /> </application> diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index 539091c7ce..4b5228d199 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -30,9 +30,8 @@ import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityDebugMenuBinding import im.vector.app.features.debug.sas.DebugSasEmojiActivity @@ -48,7 +47,6 @@ import im.vector.lib.ui.styles.debug.DebugVectorButtonStylesLightActivity import im.vector.lib.ui.styles.debug.DebugVectorTextViewDarkActivity import im.vector.lib.ui.styles.debug.DebugVectorTextViewLightActivity import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData - import timber.log.Timber import javax.inject.Inject @@ -115,6 +113,9 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() { } views.debugTestCrash.setOnClickListener { testCrash() } views.debugScanQrCode.setOnClickListener { scanQRCode() } + views.debugPermission.setOnClickListener { + startActivity(Intent(this, DebugPermissionActivity::class.java)) + } } private fun renderQrCode(text: String) { @@ -217,15 +218,13 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() { } private fun scanQRCode() { - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, permissionCameraLauncher)) { doScanQRCode() } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && allGranted(grantResults)) { + private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ -> + if (allGranted) { doScanQRCode() } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt new file mode 100644 index 0000000000..048c64bc3a --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.debug + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog +import im.vector.app.core.utils.onPermissionDeniedSnackbar +import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.app.databinding.ActivityDebugPermissionBinding +import timber.log.Timber + +class DebugPermissionActivity : VectorBaseActivity<ActivityDebugPermissionBinding>() { + + override fun getBinding() = ActivityDebugPermissionBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + // For debug + private val allPermissions = listOf( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.READ_CONTACTS) + + private var lastPermissions = emptyList<String>() + + override fun initUiAndData() { + views.status.setOnClickListener { refresh() } + + views.camera.setOnClickListener { + lastPermissions = listOf(Manifest.permission.CAMERA) + checkPerm() + } + views.audio.setOnClickListener { + lastPermissions = listOf(Manifest.permission.RECORD_AUDIO) + checkPerm() + } + views.cameraAudio.setOnClickListener { + lastPermissions = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + checkPerm() + } + views.write.setOnClickListener { + lastPermissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + checkPerm() + } + views.read.setOnClickListener { + lastPermissions = listOf(Manifest.permission.READ_EXTERNAL_STORAGE) + checkPerm() + } + views.contact.setOnClickListener { + lastPermissions = listOf(Manifest.permission.READ_CONTACTS) + checkPerm() + } + } + + private fun checkPerm() { + if (checkPermissions(lastPermissions, this, launcher, R.string.debug_rationale)) { + Toast.makeText(this, "Already granted, sync call", Toast.LENGTH_SHORT).show() + } + } + + private var dialogOrSnackbar = false + + private val launcher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + Toast.makeText(this, "All granted", Toast.LENGTH_SHORT).show() + } else { + if (deniedPermanently) { + dialogOrSnackbar = !dialogOrSnackbar + if (dialogOrSnackbar) { + onPermissionDeniedDialog(R.string.denied_permission_generic) + } else { + onPermissionDeniedSnackbar(R.string.denied_permission_generic) + } + } else { + Toast.makeText(this, "Denied", Toast.LENGTH_SHORT).show() + } + } + } + + override fun onResume() { + super.onResume() + refresh() + } + + private fun refresh() { + views.status.text = getStatus() + } + + private fun getStatus(): String { + return buildString { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Timber.v("## debugPermission() : log the permissions status used by the app") + allPermissions.forEach { permission -> + append("[$permission] : ") + if (ContextCompat.checkSelfPermission(this@DebugPermissionActivity, permission) == PackageManager.PERMISSION_GRANTED) { + append("PERMISSION_GRANTED") + } else { + append("PERMISSION_DENIED") + } + append(" show rational: ") + append(ActivityCompat.shouldShowRequestPermissionRationale(this@DebugPermissionActivity, permission)) + append("\n") + } + } else { + append("Before M!") + } + append("\n") + append("(Click to refresh)") + } + } +} diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml index a83f61266a..fadffecf83 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -152,6 +152,12 @@ android:layout_height="200dp" tools:src="@drawable/ic_qr_code_add" /> + <Button + android:id="@+id/debug_permission" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Permissions" /> + </LinearLayout> </ScrollView> diff --git a/vector/src/debug/res/layout/activity_debug_permission.xml b/vector/src/debug/res/layout/activity_debug_permission.xml new file mode 100644 index 0000000000..6340d8faa7 --- /dev/null +++ b/vector/src/debug/res/layout/activity_debug_permission.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/coordinatorLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".features.debug.DebugPermissionActivity" + tools:ignore="HardcodedText"> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:divider="@drawable/linear_divider" + android:gravity="center_horizontal" + android:orientation="vertical" + android:padding="@dimen/layout_horizontal_margin" + android:showDividers="middle"> + + <TextView + android:id="@+id/status" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:text="Status" /> + + <Button + android:id="@+id/camera" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="CAMERA" + android:textAllCaps="false" /> + + <Button + android:id="@+id/audio" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="RECORD_AUDIO" + android:textAllCaps="false" /> + + <Button + android:id="@+id/camera_audio" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="CAMERA + RECORD_AUDIO" + android:textAllCaps="false" /> + + <Button + android:id="@+id/write" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="WRITE_EXTERNAL_STORAGE" + android:textAllCaps="false" /> + + <Button + android:id="@+id/read" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="READ_EXTERNAL_STORAGE" + android:textAllCaps="false" /> + + <Button + android:id="@+id/contact" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="READ_CONTACTS" + android:textAllCaps="false" /> + + </LinearLayout> + + </ScrollView> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/vector/src/debug/res/values/strings.xml b/vector/src/debug/res/values/strings.xml new file mode 100644 index 0000000000..a7b8e38634 --- /dev/null +++ b/vector/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="debug_rationale">Rationale!</string> +</resources> \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt index 3b25fd3f89..23c2e13f6f 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt @@ -29,6 +29,7 @@ import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.features.media.createUCropWithDefaultSettings import im.vector.lib.multipicker.MultiPicker @@ -55,9 +56,11 @@ class GalleryOrCameraDialogHelper( private val listener = fragment as? Listener ?: error("Fragment must implement GalleryOrCameraDialogHelper.Listener") - private val takePhotoPermissionActivityResultLauncher = fragment.registerForPermissionsResult { allGranted -> + private val takePhotoPermissionActivityResultLauncher = fragment.registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { doOpenCamera() + } else if (deniedPermanently) { + activity.onPermissionDeniedDialog(R.string.denied_permission_camera) } } @@ -116,7 +119,7 @@ class GalleryOrCameraDialogHelper( private fun onAvatarTypeSelected(type: Type) { when (type) { - Type.Camera -> + Type.Camera -> if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) { doOpenCamera() } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 899a99c314..61abbd445b 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -32,7 +32,6 @@ import androidx.annotation.MainThread import androidx.annotation.MenuRes import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.appbar.MaterialToolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -42,6 +41,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import androidx.viewbinding.ViewBinding import com.bumptech.glide.util.Util +import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding3.view.clicks import im.vector.app.BuildConfig @@ -82,14 +82,13 @@ import im.vector.app.receivers.DebugReceiver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable - import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import timber.log.Timber import java.util.concurrent.TimeUnit import kotlin.system.measureTimeMillis -abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScreenInjector { +abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), HasScreenInjector { /* ========================================================================================== * View * ========================================================================================== */ @@ -596,12 +595,19 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr } fun showSnackbar(message: String, @StringRes withActionTitle: Int?, action: (() -> Unit)?) { - getCoordinatorLayout()?.let { - Snackbar.make(it, message, Snackbar.LENGTH_LONG).apply { + val coordinatorLayout = getCoordinatorLayout() + if (coordinatorLayout != null) { + Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).apply { withActionTitle?.let { - setAction(withActionTitle, { action?.invoke() }) + setAction(withActionTitle) { action?.invoke() } } }.show() + } else { + if (vectorPreferences.failFast()) { + error("No CoordinatorLayout to display this snackbar!") + } else { + Timber.w("No CoordinatorLayout to display this snackbar!") + } } } diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index b6566b4ce9..4268a034f5 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -18,117 +18,67 @@ package im.vector.app.core.utils import android.Manifest import android.app.Activity -import android.content.Context import android.content.pm.PackageManager -import android.os.Build -import android.widget.Toast +import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity -import timber.log.Timber - -// Android M permission request code management -private const val PERMISSIONS_GRANTED = true -private const val PERMISSIONS_DENIED = !PERMISSIONS_GRANTED - -// Permission bit -private const val PERMISSION_BYPASSED = 0x0 -const val PERMISSION_CAMERA = 0x1 -private const val PERMISSION_WRITE_EXTERNAL_STORAGE = 0x1 shl 1 -private const val PERMISSION_RECORD_AUDIO = 0x1 shl 2 -private const val PERMISSION_READ_CONTACTS = 0x1 shl 3 -private const val PERMISSION_READ_EXTERNAL_STORAGE = 0x1 shl 4 // Permissions sets -const val PERMISSIONS_FOR_AUDIO_IP_CALL = PERMISSION_RECORD_AUDIO -const val PERMISSIONS_FOR_VIDEO_IP_CALL = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO -const val PERMISSIONS_FOR_TAKING_PHOTO = PERMISSION_CAMERA -const val PERMISSIONS_FOR_MEMBERS_SEARCH = PERMISSION_READ_CONTACTS -const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS -const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA -const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO -const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE -const val PERMISSIONS_FOR_READING_FILES = PERMISSION_READ_EXTERNAL_STORAGE -const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS +val PERMISSIONS_FOR_AUDIO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO) +val PERMISSIONS_FOR_VIDEO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) +val PERMISSIONS_FOR_TAKING_PHOTO = listOf(Manifest.permission.CAMERA) +val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS) +val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) +val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) +val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) -const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED +val PERMISSIONS_EMPTY = emptyList<String>() -// Request code to ask permission to the system (arbitrary values) -const val PERMISSION_REQUEST_CODE = 567 -const val PERMISSION_REQUEST_CODE_LAUNCH_CAMERA = 568 -const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA = 569 -const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA = 570 -const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571 -const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572 -const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574 -const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 -const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576 -const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577 -const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579 +// This is not ideal to store the value like that, but it works +private var permissionDialogDisplayed = false /** - * Log the used permissions statuses. + * First boolean is true if all permissions have been granted + * Second boolean is true if the permission is denied forever AND the permission request has not been displayed. + * So when the user does not grant the permission and check the box do not ask again, this boolean will be false. + * Only useful if the first boolean is false */ -fun logPermissionStatuses(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val permissions = listOf( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.READ_CONTACTS) +fun ComponentActivity.registerForPermissionsResult(lambda: (allGranted: Boolean, deniedPermanently: Boolean) -> Unit) + : ActivityResultLauncher<Array<String>> { + return registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + onPermissionResult(result, lambda) + } +} - Timber.v("## logPermissionStatuses() : log the permissions status used by the app") +fun Fragment.registerForPermissionsResult(lambda: (allGranted: Boolean, deniedPermanently: Boolean) -> Unit): ActivityResultLauncher<Array<String>> { + return registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + onPermissionResult(result, lambda) + } +} - for (permission in permissions) { - Timber.v(("Status of [$permission] : " + - if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, permission)) { - "PERMISSION_GRANTED" - } else { - "PERMISSION_DENIED" - })) +private fun onPermissionResult(result: Map<String, Boolean>, lambda: (allGranted: Boolean, deniedPermanently: Boolean) -> Unit) { + if (result.keys.all { result[it] == true }) { + lambda(true, /* not used */ false) + } else { + if (permissionDialogDisplayed) { + // A permission dialog has been displayed, so even if the user has checked the do not ask again button, we do + // not tell the user to open the app settings + lambda(false, false) + } else { + // No dialog has been displayed, so tell the user to go to the system setting + lambda(false, true) } } -} - -fun Fragment.registerForPermissionsResult(allGranted: (Boolean) -> Unit): ActivityResultLauncher<Array<String>> { - return registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> - allGranted.invoke(result.keys.all { result[it] == true }) - } -} - -/** - * See [.checkPermissions] - * - * @param permissionsToBeGrantedBitMap - * @param activity - * @return true if the permissions are granted (synchronous flow), false otherwise (asynchronous flow) - */ -fun checkPermissions(permissionsToBeGrantedBitMap: Int, - activity: Activity, - requestCode: Int, - @StringRes rationaleMessage: Int = 0): Boolean { - return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode, rationaleMessage) -} - -/** - * See [.checkPermissions] - * - * @param permissionsToBeGrantedBitMap - * @param activityResultLauncher from the calling fragment that is requesting the permissions - * @return true if the permissions are granted (synchronous flow), false otherwise (asynchronous flow) - */ -fun checkPermissions(permissionsToBeGrantedBitMap: Int, - activity: Activity, - activityResultLauncher: ActivityResultLauncher<Array<String>>, - @StringRes rationaleMessage: Int = 0): Boolean { - return checkPermissions(permissionsToBeGrantedBitMap, activity, activityResultLauncher, 0, rationaleMessage) + // Reset + permissionDialogDisplayed = false } /** @@ -144,145 +94,65 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int, * If a permission was already denied by the user, a popup is displayed to * explain why vector needs the corresponding permission. * - * @param permissionsToBeGrantedBitMap the permissions bit map to be granted - * @param activity the calling Activity that is requesting the permissions (or fragment parent) - * @param activityResultLauncher from the calling fragment that is requesting the permissions + * @param permissionsToBeGranted the permissions to be granted + * @param activity the calling Activity that is requesting the permissions (or fragment parent) + * @param activityResultLauncher from the calling fragment/Activity that is requesting the permissions + * @param rationaleMessage message to be displayed BEFORE requesting for the permission * @return true if the permissions are granted (synchronous flow), false otherwise (asynchronous flow) */ -private fun checkPermissions(permissionsToBeGrantedBitMap: Int, - activity: Activity, - activityResultLauncher: ActivityResultLauncher<Array<String>>?, - requestCode: Int, - @StringRes rationaleMessage: Int -): Boolean { - var isPermissionGranted = false +fun checkPermissions(permissionsToBeGranted: List<String>, + activity: Activity, + activityResultLauncher: ActivityResultLauncher<Array<String>>, + @StringRes rationaleMessage: Int = 0): Boolean { + // retrieve the permissions to be granted according to the permission list + val missingPermissions = permissionsToBeGranted.filter { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_DENIED + } - // sanity check - if (PERMISSIONS_EMPTY == permissionsToBeGrantedBitMap) { - isPermissionGranted = true - } else if (PERMISSIONS_FOR_AUDIO_IP_CALL != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_VIDEO_IP_CALL != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_TAKING_PHOTO != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_MEMBERS_SEARCH != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_MEMBER_DETAILS != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_ROOM_AVATAR != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_VIDEO_RECORDING != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_WRITING_FILES != permissionsToBeGrantedBitMap - && PERMISSIONS_FOR_READING_FILES != permissionsToBeGrantedBitMap) { - Timber.w("## checkPermissions(): permissions to be granted are not supported") - isPermissionGranted = false - } else { - val permissionListAlreadyDenied = ArrayList<String>() - val permissionsListToBeGranted = ArrayList<String>() - var isRequestPermissionRequired = false + return if (missingPermissions.isNotEmpty()) { + permissionDialogDisplayed = !permissionsDeniedPermanently(missingPermissions, activity) - // retrieve the permissions to be granted according to the request code bit map - if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) { - val permissionType = Manifest.permission.CAMERA - isRequestPermissionRequired = isRequestPermissionRequired or - updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType) - } - - if (PERMISSION_RECORD_AUDIO == permissionsToBeGrantedBitMap and PERMISSION_RECORD_AUDIO) { - val permissionType = Manifest.permission.RECORD_AUDIO - isRequestPermissionRequired = isRequestPermissionRequired or - updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType) - } - - if (PERMISSION_WRITE_EXTERNAL_STORAGE == permissionsToBeGrantedBitMap and PERMISSION_WRITE_EXTERNAL_STORAGE) { - val permissionType = Manifest.permission.WRITE_EXTERNAL_STORAGE - isRequestPermissionRequired = isRequestPermissionRequired or - updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType) - } - - if (PERMISSION_READ_EXTERNAL_STORAGE == permissionsToBeGrantedBitMap and PERMISSION_READ_EXTERNAL_STORAGE) { - val permissionType = Manifest.permission.READ_EXTERNAL_STORAGE - isRequestPermissionRequired = isRequestPermissionRequired or - updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType) - } - - // the contact book access is requested for any android platforms - // for android M, we use the system preferences - // for android < M, we use a dedicated settings - if (PERMISSION_READ_CONTACTS == permissionsToBeGrantedBitMap and PERMISSION_READ_CONTACTS) { - val permissionType = Manifest.permission.READ_CONTACTS - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - isRequestPermissionRequired = isRequestPermissionRequired or - updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType) - } else { - // TODO uncomment - /*if (!ContactsManager.getInstance().isContactBookAccessRequested) { - isRequestPermissionRequired = true - permissionsListToBeGranted.add(permissionType) - }*/ - } - } - - // if some permissions were already denied: display a dialog to the user before asking again. - if (permissionListAlreadyDenied.isNotEmpty() && rationaleMessage != 0) { - // display the dialog with the info text + if (rationaleMessage != 0 && permissionDialogDisplayed) { + // display the dialog with the info text. Do not do it if no system dialog will + // be displayed MaterialAlertDialogBuilder(activity) .setTitle(R.string.permissions_rationale_popup_title) .setMessage(rationaleMessage) - .setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() } + .setCancelable(false) .setPositiveButton(R.string.ok) { _, _ -> - if (permissionsListToBeGranted.isNotEmpty()) { - activityResultLauncher - ?.launch(permissionsListToBeGranted.toTypedArray()) - ?: run { - ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode) - } - } + activityResultLauncher.launch(missingPermissions.toTypedArray()) } .show() } else { // some permissions are not granted, ask permissions - if (isRequestPermissionRequired) { - val permissionsArrayToBeGranted = permissionsListToBeGranted.toTypedArray() - - // for android < M, we use a custom dialog to request the contacts book access. - if (permissionsListToBeGranted.contains(Manifest.permission.READ_CONTACTS) - && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - TODO() - /* - MaterialAlertDialogBuilder(activity) - .setIcon(android.R.drawable.ic_dialog_info) - .setTitle(R.string.permissions_rationale_popup_title) - .setMessage(R.string.permissions_msg_contacts_warning_other_androids) - // gives the contacts book access - .setPositiveButton(R.string.yes) { _, _ -> - ContactsManager.getInstance().setIsContactBookAccessAllowed(true) - fragment?.requestPermissions(permissionsArrayToBeGranted, requestCode) - ?: run { - ActivityCompat.requestPermissions(activity, permissionsArrayToBeGranted, requestCode) - } - } - // or reject it - .setNegativeButton(R.string.no) { _, _ -> - ContactsManager.getInstance().setIsContactBookAccessAllowed(false) - fragment?.requestPermissions(permissionsArrayToBeGranted, requestCode) - ?: run { - ActivityCompat.requestPermissions(activity, permissionsArrayToBeGranted, requestCode) - } - } - .show() - */ - } else { - activityResultLauncher - ?.launch(permissionsArrayToBeGranted) - ?: run { - ActivityCompat.requestPermissions(activity, permissionsArrayToBeGranted, requestCode) - } - } - } else { - // permissions were granted, start now. - isPermissionGranted = true - } + activityResultLauncher.launch(missingPermissions.toTypedArray()) } + false + } else { + // permissions were granted, start now. + true } +} - return isPermissionGranted +/** + * To be call after the permission request + * + * @param permissionsToBeGranted the permissions to be granted + * @param activity the calling Activity that is requesting the permissions (or fragment parent) + * + * @return true if one of the permission has been denied and the user check the do not ask again checkbox + */ +private fun permissionsDeniedPermanently(permissionsToBeGranted: List<String>, + activity: Activity): Boolean { + return permissionsToBeGranted + .filter { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_DENIED + } + .any { permission -> + // If shouldShowRequestPermissionRationale() returns true, it means that the user as denied the permission, but not permanently. + // If it return false, it mean that the user as denied permanently the permission + ActivityCompat.shouldShowRequestPermissionRationale(activity, permission).not() + } } fun VectorBaseActivity<*>.onPermissionDeniedSnackbar(@StringRes rationaleMessage: Int) { @@ -291,50 +161,13 @@ fun VectorBaseActivity<*>.onPermissionDeniedSnackbar(@StringRes rationaleMessage } } -/** - * Helper method used in [.checkPermissions] to populate the list of the - * permissions to be granted (permissionsListToBeGrantedOut) and the list of the permissions already denied (permissionAlreadyDeniedListOut). - * - * @param activity calling activity - * @param permissionAlreadyDeniedListOut list to be updated with the permissions already denied by the user - * @param permissionsListToBeGrantedOut list to be updated with the permissions to be granted - * @param permissionType the permission to be checked - * @return true if the permission requires to be granted, false otherwise - */ -private fun updatePermissionsToBeGranted(activity: Activity, - permissionAlreadyDeniedListOut: MutableList<String>, - permissionsListToBeGrantedOut: MutableList<String>, - permissionType: String): Boolean { - var isRequestPermissionRequested = false - - // add permission to be granted - permissionsListToBeGrantedOut.add(permissionType) - - if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(activity.applicationContext, permissionType)) { - isRequestPermissionRequested = true - - // add permission to the ones that were already asked to the user - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permissionType)) { - permissionAlreadyDeniedListOut.add(permissionType) - } - } - return isRequestPermissionRequested -} - -/** - * Return true if all permissions are granted, false if not or if permission request has been cancelled - */ -fun allGranted(grantResults: IntArray): Boolean { - if (grantResults.isEmpty()) { - // A cancellation occurred - return false - } - - var granted = true - - grantResults.forEach { - granted = granted && PackageManager.PERMISSION_GRANTED == it - } - - return granted +fun FragmentActivity.onPermissionDeniedDialog(@StringRes rationaleMessage: Int) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.missing_permissions_title) + .setMessage(rationaleMessage) + .setPositiveButton(R.string.open_settings) { _, _ -> + openAppSettingsPage(this) + } + .setNegativeButton(R.string.cancel, null) + .show() } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index cf7270225d..c0d4669108 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -206,7 +206,7 @@ class AttachmentTypeSelectorView(context: Context, /** * The all possible types to pick with their required permissions. */ - enum class Type(val permissionsBit: Int) { + enum class Type(val permissions: List<String>) { CAMERA(PERMISSIONS_FOR_TAKING_PHOTO), GALLERY(PERMISSIONS_EMPTY), FILE(PERMISSIONS_EMPTY), diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 21939bd42b..7e84811102 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -40,8 +40,8 @@ import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL -import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.ActivityCallBinding import im.vector.app.features.call.dialpad.CallDialPadBottomSheet import im.vector.app.features.call.dialpad.DialPadFragment @@ -139,11 +139,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro .disposeOnDestroy() if (callArgs.isVideoCall) { - if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_camera_and_audio)) { + if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) { start() } } else { - if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_record_audio)) { + if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) { start() } } @@ -298,9 +298,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == CAPTURE_PERMISSION_REQUEST_CODE && allGranted(grantResults)) { + private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ -> + if (allGranted) { start() } else { // TODO display something @@ -370,8 +369,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro } companion object { - - private const val CAPTURE_PERMISSION_REQUEST_CODE = 1 private const val EXTRA_MODE = "EXTRA_MODE" private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG" diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 4aa5f023c4..68123d5e82 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -38,11 +38,9 @@ import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS -import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedSnackbar +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.contactsbook.ContactsBookViewState @@ -52,7 +50,6 @@ import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel import im.vector.app.features.userdirectory.UserListViewModel import im.vector.app.features.userdirectory.UserListViewState - import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import java.net.HttpURLConnection @@ -111,35 +108,31 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac } private fun openAddByQrCode() { - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA, 0)) { + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, permissionCameraLauncher)) { addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java) } } private fun openPhoneBook() { // Check permission first - if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, - this, - PERMISSION_REQUEST_CODE_READ_CONTACTS, - 0)) { + if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, this, permissionReadContactLauncher)) { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (allGranted(grantResults)) { - if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { - doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } - } else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { - addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java) - } - } else { - if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { - onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) - } else if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { - onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact) - } + private val permissionReadContactLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } + } else if (deniedPermanently) { + onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact) + } + } + + private val permissionCameraLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java) + } else if (deniedPermanently) { + onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt index 92a03c5483..8da0147a43 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt @@ -27,6 +27,7 @@ import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentQrCodeScannerBinding import im.vector.app.features.userdirectory.PendingSelection @@ -44,9 +45,11 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen return FragmentQrCodeScannerBinding.inflate(inflater, container, false) } - private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { startCamera() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_camera) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt index 5d114b26bf..d3f24816a5 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt @@ -23,12 +23,14 @@ import android.view.ViewGroup import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState +import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.features.crypto.verification.VerificationAction @@ -79,9 +81,11 @@ class VerificationChooseMethodFragment @Inject constructor( state.pendingRequest.invoke()?.transactionId ?: "")) } - private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { doOpenQRCodeScanner() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_camera) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index c4caff025b..b88a1a6e3a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -101,6 +101,7 @@ import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.isValidUrl +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.saveMedia @@ -1062,14 +1063,16 @@ class RoomDetailFragment @Inject constructor( } } - private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted -> + private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { (roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let { roomDetailViewModel.pendingAction = null roomDetailViewModel.handle(it) } } else { - context?.toast(R.string.permissions_action_not_performed_missing_permissions) + if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_generic) + } cleanUpAfterPermissionNotGranted() } } @@ -1738,13 +1741,16 @@ class RoomDetailFragment @Inject constructor( } } - private val saveActionActivityResultLauncher = registerForPermissionsResult { allGranted -> + private val saveActionActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { sharedActionViewModel.pendingAction?.let { handleActions(it) sharedActionViewModel.pendingAction = null } } else { + if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_generic) + } cleanUpAfterPermissionNotGranted() } } @@ -1977,7 +1983,7 @@ class RoomDetailFragment @Inject constructor( // AttachmentTypeSelectorView.Callback - private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted -> + private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { val pendingType = attachmentsHelper.pendingType if (pendingType != null) { @@ -1985,12 +1991,15 @@ class RoomDetailFragment @Inject constructor( launchAttachmentProcess(pendingType) } } else { + if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_generic) + } cleanUpAfterPermissionNotGranted() } } override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { - if (checkPermissions(type.permissionsBit, requireActivity(), typeSelectedActivityResultLauncher)) { + if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) { launchAttachmentProcess(type) } else { attachmentsHelper.pendingType = type diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index 142498e031..88998861bc 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -21,7 +21,6 @@ import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.view.View -import android.widget.Toast import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -33,9 +32,9 @@ import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH -import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS -import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedSnackbar +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.toast import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel @@ -118,22 +117,16 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa private fun openPhoneBook() { // Check permission first - if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, - this, - PERMISSION_REQUEST_CODE_READ_CONTACTS, - 0)) { + if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, this, permissionContactLauncher)) { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (allGranted(grantResults)) { - if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { - doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } - } - } else { - Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show() + private val permissionContactLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } + } else if (deniedPermanently) { + onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact) } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt index 2d03c7c4ca..da9c6792ff 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt @@ -65,7 +65,7 @@ class ScanUserCodeFragment @Inject constructor() } } - private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, _ -> if (allGranted) { startCamera() } else { @@ -112,7 +112,7 @@ class ScanUserCodeFragment @Inject constructor() super.onResume() // Register ourselves as a handler for scan results. views.userCodeScannerView.setResultHandler(this) - if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)) { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { startCamera() } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt index 042681d780..c451118813 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt @@ -43,11 +43,11 @@ class ShowUserCodeFragment @Inject constructor( val sharedViewModel: UserCodeSharedViewModel by activityViewModel() - private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { doOpenQRCodeScanner() } else { - sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted) + sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted(deniedPermanently)) } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt index 3411fe3d7f..25a7bab7da 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt @@ -24,6 +24,6 @@ sealed class UserCodeActions : VectorViewModelAction { data class SwitchMode(val mode: UserCodeState.Mode) : UserCodeActions() data class DecodedQRCode(val code: String) : UserCodeActions() data class StartChattingWithUser(val matrixItem: MatrixItem) : UserCodeActions() - object CameraPermissionNotGranted : UserCodeActions() + data class CameraPermissionNotGranted(val deniedPermanently: Boolean) : UserCodeActions() object ShareByText : UserCodeActions() } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt index 6cdde6c880..0771a5d238 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -81,13 +81,17 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(), sharedViewModel.observeViewEvents { when (it) { - UserCodeShareViewEvents.Dismiss -> ActivityCompat.finishAfterTransition(this) - UserCodeShareViewEvents.ShowWaitingScreen -> views.simpleActivityWaitingView.isVisible = true - UserCodeShareViewEvents.HideWaitingScreen -> views.simpleActivityWaitingView.isVisible = false - is UserCodeShareViewEvents.ToastMessage -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() - is UserCodeShareViewEvents.NavigateToRoom -> navigator.openRoom(this, it.roomId) - UserCodeShareViewEvents.CameraPermissionNotGranted -> onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) - else -> { + UserCodeShareViewEvents.Dismiss -> ActivityCompat.finishAfterTransition(this) + UserCodeShareViewEvents.ShowWaitingScreen -> views.simpleActivityWaitingView.isVisible = true + UserCodeShareViewEvents.HideWaitingScreen -> views.simpleActivityWaitingView.isVisible = false + is UserCodeShareViewEvents.ToastMessage -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() + is UserCodeShareViewEvents.NavigateToRoom -> navigator.openRoom(this, it.roomId) + is UserCodeShareViewEvents.CameraPermissionNotGranted -> { + if (it.deniedPermanently) { + onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) + } + } + else -> { } } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt index 67a1ab8a6c..eaa3b46af1 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt @@ -24,6 +24,6 @@ sealed class UserCodeShareViewEvents : VectorViewEvents { object HideWaitingScreen : UserCodeShareViewEvents() data class ToastMessage(val message: String) : UserCodeShareViewEvents() data class NavigateToRoom(val roomId: String) : UserCodeShareViewEvents() - object CameraPermissionNotGranted : UserCodeShareViewEvents() + data class CameraPermissionNotGranted(val deniedPermanently: Boolean) : UserCodeShareViewEvents() data class SharePlainText(val text: String, val title: String, val richPlainText: String) : UserCodeShareViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index 9637b72581..071044fc8a 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -76,7 +76,7 @@ class UserCodeSharedViewModel @AssistedInject constructor( is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) - UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted) + is UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted(action.deniedPermanently)) UserCodeActions.ShareByText -> handleShareByText() } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 0159c98c20..6c18eb2db5 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -386,12 +386,16 @@ <string name="reset">Reset</string> <string name="start_chatting">Start Chatting</string> + <!-- Permissions denied forever --> + <string name="denied_permission_generic">Some permissions are missing to perform this action, please grant the permissions from the system settings.</string> + <string name="denied_permission_camera">To perform this action, please grant the Camera permission from the system settings.</string> <!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video --> <string name="ongoing_conference_call">Ongoing conference call.\nJoin as %1$s or %2$s</string> <string name="ongoing_conference_call_voice">Voice</string> <string name="ongoing_conference_call_video">Video</string> <string name="cannot_start_call">Cannot start the call, please try later</string> + <string name="missing_permissions_title">Missing permissions</string> <string name="missing_permissions_warning">"Due to missing permissions, some features may be missing…</string> <string name="missing_permissions_error">"Due to missing permissions, this action is not possible.</string> <string name="missing_permissions_to_start_conf_call">You need permission to invite to start a conference in this room</string>