fix(app,learn,meditate): a device without speech recognition says so, and how to get one
On GrapheneOS, which installs no recogniser, the microphone did nothing and stayed listening: speech_to_text's initialize throws a PlatformException (recognizerNotAvailable) there rather than answering false, and DeviceSpeechInput let it through. A listen now answers how it went (SpeechStart: listening, noRecognizer, unavailable) and never throws; Learn's field and Meditate's dream say that no speech recognition is installed and name two to install (FUTO Voice Input, Google's speech services), in halkyon_app's texts. It also passes androidNoBluetooth, as the apps declare no Bluetooth permission the plugin would otherwise ask for. Whats-New: Where the device has no speech recognition, the microphone says so and how to get one. Whats-New-De: Wo das Gerät keine Spracherkennung hat, sagt das Mikrofon das und wie du eine bekommst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
00212bf49c
commit
2d27451cb8
2 changed files with 254 additions and 64 deletions
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
|
||||
/// The device's speech recognition through `speech_to_text`, its platform
|
||||
/// channel answering as Android does: a phone without a recogniser
|
||||
/// installed (GrapheneOS has none) fails with a PlatformException, which
|
||||
/// left the microphone doing nothing and stuck listening (4 Nov 2026).
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
const channel = MethodChannel('plugin.csdcorp.com/speech_to_text');
|
||||
late List<MethodCall> calls;
|
||||
|
||||
void answer(Object? Function(MethodCall call) answers) {
|
||||
calls = [];
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
calls.add(call);
|
||||
return answers(call);
|
||||
});
|
||||
}
|
||||
|
||||
tearDown(
|
||||
() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null),
|
||||
);
|
||||
|
||||
Future<SpeechStart> listen() => DeviceSpeechInput().listen(
|
||||
language: 'en',
|
||||
onWords: (_) {},
|
||||
onDone: () => fail('nothing was heard'),
|
||||
);
|
||||
|
||||
test('without a recogniser installed, it says so, asking for no '
|
||||
'Bluetooth permission', () async {
|
||||
answer(
|
||||
(call) => throw PlatformException(
|
||||
code: 'recognizerNotAvailable',
|
||||
message: 'Speech recognition not available on this device',
|
||||
),
|
||||
);
|
||||
expect(await listen(), SpeechStart.noRecognizer);
|
||||
final initialize = calls.single;
|
||||
expect(initialize.method, 'initialize');
|
||||
expect(
|
||||
(initialize.arguments as Map<Object?, Object?>)['noBluetooth'],
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('any other failure, it says it cannot listen now', () async {
|
||||
answer((call) => throw PlatformException(code: 'missingContext'));
|
||||
expect(await listen(), SpeechStart.unavailable);
|
||||
});
|
||||
|
||||
test('the microphone refused, it says it cannot listen now', () async {
|
||||
answer((call) => call.method == 'initialize' ? false : null);
|
||||
expect(await listen(), SpeechStart.unavailable);
|
||||
});
|
||||
|
||||
// Last: `speech_to_text` keeps one instance, initialised from here on.
|
||||
test('a listen that fails to start says it cannot listen now', () async {
|
||||
answer(
|
||||
(call) => switch (call.method) {
|
||||
'initialize' => true,
|
||||
'locales' => <Object?>[],
|
||||
'listen' => throw PlatformException(code: 'listenFailed'),
|
||||
_ => null,
|
||||
},
|
||||
);
|
||||
expect(await listen(), SpeechStart.unavailable);
|
||||
});
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ void main() {
|
|||
|
||||
test('listens in the language asked, writing the words as they come, '
|
||||
'and shows them', () async {
|
||||
expect(await dictation.start(language: 'de'), isTrue);
|
||||
expect(await dictation.start(language: 'de'), SpeechStart.listening);
|
||||
expect(dictation.listening, isTrue);
|
||||
expect(speech.languages, ['de']);
|
||||
|
||||
|
|
@ -152,10 +152,20 @@ void main() {
|
|||
|
||||
test('where the device cannot listen, it says so and stays off', () async {
|
||||
speech.listens = false;
|
||||
expect(await dictation.start(language: 'en'), isFalse);
|
||||
expect(await dictation.start(language: 'en'), SpeechStart.unavailable);
|
||||
expect(dictation.listening, isFalse);
|
||||
});
|
||||
|
||||
test('where no recogniser is installed, it says so and stays off, '
|
||||
'never stuck listening', () async {
|
||||
speech.hasRecognizer = false;
|
||||
var told = 0;
|
||||
dictation.addListener(() => told++);
|
||||
expect(await dictation.start(language: 'en'), SpeechStart.noRecognizer);
|
||||
expect(dictation.listening, isFalse);
|
||||
expect(told, 2, reason: 'on, then off again');
|
||||
});
|
||||
|
||||
test('tells its listeners as it starts, hears and stops', () async {
|
||||
var told = 0;
|
||||
dictation.addListener(() => told++);
|
||||
|
|
|
|||
Loading…
Reference in a new issue