Skip to content

Commit d895959

Browse files
committed
Add persisted voice chat settings for desktop and android
1 parent 5264cb5 commit d895959

14 files changed

Lines changed: 661 additions & 104 deletions

docs/todo.yaml

Lines changed: 112 additions & 26 deletions
Large diffs are not rendered by default.

src/McpServerManager.Android/App.axaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ void Core()
3535
AndroidLogcatBridge.EnsureInitialized();
3636
AndroidCrashDiagnostics.ReplayPendingDiagnostics();
3737
AndroidOidcJwtCacheInvalidationMonitor.EnsureInitialized();
38+
VoiceChatSettingsService.Instance.ConfigureStore(new AndroidVoiceChatSettingsStore());
3839
global::Android.Util.Log.Info("McpSM", "[App] OnFrameworkInitializationCompleted entered (Android)");
3940

4041
try
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using Android.App;
2+
using Android.Content;
3+
using McpServerManager.Core.Services;
4+
5+
namespace McpServerManager.Android.Services;
6+
7+
public sealed class AndroidVoiceChatSettingsStore : IVoiceChatSettingsStore
8+
{
9+
private const string PreferencesName = "McpServerManager.Voice";
10+
private const string LanguageKey = "VoiceLanguage";
11+
private const string AutoContinueKey = "AutoContinueEnabled";
12+
private const string WakePhraseKey = "WakePhrase";
13+
private const string WakeSensitivityKey = "WakeSensitivity";
14+
private const string AutoListenOnWakeKey = "AutoListenOnWake";
15+
private const string PicovoiceAccessKeyKey = "PicovoiceAccessKey";
16+
17+
public bool SupportsWakeWordSettings => true;
18+
19+
public VoiceChatSettings Load()
20+
{
21+
try
22+
{
23+
var prefs = GetPreferences();
24+
return VoiceChatSettingsService.Normalize(new VoiceChatSettings
25+
{
26+
Language = prefs?.GetString(LanguageKey, null) ?? VoiceChatSettingsService.DefaultLanguage,
27+
AutoContinueEnabled = prefs?.GetBoolean(AutoContinueKey, true) ?? true,
28+
WakePhrase = prefs?.GetString(WakePhraseKey, null) ?? VoiceChatSettingsService.DefaultWakePhrase,
29+
WakeSensitivity = prefs?.GetString(WakeSensitivityKey, null) ?? VoiceChatSettingsService.DefaultWakeSensitivity,
30+
AutoListenOnWake = prefs?.GetBoolean(AutoListenOnWakeKey, true) ?? true,
31+
PicovoiceAccessKey = prefs?.GetString(PicovoiceAccessKeyKey, null) ?? string.Empty
32+
});
33+
}
34+
catch
35+
{
36+
return new VoiceChatSettings();
37+
}
38+
}
39+
40+
public void Save(VoiceChatSettings settings)
41+
{
42+
var normalized = VoiceChatSettingsService.Normalize(settings);
43+
var editor = GetPreferences()?.Edit();
44+
if (editor == null)
45+
return;
46+
47+
using (editor)
48+
{
49+
editor.PutString(LanguageKey, normalized.Language);
50+
editor.PutBoolean(AutoContinueKey, normalized.AutoContinueEnabled);
51+
editor.PutString(WakePhraseKey, normalized.WakePhrase);
52+
editor.PutString(WakeSensitivityKey, normalized.WakeSensitivity);
53+
editor.PutBoolean(AutoListenOnWakeKey, normalized.AutoListenOnWake);
54+
if (string.IsNullOrWhiteSpace(normalized.PicovoiceAccessKey))
55+
editor.Remove(PicovoiceAccessKeyKey);
56+
else
57+
editor.PutString(PicovoiceAccessKeyKey, normalized.PicovoiceAccessKey);
58+
editor.Apply();
59+
}
60+
}
61+
62+
private static ISharedPreferences? GetPreferences()
63+
{
64+
var context = Application.Context;
65+
return context?.GetSharedPreferences(PreferencesName, FileCreationMode.Private);
66+
}
67+
}

