Skip to content

Commit

Permalink
We implemented the UI for the feature that allows users to select a d…
Browse files Browse the repository at this point in the history
…ate and time for the reminder notification of a note (see dialog_fragment_datetime_picker.xml, spinner_date_dropwdown_item.xml, spinner_date_item.xml, spinner_time_dropdown_item.xml and, spinner_time_item.xml). We also implemented the notifications and the broadcast receiver for the reminders (see the whole reminders package).
  • Loading branch information
XamDR committed Jul 27, 2022
1 parent f19df3e commit 6c8d4ea
Show file tree
Hide file tree
Showing 23 changed files with 611 additions and 13 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.4.2"
implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.0"
implementation "androidx.navigation:navigation-fragment-ktx:2.5.0"
Expand Down
8 changes: 4 additions & 4 deletions app/schemas/net.azurewebsites.noties.data.AppDatabase/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "0c9aa7ab303699fc74fffe705d1ca091",
"identityHash": "115c670b9b92fd3c502b24671880b356",
"entities": [
{
"tableName": "Notebooks",
Expand Down Expand Up @@ -92,7 +92,7 @@
},
{
"tableName": "Notes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `modification_date` TEXT NOT NULL, `color` INTEGER, `urls` TEXT NOT NULL, `is_protected` INTEGER NOT NULL, `is_trashed` INTEGER NOT NULL, `is_pinned` INTEGER NOT NULL, `is_todo_list` INTEGER NOT NULL, `reminder_date` TEXT NOT NULL, `notebook_id` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `modification_date` TEXT NOT NULL, `color` INTEGER, `urls` TEXT NOT NULL, `is_protected` INTEGER NOT NULL, `is_trashed` INTEGER NOT NULL, `is_pinned` INTEGER NOT NULL, `is_todo_list` INTEGER NOT NULL, `reminder_date` TEXT, `notebook_id` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
Expand Down Expand Up @@ -158,7 +158,7 @@
"fieldPath": "reminderDate",
"columnName": "reminder_date",
"affinity": "TEXT",
"notNull": true
"notNull": false
},
{
"fieldPath": "notebookId",
Expand All @@ -180,7 +180,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0c9aa7ab303699fc74fffe705d1ca091')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '115c670b9b92fd3c502b24671880b356')"
]
}
}
15 changes: 12 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera" android:required="false" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<application
android:name="net.azurewebsites.noties.App"
Expand Down Expand Up @@ -46,6 +50,11 @@
</intent-filter>
</activity>

