Proper update service, no sound on normal app start

This commit is contained in:
bytedream 2021-11-17 22:17:54 +01:00
parent ccf871ce8b
commit 498e3dcbf5
6 changed files with 207 additions and 150 deletions

View File

@ -32,7 +32,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 30 compileSdkVersion 31
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -43,7 +43,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "org.bytedream.yamete_kudasai" applicationId "org.bytedream.yamete_kudasai"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
multiDexEnabled true multiDexEnabled true

View File

@ -1,9 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.bytedream.yamete_kudasai"> package="org.bytedream.yamete_kudasai">
<application <application
android:label="Yamete Kudasai" android:label="Yamete Kudasai"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
@ -15,18 +15,18 @@
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<!-- Displays an Android View that continues showing the launch screen <!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of gap between the end of Android's launch screen and the painting of
Flutter's first frame. --> Flutter's first frame. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable" android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" android:resource="@drawable/launch_background"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@ -38,22 +38,15 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<queries> <queries>
<!-- If your app opens https URLs --> <!-- If your app opens https URLs -->
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
<!-- If your app makes calls -->
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
<!-- If your app emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries> </queries>
</manifest> </manifest>

View File

@ -38,25 +38,13 @@ void initBackground() {
); );
Map<UpdateAction, String>? data; Map<UpdateAction, String>? data;
int running = 0;
StreamSubscription<UpdateAction?>? sub = _portUpdate.stream.listen((event) async { StreamSubscription<UpdateAction?>? sub = _portUpdate.stream.listen((event) async {
data ??= (jsonDecode(generateEventData(await SharedPreferences.getInstance())) as Map<String, dynamic>) data ??= (jsonDecode(generateEventData(await SharedPreferences.getInstance())) as Map<String, dynamic>)
.map((key, value) => MapEntry(UpdateAction.values.elementAt(int.parse(key)), value as String)); .map((key, value) => MapEntry(UpdateAction.values.elementAt(int.parse(key)), value as String));
if (data!.containsKey(event!)) { if (data!.containsKey(event!)) {
final player = await _player.play(data![event]!); final player = await _player.play(data![event]!);
running++;
FlutterBackgroundService().setNotificationInfo(
title: 'Yamete Kudasai',
content: 'Dispatching ${actions.values.elementAt(event.index).toLowerCase()} event'
);
await player.onPlayerCompletion.first; await player.onPlayerCompletion.first;
if (--running == 0) {
FlutterBackgroundService().setNotificationInfo(
title: 'Yamete Kudasai',
content: 'Running'
);
}
} }
}); });

View File