src/McpServerManager.Android/Services/AndroidWakeWordServices.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public interface IAndroidWakeWordService : IDisposable
4141
bool IsMonitoring { get; }
4242
IReadOnlyList<string> AvailableWakePhrases { get; }
4343
string SelectedWakePhrase { get; }
44+
string SelectedWakeSensitivity { get; }
4445
event EventHandler<AndroidWakeWordDetectedEventArgs>? WakeWordDetected;
46+
Task ApplySettingsAsync(AndroidWakeWordSettings settings, CancellationToken cancellationToken = default);
4547
Task<bool> SetSelectedWakePhraseAsync(string phrase, CancellationToken cancellationToken = default);
4648
Task StartMonitoringAsync(CancellationToken cancellationToken = default);
4749
Task StopMonitoringAsync(CancellationToken cancellationToken = default);
@@ -199,6 +201,7 @@ public sealed class AndroidWakeWordService : IAndroidWakeWordService
199201
public bool IsMonitoring { get; private set; }
200202
public IReadOnlyList<string> AvailableWakePhrases => AndroidWakeWordCatalog.SupportedWakePhrases;
201203
public string SelectedWakePhrase => _settings.SelectedWakePhrase;
204+
public string SelectedWakeSensitivity => _settings.Sensitivity;
202205

203206
public event EventHandler<AndroidWakeWordDetectedEventArgs>? WakeWordDetected;
204207

@@ -231,20 +234,32 @@ public async Task<bool> SetSelectedWakePhraseAsync(string phrase, CancellationTo
231234
if (string.IsNullOrWhiteSpace(normalized))
232235
return false;
233236

234-
if (string.Equals(_settings.SelectedWakePhrase, normalized, StringComparison.Ordinal))
235-
return true;
236-
237-
_settings = new AndroidWakeWordSettings
237+
await ApplySettingsAsync(new AndroidWakeWordSettings
238238
{
239239
SelectedWakePhrase = normalized,
240240
Sensitivity = _settings.Sensitivity
241-
};
241+
}, cancellationToken);
242+
243+
return true;
244+
}
245+
246+
public async Task ApplySettingsAsync(AndroidWakeWordSettings settings, CancellationToken cancellationToken = default)
247+
{
248+
ThrowIfDisposed();
249+
cancellationToken.ThrowIfCancellationRequested();
250+
251+
var normalized = NormalizeSettings(settings);
252+
if (string.Equals(_settings.SelectedWakePhrase, normalized.SelectedWakePhrase, StringComparison.Ordinal) &&
253+
string.Equals(_settings.Sensitivity, normalized.Sensitivity, StringComparison.OrdinalIgnoreCase))
254+
{
255+
return;
256+
}
257+
258+
_settings = normalized;
242259
_settingsStore.Save(_settings);
243260

244261
if (IsMonitoring)
245262
await _wakeWordEngine.ConfigureAsync(_settings, cancellationToken);
246-
247-
return true;
248263
}
249264

250265
public async Task StartMonitoringAsync(CancellationToken cancellationToken = default)

src/McpServerManager.Android/Views/PhoneSettingsView.axaml

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,68 @@
44
x:Class="McpServerManager.Android.Views.PhoneSettingsView"
55
x:DataType="vm:SettingsViewModel">
66

7-
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="8">
7+
<ScrollViewer VerticalScrollBarVisibility="Auto">
8+
<StackPanel Margin="8" Spacing="12">
9+
<Border Classes="phone-outline-card" Padding="12">
10+
<StackPanel Spacing="8">
11+
<TextBlock Text="Voice Chat" FontSize="22" FontWeight="SemiBold"/>
12+
<TextBlock Text="Shared voice-chat defaults. These values seed the Android voice experience and the simplified chat view."
13+
TextWrapping="Wrap" Opacity="0.7" FontSize="18"/>
14+
<TextBlock Text="Default language" FontSize="18" FontWeight="SemiBold"/>
15+
<TextBox Text="{Binding VoiceLanguage, Mode=TwoWay}"
16+
Watermark="en-US"
17+
FontSize="20"/>
18+
<CheckBox Content="Auto-continue voice chat after each response"
19+
IsChecked="{Binding AutoContinueEnabled, Mode=TwoWay}"
20+
FontSize="19"/>
21+
</StackPanel>
22+
</Border>
823

9-
<!-- Speech Filter Section Header -->
10-
<StackPanel Grid.Row="0" Spacing="4" Margin="0,8,0,8">
11-
<TextBlock Text="Speech Filter Phrases" FontSize="22" FontWeight="SemiBold"/>
12-
<TextBlock Text="Lines containing any of these phrases (case-insensitive) will be excluded from text-to-speech. One phrase per line."
13-
TextWrapping="Wrap" Opacity="0.7" FontSize="18"/>
14-
</StackPanel>
24+
<Border Classes="phone-outline-card" Padding="12" IsVisible="{Binding SupportsWakeWordSettings}">
25+
<StackPanel Spacing="8">
26+
<TextBlock Text="Wake Word" FontSize="22" FontWeight="SemiBold"/>
27+
<TextBlock Text="Android-only wake-word settings. These are saved to the same device preferences used by the Android wake-word runtime."
28+
TextWrapping="Wrap" Opacity="0.7" FontSize="18"/>
29+
<TextBlock Text="Wake phrase" FontSize="18" FontWeight="SemiBold"/>
30+
<ComboBox ItemsSource="{Binding AvailableWakePhrases}"
31+
SelectedItem="{Binding WakePhrase, Mode=TwoWay}"
32+
FontSize="20"/>
33+
<TextBlock Text="Wake sensitivity" FontSize="18" FontWeight="SemiBold"/>
34+
<ComboBox ItemsSource="{Binding AvailableWakeSensitivities}"
35+
SelectedItem="{Binding WakeSensitivity, Mode=TwoWay}"
36+
FontSize="20"/>
37+
<CheckBox Content="Auto listen + send when wake phrase is detected"
38+
IsChecked="{Binding AutoListenOnWake, Mode=TwoWay}"
39+
FontSize="19"/>
40+
<TextBlock Text="Picovoice access key" FontSize="18" FontWeight="SemiBold"/>
41+
<TextBox Text="{Binding PicovoiceAccessKey, Mode=TwoWay}"
42+
Watermark="Optional when already set by environment or metadata"
43+
FontSize="19"/>
44+
</StackPanel>
45+
</Border>
1546

16-
<!-- Filter Words Editor -->
17-
<TextBox Grid.Row="2"
18-
Text="{Binding SpeechFilterWords, Mode=TwoWay}"
19-
AcceptsReturn="True"
20-
TextWrapping="Wrap"
21-
Watermark="Enter filter phrases, one per line..."
22-
FontSize="20"
23-
MinHeight="200"
24-
VerticalContentAlignment="Top"/>
47+
<Border Classes="phone-outline-card" Padding="12">
48+
<StackPanel Spacing="8">
49+
<TextBlock Text="Speech Filter Phrases" FontSize="22" FontWeight="SemiBold"/>
50+
<TextBlock Text="Lines containing any of these phrases (case-insensitive) will be excluded from text-to-speech. One phrase per line."
51+
TextWrapping="Wrap" Opacity="0.7" FontSize="18"/>
52+
<TextBox Text="{Binding SpeechFilterWords, Mode=TwoWay}"
53+
AcceptsReturn="True"
54+
TextWrapping="Wrap"
55+
Watermark="Enter filter phrases, one per line..."
56+
FontSize="20"
57+
MinHeight="200"
58+
VerticalContentAlignment="Top"/>
59+
</StackPanel>
60+
</Border>
2561

26-
<!-- Save/Revert/Import Buttons + Status -->
27-
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="0,8,0,4">
28-
<Button Content="Save" Command="{Binding SaveFilterWordsCommand}" Padding="14,8" FontSize="21"/>
29-
<Button Content="Revert" Command="{Binding RevertFilterWordsCommand}" Padding="14,8" FontSize="21"/>
30-
<Button Content="Import" x:Name="ImportButton" Click="OnImportClick" Padding="14,8" FontSize="21"/>
31-
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Opacity="0.7" FontSize="19"/>
62+
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
63+
<Button Content="Save" Command="{Binding SaveFilterWordsCommand}" Padding="14,8" FontSize="21"/>
64+
<Button Content="Revert" Command="{Binding RevertFilterWordsCommand}" Padding="14,8" FontSize="21"/>
65+
<Button Content="Import" x:Name="ImportButton" Click="OnImportClick" Padding="14,8" FontSize="21"/>
66+
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Opacity="0.7" FontSize="19" TextWrapping="Wrap"/>
67+
</StackPanel>
3268
</StackPanel>
33-
34-
</Grid>
69+
</ScrollViewer>
3570

3671
</UserControl>

src/McpServerManager.Android/Views/VoiceConversationView.axaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
</Grid>
6060
<CheckBox x:Name="AutoListenOnWakeCheckBox"
6161
Content="Auto listen + send on wake"
62-
IsChecked="True"/>
62+
IsCheckedChanged="OnAutoListenOnWakeCheckedChanged"/>
6363

6464
<!-- Text input -->
6565
<TextBox MinHeight="60"

0 commit comments

Comments
 (0)