Notification settings: pick custom sounds for messages and call ringtones (#6897)

* Notification settings: pick custom sounds for messages and call ringtones (#6702)

* Feature: add notification sound settings for messages and calls

* Add sound not available banner, accessibility checks

* Fix build and Konsist convention violations

- libraries:preferences:api now uses io.element.android-compose-library
  so NotificationSound's @Immutable annotation can resolve, matching the
  pattern in libraries:matrix:api.
- Rename SoundUnavailableBannerStateProvider to
  NotificationSoundUnavailableStateProvider so the class name contains
  the parameter type as required by KonsistClassNameTest.

* Suppress LargeClass on RoomListPresenterTest

The three new sound-unavailable tests pushed the class past Detekt's
default LargeClass threshold. Matches the suppression pattern already
used on five presenter test classes under features/messages/impl/test.

* NotificationSettingsView: guard ringtone-picker launchers in inspection mode

rememberLauncherForActivityResult requires a LocalActivityResultRegistryOwner,
which Paparazzi-driven preview snapshot tests don't provide, causing
NotificationSettingsViewPreview to throw IllegalStateException for every
preview parameter. Extract the click handlers into a helper that returns a
no-op when LocalInspectionMode.current is true; the runtime path is unchanged.

* Handle unresolved notification sounds mid session

* Quality checks, refactor mid session error ui

* Suppress detekt LargeClass on NotificationSettingsPresenterTest

* Remove notification sound resilience layer, replace with file copy

* Reselect element sound option, code cleanup

* Add element fade notification sound option

* Fix new file headers

* Revert to flowOf(Unit) for announcement service

* Add ringtone picker dialog to match message selector

* Update strings in conjunction with ios

* Update test to reflect new string

* Sync strings.

* Update screenshots

* Fix test

* Remove not properly located translations.
This will be fixed in the next String sync.

* Fix test on CI

---------

Co-authored-by: Jenna Vassar <jenna.vassar@toptal.com>
Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Benoit Marty
2026-05-28 16:54:12 +02:00
committed by GitHub
parent bdedf41f99
commit c88edc9621
79 changed files with 3135 additions and 139 deletions
+1
View File
@@ -20,5 +20,6 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixmedia.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.pushproviders.api)
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2026 Element Creations 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.push.api.notifications
import io.element.android.libraries.preferences.api.store.NotificationSound
/**
* Reads and writes the sound on the message / ringing-call channels. Writes create a new
* versioned channel because Android forbids mutating sound after creation.
*/
interface NotificationSoundUpdater {
fun recreateNoisyChannel(sound: NotificationSound, version: Int)
fun recreateRingingCallChannel(sound: NotificationSound, version: Int)
/** Current channel sound classified into [NotificationSound]. Null when the channel doesn't exist. */
suspend fun readNoisyChannelSound(): NotificationSound?
/** See [readNoisyChannelSound]. */
suspend fun readRingingCallChannelSound(): NotificationSound?
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations 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.push.api.notifications
/**
* Resolves the user-facing title of a custom notification sound URI (the title shown by the
* system ringtone picker). Lifts the [android.media.RingtoneManager] side effect out of UI code
* so views and presenters stay testable.
*/
interface SoundDisplayNameResolver {
/** Returns the title of the ringtone at [uri], or null if it cannot be resolved. */
suspend fun resolveCustomSoundTitle(uri: String): String?
}
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2026 Element Creations 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.push.api.notifications.sound
/**
* Copies a picked notification sound into app-private storage and re-exposes it via the app's
* FileProvider, so the persisted reference outlives the original source.
*/
interface NotificationSoundCopier {
suspend fun copyToAppFiles(sourceUriString: String, slot: SoundSlot): CopyResult
/**
* Removes any previously copied file for [slot] from app-private storage. No-op when no file
* exists. Call this when persisting a non-[NotificationSound.Custom] pick (SystemDefault /
* Silent) so the previous Custom copy doesn't linger as orphaned bytes — [copyToAppFiles]
* already sweeps stale files inline for Custom-to-Custom transitions.
*/
suspend fun deleteStoredSoundFor(slot: SoundSlot)
enum class SoundSlot { Message, Call }
sealed interface CopyResult {
/**
* @property fileProviderUriString FileProvider URI to persist as the channel sound.
* @property displayName [android.media.Ringtone.getTitle] for the source, or null. The
* picker substitutes a localised fallback label, so localisation stays in the UI layer.
*/
data class Success(val fileProviderUriString: String, val displayName: String?) : CopyResult
/** [android.media.RingtoneManager] could not open the source URI as a Ringtone. */
data object UnplayableSource : CopyResult
/** The bytes copied successfully, but the resulting file is not a playable Ringtone. */
data object UnplayableCopy : CopyResult
/** Source exceeded the 5 MB cap. */
data object FileTooLarge : CopyResult
/** Any other I/O or unexpected failure. */
data class Failure(val cause: Throwable) : CopyResult
}
}