@ -1,11 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:future_progress_dialog/future_progress_dialog.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:open_file/open_file.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:yamete_kudasai/background.dart'; import 'package:yamete_kudasai/background.dart';
@ -15,7 +19,20 @@ import 'choose_audio.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
runApp(YameteKudasai()); runApp(MaterialApp(
title: "Yamete Kudasai",
theme: ThemeData.from(
colorScheme: const ColorScheme.highContrastDark(
primary: Color(0xFFFF0000),
primaryVariant: Color(0xFFC20000),
secondary: Colors.purple,
surface: Colors.black,
background: Colors.black12,
onPrimary: Colors.white,
),
),
home: YameteKudasai(),
));
} }
class YameteKudasai extends StatefulWidget { class YameteKudasai extends StatefulWidget {
@ -26,97 +43,89 @@ class YameteKudasai extends StatefulWidget {
class _YameteKudasaiState extends State<YameteKudasai> { class _YameteKudasaiState extends State<YameteKudasai> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
http.get(Uri.https('api.github.com', 'repos/ByteDream/yamete_kudasai/releases/latest'))
.timeout(const Duration(seconds: 5), onTimeout: () => http.Response.bytes([], 504)).then((response) async {
if (response.statusCode == 200) {
final packageInfo = await PackageInfo.fromPlatform();
final tag = (jsonDecode(response.body) as Map<String, dynamic>)['tag_name'] as String;
if (int.parse(tag.substring(1).replaceAll('.', '')) > int.parse(packageInfo.version.replaceAll('.', ''))) {
showDialog(
context: context,
builder: (BuildContext context) => _buildUpdateNotification(context, tag)
);
}
}
});
FlutterBackgroundService.initialize(initBackground, foreground: false); FlutterBackgroundService.initialize(initBackground, foreground: false);
checkUpdate(context);
return MaterialApp( return Scaffold(
title: "Yamete Kudasai", appBar: AppBar(
theme: ThemeData.from( title: const Text('Yamete Kudasai'),
colorScheme: const ColorScheme.highContrastDark(
primary: Color(0xFFFF0000),
primaryVariant: Color(0xFFC20000),
secondary: Colors.purple,
surface: Colors.black,
background: Colors.black26,
onPrimary: Colors.white,
),
), ),
home: Scaffold( body: Center(
appBar: AppBar( child: Column(
title: const Text('Yamete Kudasai'), children: [
), Padding(
body: Center( padding: const EdgeInsets.symmetric(vertical: 20),
child: Column( child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Padding( children: [
padding: const EdgeInsets.symmetric(vertical: 20), ElevatedButton(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: const [
children: [ Text("Start"),
ElevatedButton( Icon(Icons.play_arrow_outlined)
child: Row( ],
children: const [
Text("Start"),
Icon(Icons.play_arrow_outlined)
],
),
onPressed: () async {
WidgetsFlutterBinding.ensureInitialized();
FlutterBackgroundService().sendData({'action': 'stop'});
while (await isRunning()) {}
FlutterBackgroundService.initialize(initBackground);
},
), ),
ElevatedButton( onPressed: () async {
child: Row( WidgetsFlutterBinding.ensureInitialized();
children: const [ FlutterBackgroundService().sendData({'action': 'stop'});
Text("Stop"), while (await isRunning()) {}
Icon(Icons.stop_outlined) FlutterBackgroundService.initialize(initBackground);
], },
), ),
onPressed: () { ElevatedButton(
FlutterBackgroundService().sendData({'action': 'stop'}); child: Row(
}, children: const [
) Text("Stop"),
], Icon(Icons.stop_outlined)
), ],
),
onPressed: () {
FlutterBackgroundService().sendData({'action': 'stop'});
},
)
],
), ),
const Divider(color: Colors.white), ),
_buildAudioSettings() const Divider(color: Colors.white),
], _buildAudioSettings()
), ],
), ),
), ),
); );
} }
Widget _buildUpdateNotification(BuildContext context, String tag) { Widget _buildUpdateNotification(BuildContext context, String tag, String apkUrl) {
return AlertDialog( return AlertDialog(
backgroundColor: Colors.black,
title: Text('Newer version is available ($tag)'), title: Text('Newer version is available ($tag)'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () async { onPressed: () async {
if (await canLaunch('https://github.com/ByteDream/yamete_kudasai/releases/tag/$tag')) { await updateAPK(context, apkUrl);
await launch('https://github.com/ByteDream/yamete_kudasai/releases/tag/$tag'); await showDialog(
} context: context,
}, builder: (BuildContext context) => FutureProgressDialog(
child: const Text('Show new release') updateAPK(context, apkUrl),
decoration: const BoxDecoration(
color: Colors.transparent
),
message: const Text('Downloading update...'),
)
);
},
child: const Text('Update')
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () async {
child: const Text('No thanks :)') if (await canLaunch('https://github.com/ByteDream/Yamete-Kudasai/releases/tag/$tag')) {
await launch('https://github.com/ByteDream/Yamete-Kudasai/releases/tag/$tag');
}
},
child: const Text('Show new release')
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('No thanks :)')
) )
], ],
); );
@ -134,56 +143,119 @@ class _YameteKudasaiState extends State<YameteKudasai> {
final entries = actions.entries; final entries = actions.entries;
return ListView.builder( return ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: actions.length, itemCount: actions.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = entries.elementAt(index); final item = entries.elementAt(index);
final activatedKey = "${item.key.index}.activated"; final activatedKey = "${item.key.index}.activated";
final targetKey = "${item.key.index}.target"; final targetKey = "${item.key.index}.target";
final activatedAudio = prefs.getBool(activatedKey) ?? true; final activatedAudio = prefs.getBool(activatedKey) ?? true;
final targetAudio = prefs.getString(targetKey) ?? "assets/audio/yamete_kudasai.mp3"; final targetAudio = prefs.getString(targetKey) ?? "assets/audio/yamete_kudasai.mp3";
return ListTile( return ListTile(
title: Text(item.value), title: Text(item.value),
subtitle: Text(audio.entries.firstWhere((element) => element.value == targetAudio).key), subtitle: Text(audio.entries.firstWhere((element) => element.value == targetAudio).key),
trailing: Switch( trailing: Switch(
activeColor: Theme.of(context).colorScheme.secondary, activeColor: Theme.of(context).colorScheme.secondary,
value: prefs.getBool(activatedKey) ?? true, value: prefs.getBool(activatedKey) ?? true,
onChanged: (bool newValue) async { onChanged: (bool newValue) async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool(activatedKey, !activatedAudio); prefs.setBool(activatedKey, !activatedAudio);
FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)});
setState(() {}); setState(() {});
}), }),
onTap: () async { onTap: () async {
final audioFile = await Navigator.push<String>( final audioFile = await Navigator.push<String>(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (BuildContext context) => ChooseAudio(targetAudio) builder: (BuildContext context) => ChooseAudio(targetAudio)
) )
); );
if (audioFile != null && audioFile != targetAudio) { if (audioFile != null && audioFile != targetAudio) {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString(targetKey, audioFile); prefs.setString(targetKey, audioFile);
FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)});
setState(() {}); setState(() {});
} }
}, },
); );
} }
); );
} }
); );
} }
Future<void> checkUpdate(BuildContext context) async {
final response = await http.get(Uri.https('api.github.com', 'repos/ByteDream/Yamete-Kudasai/releases/latest'))
.timeout(const Duration(seconds: 5), onTimeout: () => http.Response.bytes([], 504));
if (response.statusCode == 200) {
final packageInfo = await PackageInfo.fromPlatform();
final json = (jsonDecode(response.body) as Map<String, dynamic>);
final tag = json['tag_name'] as String;
final apkUrl = json['assets'][0]['browser_download_url'] as String;
if (int.parse(tag.substring(1).replaceAll('.', '')) > int.parse(packageInfo.version.replaceAll('.', ''))) {
await showDialog(
context: context,
builder: (BuildContext context) => _buildUpdateNotification(context, tag, apkUrl)
);
}
}
}
Future<void> updateAPK(BuildContext context, String apkUrl) async {
ResultType result;
if ((await Permission.storage.request()).isGranted) {
final file = File('/storage/emulated/0/Download/${apkUrl.split('/').last}');
final completer = Completer<void>();
showDialog(
context: context,
builder: (BuildContext context) {
return FutureProgressDialog(
completer.future,
decoration: const BoxDecoration(
color: Colors.transparent
),
message: const Text('Downloading update...'),
);
}
);
if (!(await file.exists())) {
final resp = await http.get(Uri.parse(apkUrl));
await file.writeAsBytes(resp.bodyBytes);
}
result = (await OpenFile.open(file.path)).type;
completer.complete();
} else {
result = ResultType.error;
}
if (result != ResultType.done) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Failed to install update'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Ok'),
)
],
);
}
);
}
Navigator.pop(context);
// await file.delete();
}
Future<bool> isRunning() async { Future<bool> isRunning() async {
try { try {
FlutterBackgroundService().sendData({'action': 'ping'}); FlutterBackgroundService().sendData({'action': 'ping'});
await FlutterBackgroundService().onDataReceived.first.timeout(const Duration(milliseconds: 500)); await FlutterBackgroundService().onDataReceived.first.timeout(const Duration(milliseconds: 500));
return true; return true;
} on Exception catch (e) { } on Exception {
return false; return false;
} }
} }

