diff --git a/android/app/build.gradle b/android/app/build.gradle index 389ec87..40de5e6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 compileOptions { 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). applicationId "org.bytedream.yamete_kudasai" minSdkVersion 16 - targetSdkVersion 30 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 681b907..68e1d81 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,9 @@ - - + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> + android:name="io.flutter.embedding.android.SplashScreenDrawable" + android:resource="@drawable/launch_background" + /> @@ -38,22 +38,15 @@ android:name="flutterEmbedding" android:value="2" /> + + + - - - - - - - - - - diff --git a/lib/background.dart b/lib/background.dart index 04b5ebb..c4cf5d5 100644 --- a/lib/background.dart +++ b/lib/background.dart @@ -38,25 +38,13 @@ void initBackground() { ); Map? data; - int running = 0; StreamSubscription? sub = _portUpdate.stream.listen((event) async { data ??= (jsonDecode(generateEventData(await SharedPreferences.getInstance())) as Map) .map((key, value) => MapEntry(UpdateAction.values.elementAt(int.parse(key)), value as String)); if (data!.containsKey(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; - if (--running == 0) { - FlutterBackgroundService().setNotificationInfo( - title: 'Yamete Kudasai', - content: 'Running' - ); - } } }); diff --git a/lib/main.dart b/lib/main.dart index a70ef9c..0146fc7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,15 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.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:open_file/open_file.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:url_launcher/url_launcher.dart'; import 'package:yamete_kudasai/background.dart'; @@ -15,7 +19,20 @@ import 'choose_audio.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); 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 { @@ -26,97 +43,89 @@ class YameteKudasai extends StatefulWidget { class _YameteKudasaiState extends State { @override 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)['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); + checkUpdate(context); - return 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.black26, - onPrimary: Colors.white, - ), + return Scaffold( + appBar: AppBar( + title: const Text('Yamete Kudasai'), ), - home: Scaffold( - appBar: AppBar( - title: const Text('Yamete Kudasai'), - ), - body: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - 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); - }, + body: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + child: Row( + children: const [ + Text("Start"), + Icon(Icons.play_arrow_outlined) + ], ), - ElevatedButton( - child: Row( - children: const [ - Text("Stop"), - Icon(Icons.stop_outlined) - ], - ), - onPressed: () { - FlutterBackgroundService().sendData({'action': 'stop'}); - }, - ) - ], - ), + onPressed: () async { + WidgetsFlutterBinding.ensureInitialized(); + FlutterBackgroundService().sendData({'action': 'stop'}); + while (await isRunning()) {} + FlutterBackgroundService.initialize(initBackground); + }, + ), + ElevatedButton( + 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( + backgroundColor: Colors.black, title: Text('Newer version is available ($tag)'), actions: [ TextButton( - onPressed: () async { - 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') + onPressed: () async { + await updateAPK(context, apkUrl); + await showDialog( + context: context, + builder: (BuildContext context) => FutureProgressDialog( + updateAPK(context, apkUrl), + decoration: const BoxDecoration( + color: Colors.transparent + ), + message: const Text('Downloading update...'), + ) + ); + }, + child: const Text('Update') ), TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('No thanks :)') + onPressed: () async { + 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 { final entries = actions.entries; return ListView.builder( - shrinkWrap: true, - itemCount: actions.length, - itemBuilder: (BuildContext context, int index) { - final item = entries.elementAt(index); + shrinkWrap: true, + itemCount: actions.length, + itemBuilder: (BuildContext context, int index) { + final item = entries.elementAt(index); - final activatedKey = "${item.key.index}.activated"; - final targetKey = "${item.key.index}.target"; + final activatedKey = "${item.key.index}.activated"; + final targetKey = "${item.key.index}.target"; - final activatedAudio = prefs.getBool(activatedKey) ?? true; - final targetAudio = prefs.getString(targetKey) ?? "assets/audio/yamete_kudasai.mp3"; + final activatedAudio = prefs.getBool(activatedKey) ?? true; + final targetAudio = prefs.getString(targetKey) ?? "assets/audio/yamete_kudasai.mp3"; - return ListTile( - title: Text(item.value), - subtitle: Text(audio.entries.firstWhere((element) => element.value == targetAudio).key), - trailing: Switch( - activeColor: Theme.of(context).colorScheme.secondary, - value: prefs.getBool(activatedKey) ?? true, - onChanged: (bool newValue) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool(activatedKey, !activatedAudio); - FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); - setState(() {}); - }), - onTap: () async { - final audioFile = await Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ChooseAudio(targetAudio) - ) - ); - if (audioFile != null && audioFile != targetAudio) { - SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setString(targetKey, audioFile); - FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); - setState(() {}); - } - }, - ); - } + return ListTile( + title: Text(item.value), + subtitle: Text(audio.entries.firstWhere((element) => element.value == targetAudio).key), + trailing: Switch( + activeColor: Theme.of(context).colorScheme.secondary, + value: prefs.getBool(activatedKey) ?? true, + onChanged: (bool newValue) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool(activatedKey, !activatedAudio); + FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); + setState(() {}); + }), + onTap: () async { + final audioFile = await Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ChooseAudio(targetAudio) + ) + ); + if (audioFile != null && audioFile != targetAudio) { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString(targetKey, audioFile); + FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); + setState(() {}); + } + }, + ); + } ); } ); } + Future 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); + 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 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(); + 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 isRunning() async { try { FlutterBackgroundService().sendData({'action': 'ping'}); await FlutterBackgroundService().onDataReceived.first.timeout(const Duration(milliseconds: 500)); return true; - } on Exception catch (e) { + } on Exception { return false; } } diff --git a/plugin/port_update/android/src/main/java/org/bytedream/port_update/PortUpdatePlugin.java b/plugin/port_update/android/src/main/java/org/bytedream/port_update/PortUpdatePlugin.java index 592304e..d3266b3 100644 --- a/plugin/port_update/android/src/main/java/org/bytedream/port_update/PortUpdatePlugin.java +++ b/plugin/port_update/android/src/main/java/org/bytedream/port_update/PortUpdatePlugin.java @@ -45,11 +45,11 @@ public class PortUpdatePlugin implements FlutterPlugin, StreamHandler { batteryReceiver = createBatteryReceiver(eventSink); headphoneReceiver = createHeadphoneReceiver(eventSink); - context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - context.registerReceiver(headphoneReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + Intent batteryIntent = context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + Intent headphoneIntent = context.registerReceiver(headphoneReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); - // lastBatteryStatus = getBatteryStatus(intent); - // lastHeadphoneStatus = getHeadphoneStatus(intent); + if (batteryIntent != null) lastBatteryStatus = getBatteryStatus(batteryIntent); + if (headphoneIntent != null) lastHeadphoneStatus = getHeadphoneStatus(headphoneIntent); } @Override diff --git a/pubspec.yaml b/pubspec.yaml index 93fc0ac..1cb2461 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0 +version: 1.1.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -36,8 +36,12 @@ dependencies: audioplayers: ^0.20.1 cupertino_icons: ^1.0.2 flutter_background_service: ^0.1.5 + future_progress_dialog: ^0.2.0 http: ^0.13.4 + open_file: ^3.2.1 package_info_plus: ^1.3.0 + path_provider: ^2.0.7 + permission_handler: ^8.3.0 shared_preferences: ^2.0.8 url_launcher: ^6.0.13