Merge branch 'develop' into feature/ons/voice_message

This commit is contained in:
Benoit Marty 2021-07-13 10:36:05 +02:00
commit 9df874c975
29 changed files with 423 additions and 348 deletions

1
changelog.d/3655.bugfix Normal file
View file

@ -0,0 +1 @@
Fix unread messages marker being hidden in collapsed membership item

1
changelog.d/3661.bugfix Normal file
View file

@ -0,0 +1 @@
Ensure reaction emoji picker tabs look fine on small displays

1
changelog.d/3667.feature Normal file
View file

@ -0,0 +1 @@
Better management of permission requests

View file

@ -97,7 +97,7 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
// Sign userSigningKey with master // Sign selfSigningKey with master
val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
.key(sskPublicKey) .key(sskPublicKey)
.build() .build()

View file

@ -4,6 +4,7 @@
<application> <application>
<activity android:name=".features.debug.TestLinkifyActivity" /> <activity android:name=".features.debug.TestLinkifyActivity" />
<activity android:name=".features.debug.DebugPermissionActivity" />
<activity android:name=".features.debug.sas.DebugSasEmojiActivity" /> <activity android:name=".features.debug.sas.DebugSasEmojiActivity" />
</application> </application>

View file

@ -30,9 +30,8 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO 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.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityDebugMenuBinding import im.vector.app.databinding.ActivityDebugMenuBinding
import im.vector.app.features.debug.sas.DebugSasEmojiActivity 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.DebugVectorTextViewDarkActivity
import im.vector.lib.ui.styles.debug.DebugVectorTextViewLightActivity import im.vector.lib.ui.styles.debug.DebugVectorTextViewLightActivity
import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -115,6 +113,9 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() {
} }
views.debugTestCrash.setOnClickListener { testCrash() } views.debugTestCrash.setOnClickListener { testCrash() }
views.debugScanQrCode.setOnClickListener { scanQRCode() } views.debugScanQrCode.setOnClickListener { scanQRCode() }
views.debugPermission.setOnClickListener {
startActivity(Intent(this, DebugPermissionActivity::class.java))
}
} }
private fun renderQrCode(text: String) { private fun renderQrCode(text: String) {
@ -217,15 +218,13 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() {
} }
private fun scanQRCode() { private fun scanQRCode() {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, permissionCameraLauncher)) {
doScanQRCode() doScanQRCode()
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ ->
super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (allGranted) {
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && allGranted(grantResults)) {
doScanQRCode() doScanQRCode()
} }
} }

View file

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

View file

@ -152,6 +152,12 @@
android:layout_height="200dp" android:layout_height="200dp"
tools:src="@drawable/ic_qr_code_add" /> 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> </LinearLayout>
</ScrollView> </ScrollView>

View file

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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="debug_rationale">Rationale!</string>
</resources>

View file