View File

@ -45,11 +45,11 @@ public class PortUpdatePlugin implements FlutterPlugin, StreamHandler {
batteryReceiver = createBatteryReceiver(eventSink); batteryReceiver = createBatteryReceiver(eventSink);
headphoneReceiver = createHeadphoneReceiver(eventSink); headphoneReceiver = createHeadphoneReceiver(eventSink);
context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); Intent batteryIntent = context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
context.registerReceiver(headphoneReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); Intent headphoneIntent = context.registerReceiver(headphoneReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
// lastBatteryStatus = getBatteryStatus(intent); if (batteryIntent != null) lastBatteryStatus = getBatteryStatus(batteryIntent);
// lastHeadphoneStatus = getHeadphoneStatus(intent); if (headphoneIntent != null) lastHeadphoneStatus = getHeadphoneStatus(headphoneIntent);
} }
@Override @Override

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0 version: 1.1.0
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
@ -36,8 +36,12 @@ dependencies:
audioplayers: ^0.20.1 audioplayers: ^0.20.1
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
flutter_background_service: ^0.1.5 flutter_background_service: ^0.1.5
future_progress_dialog: ^0.2.0
http: ^0.13.4 http: ^0.13.4
open_file: ^3.2.1
package_info_plus: ^1.3.0 package_info_plus: ^1.3.0
path_provider: ^2.0.7
permission_handler: ^8.3.0
shared_preferences: ^2.0.8 shared_preferences: ^2.0.8
url_launcher: ^6.0.13 url_launcher: ^6.0.13