Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ If there's a device not in this list that you want to see supported, check if th
| A3957 | Soundcore Liberty 5 |
| A3959 | Soundcore P30i / R50i NC |
| A3968 | Soundcore Sport X20 |
| A6611 | Soundcore Sleep A20 |
| D1101 | Soundcore C50i |
| D1202 | Soundcore P31i |
| D1202C | Soundcore R60i NC |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ private fun DeviceModelIcon(modifier: Modifier, model: String) {
"SoundcoreA3951" -> EarbudsIcon(modifier)
"SoundcoreA3955" -> EarbudsIcon(modifier)
"SoundcoreA3959" -> EarbudsIcon(modifier)
"SoundcoreA6611" -> EarbudsIcon(modifier)
"SoundcoreDevelopment" -> HeadphonesIcon(modifier)
else -> HeadphonesIcon(modifier)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerDialog
import androidx.compose.material3.TimePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
Expand All @@ -46,6 +50,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import com.oppzippy.openscq30.R
import com.oppzippy.openscq30.lib.bindings.settingRequiresConfirmation
import com.oppzippy.openscq30.lib.bindings.translateSettingId
import com.oppzippy.openscq30.lib.wrapper.ModifiableSelectCommandInner
import com.oppzippy.openscq30.lib.wrapper.MultiSelectWithRemoveCommandInner
Expand Down Expand Up @@ -98,11 +103,19 @@ private fun FallbackSettingCategoryScreen(
settings.forEach { (settingId, setting) ->
val name = translateSettingId(settingId)
when (setting) {
is Setting.ToggleSetting -> Toggle(
name = name,
isEnabled = setting.value,
onChange = { setSetting(settingId, it.toValue()) },
)
is Setting.ToggleSetting -> if (settingRequiresConfirmation(settingId)) {
ConfirmedToggle(
name = name,
isEnabled = setting.value,
onChange = { setSetting(settingId, it.toValue()) },
)
} else {
Toggle(
name = name,
isEnabled = setting.value,
onChange = { setSetting(settingId, it.toValue()) },
)
}

is Setting.EqualizerSetting -> Equalizer(
name = name,
Expand Down Expand Up @@ -187,6 +200,55 @@ private fun Toggle(name: String, isEnabled: Boolean, onChange: (Boolean) -> Unit
LabeledSwitch(name, isEnabled, onChange)
}

// Turning on requires explicit confirmation, cancel is the default for every dismissal path. Turning off is immediate.
@Composable
private fun ConfirmedToggle(name: String, isEnabled: Boolean, onChange: (Boolean) -> Unit) {
var isDialogOpen by remember { mutableStateOf(false) }
LabeledSwitch(
label = name,
isChecked = isEnabled,
onCheckedChange = { newValue ->
if (newValue) {
isDialogOpen = true
} else {
onChange(false)
}
},
)
if (isDialogOpen) {
val cancelFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { cancelFocusRequester.requestFocus() }
AlertDialog(
modifier = Modifier.testTag("confirmToggleDialog"),
onDismissRequest = { isDialogOpen = false },
icon = { Icon(painterResource(R.drawable.warning_24px), contentDescription = null) },
title = { Text(stringResource(R.string.loud_sound_warning_title)) },
text = { Text(stringResource(R.string.loud_sound_warning_message, name)) },
confirmButton = {
Button(
modifier = Modifier
.focusRequester(cancelFocusRequester)
.testTag("confirmToggleCancel"),
onClick = { isDialogOpen = false },
) {
Text(stringResource(R.string.cancel))
}
},
dismissButton = {
TextButton(
modifier = Modifier.testTag("confirmToggleYes"),
onClick = {
isDialogOpen = false
onChange(true)
},
) {
Text(stringResource(R.string.yes))
}
},
)
}
}

@Composable
private fun InformationSetting(name: String, text: String) {
val context = LocalContext.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import com.oppzippy.openscq30.R
import com.oppzippy.openscq30.features.soundcoredevice.service.ConnectionStatus
import com.oppzippy.openscq30.features.soundcoredevice.service.DeviceService
import com.oppzippy.openscq30.lib.bindings.OpenScq30Session
import com.oppzippy.openscq30.lib.bindings.settingRequiresConfirmation
import com.oppzippy.openscq30.lib.bindings.translateDeviceModel
import com.oppzippy.openscq30.lib.bindings.translateSettingId
import com.oppzippy.openscq30.lib.wrapper.PairedDevice
Expand Down Expand Up @@ -202,7 +203,7 @@ class SettingWidget : GlanceAppWidget() {
} else {
SettingWidgetState.Connected(
deviceName = translateDeviceModel(device.model()),
settings = settingIds.map { settingId ->
settings = settingIds.filterNot { settingRequiresConfirmation(it) }.map { settingId ->
Pair(
settingId,
device.setting(settingId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import com.oppzippy.openscq30.R
import com.oppzippy.openscq30.features.soundcoredevice.service.ConnectionStatus
import com.oppzippy.openscq30.features.soundcoredevice.service.DeviceService
import com.oppzippy.openscq30.lib.bindings.OpenScq30Session
import com.oppzippy.openscq30.lib.bindings.settingRequiresConfirmation
import com.oppzippy.openscq30.lib.bindings.translateCategoryId
import com.oppzippy.openscq30.lib.bindings.translateSettingId
import com.oppzippy.openscq30.ui.DeviceServiceConnection
Expand Down Expand Up @@ -290,7 +291,11 @@ private fun Content(
enabledSettingIds = enabledSettingIds,
settingCategories = device.categories()
.map { categoryId ->
Pair(categoryId, device.settingsInCategory(categoryId))
Pair(
categoryId,
device.settingsInCategory(categoryId)
.filterNot { settingRequiresConfirmation(it) },
)
},
onToggle = { settingId, isEnabled ->
onSetSettingIdEnabled(device.model(), settingId, isEnabled)
Expand Down
10 changes: 10 additions & 0 deletions android/app/src/main/res/drawable/warning_24px.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720Q497,720 508.5,708.5ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500L480,500Z"/>
</vector>
3 changes: 3 additions & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,7 @@
<string name="visible_settings">Visible Settings</string>
<string name="appearance">Appearance</string>
<string name="compact_layout">Compact Layout</string>
<string name="yes">Yes</string>
<string name="loud_sound_warning_title">Loud sound warning</string>
<string name="loud_sound_warning_message">%s plays a very loud tone that can damage your hearing. Remove the earbud from your ear before continuing. Turn it on?</string>
</resources>
5 changes: 5 additions & 0 deletions android/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,8 @@ pub enum AndroidError {
pub fn device_models() -> Vec<serializable::DeviceModel> {
DeviceModel::iter().map(serializable::DeviceModel).collect()
}

#[uniffi::export]
pub fn setting_requires_confirmation(setting_id: serializable::SettingId) -> bool {
setting_id.0.requires_confirmation()
}
4 changes: 4 additions & 0 deletions lib/i18n/en/openscq30-lib.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ soundcore-d1101 = Soundcore C50i
soundcore-d1202 = Soundcore P31i
soundcore-d1202c = Soundcore R60i NC
soundcore-d1301 = Soundcore Sleep A30
soundcore-a6611 = Soundcore Sleep A20
soundcore-development = Soundcore Development Information

general = General
Expand Down Expand Up @@ -243,6 +244,9 @@ case-language = Case Language
atmospheric = Atmospheric
remote-camera = Remote Camera
find-device = Find Device
find-device-left = Find Left Earbud
find-device-right = Find Right Earbud
sleep-mode = Sleep Mode
spatial-audio = Spatial Audio
spatial-audio-mode = Spatial Audio Mode
spatial-audio-music-mode = Spatial Audio Music Mode
Expand Down
2 changes: 1 addition & 1 deletion lib/src/api/quick_presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl QuickPresetsHandler {
setting_id,
SettingId::ImportCustomEqualizerProfiles
| SettingId::ExportCustomEqualizerProfiles,
)
) && !setting_id.requires_confirmation()
})
.filter_map(|setting_id| {
device
Expand Down
11 changes: 11 additions & 0 deletions lib/src/api/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,17 @@ pub enum SettingId {
Alarm5Volume,
#[translate("alarm-n-snooze-duration-minutes", number = 5)]
Alarm5SnoozeDuration,
FindDeviceLeft,
FindDeviceRight,
SleepMode,
}

impl SettingId {
/// Settings that can cause harm if changed by accident, such as ringing an earbud while it is being worn. Frontends
/// should ask for confirmation before enabling these, and they are excluded from quick presets.
pub fn requires_confirmation(&self) -> bool {
matches!(self, Self::FindDeviceLeft | Self::FindDeviceRight)
}
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
Expand Down
3 changes: 3 additions & 0 deletions lib/src/devices/device_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub enum DeviceModel {
SoundcoreD1202,
SoundcoreD1202C,
SoundcoreD1301,
SoundcoreA6611,
SoundcoreDevelopment,
}

Expand Down Expand Up @@ -131,6 +132,7 @@ impl DeviceModel {
Self::SoundcoreD1202 => new_soundcore_device!(soundcore::d1202),
Self::SoundcoreD1202C => new_soundcore_device!(soundcore::d1202),
Self::SoundcoreD1301 => new_soundcore_device!(soundcore::d1301),
Self::SoundcoreA6611 => new_soundcore_device!(soundcore::a6611),
Self::SoundcoreDevelopment => new_soundcore_device!(soundcore::development),
}
}
Expand Down Expand Up @@ -188,6 +190,7 @@ impl DeviceModel {
Self::SoundcoreD1202 => new_soundcore_device!(soundcore::d1202),
Self::SoundcoreD1202C => new_soundcore_device!(soundcore::d1202),
Self::SoundcoreD1301 => new_soundcore_device!(soundcore::d1301),
Self::SoundcoreA6611 => new_soundcore_device!(soundcore::a6611),
Self::SoundcoreDevelopment => new_soundcore_device!(soundcore::development),
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/src/devices/soundcore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod a3955;
pub mod a3957;
pub mod a3959;
pub mod a3968;
pub mod a6611;
pub mod common;
pub mod d1101;
pub mod d1202;
Expand Down
Loading
Loading