Skip to content

Commit 3ba563e

Browse files
committed
added flutter_foreground_task and associated changes to allow the transcription app to work while the phone is locked/backgrounded
1 parent 94513a3 commit 3ba563e

File tree

6 files changed

+159
-69
lines changed

6 files changed

+159
-69
lines changed

android/app/src/main/AndroidManifest.xml

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
<!-- legacy for Android 9 or lower -->
1717
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
1818

19+
<!-- Foreground service required to maintain BLE connection when phone is locked or app is backgrounded -->
20+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
21+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
22+
1923
<!-- Internet permission to contact Google Cloud servers for Speech-to-Text API -->
2024
<uses-permission android:name="android.permission.INTERNET"/>
2125

@@ -45,6 +49,13 @@
4549
<category android:name="android.intent.category.LAUNCHER"/>
4650
</intent-filter>
4751
</activity>
52+
53+
<service
54+
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
55+
android:foregroundServiceType="connectedDevice"
56+
android:exported="false"
57+
/>
58+
4859
<!-- Don't delete the meta-data below.
4960
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
5061
<meta-data

ios/Runner/Info.plist

+4
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,9 @@
4747
<true/>
4848
<key>NSBluetoothAlwaysUsageDescription</key>
4949
<string>This app needs Bluetooth to function</string>
50+
<key>UIBackgroundModes</key>
51+
<array>
52+
<string>bluetooth-central</string>
53+
</array>
5054
</dict>
5155
</plist>

lib/foreground_service.dart

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'dart:isolate';
2+
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
3+
import 'package:logging/logging.dart';
4+
5+
final _log = Logger("Foreground task");
6+
7+
void initializeForegroundService() {
8+
FlutterForegroundTask.init(
9+
androidNotificationOptions: AndroidNotificationOptions(
10+
channelId: 'foreground_service',
11+
channelName: 'Frame Service',
12+
channelImportance: NotificationChannelImportance.MIN,
13+
iconData: null,
14+
),
15+
iosNotificationOptions: const IOSNotificationOptions(
16+
showNotification: false,
17+
playSound: false,
18+
),
19+
foregroundTaskOptions: const ForegroundTaskOptions(
20+
isOnceEvent: true,
21+
),
22+
);
23+
}
24+
25+
Future<void> startForegroundService() async {
26+
if (!(await FlutterForegroundTask.isRunningService)) {
27+
FlutterForegroundTask.startService(
28+
notificationTitle: 'Frame is connected',
29+
notificationText: 'Tap to return to the app',
30+
callback: _startForegroundCallback,
31+
);
32+
}
33+
}
34+
35+
@pragma('vm:entry-point')
36+
void _startForegroundCallback() {
37+
FlutterForegroundTask.setTaskHandler(_ForegroundFirstTaskHandler());
38+
}
39+
40+
class _ForegroundFirstTaskHandler extends TaskHandler {
41+
@override
42+
void onStart(DateTime timestamp, SendPort? sendPort) async {
43+
_log.info("Starting foreground task");
44+
}
45+
46+
@override
47+
void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {
48+
_log.info("Foreground repeat event triggered");
49+
}
50+
51+
@override
52+
void onDestroy(DateTime timestamp, SendPort? sendPort) async {
53+
_log.info("Destroying foreground task");
54+
FlutterForegroundTask.stopService();
55+
}
56+
}

lib/main.dart

+79-69
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:typed_data';
33

44
import 'package:flutter/material.dart';
5+
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
56
import 'package:google_speech/endless_streaming_service_v2.dart';
67
import 'package:google_speech/generated/google/cloud/speech/v2/cloud_speech.pb.dart';
78
import 'package:google_speech/google_speech.dart';
@@ -14,8 +15,14 @@ import 'package:simple_frame_app/tx/code.dart';
1415
import 'package:simple_frame_app/tx/plain_text.dart';
1516
import 'package:simple_frame_app/text_utils.dart';
1617
import 'package:simple_frame_app/simple_frame_app.dart';
18+
import 'foreground_service.dart';
1719

18-
void main() => runApp(const MainApp());
20+
void main() {
21+
// Set up Android foreground service
22+
initializeForegroundService();
23+
24+
runApp(const MainApp());
25+
}
1926

2027
final _log = Logger("MainApp");
2128

