Replace notification permission dialog with a screen (#1223)

* Replace notification permission dialog with a screen

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-09-05 18:58:05 +02:00 committed by GitHub
parent d4b527fb80
commit cfdccc904e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1027 additions and 69 deletions

View file

@ -42,4 +42,6 @@ enum class AvatarSize(val dp: Dp) {
RoomInviteItem(52.dp),
InviteSender(16.dp),
NotificationsOptIn(32.dp),
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.permissions.api
import kotlinx.coroutines.flow.Flow
interface PermissionStateProvider {
fun isPermissionGranted(permission: String): Boolean
suspend fun setPermissionDenied(permission: String, value: Boolean)
fun isPermissionDenied(permission: String): Flow<Boolean>
suspend fun setPermissionAsked(permission: String, value: Boolean)
fun isPermissionAsked(permission: String): Flow<Boolean>
suspend fun resetPermission(permission: String)
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.permissions.impl
package io.element.android.libraries.permissions.api
import kotlinx.coroutines.flow.Flow

View file

@ -56,6 +56,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.permissions.test)
ksp(libs.showkase.processor)
}

View file

@ -26,13 +26,13 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
interface PermissionStateProvider {
interface ComposablePermissionStateProvider {
@Composable
fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState
}
@ContributesBinding(AppScope::class)
class AccompanistPermissionStateProvider @Inject constructor() : PermissionStateProvider {
class AccompanistPermissionStateProvider @Inject constructor() : ComposablePermissionStateProvider {
@Composable
override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState {
return rememberPermissionState(

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.permissions.impl
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsStore
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultPermissionStateProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val permissionsStore: PermissionsStore,
): PermissionStateProvider {
override fun isPermissionGranted(permission: String): Boolean {
return context.checkSelfPermission(permission) == android.content.pm.PackageManager.PERMISSION_GRANTED
}
override suspend fun setPermissionDenied(permission: String, value: Boolean) = permissionsStore.setPermissionDenied(permission, value)
override fun isPermissionDenied(permission: String): Flow<Boolean> = permissionsStore.isPermissionDenied(permission)
override suspend fun setPermissionAsked(permission: String, value: Boolean) = permissionsStore.setPermissionAsked(permission, value)
override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionsStore.isPermissionAsked(permission)
override suspend fun resetPermission(permission: String) = permissionsStore.resetPermission(permission)
}

View file

@ -38,6 +38,7 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.PermissionsState
import io.element.android.libraries.permissions.api.PermissionsStore
import kotlinx.coroutines.launch
import timber.log.Timber
@ -46,7 +47,7 @@ private val loggerTag = LoggerTag("DefaultPermissionsPresenter")
class DefaultPermissionsPresenter @AssistedInject constructor(
@Assisted val permission: String,
private val permissionsStore: PermissionsStore,
private val permissionStateProvider: PermissionStateProvider,
private val composablePermissionStateProvider: ComposablePermissionStateProvider,
) : PermissionsPresenter {
@AssistedFactory
@ -90,7 +91,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
}
}
permissionState = permissionStateProvider.provide(
permissionState = composablePermissionStateProvider.provide(
permission = permission,
onPermissionResult = ::onPermissionResult
)

View file

@ -26,6 +26,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.permissions.api.PermissionsStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@ -34,7 +35,7 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
@ContributesBinding(AppScope::class)
class DefaultPermissionsStore @Inject constructor(
@ApplicationContext context: Context,
@ApplicationContext private val context: Context,
) : PermissionsStore {
private val store = context.dataStore

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.permissions.impl
import io.element.android.libraries.permissions.api.PermissionStateProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakePermissionStateProvider(
private var permissionGranted: Boolean = true,
permissionDenied: Boolean = false,
permissionAsked: Boolean = false,
): PermissionStateProvider {
private val permissionDeniedFlow = MutableStateFlow(permissionDenied)
private val permissionAskedFlow = MutableStateFlow(permissionAsked)
fun setPermissionGranted() {
permissionGranted = true
}
override fun isPermissionGranted(permission: String): Boolean = permissionGranted
override suspend fun setPermissionDenied(permission: String, value: Boolean) {
permissionDeniedFlow.value = value
}
override fun isPermissionDenied(permission: String): Flow<Boolean> = permissionDeniedFlow
override suspend fun setPermissionAsked(permission: String, value: Boolean) {
permissionAskedFlow.value = value
}
override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionAskedFlow
override suspend fun resetPermission(permission: String) {
setPermissionAsked(permission, false)
setPermissionDenied(permission, false)
}
}

View file

@ -25,6 +25,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.test.InMemoryPermissionsStore
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -34,8 +35,14 @@ class DefaultPermissionsPresenterTest {
@Test
fun `present - initial state`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Granted)
val permissionStateProvider = FakePermissionStateProvider(permissionState)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Granted
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
@ -57,8 +64,14 @@ class DefaultPermissionsPresenterTest {
@Test
fun `present - user closes dialog`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
val permissionStateProvider = FakePermissionStateProvider(permissionState)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
@ -77,8 +90,14 @@ class DefaultPermissionsPresenterTest {
@Test
fun `present - user does not grant permission`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
val permissionStateProvider = FakePermissionStateProvider(permissionState)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
@ -106,8 +125,14 @@ class DefaultPermissionsPresenterTest {
@Test
fun `present - user does not grant permission second time`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = true))
val permissionStateProvider = FakePermissionStateProvider(permissionState)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = true)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
@ -134,9 +159,19 @@ class DefaultPermissionsPresenterTest {
@Test
fun `present - user does not grant permission third time`() = runTest {
val permissionsStore = InMemoryPermissionsStore(permissionDenied = true, permissionAsked = true)
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
val permissionStateProvider = FakePermissionStateProvider(permissionState)
val permissionsStore =
InMemoryPermissionsStore(
permissionDenied = true,
permissionAsked = true
)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
@ -157,8 +192,14 @@ class DefaultPermissionsPresenterTest {
@Test
fun `present - user grants permission`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(A_PERMISSION, PermissionStatus.Denied(shouldShowRationale = false))
val permissionStateProvider = FakePermissionStateProvider(permissionState)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,

View file

@ -27,9 +27,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
class FakePermissionStateProvider constructor(
class FakeComposablePermissionStateProvider constructor(
private val permissionState: FakePermissionState
) : PermissionStateProvider {
) : ComposablePermissionStateProvider {
private lateinit var onPermissionResult: (Boolean) -> Unit
@OptIn(ExperimentalPermissionsApi::class)

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 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.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.permissions.test"
}
dependencies {
implementation(projects.libraries.architecture)
api(projects.libraries.permissions.api)
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.permissions.test
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.PermissionsState
import io.element.android.libraries.permissions.api.aPermissionsState
class FakePermissionsPresenter(
private val initialState: PermissionsState = aPermissionsState().copy(showDialog = false),
) : PermissionsPresenter {
private fun eventSink(events: PermissionsEvents) {
when (events) {
PermissionsEvents.OpenSystemDialog -> state.value = state.value.copy(showDialog = true, permissionAlreadyAsked = true)
PermissionsEvents.CloseDialog -> state.value = state.value.copy(showDialog = false)
}
}
private val state = mutableStateOf(initialState.copy(eventSink = ::eventSink))
fun setPermissionGranted() {
state.value = state.value.copy(permissionGranted = true)
}
fun setPermissionDenied() {
state.value = state.value.copy(permissionAlreadyDenied = true)
}
@Composable
override fun present(): PermissionsState {
return state.value
}
}

View file

@ -14,8 +14,9 @@
* limitations under the License.
*/
package io.element.android.libraries.permissions.impl
package io.element.android.libraries.permissions.test
import io.element.android.libraries.permissions.api.PermissionsStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -43,6 +44,5 @@ class InMemoryPermissionsStore(
setPermissionDenied(permission, false)
}
override suspend fun resetStore() {
}
override suspend fun resetStore() = Unit
}