Skip to content

Commit

Permalink
Implement new feature to add daily reminders.
Browse files Browse the repository at this point in the history
  • Loading branch information
SankethBK authored Oct 19, 2023
1 parent 35230a4 commit 2127d75
Show file tree
Hide file tree
Showing 14 changed files with 452 additions and 26 deletions.
6 changes: 5 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ if (keystorePropertiesFile.exists()) {


android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33

compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
Expand Down Expand Up @@ -83,4 +84,7 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation platform('com.google.firebase:firebase-bom:29.2.1')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'
}
20 changes: 17 additions & 3 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.dairy_app">
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
<application
android:label="Diary Vault"
Expand Down Expand Up @@ -54,5 +56,17 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />

<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<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>
</manifest>
Binary file added android/app/src/main/res/drawable/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions lib/core/dependency_injection/injection_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import 'package:dairy_app/features/auth/presentation/bloc/user_config/user_confi
import 'package:dairy_app/features/notes/data/datasources/local%20data%20sources/local_data_source.dart';
import 'package:dairy_app/features/notes/data/datasources/local%20data%20sources/local_data_source_template.dart';
import 'package:dairy_app/features/notes/data/repositories/notes_repository.dart';
import 'package:dairy_app/features/notes/data/repositories/notifications_repository.dart';
import 'package:dairy_app/features/notes/domain/repositories/notes_repository.dart';
import 'package:dairy_app/features/notes/domain/repositories/notifications_repository.dart';
import 'package:dairy_app/features/notes/presentation/bloc/notes/notes_bloc.dart';
import 'package:dairy_app/features/notes/presentation/bloc/notes_fetch/notes_fetch_cubit.dart';
import 'package:dairy_app/features/notes/presentation/bloc/selectable_list/selectable_list_cubit.dart';
Expand All @@ -29,6 +31,7 @@ import 'package:dairy_app/features/sync/data/datasources/temeplates/key_value_da
import 'package:dairy_app/features/sync/data/repositories/sync_repository.dart';
import 'package:dairy_app/features/sync/domain/repositories/sync_repository_template.dart';
import 'package:dairy_app/features/sync/presentation/bloc/notes_sync/notesync_cubit.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get_it/get_it.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';

Expand Down Expand Up @@ -119,6 +122,21 @@ Future<void> init() async {
sl.registerSingleton<INotesRepository>(
NotesRepository(notesLocalDataSource: sl(), authSessionBloc: sl()));

sl.registerSingletonAsync<INotificationsRepository>(() async {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');

const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
);

await flutterLocalNotificationsPlugin.initialize(initializationSettings);
return NotificationsRepository(
flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin);
});

