Skip to content

Commit

Permalink
Add due date and implement local notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
TheMedo committed Nov 19, 2024
1 parent 92f4214 commit ec51cba
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 22 deletions.
16 changes: 11 additions & 5 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ plugins {

android {
namespace = "edu.todo.fall20240"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileSdk = 34
ndkVersion = "27.0.12077973"

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
jvmTarget = JavaVersion.VERSION_17
}

defaultConfig {
Expand All @@ -30,6 +31,7 @@ android {
targetSdk = 34
versionCode = flutter.versionCode
versionName = flutter.versionName
multiDexEnabled true
}

buildTypes {
Expand All @@ -44,3 +46,7 @@ android {
flutter {
source = "../.."
}

dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
37 changes: 26 additions & 11 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:label="todo_fall_2024_0"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="todo_fall_2024_0">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />

<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false" />
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
Expand All @@ -38,8 +53,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
6 changes: 3 additions & 3 deletions android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "com.android.application" version "8.7.1" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.gms.google-services" version "4.4.2" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}

include ":app"
5 changes: 4 additions & 1 deletion lib/data/model/todo.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import 'package:cloud_firestore/cloud_firestore.dart';

// TODO: Use https://pub.dev/packages/equatable
class Todo {
final String id;
final DateTime? completedAt;
final String text;
final DateTime? timestamp;
final DateTime? dueDate;
final String userId;

Todo({
required this.id,
required this.completedAt,
required this.text,
required this.timestamp,
required this.dueDate,
required this.userId,
});

Expand All @@ -22,6 +23,7 @@ class Todo {
completedAt: (map['completedAt'] as Timestamp?)?.toDate(),
text: map['text']?.toString() ?? '',
timestamp: (map['timestamp'] as Timestamp?)?.toDate(),
dueDate: (map['dueDate'] as Timestamp?)?.toDate(),
userId: map['userId']?.toString() ?? '',
);
}
Expand All @@ -31,6 +33,7 @@ class Todo {
'completedAt': completedAt == null ? null : Timestamp.fromDate(completedAt!),
'text': text,
'timestamp': timestamp == null ? null : Timestamp.fromDate(timestamp!),
'dueDate': dueDate == null ? null : Timestamp.fromDate(dueDate!),
'userId': userId,
};
}
Expand Down
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:todo_fall_2024_0/firebase_options.dart';
import 'package:todo_fall_2024_0/screen/router/router_screen.dart';
import 'package:timezone/data/latest.dart' as tz;

void main() async {
WidgetsFlutterBinding.ensureInitialized();

await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
tz.initializeTimeZones();

runApp(const MyApp());
}
Expand Down
152 changes: 151 additions & 1 deletion lib/screen/details/details_screen.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import 'dart:developer';

import 'package:app_settings/app_settings.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:todo_fall_2024_0/data/model/todo.dart';
import 'package:todo_fall_2024_0/screen/home/home_screen.dart';

final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

class DetailsScreen extends StatefulWidget {
const DetailsScreen({
required this.todo,
Expand All @@ -21,11 +26,13 @@ class _DetailsScreenState extends State<DetailsScreen> {
TextEditingController? _controller;

String? text;
DateTime? dueDate;

@override
void initState() {
super.initState();
text = widget.todo.text;
dueDate = widget.todo.dueDate;
_controller = TextEditingController(text: text);
}

Expand Down Expand Up @@ -74,6 +81,72 @@ class _DetailsScreenState extends State<DetailsScreen> {
});
},
),
SizedBox(height: 16),
ListTile(
title: Text('Due date'),
subtitle: dueDate == null ? null : Text(dueDate?.toString() ?? ''),
trailing: dueDate == null
? IconButton(
onPressed: () async {
final isGranted = await _requestNotificationPermission();
if (!context.mounted) return;

if (!isGranted) {
_showPermissionDeniedSnackbar(context);
return;
}

await _initializeNotifications();
if (!context.mounted) return;

final pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime(2100),
);
if (!context.mounted) return;

if (pickedDate != null) {
final pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(DateTime.now()),
);

if (pickedTime != null) {
final dueDate = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
pickedTime.hour,
pickedTime.minute,
);
_setDueDate(widget.todo.id, dueDate);
setState(() {
this.dueDate = dueDate;
});

await _scheduleNotification(
widget.todo.id,
dueDate,
widget.todo.text,
);
}
}
},
icon: Icon(Icons.add),
)
: IconButton(
onPressed: () {
_setDueDate(widget.todo.id, null);
setState(() {
dueDate = null;
});
_cancelNotification(widget.todo.id);
},
icon: Icon(Icons.close),
),
),
],
),
),
Expand All @@ -96,6 +169,83 @@ Future<void> _updateTodo(String todoId, String text) async {
'text': text,
});
} catch (e, st) {
log('Error deleting todo', error: e, stackTrace: st);
log('Error updating todo', error: e, stackTrace: st);
}
}

Future<void> _setDueDate(String todoId, DateTime? dueDate) async {
try {
await FirebaseFirestore.instance.collection(collectionTodo).doc(todoId).update({
'dueDate': dueDate,
});
} catch (e, st) {
log('Error setting due date', error: e, stackTrace: st);
}
}

Future<bool> _requestNotificationPermission() async {
final isGranted = await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission() ??
false;
return isGranted;
}

void _showPermissionDeniedSnackbar(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'You need to enable notifications to set due date.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white),
),
backgroundColor: Colors.redAccent,
duration: Duration(seconds: 10),
action: SnackBarAction(
label: 'Open Settings',
textColor: Colors.white,
onPressed: () {
AppSettings.openAppSettings(
type: AppSettingsType.notification,
);
},
),
),
);
}

Future<void> _initializeNotifications() async {
final initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
);
}

Future<void> _scheduleNotification(
String todoId,
DateTime dueDate,
String text,
) async {
final tzDateTime = tz.TZDateTime.from(dueDate, tz.local);
await flutterLocalNotificationsPlugin.zonedSchedule(
todoId.hashCode,
'Task due',
text,
tzDateTime,
const NotificationDetails(
android: AndroidNotificationDetails(
'general_channel',
'General Notifications',
),
),
androidScheduleMode: AndroidScheduleMode.inexact,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.dateAndTime,
);
}

Future<void> _cancelNotification(String todoId) async {
await flutterLocalNotificationsPlugin.cancel(todoId.hashCode);
}
1 change: 1 addition & 0 deletions lib/screen/home/widgets/todos_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class TodosList extends StatelessWidget {
decoration: isCompleted ? TextDecoration.lineThrough : null,
),
),
subtitle: todo.dueDate == null ? null : Text(todo.dueDate.toString()),
trailing: IconButton(
icon: Icon(Icons.chevron_right),
onPressed: () {
Expand Down
Loading

0 comments on commit ec51cba

Please sign in to comment.