<receiver
android:name=".ui.reminders.AlarmReceiver"
android:enabled="true"
android:exported="false"/>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="net.azurewebsites.noties"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data class NoteEntity(
@ColumnInfo(name = "is_trashed") val isTrashed: Boolean = false,
@ColumnInfo(name = "is_pinned") val isPinned: Boolean = false,
@ColumnInfo(name = "is_todo_list") val isTodoList: Boolean = false,
@ColumnInfo(name = "reminder_date") val reminderDate: ZonedDateTime = ZonedDateTime.now(),
@ColumnInfo(name = "reminder_date") val reminderDate: ZonedDateTime? = null,
@ColumnInfo(name = "notebook_id") val notebookId: Int = 0) : Parcelable {

fun getUrlCount() = urls.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import java.time.format.DateTimeFormatter
class ZonedDateTimeToStringConverter {

@TypeConverter
fun fromZonedDateTime(dateTime: ZonedDateTime): String {
fun fromZonedDateTime(dateTime: ZonedDateTime?): String? {
val formatter = DateTimeFormatter.ofPattern(pattern).withZone(ZoneId.systemDefault())
return formatter.format(dateTime)
return if (dateTime != null) formatter.format(dateTime) else null
}

@TypeConverter
fun toZonedDateTime(value: String): ZonedDateTime {
fun toZonedDateTime(value: String?): ZonedDateTime? {
val formatter = DateTimeFormatter.ofPattern(pattern).withZone(ZoneId.systemDefault())
return ZonedDateTime.parse(value, formatter)
return if (value != null) ZonedDateTime.parse(value, formatter) else null
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,8 @@ class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedList
}

private var onFabClickCallback: () -> Unit = {}

companion object {
const val CHANNEL_ID = "NOTIES_CHANNEL"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import net.azurewebsites.noties.ui.editor.todos.TodoItemAdapter
import net.azurewebsites.noties.ui.helpers.*
import net.azurewebsites.noties.ui.image.*
import net.azurewebsites.noties.ui.notes.NotesFragment
import net.azurewebsites.noties.ui.reminders.DateTimePickerDialogFragment
import net.azurewebsites.noties.ui.urls.JsoupHelper
import java.io.FileNotFoundException
import java.time.ZonedDateTime
Expand Down Expand Up @@ -243,6 +244,7 @@ class EditorFragment : Fragment(), AttachImagesListener, LinkClickedListener,
setOnActivityResultListener { pickImagesLauncher.launch(arrayOf(MIME_TYPE_IMAGE)) }
setOnTakePictureListener { takePictureOrRequestPermission() }
setOnMakeTodoListListener { makeTodoList() }
setOnShowDateTimePickerListener { showDateTimePickerDialog() }
}
showDialog(menuDialog, MENU_DIALOG_TAG)
}
Expand Down Expand Up @@ -434,6 +436,11 @@ class EditorFragment : Fragment(), AttachImagesListener, LinkClickedListener,
}
}

private fun showDateTimePickerDialog() {
val dateTimePickerDialog = DateTimePickerDialogFragment()
showDialog(dateTimePickerDialog, DATE_TIME_PICKER_DIALOG_TAG)
}

private fun addChildHeadlessFragments() {
if (childFragmentManager.findFragmentByTag(ADD_IMAGES_TAG) == null &&
childFragmentManager.findFragmentByTag(SHARE_CONTENT_TAG) == null) {
Expand Down Expand Up @@ -476,6 +483,7 @@ class EditorFragment : Fragment(), AttachImagesListener, LinkClickedListener,
const val MIME_TYPE_TEXT = "text/plain"
private const val MENU_DIALOG_TAG = "MENU_DIALOG"
private const val COLOR_DIALOG_TAG = "COLOR_DIALOG"
private const val DATE_TIME_PICKER_DIALOG_TAG = "DATE_TIME_PICKER"
private const val ALT_TEXT_DIALOG_TAG = "ALT_TEXT_DIALOG"
private const val DELETE_IMAGES_DIALOG_TAG = "DELETE_IMAGES"
private const val ADD_IMAGES_TAG = "ADD_IMAGES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class EditorMenuFragment : BottomSheetDialogFragment() {
dismiss()
}

fun invokeShowDateTimePickerCallback() {
onShowDateTimePickerCallback()
dismiss()
}

fun setOnActivityResultListener(callback: () -> Unit) {
onActivityResultCallback = callback
}
Expand All @@ -56,9 +61,15 @@ class EditorMenuFragment : BottomSheetDialogFragment() {
onMakeTodoListCallback = callback
}

fun setOnShowDateTimePickerListener(callback: () -> Unit) {
onShowDateTimePickerCallback = callback
}

private var onActivityResultCallback: () -> Unit = {}

private var onTakePictureCallback: () -> Unit = {}

private var onMakeTodoListCallback: () -> Unit = {}

private var onShowDateTimePickerCallback: () -> Unit = {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package net.azurewebsites.noties.ui.reminders

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.RingtoneManager
import android.os.Build
import androidx.core.content.getSystemService
import net.azurewebsites.noties.ui.MainActivity

class AlarmReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent?) {
if (intent != null) {
notify(context, intent)
}
}

private fun notify(context: Context, intent: Intent) {
val notificationManager = context.getSystemService<NotificationManager>() ?: return
val notification = intent.getParcelableExtra<Notification>(NOTIFICATION)
createNotificationChannel(notificationManager)
val id = intent.getIntExtra(NOTIFICATION_ID, 0)
notificationManager.notify(id, notification)
}

private fun createNotificationChannel(notificationManager: NotificationManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = CHANNEL_NAME
val descriptionText = CHANNEL_DESCRIPTION
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(MainActivity.CHANNEL_ID, name, importance).apply {
description = descriptionText
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
setSound(
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
AudioAttributes.Builder().build()
)
enableVibration(true)
enableLights(true)
}
notificationManager.createNotificationChannel(channel)
}
}

companion object {
const val NOTIFICATION = "NOTIFICATION"
const val NOTIFICATION_ID = "NOTIFICATION_ID"
private const val CHANNEL_NAME = "NOTIES_CHANNEL"
private const val CHANNEL_DESCRIPTION = "NOTIES"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package net.azurewebsites.noties.ui.reminders

import android.app.*
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.format.DateFormat
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.core.app.AlarmManagerCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import net.azurewebsites.noties.R
import net.azurewebsites.noties.databinding.DialogFragmentDatetimePickerBinding
import net.azurewebsites.noties.ui.editor.EditorViewModel
import net.azurewebsites.noties.ui.helpers.printDebug
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime

class DateTimePickerDialogFragment : DialogFragment(), DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener {

private var _binding: DialogFragmentDatetimePickerBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModels<EditorViewModel>({ requireParentFragment() })
private lateinit var helper: DateTimePickerHelper
private lateinit var dateAdapter: ReminderDateAdapter
private lateinit var timeAdapter: ReminderTimeAdapter
private var selectedDate: LocalDate? = null
private var selectedTime: LocalTime? = null

override fun onAttach(context: Context) {
super.onAttach(context)
helper = DateTimePickerHelper(context)
dateAdapter = ReminderDateAdapter(context, R.layout.spinner_date_item, helper.dates)
timeAdapter = ReminderTimeAdapter(context, R.layout.spinner_time_item, helper.times)
selectedDate = helper.dates[0].value
selectedTime = helper.times[0].value
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogFragmentDatetimePickerBinding.inflate(layoutInflater).apply {
spinnerDate.adapter = dateAdapter
spinnerTime.adapter = timeAdapter
}
onItemClick()
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.add_reminder)
.setView(binding.root)
.setNegativeButton(R.string.cancel_button, null)
.setPositiveButton(R.string.ok_button) { _, _ -> scheduleNotification() }
.create()
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {

}

override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) {

}

private fun scheduleNotification() {
if (selectedDate != null && selectedTime != null) {
val selectedDateTime = ZonedDateTime.of(selectedDate, selectedTime, ZoneId.systemDefault())
printDebug(TAG, selectedDateTime)
viewModel.updateNote(viewModel.entity.copy(reminderDate = selectedDateTime))
val delay = selectedDateTime.toInstant().toEpochMilli()
val notification = NotificationHelper.buildNotification(requireContext(), delay)
setAlarmManager(requireContext(), notification, delay)
}
}

private fun setAlarmManager(context: Context, notification: Notification, delay: Long) {
val intent = Intent(context, AlarmReceiver::class.java).apply {
putExtra(AlarmReceiver.NOTIFICATION_ID, 1)
putExtra(AlarmReceiver.NOTIFICATION, notification)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
NotificationHelper.REQUEST_CODE,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val alarmManager = context.getSystemService<AlarmManager>() ?: return
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, delay, pendingIntent)
}

private fun onItemClick() {
binding.spinnerDate.apply {
(adapter as ReminderDateAdapter).setOnItemClickListener { _, _, position, _ ->
hideDropDown()
if (position == helper.dates.size - 1) {
showDatePickerDialog()
}
else {
binding.spinnerDate.setSelection(position)
selectedDate = helper.dates[position].value
}
}
}
binding.spinnerTime.apply {
(adapter as ReminderTimeAdapter).setOnItemClickListener { _, _, position, _ ->
hideDropDown()
if (position == helper.times.size - 1) {
showTimePickerDialog()
}
else {
binding.spinnerTime.setSelection(position)
selectedTime = helper.times[position].value
}
}
}
}

private fun showDatePickerDialog() {
val reminderDate = viewModel.entity.reminderDate ?: ZonedDateTime.now()
DatePickerDialog(
requireContext(),
this,
reminderDate.year,
reminderDate.monthValue - 1,
reminderDate.dayOfMonth
).show()
}

private fun showTimePickerDialog() {
val reminderDate = viewModel.entity.reminderDate ?: ZonedDateTime.now()
TimePickerDialog(
requireContext(),
this,
reminderDate.hour,
reminderDate.minute,
DateFormat.is24HourFormat(requireContext())
).show()
}

private companion object {
private const val TAG = "DATE_TIME"
}
}
Loading

0 comments on commit 6c8d4ea

Please sign in to comment.