Skip to content

Instantly share code, notes, and snippets.

@Nek-12
Created May 24, 2025 10:38
Show Gist options
  • Save Nek-12/6f6132f0205d3dc02829b6fb1d196050 to your computer and use it in GitHub Desktop.
Save Nek-12/6f6132f0205d3dc02829b6fb1d196050 to your computer and use it in GitHub Desktop.
Android Ringtone picker contract with edge cases
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.core.net.toUri
import pro.respawn.apiresult.fold
import pro.respawn.apiresult.runResulting
import pro.respawn.kmmutils.system.android.PickRingtoneContract.RingtoneOptions
import pro.respawn.kmmutils.system.android.parcelable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import pro.respawn.apiresult.ApiResult
@Composable
fun rememberRingtonePicker(onComplete: (RingtoneResult) -> Unit): RingtonePicker {
val block by rememberUpdatedState(onComplete)
val ringtoneLauncher = rememberLauncherForActivityResult(PickRingtoneContract()) { it.let(block) }
return remember(ringtoneLauncher) {
RingtonePicker {
ApiResult {
ringtoneLauncher.launch(
RingtoneOptions(
ringtoneType = it.ringtoneType.asAndroidType,
showDefault = it.showDefault,
allowSilent = it.allowSilent,
defaultUri = it.preselectedRingtoneUri?.toUri(),
)
)
}
}
}
}
private val RingtoneType.asAndroidType: Int
get() = when (this) {
RingtoneType.Alarm -> RingtoneManager.TYPE_ALARM
RingtoneType.Call -> RingtoneManager.TYPE_RINGTONE
RingtoneType.Notification -> RingtoneManager.TYPE_NOTIFICATION
RingtoneType.Any -> RingtoneManager.TYPE_ALL
}
private class PickRingtoneContract : ActivityResultContract<RingtoneOptions, RingtoneResult>() {
override fun createIntent(context: Context, input: RingtoneOptions): Intent =
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, input.showDefault)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, input.allowSilent)
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, input.ringtoneType)
input.title?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, it) }
input.defaultUri?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, it) }
input.existingUri?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, it) }
}
override fun getSynchronousResult(context: Context, input: RingtoneOptions) = null
override fun parseResult(resultCode: Int, intent: Intent?): RingtoneResult {
if (resultCode == Activity.RESULT_CANCELED) return RingtoneResult.Canceled
if (intent == null) return RingtoneResult.Error(IllegalArgumentException("ringtone picker intent is null")
return intent.runResulting {
parcelable<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
?: data
?: return@runResulting RingtoneResult.Canceled
}.fold(
onSuccess = { RingtoneResult.Success(it.toString()) },
onError = RingtoneResult::Error
)
}
}
enum class RingtoneType {
Alarm, Call, Notification, Any
}
@Immutable
data class RingtoneOptions(
val ringtoneType: RingtoneType,
val preselectedRingtoneUri: String? = null,
val showDefault: Boolean = true,
val allowSilent: Boolean = false,
)
@Stable
fun interface RingtonePicker {
suspend fun launch(options: RingtoneOptions): ApiResult<Unit>
}
sealed interface RingtoneResult {
data class Success(val uri: String) : RingtoneResult
data object Canceled : RingtoneResult
data class Error(val e: Exception) : RingtoneResult
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment