I am trying out Microsoft .NET MAUI that currently in Preview stage. I try to make a small Android app that will use Google voice recognizer service as a way to let user navigate the app. Just a small demo to see what can I do with it. This is also my first time to actually write a Xamarin/MAUI project, so I am not really sure what I can actually do wit the platform.
The problem is that I would like to have this Google service to always on (without timeout) or auto-close then re-open when timeout. In short, I want user to never actually have to deal with this screen:
My intention is that the will be a background thread to keep asking user to say the command, only stop when user do, and the service will always ready to receive the speech. However, I am unable to keep the above service always on or auto-close=>reopen when timeout.
I am search around and it seems that I cannot change the timeout of the service, so the only way is trying to auto-close=>reopen the service, but I don't know how to.
The below is my code, could you guy give me some direction with it?
1. The login page: only have username and password field, use will be asked to say the username. If it is exist, then asked to say password.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiDemo.LoginPage"
BackgroundColor="White">
<ContentPage.Content>
<StackLayout Margin="30" VerticalOptions="StartAndExpand">
<Label
x:Name="lblTitle"
HorizontalTextAlignment="Center"
FontSize="Large"
FontAttributes="Bold"
/>
<Label/>
<Button
x:Name="btnSpeak"
Text="Start"
Clicked="btnSpeak_Clicked"
FontAttributes="Bold"
BackgroundColor="DarkGreen"
/>
<Label/>
<Label
x:Name="lblUsername"
Text="Username"
FontAttributes="Bold"
/>
<Entry
x:Name="txtUsername"
TextColor="Black"
FontSize="18"
VerticalOptions="StartAndExpand"
HorizontalOptions="Fill"
IsReadOnly="True"
/>
<Label/>
<Label
x:Name="lblPassword"
Text="Password"
FontAttributes="Bold"
/>
<Entry
x:Name="txtPassword"
IsPassword="True"
TextColor="Black"
FontSize="18"
VerticalOptions="StartAndExpand"
HorizontalOptions="Fill"
IsReadOnly="True"
/>
<Label/>
<Label
x:Name="lblDisplayname"
Text="Name"
FontAttributes="Bold"
/>
<Label
x:Name="txtDisplayname"
/>
<Label/>
<Label
x:Name="lblMessage"
Text=""/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
using MauiDemo.Common;
using MauiDemo.Speech;
using Microsoft.Maui.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MauiDemo
{
public partial class LoginPage : ContentPage
{
private string _field = string.Empty;
private int _waitTime = 2000;
public List<Language> Languages { get; }
private SpeechToTextImplementation _speechRecongnitionInstance;
private struct VoiceMode
{
int Username = 1;
int Password = 2;
}
public LoginPage()
{
InitializeComponent();
this.lblTitle.Text = "Login" + App.Status;
CheckMicrophone();
CommonData.CurrentField = string.Empty;
try
{
_speechRecongnitionInstance = new SpeechToTextImplementation();
_speechRecongnitionInstance.Language = DefaultData.SettingLanguage;
}
catch (Exception ex)
{
DisplayAlert("Error", ex.Message, "OK");
}
MessagingCenter.Subscribe<ISpeechToText, string>(this, "STT", (sender, args) =>
{
ReceivedUsernameAsync(args);
});
MessagingCenter.Subscribe<ISpeechToText>(this, "Final", (sender) =>
{
btnSpeak.IsEnabled = true;
});
MessagingCenter.Subscribe<IMessageSender, string>(this, "STT", (sender, args) =>
{
SpeechToTextRecievedAsync(args);
});
isReceiveUsername = false;
isReceivePassword = false;
RequestUsername();
}
protected override void OnDisappearing()
{
CommonData.CurrentField = string.Empty;
base.OnDisappearing();
}
private async void btnSpeak_Clicked(Object sender, EventArgs e)
{
isReceiveUsername = false;
isReceivePassword = false;
await RequestUsername();
}
private async void SpeechToTextRecievedAsync(string args)
{
switch (_field)
{
case "Username":
await this.ReceivedUsernameAsync(args);
break;
case "Password":
await this.ReceivedPasswordAsync(args);
break;
}
}
bool isReceiveUsername = false;
bool isReceivePassword = false;
private async Task ReceivedUsernameAsync(string args)
{
txtUsername.Text = args.Replace(" ", string.Empty);
lblMessage.Text = string.Empty;
if (string.IsNullOrWhiteSpace(txtUsername.Text))
{
isReceiveUsername = false;
}
else
{
isReceiveUsername = true;
var checkUser = DefaultData.Users.Where(x => x.Username.ToLower().Equals(txtUsername.Text.ToLower()));
if (checkUser.Any())
{
await RequestPassword();
}
else
{
string message = CommonData.GetMessage(MessageCode.WrongUsername);
lblMessage.Text = message;
isReceiveUsername = false;
await RequestUsername(message);
}
}
}
private async Task ReceivedPasswordAsync(string args)
{
txtPassword.Text = args.Replace(" ", string.Empty);
lblMessage.Text = string.Empty;
if (string.IsNullOrWhiteSpace(txtPassword.Text))
{
isReceivePassword = false;
}
else
{
isReceivePassword = true;
var checkUser = DefaultData.Users.Where(x => x.Username.ToLower().Equals(txtUsername.Text.ToLower()) && x.Password.Equals(txtPassword.Text));
if (checkUser.Any())
{
_field = "";
lblDisplayname.Text = checkUser.FirstOrDefault().Displayname;
string msg = CommonData.GetMessage(MessageCode.LoginSuccess);
await Plugin.TextToSpeech.CrossTextToSpeech.Current.Speak(
msg
, crossLocale: CommonData.GetCrossLocale(DefaultData.SettingLanguage)
, speakRate: DefaultData.SettingSpeed
, pitch: DefaultData.SettingPitch
);
await Navigation.PushAsync(new MainPage());
}
else
{
string message = CommonData.GetMessage(MessageCode.WrongPassword);
lblMessage.Text = message;
isReceivePassword = false;
await RequestPassword(message);
}
}
}
private async Task RepeatVoiceUsername(string message)
{
do
{
//_speechRecongnitionInstance.StopSpeechToText();
//_speechRecongnitionInstance.StartSpeechToText();
await Plugin.TextToSpeech.CrossTextToSpeech.Current.Speak(
message
, crossLocale: CommonData.GetCrossLocale(DefaultData.SettingLanguage)
, speakRate: DefaultData.SettingSpeed
, pitch: DefaultData.SettingPitch
);
Thread.Sleep(_waitTime);
}
while (!isReceiveUsername);
}
private async Task RepeatVoicePassword(string message)
{
do
{
//_speechRecongnitionInstance.StopSpeechToText();
//_speechRecongnitionInstance.StartSpeechToText();
await Plugin.TextToSpeech.CrossTextToSpeech.Current.Speak(
message
, crossLocale: CommonData.GetCrossLocale(DefaultData.SettingLanguage)
, speakRate: DefaultData.SettingSpeed
, pitch: DefaultData.SettingPitch
);
Thread.Sleep(_waitTime);
}
while (!isReceivePassword);
}
private bool CheckMicrophone()
{
string rec = Android.Content.PM.PackageManager.FeatureMicrophone;
if (rec != "android.hardware.microphone")
{
// no microphone, no recording. Disable the button and output an alert
DisplayAlert("Error", CommonData.GetMessage(MessageCode.SettingSaveSuccess), "OK");
btnSpeak.IsEnabled = false;
return false;
}
return true;
}
private async Task RequestUsername(string message = "")
{
_field = "Username";
isReceiveUsername = false;
txtUsername.Text = string.Empty;
lblDisplayname.Text = string.Empty;
txtUsername.Focus();
message = (message.IsNullOrWhiteSpace() ? CommonData.GetMessage(MessageCode.InputUsername) : message);
Task.Run(() => RepeatVoiceUsername(message));
_speechRecongnitionInstance.StartSpeechToText(_field);
}
private async Task RequestPassword(string message = "")
{
_field = "Password";
isReceivePassword = false;
txtPassword.Text = string.Empty;
lblDisplayname.Text = string.Empty;
txtPassword.Focus();
message = (message.IsNullOrWhiteSpace() ? CommonData.GetMessage(MessageCode.InputPassword) : message);
Task.Run(() => RepeatVoicePassword(message));
_speechRecongnitionInstance.StartSpeechToText(_field);
}
}
}
2. The Speech Recognizer class:
using Android.App;
using Android.Content;
using Android.Speech;
using Java.Util;
using Plugin.CurrentActivity;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MauiDemo.Speech
{
public class SpeechToTextImplementation
{
public static AutoResetEvent autoEvent = new AutoResetEvent(false);
private readonly int VOICE = 10;
private Activity _activity;
private float _timeOut = 3;
private string _text;
public SpeechToTextImplementation()
{
_activity = CrossCurrentActivity.Current.Activity;
}
public SpeechToTextImplementation(string text)
{
_text = text;
_activity = CrossCurrentActivity.Current.Activity;
}
public string Language;
public void StartSpeechToText()
{
StartRecordingAndRecognizing();
}
public void StartSpeechToText(string text)
{
_text = text;
StartRecordingAndRecognizing();
}
private async void StartRecordingAndRecognizing()
{
string rec = global::Android.Content.PM.PackageManager.FeatureMicrophone;
if (rec == "android.hardware.microphone")
{
try
{
var locale = Locale.Default;
if (!string.IsNullOrWhiteSpace(Language))
{
locale = new Locale(Language);
}
Intent voiceIntent = new Intent(RecognizerIntent.ActionRecognizeSpeech);
voiceIntent.PutExtra(RecognizerIntent.ExtraLanguageModel, RecognizerIntent.LanguageModelFreeForm);
voiceIntent.PutExtra(RecognizerIntent.ExtraPrompt, _text);
voiceIntent.PutExtra(RecognizerIntent.ExtraSpeechInputCompleteSilenceLengthMillis, _timeOut * 1000);
voiceIntent.PutExtra(RecognizerIntent.ExtraSpeechInputPossiblyCompleteSilenceLengthMillis, _timeOut * 1000);
voiceIntent.PutExtra(RecognizerIntent.ExtraSpeechInputMinimumLengthMillis, _timeOut * 1000);
voiceIntent.PutExtra(RecognizerIntent.ExtraMaxResults, 1);
voiceIntent.PutExtra(RecognizerIntent.ExtraLanguage, locale.ToString());
_activity.StartActivityForResult(voiceIntent, VOICE);
await Task.Run(() => { autoEvent.WaitOne(new TimeSpan(0, 2, 0)); });
}
catch (ActivityNotFoundException ex)
{
String appPackageName = "com.google.android.googlequicksearchbox";
try
{
Intent intent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse("market://details?id=" + appPackageName));
_activity.StartActivityForResult(intent, VOICE);
}
catch (ActivityNotFoundException e)
{
Intent intent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse("https://play.google.com/store/apps/details?id=" + appPackageName));
_activity.StartActivityForResult(intent, VOICE);
}
}
}
else
{
throw new Exception("No mic found");
}
}
public void StopSpeechToText()
{
// Do something here to close the service
}
}
}
3. MainActivity:
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Speech;
using MauiDemo.Common;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
namespace MauiDemo
{
[Activity(Label = "Maui Demo", Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)]
public class MainActivity : MauiAppCompatActivity, IMessageSender
{
private readonly int VOICE = 10;
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (requestCode == VOICE)
{
if (resultCode == Result.Ok)
{
var matches = data.GetStringArrayListExtra(RecognizerIntent.ExtraResults);
if (matches.Count != 0)
{
string textInput = matches[0];
MessagingCenter.Send<IMessageSender, string>(this, "STT", textInput);
}
else
{
MessagingCenter.Send<IMessageSender, string>(this, "STT", "");
}
}
}
base.OnActivityResult(requestCode, resultCode, data);
}
}
}
4. MainApplication
using Android.App;
using Android.OS;
using Android.Runtime;
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Plugin.CurrentActivity;
using System;
namespace MauiDemo
{
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override void OnCreate()
{
base.OnCreate();
CrossCurrentActivity.Current.Init(this);
}
public override void OnTerminate()
{
base.OnTerminate();
}
public void OnActivityCreated(Activity activity, Bundle savedInstanceState)
{
CrossCurrentActivity.Current.Activity = activity;
}
public void OnActivityDestroyed(Activity activity)
{
}
public void OnActivityPaused(Activity activity)
{
}
public void OnActivityResumed(Activity activity)
{
CrossCurrentActivity.Current.Activity = activity;
}
public void OnActivitySaveInstanceState(Activity activity, Bundle outState)
{
}
public void OnActivityStarted(Activity activity)
{
CrossCurrentActivity.Current.Activity = activity;
}
public void OnActivityStopped(Activity activity)
{
}
}
}