Skip to content

Commit

Permalink
basic timer notification with stop button
Browse files Browse the repository at this point in the history
  • Loading branch information
qrhfz committed Feb 1, 2024
1 parent 84781e7 commit 185f631
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 6 deletions.
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<service
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:name=".TimerService"
android:exported="false"
android:stopWithTask="true"
/>
Expand Down
68 changes: 67 additions & 1 deletion android/app/src/main/kotlin/dev/qori/interval/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,72 @@
package dev.qori.interval

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.Build
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
class MainActivity : FlutterActivity() {

companion object {
const val FLUTTER_CHANNEL_ID = "interval.qori.dev/notification"
}

private var onTimerDismissed:((Unit)->Unit)? = null

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create the NotificationChannel.
val name = "timer notification"
val descriptionText = "to show the remaining time when doing a task"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val mChannel = NotificationChannel(TimerService.NOTIFICATION_CHANNEL_ID, name, importance)
mChannel.description = descriptionText
// Register the channel with the system. You can't change the importance
// or other notification behaviors after this.
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(mChannel)
}


val channel = MethodChannel(flutterEngine .dartExecutor.binaryMessenger, FLUTTER_CHANNEL_ID)

channel.setMethodCallHandler { call, result ->
when (call.method) {
"showTimer" -> handleShowTimer(call)
"dismissTimer" -> handleDismissTimer(call)
else -> result.notImplemented()
}
}

val _onTimerDismissed = {_:Unit->
channel.invokeMethod("onTimerDismissed", null)
}
TimerService.timerDismissedObservable.addSubscriber(_onTimerDismissed)
onTimerDismissed = _onTimerDismissed
}

override fun onDestroy() {
super.onDestroy()
val cb = onTimerDismissed
if (cb!==null){
TimerService.timerDismissedObservable.removeSubscriber(cb)
}
}

private fun handleShowTimer(call: MethodCall) {
val taskName = call.argument<String>("taskName")!!
val formattedDuration = call.argument<String>("formattedDuration")!!

TimerService.showTimer(this, taskName, formattedDuration)
}

private fun handleDismissTimer(call: MethodCall) {
TimerService.dismissTimer(this)
}
}
19 changes: 19 additions & 0 deletions android/app/src/main/kotlin/dev/qori/interval/Observable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.qori.interval

class Observable<T> {
private val subscribers = mutableListOf<(T)->Unit>()

fun addSubscriber(subscriber:(T)->Unit){
subscribers.add(subscriber)
}

fun removeSubscriber(subscriber:(T)->Unit){
subscribers.remove(subscriber)
}

fun notifySubscribers(data:T){
subscribers.forEach { sub->
sub(data)
}
}
}
92 changes: 92 additions & 0 deletions android/app/src/main/kotlin/dev/qori/interval/TimerService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package dev.qori.interval

import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat

class TimerService : Service() {
companion object {
const val NOTIFICATION_CHANNEL_ID = "TimerServiceNotificationChannelID"
const val ACTION_STOP = "ACTION_STOP"
const val ACTION_SHOW = "ACTION_SHOW"
val timerDismissedObservable = Observable<Unit>()

fun showTimer(ctx: Context, taskName: String, formattedTime: String) {
val i = Intent(ctx, TimerService::class.java)
i.action = ACTION_SHOW
i.putExtra("taskName", taskName)
i.putExtra("formattedTime", formattedTime)
start(ctx, i)
}

fun dismissTimer(ctx: Context) {
val i = Intent(ctx, TimerService::class.java)
i.action = ACTION_STOP
start(ctx, i)
}

private fun start(ctx: Context, i: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ctx.startForegroundService(i)
}
ctx.startService(i)
}
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent !== null) {
when (intent.action) {
ACTION_SHOW -> {
val taskName = intent.getStringExtra("taskName")!!
val formattedTime = intent.getStringExtra("formattedTime")!!
startForeground(1, makeNotif(taskName, formattedTime))
}

ACTION_STOP -> {
timerDismissedObservable.notifySubscribers(Unit)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
}

else -> Unit

}
}
return START_NOT_STICKY
}


private fun makeNotif(taskName: String, formattedTime: String): Notification {

val notifBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)

return notifBuilder
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(taskName)
.setContentText(formattedTime)
.setOnlyAlertOnce(true)
.addAction(makeStopAction())
.build()
}

private fun makeStopAction(): NotificationCompat.Action {
val stopIntent = Intent(this, this::class.java)
stopIntent.action = ACTION_STOP
val stopPendingIntent = PendingIntent
.getService(this, 1, stopIntent, PendingIntent.FLAG_IMMUTABLE)

return NotificationCompat
.Action(R.drawable.baseline_stop_24, "Stop", stopPendingIntent)
}

override fun onBind(intent: Intent?): IBinder? = null
}
5 changes: 5 additions & 0 deletions android/app/src/main/res/drawable/baseline_stop_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,6h12v12H6z"/>
</vector>
23 changes: 19 additions & 4 deletions lib/app/interval/interval_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:interval/app/interval/cubit/interval_cubit.dart';
import 'package:interval/app/interval/cubit/timer_cubit.dart';
import 'package:interval/app/notification_manager.dart';
import 'package:interval/audio.dart';
import 'package:interval/domain/entitites/task.dart';
import 'package:interval/utils/duration_extension.dart';
Expand All @@ -24,6 +25,9 @@ class IntervalRoute extends StatefulWidget {

class _IntervalRouteState extends State<IntervalRoute> with RouteAware {
bool mute = false;
late final notifManager = NotificationManager(() {
stop();
});

@override
void initState() {
Expand All @@ -37,6 +41,11 @@ class _IntervalRouteState extends State<IntervalRoute> with RouteAware {
});
}

void stop() {
context.read<TimerCubit>().quit();
context.read<IntervalCubit>().stop();
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
Expand All @@ -52,14 +61,20 @@ class _IntervalRouteState extends State<IntervalRoute> with RouteAware {

@override
void didPop() {
context.read<TimerCubit>().quit();
context.read<IntervalCubit>().stop();
stop();
super.didPop();
}

Future<void> startNotification(Task currentTask, Duration timeleft) async {}
Future<void> startNotification(Task currentTask, Duration timeleft) async {
notifManager.showTimer(
currentTask.name,
timeleft.toHHMMSS(),
);
}

Future<void> stopNotification() async {}
Future<void> stopNotification() async {
notifManager.dismissTimer();
}

@override
Widget build(BuildContext context) {
Expand Down
30 changes: 30 additions & 0 deletions lib/app/notification_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:flutter/services.dart';

class NotificationManager {
final void Function() onTimerDissmissed;

NotificationManager(this.onTimerDissmissed) {
channel.setMethodCallHandler((call) async {
switch (call.method) {
case "onTimerDismissed":
onTimerDissmissed();
break;
}
});
}

final channel = const MethodChannel('interval.qori.dev/notification');

void showTimer(String taskName, String formattedDuration) {
channel.invokeMethod("showTimer", {
"taskName": taskName,
"formattedDuration": formattedDuration,
});
}

void dismissTimer() {
channel.invokeMethod("dismissTimer");
}

void setOnTimerDismissed(void Function() onTimerDissmissed) {}
}

0 comments on commit 185f631

Please sign in to comment.