@@ -329,79 +336,82 @@ class MainAppState extends State<MainApp> with SimpleFrameAppState {
329336

330337
@override
331338
Widget build(BuildContext context) {
332-
return MaterialApp(
333-
title: 'Transcribe - Google Cloud Speech',
334-
theme: ThemeData.dark(),
335-
home: Scaffold(
336-
appBar: AppBar(
337-
title: const Text('Transcribe - Google Cloud Speech'),
338-
actions: [getBatteryWidget()]
339-
),
340-
body: Center(
341-
child: Container(
342-
margin: const EdgeInsets.symmetric(horizontal: 16),
343-
child: Column(
344-
mainAxisAlignment: MainAxisAlignment.center,
345-
children: [
346-
TextField(controller: _serviceAccountJsonController, obscureText: false, decoration: const InputDecoration(hintText: 'Enter Service Account JSON'),),
347-
TextField(controller: _projectIdController, obscureText: false, decoration: const InputDecoration(hintText: 'Enter Project Id'),),
348-
TextField(controller: _languageCodeController, obscureText: false, decoration: const InputDecoration(hintText: 'Enter Language Code e.g. en-US'),),
349-
if (_errorMsg != null) Text(_errorMsg!, style: const TextStyle(backgroundColor: Colors.red)),
350-
ElevatedButton(onPressed: _savePrefs, child: const Text('Save')),
351-
352-
Expanded(child: Column(
353-
mainAxisAlignment: MainAxisAlignment.center,
354-
children: [
355-
Expanded(
356-
child: ListView.builder(
357-
controller: _transcriptController, // Auto-scroll controller
358-
itemCount: _transcript.length,
359-
itemBuilder: (context, index) {
360-
return Text(
361-
_transcript[index],
362-
style: _textStyle,
363-
);
364-
},
365-
),
366-
),
367-
const Divider(),
368-
ConstrainedBox(
369-
constraints: BoxConstraints(
370-
maxHeight: _textStyle.fontSize! * 5
339+
startForegroundService();
340+
return WithForegroundTask(
341+
child: MaterialApp(
342+
title: 'Transcribe - Google Cloud Speech',
343+
theme: ThemeData.dark(),
344+
home: Scaffold(
345+
appBar: AppBar(
346+
title: const Text('Transcribe - Google Cloud Speech'),
347+
actions: [getBatteryWidget()]
348+
),
349+
body: Center(
350+
child: Container(
351+
margin: const EdgeInsets.symmetric(horizontal: 16),
352+
child: Column(
353+
mainAxisAlignment: MainAxisAlignment.center,
354+
children: [
355+
TextField(controller: _serviceAccountJsonController, obscureText: false, decoration: const InputDecoration(hintText: 'Enter Service Account JSON'),),
356+
TextField(controller: _projectIdController, obscureText: false, decoration: const InputDecoration(hintText: 'Enter Project Id'),),
357+
TextField(controller: _languageCodeController, obscureText: false, decoration: const InputDecoration(hintText: 'Enter Language Code e.g. en-US'),),
358+
if (_errorMsg != null) Text(_errorMsg!, style: const TextStyle(backgroundColor: Colors.red)),
359+
ElevatedButton(onPressed: _savePrefs, child: const Text('Save')),
360+
361+
Expanded(child: Column(
362+
mainAxisAlignment: MainAxisAlignment.center,
363+
children: [
364+
Expanded(
365+
child: ListView.builder(
366+
controller: _transcriptController, // Auto-scroll controller
367+
itemCount: _transcript.length,
368+
itemBuilder: (context, index) {
369+
return Text(
370+
_transcript[index],
371+
style: _textStyle,
372+
);
373+
},
374+
),
371375
),
372-
child: Align(alignment: Alignment.centerLeft,
373-
child: SingleChildScrollView(
374-
controller: _partialResultController,
375-
child: Text(_partialResult, style: _textStyle)
376-
)
376+
const Divider(),
377+
ConstrainedBox(
378+
constraints: BoxConstraints(
379+
maxHeight: _textStyle.fontSize! * 5
380+
),
381+
child: Align(alignment: Alignment.centerLeft,
382+
child: SingleChildScrollView(
383+
controller: _partialResultController,
384+
child: Text(_partialResult, style: _textStyle)
385+
)
386+
),
377387
),
378-
),
379-
],
380-
)),
381-
],
388+
],
389+
)),
390+
],
391+
),
382392
),
383393
),
394+
floatingActionButton: Stack(
395+
children: [
396+
if (_transcript.isNotEmpty) Positioned(
397+
bottom: 90,
398+
right: 20,
399+
child: FloatingActionButton(
400+
onPressed: () {
401+
Share.share(_transcript.join('\n'));
402+
},
403+
child: const Icon(Icons.share)),
404+
),
405+
Positioned(
406+
bottom: 20,
407+
right: 20,
408+
child: getFloatingActionButtonWidget(const Icon(Icons.mic), const Icon(Icons.mic_off)) ?? Container(),
409+
),
410+
]
411+
),
412+
persistentFooterButtons: getFooterButtonsWidget(),
384413
),
385-
floatingActionButton: Stack(
386-
children: [
387-
if (_transcript.isNotEmpty) Positioned(
388-
bottom: 90,
389-
right: 20,
390-
child: FloatingActionButton(
391-
onPressed: () {
392-
Share.share(_transcript.join('\n'));
393-
},
394-
child: const Icon(Icons.share)),
395-
),
396-
Positioned(
397-
bottom: 20,
398-
right: 20,
399-
child: getFloatingActionButtonWidget(const Icon(Icons.mic), const Icon(Icons.mic_off)) ?? Container(),
400-
),
401-
]
402-
),
403-
persistentFooterButtons: getFooterButtonsWidget(),
404-
),
414+
)
405415
);
406416
}
407417
}

pubspec.lock

+8
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ packages:
118118
url: "https://pub.dev"
119119
source: hosted
120120
version: "1.32.12"
121+
flutter_foreground_task:
122+
dependency: "direct main"
123+
description:
124+
name: flutter_foreground_task
125+
sha256: "6cf10a27f5e344cd2ecad0752d3a5f4ec32846d82fda8753b3fe2480ebb832a3"
126+
url: "https://pub.dev"
127+
source: hosted
128+
version: "6.5.0"
121129
flutter_lints:
122130
dependency: "direct dev"
123131
description:

pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
simple_frame_app: ^2.0.0
1515
shared_preferences: ^2.3.2
1616
share_plus: ^10.1.0
17+
flutter_foreground_task: ^6.5.0
1718

1819
dev_dependencies:
1920
flutter_test:

0 commit comments

Comments
 (0)