@ -29,6 +29,7 @@ import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions 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.core.utils.registerForPermissionsResult
import im.vector.app.features.media.createUCropWithDefaultSettings import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.lib.multipicker.MultiPicker 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 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) { if (allGranted) {
doOpenCamera() doOpenCamera()
} else if (deniedPermanently) {
activity.onPermissionDeniedDialog(R.string.denied_permission_camera)
} }
} }
@ -116,7 +119,7 @@ class GalleryOrCameraDialogHelper(
private fun onAvatarTypeSelected(type: Type) { private fun onAvatarTypeSelected(type: Type) {
when (type) { when (type) {
Type.Camera -> Type.Camera ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) {
doOpenCamera() doOpenCamera()
} }

View file

@ -32,7 +32,6 @@ import androidx.annotation.MainThread
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.appbar.MaterialToolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -42,6 +41,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.bumptech.glide.util.Util import com.bumptech.glide.util.Util
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.view.clicks
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
@ -82,14 +82,13 @@ import im.vector.app.receivers.DebugReceiver
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.GlobalError
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScreenInjector { abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), HasScreenInjector {
/* ========================================================================================== /* ==========================================================================================
* View * View
* ========================================================================================== */ * ========================================================================================== */
@ -596,12 +595,19 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr
} }
fun showSnackbar(message: String, @StringRes withActionTitle: Int?, action: (() -> Unit)?) { fun showSnackbar(message: String, @StringRes withActionTitle: Int?, action: (() -> Unit)?) {
getCoordinatorLayout()?.let { val coordinatorLayout = getCoordinatorLayout()
Snackbar.make(it, message, Snackbar.LENGTH_LONG).apply { if (coordinatorLayout != null) {
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).apply {
withActionTitle?.let { withActionTitle?.let {
setAction(withActionTitle, { action?.invoke() }) setAction(withActionTitle) { action?.invoke() }
} }
}.show() }.show()
} else {
if (vectorPreferences.failFast()) {
error("No CoordinatorLayout to display this snackbar!")
} else {
Timber.w("No CoordinatorLayout to display this snackbar!")
}
} }
} }

View file

@ -18,117 +18,67 @@ package im.vector.app.core.utils
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import androidx.activity.ComponentActivity
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity 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 // Permissions sets
const val PERMISSIONS_FOR_AUDIO_IP_CALL = PERMISSION_RECORD_AUDIO val PERMISSIONS_FOR_AUDIO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO)
const val PERMISSIONS_FOR_VIDEO_IP_CALL = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO val PERMISSIONS_FOR_VIDEO_IP_CALL = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
const val PERMISSIONS_FOR_TAKING_PHOTO = PERMISSION_CAMERA val PERMISSIONS_FOR_TAKING_PHOTO = listOf(Manifest.permission.CAMERA)
const val PERMISSIONS_FOR_MEMBERS_SEARCH = PERMISSION_READ_CONTACTS val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS)
const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA)
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS)
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
const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED val PERMISSIONS_EMPTY = emptyList<String>()
// Request code to ask permission to the system (arbitrary values) // This is not ideal to store the value like that, but it works
const val PERMISSION_REQUEST_CODE = 567 private var permissionDialogDisplayed = false
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
/** /**
* 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) { fun ComponentActivity.registerForPermissionsResult(lambda: (allGranted: Boolean, deniedPermanently: Boolean) -> Unit)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { : ActivityResultLauncher<Array<String>> {
val permissions = listOf( return registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
Manifest.permission.CAMERA, onPermissionResult(result, lambda)
Manifest.permission.RECORD_AUDIO, }
Manifest.permission.WRITE_EXTERNAL_STORAGE, }
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_CONTACTS)
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) { private fun onPermissionResult(result: Map<String, Boolean>, lambda: (allGranted: Boolean, deniedPermanently: Boolean) -> Unit) {
Timber.v(("Status of [$permission] : " + if (result.keys.all { result[it] == true }) {
if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, permission)) { lambda(true, /* not used */ false)
"PERMISSION_GRANTED" } else {
} else { if (permissionDialogDisplayed) {
"PERMISSION_DENIED" // 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)
} }
} }
} // Reset
permissionDialogDisplayed = false
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)
} }
/** /**
@ -144,145 +94,65 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
* If a permission was already denied by the user, a popup is displayed to * If a permission was already denied by the user, a popup is displayed to
* explain why vector needs the corresponding permission. * explain why vector needs the corresponding permission.
* *
* @param permissionsToBeGrantedBitMap the permissions bit map to be granted * @param permissionsToBeGranted the permissions to be granted
* @param activity the calling Activity that is requesting the permissions (or fragment parent) * @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 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) * @return true if the permissions are granted (synchronous flow), false otherwise (asynchronous flow)
*/ */
private fun checkPermissions(permissionsToBeGrantedBitMap: Int, fun checkPermissions(permissionsToBeGranted: List<String>,
activity: Activity, activity: Activity,
activityResultLauncher: ActivityResultLauncher<Array<String>>?, activityResultLauncher: ActivityResultLauncher<Array<String>>,
requestCode: Int, @StringRes rationaleMessage: Int = 0): Boolean {
@StringRes rationaleMessage: Int // retrieve the permissions to be granted according to the permission list
): Boolean { val missingPermissions = permissionsToBeGranted.filter { permission ->
var isPermissionGranted = false ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_DENIED
}
// sanity check return if (missingPermissions.isNotEmpty()) {
if (PERMISSIONS_EMPTY == permissionsToBeGrantedBitMap) { permissionDialogDisplayed = !permissionsDeniedPermanently(missingPermissions, activity)
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
// retrieve the permissions to be granted according to the request code bit map if (rationaleMessage != 0 && permissionDialogDisplayed) {
if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) { // display the dialog with the info text. Do not do it if no system dialog will
val permissionType = Manifest.permission.CAMERA // be displayed
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
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.permissions_rationale_popup_title) .setTitle(R.string.permissions_rationale_popup_title)
.setMessage(rationaleMessage) .setMessage(rationaleMessage)
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() } .setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
if (permissionsListToBeGranted.isNotEmpty()) { activityResultLauncher.launch(missingPermissions.toTypedArray())
activityResultLauncher
?.launch(permissionsListToBeGranted.toTypedArray())
?: run {
ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode)
}
}
} }
.show() .show()
} else { } else {
// some permissions are not granted, ask permissions // some permissions are not granted, ask permissions
if (isRequestPermissionRequired) { activityResultLauncher.launch(missingPermissions.toTypedArray())
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
}
} }
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) { fun VectorBaseActivity<*>.onPermissionDeniedSnackbar(@StringRes rationaleMessage: Int) {
@ -291,50 +161,13 @@ fun VectorBaseActivity<*>.onPermissionDeniedSnackbar(@StringRes rationaleMessage
} }
} }
/** fun FragmentActivity.onPermissionDeniedDialog(@StringRes rationaleMessage: Int) {
* Helper method used in [.checkPermissions] to populate the list of the MaterialAlertDialogBuilder(this)
* permissions to be granted (permissionsListToBeGrantedOut) and the list of the permissions already denied (permissionAlreadyDeniedListOut). .setTitle(R.string.missing_permissions_title)
* .setMessage(rationaleMessage)
* @param activity calling activity .setPositiveButton(R.string.open_settings) { _, _ ->
* @param permissionAlreadyDeniedListOut list to be updated with the permissions already denied by the user openAppSettingsPage(this)
* @param permissionsListToBeGrantedOut list to be updated with the permissions to be granted }
* @param permissionType the permission to be checked .setNegativeButton(R.string.cancel, null)
* @return true if the permission requires to be granted, false otherwise .show()
*/
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
} }

