diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3d0aacf..e2907e3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ - + + diff --git a/android/app/src/main/kotlin/com/example/fmassive/Alarm.kt b/android/app/src/main/kotlin/com/example/fmassive/Alarm.kt new file mode 100644 index 0000000..dbce04f --- /dev/null +++ b/android/app/src/main/kotlin/com/example/fmassive/Alarm.kt @@ -0,0 +1,191 @@ +package com.example.fmassive + +import android.annotation.SuppressLint +import android.app.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.CountDownTimer +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlin.math.floor + + +class AlarmModule(private val context: Context) : MethodCallHandler { + + private var countdownTimer: CountDownTimer? = null + var currentMs: Long = 0 + private var running = false + + private val stopReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("AlarmModule", "Received stop broadcast intent") + if (context != null) { + stop(context) + } + } + } + + private val addReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) + override fun onReceive(context: Context?, intent: Intent?) { + if (context != null) { + add(context) + } + } + } + + init { + } + + @RequiresApi(api = Build.VERSION_CODES.O) + fun add(context: Context) { + Log.d("AlarmModule", "Add 1 min to alarm.") + countdownTimer?.cancel() + val newMs = if (running) currentMs.toInt().plus(60000) else 60000 + countdownTimer = getTimer(context, newMs) + countdownTimer?.start() + running = true + //val manager = getManager() + //manager.cancel(AlarmService.NOTIFICATION_ID_DONE) + //val intent = Intent(context, AlarmService::class.java) + //context.stopService(intent) + } + + @RequiresApi(api = Build.VERSION_CODES.O) + fun stop(context: Context) { + Log.d("AlarmModule", "Stop alarm.") + countdownTimer?.cancel() + running = false + //val intent = Intent(context, AlarmService::class.java) + //reactApplicationContext?.stopService(intent) + //val manager = getManager() + //manager.cancel(AlarmService.NOTIFICATION_ID_DONE) + //manager.cancel(NOTIFICATION_ID_PENDING) + //val params = Arguments.createMap().apply { + // putString("minutes", "00") + // putString("seconds", "00") + //} + //reactApplicationContext + // .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + // .emit("tick", params) + } + + @RequiresApi(api = Build.VERSION_CODES.O) + fun timer(context: Context, milliseconds: Int) { + context.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) + context.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST)) + Log.d("AlarmModule", "Queue alarm for $milliseconds delay") + val manager = getManager(context) + //manager.cancel(AlarmService.NOTIFICATION_ID_DONE) + //val intent = Intent(reactApplicationContext, AlarmService::class.java) + //reactApplicationContext.stopService(intent) + countdownTimer?.cancel() + countdownTimer = getTimer(context, milliseconds) + countdownTimer?.start() + running = true + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getTimer( + context: Context, + endMs: Int, + ): CountDownTimer { + val builder = getBuilder(context) + return object : CountDownTimer(endMs.toLong(), 1000) { + @RequiresApi(Build.VERSION_CODES.O) + override fun onTick(current: Long) { + currentMs = current + val seconds = + floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0') + val minutes = + floor((current / 1000).toDouble() / 60).toInt().toString().padStart(2, '0') + builder.setContentText("$minutes:$seconds").setAutoCancel(false).setDefaults(0) + .setProgress(endMs, current.toInt(), false) + .setCategory(NotificationCompat.CATEGORY_PROGRESS).priority = + NotificationCompat.PRIORITY_LOW + val manager = getManager(context) + manager.notify(NOTIFICATION_ID_PENDING, builder.build()) + //val params = Arguments.createMap().apply { + // putString("minutes", minutes) + // putString("seconds", seconds) + //} + //reactApplicationContext + // .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + // .emit("tick", params) + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun onFinish() { + //val context = reactApplicationContext + //context.startForegroundService(Intent(context, AlarmService::class.java)) + //context + // .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + // .emit("finish", Arguments.createMap().apply { + // putString("minutes", "00") + // putString("seconds", "00") + // }) + } + } + } + + @SuppressLint("UnspecifiedImmutableFlag") + @RequiresApi(Build.VERSION_CODES.M) + private fun getBuilder(context: Context): NotificationCompat.Builder { + val contentIntent = Intent(context, MainActivity::class.java) + val pendingContent = + PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) + val addBroadcast = Intent(ADD_BROADCAST).apply { + setPackage(context.packageName) + } + val pendingAdd = + PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE) + val stopBroadcast = Intent(STOP_BROADCAST) + stopBroadcast.setPackage(context.packageName) + val pendingStop = + PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE) + return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) + .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting") + .setContentIntent(pendingContent) + .addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop) + .addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd) + .setDeleteIntent(pendingStop) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun getManager(context: Context): NotificationManager { + val notificationManager = context.getSystemService( + NotificationManager::class.java + ) + val timersChannel = NotificationChannel( + CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW + ) + timersChannel.setSound(null, null) + timersChannel.description = "Progress on rest timers." + notificationManager.createNotificationChannel(timersChannel) + return notificationManager + } + + companion object { + const val STOP_BROADCAST = "stop-timer-event" + const val ADD_BROADCAST = "add-timer-event" + const val CHANNEL_ID_PENDING = "Timer" + const val NOTIFICATION_ID_PENDING = 1 + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "timer" -> { + + } + else -> result.notImplemented() + } + } +} diff --git a/android/app/src/main/kotlin/com/example/fmassive/MainActivity.kt b/android/app/src/main/kotlin/com/example/fmassive/MainActivity.kt index 33c9538..3078c6f 100644 --- a/android/app/src/main/kotlin/com/example/fmassive/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/fmassive/MainActivity.kt @@ -1,6 +1,195 @@ package com.example.fmassive +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.CountDownTimer +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import kotlin.math.floor -class MainActivity: FlutterActivity() { +class MainActivity : FlutterActivity() { + private val CHANNEL = "com.massive/android" + + @RequiresApi(Build.VERSION_CODES.O) + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { + // This method is invoked on the main thread. + call, result -> + if (call.method == "timer") { + val args = call.arguments as ArrayList + timer(args[0]) + } else { + result.notImplemented() + } + } + } + + private var countdownTimer: CountDownTimer? = null + var currentMs: Long = 0 + private var running = false + + private val stopReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("AlarmModule", "Received stop broadcast intent") + stop() + } + } + + private val addReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) + override fun onReceive(context: Context?, intent: Intent?) { + add() + } + } + + init { + } + + @RequiresApi(api = Build.VERSION_CODES.O) + fun add() { + Log.d("AlarmModule", "Add 1 min to alarm.") + countdownTimer?.cancel() + val newMs = if (running) currentMs.toInt().plus(60000) else 60000 + countdownTimer = getTimer(newMs.toLong()) + countdownTimer?.start() + running = true + //val manager = getManager() + //manager.cancel(AlarmService.NOTIFICATION_ID_DONE) + //val intent = Intent(context, AlarmService::class.java) + //context.stopService(intent) + } + + @RequiresApi(api = Build.VERSION_CODES.O) + fun stop() { + Log.d("AlarmModule", "Stop alarm.") + countdownTimer?.cancel() + running = false + //val intent = Intent(context, AlarmService::class.java) + //reactApplicationContext?.stopService(intent) + //val manager = getManager() + //manager.cancel(AlarmService.NOTIFICATION_ID_DONE) + //manager.cancel(NOTIFICATION_ID_PENDING) + //val params = Arguments.createMap().apply { + // putString("minutes", "00") + // putString("seconds", "00") + //} + //reactApplicationContext + // .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + // .emit("tick", params) + } + + @RequiresApi(api = Build.VERSION_CODES.O) + fun timer(milliseconds: Long) { + context.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) + context.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST)) + Log.d("AlarmModule", "Queue alarm for $milliseconds delay") + //val manager = getManager() + //manager.cancel(AlarmService.NOTIFICATION_ID_DONE) + //val intent = Intent(reactApplicationContext, AlarmService::class.java) + //reactApplicationContext.stopService(intent) + countdownTimer?.cancel() + countdownTimer = getTimer(milliseconds) + countdownTimer?.start() + running = true + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getTimer( + endMs: Long, + ): CountDownTimer { + val builder = getBuilder() + return object : CountDownTimer(endMs.toLong(), 1000) { + @RequiresApi(Build.VERSION_CODES.O) + override fun onTick(current: Long) { + currentMs = current + val seconds = + floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0') + val minutes = + floor((current / 1000).toDouble() / 60).toInt().toString().padStart(2, '0') + builder.setContentText("$minutes:$seconds").setAutoCancel(false).setDefaults(0) + .setProgress(endMs.toInt(), current.toInt(), false) + .setCategory(NotificationCompat.CATEGORY_PROGRESS).priority = + NotificationCompat.PRIORITY_LOW + val manager = getManager() + Log.d("AlarmModule", "Notify $NOTIFICATION_ID_PENDING") + manager.notify(NOTIFICATION_ID_PENDING, builder.build()) + //val params = Arguments.createMap().apply { + // putString("minutes", minutes) + // putString("seconds", seconds) + //} + //reactApplicationContext + // .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + // .emit("tick", params) + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun onFinish() { + //val context = reactApplicationContext + //context.startForegroundService(Intent(context, AlarmService::class.java)) + //context + // .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + // .emit("finish", Arguments.createMap().apply { + // putString("minutes", "00") + // putString("seconds", "00") + // }) + } + } + } + + @SuppressLint("UnspecifiedImmutableFlag") + @RequiresApi(Build.VERSION_CODES.M) + private fun getBuilder(): NotificationCompat.Builder { + val contentIntent = Intent(context, MainActivity::class.java) + val pendingContent = + PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) + val addBroadcast = Intent(ADD_BROADCAST).apply { + setPackage(context.packageName) + } + val pendingAdd = + PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE) + val stopBroadcast = Intent(STOP_BROADCAST) + stopBroadcast.setPackage(context.packageName) + val pendingStop = + PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE) + return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) + .setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting") + .setContentIntent(pendingContent) + .addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop) + .addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd) + .setDeleteIntent(pendingStop) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun getManager(): NotificationManager { + val notificationManager = context.getSystemService( + NotificationManager::class.java + ) + val timersChannel = NotificationChannel( + CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW + ) + timersChannel.setSound(null, null) + timersChannel.description = "Progress on rest timers." + notificationManager.createNotificationChannel(timersChannel) + return notificationManager + } + + companion object { + const val STOP_BROADCAST = "stop-timer-event" + const val ADD_BROADCAST = "add-timer-event" + const val CHANNEL_ID_PENDING = "Timer" + const val NOTIFICATION_ID_PENDING = 1 + } } diff --git a/android/app/src/main/kotlin/com/example/fmassive/MyFlutterPlugin.kt b/android/app/src/main/kotlin/com/example/fmassive/MyFlutterPlugin.kt new file mode 100644 index 0000000..b7eb3a7 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/fmassive/MyFlutterPlugin.kt @@ -0,0 +1,19 @@ +package com.example.fmassive + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodChannel + +class MyFlutterPlugin : FlutterPlugin { + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + val context = binding.applicationContext + val handler = AlarmModule(context) + channel = MethodChannel(binding.binaryMessenger, "my_channel") + channel.setMethodCallHandler(handler) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_baseline_hourglass_bottom_24.xml b/android/app/src/main/res/drawable/ic_baseline_hourglass_bottom_24.xml new file mode 100644 index 0000000..a8b409b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_hourglass_bottom_24.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_baseline_stop_24.xml b/android/app/src/main/res/drawable/ic_baseline_stop_24.xml new file mode 100644 index 0000000..a8b409b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_stop_24.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 1387898..324f140 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; void main() { runApp(const MyApp()); @@ -48,17 +51,10 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - int _counter = 0; + static const platform = MethodChannel('com.massive/android'); - void _incrementCounter() async { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); + Future _timer() async { + await platform.invokeMethod('timer', [3000]); } @override @@ -99,14 +95,14 @@ class _MyHomePageState extends State { 'You have pushed the button this many times:', ), Text( - '$_counter', + 'Tap to start timer', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, + onPressed: _timer, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. diff --git a/pubspec.lock b/pubspec.lock index aaba50b..8ba7880 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -62,14 +62,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_foreground_plugin: - dependency: "direct main" - description: - name: flutter_foreground_plugin - sha256: "640ce980401eece566441b867400e137357d2331a564f1670d6d5c46fa12d8d4" - url: "https://pub.dev" - source: hosted - version: "0.8.0" flutter_lints: dependency: "direct dev" description: @@ -194,4 +186,3 @@ packages: version: "2.1.4" sdks: dart: ">=2.19.5 <3.0.0" - flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 604f0df..9e6ee84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - flutter_foreground_plugin: ^0.8.0 dev_dependencies: flutter_test: