First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.permissions.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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>
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.api
sealed interface PermissionsEvents {
data object RequestPermissions : PermissionsEvents
data object CloseDialog : PermissionsEvents
data object OpenSystemSettingAndCloseDialog : PermissionsEvents
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.api
import io.element.android.libraries.architecture.Presenter
interface PermissionsPresenter : Presenter<PermissionsState> {
interface Factory {
fun create(permission: String): PermissionsPresenter
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.api
data class PermissionsState(
// For instance Manifest.permission.POST_NOTIFICATIONS
val permission: String,
val permissionGranted: Boolean,
val shouldShowRationale: Boolean,
val showDialog: Boolean,
val permissionAlreadyAsked: Boolean,
// If true, there is no need to ask again, the system dialog will not be displayed
val permissionAlreadyDenied: Boolean,
val eventSink: (PermissionsEvents) -> Unit
)

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.api
import android.Manifest
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class PermissionsStateProvider : PreviewParameterProvider<PermissionsState> {
override val values: Sequence<PermissionsState>
get() = sequenceOf(
aPermissionsState(showDialog = true, permission = Manifest.permission.POST_NOTIFICATIONS),
aPermissionsState(showDialog = true, permission = Manifest.permission.CAMERA),
aPermissionsState(showDialog = true, permission = Manifest.permission.RECORD_AUDIO),
aPermissionsState(showDialog = true, permission = Manifest.permission.INTERNET),
)
}
fun aPermissionsState(
showDialog: Boolean,
permission: String = Manifest.permission.POST_NOTIFICATIONS,
permissionGranted: Boolean = false,
) = PermissionsState(
permission = permission,
permissionGranted = permissionGranted,
shouldShowRationale = false,
showDialog = showDialog,
permissionAlreadyAsked = false,
permissionAlreadyDenied = false,
eventSink = {}
)

View File

@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.api
import kotlinx.coroutines.flow.Flow
interface PermissionsStore {
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)
// To debug
suspend fun resetStore()
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.api
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun PermissionsView(
state: PermissionsState,
modifier: Modifier = Modifier,
title: String = stringResource(id = CommonStrings.common_permission),
content: String? = null,
icon: @Composable (() -> Unit)? = null,
) {
if (state.showDialog.not()) return
ConfirmationDialog(
modifier = modifier,
title = title,
content = content ?: state.permission.toDialogContent(),
submitText = stringResource(id = CommonStrings.action_open_settings),
onSubmitClick = {
state.eventSink.invoke(PermissionsEvents.OpenSystemSettingAndCloseDialog)
},
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) },
icon = icon,
)
}
@Composable
@ReadOnlyComposable
private fun String.toDialogContent(): String {
return when (this) {
Manifest.permission.POST_NOTIFICATIONS -> stringResource(id = R.string.dialog_permission_notification)
Manifest.permission.CAMERA -> stringResource(id = R.string.dialog_permission_camera)
Manifest.permission.RECORD_AUDIO -> stringResource(id = R.string.dialog_permission_microphone)
else -> stringResource(id = R.string.dialog_permission_generic)
}
}
@PreviewsDayNight
@Composable
internal fun PermissionsViewPreview(@PreviewParameter(PermissionsStateProvider::class) state: PermissionsState) = ElementPreview {
PermissionsView(
state = state,
)
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.api
fun createDummyPostNotificationPermissionsState() = PermissionsState(
permission = "Manifest.permission.POST_NOTIFICATIONS",
permissionGranted = true,
shouldShowRationale = false,
showDialog = false,
permissionAlreadyAsked = false,
permissionAlreadyDenied = false,
eventSink = { }
)

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Каб дазволіць праграме выкарыстоўваць камеру, дайце дазвол у наладах сістэмы."</string>
<string name="dialog_permission_generic">"Калі ласка, дайце дазвол у наладах сістэмы."</string>
<string name="dialog_permission_microphone">"Каб дазволіць праграме выкарыстоўваць мікрафон, дайце дазвол у наладах сістэмы."</string>
<string name="dialog_permission_notification">"Каб дазволіць праграме паказваць апавяшчэнні, дайце дазвол у наладах сістэмы."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Aby mohla aplikace používat fotoaparát, udělte prosím oprávnění v nastavení systému."</string>
<string name="dialog_permission_generic">"Udělte prosím oprávnění v nastavení systému."</string>
<string name="dialog_permission_microphone">"Aby aplikace mohla používat mikrofon, udělte prosím oprávnění v nastavení systému."</string>
<string name="dialog_permission_notification">"Aby aplikace mohla zobrazovat upozornění, udělte prosím oprávnění v nastavení systému."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Er mwyn gadael i\'r rhaglen ddefnyddio\'r camera, rhowch ganiatâd iddo yn y gosodiadau system."</string>
<string name="dialog_permission_generic">"Rhowch ganiatâd yn y gosodiadau system."</string>
<string name="dialog_permission_microphone">"Er mwyn gadael i\'r cais ddefnyddio\'r meicroffon, rhowch ganiatâd yng ngosodiadau\'r system."</string>
<string name="dialog_permission_notification">"Er mwyn gadael i\'r ap ddangos hysbysiadau, rhowch ganiatâd yn y gosodiadau system."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"For at lade applikationen bruge kameraet, skal du give tilladelsen i systemindstillingerne."</string>
<string name="dialog_permission_generic">"Giv venligst tilladelsen i systemindstillingerne."</string>
<string name="dialog_permission_microphone">"For at lade applikationen bruge mikrofonen, skal du give tilladelsen i systemindstillingerne."</string>
<string name="dialog_permission_notification">"For at lade applikationen vise notifikationer, skal du give tilladelsen i systemindstillingerne."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Damit die Anwendung die Kamera verwenden kann, erteile bitte die Berechtigung in den Systemeinstellungen."</string>
<string name="dialog_permission_generic">"Bitte erteile die Berechtigung in den Systemeinstellungen."</string>
<string name="dialog_permission_microphone">"Damit die App das Mikrofon nutzen kann, gib bitte die Berechtigung in den Systemeinstellungen frei."</string>
<string name="dialog_permission_notification">"Damit die App Benachrichtigungen anzeigen kann, gib bitte die Berechtigung in den Systemeinstellungen frei."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Για να επιτρέψεις στην εφαρμογή να χρησιμοποιεί την κάμερα, παραχώρησε την άδεια στις ρυθμίσεις συστήματος."</string>
<string name="dialog_permission_generic">"Παρακαλώ παραχώρησε την άδεια στις ρυθμίσεις συστήματος."</string>
<string name="dialog_permission_microphone">"Για να επιτρέψεις στην εφαρμογή να χρησιμοποιεί το μικρόφωνο, παραχώρησε την άδεια στις ρυθμίσεις συστήματος."</string>
<string name="dialog_permission_notification">"Για να επιτρέψεις στην εφαρμογή να εμφανίζει ειδοποιήσεις, παραχώρησε την άδεια στις ρυθμίσεις συστήματος."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Para permitir que la aplicación utilice la cámara, por favor concede el permiso en los ajustes del sistema."</string>
<string name="dialog_permission_generic">"Por favor concede el permiso en los ajustes del sistema."</string>
<string name="dialog_permission_microphone">"Para permitir que la aplicación utilice el micrófono, por favor conceda el permiso en los ajustes del sistema."</string>
<string name="dialog_permission_notification">"Para permitir que la aplicación muestre notificaciones, por favor concede el permiso en los ajustes del sistema."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Selleks, et rakendus saaks kaamerat kasutada, palun luba see süsteemi seadistuses."</string>
<string name="dialog_permission_generic">"Palun luba süsteemi seadistustest vajalikud õigused."</string>
<string name="dialog_permission_microphone">"Selleks, et rakendus saaks mikrofoni kasutada, palun luba see süsteemi seadistuses."</string>
<string name="dialog_permission_notification">"Selleks, et rakendus saaks kuvada teavitusi, palun anna vajalikud õiguse süsteemi seadistustes."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Aplikazioak kamera erabiltzeko, eman baimena sistemaren ezarpenetan."</string>
<string name="dialog_permission_generic">"Eman baimena sistemaren ezarpenetan."</string>
<string name="dialog_permission_microphone">"Aplikazioak mikrofonoa erabiltzeko, eman baimena sistemaren ezarpenetan."</string>
<string name="dialog_permission_notification">"Aplikazioak jakinarazpenak bistaratzeko, eman baimena sistemaren ezarpenetan."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"برای اینکه برنامه از دوربین استفاده کند، لطفا در تنظیمات سیستم مجوز بدهید."</string>
<string name="dialog_permission_generic">"لطفاً در تنظیمات سامانه اجازه بدهید."</string>
<string name="dialog_permission_microphone">"برای اینکه برنامه از میکروفون استفاده کند، لطفا اجازه دهید در تنظیمات سیستم مجوز بدهید."</string>
<string name="dialog_permission_notification">"برای اینکه برنامه اعلان ها را نمایش دهد، لطفا مجوز را در تنظیمات سیستم اعطا کنید."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Jotta sovellus voisi käyttää kameraa, anna lupa järjestelmän asetuksista."</string>
<string name="dialog_permission_generic">"Anna lupa järjestelmän asetuksista."</string>
<string name="dialog_permission_microphone">"Jotta sovellus voisi käyttää mikrofonia, anna lupa järjestelmän asetuksista."</string>
<string name="dialog_permission_notification">"Jotta sovellus voisi näyttää ilmoituksia, anna lupa järjestelmän asetuksista."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Pour permettre à lapplication dutiliser lappareil photo, veuillez accorder lautorisation dans les paramètres du système."</string>
<string name="dialog_permission_generic">"Veuillez accorder lautorisation dans les paramètres du système."</string>
<string name="dialog_permission_microphone">"Pour permettre à lapplication dutiliser le microphone, veuillez accorder lautorisation dans les paramètres du système."</string>
<string name="dialog_permission_notification">"Pour permettre à lapplication dafficher les notifications, veuillez accorder lautorisation dans les paramètres du système."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Hogy az alkalmazás használhassa a kamerát, adja meg az engedélyt a rendszerbeállításokban."</string>
<string name="dialog_permission_generic">"Adja meg az engedélyt a rendszerbeállításokban."</string>
<string name="dialog_permission_microphone">"Hogy az alkalmazás használhassa a mikrofont, adja meg az engedélyt a rendszerbeállításokban."</string>
<string name="dialog_permission_notification">"Hogy az alkalmazás megjeleníthesse az értesítéseket, adja meg az engedélyt a rendszerbeállításokban."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Supaya aplikasinya dapat menggunakan kamera, berikan izin dalam pengaturan sistem."</string>
<string name="dialog_permission_generic">"Silakan memberikan izin dalam pengaturan sistem."</string>
<string name="dialog_permission_microphone">"Supaya aplikasinya dapat menggunakan mikrofon, berikan izin dalam pengaturan sistem."</string>
<string name="dialog_permission_notification">"Supaya aplikasinya dapat menampilkan notifikasi, berikan izin dalam pengaturan sistem."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Per permettere all\'applicazione di usare la fotocamera, concedi l\'autorizzazione nelle impostazioni di sistema."</string>
<string name="dialog_permission_generic">"Concedi l\'autorizzazione nelle impostazioni di sistema."</string>
<string name="dialog_permission_microphone">"Per permettere all\'applicazione di usare il microfono, concedi l\'autorizzazione nelle impostazioni di sistema."</string>
<string name="dialog_permission_notification">"Per permettere all\'applicazione di mostrare notifiche, concedi l\'autorizzazione nelle impostazioni di sistema."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"იმისათვის, რომ აპლიკაციამ გამოიყენოს კამერა, გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
<string name="dialog_permission_generic">"გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
<string name="dialog_permission_microphone">"იმისათვის, რომ აპლიკაციამ მიკროფონი გამოიყენოს, გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
<string name="dialog_permission_notification">"იმისათვის, რომ აპლიკაციამ გამოაჩინოს შეტყობინებები, გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"애플리케이션이 카메라를 사용할 수 있도록 시스템 설정에서 권한을 허용해주세요."</string>
<string name="dialog_permission_generic">"시스템 설정에서 권한을 허용해주세요."</string>
<string name="dialog_permission_microphone">"애플리케이션이 마이크를 사용할 수 있도록 시스템 설정에서 권한을 허용해주세요."</string>
<string name="dialog_permission_notification">"애플리케이션이 알림을 표시할 수 있도록 시스템 설정에서 권한을 허용해주세요."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"For å la programmet bruke kameraet, vennligst gi tillatelse i systeminnstillingene."</string>
<string name="dialog_permission_generic">"Vennligst gi tillatelse i systeminnstillingene."</string>
<string name="dialog_permission_microphone">"For å la applikasjonen bruke mikrofonen, må du gi tillatelse i systeminnstillingene."</string>
<string name="dialog_permission_notification">"For å la applikasjonen vise varsler, må du gi tillatelse til dette i systeminnstillingene."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Geef toestemming in de systeeminstellingen om de applicatie de camera te laten gebruiken."</string>
<string name="dialog_permission_generic">"Geef hiervoor toestemming in de systeeminstellingen."</string>
<string name="dialog_permission_microphone">"Geef toestemming in de systeeminstellingen om de applicatie de microfoon te laten gebruiken."</string>
<string name="dialog_permission_notification">"Geef toestemming in de systeeminstellingen om de applicatie meldingen te laten weergeven."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Aby umożliwić aplikacji korzystanie z aparatu, prosimy o udzielenie zezwolenia w ustawieniach systemowych."</string>
<string name="dialog_permission_generic">"Proszę nadać uprawnienia w ustawieniach systemowych."</string>
<string name="dialog_permission_microphone">"Aby umożliwić aplikacji korzystanie z mikrofonu, prosimy o udzielenie zezwolenia w ustawieniach systemowych."</string>
<string name="dialog_permission_notification">"Aby aplikacja mogła wyświetlać powiadomienia, udziel uprawnienia w ustawieniach systemowych."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Para permitir que o aplicativo use a câmera, conceda a permissão nas configurações do sistema."</string>
<string name="dialog_permission_generic">"Por favor, conceda a permissão nas configurações do sistema."</string>
<string name="dialog_permission_microphone">"Para permitir que o aplicativo use o microfone, conceda a permissão nas configurações do sistema."</string>
<string name="dialog_permission_notification">"Para permitir que o aplicativo exiba notificações, conceda a permissão nas configurações do sistema."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Para que a aplicação possa utilizar a câmara, concede a permissão nas configurações do sistema."</string>
<string name="dialog_permission_generic">"Concede a permissão nas configurações do sistema."</string>
<string name="dialog_permission_microphone">"Para que a aplicação possa utilizar o microfone, concede essa permissão nas configurações do sistema."</string>
<string name="dialog_permission_notification">"Para permitir que a aplicação apresente notificações, concede a permissão nas configurações do sistema."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Pentru a permite aplicației să utilizeze camera, vă rugăm să acordați permisiunea în setările sistemului."</string>
<string name="dialog_permission_generic">"Vă rugăm să acordați permisiunea în setările sistemului."</string>
<string name="dialog_permission_microphone">"Pentru a permite aplicației să utilizeze microfonul, vă rugăm să acordați permisiunea în setările sistemului."</string>
<string name="dialog_permission_notification">"Pentru a permite aplicației să afișeze notificări, vă rugăm să acordați permisiunea în setările sistemului."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Чтобы приложение могло использовать камеру, предоставьте разрешение в системных настройках."</string>
<string name="dialog_permission_generic">"Пожалуйста, предоставьте разрешение в системных настройках."</string>
<string name="dialog_permission_microphone">"Чтобы приложение могло использовать микрофон, предоставьте разрешение в системных настройках."</string>
<string name="dialog_permission_notification">"Чтобы приложение отображало уведомления, предоставьте разрешение в системных настройках."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Aby aplikácia mohla používať fotoaparát, udeľte povolenie v systémových nastaveniach."</string>
<string name="dialog_permission_generic">"Udeľte prosím povolenie v systémových nastaveniach."</string>
<string name="dialog_permission_microphone">"Aby aplikácia mohla používať mikrofón, udeľte povolenie v systémových nastaveniach."</string>
<string name="dialog_permission_notification">"Ak chcete, aby aplikácia zobrazovala oznámenia, udeľte povolenie v nastaveniach systému."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"För att låta programmet använda kameran, vänligen ge behörigheten i systeminställningarna."</string>
<string name="dialog_permission_generic">"Vänligen ge behörigheten i systeminställningarna."</string>
<string name="dialog_permission_microphone">"För att låta programmet använda mikrofonen, vänligen ge behörigheten i systeminställningarna."</string>
<string name="dialog_permission_notification">"För att låta applikationen visa aviseringar, vänligen bevilja behörighet i systeminställningarna."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Uygulamanın kamerayı kullanmasına izin vermek için lütfen sistem ayarlarından izin verin."</string>
<string name="dialog_permission_generic">"Lütfen sistem ayarlarından izin verin."</string>
<string name="dialog_permission_microphone">"Uygulamanın mikrofonu kullanmasına izin vermek için lütfen sistem ayarlarından izin verin."</string>
<string name="dialog_permission_notification">"Uygulamanın bildirimleri görüntülemesine izin vermek için lütfen sistem ayarlarından izin verin."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Щоб дозволити застосунку використовувати камеру, надайте дозвіл у системних налаштуваннях."</string>
<string name="dialog_permission_generic">"Надайте дозвіл в системних налаштуваннях."</string>
<string name="dialog_permission_microphone">"Щоб дозволити застосунку використовувати мікрофон, надайте дозвіл у налаштуваннях системи."</string>
<string name="dialog_permission_notification">"Щоб застосунок показував сповіщення, надайте дозвіл у налаштуваннях системи."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"اطلاقیے کو تصویرگر استعمال کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
<string name="dialog_permission_generic">"برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
<string name="dialog_permission_microphone">"اطلاقیے کو صوتگر استعمال کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
<string name="dialog_permission_notification">"اطلاقیے کو اطلاعات ظاہر کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Ilovaga kameradan foydalanishiga ruxsat berish uchun tizim sozlamalarida ruxsat bering."</string>
<string name="dialog_permission_generic">"Iltimos, tizim sozlamalarida ruxsat bering."</string>
<string name="dialog_permission_microphone">"Ilovaga mikrofondan foydalanishiga ruxsat berish uchun tizim sozlamalarida ruxsat bering."</string>
<string name="dialog_permission_notification">"Ilova bildirishnomalarni ko\'rsatishi uchun tizim sozlamalarida ruxsat bering."</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"為了讓應用程式使用相機,請到系統設定中開啟權限。"</string>
<string name="dialog_permission_generic">"請到系統設定中開啟權限。"</string>
<string name="dialog_permission_microphone">"為了讓應用程式使用麥克風,請到系統設定中開啟權限。"</string>
<string name="dialog_permission_notification">"為了讓應用程式顯示通知,請到系統設定中開啟權限。"</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"为了让应用程序使用相机,请在系统设置中授予权限。"</string>
<string name="dialog_permission_generic">"请在系统设置中授予权限。"</string>
<string name="dialog_permission_microphone">"为了让应用程序使用麦克风,请在系统设置中授予权限。"</string>
<string name="dialog_permission_notification">"为了让应用程序显示通知,请在系统设置中授予权限。"</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"In order to let the application use the camera, please grant the permission in the system settings."</string>
<string name="dialog_permission_generic">"Please grant the permission in the system settings."</string>
<string name="dialog_permission_microphone">"In order to let the application use the microphone, please grant the permission in the system settings."</string>
<string name="dialog_permission_notification">"In order to let the application display notifications, please grant the permission in the system settings."</string>
</resources>

View File

@@ -0,0 +1,49 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.permissions.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
implementation(libs.accompanist.permission)
implementation(libs.androidx.datastore.preferences)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.troubleshoot.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.preferences.api)
implementation(projects.services.toolbox.api)
api(projects.libraries.permissions.api)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.services.toolbox.test)
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalPermissionsApi::class)
package io.element.android.libraries.permissions.impl
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberPermissionState
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
interface ComposablePermissionStateProvider {
@Composable
fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState
}
@ContributesBinding(AppScope::class)
class AccompanistPermissionStateProvider : ComposablePermissionStateProvider {
@Composable
override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState {
return rememberPermissionState(
permission = permission,
onPermissionResult = onPermissionResult
)
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsStore
import kotlinx.coroutines.flow.Flow
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultPermissionStateProvider(
@ApplicationContext private val context: Context,
private val permissionsStore: PermissionsStore,
) : PermissionStateProvider {
override fun isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(
context,
permission,
) == 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)
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.shouldShowRationale
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.log.logger.LoggerTag
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 io.element.android.libraries.permissions.impl.action.PermissionActions
import kotlinx.coroutines.launch
import timber.log.Timber
private val loggerTag = LoggerTag("DefaultPermissionsPresenter")
@AssistedInject
class DefaultPermissionsPresenter(
@Assisted val permission: String,
private val permissionsStore: PermissionsStore,
private val composablePermissionStateProvider: ComposablePermissionStateProvider,
private val permissionActions: PermissionActions,
) : PermissionsPresenter {
@AssistedFactory
@ContributesBinding(AppScope::class)
interface Factory : PermissionsPresenter.Factory {
override fun create(permission: String): DefaultPermissionsPresenter
}
@OptIn(ExperimentalPermissionsApi::class)
@SuppressLint("InlinedApi")
@Composable
override fun present(): PermissionsState {
val localCoroutineScope = rememberCoroutineScope()
// To reset the store: ResetStore()
val isAlreadyDenied: Boolean by remember {
permissionsStore.isPermissionDenied(permission)
}.collectAsState(initial = false)
val isAlreadyAsked: Boolean by remember {
permissionsStore.isPermissionAsked(permission)
}.collectAsState(initial = false)
var permissionState: PermissionState? = null
fun onPermissionResult(result: Boolean) {
Timber.tag(loggerTag.value).d("onPermissionResult: $result")
localCoroutineScope.launch {
permissionsStore.setPermissionAsked(permission, true)
}
if (!result) {
// Should show rational true -> denied.
if (permissionState?.status?.shouldShowRationale == true) {
Timber.tag(loggerTag.value).d("onPermissionResult: setPermissionDenied to true")
localCoroutineScope.launch {
permissionsStore.setPermissionDenied(permission, true)
}
}
}
}
permissionState = composablePermissionStateProvider.provide(
permission = permission,
onPermissionResult = ::onPermissionResult
)
LaunchedEffect(this) {
if (permissionState.status.isGranted) {
// User may have granted permission from the settings, so reset the store regarding this permission
permissionsStore.resetPermission(permission)
}
}
val showDialog = rememberSaveable { mutableStateOf(false) }
fun handleEvent(event: PermissionsEvents) {
when (event) {
PermissionsEvents.CloseDialog -> {
showDialog.value = false
}
PermissionsEvents.RequestPermissions -> {
if (permissionState.status !is PermissionStatus.Granted && isAlreadyDenied) {
showDialog.value = true
} else {
permissionState.launchPermissionRequest()
}
}
PermissionsEvents.OpenSystemSettingAndCloseDialog -> {
permissionActions.openSettings()
showDialog.value = false
}
}
}
return PermissionsState(
permission = permissionState.permission,
permissionGranted = permissionState.status.isGranted,
shouldShowRationale = permissionState.status.shouldShowRationale,
showDialog = showDialog.value,
permissionAlreadyAsked = isAlreadyAsked,
permissionAlreadyDenied = isAlreadyDenied,
eventSink = ::handleEvent,
)
}
/*
@Composable
private fun ResetStore() {
LaunchedEffect(this@DefaultPermissionsPresenter) {
launch {
permissionsStore.resetStore()
}
}
}
*/
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.permissions.api.PermissionsStore
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@ContributesBinding(AppScope::class)
class DefaultPermissionsStore(
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : PermissionsStore {
private val store = preferenceDataStoreFactory.create("permissions_store")
override suspend fun setPermissionDenied(permission: String, value: Boolean) {
store.edit { prefs ->
prefs[getDeniedPreferenceKey(permission)] = value
}
}
override fun isPermissionDenied(permission: String): Flow<Boolean> {
return store.data.map {
it[getDeniedPreferenceKey(permission)].orFalse()
}
}
override suspend fun setPermissionAsked(permission: String, value: Boolean) {
store.edit { prefs ->
prefs[getAskedPreferenceKey(permission)] = value
}
}
override fun isPermissionAsked(permission: String): Flow<Boolean> {
return store.data.map {
it[getAskedPreferenceKey(permission)].orFalse()
}
}
override suspend fun resetPermission(permission: String) {
setPermissionAsked(permission, false)
setPermissionDenied(permission, false)
}
override suspend fun resetStore() {
store.edit { it.clear() }
}
private fun getDeniedPreferenceKey(permission: String) = booleanPreferencesKey("${permission}_denied")
private fun getAskedPreferenceKey(permission: String) = booleanPreferencesKey("${permission}_asked")
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl.action
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
import io.element.android.libraries.di.annotations.ApplicationContext
@ContributesBinding(AppScope::class)
class AndroidPermissionActions(
@ApplicationContext private val context: Context
) : PermissionActions {
override fun openSettings() {
context.startNotificationSettingsIntent()
}
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl.action
interface PermissionActions {
fun openSettings()
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl.troubleshoot
import android.Manifest
import android.os.Build
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.impl.R
import io.element.android.libraries.permissions.impl.action.PermissionActions
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
@ContributesIntoSet(AppScope::class)
@Inject
class NotificationTroubleshootCheckPermissionTest(
private val permissionStateProvider: PermissionStateProvider,
private val sdkVersionProvider: BuildVersionSdkIntProvider,
private val permissionActions: PermissionActions,
stringProvider: StringProvider,
) : NotificationTroubleshootTest {
override val order: Int = 0
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_check_permission_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_check_permission_description),
hasQuickFix = true,
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val result = if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionStateProvider.isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS)
} else {
true
}
delegate.done(result)
}
override suspend fun reset() = delegate.reset()
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
// Do not bother about asking the permission inline, just lead the user to the settings
permissionActions.openSettings()
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Пераканайцеся, што праграма можа паказваць апавяшчэнні."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Праверце дазволы"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Проверка дали приложението може да показва известия."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Проверка на разрешенията"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Ujistěte se, že aplikace může zobrazovat oznámení."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Kontrola oprávnění"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Gwiriwch y gall y rhaglen ddangos hysbysiadau."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Gwirio caniatâd"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Kontroller, at applikationen kan vise underretninger."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Kontroller tilladelser"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Prüfe, dass die Anwendung Benachrichtigungen anzeigen kann."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Berechtigungen überprüfen"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Έλεγξε ότι η εφαρμογή μπορεί να εμφανίζει ειδοποιήσεις."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Έλεγχος δικαιωμάτων"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Verificar que la aplicación pueda mostrar notificaciones."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Verificar permisos"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Kontrolli, kas rakendus võib kuvada teavitusi."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Täpsusta õigusi"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Egiaztatu aplikazioak jakinarazpenak erakutsi ditzakeela."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Egiaztatu baimenak"</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_title">"بررسی اجازه‌ها"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Tarkistaa, että sovellus voi näyttää ilmoituksia."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Lupien tarkistus"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Vérifie que lapplication peut afficher des notifications."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Vérifier les autorisations"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Ellenőrizze, hogy az alkalmazás képes-e értesítéseket megjeleníteni."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Engedélyek ellenőrzése"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Pastikan aplikasi dapat menampilkan notifikasi."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Periksa izin"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Verifica che l\'applicazione possa mostrare le notifiche."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Controlla autorizzazioni"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"შეამოწმეთ რომ აპლიკაციას შეტყობინებების ჩვენება შეუძლია."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"ნებართვების შემოწმება"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"애플리케이션에서 알림을 표시할 수 있는지 확인하세요."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"권한 확인"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Kontroller at programmet kan vise varsler."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Sjekk tillatelser"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Controleren of de applicatie meldingen kan weergeven."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Controleer machtigingen"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Sprawdź, czy aplikacja może wyświetlać powiadomienia."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Sprawdź uprawnienia"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Verifique se o aplicativo pode mostrar notificações."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Verifique as permissões"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Verificar se a aplicação consegue mostrar notificações."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Verificar permissões"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Verificați dacă aplicația poate afișa notificări."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Verificați permisiunile"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Убедитесь, что приложение может показывать уведомления."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Проверка разрешений"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Uistite sa, že aplikácia dokáže zobrazovať upozornenia."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Skontrolovať povolenia"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Kontrollera att applikationen kan visa aviseringar."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Kontrollera behörigheter"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Uygulamanın bildirimleri gösterebildiğini kontrol edin."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"İzinleri kontrol et"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Перевірте, чи може застосунок показувати сповіщення."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Перевірте дозволи"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"پڑتال کریں کہ اطلاقیہ اطلاعات دکھا سکتا ہے"</string>
<string name="troubleshoot_notifications_test_check_permission_title">"اجازتوں کی پڑتال کریں"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Ilova bildirishnomalarni korsata olishini tekshiring."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Ruxsatlarni tekshiring"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"檢查應用程式是否可以顯示通知。"</string>
<string name="troubleshoot_notifications_test_check_permission_title">"檢查權限"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"检查应用程序是否可以显示通知。"</string>
<string name="troubleshoot_notifications_test_check_permission_title">"检查权限"</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Check that the application can show notifications."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Check permissions"</string>
</resources>

View File

@@ -0,0 +1,271 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalPermissionsApi::class)
package io.element.android.libraries.permissions.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.impl.action.FakePermissionActions
import io.element.android.libraries.permissions.test.InMemoryPermissionsStore
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
const val A_PERMISSION = "A_PERMISSION"
class DefaultPermissionsPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Granted
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.permission).isEqualTo(A_PERMISSION)
assertThat(initialState.permissionGranted).isTrue()
assertThat(initialState.shouldShowRationale).isFalse()
assertThat(initialState.permissionAlreadyAsked).isFalse()
assertThat(initialState.permissionAlreadyDenied).isFalse()
assertThat(initialState.showDialog).isFalse()
}
}
@Test
fun `present - user closes dialog`() = runTest {
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,
permissionStateProvider,
FakePermissionActions(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions)
val withDialogState = awaitItem()
assertThat(withDialogState.showDialog).isTrue()
withDialogState.eventSink.invoke(PermissionsEvents.CloseDialog)
assertThat(awaitItem().showDialog).isFalse()
}
}
@Test
fun `present - user open settings`() = runTest {
val permissionsStore = InMemoryPermissionsStore(
permissionDenied = true,
permissionAsked = true
)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val permissionActions = FakePermissionActions()
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
permissionActions,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions)
val withDialogState = awaitItem()
assertThat(withDialogState.showDialog).isTrue()
assertThat(permissionActions.openSettingsCalled).isFalse()
withDialogState.eventSink.invoke(PermissionsEvents.OpenSystemSettingAndCloseDialog)
assertThat(awaitItem().showDialog).isFalse()
assertThat(permissionActions.openSettingsCalled).isTrue()
}
}
@Test
fun `present - user does not grant permission`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.showDialog).isFalse()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions)
assertThat(permissionState.launchPermissionRequestCalled).isTrue()
// User does not grant permission
permissionStateProvider.userGiveAnswer(answer = false, firstTime = true)
skipItems(1)
val state = awaitItem()
assertThat(state.permissionGranted).isFalse()
assertThat(state.showDialog).isFalse()
assertThat(state.permissionAlreadyDenied).isFalse()
assertThat(state.permissionAlreadyAsked).isTrue()
}
}
@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 =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.showDialog).isFalse()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions)
assertThat(permissionState.launchPermissionRequestCalled).isTrue()
// User does not grant permission
permissionStateProvider.userGiveAnswer(answer = false, firstTime = false)
skipItems(2)
val state = awaitItem()
assertThat(state.permissionGranted).isFalse()
assertThat(state.showDialog).isFalse()
assertThat(state.permissionAlreadyDenied).isTrue()
assertThat(state.permissionAlreadyAsked).isTrue()
}
}
@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 =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions)
val withDialogState = awaitItem()
assertThat(withDialogState.showDialog).isTrue()
assertThat(withDialogState.permissionGranted).isFalse()
assertThat(withDialogState.permissionAlreadyDenied).isTrue()
assertThat(withDialogState.permissionAlreadyAsked).isTrue()
}
}
@Test
fun `present - user grants permission`() = runTest {
val permissionsStore = InMemoryPermissionsStore()
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.showDialog).isFalse()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions)
assertThat(permissionState.launchPermissionRequestCalled).isTrue()
// User grants permission
permissionStateProvider.userGiveAnswer(answer = true, firstTime = true)
skipItems(1)
val state = awaitItem()
assertThat(state.permissionGranted).isTrue()
assertThat(state.showDialog).isFalse()
assertThat(state.permissionAlreadyDenied).isFalse()
assertThat(state.permissionAlreadyAsked).isTrue()
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalPermissionsApi::class)
package io.element.android.libraries.permissions.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
class FakeComposablePermissionStateProvider(
private val permissionState: FakePermissionState
) : ComposablePermissionStateProvider {
private lateinit var onPermissionResult: (Boolean) -> Unit
@OptIn(ExperimentalPermissionsApi::class)
@Composable
override fun provide(permission: String, onPermissionResult: (Boolean) -> Unit): PermissionState {
this.onPermissionResult = onPermissionResult
return permissionState
}
fun userGiveAnswer(answer: Boolean, firstTime: Boolean) {
onPermissionResult.invoke(answer)
permissionState.givenPermissionStatus(answer, firstTime)
}
}
@Stable
class FakePermissionState(
override val permission: String,
initialStatus: PermissionStatus,
) : PermissionState {
override var status: PermissionStatus by mutableStateOf(initialStatus)
var launchPermissionRequestCalled = false
private set
override fun launchPermissionRequest() {
launchPermissionRequestCalled = true
}
fun givenPermissionStatus(hasPermission: Boolean, shouldShowRationale: Boolean) {
status = if (hasPermission) PermissionStatus.Granted else PermissionStatus.Denied(shouldShowRationale)
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl.action
class FakePermissionActions(
val openSettingsAction: () -> Unit = {}
) : PermissionActions {
var openSettingsCalled = false
private set
override fun openSettings() {
openSettingsAction()
openSettingsCalled = true
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl.troubleshoot
import android.os.Build
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.permissions.impl.action.FakePermissionActions
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class NotificationTroubleshootCheckPermissionTestTest {
@Test
fun `test NotificationTroubleshootCheckPermissionTest below TIRAMISU success`() = runTest {
val sut = NotificationTroubleshootCheckPermissionTest(
permissionStateProvider = FakePermissionStateProvider(),
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU - 1),
permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test NotificationTroubleshootCheckPermissionTest TIRAMISU success`() = runTest {
val sut = NotificationTroubleshootCheckPermissionTest(
permissionStateProvider = FakePermissionStateProvider(),
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU),
permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test NotificationTroubleshootCheckPermissionTest TIRAMISU error`() = runTest {
val permissionStateProvider = FakePermissionStateProvider(
permissionGranted = false
)
val actions = FakePermissionActions(
openSettingsAction = {
permissionStateProvider.setPermissionGranted()
}
)
val sut = NotificationTroubleshootCheckPermissionTest(
permissionStateProvider = permissionStateProvider,
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU),
permissionActions = actions,
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
// Quick fix
backgroundScope.launch {
sut.quickFix(this, FakeNotificationTroubleshootNavigator())
// Run the test again (IRL it will be done thanks to the resuming of the application)
sut.run(this)
}
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test NotificationTroubleshootCheckPermissionTest error and reset`() = runTest {
val permissionStateProvider = FakePermissionStateProvider(
permissionGranted = false
)
val actions = FakePermissionActions(
openSettingsAction = {
permissionStateProvider.setPermissionGranted()
}
)
val sut = NotificationTroubleshootCheckPermissionTest(
permissionStateProvider = permissionStateProvider,
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU),
permissionActions = actions,
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
}
}
}

View File

@@ -0,0 +1,24 @@
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.permissions.noop"
}
dependencies {
implementation(projects.libraries.architecture)
api(projects.libraries.permissions.api)
testCommonDependencies(libs)
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.noop
import androidx.compose.runtime.Composable
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.PermissionsState
class NoopPermissionsPresenter(
private val isGranted: Boolean = false,
) : PermissionsPresenter {
@Composable
override fun present(): PermissionsState {
return PermissionsState(
permission = "",
permissionGranted = isGranted,
shouldShowRationale = false,
showDialog = false,
permissionAlreadyAsked = false,
permissionAlreadyDenied = false,
eventSink = {},
)
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.noop
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class NoopPermissionsPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = NoopPermissionsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.permission).isEmpty()
assertThat(initialState.permissionGranted).isFalse()
assertThat(initialState.shouldShowRationale).isFalse()
assertThat(initialState.permissionAlreadyAsked).isFalse()
assertThat(initialState.permissionAlreadyDenied).isFalse()
assertThat(initialState.showDialog).isFalse()
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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,40 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.test
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
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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(showDialog = false),
) : PermissionsPresenter {
private fun handleEvent(event: PermissionsEvents) {
when (event) {
PermissionsEvents.RequestPermissions -> state.value = state.value.copy(showDialog = true, permissionAlreadyAsked = true)
PermissionsEvents.CloseDialog -> state.value = state.value.copy(showDialog = false)
PermissionsEvents.OpenSystemSettingAndCloseDialog -> state.value = state.value.copy(showDialog = false)
}
}
private val state = mutableStateOf(initialState.copy(eventSink = ::handleEvent))
fun setPermissionGranted() {
state.value = state.value.copy(permissionGranted = true)
}
fun setPermissionDenied() {
state.value = state.value.copy(permissionGranted = false, permissionAlreadyDenied = true)
}
@Composable
override fun present(): PermissionsState {
return state.value
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.test
import io.element.android.libraries.permissions.api.PermissionsPresenter
class FakePermissionsPresenterFactory(
private val permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
) : PermissionsPresenter.Factory {
override fun create(permission: String): PermissionsPresenter {
return permissionPresenter
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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
class InMemoryPermissionsStore(
permissionDenied: Boolean = false,
permissionAsked: Boolean = false,
) : PermissionsStore {
private val permissionDeniedFlow = MutableStateFlow(permissionDenied)
private val permissionAskedFlow = MutableStateFlow(permissionAsked)
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)
}
override suspend fun resetStore() = Unit
}