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>
2026-11-04 20:45:55 +01:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18)
On Android the microphone now opens the app handling
RECOGNIZE_SPEECH (FUTO Voice Input, Google's), the default the person
chose in Android, and puts the text it returns where the cursor stood,
as Dictation does; cancelled, nothing. Only where none is installed, or
it can't be opened, does speech_to_text listen in line as before.
halkyon_app: VoiceInputApp (one listen, an answer once closed) on the
channel app.halkyon/voice_input, which DeviceSpeechInput asks first at
every listen, answering SpeechStart.finished so that Dictation doesn't
listen again; speech_to_text isn't touched on that route, so our app
asks for no microphone. SpeechInput.listen and Dictation.start take the
prompt the voice input app shows (the field's hint).
Each app: VoiceInput.kt (queryIntentActivities, startActivityForResult,
the first of EXTRA_RESULTS) attached in MainActivity, which hands it
activity results, and RECOGNIZE_SPEECH among the manifest's queries.
The privacy pages of Learn and Meditate say so.
Our own Whisper and Meditate's dream recordings are dropped (D18).
Whats-New: The microphone now dictates through your phone's voice input app, such as FUTO Voice Input.
Whats-New-De: Das Mikrofon diktiert jetzt über die Spracheingabe-App deines Telefons, etwa FUTO Voice Input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-07 23:45:20 +01:00
|
|
|
group('with a voice input app installed (ADR 0038, D18)', () {
|
|
|
|
|
late _VoiceInputApp app;
|
|
|
|
|
late List<String> heard;
|
|
|
|
|
late SpeechInput speech;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
// speech_to_text answering as with the microphone refused, which
|
|
|
|
|
// leaves its one instance uninitialised for the tests after these.
|
|
|
|
|
answer((call) => call.method == 'initialize' ? false : null);
|
|
|
|
|
app = _VoiceInputApp();
|
|
|
|
|
heard = [];
|
|
|
|
|
speech = DeviceSpeechInput(voiceInputApp: app);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Future<SpeechStart> listen() => speech.listen(
|
|
|
|
|
language: 'de-DE',
|
|
|
|
|
prompt: 'Was hast du geträumt?',
|
|
|
|
|
onWords: heard.add,
|
|
|
|
|
onDone: () => fail('the app has listened, once'),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
test('hands listening to it, in the language and with the prompt '
|
|
|
|
|
'given, and the words it returns on, once', () async {
|
|
|
|
|
app.words = 'Ich war am Meer';
|
|
|
|
|
expect(await listen(), SpeechStart.finished);
|
|
|
|
|
expect(app.asked, [('de-DE', 'Was hast du geträumt?')]);
|
|
|
|
|
expect(heard, ['Ich war am Meer']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('asks speech_to_text nothing, so our app never asks for the '
|
|
|
|
|
'microphone: the voice input app records', () async {
|
|
|
|
|
app.words = 'Ich war am Meer';
|
|
|
|
|
await listen();
|
|
|
|
|
expect(calls, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('cancelled in it, hands nothing on and listens no more', () async {
|
|
|
|
|
app.words = '';
|
|
|
|
|
expect(await listen(), SpeechStart.finished);
|
|
|
|
|
expect(heard, isEmpty);
|
|
|
|
|
expect(calls, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('where it could not be opened, listens in line instead', () async {
|
|
|
|
|
app.words = null;
|
|
|
|
|
expect(await listen(), SpeechStart.unavailable);
|
|
|
|
|
expect(app.asked, hasLength(1));
|
|
|
|
|
expect(calls.first.method, 'initialize');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('none installed, listens in line', () async {
|
|
|
|
|
app.installs = false;
|
|
|
|
|
expect(await listen(), SpeechStart.unavailable);
|
|
|
|
|
expect(app.asked, isEmpty);
|
|
|
|
|
expect(calls.first.method, 'initialize');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
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>
2026-11-04 20:45:55 +01:00
|
|
|
// 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);
|
|
|
|
|
});
|
|
|
|
|
}
|
feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18)
On Android the microphone now opens the app handling
RECOGNIZE_SPEECH (FUTO Voice Input, Google's), the default the person
chose in Android, and puts the text it returns where the cursor stood,
as Dictation does; cancelled, nothing. Only where none is installed, or
it can't be opened, does speech_to_text listen in line as before.
halkyon_app: VoiceInputApp (one listen, an answer once closed) on the
channel app.halkyon/voice_input, which DeviceSpeechInput asks first at
every listen, answering SpeechStart.finished so that Dictation doesn't
listen again; speech_to_text isn't touched on that route, so our app
asks for no microphone. SpeechInput.listen and Dictation.start take the
prompt the voice input app shows (the field's hint).
Each app: VoiceInput.kt (queryIntentActivities, startActivityForResult,
the first of EXTRA_RESULTS) attached in MainActivity, which hands it
activity results, and RECOGNIZE_SPEECH among the manifest's queries.
The privacy pages of Learn and Meditate say so.
Our own Whisper and Meditate's dream recordings are dropped (D18).
Whats-New: The microphone now dictates through your phone's voice input app, such as FUTO Voice Input.
Whats-New-De: Das Mikrofon diktiert jetzt über die Spracheingabe-App deines Telefons, etwa FUTO Voice Input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-07 23:45:20 +01:00
|
|
|
|
|
|
|
|
/// A voice input app the test sets up: installed or not, returning [words]
|
|
|
|
|
/// ('' as cancelled, null as failing to open).
|
|
|
|
|
class _VoiceInputApp() implements VoiceInputApp {
|
|
|
|
|
bool installs = true;
|
|
|
|
|
String? words = '';
|
|
|
|
|
|
|
|
|
|
/// The language and prompt of each listen, in order.
|
|
|
|
|
final asked = <(String?, String?)>[];
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<bool> installed() async => installs;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<String?> listen({required String? language, String? prompt}) async {
|
|
|
|
|
asked.add((language, prompt));
|
|
|
|
|
return words;
|
|
|
|
|
}
|
|
|
|
|
}
|