View file

@ -206,7 +206,7 @@ class AttachmentTypeSelectorView(context: Context,
/** /**
* The all possible types to pick with their required permissions. * 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), CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
GALLERY(PERMISSIONS_EMPTY), GALLERY(PERMISSIONS_EMPTY),
FILE(PERMISSIONS_EMPTY), FILE(PERMISSIONS_EMPTY),

View file

@ -40,8 +40,8 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.platform.VectorBaseActivity 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_AUDIO_IP_CALL
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_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.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.ActivityCallBinding import im.vector.app.databinding.ActivityCallBinding
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.dialpad.DialPadFragment
@ -139,11 +139,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
.disposeOnDestroy() .disposeOnDestroy()
if (callArgs.isVideoCall) { 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() start()
} }
} else { } 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() start()
} }
} }
@ -298,9 +298,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ ->
super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (allGranted) {
if (requestCode == CAPTURE_PERMISSION_REQUEST_CODE && allGranted(grantResults)) {
start() start()
} else { } else {
// TODO display something // TODO display something
@ -370,8 +369,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
companion object { companion object {
private const val CAPTURE_PERMISSION_REQUEST_CODE = 1
private const val EXTRA_MODE = "EXTRA_MODE" private const val EXTRA_MODE = "EXTRA_MODE"
private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG" private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"

View file

@ -38,11 +38,9 @@ import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData 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_MEMBERS_SEARCH
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO 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.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedSnackbar 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.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.contactsbook.ContactsBookViewState 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.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState import im.vector.app.features.userdirectory.UserListViewState
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import java.net.HttpURLConnection import java.net.HttpURLConnection
@ -111,35 +108,31 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac
} }
private fun openAddByQrCode() { 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) addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
} }
} }
private fun openPhoneBook() { private fun openPhoneBook() {
// Check permission first // Check permission first
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, this, permissionReadContactLauncher)) {
this,
PERMISSION_REQUEST_CODE_READ_CONTACTS,
0)) {
addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { private val permissionReadContactLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (allGranted) {
if (allGranted(grantResults)) { doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { } else if (deniedPermanently) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact)
} else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { }
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java) }
}
} else { private val permissionCameraLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { if (allGranted) {
onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
} else if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { } else if (deniedPermanently) {
onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact) onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
}
} }
} }

View file

@ -27,6 +27,7 @@ import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions 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.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentQrCodeScannerBinding import im.vector.app.databinding.FragmentQrCodeScannerBinding
import im.vector.app.features.userdirectory.PendingSelection import im.vector.app.features.userdirectory.PendingSelection
@ -44,9 +45,11 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen
return FragmentQrCodeScannerBinding.inflate(inflater, container, false) return FragmentQrCodeScannerBinding.inflate(inflater, container, false)
} }
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) { if (allGranted) {
startCamera() startCamera()
} else if (deniedPermanently) {
activity?.onPermissionDeniedDialog(R.string.denied_permission_camera)
} }
} }

View file

@ -23,12 +23,14 @@ import android.view.ViewGroup
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions 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.core.utils.registerForPermissionsResult
import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding
import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationAction
@ -79,9 +81,11 @@ class VerificationChooseMethodFragment @Inject constructor(
state.pendingRequest.invoke()?.transactionId ?: "")) state.pendingRequest.invoke()?.transactionId ?: ""))
} }
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) { if (allGranted) {
doOpenQRCodeScanner() doOpenQRCodeScanner()
} else if (deniedPermanently) {
activity?.onPermissionDeniedDialog(R.string.denied_permission_camera)
} }
} }

View file

@ -103,6 +103,7 @@ import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createJSonViewerStyleProvider
import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.createUIHandler
import im.vector.app.core.utils.isValidUrl 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.openUrlInExternalBrowser
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.saveMedia
@ -1107,14 +1108,16 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted -> private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) { if (allGranted) {
(roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let { (roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let {
roomDetailViewModel.pendingAction = null roomDetailViewModel.pendingAction = null
roomDetailViewModel.handle(it) roomDetailViewModel.handle(it)
} }
} else { } else {
context?.toast(R.string.permissions_action_not_performed_missing_permissions) if (deniedPermanently) {
activity?.onPermissionDeniedDialog(R.string.denied_permission_generic)
}
cleanUpAfterPermissionNotGranted() cleanUpAfterPermissionNotGranted()
} }
} }
@ -1800,13 +1803,16 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private val saveActionActivityResultLauncher = registerForPermissionsResult { allGranted -> private val saveActionActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) { if (allGranted) {
sharedActionViewModel.pendingAction?.let { sharedActionViewModel.pendingAction?.let {
handleActions(it) handleActions(it)
sharedActionViewModel.pendingAction = null sharedActionViewModel.pendingAction = null
} }
} else { } else {
if (deniedPermanently) {
activity?.onPermissionDeniedDialog(R.string.denied_permission_generic)
}
cleanUpAfterPermissionNotGranted() cleanUpAfterPermissionNotGranted()
} }
} }
@ -2039,7 +2045,7 @@ class RoomDetailFragment @Inject constructor(
// AttachmentTypeSelectorView.Callback // AttachmentTypeSelectorView.Callback
private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted -> private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) { if (allGranted) {
val pendingType = attachmentsHelper.pendingType val pendingType = attachmentsHelper.pendingType
if (pendingType != null) { if (pendingType != null) {
@ -2047,12 +2053,15 @@ class RoomDetailFragment @Inject constructor(
launchAttachmentProcess(pendingType) launchAttachmentProcess(pendingType)
} }
} else { } else {
if (deniedPermanently) {
activity?.onPermissionDeniedDialog(R.string.denied_permission_generic)
}
cleanUpAfterPermissionNotGranted() cleanUpAfterPermissionNotGranted()
} }
} }
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
if (checkPermissions(type.permissionsBit, requireActivity(), typeSelectedActivityResultLauncher)) { if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) {
launchAttachmentProcess(type) launchAttachmentProcess(type)
} else { } else {
attachmentsHelper.pendingType = type attachmentsHelper.pendingType = type

View file

@ -73,7 +73,8 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
} }
epoxyModel.getEventIds().forEach { eventId -> epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index adapterPositionMapping[eventId] = index
appendReadMarker = epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker appendReadMarker = appendReadMarker
|| (epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker)
} }
} }
if (epoxyModel is DaySeparatorItem) { if (epoxyModel is DaySeparatorItem) {

View file

@ -21,7 +21,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import android.widget.Toast
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData 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_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.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.core.utils.toast
import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.contactsbook.ContactsBookViewModel
@ -118,22 +117,16 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
private fun openPhoneBook() { private fun openPhoneBook() {
// Check permission first // Check permission first
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH, this, permissionContactLauncher)) {
this,
PERMISSION_REQUEST_CODE_READ_CONTACTS,
0)) {
addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { private val permissionContactLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (allGranted) {
if (allGranted(grantResults)) { doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { } else if (deniedPermanently) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact)
}
} else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
} }
} }

View file

@ -65,7 +65,7 @@ class ScanUserCodeFragment @Inject constructor()
} }
} }
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, _ ->
if (allGranted) { if (allGranted) {
startCamera() startCamera()
} else { } else {
@ -112,7 +112,7 @@ class ScanUserCodeFragment @Inject constructor()
super.onResume() super.onResume()
// Register ourselves as a handler for scan results. // Register ourselves as a handler for scan results.
views.userCodeScannerView.setResultHandler(this) 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() startCamera()
} }
} }

View file

@ -43,11 +43,11 @@ class ShowUserCodeFragment @Inject constructor(
val sharedViewModel: UserCodeSharedViewModel by activityViewModel() val sharedViewModel: UserCodeSharedViewModel by activityViewModel()
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted -> private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) { if (allGranted) {
doOpenQRCodeScanner() doOpenQRCodeScanner()
} else { } else {
sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted) sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted(deniedPermanently))
} }
} }

View file

@ -24,6 +24,6 @@ sealed class UserCodeActions : VectorViewModelAction {
data class SwitchMode(val mode: UserCodeState.Mode) : UserCodeActions() data class SwitchMode(val mode: UserCodeState.Mode) : UserCodeActions()
data class DecodedQRCode(val code: String) : UserCodeActions() data class DecodedQRCode(val code: String) : UserCodeActions()
data class StartChattingWithUser(val matrixItem: MatrixItem) : UserCodeActions() data class StartChattingWithUser(val matrixItem: MatrixItem) : UserCodeActions()
object CameraPermissionNotGranted : UserCodeActions() data class CameraPermissionNotGranted(val deniedPermanently: Boolean) : UserCodeActions()
object ShareByText : UserCodeActions() object ShareByText : UserCodeActions()
} }

View file

@ -81,13 +81,17 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
sharedViewModel.observeViewEvents { sharedViewModel.observeViewEvents {
when (it) { when (it) {
UserCodeShareViewEvents.Dismiss -> ActivityCompat.finishAfterTransition(this) UserCodeShareViewEvents.Dismiss -> ActivityCompat.finishAfterTransition(this)
UserCodeShareViewEvents.ShowWaitingScreen -> views.simpleActivityWaitingView.isVisible = true UserCodeShareViewEvents.ShowWaitingScreen -> views.simpleActivityWaitingView.isVisible = true
UserCodeShareViewEvents.HideWaitingScreen -> views.simpleActivityWaitingView.isVisible = false UserCodeShareViewEvents.HideWaitingScreen -> views.simpleActivityWaitingView.isVisible = false
is UserCodeShareViewEvents.ToastMessage -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() is UserCodeShareViewEvents.ToastMessage -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
is UserCodeShareViewEvents.NavigateToRoom -> navigator.openRoom(this, it.roomId) is UserCodeShareViewEvents.NavigateToRoom -> navigator.openRoom(this, it.roomId)
UserCodeShareViewEvents.CameraPermissionNotGranted -> onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) is UserCodeShareViewEvents.CameraPermissionNotGranted -> {
else -> { if (it.deniedPermanently) {
onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
}
}
else -> {
} }
} }
} }

View file

@ -24,6 +24,6 @@ sealed class UserCodeShareViewEvents : VectorViewEvents {
object HideWaitingScreen : UserCodeShareViewEvents() object HideWaitingScreen : UserCodeShareViewEvents()
data class ToastMessage(val message: String) : UserCodeShareViewEvents() data class ToastMessage(val message: String) : UserCodeShareViewEvents()
data class NavigateToRoom(val roomId: 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() data class SharePlainText(val text: String, val title: String, val richPlainText: String) : UserCodeShareViewEvents()
} }

View file

@ -76,7 +76,7 @@ class UserCodeSharedViewModel @AssistedInject constructor(
is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) }
is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action)
is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) is UserCodeActions.StartChattingWithUser -> handleStartChatting(action)
UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted) is UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted(action.deniedPermanently))
UserCodeActions.ShareByText -> handleShareByText() UserCodeActions.ShareByText -> handleShareByText()
} }
} }

View file

@ -39,8 +39,10 @@
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" /> android:layout_height="40dp"
app:tabPaddingEnd="0dp"
app:tabPaddingStart="0dp" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -386,12 +386,16 @@
<string name="reset">Reset</string> <string name="reset">Reset</string>
<string name="start_chatting">Start Chatting</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 --> <!-- 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">Ongoing conference call.\nJoin as %1$s or %2$s</string>
<string name="ongoing_conference_call_voice">Voice</string> <string name="ongoing_conference_call_voice">Voice</string>
<string name="ongoing_conference_call_video">Video</string> <string name="ongoing_conference_call_video">Video</string>
<string name="cannot_start_call">Cannot start the call, please try later</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_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_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> <string name="missing_permissions_to_start_conf_call">You need permission to invite to start a conference in this room</string>