Browse Source
* Replace notification permission dialog with a screen --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>pull/1229/head
Jorge Martin Espinosa
1 year ago
committed by
GitHub
43 changed files with 1027 additions and 69 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Add a notification permission screen to the initial flow. |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
/* |
||||
* 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.features.ftue.impl.notifications |
||||
|
||||
sealed interface NotificationsOptInEvents { |
||||
data object ContinueClicked : NotificationsOptInEvents |
||||
data object NotNowClicked : NotificationsOptInEvents |
||||
} |
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
/* |
||||
* 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.features.ftue.impl.notifications |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.libraries.architecture.NodeInputs |
||||
import io.element.android.libraries.architecture.inputs |
||||
import io.element.android.libraries.di.AppScope |
||||
|
||||
@ContributesNode(AppScope::class) |
||||
class NotificationsOptInNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
private val presenterFactory: NotificationsOptInPresenter.Factory, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
|
||||
interface Callback: NodeInputs { |
||||
fun onNotificationsOptInFinished() |
||||
} |
||||
|
||||
private val callback = inputs<Callback>() |
||||
|
||||
private val presenter: NotificationsOptInPresenter by lazy { |
||||
presenterFactory.create(callback) |
||||
} |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val state = presenter.present() |
||||
NotificationsOptInView( |
||||
state = state, |
||||
onBack = { callback.onNotificationsOptInFinished() }, |
||||
modifier = modifier |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
/* |
||||
* 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.features.ftue.impl.notifications |
||||
|
||||
import android.Manifest |
||||
import android.os.Build |
||||
import androidx.annotation.RequiresApi |
||||
import androidx.compose.runtime.Composable |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedFactory |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider |
||||
import io.element.android.libraries.permissions.api.PermissionsEvents |
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter |
||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter |
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.launch |
||||
|
||||
class NotificationsOptInPresenter @AssistedInject constructor( |
||||
private val permissionsPresenterFactory: PermissionsPresenter.Factory, |
||||
@Assisted private val callback: NotificationsOptInNode.Callback, |
||||
private val appCoroutineScope: CoroutineScope, |
||||
private val permissionStateProvider: PermissionStateProvider, |
||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, |
||||
) : Presenter<NotificationsOptInState> { |
||||
|
||||
@AssistedFactory |
||||
interface Factory { |
||||
fun create(callback: NotificationsOptInNode.Callback): NotificationsOptInPresenter |
||||
} |
||||
|
||||
private val postNotificationPermissionsPresenter by lazy { |
||||
// Ask for POST_NOTIFICATION PERMISSION on Android 13+ |
||||
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { |
||||
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS) |
||||
} else { |
||||
NoopPermissionsPresenter() |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
override fun present(): NotificationsOptInState { |
||||
val notificationPremissionsState = postNotificationPermissionsPresenter.present() |
||||
|
||||
fun handleEvents(event: NotificationsOptInEvents) { |
||||
when (event) { |
||||
NotificationsOptInEvents.ContinueClicked -> { |
||||
if (notificationPremissionsState.permissionGranted) { |
||||
callback.onNotificationsOptInFinished() |
||||
} else { |
||||
notificationPremissionsState.eventSink(PermissionsEvents.OpenSystemDialog) |
||||
} |
||||
} |
||||
NotificationsOptInEvents.NotNowClicked -> { |
||||
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { |
||||
appCoroutineScope.setPermissionDenied() |
||||
} |
||||
callback.onNotificationsOptInFinished() |
||||
} |
||||
} |
||||
} |
||||
|
||||
return NotificationsOptInState( |
||||
notificationsPermissionState = notificationPremissionsState, |
||||
eventSink = ::handleEvents |
||||
) |
||||
} |
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU) |
||||
private fun CoroutineScope.setPermissionDenied() = launch { |
||||
permissionStateProvider.setPermissionDenied(Manifest.permission.POST_NOTIFICATIONS, true) |
||||
} |
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
/* |
||||
* 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.features.ftue.impl.notifications |
||||
|
||||
import io.element.android.libraries.permissions.api.PermissionsState |
||||
|
||||
data class NotificationsOptInState( |
||||
val notificationsPermissionState: PermissionsState, |
||||
val eventSink: (NotificationsOptInEvents) -> Unit |
||||
) |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* 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.features.ftue.impl.notifications |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
import io.element.android.libraries.permissions.api.aPermissionsState |
||||
|
||||
open class NotificationsOptInStateProvider : PreviewParameterProvider<NotificationsOptInState> { |
||||
override val values: Sequence<NotificationsOptInState> |
||||
get() = sequenceOf( |
||||
aNotificationsOptInState(), |
||||
// Add other states here |
||||
) |
||||
} |
||||
|
||||
fun aNotificationsOptInState() = NotificationsOptInState( |
||||
notificationsPermissionState = aPermissionsState(), |
||||
eventSink = {} |
||||
) |
@ -0,0 +1,206 @@
@@ -0,0 +1,206 @@
|
||||
/* |
||||
* 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.features.ftue.impl.notifications |
||||
|
||||
import androidx.activity.compose.BackHandler |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.systemBarsPadding |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Notifications |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.clip |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.features.ftue.impl.R |
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule |
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule |
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage |
||||
import io.element.android.libraries.designsystem.colors.AvatarColors |
||||
import io.element.android.libraries.designsystem.colors.avatarColors |
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews |
||||
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.theme.components.Button |
||||
import io.element.android.libraries.designsystem.theme.components.Surface |
||||
import io.element.android.libraries.designsystem.theme.components.TextButton |
||||
import io.element.android.libraries.theme.ElementTheme |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
|
||||
@Composable |
||||
fun NotificationsOptInView( |
||||
state: NotificationsOptInState, |
||||
onBack: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
BackHandler(onBack = onBack) |
||||
|
||||
if (state.notificationsPermissionState.permissionAlreadyDenied) { |
||||
LaunchedEffect(Unit) { |
||||
state.eventSink(NotificationsOptInEvents.NotNowClicked) |
||||
} |
||||
} |
||||
|
||||
HeaderFooterPage( |
||||
modifier = modifier |
||||
.systemBarsPadding() |
||||
.fillMaxSize(), |
||||
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp),) }, |
||||
footer = { NotificationsOptInFooter(state) }, |
||||
) { |
||||
NotificationsOptInContent(modifier = Modifier.fillMaxWidth()) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun NotificationsOptInHeader( |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
IconTitleSubtitleMolecule( |
||||
modifier = modifier, |
||||
title = stringResource(R.string.screen_notification_optin_title), |
||||
subTitle = stringResource(R.string.screen_notification_optin_subtitle), |
||||
iconImageVector = Icons.Default.Notifications, |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun NotificationsOptInFooter(state: NotificationsOptInState) { |
||||
ButtonColumnMolecule { |
||||
Button( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
text = stringResource(CommonStrings.action_ok), |
||||
onClick = { |
||||
state.eventSink(NotificationsOptInEvents.ContinueClicked) |
||||
} |
||||
) |
||||
TextButton( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
text = stringResource(CommonStrings.action_not_now), |
||||
onClick = { |
||||
state.eventSink(NotificationsOptInEvents.NotNowClicked) |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun NotificationsOptInContent( |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { |
||||
Column( |
||||
verticalArrangement = Arrangement.spacedBy( |
||||
16.dp, |
||||
alignment = Alignment.CenterVertically |
||||
) |
||||
) { |
||||
NotificationRow( |
||||
avatarLetter = "M", |
||||
avatarColors = avatarColors("5"), |
||||
firstRowPercent = 1f, |
||||
secondRowPercent = 0.4f |
||||
) |
||||
|
||||
NotificationRow( |
||||
avatarLetter = "A", |
||||
avatarColors = avatarColors("1"), |
||||
firstRowPercent = 1f, |
||||
secondRowPercent = 1f |
||||
) |
||||
|
||||
NotificationRow( |
||||
avatarLetter = "T", |
||||
avatarColors = avatarColors("4"), |
||||
firstRowPercent = 0.65f, |
||||
secondRowPercent = 0f |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun NotificationRow( |
||||
avatarLetter: String, |
||||
avatarColors: AvatarColors, |
||||
firstRowPercent: Float, |
||||
secondRowPercent: Float, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Surface( |
||||
modifier = modifier, |
||||
color = ElementTheme.colors.bgCanvasDisabled, |
||||
shape = RoundedCornerShape(14.dp), |
||||
shadowElevation = 2.dp, |
||||
) { |
||||
Row( |
||||
modifier = Modifier.padding(16.dp), |
||||
horizontalArrangement = Arrangement.spacedBy(16.dp), |
||||
verticalAlignment = Alignment.CenterVertically |
||||
) { |
||||
Avatar( |
||||
avatarData = AvatarData(id = "", name = avatarLetter, size = AvatarSize.NotificationsOptIn), |
||||
initialAvatarColors = avatarColors, |
||||
) |
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { |
||||
Box( |
||||
modifier = Modifier |
||||
.clip(CircleShape) |
||||
.fillMaxWidth(firstRowPercent) |
||||
.height(10.dp) |
||||
.background(ElementTheme.colors.borderInteractiveSecondary) |
||||
) |
||||
if (secondRowPercent > 0f) { |
||||
Box( |
||||
modifier = Modifier.clip(CircleShape) |
||||
.fillMaxWidth(secondRowPercent) |
||||
.height(10.dp) |
||||
.background(ElementTheme.colors.borderInteractiveSecondary) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@DayNightPreviews |
||||
@Composable |
||||
internal fun NotificationsOptInViewPreview( |
||||
@PreviewParameter(NotificationsOptInStateProvider::class) state: NotificationsOptInState |
||||
) { |
||||
ElementPreview { |
||||
NotificationsOptInView( |
||||
onBack = {}, |
||||
state = state, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
/* |
||||
* 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.features.ftue.impl.notifications |
||||
|
||||
import android.os.Build |
||||
import app.cash.molecule.RecompositionMode |
||||
import app.cash.molecule.moleculeFlow |
||||
import app.cash.turbine.test |
||||
import com.google.common.truth.Truth |
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider |
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter |
||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider |
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter |
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider |
||||
import kotlinx.coroutines.flow.first |
||||
import kotlinx.coroutines.runBlocking |
||||
import kotlinx.coroutines.test.StandardTestDispatcher |
||||
import kotlinx.coroutines.test.TestScope |
||||
import kotlinx.coroutines.test.runCurrent |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
class NotificationsOptInPresenterTests { |
||||
|
||||
private var isFinished = false |
||||
|
||||
@Test |
||||
fun `initial state`() = runTest { |
||||
val presenter = createPresenter() |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
Truth.assertThat(initialState.notificationsPermissionState.showDialog).isFalse() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `show dialog on continue clicked`() = runTest { |
||||
val permissionPresenter = FakePermissionsPresenter() |
||||
val presenter = createPresenter(permissionPresenter) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(NotificationsOptInEvents.ContinueClicked) |
||||
Truth.assertThat(awaitItem().notificationsPermissionState.showDialog).isTrue() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `finish flow on continue clicked with permission already granted`() = runTest { |
||||
val permissionPresenter = FakePermissionsPresenter().apply { |
||||
setPermissionGranted() |
||||
} |
||||
val presenter = createPresenter(permissionPresenter) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(NotificationsOptInEvents.ContinueClicked) |
||||
Truth.assertThat(isFinished).isTrue() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `finish flow on not now clicked`() = runTest { |
||||
val permissionPresenter = FakePermissionsPresenter() |
||||
val presenter = createPresenter( |
||||
permissionsPresenter = permissionPresenter, |
||||
sdkIntVersion = Build.VERSION_CODES.M |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(NotificationsOptInEvents.NotNowClicked) |
||||
Truth.assertThat(isFinished).isTrue() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `set permission denied on not now clicked in API 33`() = runTest(StandardTestDispatcher()) { |
||||
val permissionPresenter = FakePermissionsPresenter() |
||||
val permissionStateProvider = FakePermissionStateProvider() |
||||
val presenter = createPresenter( |
||||
permissionsPresenter = permissionPresenter, |
||||
permissionStateProvider = permissionStateProvider, |
||||
sdkIntVersion = Build.VERSION_CODES.TIRAMISU |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(NotificationsOptInEvents.NotNowClicked) |
||||
|
||||
// Allow background coroutines to run |
||||
runCurrent() |
||||
|
||||
val isPermissionDenied = runBlocking { |
||||
permissionStateProvider.isPermissionDenied("notifications").first() |
||||
} |
||||
Truth.assertThat(isPermissionDenied).isTrue() |
||||
} |
||||
} |
||||
|
||||
private fun TestScope.createPresenter( |
||||
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), |
||||
permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(), |
||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, |
||||
) = NotificationsOptInPresenter( |
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory { |
||||
override fun create(permission: String): PermissionsPresenter { |
||||
return permissionsPresenter |
||||
} |
||||
}, |
||||
callback = object : NotificationsOptInNode.Callback { |
||||
override fun onNotificationsOptInFinished() { |
||||
isFinished = true |
||||
} |
||||
}, |
||||
appCoroutineScope = this, |
||||
permissionStateProvider = permissionStateProvider, |
||||
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), |
||||
) |
||||
} |
@ -0,0 +1,30 @@
@@ -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) |
||||
} |
@ -0,0 +1,48 @@
@@ -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) |
||||
} |
@ -0,0 +1,53 @@
@@ -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) |
||||
} |
||||
} |
@ -0,0 +1,28 @@
@@ -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) |
||||
} |
@ -0,0 +1,51 @@
@@ -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 |
||||
} |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
/* |
||||
* 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-library") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.services.toolbox.test" |
||||
} |
||||
|
||||
dependencies { |
||||
api(projects.services.toolbox.api) |
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
/* |
||||
* 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.services.toolbox.test.sdk |
||||
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider |
||||
|
||||
class FakeBuildVersionSdkIntProvider( |
||||
private val sdkInt: Int |
||||
) : BuildVersionSdkIntProvider { |
||||
override fun get(): Int = sdkInt |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue