Merge branch 'develop' of github.com:vector-im/element-android into feature/dla/reorg_account_notification_settings

This commit is contained in:
David Langley 2021-07-14 10:18:54 +01:00
commit baab726df2
31 changed files with 425 additions and 350 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

@ -169,7 +169,7 @@ dependencies {
implementation 'com.otaliastudios:transcoder:0.10.3'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.26'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.27'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.5.1'

View file

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

View file

@ -363,7 +363,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.26'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.27'
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'

View file

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

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

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

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

View file

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

View file

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

View file

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

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.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"

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.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)
}
}

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.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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,7 +39,9 @@
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
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>

View file

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