//* Blocs
sl.registerLazySingleton(() => NotesBloc(notesRepository: sl()));
sl.registerLazySingleton(() => NotesFetchCubit(
Expand Down
3 changes: 3 additions & 0 deletions lib/core/pages/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:dairy_app/core/widgets/version_number.dart';
import 'package:dairy_app/features/auth/presentation/bloc/auth_session/auth_session_bloc.dart';
import 'package:dairy_app/features/auth/presentation/widgets/security_settings.dart';
import 'package:dairy_app/features/auth/presentation/widgets/setup_account.dart';
import 'package:dairy_app/features/notes/presentation/widgets/daily_reminders.dart';
import 'package:dairy_app/features/sync/presentation/widgets/sync_settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand Down Expand Up @@ -127,6 +128,8 @@ class _SettingsPageState extends State<SettingsPage> {
const SizedBox(height: 15),
const AutoSaveToggleButton(),
const SizedBox(height: 10),
const DailyReminders(),
const SizedBox(height: 10),
const ThemeDropdown(),
const SizedBox(height: 15),
const SendFeedBack(),
Expand Down
2 changes: 2 additions & 0 deletions lib/features/auth/core/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class UserConfigConstants {
static String isAutoSyncEnabled = "is_auto_sync_enabled";
static String isFingerPrintLoginEnabled = "is_finger_print_log_enabled";
static String isAutoSaveEnabled = "is_auto_save_enabled";
static String isDailyReminderEnabled = "is_daily_reminder_enabled";
static String reminderTime = "reminder_time";
}

class SyncConstants {
Expand Down
67 changes: 55 additions & 12 deletions lib/features/auth/data/models/user_config_model.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:dairy_app/features/auth/core/constants.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';

/// class to store non-critical properties of user
/// it is stored apart from user table, which stores critical properties of user
Expand All @@ -13,17 +14,22 @@ class UserConfigModel extends Equatable {
final bool? isAutoSyncEnabled;
final bool? isFingerPrintLoginEnabled;
final bool? isAutoSaveEnabled;
final bool? isDailyReminderEnabled;
final TimeOfDay? reminderTime;

const UserConfigModel(
{required this.userId,
this.preferredSyncOption,
this.lastGoogleDriveSync,
this.lastDropboxSync,
this.googleDriveUserInfo,
this.dropBoxUserInfo,
this.isAutoSyncEnabled,
this.isFingerPrintLoginEnabled,
this.isAutoSaveEnabled});
const UserConfigModel({
required this.userId,
this.preferredSyncOption,
this.lastGoogleDriveSync,
this.lastDropboxSync,
this.googleDriveUserInfo,
this.dropBoxUserInfo,
this.isAutoSyncEnabled,
this.isFingerPrintLoginEnabled,
this.isAutoSaveEnabled,
this.isDailyReminderEnabled,
this.reminderTime,
});

@override
List<Object?> get props => [
Expand All @@ -35,9 +41,40 @@ class UserConfigModel extends Equatable {
dropBoxUserInfo,
isAutoSyncEnabled,
isFingerPrintLoginEnabled,
isAutoSaveEnabled
isAutoSaveEnabled,
isDailyReminderEnabled,
reminderTime
];

static TimeOfDay? getTimeOfDayFromTimeString(String? timeString) {
if (timeString != null) {
final parts = timeString.split(':');

if (parts.length == 2) {
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
return TimeOfDay(hour: hour, minute: minute);
}

return null;
}
return null;
}

static String? getTimeOfDayToString(TimeOfDay? time) {
if (time == null) {
return null;
}

final hour = time.hour.toString();
final minute = time.minute.toString();
final formattedHour = hour.length == 1 ? hour.padLeft(2, '0') : hour;
final formattedMinute =
minute.length == 1 ? minute.padLeft(2, '0') : minute;

return '$formattedHour:$formattedMinute';
}

factory UserConfigModel.fromJson(Map<String, dynamic> jsonMap) {
return UserConfigModel(
userId: jsonMap[UserConfigConstants.userId],
Expand All @@ -57,7 +94,11 @@ class UserConfigModel extends Equatable {
isFingerPrintLoginEnabled:
jsonMap[UserConfigConstants.isFingerPrintLoginEnabled],
isAutoSaveEnabled: jsonMap[UserConfigConstants.isAutoSaveEnabled],
); // default theme is coral bubbles
isDailyReminderEnabled:
jsonMap[UserConfigConstants.isDailyReminderEnabled],
reminderTime:
getTimeOfDayFromTimeString(jsonMap[UserConfigConstants.reminderTime]),
);
}

Map<String, dynamic> toJson() {
Expand All @@ -73,6 +114,8 @@ class UserConfigModel extends Equatable {
UserConfigConstants.isAutoSyncEnabled: isAutoSyncEnabled,
UserConfigConstants.isFingerPrintLoginEnabled: isFingerPrintLoginEnabled,
UserConfigConstants.isAutoSaveEnabled: isAutoSaveEnabled,
UserConfigConstants.isDailyReminderEnabled: isDailyReminderEnabled,
UserConfigConstants.reminderTime: getTimeOfDayToString(reminderTime)
};
}
}
102 changes: 102 additions & 0 deletions lib/features/notes/data/repositories/notifications_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:dairy_app/core/logger/logger.dart';
import 'package:dairy_app/features/notes/domain/repositories/notifications_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

final log = printer("NotificationsRepository");

class NotificationsRepository implements INotificationsRepository {
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;

NotificationsRepository({required this.flutterLocalNotificationsPlugin});

tz.TZDateTime nextInstanceOfTime(TimeOfDay time, tz.Location localTimeZOne) {
final tz.TZDateTime now = tz.TZDateTime.now(localTimeZOne);

tz.TZDateTime scheduledDate = tz.TZDateTime(
tz.local, now.year, now.month, now.day, time.hour, time.minute);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}

log.i("Scheduling alarm at $scheduledDate");
return scheduledDate;
}

@override
Future<void> zonedScheduleNotification(TimeOfDay time) async {
try {
final permissionsEnabled = await areNotificationsEnabled();

log.w("permissions enabled = $permissionsEnabled");

if (!permissionsEnabled) {
// We can request permission from within the App for >= Android 13
final arePermissionsGranted = await requestPermission();
if (!arePermissionsGranted) {
throw Exception("Notification permissions are not enabled");
}
}

// inititalize time zones
tz.initializeTimeZones();
final String? timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName!));

log.i("Local timezone = ${tz.local}");

// cancel all previously scheduled notifications before scheduling new ones
cancelAllNotifications();

await flutterLocalNotificationsPlugin.zonedSchedule(
0,
'Time to Journal!',
'Take a few minutes to reflect on your day in your diary',
nextInstanceOfTime(time, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails(
'daily_reminder',
'Daily reminders',
importance: Importance.high,
channelDescription: 'Daily Reminder Notifications',
),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime);
} catch (e) {
log.e(e);
rethrow;
}
}

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

Future<bool> requestPermission() async {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();

final bool grantedNotificationPermission =
await androidImplementation?.requestNotificationsPermission() ?? false;

return grantedNotificationPermission;
}

@override
Future<void> cancelAllNotifications() async {
log.i("Cancelling all scheduled notifications");
await flutterLocalNotificationsPlugin.cancelAll();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';

abstract class INotificationsRepository {
/// Schedules daily notification at [time]
Future<void> zonedScheduleNotification(TimeOfDay time);

/// Cancels/removes all notifications that have been scheduled and those
/// that have already been presented.
Future<void> cancelAllNotifications();
}
Loading

0 comments on commit 2127d75

Please sign in to comment.