Showing preview only (1,055K chars total). Download the full file or copy to clipboard to get everything.
Repository: icabetong/fokus-android
Branch: main
Commit: fb57365edc3f
Files: 335
Total size: 937.2 KB
Directory structure:
gitextract_ht_tjfbb/
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ ├── schemas/
│ │ └── com.isaiahvonrundstedt.fokus.database.AppDatabase/
│ │ └── 8.json
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── isaiahvonrundstedt/
│ │ └── fokus/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── isaiahvonrundstedt/
│ │ │ └── fokus/
│ │ │ ├── Fokus.kt
│ │ │ ├── components/
│ │ │ │ ├── custom/
│ │ │ │ │ ├── ItemDecoration.kt
│ │ │ │ │ └── ItemSwipeCallback.kt
│ │ │ │ ├── enums/
│ │ │ │ │ └── SortDirection.kt
│ │ │ │ ├── extensions/
│ │ │ │ │ ├── android/
│ │ │ │ │ │ ├── AppCompatExtensions.kt
│ │ │ │ │ │ ├── IntentExtensions.kt
│ │ │ │ │ │ ├── TextViewExtensions.kt
│ │ │ │ │ │ └── UriExtensions.kt
│ │ │ │ │ └── jdk/
│ │ │ │ │ ├── CalendarExtensions.kt
│ │ │ │ │ ├── ListExtensions.kt
│ │ │ │ │ └── TimeExtensions.kt
│ │ │ │ ├── interfaces/
│ │ │ │ │ ├── Streamable.kt
│ │ │ │ │ └── Swipeable.kt
│ │ │ │ ├── json/
│ │ │ │ │ ├── JsonDataStreamer.kt
│ │ │ │ │ └── Metadata.kt
│ │ │ │ ├── modules/
│ │ │ │ │ ├── DatabaseModule.kt
│ │ │ │ │ ├── ExternalModule.kt
│ │ │ │ │ └── InternalModule.kt
│ │ │ │ ├── preference/
│ │ │ │ │ └── InformationHolder.kt
│ │ │ │ ├── receiver/
│ │ │ │ │ └── LocalizationReceiver.kt
│ │ │ │ ├── service/
│ │ │ │ │ ├── BackupRestoreService.kt
│ │ │ │ │ ├── DataExporterService.kt
│ │ │ │ │ ├── DataImporterService.kt
│ │ │ │ │ ├── FileImporterService.kt
│ │ │ │ │ └── NotificationActionService.kt
│ │ │ │ ├── utils/
│ │ │ │ │ ├── DataArchiver.kt
│ │ │ │ │ ├── NotificationChannelManager.kt
│ │ │ │ │ ├── PermissionManager.kt
│ │ │ │ │ └── PreferenceManager.kt
│ │ │ │ └── views/
│ │ │ │ ├── RadioButtonCompat.kt
│ │ │ │ ├── ReactiveTextColorSwitch.kt
│ │ │ │ └── TwoLineRadioButton.kt
│ │ │ ├── database/
│ │ │ │ ├── AppDatabase.kt
│ │ │ │ ├── converter/
│ │ │ │ │ ├── ColorConverter.kt
│ │ │ │ │ └── DateTimeConverter.kt
│ │ │ │ ├── dao/
│ │ │ │ │ ├── AttachmentDAO.kt
│ │ │ │ │ ├── EventDAO.kt
│ │ │ │ │ ├── LogDAO.kt
│ │ │ │ │ ├── ScheduleDAO.kt
│ │ │ │ │ ├── SubjectDAO.kt
│ │ │ │ │ └── TaskDAO.kt
│ │ │ │ └── repository/
│ │ │ │ ├── EventRepository.kt
│ │ │ │ ├── LogRepository.kt
│ │ │ │ ├── SubjectRepository.kt
│ │ │ │ └── TaskRepository.kt
│ │ │ └── features/
│ │ │ ├── about/
│ │ │ │ ├── AboutFragment.kt
│ │ │ │ ├── LibrariesFragment.kt
│ │ │ │ └── NoticesFragment.kt
│ │ │ ├── attachments/
│ │ │ │ ├── Attachment.kt
│ │ │ │ ├── AttachmentOptionSheet.kt
│ │ │ │ └── attach/
│ │ │ │ ├── AttachToTaskActivity.kt
│ │ │ │ ├── AttachToTaskAdapter.kt
│ │ │ │ └── AttachToTaskViewModel.kt
│ │ │ ├── core/
│ │ │ │ ├── activities/
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ ├── fragment/
│ │ │ │ │ └── RootFragment.kt
│ │ │ │ └── worker/
│ │ │ │ └── ActionWorker.kt
│ │ │ ├── event/
│ │ │ │ ├── Event.kt
│ │ │ │ ├── EventAdapter.kt
│ │ │ │ ├── EventFragment.kt
│ │ │ │ ├── EventPackage.kt
│ │ │ │ ├── EventViewModel.kt
│ │ │ │ ├── archived/
│ │ │ │ │ ├── ArchivedEventAdapter.kt
│ │ │ │ │ ├── ArchivedEventFragment.kt
│ │ │ │ │ └── ArchivedEventViewModel.kt
│ │ │ │ ├── editor/
│ │ │ │ │ ├── EventEditorContainer.kt
│ │ │ │ │ ├── EventEditorFragment.kt
│ │ │ │ │ └── EventEditorViewModel.kt
│ │ │ │ └── widget/
│ │ │ │ ├── EventWidgetProvider.kt
│ │ │ │ ├── EventWidgetRemoteViewFactory.kt
│ │ │ │ └── EventWidgetService.kt
│ │ │ ├── log/
│ │ │ │ ├── Log.kt
│ │ │ │ ├── LogAdapter.kt
│ │ │ │ ├── LogFragment.kt
│ │ │ │ └── LogViewModel.kt
│ │ │ ├── notifications/
│ │ │ │ ├── NotificationWorker.kt
│ │ │ │ ├── event/
│ │ │ │ │ ├── EventNotificationScheduler.kt
│ │ │ │ │ └── EventNotificationWorker.kt
│ │ │ │ ├── subject/
│ │ │ │ │ ├── ClassNotificationScheduler.kt
│ │ │ │ │ └── ClassNotificationWorker.kt
│ │ │ │ └── task/
│ │ │ │ ├── TaskNotificationScheduler.kt
│ │ │ │ ├── TaskNotificationWorker.kt
│ │ │ │ └── TaskReminderWorker.kt
│ │ │ ├── schedule/
│ │ │ │ ├── Schedule.kt
│ │ │ │ ├── ScheduleEditor.kt
│ │ │ │ ├── picker/
│ │ │ │ │ ├── SchedulePickerAdapter.kt
│ │ │ │ │ └── SchedulePickerSheet.kt
│ │ │ │ └── viewer/
│ │ │ │ ├── ScheduleViewerAdapter.kt
│ │ │ │ └── ScheduleViewerSheet.kt
│ │ │ ├── settings/
│ │ │ │ ├── BackupFragment.kt
│ │ │ │ └── SettingsFragment.kt
│ │ │ ├── shared/
│ │ │ │ ├── abstracts/
│ │ │ │ │ ├── BaseActivity.kt
│ │ │ │ │ ├── BaseAdapter.kt
│ │ │ │ │ ├── BaseBasicAdapter.kt
│ │ │ │ │ ├── BaseBottomSheet.kt
│ │ │ │ │ ├── BaseEditorFragment.kt
│ │ │ │ │ ├── BaseFragment.kt
│ │ │ │ │ ├── BasePickerFragment.kt
│ │ │ │ │ ├── BasePreference.kt
│ │ │ │ │ ├── BaseService.kt
│ │ │ │ │ ├── BaseViewerFragment.kt
│ │ │ │ │ └── BaseWorker.kt
│ │ │ │ └── adapters/
│ │ │ │ └── MenuAdapter.kt
│ │ │ ├── subject/
│ │ │ │ ├── Subject.kt
│ │ │ │ ├── SubjectAdapter.kt
│ │ │ │ ├── SubjectFragment.kt
│ │ │ │ ├── SubjectPackage.kt
│ │ │ │ ├── SubjectViewModel.kt
│ │ │ │ ├── archived/
│ │ │ │ │ ├── ArchivedSubjectAdapter.kt
│ │ │ │ │ ├── ArchivedSubjectFragment.kt
│ │ │ │ │ └── ArchivedSubjectViewModel.kt
│ │ │ │ ├── editor/
│ │ │ │ │ ├── SubjectEditorContainer.kt
│ │ │ │ │ ├── SubjectEditorFragment.kt
│ │ │ │ │ └── SubjectEditorViewModel.kt
│ │ │ │ ├── picker/
│ │ │ │ │ ├── SubjectPickerAdapter.kt
│ │ │ │ │ ├── SubjectPickerFragment.kt
│ │ │ │ │ └── SubjectPickerViewModel.kt
│ │ │ │ └── widget/
│ │ │ │ ├── SubjectWidgetProvider.kt
│ │ │ │ ├── SubjectWidgetRemoteViewFactory.kt
│ │ │ │ └── SubjectWidgetService.kt
│ │ │ ├── task/
│ │ │ │ ├── Task.kt
│ │ │ │ ├── TaskAdapter.kt
│ │ │ │ ├── TaskFragment.kt
│ │ │ │ ├── TaskPackage.kt
│ │ │ │ ├── TaskViewModel.kt
│ │ │ │ ├── archived/
│ │ │ │ │ ├── ArchivedTaskAdapter.kt
│ │ │ │ │ ├── ArchivedTaskFragment.kt
│ │ │ │ │ └── ArchivedTaskViewModel.kt
│ │ │ │ ├── editor/
│ │ │ │ │ ├── TaskEditorContainer.kt
│ │ │ │ │ ├── TaskEditorFragment.kt
│ │ │ │ │ └── TaskEditorViewModel.kt
│ │ │ │ └── widget/
│ │ │ │ ├── TaskWidgetProvider.kt
│ │ │ │ ├── TaskWidgetRemoteViewFactory.kt
│ │ │ │ └── TaskWidgetService.kt
│ │ │ └── viewer/
│ │ │ └── ImageViewer.kt
│ │ └── res/
│ │ ├── anim/
│ │ │ ├── anim_fade_in.xml
│ │ │ ├── anim_slide_down.xml
│ │ │ └── anim_slide_up.xml
│ │ ├── color/
│ │ │ ├── color_text_input_stroke.xml
│ │ │ ├── selector_chip_background.xml
│ │ │ ├── selector_chip_stroke_color.xml
│ │ │ └── selector_chip_text_color.xml
│ │ ├── drawable/
│ │ │ ├── ic_hero_sort_ascending_24.xml
│ │ │ ├── ic_hero_sort_descending_24.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ ├── ic_launcher_monochrome.xml
│ │ │ ├── ic_outline_access_time_24.xml
│ │ │ ├── ic_outline_add_24.xml
│ │ │ ├── ic_outline_archive_24.xml
│ │ │ ├── ic_outline_arrow_back_24.xml
│ │ │ ├── ic_outline_attach_file_24.xml
│ │ │ ├── ic_outline_balance_24.xml
│ │ │ ├── ic_outline_calendar_month_24.xml
│ │ │ ├── ic_outline_celebration_24.xml
│ │ │ ├── ic_outline_check_24.xml
│ │ │ ├── ic_outline_checklist_24.xml
│ │ │ ├── ic_outline_close_24.xml
│ │ │ ├── ic_outline_code_24.xml
│ │ │ ├── ic_outline_color_lens_24.xml
│ │ │ ├── ic_outline_confirmation_number_24.xml
│ │ │ ├── ic_outline_date_range_24.xml
│ │ │ ├── ic_outline_delete_24.xml
│ │ │ ├── ic_outline_edit_note_24.xml
│ │ │ ├── ic_outline_event_24.xml
│ │ │ ├── ic_outline_event_busy_24.xml
│ │ │ ├── ic_outline_event_repeat_24.xml
│ │ │ ├── ic_outline_file_download_24.xml
│ │ │ ├── ic_outline_file_open_24.xml
│ │ │ ├── ic_outline_file_upload_24.xml
│ │ │ ├── ic_outline_filter_alt_24.xml
│ │ │ ├── ic_outline_info_24.xml
│ │ │ ├── ic_outline_lightbulb_24.xml
│ │ │ ├── ic_outline_link_24.xml
│ │ │ ├── ic_outline_location_on_24.xml
│ │ │ ├── ic_outline_menu_24.xml
│ │ │ ├── ic_outline_more_vert_24.xml
│ │ │ ├── ic_outline_music_note_24.xml
│ │ │ ├── ic_outline_notes_24.xml
│ │ │ ├── ic_outline_notifications_active_24.xml
│ │ │ ├── ic_outline_numbers_24.xml
│ │ │ ├── ic_outline_open_in_new_24.xml
│ │ │ ├── ic_outline_person_2_24.xml
│ │ │ ├── ic_outline_priority_high_24.xml
│ │ │ ├── ic_outline_query_stats_24.xml
│ │ │ ├── ic_outline_save_24.xml
│ │ │ ├── ic_outline_science_24.xml
│ │ │ ├── ic_outline_sensor_door_24.xml
│ │ │ ├── ic_outline_settings_24.xml
│ │ │ ├── ic_outline_share_24.xml
│ │ │ ├── ic_outline_translate_24.xml
│ │ │ ├── ic_outline_verified_24.xml
│ │ │ ├── ic_outline_wb_sunny_24.xml
│ │ │ ├── selector_checkbox.xml
│ │ │ ├── shape_bottom_sheet.xml
│ │ │ ├── shape_calendar_current_day.xml
│ │ │ ├── shape_calendar_selected_day.xml
│ │ │ ├── shape_cascade_background.xml
│ │ │ ├── shape_color_holder.xml
│ │ │ ├── shape_color_holder_chip.xml
│ │ │ ├── shape_color_holder_vertical.xml
│ │ │ ├── shape_icon_background.xml
│ │ │ ├── shape_widget_background.xml
│ │ │ ├── shortcut_icon_base_event.xml
│ │ │ ├── shortcut_icon_base_subject.xml
│ │ │ ├── shortcut_icon_base_task.xml
│ │ │ ├── shortcut_icon_event.xml
│ │ │ ├── shortcut_icon_subject.xml
│ │ │ ├── shortcut_icon_task.xml
│ │ │ ├── toggle_checked_to_unchecked.xml
│ │ │ ├── toggle_unchecked_to_checked.xml
│ │ │ ├── vector_checked.xml
│ │ │ └── vector_unchecked.xml
│ │ ├── layout/
│ │ │ ├── activity_attach_to_task.xml
│ │ │ ├── activity_container_event.xml
│ │ │ ├── activity_container_subject.xml
│ │ │ ├── activity_container_task.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── fragment_about.xml
│ │ │ ├── fragment_archived_event.xml
│ │ │ ├── fragment_archived_subject.xml
│ │ │ ├── fragment_archived_task.xml
│ │ │ ├── fragment_backup.xml
│ │ │ ├── fragment_editor_event.xml
│ │ │ ├── fragment_editor_subject.xml
│ │ │ ├── fragment_editor_task.xml
│ │ │ ├── fragment_event.xml
│ │ │ ├── fragment_libraries.xml
│ │ │ ├── fragment_logs.xml
│ │ │ ├── fragment_notices.xml
│ │ │ ├── fragment_picker_subject.xml
│ │ │ ├── fragment_root.xml
│ │ │ ├── fragment_settings.xml
│ │ │ ├── fragment_subject.xml
│ │ │ ├── fragment_task.xml
│ │ │ ├── layout_appbar.xml
│ │ │ ├── layout_appbar_editor.xml
│ │ │ ├── layout_appbar_viewer.xml
│ │ │ ├── layout_calendar_day.xml
│ │ │ ├── layout_calendar_week_days.xml
│ │ │ ├── layout_dialog_input_attachment.xml
│ │ │ ├── layout_item_add.xml
│ │ │ ├── layout_item_archived_event.xml
│ │ │ ├── layout_item_archived_subject.xml
│ │ │ ├── layout_item_archived_task.xml
│ │ │ ├── layout_item_event.xml
│ │ │ ├── layout_item_library.xml
│ │ │ ├── layout_item_log.xml
│ │ │ ├── layout_item_menu.xml
│ │ │ ├── layout_item_schedule.xml
│ │ │ ├── layout_item_subject.xml
│ │ │ ├── layout_item_subject_picker.xml
│ │ │ ├── layout_item_subject_single.xml
│ │ │ ├── layout_item_task.xml
│ │ │ ├── layout_item_task_send.xml
│ │ │ ├── layout_item_widget.xml
│ │ │ ├── layout_navigation_header.xml
│ │ │ ├── layout_preference_info.xml
│ │ │ ├── layout_sheet_options.xml
│ │ │ ├── layout_sheet_schedule.xml
│ │ │ ├── layout_sheet_schedule_editor.xml
│ │ │ ├── layout_viewer_image.xml
│ │ │ ├── layout_widget_events.xml
│ │ │ ├── layout_widget_progress.xml
│ │ │ ├── layout_widget_subjects.xml
│ │ │ └── layout_widget_tasks.xml
│ │ ├── menu/
│ │ │ ├── menu_add.xml
│ │ │ ├── menu_attachment.xml
│ │ │ ├── menu_editor.xml
│ │ │ ├── menu_events.xml
│ │ │ ├── menu_logs.xml
│ │ │ ├── menu_share.xml
│ │ │ ├── menu_subjects.xml
│ │ │ ├── menu_tasks.xml
│ │ │ └── navigation_main.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── navigation/
│ │ │ ├── navigation_container_event.xml
│ │ │ ├── navigation_container_subject.xml
│ │ │ ├── navigation_container_task.xml
│ │ │ ├── navigation_main.xml
│ │ │ └── navigation_root.xml
│ │ ├── values/
│ │ │ ├── array.xml
│ │ │ ├── attrs.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimen.xml
│ │ │ ├── integer.xml
│ │ │ ├── strings.xml
│ │ │ ├── themes.xml
│ │ │ └── values.xml
│ │ ├── values-ar/
│ │ │ └── strings.xml
│ │ ├── values-de/
│ │ │ └── strings.xml
│ │ ├── values-es/
│ │ │ └── strings.xml
│ │ ├── values-fr/
│ │ │ └── strings.xml
│ │ ├── values-hdpi/
│ │ │ └── dimen.xml
│ │ ├── values-id/
│ │ │ └── strings.xml
│ │ ├── values-night/
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ ├── values-night-v23/
│ │ │ └── themes.xml
│ │ ├── values-night-v27/
│ │ │ └── themes.xml
│ │ ├── values-ru/
│ │ │ └── strings.xml
│ │ ├── values-tr/
│ │ │ └── strings.xml
│ │ ├── values-v23/
│ │ │ └── themes.xml
│ │ ├── values-v27/
│ │ │ └── themes.xml
│ │ └── xml/
│ │ ├── xml_about_main.xml
│ │ ├── xml_about_notices.xml
│ │ ├── xml_launcher_shortcuts.xml
│ │ ├── xml_provider_paths.xml
│ │ ├── xml_settings_backups.xml
│ │ ├── xml_settings_main.xml
│ │ ├── xml_widget_events.xml
│ │ ├── xml_widget_subjects.xml
│ │ └── xml_widget_tasks.xml
│ └── test/
│ └── java/
│ └── com/
│ └── isaiahvonrundstedt/
│ └── fokus/
│ ├── ExampleUnitTest.kt
│ └── features/
│ ├── attachments/
│ │ └── AttachmentTests.kt
│ ├── event/
│ │ └── EventUnitTest.kt
│ ├── schedule/
│ │ └── ScheduleTests.kt
│ └── task/
│ └── TaskUnitTest.kt
├── build.gradle
├── fastlane/
│ └── metadata/
│ └── android/
│ ├── de/
│ │ └── short_description.txt
│ └── en-US/
│ ├── full_description.txt
│ └── short_description.txt
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/*
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/tasks.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/app/build/
/app/release/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
================================================
FILE: LICENSE
================================================
Copyright 2023, Isaiah Collins Abetong
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Fokus - To Do app tailored specifically for students
[](https://www.gnu.org/licenses/gpl-3.0.en.html)


Fokus is an open source application that combines a todo list and a calendar that can help you manage your school related work and events in one place. It's fast and beautiful yet simple design that can help you focus on what matters most.
### Not maintained anymore.
I cannot maintain this project anymore due to my full time work. If you want to continue it's development, you can fork this repository and continue maintaining the application. Thank you for using my simple application.
## Features
* Get reminded when a task is nearing its due
* Get reminded about incoming events
* Add attachments to your tasks
* Persistent notifications for important tasks or events
* No ads or any tracking
* Open Source Code
* On-Device Database
## Screenshots
<img src="art/preview-1.jpeg" width="200"><img src="art/preview-2.jpeg" width="200"><img src="art/preview-3.jpeg" width="200"><img src="art/preview-4.jpeg" width="200">
*Our preview mockups were created using 'Previewed' at https://previewed.app*
## Translations
* 🇮🇶 Arabic (Thanks! Mustafa K. Mirza)
* 🇺🇸 English
* 🇩🇪 German (Thanks! [mschmidm](https://github.com/mschmidm))
* 🇫🇷 French (Thanks! David Simon)
* 🇪🇸 Spanish (Thanks! Emmanuel Kunst)
* 🇷🇺 Russian
* 🇮🇩 Indonesian (Thanks! [Ilham Syahid S](https://github.com/ilhamsyahids))
* 🇹🇷 Turkish (Thanks! [Ahmet YÜREKLİ](https://github.com/vedfi))
## Built with
* Kotlin
* Room
* AndroidX
* Other cool open-source libraries (see Acknowledgements)
## Versioning
We use [SemVer](http://www.semver.org) for versioning. For the versions available, see the [tags on this repository](https://github.com/asayah-san/fokus-android/tags)
## Licenses
This project is licensed under the GPL-3.0 - see the license file for more details
## Contributing
This is an open-source personal project and I am very happy to accept community contributions. Open a PR to get started.
## Acknowledgements
### Libraries used
* [Material Dialogs](https://github.com/afollestad/material-dialogs)
* [Konfetti](https://github.com/DanielMartinus/Konfetti)
* [ExpandableBottomBar](https://github.com/st235/ExpandableBottomBar)
* [CommonsIO](https://commons.apache.org/proper/commons-io/)
* [Moshi](https://github.com/square/moshi)
* [Okio](https://github.com/square/okio)
* [CalendarView](https://github.com/kizitonwose/CalendarView)
* [AboutLibraries](https://github.com/mikepenz/AboutLibraries)
* [Cascade](https://github.com/saket/cascade)
### Other materials used
* [HeroIcons](https://www.heroicons.dev) - Tailwind Labs
* [Launcher Icon](https://www.flaticon.com/authors/freepik) - Freepik via Flaticon
* [Notification Sound](https://www.zapsplat.com/music/ui-alert-prompt-warm-wooden-mallet-style-notification-tone-generic-11/) - Zapsplat
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-parcelize'
id 'com.mikepenz.aboutlibraries.plugin'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 33
buildToolsVersion "33.0.0"
defaultConfig {
applicationId "com.isaiahvonrundstedt.fokus"
minSdkVersion 21
targetSdkVersion 33
versionCode 19
versionName "2.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.1.0-beta02"
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
kapt {
correctErrorTypes true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.afollestad.material-dialogs:core:3.3.0'
implementation 'com.afollestad.material-dialogs:color:3.3.0'
implementation 'com.afollestad.material-dialogs:datetime:3.3.0'
implementation 'com.afollestad.material-dialogs:lifecycle:3.3.0'
implementation 'com.github.kizitonwose:CalendarView:1.0.3'
implementation 'nl.dionsegijn:konfetti-xml:2.0.2'
implementation 'commons-io:commons-io:2.7'
implementation 'com.mikepenz:aboutlibraries-core:8.9.4'
implementation 'me.saket.cascade:cascade:1.3.0'
implementation 'io.coil-kt:coil:1.2.2'
implementation 'com.squareup.okio:okio:2.10.0'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.google.accompanist:accompanist-insets:0.20.2'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'
implementation "androidx.navigation:navigation-compose:2.5.0"
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.transition:transition:1.4.1'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0'
kapt 'androidx.lifecycle:lifecycle-common-java8:2.5.0'
implementation 'androidx.room:room-runtime:2.4.2'
implementation 'androidx.room:room-ktx:2.4.2'
kapt 'androidx.room:room-compiler:2.4.2'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-android-compiler:2.42'
implementation 'com.squareup.moshi:moshi:1.13.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.13.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class *.R
-keep class **.R$* {
<fields>;
}
================================================
FILE: app/schemas/com.isaiahvonrundstedt.fokus.database.AppDatabase/8.json
================================================
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "a5a325b51a216bf9c8987bdd216bf28b",
"entities": [
{
"tableName": "subjects",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subjectID` TEXT NOT NULL, `code` TEXT, `description` TEXT, `instructor` TEXT, `tag` INTEGER NOT NULL, `isSubjectArchived` INTEGER NOT NULL, PRIMARY KEY(`subjectID`))",
"fields": [
{
"fieldPath": "subjectID",
"columnName": "subjectID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "code",
"columnName": "code",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "instructor",
"columnName": "instructor",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSubjectArchived",
"columnName": "isSubjectArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"subjectID"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_subjects_subjectID",
"unique": false,
"columnNames": [
"subjectID"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subjects_subjectID` ON `${TABLE_NAME}` (`subjectID`)"
}
],
"foreignKeys": []
},
{
"tableName": "tasks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`taskID` TEXT NOT NULL, `name` TEXT, `notes` TEXT, `subject` TEXT, `isImportant` INTEGER NOT NULL, `dueDate` TEXT, `isFinished` INTEGER NOT NULL, `isTaskArchived` INTEGER NOT NULL, `dateAdded` TEXT, PRIMARY KEY(`taskID`), FOREIGN KEY(`subject`) REFERENCES `subjects`(`subjectID`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "taskID",
"columnName": "taskID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notes",
"columnName": "notes",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isImportant",
"columnName": "isImportant",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dueDate",
"columnName": "dueDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFinished",
"columnName": "isFinished",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTaskArchived",
"columnName": "isTaskArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateAdded",
"columnName": "dateAdded",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"taskID"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_tasks_taskID",
"unique": false,
"columnNames": [
"taskID"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_taskID` ON `${TABLE_NAME}` (`taskID`)"
}
],
"foreignKeys": [
{
"table": "subjects",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"subject"
],
"referencedColumns": [
"subjectID"
]
}
]
},
{
"tableName": "attachments",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`attachmentID` TEXT NOT NULL, `name` TEXT, `target` TEXT, `task` TEXT NOT NULL, `type` INTEGER NOT NULL, `dateAttached` TEXT, PRIMARY KEY(`attachmentID`), FOREIGN KEY(`task`) REFERENCES `tasks`(`taskID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "attachmentID",
"columnName": "attachmentID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "target",
"columnName": "target",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "task",
"columnName": "task",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateAttached",
"columnName": "dateAttached",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"attachmentID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "tasks",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"task"
],
"referencedColumns": [
"taskID"
]
}
]
},
{
"tableName": "logs",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`logID` TEXT NOT NULL, `title` TEXT, `content` TEXT, `data` TEXT, `type` INTEGER NOT NULL, `isImportant` INTEGER NOT NULL, `dateTimeTriggered` TEXT, PRIMARY KEY(`logID`))",
"fields": [
{
"fieldPath": "logID",
"columnName": "logID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isImportant",
"columnName": "isImportant",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateTimeTriggered",
"columnName": "dateTimeTriggered",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"logID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "events",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventID` TEXT NOT NULL, `name` TEXT, `notes` TEXT, `location` TEXT, `subject` TEXT, `isImportant` INTEGER NOT NULL, `isEventArchived` INTEGER NOT NULL, `schedule` TEXT, `dateAdded` TEXT, PRIMARY KEY(`eventID`), FOREIGN KEY(`subject`) REFERENCES `subjects`(`subjectID`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "eventID",
"columnName": "eventID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "notes",
"columnName": "notes",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "location",
"columnName": "location",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isImportant",
"columnName": "isImportant",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isEventArchived",
"columnName": "isEventArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "schedule",
"columnName": "schedule",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "dateAdded",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"eventID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "subjects",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"subject"
],
"referencedColumns": [
"subjectID"
]
}
]
},
{
"tableName": "schedules",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scheduleID` TEXT NOT NULL, `classroom` TEXT, `daysOfWeek` INTEGER NOT NULL, `weeksOfMonth` INTEGER NOT NULL, `startTime` TEXT, `endTime` TEXT, `subject` TEXT, PRIMARY KEY(`scheduleID`), FOREIGN KEY(`subject`) REFERENCES `subjects`(`subjectID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "scheduleID",
"columnName": "scheduleID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "classroom",
"columnName": "classroom",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "daysOfWeek",
"columnName": "daysOfWeek",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "weeksOfMonth",
"columnName": "weeksOfMonth",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startTime",
"columnName": "startTime",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "endTime",
"columnName": "endTime",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"scheduleID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "subjects",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"subject"
],
"referencedColumns": [
"subjectID"
]
}
]
}
],
"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, 'a5a325b51a216bf9c8987bdd216bf28b')"
]
}
}
================================================
FILE: app/src/androidTest/java/com/isaiahvonrundstedt/fokus/ExampleInstrumentedTest.kt
================================================
package com.isaiahvonrundstedt.fokus
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.isaiahvonrundstedt.digitaloasis", appContext.packageName)
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.isaiahvonrundstedt.fokus">
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application
android:name=".Fokus"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Fokus.Theme.Core">
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.isaiahvonrundstedt.fokus.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/xml_provider_paths"/>
</provider>
<activity android:name=".features.core.activities.MainActivity"
android:exported="true"
android:theme="@style/Fokus.Theme.Core.Navigation" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/xml_launcher_shortcuts"/>
</activity>
<activity android:name=".features.attachments.attach.AttachToTaskActivity"
android:exported="true">
<intent-filter android:label="@string/sharing_attach_to_task">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
<activity android:name=".features.subject.editor.SubjectEditorContainer" />
<activity android:name=".features.task.editor.TaskEditorContainer"/>
<activity android:name=".features.event.editor.EventEditorContainer"/>
<service android:name=".components.service.NotificationActionService"
android:exported="false"/>
<service android:name=".components.service.BackupRestoreService"
android:exported="false"/>
<service android:name=".components.service.FileImporterService"
android:exported="false"/>
<service android:name=".components.service.DataExporterService"
android:exported="false"/>
<service android:name=".components.service.DataImporterService"
android:exported="false"/>
<service android:name=".features.task.widget.TaskWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS"/>
<service android:name="com.isaiahvonrundstedt.fokus.features.event.widget.EventWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS"/>
<service android:name="com.isaiahvonrundstedt.fokus.features.subject.widget.SubjectWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS"/>
<receiver android:name=".components.receiver.LocalizationReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED"/>
</intent-filter>
</receiver>
<receiver
android:name="com.isaiahvonrundstedt.fokus.features.task.widget.TaskWidgetProvider"
android:label="@string/widget_task_name"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/xml_widget_tasks"/>
</receiver>
<receiver
android:name="com.isaiahvonrundstedt.fokus.features.event.widget.EventWidgetProvider"
android:label="@string/widget_event_name"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/xml_widget_events"/>
</receiver>
<receiver
android:name="com.isaiahvonrundstedt.fokus.features.subject.widget.SubjectWidgetProvider"
android:label="@string/widget_class_name"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/xml_widget_subjects"/>
</receiver>
</application>
</manifest>
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/Fokus.kt
================================================
package com.isaiahvonrundstedt.fokus
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp
import java.io.File
import javax.inject.Inject
@HiltAndroidApp
class Fokus : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
companion object {
fun obtainUriForFile(context: Context, source: File): Uri {
return FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.provider", source
)
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/custom/ItemDecoration.kt
================================================
package com.isaiahvonrundstedt.fokus.components.custom
import android.content.Context
import androidx.recyclerview.widget.DividerItemDecoration
class ItemDecoration(context: Context) : DividerItemDecoration(context, VERTICAL)
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/custom/ItemSwipeCallback.kt
================================================
package com.isaiahvonrundstedt.fokus.components.custom
import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.interfaces.Swipeable
class ItemSwipeCallback<T : Swipeable>(context: Context, private var adapter: T) :
ItemTouchHelper.Callback() {
private var isThemeDark: Boolean = false
private var iconDelete = ContextCompat.getDrawable(context, R.drawable.ic_outline_delete_24)
private var iconArchive = ContextCompat.getDrawable(context, R.drawable.ic_outline_archive_24)
private var backgroundDelete: Drawable
private var backgroundArchive: Drawable
init {
isThemeDark = (context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val colorDelete = if (isThemeDark)
Color.parseColor(COLOR_BACKGROUND_DARK_DELETE)
else Color.parseColor(COLOR_BACKGROUND_LIGHT_DELETE)
val colorArchive = if (isThemeDark)
Color.parseColor(COLOR_BACKGROUND_DARK_ARCHIVE)
else Color.parseColor(COLOR_BACKGROUND_LIGHT_ARCHIVE)
backgroundDelete = ColorDrawable(colorDelete)
backgroundArchive = ColorDrawable(colorArchive)
}
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(0, ItemTouchHelper.START or ItemTouchHelper.END)
}
override fun onMove(
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
adapter.onSwipe(viewHolder.bindingAdapterPosition, direction)
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
return defaultValue * 10
}
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
val itemView: View = viewHolder.itemView
val backgroundCornerOffset = 40
if (dX > 0) {
with(backgroundArchive) {
setBounds(
itemView.left, itemView.top,
itemView.left + dX.toInt() + backgroundCornerOffset, itemView.bottom
)
draw(c)
}
iconArchive?.let {
val tintColor = if (isThemeDark)
Color.parseColor(COLOR_ICON_DARK_ARCHIVE)
else Color.parseColor(COLOR_ICON_LIGHT_ARCHIVE)
it.mutate()
it.setTint(tintColor)
val iconMargin: Int = (itemView.height - it.intrinsicHeight) / 2
val iconTop: Int = itemView.top + (itemView.height - it.intrinsicHeight) / 2
val iconBottom: Int = iconTop + it.intrinsicHeight
val iconLeft: Int = itemView.left + iconMargin
val iconRight: Int = iconLeft + it.intrinsicWidth
it.setBounds(iconLeft, iconTop, iconRight, iconBottom)
it.draw(c)
}
} else if (dX < 0) {
with(backgroundDelete) {
setBounds(
itemView.right + dX.toInt() - backgroundCornerOffset, itemView.top,
itemView.right, itemView.bottom
)
draw(c)
}
iconDelete?.let {
val tintColor = if (isThemeDark)
Color.parseColor(COLOR_ICON_DARK_DELETE)
else Color.parseColor(COLOR_ICON_LIGHT_DELETE)
it.mutate()
it.setTint(tintColor)
val iconMargin: Int = (itemView.height - it.intrinsicHeight) / 2
val iconTop: Int = itemView.top + (itemView.height - it.intrinsicHeight) / 2
val iconBottom: Int = iconTop + it.intrinsicHeight
val iconLeft: Int = itemView.right - iconMargin - it.intrinsicWidth
val iconRight: Int = itemView.right - iconMargin
it.setBounds(iconLeft, iconTop, iconRight, iconBottom)
it.draw(c)
}
}
}
companion object {
const val COLOR_ICON_LIGHT_DELETE = "#ea4335"
const val COLOR_ICON_DARK_DELETE = "#000000"
const val COLOR_ICON_LIGHT_ARCHIVE = "#00c853"
const val COLOR_ICON_DARK_ARCHIVE = "#000000"
const val COLOR_BACKGROUND_LIGHT_DELETE = "#66ea4335"
const val COLOR_BACKGROUND_DARK_DELETE = "#ea4335"
const val COLOR_BACKGROUND_LIGHT_ARCHIVE = "#6600c853"
const val COLOR_BACKGROUND_DARK_ARCHIVE = "#00c853"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/enums/SortDirection.kt
================================================
package com.isaiahvonrundstedt.fokus.components.enums
enum class SortDirection {
ASCENDING, DESCENDING;
companion object {
fun parse(s: String?): SortDirection {
return when (s) {
ASCENDING.toString() -> ASCENDING
DESCENDING.toString() -> DESCENDING
else -> throw IllegalStateException("Sort Direction must be ascending or descending")
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/AppCompatExtensions.kt
================================================
package com.isaiahvonrundstedt.fokus.components.extensions.android
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.view.View
import android.widget.Toast
import androidx.annotation.DimenRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
fun Context.isDark(): Boolean {
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
fun Context.getDimension(@DimenRes res: Int): Int {
return (resources.getDimension(res) / resources.displayMetrics.density).toInt()
}
fun Context.startForegroundServiceCompat(service: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
startForegroundService(service)
else startService(service)
}
fun AppCompatActivity.createSnackbar(
@StringRes textRes: Int,
view: View = window.decorView.rootView,
duration: Int = Snackbar.LENGTH_SHORT
): Snackbar {
return Snackbar.make(view, getString(textRes), duration).apply { show() }
}
fun AppCompatActivity.createToast(
@StringRes textRes: Int,
duration: Int = Toast.LENGTH_SHORT
): Toast {
return Toast.makeText(this, getString(textRes), duration).apply { show() }
}
fun Fragment.createSnackbar(
@StringRes textRes: Int,
view: View = this.requireView(),
duration: Int = Snackbar.LENGTH_SHORT
): Snackbar {
return Snackbar.make(view, getString(textRes), duration).apply { show() }
}
fun Fragment.createToast(
@StringRes textRes: Int,
duration: Int = Toast.LENGTH_SHORT
): Toast {
return Toast.makeText(requireContext(), getString(textRes), duration).apply { show() }
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/IntentExtensions.kt
================================================
package com.isaiahvonrundstedt.fokus.components.extensions.android
import android.content.Intent
import android.os.Parcelable
import com.isaiahvonrundstedt.fokus.components.extensions.jdk.toArrayList
fun <T : Parcelable> Intent.putExtra(key: String, items: List<T>) {
putParcelableArrayListExtra(key, items.toArrayList())
}
fun <T : Parcelable> Intent.getParcelableListExtra(key: String): List<T>? {
return getParcelableArrayListExtra<T>(key)?.toList()
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/TextViewExtensions.kt
================================================
package com.isaiahvonrundstedt.fokus.components.extensions.android
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
/**
* Extension function used to change the text color
* of a AppCompatTextView
* @param id a color id from the Android resource
*/
fun TextView.setTextColorFromResource(@ColorRes id: Int) {
this.setTextColor(ContextCompat.getColor(this.context, id))
}
/**
* Extension function used to set an strike through effect on the
* painted text on the view
* @param status determines whether to add or remove the effect on the text
*/
fun TextView.setStrikeThroughEffect(status: Boolean) {
if (status) this.paintFlags = this.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
else this.paintFlags = this.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
/**
* Extension function used to add a compound drawable in the
* TextView at a specific position
*/
fun TextView.setCompoundDrawableAtStart(drawable: Drawable?) {
this.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null)
}
/**
* Extension function used to get the compound drawable in the
* TextView at the specific position
*/
fun TextView.getCompoundDrawableAtStart(): Drawable? {
// Start, Top, End, Bottom
return this.compoundDrawablesRelative[0]
}
/**
* Extension function to remove the compound drawable
* in the TextView at the specific position
*/
fun TextView.removeCompoundDrawableAtStart() {
this.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/UriExtensions.kt
================================================
package com.isaiahvonrundstedt.fokus.components.extensions.android
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
/**
* Extension function used to get the file name
* of an uri object
* @param context used to get access for a contentResolver object
*/
fun Uri.getFileName(context: Context): String {
var result = ""
if (this.scheme == "content") {
val cursor: Cursor? = context.contentResolver?.query(this, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst())
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
} catch (ex: Exception) {
} finally {
cursor?.close()
}
} else {
result = this.path.toString()
val index = result.lastIndexOf('/')
if (index != 1)
result = result.substring(index + 1)
}
return result
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/jdk/CalendarExtensions.kt
================================================
package com.isaiahvonrundstedt.fokus.components.extensions.jdk
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.*
/**
* An extension function used to convert
* the legacy Calendar instance to the
* modern java.time.LocalTime instance
*
* @return the converted LocalTime instance
*/
fun Calendar.toLocalTime(): LocalTime {
return LocalTime.of(
this.get(Calendar.HOUR_OF_DAY), this.get(Calendar.MINUTE),
this.get(Calendar.SECOND)
)
}
/**
* An extension function used to convert
* the legacy Calendar instance to the
* modern java.time.ZonedDateTime instance
*
* @return the converted ZonedDateTime instance
*/
fun Calendar.toZonedDateTime(): ZonedDateTime? {
return ZonedDateTime.ofInstant(this.toInstant(), ZoneId.systemDefault())
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/jdk/ListExtensions.kt
================================================
package com.isaiahvonrundstedt.fokus.components.extensions.jdk
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.log.Log
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.task.Task
fun <T> List<T>.getIndexByID(id: String): Int {
this.forEachIndexed { index, it ->
if (it is Task)
if (it.taskID == id) return index
if (it is Event)
if (it.eventID == id) return index
if (it is Subject)
if (it.subjectID == id) return index
if (it is Attachment)
if (it.attachmentID == id) return index
if (it is Log)
if (it.logID == id) return index
if (it is Schedule)
if (it.scheduleID == id) return index
}
return -1
}
/**
* Extension function to create an ArrayList
* from the current List object
*/
fun <T> List<T>.toArrayList(): ArrayList<T> {
return ArrayList(this)
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/jdk/TimeExtensions.kt
================================================
package com.isaiahvonrundstedt.fokus.components.extensions.jdk
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.*
/**
* An extension function to convert the
* LocalTime instance to a ZonedDateTime instance
* with the current date
*
* @return the ZonedDateTime instance with the values of the
* LocalTime instance
*/
fun LocalTime.toZonedDateTimeToday(): ZonedDateTime? {
return LocalDate.now().atStartOfDay(ZoneId.systemDefault())
.with(this)
}
/**
* An extension function used to
* determine if the ZonedDateTime object
* is same as the date today
*
* @return true if the date matches from the current date
*/
fun ZonedDateTime.isToday(): Boolean {
return LocalDate.now().isEqual(this.toLocalDate())
}
/**
* An extension function used to
* determine if the ZonedDateTime object
* is the same as the next day
* @return true if the ZonedDateTime object is the
* same as the next day
*/
fun ZonedDateTime.isTomorrow(): Boolean {
return LocalDate.now().plusDays(1).compareTo(this.toLocalDate()) == 0
}
/**
* An extension function used to
* determine if the ZonedDateTime object
* is the same as the previous day
* @return true if the ZonedDateTime object is the
* same as the previous day
*/
fun ZonedDateTime.isYesterday(): Boolean {
return LocalDate.now().minusDays(1).compareTo(this.toLocalDate()) == 0
}
/**
* An extension function used to determine
* if the ZonedDateTime object is before
* the current datetime
* @return true if the current ZonedDateTime object
* is before the current date-time
*/
fun ZonedDateTime.isBeforeNow(): Boolean {
return this.isBefore(ZonedDateTime.now())
}
/**
* An extension function used to determine
* if the ZonedDateTime object is after
* the current datetime
* @return true if the current ZonedDateTime object
* is after the current date-time
*/
fun ZonedDateTime.isAfterNow(): Boolean {
return this.isAfter(ZonedDateTime.now())
}
/**
* An extension function used to convert the
* current instance of ZonedDateTime to
* a legacy Calendar instance
* @return calendar instance with the same values as
* this instance
*/
fun ZonedDateTime.toCalendar(): Calendar {
return GregorianCalendar.from(this)
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/interfaces/Streamable.kt
================================================
package com.isaiahvonrundstedt.fokus.components.interfaces
import java.io.File
import java.io.InputStream
interface Streamable {
fun toJsonString(): String?
fun toJsonFile(destination: File, name: String): File
fun fromInputStream(inputStream: InputStream)
companion object {
const val ARCHIVE_NAME_GENERIC = "exported"
const val FILE_NAME_TASK = "task.json"
const val FILE_NAME_ATTACHMENT = "attachment.json"
const val FILE_NAME_SUBJECT = "subject.json"
const val FILE_NAME_SCHEDULE = "schedule.json"
const val FILE_NAME_EVENT = "event.json"
const val FILE_NAME_LOG = "log.json"
const val DIRECTORY_GENERIC = "others"
const val DIRECTORY_ATTACHMENTS = "attachments"
const val MIME_TYPE_ZIP = "application/zip"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/interfaces/Swipeable.kt
================================================
package com.isaiahvonrundstedt.fokus.components.interfaces
interface Swipeable {
fun onSwipe(position: Int, direction: Int)
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/json/JsonDataStreamer.kt
================================================
package com.isaiahvonrundstedt.fokus.components.json
import android.net.Uri
import com.isaiahvonrundstedt.fokus.database.converter.DateTimeConverter
import com.squareup.moshi.*
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okio.buffer
import okio.source
import java.io.InputStream
import java.time.LocalTime
import java.time.ZonedDateTime
class JsonDataStreamer private constructor() {
class DateTimeAdapter {
@FromJson
fun toDateTime(string: String): ZonedDateTime? = DateTimeConverter.toZonedDateTime(string)
@ToJson
fun fromDateTime(dateTime: ZonedDateTime): String? =
DateTimeConverter.fromZonedDateTime(dateTime)
}
class LocalTimeAdapter {
@FromJson
fun toLocalTime(string: String): LocalTime? = DateTimeConverter.toLocalTime(string)
@ToJson
fun fromLocalTime(time: LocalTime): String? = DateTimeConverter.fromLocalTime(time)
}
class UriAdapter {
@FromJson
fun toUri(data: String): Uri = Uri.parse(data)
@ToJson
fun fromUri(uri: Uri): String = uri.toString()
}
companion object {
val moshi: Moshi
get() = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(DateTimeAdapter())
.add(LocalTimeAdapter())
.add(UriAdapter())
.build()
fun <T> encodeToJson(data: T?, dataType: Class<T>): String? {
if (data == null) return null
val adapter: JsonAdapter<T> = moshi.adapter(dataType)
return adapter.toJson(data)
}
fun <T> encodeToJson(dataItems: List<T>?, dataType: Class<T>): String? {
if (dataItems == null || dataItems.isEmpty()) return null
val type = Types.newParameterizedType(List::class.java, dataType)
val adapter: JsonAdapter<List<T>> = moshi.adapter(type)
return adapter.toJson(dataItems)
}
fun <T> decodeOnceFromJson(stream: InputStream, dataType: Class<T>): T? {
if (stream.isEmpty()) return null
val adapter: JsonAdapter<T> = moshi.adapter(dataType)
return adapter.fromJson(stream.source().buffer())
}
fun <T> decodeFromJson(stream: InputStream, dataType: Class<T>): List<T>? {
if (stream.isEmpty()) return emptyList()
val type = Types.newParameterizedType(List::class.java, dataType)
val adapter: JsonAdapter<List<T>> = moshi.adapter(type)
return adapter.fromJson(stream.source().buffer())
}
private fun InputStream.isEmpty(): Boolean {
return this.available() < 1
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/json/Metadata.kt
================================================
package com.isaiahvonrundstedt.fokus.components.json
import com.isaiahvonrundstedt.fokus.BuildConfig
import com.isaiahvonrundstedt.fokus.components.interfaces.Streamable
import com.isaiahvonrundstedt.fokus.database.AppDatabase
import com.squareup.moshi.JsonClass
import okio.buffer
import okio.sink
import java.io.File
import java.io.InputStream
/**
* Metadata class handles exported json
* data the is packaged through zip.
* It reads what data are inside the package,
* and if the corresponding data is compatible
* with the application.
*/
@JsonClass(generateAdapter = true)
data class Metadata @JvmOverloads constructor(
var appVersion: Int = BuildConfig.VERSION_CODE,
var appBuildName: String? = BuildConfig.VERSION_NAME,
var databaseVersion: Int = AppDatabase.DATABASE_VERSION,
var data: String? = null
) : Streamable {
fun verify(dataString: String): Boolean {
return databaseVersion == AppDatabase.DATABASE_VERSION &&
data == dataString
}
override fun toJsonString(): String? {
return JsonDataStreamer.encodeToJson(this, Metadata::class.java)
}
override fun toJsonFile(destination: File, name: String): File {
return File(destination, name).apply {
this.sink().buffer().use {
toJsonString()?.also { json -> it.write(json.toByteArray()) }
}
}
}
override fun fromInputStream(inputStream: InputStream) {
JsonDataStreamer.decodeOnceFromJson(inputStream, Metadata::class.java)?.also {
appVersion = it.appVersion
appBuildName = it.appBuildName
databaseVersion = it.databaseVersion
data = it.data
}
}
companion object {
const val DATA_BUNDLE = "data:bundle"
const val DATA_TASK = "data:task"
const val DATA_SUBJECT = "data:subject"
const val DATA_EVENT = "data:event"
const val DATA_LOG = "data:log"
const val FILE_NAME = "metadata.json"
fun fromInputStream(inputStream: InputStream): Metadata {
return Metadata().apply {
this.fromInputStream(inputStream)
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/modules/DatabaseModule.kt
================================================
package com.isaiahvonrundstedt.fokus.components.modules
import android.app.NotificationManager
import android.content.Context
import androidx.work.WorkManager
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import com.isaiahvonrundstedt.fokus.database.AppDatabase
import com.isaiahvonrundstedt.fokus.database.dao.*
import com.isaiahvonrundstedt.fokus.database.repository.EventRepository
import com.isaiahvonrundstedt.fokus.database.repository.LogRepository
import com.isaiahvonrundstedt.fokus.database.repository.SubjectRepository
import com.isaiahvonrundstedt.fokus.database.repository.TaskRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return AppDatabase.getInstance(context)
}
@Provides
fun provideTaskDao(database: AppDatabase): TaskDAO = database.tasks()
@Provides
fun provideAttachmentDao(database: AppDatabase): AttachmentDAO = database.attachments()
@Provides
fun provideSubjectDao(database: AppDatabase): SubjectDAO = database.subjects()
@Provides
fun provideScheduleDao(database: AppDatabase): ScheduleDAO = database.schedules()
@Provides
fun provideEventDao(database: AppDatabase): EventDAO = database.events()
@Provides
fun provideLogDao(database: AppDatabase): LogDAO = database.logs()
@Provides
fun provideTaskRepository(
@ApplicationContext
context: Context,
taskDao: TaskDAO,
attachmentDao: AttachmentDAO,
preferenceManager: PreferenceManager,
workManager: WorkManager,
notificationManager: NotificationManager
): TaskRepository {
return TaskRepository(
context,
taskDao,
attachmentDao,
preferenceManager,
workManager,
notificationManager
)
}
@Provides
fun provideSubjectRepository(
@ApplicationContext
context: Context,
subjectDAO: SubjectDAO,
scheduleDAO: ScheduleDAO,
preferenceManager: PreferenceManager,
workManager: WorkManager
): SubjectRepository {
return SubjectRepository(context, subjectDAO, scheduleDAO, preferenceManager, workManager)
}
@Provides
fun provideEventRepository(
@ApplicationContext
context: Context,
eventDAO: EventDAO,
preferenceManager: PreferenceManager,
workManager: WorkManager,
notificationManager: NotificationManager
): EventRepository {
return EventRepository(
context,
eventDAO,
preferenceManager,
workManager,
notificationManager
)
}
@Provides
fun provideLogRepository(dao: LogDAO): LogRepository = LogRepository(dao)
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/modules/ExternalModule.kt
================================================
package com.isaiahvonrundstedt.fokus.components.modules
import android.app.NotificationManager
import android.content.ClipboardManager
import android.content.Context
import androidx.work.WorkManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class ExternalModule {
@Singleton
@Provides
fun provideWorkManager(@ApplicationContext context: Context): WorkManager {
return WorkManager.getInstance(context)
}
@Provides
fun provideNotificationManager(@ApplicationContext context: Context): NotificationManager {
return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
@Provides
fun provideClipboardManager(@ApplicationContext context: Context): ClipboardManager {
return context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/modules/InternalModule.kt
================================================
package com.isaiahvonrundstedt.fokus.components.modules
import android.content.Context
import com.isaiahvonrundstedt.fokus.components.utils.PermissionManager
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class InternalModule {
@Singleton
@Provides
fun providePreferenceManager(@ApplicationContext context: Context): PreferenceManager {
return PreferenceManager(context)
}
@Singleton
@Provides
fun providePermissionManager(@ApplicationContext context: Context): PermissionManager {
return PermissionManager(context)
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/preference/InformationHolder.kt
================================================
package com.isaiahvonrundstedt.fokus.components.preference
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import androidx.core.content.res.TypedArrayUtils
import androidx.preference.Preference
import com.isaiahvonrundstedt.fokus.R
@SuppressLint("RestrictedApi")
class InformationHolder(
context: Context,
attr: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : Preference(context, attr, defStyleAttr, defStyleRes) {
constructor(context: Context, attr: AttributeSet?, defStyleAttr: Int)
: this(context, attr, defStyleAttr, 0)
constructor(context: Context, attr: AttributeSet?)
: this(
context, attr, TypedArrayUtils.getAttr(
context,
androidx.preference.R.attr.preferenceStyle, android.R.attr.preferenceStyle
)
)
constructor(context: Context) : this(context, null)
init {
layoutResource = R.layout.layout_preference_info
}
override fun onClick() {}
override fun performClick() {}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/receiver/LocalizationReceiver.kt
================================================
package com.isaiahvonrundstedt.fokus.components.receiver
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import com.isaiahvonrundstedt.fokus.components.utils.NotificationChannelManager
class LocalizationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return
if (intent?.action == Intent.ACTION_LOCALE_CHANGED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
with(NotificationChannelManager(context!!)) {
register(
NotificationChannelManager.CHANNEL_ID_GENERIC,
NotificationManager.IMPORTANCE_DEFAULT
)
register(
NotificationChannelManager.CHANNEL_ID_TASK,
groupID = NotificationChannelManager.CHANNEL_GROUP_ID_REMINDERS
)
register(
NotificationChannelManager.CHANNEL_ID_EVENT,
groupID = NotificationChannelManager.CHANNEL_GROUP_ID_REMINDERS
)
register(
NotificationChannelManager.CHANNEL_ID_CLASS,
groupID = NotificationChannelManager.CHANNEL_GROUP_ID_REMINDERS
)
}
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/BackupRestoreService.kt
================================================
package com.isaiahvonrundstedt.fokus.components.service
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.os.IBinder
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.interfaces.Streamable
import com.isaiahvonrundstedt.fokus.components.json.JsonDataStreamer
import com.isaiahvonrundstedt.fokus.components.json.Metadata
import com.isaiahvonrundstedt.fokus.components.utils.DataArchiver
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import com.isaiahvonrundstedt.fokus.database.AppDatabase
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.log.Log
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseService
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.task.Task
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import okio.buffer
import okio.sink
import org.apache.commons.io.FileUtils
import java.io.EOFException
import java.io.File
import java.io.InputStream
import java.time.ZonedDateTime
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
class BackupRestoreService : BaseService() {
private lateinit var database: AppDatabase
override fun onCreate() {
super.onCreate()
database = AppDatabase.getInstance(this)
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.data?.also {
when (intent.action) {
ACTION_BACKUP ->
startBackup(it)
ACTION_RESTORE ->
startRestore(it)
}
}
return START_NOT_STICKY
}
private fun startRestore(uri: Uri) {
startForegroundCompat(
NOTIFICATION_RESTORE_ONGOING,
createNotification(
true, R.string.notification_restore_ongoing,
iconRes = R.drawable.ic_outline_file_download_24
)
)
try {
val archiveStream: InputStream? = contentResolver.openInputStream(uri)
val archive = DataArchiver.parseInputStream(this, archiveStream)
archive.getInputStream(archive.getEntry(Metadata.FILE_NAME))?.use {
val metadata = Metadata.fromInputStream(it)
if (!metadata.verify(Metadata.DATA_BUNDLE)) {
stopForegroundCompat(NOTIFICATION_RESTORE_ONGOING)
manager?.notify(
NOTIFICATION_RESTORE_FAILED,
createNotification(titleRes = R.string.notification_restore_error)
)
terminateService()
archive.close()
}
}
for (entry: ZipEntry in archive.entries()) {
archive.getInputStream(entry)?.use {
tryParse(archive, entry, it)
}
}
stopForegroundCompat(NOTIFICATION_RESTORE_ONGOING)
manager?.notify(
NOTIFICATION_RESTORE_SUCCESS,
createNotification(titleRes = R.string.notification_restore_success)
)
terminateService()
archive.close()
} catch (e: EOFException) {
e.printStackTrace()
stopForegroundCompat(NOTIFICATION_RESTORE_ONGOING)
manager?.notify(
NOTIFICATION_RESTORE_FAILED,
createNotification(
titleRes = R.string.notification_restore_error,
contentRes = R.string.feedback_restore_corrupted
)
)
terminateService()
} catch (e: Exception) {
e.printStackTrace()
stopForegroundCompat(NOTIFICATION_RESTORE_ONGOING)
manager?.notify(
NOTIFICATION_RESTORE_FAILED,
createNotification(
titleRes = R.string.notification_restore_error,
contentRes = R.string.feedback_restore_invalid
)
)
terminateService()
}
}
private fun tryParse(archive: ZipFile, entry: ZipEntry, stream: InputStream) {
if (entry.name == Streamable.FILE_NAME_SUBJECT) {
JsonDataStreamer.decodeFromJson(stream, Subject::class.java)?.run {
runBlocking { forEach { database.subjects().insert(it) } }
}
} else if (entry.name == Streamable.FILE_NAME_SCHEDULE) {
JsonDataStreamer.decodeFromJson(stream, Schedule::class.java)?.run {
runBlocking { forEach { database.schedules().insert(it) } }
}
} else if (entry.name == Streamable.FILE_NAME_TASK) {
JsonDataStreamer.decodeFromJson(stream, Task::class.java)?.run {
runBlocking { forEach { database.tasks().insert(it) } }
}
} else if (entry.name == Streamable.FILE_NAME_ATTACHMENT) {
JsonDataStreamer.decodeFromJson(stream, Attachment::class.java)?.run {
runBlocking { forEach { database.attachments().insert(it) } }
}
} else if (entry.name == Streamable.FILE_NAME_EVENT) {
JsonDataStreamer.decodeFromJson(stream, Event::class.java)?.run {
runBlocking { forEach { database.events().insert(it) } }
}
} else if (entry.name == Streamable.FILE_NAME_LOG) {
JsonDataStreamer.decodeFromJson(stream, Log::class.java)?.run {
runBlocking { forEach { database.logs().insert(it) } }
}
} else if (entry.name.contains(Streamable.DIRECTORY_ATTACHMENTS)
&& !entry.isDirectory
) {
val targetDirectory = File(
getExternalFilesDir(null),
Streamable.DIRECTORY_ATTACHMENTS
)
val destination = File(targetDirectory, File(entry.name).name)
archive.getInputStream(entry)?.use { inputStream ->
FileUtils.copyToFile(inputStream, destination)
}
}
}
private fun startBackup(destination: Uri) {
if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED)
return
startForegroundCompat(
NOTIFICATION_BACKUP_ONGOING,
createNotification(
ongoing = true, titleRes = R.string.notification_backup_ongoing,
iconRes = R.drawable.ic_outline_file_upload_24
)
)
try {
runBlocking {
val items = mutableListOf<File>()
var fetchJob: Job
fetchJob = async { database.subjects().fetch() }
JsonDataStreamer.encodeToJson(fetchJob.await(), Subject::class.java)?.let {
items.add(createCache(Streamable.FILE_NAME_SUBJECT, it))
}
fetchJob = async { database.schedules().fetch() }
JsonDataStreamer.encodeToJson(fetchJob.await(), Schedule::class.java)?.let {
items.add(createCache(Streamable.FILE_NAME_SCHEDULE, it))
}
fetchJob = async { database.tasks().fetch() }
JsonDataStreamer.encodeToJson(fetchJob.await(), Task::class.java)?.let {
items.add(createCache(Streamable.FILE_NAME_TASK, it))
}
fetchJob = async { database.attachments().fetch() }
val attachments: List<Attachment>? = fetchJob.await()
JsonDataStreamer.encodeToJson(attachments, Attachment::class.java)?.let {
items.add(createCache(Streamable.FILE_NAME_ATTACHMENT, it))
}
val attachmentFolder = File(
cacheDir,
Streamable.DIRECTORY_ATTACHMENTS
)
if (!attachmentFolder.exists()) attachmentFolder.mkdir()
attachments?.forEach {
if (it.type == Attachment.TYPE_IMPORTED_FILE && it.target != null)
FileUtils.copyFileToDirectory(
File(it.target!!),
attachmentFolder
)
}
items.add(attachmentFolder)
fetchJob = async { database.events().fetch() }
JsonDataStreamer.encodeToJson(fetchJob.await(), Event::class.java)?.let {
items.add(createCache(Streamable.FILE_NAME_EVENT, it))
}
fetchJob = async { database.logs().fetchCore() }
JsonDataStreamer.encodeToJson(fetchJob.await(), Log::class.java)?.let {
items.add(createCache(Streamable.FILE_NAME_LOG, it))
}
items.add(
Metadata(data = Metadata.DATA_BUNDLE)
.toJsonFile(cacheDir, Metadata.FILE_NAME)
)
if (items.isEmpty()) {
stopForegroundCompat(NOTIFICATION_BACKUP_ONGOING)
terminateService(BROADCAST_BACKUP_EMPTY)
}
DataArchiver.Create(this@BackupRestoreService)
.addSource(items)
.toDestination(destination)
.start()
PreferenceManager(this@BackupRestoreService)
.previousBackupDate = ZonedDateTime.now()
stopForegroundCompat(NOTIFICATION_BACKUP_ONGOING)
manager?.notify(
NOTIFICATION_BACKUP_SUCCESS,
createNotification(titleRes = R.string.notification_backup_success)
)
terminateService(BROADCAST_BACKUP_SUCCESS)
}
} catch (e: Exception) {
e.printStackTrace()
stopForegroundCompat(NOTIFICATION_BACKUP_ONGOING)
manager?.notify(
NOTIFICATION_BACKUP_FAILED,
createNotification(titleRes = R.string.notification_backup_error)
)
terminateService(BROADCAST_BACKUP_FAILED)
}
}
private fun createCache(name: String, json: String): File {
return File(cacheDir, name).apply {
this.sink().buffer().use {
it.write(json.toByteArray())
it.flush()
}
}
}
companion object {
const val FILE_BACKUP_NAME = "backup"
const val ACTION_BACKUP = "action:backup"
const val ACTION_RESTORE = "action:restore"
const val NOTIFICATION_BACKUP_ONGOING = 1
const val NOTIFICATION_BACKUP_SUCCESS = 2
const val NOTIFICATION_BACKUP_FAILED = 3
const val NOTIFICATION_RESTORE_ONGOING = 4
const val NOTIFICATION_RESTORE_SUCCESS = 5
const val NOTIFICATION_RESTORE_FAILED = 6
const val BROADCAST_BACKUP_SUCCESS = "broadcast:backup:success"
const val BROADCAST_BACKUP_FAILED = "broadcast:backup:failed"
const val BROADCAST_BACKUP_EMPTY = "broadcast:backup:empty"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/DataExporterService.kt
================================================
package com.isaiahvonrundstedt.fokus.components.service
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import com.isaiahvonrundstedt.fokus.components.extensions.android.getParcelableListExtra
import com.isaiahvonrundstedt.fokus.components.interfaces.Streamable
import com.isaiahvonrundstedt.fokus.components.json.Metadata
import com.isaiahvonrundstedt.fokus.components.utils.DataArchiver
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseService
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.task.Task
import java.io.File
class DataExporterService : BaseService() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
onExport(intent)
sendLocalBroadcast(BROADCAST_EXPORT_ONGOING)
return START_REDELIVER_INTENT
}
private fun onExport(intent: Intent?) {
if (intent?.hasExtra(EXTRA_EXPORT_SOURCE) == false)
terminateService(BROADCAST_EXPORT_FAILED)
val destination: Uri? = intent?.data
val items = mutableListOf<File>()
var fileName: String = Streamable.ARCHIVE_NAME_GENERIC
try {
when (intent?.action) {
ACTION_EXPORT_SUBJECT -> {
items.add(
Metadata(data = Metadata.DATA_SUBJECT)
.toJsonFile(cacheDir, Metadata.FILE_NAME)
)
val subject: Subject? = intent.getParcelableExtra(EXTRA_EXPORT_SOURCE)
val schedules: List<Schedule>? =
intent.getParcelableListExtra(EXTRA_EXPORT_DEPENDENTS)
if (subject != null) {
fileName = subject.code ?: Streamable.ARCHIVE_NAME_GENERIC
items.add(subject.toJsonFile(cacheDir, Streamable.FILE_NAME_SUBJECT))
}
items.add(Schedule.toJsonFile(schedules ?: emptyList(), cacheDir))
}
ACTION_EXPORT_TASK -> {
items.add(
Metadata(data = Metadata.DATA_TASK)
.toJsonFile(cacheDir, Metadata.FILE_NAME)
)
val task: Task? = intent.getParcelableExtra(EXTRA_EXPORT_SOURCE)
if (task != null) {
fileName = task.name ?: Streamable.ARCHIVE_NAME_GENERIC
items.add(task.toJsonFile(cacheDir, Streamable.FILE_NAME_TASK))
}
var attachments: List<Attachment> =
intent.getParcelableListExtra(EXTRA_EXPORT_DEPENDENTS) ?: emptyList()
attachments = attachments.filter { it.type == Attachment.TYPE_WEBSITE_LINK }
items.add(Attachment.toJsonFile(attachments, cacheDir))
}
ACTION_EXPORT_EVENT -> {
items.add(
Metadata(data = Metadata.DATA_EVENT)
.toJsonFile(cacheDir, Metadata.FILE_NAME)
)
val event: Event? = intent.getParcelableExtra(EXTRA_EXPORT_SOURCE)
if (event != null) {
fileName = event.name ?: Streamable.ARCHIVE_NAME_GENERIC
items.add(event.toJsonFile(cacheDir, Streamable.FILE_NAME_EVENT))
}
}
}
if (destination == null) {
val cache = File(externalCacheDir, fileName)
DataArchiver.Create(this)
.addSource(items)
.toDestination(cache)
.start()
terminateService(BROADCAST_EXPORT_COMPLETED, cache.path)
} else {
DataArchiver.Create(this)
.addSource(items)
.toDestination(destination)
.start()
terminateService(BROADCAST_EXPORT_COMPLETED)
}
} catch (e: Exception) {
terminateService(BROADCAST_EXPORT_FAILED)
}
}
companion object {
const val EXTRA_EXPORT_SOURCE = "extra:export:source"
const val EXTRA_EXPORT_DEPENDENTS = "extra:export:dependents"
const val ACTION_EXPORT_SUBJECT = "action:export:subject"
const val ACTION_EXPORT_TASK = "action:export:task"
const val ACTION_EXPORT_EVENT = "action:export:event"
const val BROADCAST_EXPORT_ONGOING = "broadcast:export:ongoing"
const val BROADCAST_EXPORT_COMPLETED = "broadcast:export:completed"
const val BROADCAST_EXPORT_FAILED = "broadcast:export:failed"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/DataImporterService.kt
================================================
package com.isaiahvonrundstedt.fokus.components.service
import android.content.Intent
import android.os.IBinder
import android.os.Parcelable
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.isaiahvonrundstedt.fokus.components.interfaces.Streamable
import com.isaiahvonrundstedt.fokus.components.json.JsonDataStreamer
import com.isaiahvonrundstedt.fokus.components.json.Metadata
import com.isaiahvonrundstedt.fokus.components.utils.DataArchiver
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.event.EventPackage
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseService
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.subject.SubjectPackage
import com.isaiahvonrundstedt.fokus.features.task.Task
import com.isaiahvonrundstedt.fokus.features.task.TaskPackage
import java.util.zip.ZipEntry
class DataImporterService : BaseService() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
onImport(intent)
return START_REDELIVER_INTENT
}
private fun onImport(intent: Intent?) {
if (intent?.data == null)
terminateService(BROADCAST_IMPORT_FAILED)
contentResolver.openInputStream(intent?.data!!)?.use { inputStream ->
val archive = DataArchiver.parseInputStream(this, inputStream)
try {
archive.getInputStream(archive.getEntry(Metadata.FILE_NAME)).use { it ->
val metadata = Metadata.fromInputStream(it)
if (metadata.verify(Metadata.DATA_SUBJECT) &&
intent.action == ACTION_IMPORT_SUBJECT
) {
val subjectPackage = SubjectPackage(Subject())
for (entry: ZipEntry in archive.entries()) {
if (entry.name == Streamable.FILE_NAME_SUBJECT) {
archive.getInputStream(entry)?.use { inputStream ->
subjectPackage.subject = Subject.fromInputStream(inputStream)
}
} else if (entry.name == Streamable.FILE_NAME_SCHEDULE) {
archive.getInputStream(entry)?.use { inputStream ->
JsonDataStreamer.decodeFromJson(
inputStream,
Schedule::class.java
)
?.also { items ->
subjectPackage.schedules = items
}
}
}
}
sendResult(subjectPackage)
} else if (metadata.verify(Metadata.DATA_TASK) &&
intent.action == ACTION_IMPORT_TASK
) {
val taskPackage = TaskPackage(Task())
for (entry: ZipEntry in archive.entries()) {
if (entry.name == Streamable.FILE_NAME_TASK) {
archive.getInputStream(entry)?.use { inputStream ->
taskPackage.task = Task.fromInputStream(inputStream)
}
} else if (entry.name == Streamable.FILE_NAME_ATTACHMENT) {
archive.getInputStream(entry)?.use { inputStream ->
JsonDataStreamer.decodeFromJson(
inputStream,
Attachment::class.java
)
?.also { items ->
taskPackage.attachments = items
}
}
}
}
sendResult(taskPackage)
} else if (metadata.verify(Metadata.DATA_EVENT) &&
intent.action == ACTION_IMPORT_EVENT
) {
val eventPackage = EventPackage(Event())
for (entry: ZipEntry in archive.entries()) {
if (entry.name == Streamable.FILE_NAME_EVENT) {
archive.getInputStream(entry)?.use { inputStream ->
eventPackage.event = Event.fromInputStream(inputStream)
}
}
}
sendResult(eventPackage)
} else terminateService(BROADCAST_IMPORT_FAILED)
}
} catch (e: Exception) {
e.printStackTrace()
terminateService(BROADCAST_IMPORT_FAILED)
}
}
}
private fun <T : Parcelable> sendResult(t: T) {
LocalBroadcastManager.getInstance(this)
.sendBroadcast(Intent(ACTION_SERVICE_BROADCAST).apply {
putExtra(EXTRA_BROADCAST_STATUS, BROADCAST_IMPORT_COMPLETED)
putExtra(EXTRA_BROADCAST_DATA, t)
})
terminateService()
}
companion object {
const val ACTION_IMPORT_TASK = "action:import:task"
const val ACTION_IMPORT_SUBJECT = "action:import:subject"
const val ACTION_IMPORT_EVENT = "action:import:event"
const val BROADCAST_IMPORT_ONGOING = "broadcast:import:ongoing"
const val BROADCAST_IMPORT_COMPLETED = "broadcast:import:completed"
const val BROADCAST_IMPORT_FAILED = "broadcast:import:failed"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/FileImporterService.kt
================================================
package com.isaiahvonrundstedt.fokus.components.service
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.os.IBinder
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.isaiahvonrundstedt.fokus.components.extensions.android.getFileName
import com.isaiahvonrundstedt.fokus.components.interfaces.Streamable
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseService
import org.apache.commons.io.FileUtils
import java.io.File
class FileImporterService : BaseService() {
private lateinit var targetDirectory: File
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
targetDirectory = File(
getExternalFilesDir(null),
Streamable.DIRECTORY_ATTACHMENTS
)
intent.data?.let { onStartCopy(it, intent.getStringExtra(EXTRA_OBJECT_ID)!!) }
}
ACTION_CANCEL -> terminateService()
}
return START_REDELIVER_INTENT
}
private fun onStartCopy(uri: Uri, id: String) {
// Check if we have access to the storage
if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) {
terminateService(BROADCAST_IMPORT_FAILED)
return
}
sendLocalBroadcast(BROADCAST_IMPORT_ONGOING)
try {
contentResolver.openInputStream(uri)?.use {
// Use the attachment id to link the raw file
// to the database
// note: need to remove the target column in the database as it becomes redundant.
val fileName = uri.getFileName(this)
val extension = File(fileName).extension
val targetFile = File(targetDirectory, "${id}.${extension}")
FileUtils.copyToFile(it, targetFile)
broadcastResultThenTerminate(id, fileName)
}
} catch (e: Exception) {
e.printStackTrace()
terminateService(BROADCAST_IMPORT_FAILED)
}
}
/**
* Sends the required data back to the activity then
* terminates itself.
* @param id the attachment id that the file will be linked in
* @param name the original file name of the file
*/
private fun broadcastResultThenTerminate(id: String, name: String) {
LocalBroadcastManager.getInstance(this)
.sendBroadcast(Intent(ACTION_SERVICE_BROADCAST).apply {
// This function is only called when the import is
// completed and therefore we should just
// put a completed status in the broadcast
putExtra(EXTRA_BROADCAST_STATUS, BROADCAST_IMPORT_COMPLETED)
// Send the attachment id back to the calling activity
putExtra(EXTRA_BROADCAST_DATA, id)
// Send the file name back to the calling activity
putExtra(EXTRA_BROADCAST_EXTRA, name)
})
stopSelf()
}
companion object {
const val ACTION_START = "action:start"
const val ACTION_CANCEL = "action:cancel"
const val EXTRA_OBJECT_ID = "extra:id"
const val EXTRA_BROADCAST_EXTRA = "extra:broadcast:extra"
const val BROADCAST_IMPORT_ONGOING = "broadcast:attachment:ongoing"
const val BROADCAST_IMPORT_COMPLETED = "broadcast:attachment:completed"
const val BROADCAST_IMPORT_FAILED = "broadcast:attachment:failed"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/NotificationActionService.kt
================================================
package com.isaiahvonrundstedt.fokus.components.service
import android.app.IntentService
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.isaiahvonrundstedt.fokus.features.core.worker.ActionWorker
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseWorker
// This service function is to trigger the worker
// that will perform the appropriate action
// based on what the user tapped on the fokus
// Since PendingIntents can trigger Workers, this service
// acts like a middle man
class NotificationActionService : IntentService(SERVICE_NAME) {
private val manager by lazy {
getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
}
@Deprecated("Deprecated in Java")
override fun onHandleIntent(intent: Intent?) {
val taskID = intent?.getStringExtra(EXTRA_TASK_ID)
val isPersistent = intent?.getBooleanExtra(EXTRA_IS_PERSISTENT, false) ?: false
if (isPersistent)
manager?.cancel(taskID, BaseWorker.NOTIFICATION_ID_TASK)
else manager?.cancel(BaseWorker.NOTIFICATION_TAG_TASK, BaseWorker.NOTIFICATION_ID_TASK)
val data = Data.Builder()
data.putString(EXTRA_TASK_ID, taskID)
if (intent?.action == ACTION_FINISHED)
data.putString(EXTRA_ACTION, ACTION_FINISHED)
val workRequest = OneTimeWorkRequest.Builder(ActionWorker::class.java)
.setInputData(data.build())
.addTag(ActionWorker::class.java.simpleName)
.build()
WorkManager.getInstance(this).enqueue(workRequest)
}
companion object {
const val SERVICE_NAME = "service:notification:actions"
const val EXTRA_TASK_ID = "extra:taskID"
const val EXTRA_IS_PERSISTENT = "extra:isPersistent"
const val EXTRA_ACTION = "extra:action"
const val ACTION_FINISHED = "action:finished"
const val NOTIFICATION_ID_FINISH = 28
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/DataArchiver.kt
================================================
package com.isaiahvonrundstedt.fokus.components.utils
import android.content.Context
import android.net.Uri
import org.apache.commons.io.FileUtils
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
class DataArchiver private constructor(private var context: Context) {
private var uri: Uri? = null
private var destination: File? = null
private var items = mutableListOf<File>()
fun archive() {
if (destination != null) {
FileOutputStream(destination).use {
ZipOutputStream(BufferedOutputStream(it)).use { outputStream ->
setEntries(outputStream)
}
}
} else if (uri != null) {
context.contentResolver.openOutputStream(uri!!)?.use {
ZipOutputStream(BufferedOutputStream(it)).use { outputStream ->
setEntries(outputStream)
}
}
}
}
private fun setEntries(zip: ZipOutputStream) {
items.forEach {
if (it.isDirectory) {
zip.putNextEntry(ZipEntry("${it.name}/"))
for (source: File in it.listFiles()!!) {
onCopyToOutputStream(zip, "${it.name}/${source.name}", source)
}
} else onCopyToOutputStream(zip, it.name, it)
}
}
private fun onCopyToOutputStream(zip: ZipOutputStream, entryName: String, source: File) {
BufferedInputStream(FileInputStream(source), BUFFER).use { stream ->
zip.putNextEntry(ZipEntry(entryName))
stream.copyTo(zip, BUFFER)
}
}
class Create(context: Context) {
private var zip = DataArchiver(context)
fun addSource(items: List<File>): Create {
zip.items.addAll(items)
return this
}
fun addSource(file: File): Create {
zip.items.add(file)
return this
}
fun toDestination(uri: Uri): Create {
zip.uri = uri
return this
}
fun toDestination(file: File): Create {
zip.destination = file
return this
}
fun start() = zip.archive()
}
companion object {
private const val BUFFER = 4096
private const val FILE_TEMP_WORKING_FILE = "temp.fts"
fun parseInputStream(context: Context, stream: InputStream?): ZipFile {
val temp = File(context.cacheDir, FILE_TEMP_WORKING_FILE)
FileUtils.copyToFile(stream, temp)
return ZipFile(temp)
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/NotificationChannelManager.kt
================================================
package com.isaiahvonrundstedt.fokus.components.utils
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.content.Context
import android.media.AudioAttributes
import android.net.Uri
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.app.NotificationManagerCompat
import com.isaiahvonrundstedt.fokus.R
class NotificationChannelManager(private var context: Context) {
private val manager = NotificationManagerCompat.from(context)
@RequiresApi(26)
fun register(
id: String,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
groupID: String? = null
) {
if (groupID != null && manager.getNotificationChannelGroup(groupID) == null) {
manager.createNotificationChannelGroup(createGroup(groupID))
}
manager.createNotificationChannel(createChannel(id, importance, groupID))
}
@RequiresApi(26)
private fun createChannel(
id: String,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
groupID: String?
): NotificationChannel {
return NotificationChannel(id, context.getString(getChannelNameRes(id)), importance).apply {
setSound(notificationSoundUri, attributes)
group = groupID
}
}
@RequiresApi(26)
private fun createGroup(id: String): NotificationChannelGroup {
return NotificationChannelGroup(id, context.getString(getGroupNameRes(id)))
}
@StringRes
private fun getChannelNameRes(id: String): Int {
return when (id) {
CHANNEL_ID_TASK -> R.string.notification_channel_task_reminders
CHANNEL_ID_EVENT -> R.string.notification_channel_event_reminders
CHANNEL_ID_CLASS -> R.string.notification_channel_class_reminders
CHANNEL_ID_GENERIC -> R.string.notification_channel_general
else -> 0
}
}
@StringRes
private fun getGroupNameRes(id: String): Int {
return when (id) {
CHANNEL_GROUP_ID_REMINDERS -> R.string.notification_channel_group_reminders
else -> 0
}
}
private val attributes: AudioAttributes
get() = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
private val notificationSoundUri: Uri
get() = Uri.parse(PreferenceManager.DEFAULT_SOUND)
companion object {
const val CHANNEL_ID_TASK = "channel:task"
const val CHANNEL_ID_EVENT = "channel:event"
const val CHANNEL_ID_CLASS = "channel:class"
const val CHANNEL_ID_GENERIC = "channel:generic"
const val CHANNEL_GROUP_ID_REMINDERS = "group:reminders"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/PermissionManager.kt
================================================
package com.isaiahvonrundstedt.fokus.components.utils
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
class PermissionManager(var context: Context) {
val readStorageGranted: Boolean
get() {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
else true
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/PreferenceManager.kt
================================================
package com.isaiahvonrundstedt.fokus.components.utils
import android.content.ContentResolver
import android.content.Context
import androidx.preference.PreferenceManager
import com.isaiahvonrundstedt.fokus.BuildConfig
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.enums.SortDirection
import com.isaiahvonrundstedt.fokus.database.converter.DateTimeConverter
import com.isaiahvonrundstedt.fokus.features.subject.SubjectViewModel
import com.isaiahvonrundstedt.fokus.features.task.TaskViewModel
import java.time.LocalTime
import java.time.ZonedDateTime
class PreferenceManager(private val context: Context) {
enum class Theme {
SYSTEM, DARK, LIGHT;
companion object {
fun parse(s: String?): Theme {
return when (s) {
DARK.toString() -> DARK
LIGHT.toString() -> LIGHT
else -> SYSTEM
}
}
}
}
var theme: Theme
get() = Theme.parse(
sharedPreference.getString(
PREFERENCE_THEME,
Theme.SYSTEM.toString()
)
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_THEME, value.toString())
apply()
}
}
var previousBackupDate: ZonedDateTime?
get() = DateTimeConverter.toZonedDateTime(
sharedPreference.getString(PREFERENCE_BACKUP, null)
)
set(value) {
sharedPreference.edit().run {
putString(
PREFERENCE_BACKUP,
DateTimeConverter.fromZonedDateTime(value)
)
apply()
}
}
var reminderTime: LocalTime?
get() = DateTimeConverter.toLocalTime(
sharedPreference.getString(PREFERENCE_REMINDER_TIME, "08:30")
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_REMINDER_TIME, DateTimeConverter.fromLocalTime(value))
apply()
}
}
var taskConstraint: TaskViewModel.Constraint
get() = TaskViewModel.Constraint.parse(
sharedPreference.getString(
PREFERENCE_TASK_FILTER_OPTION,
TaskViewModel.Constraint.ALL.toString()
) ?: TaskViewModel.Constraint.ALL.toString()
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_TASK_FILTER_OPTION, value.toString())
apply()
}
}
var tasksSort: TaskViewModel.Sort
get() = TaskViewModel.Sort.parse(
sharedPreference.getString(
PREFERENCE_TASK_SORT_OPTION,
TaskViewModel.Sort.NAME.toString()
) ?: TaskViewModel.Sort.NAME.toString()
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_TASK_SORT_OPTION, value.toString())
apply()
}
}
var tasksSortDirection: SortDirection
get() = SortDirection.parse(
sharedPreference.getString(
PREFERENCE_TASK_SORT_DIRECTION,
SortDirection.ASCENDING.toString()
)
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_TASK_SORT_DIRECTION, value.toString())
apply()
}
}
var subjectConstraint: SubjectViewModel.Constraint
get() = SubjectViewModel.Constraint.parse(
sharedPreference.getString(
PREFERENCE_SUBJECT_FILTER_OPTION,
SubjectViewModel.Constraint.ALL.toString()
) ?: SubjectViewModel.Constraint.ALL.toString()
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_SUBJECT_FILTER_OPTION, value.toString())
apply()
}
}
var subjectSort: SubjectViewModel.Sort
get() = SubjectViewModel.Sort.parse(
sharedPreference.getString(
PREFERENCE_SUBJECT_SORT_OPTION,
SubjectViewModel.Sort.CODE.toString()
) ?: SubjectViewModel.Sort.CODE.toString()
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_SUBJECT_SORT_OPTION, value.toString())
apply()
}
}
var subjectSortDirection: SortDirection
get() = SortDirection.parse(
sharedPreference.getString(
PREFERENCE_SUBJECT_SORT_DIRECTION,
SortDirection.ASCENDING.toString()
) ?: SubjectViewModel.Sort.CODE.toString()
)
set(value) {
sharedPreference.edit().run {
putString(PREFERENCE_SUBJECT_SORT_DIRECTION, value.toString())
apply()
}
}
val confetti: Boolean
get() = sharedPreference.getBoolean(PREFERENCE_CONFETTI, true)
val sounds: Boolean
get() = sharedPreference.getBoolean(PREFERENCE_SOUND, true)
val taskReminder: Boolean
get() = sharedPreference.getBoolean(PREFERENCE_TASK_NOTIFICATION, true)
val eventReminder: Boolean
get() = sharedPreference.getBoolean(PREFERENCE_EVENT_NOTIFICATION, true)
val subjectReminder: Boolean
get() = sharedPreference.getBoolean(PREFERENCE_COURSE_NOTIFICATION, true)
val useExternalBrowser: Boolean
get() = sharedPreference.getBoolean(PREFERENCE_USE_EXTERNAL_BROWSER, false)
val allowWeekNumbers: Boolean
get() = sharedPreference.getBoolean(PREFERENCE_ALLOW_WEEK_NUMBERS, false)
val reminderFrequency: String
get() = sharedPreference.getString(
PREFERENCE_REMINDER_FREQUENCY,
DURATION_EVERYDAY
) ?: DURATION_EVERYDAY
val taskReminderInterval: String
get() = sharedPreference.getString(
PREFERENCE_TASK_NOTIFICATION_INTERVAL,
TASK_REMINDER_INTERVAL_3_HOURS
) ?: TASK_REMINDER_INTERVAL_3_HOURS
val eventReminderInterval: String
get() = sharedPreference.getString(
PREFERENCE_EVENT_NOTIFICATION_INTERVAL,
EVENT_REMINDER_INTERVAL_30_MINUTES
) ?: EVENT_REMINDER_INTERVAL_30_MINUTES
val subjectReminderInterval: String
get() = sharedPreference.getString(
PREFERENCE_COURSE_NOTIFICATION_INTERVAL,
SUBJECT_REMINDER_INTERVAL_30_MINUTES
) ?: SUBJECT_REMINDER_INTERVAL_30_MINUTES
/* User-Defined Settings */
var noConfirmImport: Boolean
get() = sharedPreference.getBoolean(
PREFERENCE_NO_CONFIRM_IMPORT,
false
)
set(value) {
sharedPreference.edit().run {
putBoolean(PREFERENCE_NO_CONFIRM_IMPORT, value)
commit()
}
}
private val sharedPreference by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
companion object {
const val DEFAULT_SOUND =
"${ContentResolver.SCHEME_ANDROID_RESOURCE}://${BuildConfig.APPLICATION_ID}/${R.raw.fokus}"
const val DURATION_EVERYDAY = "EVERYDAY"
const val DURATION_WEEKENDS = "WEEKENDS"
const val TASK_REMINDER_INTERVAL_1_HOUR = "1"
const val TASK_REMINDER_INTERVAL_3_HOURS = "3"
const val TASK_REMINDER_INTERVAL_24_HOURS = "24"
const val EVENT_REMINDER_INTERVAL_15_MINUTES = "15"
const val EVENT_REMINDER_INTERVAL_30_MINUTES = "30"
const val EVENT_REMINDER_INTERVAL_60_MINUTES = "60"
const val SUBJECT_REMINDER_INTERVAL_5_MINUTES = "5"
const val SUBJECT_REMINDER_INTERVAL_15_MINUTES = "15"
const val SUBJECT_REMINDER_INTERVAL_30_MINUTES = "30"
// Preferences that are visible in the SettingsActivity
const val PREFERENCE_THEME = "KEY_THEME"
const val PREFERENCE_CONFETTI = "KEY_CONFETTI"
const val PREFERENCE_SOUND = "KEY_SOUND"
const val PREFERENCE_REMINDER_FREQUENCY = "KEY_REMINDER_FREQUENCY"
const val PREFERENCE_REMINDER_TIME = "KEY_REMINDER_TIME"
const val PREFERENCE_TASK_NOTIFICATION = "KEY_TASK_NOTIFICATION"
const val PREFERENCE_TASK_NOTIFICATION_INTERVAL = "KEY_TASK_NOTIFICATION_INTERVAL"
const val PREFERENCE_EVENT_NOTIFICATION = "KEY_EVENT_NOTIFICATION"
const val PREFERENCE_EVENT_NOTIFICATION_INTERVAL = "KEY_EVENT_NOTIFICATION_INTERVAL"
const val PREFERENCE_COURSE_NOTIFICATION = "KEY_COURSE_NOTIFICATION"
const val PREFERENCE_COURSE_NOTIFICATION_INTERVAL = "KEY_COURSE_NOTIFICATION_INTERVAL"
const val PREFERENCE_SYSTEM_NOTIFICATION = "KEY_SYSTEM_NOTIFICATION"
const val PREFERENCE_ALLOW_WEEK_NUMBERS = "KEY_ALLOW_WEEK_NUMBERS"
const val PREFERENCE_BACKUP_RESTORE = "KEY_BACKUP_RESTORE"
const val PREFERENCE_BACKUP = "KEY_BACKUP"
const val PREFERENCE_RESTORE = "KEY_RESTORE"
const val PREFERENCE_USE_EXTERNAL_BROWSER = "KEY_USE_EXTERNAL_BROWSER"
const val PREFERENCE_BATTERY_OPTIMIZATION = "KEY_BATTERY_OPTIMIZATION"
// Preferences that are visible in AboutActivity
const val PREFERENCE_REPORT_ISSUE = "KEY_REPORT_ISSUE"
const val PREFERENCE_TRANSLATE = "KEY_TRANSLATE"
const val PREFERENCE_NOTICES = "KEY_NOTICES"
const val PREFERENCE_VERSION = "KEY_VERSION"
// Preferences that are visible in NoticesActivity
const val PREFERENCE_LIBRARIES = "KEY_LIBRARIES"
const val PREFERENCE_NOTIFICATION_SOUND = "KEY_NOTIFICATION_SOUND"
const val PREFERENCE_LAUNCHER_ICON = "KEY_LAUNCHER_ICON"
const val PREFERENCE_UI_ICONS = "KEY_UI_ICONS"
// Preferences that are not visible in anywhere and are only
// used for remembering user choices in the UI
const val PREFERENCE_NO_CONFIRM_IMPORT = "KEY_NO_CONFIRM_IMPORT"
const val PREFERENCE_TASK_FILTER_OPTION = "KEY_TASKS_FILTER_OPTION"
const val PREFERENCE_TASK_SORT_OPTION = "KEY_TASK_SORT_OPTION"
const val PREFERENCE_TASK_SORT_DIRECTION = "KEY_TASK_SORT_DIRECTION"
const val PREFERENCE_SUBJECT_FILTER_OPTION = "KEY_SUBJECT_FILTER_OPTION"
const val PREFERENCE_SUBJECT_SORT_OPTION = "KEY_SUBJECT_SORT_OPTION"
const val PREFERENCE_SUBJECT_SORT_DIRECTION = "KEY_SUBJECT_SORT_DIRECTION"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/views/RadioButtonCompat.kt
================================================
package com.isaiahvonrundstedt.fokus.components.views
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatRadioButton
import com.isaiahvonrundstedt.fokus.R
open class RadioButtonCompat @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = R.attr.radioButtonStyle
) : AppCompatRadioButton(context, attributeSet, defStyleAttr) {
init {
val typedArray = context.obtainStyledAttributes(
attributeSet,
R.styleable.RadioButtonCompat,
defStyleAttr,
0
)
try {
val textAppearance = typedArray.getResourceId(
R.styleable.RadioButtonCompat_textAppearanceCompat,
R.style.Fokus_TextAppearance_Body_Medium
)
setTextAppearanceCompat(textAppearance)
} catch (e: Exception) {
}
typedArray.recycle()
}
@Suppress("DEPRECATION")
fun setTextAppearanceCompat(@StyleRes resId: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
this.setTextAppearance(resId)
else this.setTextAppearance(context, resId)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setPaddingRelative(16, 0, 16, 0)
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/views/ReactiveTextColorSwitch.kt
================================================
package com.isaiahvonrundstedt.fokus.components.views
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import com.google.android.material.switchmaterial.SwitchMaterial
import com.isaiahvonrundstedt.fokus.R
class ReactiveTextColorSwitch @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = R.attr.switchStyle
) : SwitchMaterial(context, attributeSet, defStyleAttr) {
init {
setSwitchTextAppearance(context, R.style.Fokus_TextAppearance_Label_Large)
}
override fun setChecked(checked: Boolean) {
super.setChecked(checked)
if (checked)
setTextColorRes(R.color.color_primary_text)
else setTextColorRes(R.color.color_secondary_text)
}
private fun setTextColorRes(@ColorRes resId: Int) {
setTextColor(ContextCompat.getColor(context, resId))
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/components/views/TwoLineRadioButton.kt
================================================
package com.isaiahvonrundstedt.fokus.components.views
import android.content.Context
import android.os.Build
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.TextAppearanceSpan
import android.util.AttributeSet
import androidx.core.content.ContextCompat
import com.isaiahvonrundstedt.fokus.R
class TwoLineRadioButton @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = R.attr.radioButtonStyle
) : RadioButtonCompat(context, attributeSet, defStyleAttr) {
private val titleSpan: TextAppearanceSpan
private val subtitleSpan: TextAppearanceSpan
private var titleTextColorSpan: ForegroundColorSpan
private var subtitleTextColorSpan: ForegroundColorSpan
var title: String = ""
set(value) {
field = value
renderText()
}
var subtitle: String? = null
set(value) {
field = value
renderText()
}
var titleTextColor: Int = ContextCompat.getColor(context, R.color.color_secondary_text)
set(value) {
field = value
renderText()
}
var subtitleTextColor: Int = ContextCompat.getColor(context, R.color.color_secondary_text)
set(value) {
field = value
renderText()
}
init {
val typedArray = context.obtainStyledAttributes(
attributeSet,
R.styleable.TwoLineRadioButton,
defStyleAttr, 0
)
try {
title = typedArray.getString(R.styleable.TwoLineRadioButton_titleText) ?: ""
subtitle = typedArray.getString(R.styleable.TwoLineRadioButton_subtitleText)
titleTextColor = typedArray.getColor(
R.styleable.TwoLineRadioButton_titleTextColor,
titleTextColor
)
subtitleTextColor = typedArray.getColor(
R.styleable.TwoLineRadioButton_subtitleTextColor,
subtitleTextColor
)
val titleTextAppearance = typedArray.getResourceId(
R.styleable.TwoLineRadioButton_titleTextAppearance,
R.style.TextAppearance_AppCompat_Body1
)
titleSpan = TextAppearanceSpan(context, titleTextAppearance)
titleTextColorSpan = ForegroundColorSpan(titleTextColor)
val subtitleTextAppearance = typedArray.getResourceId(
R.styleable.TwoLineRadioButton_subtitleTextAppearance,
R.style.TextAppearance_AppCompat_Caption
)
subtitleSpan = TextAppearanceSpan(context, subtitleTextAppearance)
subtitleTextColorSpan = ForegroundColorSpan(subtitleTextColor)
} finally {
typedArray.recycle()
}
renderText()
}
private fun renderText() {
titleTextColorSpan = ForegroundColorSpan(titleTextColor)
subtitleTextColorSpan = ForegroundColorSpan(subtitleTextColor)
val textToRender = if (subtitle.isNullOrEmpty()) title
else "$title\n$subtitle"
val builder = SpannableStringBuilder(textToRender).apply {
setSpan(
titleSpan, 0, title.length,
SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
setSpan(
titleTextColorSpan, 0, title.length,
SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
subtitle?.also {
if (it.isNotEmpty()) {
setSpan(
subtitleSpan, title.length,
title.length + it.length + 1, SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
)
setSpan(
subtitleTextColorSpan, title.length,
title.length + it.length + 1, SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
}
text = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P)
builder.toString()
else builder
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/AppDatabase.kt
================================================
package com.isaiahvonrundstedt.fokus.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.isaiahvonrundstedt.fokus.database.converter.ColorConverter
import com.isaiahvonrundstedt.fokus.database.converter.DateTimeConverter
import com.isaiahvonrundstedt.fokus.database.dao.*
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.log.Log
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.task.Task
import java.time.ZonedDateTime
@Database(
entities = [Subject::class, Task::class, Attachment::class, Log::class, Event::class, Schedule::class],
version = AppDatabase.DATABASE_VERSION,
exportSchema = true
)
@TypeConverters(DateTimeConverter::class, ColorConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun subjects(): SubjectDAO
abstract fun schedules(): ScheduleDAO
abstract fun tasks(): TaskDAO
abstract fun attachments(): AttachmentDAO
abstract fun events(): EventDAO
abstract fun logs(): LogDAO
companion object {
const val DATABASE_VERSION = 8
private const val DATABASE_NAME = "fokus"
private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
if (instance == null) {
synchronized(AppDatabase::class) {
instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java, DATABASE_NAME
)
.addMigrations(*migrations)
.build()
}
}
return instance!!
}
private var migration_4_6 = object : Migration(4, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
with(database) {
// schedules
execSQL("CREATE TABLE IF NOT EXISTS `schedules_new` (`scheduleID` TEXT NOT NULL, `daysOfWeek` INTEGER NOT NULL, `weeksOfMonth` INTEGER NOT NULL DEFAULT 15,`startTime` TEXT, `endTime` TEXT, `subject` TEXT, PRIMARY KEY(`scheduleID`), FOREIGN KEY(`subject`) REFERENCES `subjects`(`subjectID`) ON UPDATE NO ACTION ON DELETE CASCADE )")
execSQL("ALTER TABLE schedules RENAME TO schedules_old")
execSQL("ALTER TABLE schedules_new RENAME TO schedules")
execSQL("INSERT INTO schedules (`scheduleID`, `daysOfWeek`, `startTime`, `endTime`, `subject`) SELECT * FROM `schedules_old`")
execSQL("DROP TABLE schedules_old")
execSQL("ALTER TABLE tasks ADD COLUMN `isTaskArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE events ADD COLUMN `isEventArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE subjects ADD COLUMN `isSubjectArchived` INTEGER NOT NULL DEFAULT 0")
}
}
}
private var migration_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
with(database) {
execSQL("ALTER TABLE tasks ADD COLUMN `isTaskArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE events ADD COLUMN `isEventArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE subjects ADD COLUMN `isSubjectArchived` INTEGER NOT NULL DEFAULT 0")
}
}
}
private var migration_4_7 = object : Migration(4, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
with(database) {
// schedules
execSQL("CREATE TABLE IF NOT EXISTS `schedules_new` (`scheduleID` TEXT NOT NULL, `daysOfWeek` INTEGER NOT NULL, `weeksOfMonth` INTEGER NOT NULL DEFAULT 15,`startTime` TEXT, `endTime` TEXT, `subject` TEXT, PRIMARY KEY(`scheduleID`), FOREIGN KEY(`subject`) REFERENCES `subjects`(`subjectID`) ON UPDATE NO ACTION ON DELETE CASCADE )")
execSQL("ALTER TABLE schedules RENAME TO schedules_old")
execSQL("ALTER TABLE schedules_new RENAME TO schedules")
execSQL("INSERT INTO schedules (`scheduleID`, `daysOfWeek`, `startTime`, `endTime`, `subject`) SELECT * FROM `schedules_old`")
execSQL("DROP TABLE schedules_old")
execSQL("ALTER TABLE tasks ADD COLUMN `isTaskArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE events ADD COLUMN `isEventArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE subjects ADD COLUMN `isSubjectArchived` INTEGER NOT NULL DEFAULT 0")
}
var cursor = database.query("SELECT * FROM tasks")
cursor.moveToFirst()
val taskList = arrayListOf<Task>()
while (cursor.moveToNext()) {
val task = Task()
task.taskID = cursor.getString(cursor.getColumnIndex("taskID"))
task.name = cursor.getString(cursor.getColumnIndex("name"))
task.notes = cursor.getString(cursor.getColumnIndex("notes"))
task.subject = cursor.getString(cursor.getColumnIndex("subject"))
task.isFinished = cursor.getInt(cursor.getColumnIndex("isFinished")) > 0
task.isImportant = cursor.getInt(cursor.getColumnIndex("isImportant")) > 0
task.isTaskArchived = cursor.getInt(cursor.getColumnIndex("isTaskArchived")) > 0
task.dueDate =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dueDate")))
task.dateAdded =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dateAdded")))
?: ZonedDateTime.now()
taskList.add(task)
}
cursor.close()
database.execSQL("DELETE FROM tasks")
taskList.forEach {
val statement =
database.compileStatement("INSERT INTO tasks VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
with(statement) {
bindString(1, it.taskID)
bindString(2, it.name)
bindString(3, it.notes)
bindString(4, it.subject)
bindLong(5, if (it.isImportant) 1 else 0)
bindString(6, DateTimeConverter.fromZonedDateTime(it.dateAdded))
bindString(7, DateTimeConverter.fromZonedDateTime(it.dueDate))
bindLong(8, if (it.isFinished) 1 else 0)
bindLong(9, if (it.isTaskArchived) 1 else 0)
}
statement.executeInsert()
}
taskList.clear()
cursor = database.query("SELECT * FROM events")
cursor.moveToFirst()
val eventList = arrayListOf<Event>()
while (cursor.moveToNext()) {
val event = Event()
event.eventID = cursor.getString(cursor.getColumnIndex("eventID"))
event.name = cursor.getString(cursor.getColumnIndex("name"))
event.notes = cursor.getString(cursor.getColumnIndex("notes"))
event.schedule =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("schedule")))
event.location = cursor.getString(cursor.getColumnIndex("location"))
event.isImportant = cursor.getInt(cursor.getColumnIndex("isImportant")) > 0
event.isEventArchived =
cursor.getInt(cursor.getColumnIndex("isEventArchived")) > 0
event.dateAdded =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dateAdded")))
?: ZonedDateTime.now()
}
cursor.close()
database.execSQL("DELETE FROM events")
eventList.forEach {
val statement =
database.compileStatement("INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
with(statement) {
bindString(1, it.eventID)
bindString(2, it.name)
bindString(3, it.location)
bindString(4, it.subject)
bindLong(5, if (it.isImportant) 1 else 0)
bindLong(6, if (it.isEventArchived) 1 else 0)
bindString(7, DateTimeConverter.fromZonedDateTime(it.schedule))
bindString(8, DateTimeConverter.fromZonedDateTime(it.dateAdded))
}
statement.executeInsert()
}
eventList.clear()
}
}
private var migration_5_7 = object : Migration(5, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
with(database) {
execSQL("ALTER TABLE tasks ADD COLUMN `isTaskArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE events ADD COLUMN `isEventArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE subjects ADD COLUMN `isSubjectArchived` INTEGER NOT NULL DEFAULT 0")
execSQL("CREATE TABLE IF NOT EXISTS `events_new` (`eventID` TEXT NOT NULL, `name` TEXT, `notes` TEXT, `location` TEXT, `subject` TEXT, `isImportant` INTEGER NOT NULL, `isEventArchived` INTEGER NOT NULL, `schedule` TEXT, `dateAdded` TEXT, PRIMARY KEY(`eventID`), FOREIGN KEY(`subject`) REFERENCES `subjects`(`subjectID`) ON UPDATE NO ACTION ON DELETE SET NULL )")
execSQL("INSERT INTO `events_new` SELECT * FROM `events`")
execSQL("DROP TABLE `events`")
execSQL("ALTER TABLE `events_new` RENAME TO `events`")
}
var cursor = database.query("SELECT * FROM tasks")
cursor.moveToFirst()
val taskList = arrayListOf<Task>()
while (cursor.moveToNext()) {
val task = Task()
task.taskID = cursor.getString(cursor.getColumnIndex("taskID"))
task.name = cursor.getString(cursor.getColumnIndex("name"))
task.notes = cursor.getString(cursor.getColumnIndex("notes"))
task.subject = cursor.getString(cursor.getColumnIndex("subject"))
task.isFinished = cursor.getInt(cursor.getColumnIndex("isFinished")) > 0
task.isImportant = cursor.getInt(cursor.getColumnIndex("isImportant")) > 0
task.isTaskArchived = cursor.getInt(cursor.getColumnIndex("isTaskArchived")) > 0
task.dueDate =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dueDate")))
task.dateAdded =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dateAdded")))
?: ZonedDateTime.now()
taskList.add(task)
}
cursor.close()
database.execSQL("DELETE FROM tasks")
taskList.forEach {
val statement =
database.compileStatement("INSERT INTO tasks VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
with(statement) {
bindString(1, it.taskID)
bindString(2, it.name)
bindString(3, it.notes)
bindString(4, it.subject)
bindLong(5, if (it.isImportant) 1 else 0)
bindString(6, DateTimeConverter.fromZonedDateTime(it.dateAdded))
bindString(7, DateTimeConverter.fromZonedDateTime(it.dueDate))
bindLong(8, if (it.isFinished) 1 else 0)
bindLong(9, if (it.isTaskArchived) 1 else 0)
}
statement.executeInsert()
}
taskList.clear()
cursor = database.query("SELECT * FROM events")
cursor.moveToFirst()
val eventList = arrayListOf<Event>()
while (cursor.moveToNext()) {
val event = Event()
event.eventID = cursor.getString(cursor.getColumnIndex("eventID"))
event.name = cursor.getString(cursor.getColumnIndex("name"))
event.notes = cursor.getString(cursor.getColumnIndex("notes"))
event.schedule =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("schedule")))
event.location = cursor.getString(cursor.getColumnIndex("location"))
event.isImportant = cursor.getInt(cursor.getColumnIndex("isImportant")) > 0
event.isEventArchived =
cursor.getInt(cursor.getColumnIndex("isEventArchived")) > 0
event.dateAdded =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dateAdded")))
?: ZonedDateTime.now()
}
cursor.close()
database.execSQL("DELETE FROM events")
eventList.forEach {
val statement =
database.compileStatement("INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
with(statement) {
bindString(1, it.eventID)
bindString(2, it.name)
bindString(3, it.notes)
bindString(4, it.location)
bindString(5, it.subject)
bindLong(6, if (it.isImportant) 1 else 0)
bindLong(7, if (it.isEventArchived) 1 else 0)
bindString(8, DateTimeConverter.fromZonedDateTime(it.schedule))
bindString(9, DateTimeConverter.fromZonedDateTime(it.dateAdded))
}
statement.executeInsert()
}
eventList.clear()
}
}
private var migration_6_7 = object : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
with(database) {
execSQL("CREATE TABLE IF NOT EXISTS `events_new` (`eventID` TEXT NOT NULL, `name` TEXT, `notes` TEXT, `location` TEXT, `subject` TEXT, `isImportant` INTEGER NOT NULL, `isEventArchived` INTEGER NOT NULL, `schedule` TEXT, `dateAdded` TEXT, PRIMARY KEY(`eventID`), FOREIGN KEY(`subject`) REFERENCES `subjects`(`subjectID`) ON UPDATE NO ACTION ON DELETE SET NULL )")
execSQL("INSERT INTO `events_new` SELECT * FROM `events`")
execSQL("DROP TABLE `events`")
execSQL("ALTER TABLE `events_new` RENAME TO `events`")
}
var cursor = database.query("SELECT * FROM tasks")
cursor.moveToFirst()
val taskList = arrayListOf<Task>()
while (cursor.moveToNext()) {
val task = Task()
task.taskID = cursor.getString(cursor.getColumnIndex("taskID"))
task.name = cursor.getString(cursor.getColumnIndex("name"))
task.notes = cursor.getString(cursor.getColumnIndex("notes"))
task.subject = cursor.getString(cursor.getColumnIndex("subject"))
task.isFinished = cursor.getInt(cursor.getColumnIndex("isFinished")) > 0
task.isImportant = cursor.getInt(cursor.getColumnIndex("isImportant")) > 0
task.isTaskArchived = cursor.getInt(cursor.getColumnIndex("isTaskArchived")) > 0
task.dueDate =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dueDate")))
task.dateAdded =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dateAdded")))
?: ZonedDateTime.now()
taskList.add(task)
}
cursor.close()
database.execSQL("DELETE FROM tasks")
taskList.forEach {
val statement =
database.compileStatement("INSERT INTO tasks VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
with(statement) {
bindString(1, it.taskID)
bindString(2, it.name)
bindString(3, it.notes)
bindString(4, it.subject ?: "")
bindLong(5, if (it.isImportant) 1 else 0)
bindString(6, DateTimeConverter.fromZonedDateTime(it.dateAdded))
bindString(7, DateTimeConverter.fromZonedDateTime(it.dueDate))
bindLong(8, if (it.isFinished) 1 else 0)
bindLong(9, if (it.isTaskArchived) 1 else 0)
}
statement.executeInsert()
}
taskList.clear()
cursor = database.query("SELECT * FROM events")
cursor.moveToFirst()
val eventList = arrayListOf<Event>()
while (cursor.moveToNext()) {
val event = Event()
event.eventID = cursor.getString(cursor.getColumnIndex("eventID"))
event.name = cursor.getString(cursor.getColumnIndex("name"))
event.notes = cursor.getString(cursor.getColumnIndex("notes"))
event.schedule =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("schedule")))
event.location = cursor.getString(cursor.getColumnIndex("location"))
event.isImportant = cursor.getInt(cursor.getColumnIndex("isImportant")) > 0
event.isEventArchived =
cursor.getInt(cursor.getColumnIndex("isEventArchived")) > 0
event.dateAdded =
DateTimeConverter.toZonedDateTime(cursor.getString(cursor.getColumnIndex("dateAdded")))
?: ZonedDateTime.now()
event.subject = cursor.getString(cursor.getColumnIndex("subject"))
}
cursor.close()
database.execSQL("DELETE FROM events")
eventList.forEach {
val statement =
database.compileStatement("INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
with(statement) {
bindString(1, it.eventID)
bindString(2, it.name)
bindString(3, it.notes)
bindString(4, it.location)
bindString(5, it.subject ?: "")
bindLong(6, if (it.isImportant) 1 else 0)
bindLong(7, if (it.isEventArchived) 1 else 0)
bindString(8, DateTimeConverter.fromZonedDateTime(it.schedule))
bindString(9, DateTimeConverter.fromZonedDateTime(it.dateAdded))
}
statement.executeInsert()
}
eventList.clear()
}
}
private var migration_7_8 = object : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
with(database) {
execSQL("ALTER TABLE subjects ADD COLUMN `instructor` TEXT")
execSQL("ALTER TABLE schedules ADD COLUMN `classroom` TEXT")
}
}
}
private val migrations =
arrayOf(migration_4_6, migration_5_6, migration_4_7, migration_5_7, migration_6_7, migration_7_8)
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/converter/ColorConverter.kt
================================================
package com.isaiahvonrundstedt.fokus.database.converter
import androidx.room.TypeConverter
import com.isaiahvonrundstedt.fokus.features.subject.Subject
class ColorConverter private constructor() {
companion object {
@JvmStatic
@TypeConverter
fun toColor(actualColor: Int): Subject.Tag? {
return Subject.Tag.convertColorToTag(actualColor)
}
@JvmStatic
@TypeConverter
fun fromColor(tag: Subject.Tag): Int {
return tag.color
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/converter/DateTimeConverter.kt
================================================
package com.isaiahvonrundstedt.fokus.database.converter
import android.content.Context
import android.text.format.DateFormat
import androidx.room.TypeConverter
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class DateTimeConverter private constructor() {
companion object {
private const val FORMAT_TIME_12_HOUR = "h:mm a"
private const val FORMAT_TIME_24_HOUR = "H:mm"
private const val FORMAT_DATE_TIME_12_HOUR_SHORT = "M-d, h:mm a"
private const val FORMAT_DATE_TIME_24_HOUR_SHORT = "M-d, H:mm"
private const val FORMAT_DATE_TIME_WITH_YEAR_12_HOUR_SHORT = "M-d-yy, h:mm a"
private const val FORMAT_DATE_TIME_WITH_YEAR_24_HOUR_SHORT = "M-d-yy, H:mm"
private const val FORMAT_DATE_TIME_12_HOUR = "MMMM d, h:mm a"
private const val FORMAT_DATE_TIME_24_HOUR = "MMMM d, H:mm"
private const val FORMAT_DATE_TIME_WITH_YEAR_12_HOUR = "MM d yyyy, h:mm a"
private const val FORMAT_DATE_TIME_WITH_YEAR_24_HOUR = "Mm d yyyy, H:mm"
fun getTimeFormatter(context: Context): DateTimeFormatter {
val pattern = if (DateFormat.is24HourFormat(context))
FORMAT_TIME_24_HOUR
else FORMAT_TIME_12_HOUR
return DateTimeFormatter.ofPattern(pattern)
}
fun getDateTimeFormatter(
context: Context,
isShort: Boolean = false,
withYear: Boolean = false
): DateTimeFormatter {
val is24Hour: Boolean = DateFormat.is24HourFormat(context)
val pattern = if (isShort) {
if (withYear) {
if (is24Hour)
FORMAT_DATE_TIME_WITH_YEAR_24_HOUR_SHORT
else FORMAT_DATE_TIME_WITH_YEAR_12_HOUR_SHORT
} else {
if (is24Hour)
FORMAT_DATE_TIME_24_HOUR_SHORT
else FORMAT_DATE_TIME_12_HOUR_SHORT
}
} else {
if (withYear) {
if (is24Hour)
FORMAT_DATE_TIME_WITH_YEAR_24_HOUR
else FORMAT_DATE_TIME_WITH_YEAR_12_HOUR
} else {
if (is24Hour)
FORMAT_DATE_TIME_24_HOUR
else FORMAT_DATE_TIME_12_HOUR
}
}
return DateTimeFormatter.ofPattern(pattern)
}
@JvmStatic
@TypeConverter
fun fromLocalDate(localDate: LocalDate?): String? {
return if (localDate != null)
DateTimeFormatter.ISO_LOCAL_DATE.format(localDate)
else null
}
@JvmStatic
@TypeConverter
fun toLocalDate(date: String?): LocalDate? {
return if (date.isNullOrEmpty()) null
else LocalDate.parse(date)
}
@JvmStatic
@TypeConverter
fun toZonedDateTime(time: String?): ZonedDateTime? {
return if (time.isNullOrEmpty()) null
else ZonedDateTime.parse(time)
}
@JvmStatic
@TypeConverter
fun fromZonedDateTime(zonedDateTime: ZonedDateTime?): String? {
return if (zonedDateTime != null)
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zonedDateTime)
else null
}
@JvmStatic
@TypeConverter
fun toLocalTime(time: String?): LocalTime? {
return if (time.isNullOrEmpty()) null
else LocalTime.parse(time)
}
@JvmStatic
@TypeConverter
fun fromLocalTime(time: LocalTime?): String? {
return DateTimeFormatter.ISO_LOCAL_TIME.format(time)
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/AttachmentDAO.kt
================================================
package com.isaiahvonrundstedt.fokus.database.dao
import androidx.room.*
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
@Dao
interface AttachmentDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(attachment: Attachment)
@Delete
fun remove(attachment: Attachment)
@Update
fun update(attachment: Attachment)
@Query("SELECT * FROM attachments")
suspend fun fetch(): List<Attachment>
@Query("DELETE FROM attachments WHERE task = :id")
fun removeUsingTaskID(id: String)
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/EventDAO.kt
================================================
package com.isaiahvonrundstedt.fokus.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.event.EventPackage
@Dao
interface EventDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(event: Event)
@Delete
fun remove(event: Event)
@Update
fun update(event: Event)
@Query("SELECT eventID FROM events WHERE name = :event AND DATE(schedule) = DATE(:schedule) COLLATE NOCASE AND eventId != :eventId")
suspend fun checkNameUniqueness(event: String?, schedule: String?, eventId: String?): List<String>
@Query("SELECT * FROM events")
suspend fun fetch(): List<Event>
@Query("SELECT * FROM events")
suspend fun fetchPackage(): List<EventPackage>
@Transaction
@Query("SELECT * FROM events LEFT JOIN subjects ON events.subject == subjects.subjectID WHERE isEventArchived = 0 ORDER BY schedule ASC")
fun fetchLiveData(): LiveData<List<EventPackage>>
@Transaction
@Query("SELECT * FROM events LEFT JOIN subjects ON events.subject == subjects.subjectID WHERE isEventArchived = 1 ORDER BY schedule ASC")
fun fetchArchivedLiveData(): LiveData<List<EventPackage>>
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/LogDAO.kt
================================================
package com.isaiahvonrundstedt.fokus.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.isaiahvonrundstedt.fokus.features.log.Log
@Dao
interface LogDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(log: Log)
@Delete
fun remove(log: Log)
@Update
fun update(log: Log)
@Query("SELECT * FROM logs")
fun fetchCore(): List<Log>
@Query("DELETE FROM logs")
suspend fun removeLogs()
@Query("SELECT * FROM logs ORDER BY dateTimeTriggered ASC")
fun fetch(): LiveData<List<Log>>
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/ScheduleDAO.kt
================================================
package com.isaiahvonrundstedt.fokus.database.dao
import androidx.room.*
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
@Dao
interface ScheduleDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg schedule: Schedule)
@Delete
fun remove(schedule: Schedule)
@Update
fun update(schedule: Schedule)
@Query("DELETE FROM schedules WHERE subject = :id")
suspend fun removeUsingSubjectID(id: String)
@Query("SELECT * FROM schedules")
suspend fun fetch(): List<Schedule>
@Query("SELECT * FROM schedules WHERE subject = :id")
suspend fun fetchUsingID(id: String?): List<Schedule>
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/SubjectDAO.kt
================================================
package com.isaiahvonrundstedt.fokus.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.subject.SubjectPackage
@Dao
interface SubjectDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(subject: Subject)
@Delete
suspend fun remove(subject: Subject)
@Update
suspend fun update(subject: Subject)
@Query("SELECT subjectID FROM subjects WHERE code = :code COLLATE NOCASE AND subjectID != :subjectId")
suspend fun checkCodeUniqueness(code: String?, subjectId: String?): List<String>
@Query("SELECT * FROM subjects")
suspend fun fetch(): List<Subject>
@Query("SELECT * FROM subjects")
suspend fun fetchAsPackage(): List<SubjectPackage>
@Transaction
@Query("SELECT * FROM subjects WHERE isSubjectArchived = 0 ORDER BY code ASC")
fun fetchLiveData(): LiveData<List<SubjectPackage>>
@Transaction
@Query("SELECT * FROM subjects WHERE isSubjectArchived = 1 ORDER BY code ASC")
fun fetchArchivedLiveData(): LiveData<List<SubjectPackage>>
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/TaskDAO.kt
================================================
package com.isaiahvonrundstedt.fokus.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.isaiahvonrundstedt.fokus.features.task.Task
import com.isaiahvonrundstedt.fokus.features.task.TaskPackage
@Dao
interface TaskDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(task: Task)
@Delete
fun remove(task: Task)
@Update
fun update(task: Task)
@Query("SELECT taskID FROM tasks WHERE name = :task COLLATE NOCASE AND taskID != :taskId")
suspend fun checkNameUniqueness(task: String?, taskId: String?): List<String>
@Query("UPDATE tasks SET isFinished = :status WHERE taskID = :taskID")
suspend fun setFinished(taskID: String, status: Int)
@Query("SELECT * FROM tasks WHERE isFinished = 0 AND isTaskArchived = 0")
suspend fun fetch(): List<Task>
@Query("SELECT COUNT(*) FROM tasks WHERE isFinished = 0 AND isTaskArchived = 0")
suspend fun fetchCount(): Int
@Query("SELECT * FROM tasks WHERE isFinished = 0")
suspend fun fetchAsPackage(): List<TaskPackage>
@Transaction
@Query("SELECT * FROM tasks LEFT JOIN subjects ON tasks.subject == subjects.subjectID WHERE isTaskArchived = 0 ORDER BY dueDate ASC")
fun fetchLiveData(): LiveData<List<TaskPackage>>
@Transaction
@Query("SELECT * FROM tasks LEFT JOIN subjects ON tasks.subject == subjects.subjectID WHERE isTaskArchived = 1 ORDER BY dueDate ASC")
fun fetchArchivedLiveData(): LiveData<List<TaskPackage>>
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/EventRepository.kt
================================================
package com.isaiahvonrundstedt.fokus.database.repository
import android.app.NotificationManager
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.isaiahvonrundstedt.fokus.components.extensions.jdk.isAfterNow
import com.isaiahvonrundstedt.fokus.components.extensions.jdk.isBeforeNow
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import com.isaiahvonrundstedt.fokus.database.dao.EventDAO
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.event.EventPackage
import com.isaiahvonrundstedt.fokus.features.event.widget.EventWidgetProvider
import com.isaiahvonrundstedt.fokus.features.notifications.event.EventNotificationWorker
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseWorker
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class EventRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val events: EventDAO,
private val preferenceManager: PreferenceManager,
private val workManager: WorkManager,
private val notificationManager: NotificationManager
) {
fun fetchLiveData(): LiveData<List<EventPackage>> = events.fetchLiveData()
fun fetchArchivedLiveData(): LiveData<List<EventPackage>> = events.fetchArchivedLiveData()
suspend fun checkNameUniqueness(name: String?, schedule: String?, eventId: String?): List<String> =
events.checkNameUniqueness(name, schedule, eventId)
suspend fun fetch(): List<EventPackage> = events.fetchPackage()
suspend fun fetchCore(): List<Event> = events.fetch()
suspend fun insert(event: Event) {
events.insert(event)
EventWidgetProvider.triggerRefresh(context)
if (preferenceManager.eventReminder && event.schedule?.isAfterNow() == true) {
val data = BaseWorker.convertEventToData(event)
val request = OneTimeWorkRequest.Builder(EventNotificationWorker::class.java)
.setInputData(data)
.addTag(event.eventID)
.build()
workManager.enqueueUniqueWork(
event.eventID, ExistingWorkPolicy.REPLACE,
request
)
}
}
suspend fun remove(event: Event) {
events.remove(event)
EventWidgetProvider.triggerRefresh(context)
if (event.isImportant)
notificationManager.cancel(event.eventID, BaseWorker.NOTIFICATION_ID_EVENT)
workManager.cancelUniqueWork(event.eventID)
}
suspend fun update(event: Event) {
events.update(event)
EventWidgetProvider.triggerRefresh(context)
if (event.schedule?.isBeforeNow() == true || !event.isImportant)
notificationManager.cancel(event.eventID, BaseWorker.NOTIFICATION_ID_EVENT)
if (preferenceManager.eventReminder && event.schedule?.isAfterNow() == true) {
val data = BaseWorker.convertEventToData(event)
val request = OneTimeWorkRequest.Builder(EventNotificationWorker::class.java)
.setInputData(data)
.addTag(event.eventID)
.build()
workManager.enqueueUniqueWork(
event.eventID, ExistingWorkPolicy.REPLACE,
request
)
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/LogRepository.kt
================================================
package com.isaiahvonrundstedt.fokus.database.repository
import androidx.lifecycle.LiveData
import com.isaiahvonrundstedt.fokus.database.dao.LogDAO
import com.isaiahvonrundstedt.fokus.features.log.Log
import javax.inject.Inject
class LogRepository @Inject constructor(private val logs: LogDAO) {
fun fetch(): LiveData<List<Log>> = logs.fetch()
suspend fun insert(log: Log) {
logs.insert(log)
}
suspend fun remove(log: Log) {
logs.remove(log)
}
suspend fun update(log: Log) {
logs.update(log)
}
suspend fun removeLogs() {
logs.removeLogs()
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/SubjectRepository.kt
================================================
package com.isaiahvonrundstedt.fokus.database.repository
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import com.isaiahvonrundstedt.fokus.database.dao.ScheduleDAO
import com.isaiahvonrundstedt.fokus.database.dao.SubjectDAO
import com.isaiahvonrundstedt.fokus.features.notifications.subject.ClassNotificationWorker
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseWorker
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.subject.SubjectPackage
import com.isaiahvonrundstedt.fokus.features.subject.widget.SubjectWidgetProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class SubjectRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val subjects: SubjectDAO,
private val schedules: ScheduleDAO,
private val preferenceManager: PreferenceManager,
private val workManager: WorkManager
) {
fun fetchLiveData(): LiveData<List<SubjectPackage>> = subjects.fetchLiveData()
fun fetchArchivedLiveData(): LiveData<List<SubjectPackage>> = subjects.fetchArchivedLiveData()
suspend fun fetch(): List<SubjectPackage> = subjects.fetchAsPackage()
suspend fun checkCodeExists(code: String?, subjectId: String?): List<String> = subjects.checkCodeUniqueness(code, subjectId)
suspend fun insert(subject: Subject, scheduleList: List<Schedule>) {
subjects.insert(subject)
scheduleList.forEach {
schedules.insert(it)
}
SubjectWidgetProvider.triggerRefresh(context)
if (preferenceManager.subjectReminder) {
scheduleList.forEach {
it.subject = subject.code
val data = BaseWorker.convertScheduleToData(it)
val request = OneTimeWorkRequest.Builder(ClassNotificationWorker::class.java)
.setInputData(data)
.addTag(subject.subjectID)
.addTag(it.scheduleID)
.build()
workManager.enqueueUniqueWork(
it.scheduleID, ExistingWorkPolicy.REPLACE,
request
)
}
}
}
suspend fun remove(subject: Subject) {
subjects.remove(subject)
SubjectWidgetProvider.triggerRefresh(context)
workManager.cancelAllWorkByTag(subject.subjectID)
}
suspend fun update(subject: Subject, scheduleList: List<Schedule> = emptyList()) {
subjects.update(subject)
schedules.removeUsingSubjectID(subject.subjectID)
if (scheduleList.isNotEmpty())
scheduleList.forEach { schedules.insert(it) }
SubjectWidgetProvider.triggerRefresh(context)
if (preferenceManager.subjectReminder) {
scheduleList.forEach {
workManager.cancelAllWorkByTag(it.scheduleID)
it.subject = subject.code
val data = BaseWorker.convertScheduleToData(it)
val request = OneTimeWorkRequest.Builder(ClassNotificationWorker::class.java)
.setInputData(data)
.addTag(subject.subjectID)
.addTag(it.scheduleID)
.build()
workManager.enqueueUniqueWork(
it.scheduleID, ExistingWorkPolicy.REPLACE,
request
)
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/TaskRepository.kt
================================================
package com.isaiahvonrundstedt.fokus.database.repository
import android.app.NotificationManager
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.isaiahvonrundstedt.fokus.components.extensions.jdk.isBeforeNow
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import com.isaiahvonrundstedt.fokus.database.dao.AttachmentDAO
import com.isaiahvonrundstedt.fokus.database.dao.TaskDAO
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.notifications.task.TaskNotificationWorker
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseWorker
import com.isaiahvonrundstedt.fokus.features.task.Task
import com.isaiahvonrundstedt.fokus.features.task.TaskPackage
import com.isaiahvonrundstedt.fokus.features.task.widget.TaskWidgetProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class TaskRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val tasks: TaskDAO,
private val attachments: AttachmentDAO,
private val preferenceManager: PreferenceManager,
private val workManager: WorkManager,
private val notificationManager: NotificationManager
) {
fun fetchLiveData(): LiveData<List<TaskPackage>> = tasks.fetchLiveData()
fun fetchArchived(): LiveData<List<TaskPackage>> = tasks.fetchArchivedLiveData()
suspend fun fetchCore(): List<Task> = tasks.fetch()
suspend fun fetchCount(): Int = tasks.fetchCount()
suspend fun fetchAsPackage(): List<TaskPackage> = tasks.fetchAsPackage()
suspend fun checkNameUniqueness(name: String?, id: String?): List<String> = tasks.checkNameUniqueness(name, id)
suspend fun insert(task: Task, attachmentList: List<Attachment> = emptyList()) {
tasks.insert(task)
if (attachmentList.isNotEmpty())
attachmentList.forEach { attachments.insert(it) }
TaskWidgetProvider.triggerRefresh(context)
// Check if notifications for tasks are turned on and check if
// the task is not finished, then schedule a notification
if (preferenceManager.taskReminder && !task.isFinished && task.isDueDateInFuture() &&
task.hasDueDate()
) {
val data = BaseWorker.convertTaskToData(task)
val request = OneTimeWorkRequest.Builder(TaskNotificationWorker::class.java)
.setInputData(data)
.addTag(task.taskID)
.build()
workManager.enqueueUniqueWork(
task.taskID, ExistingWorkPolicy.REPLACE,
request
)
}
}
suspend fun remove(task: Task) {
tasks.remove(task)
TaskWidgetProvider.triggerRefresh(context)
// If the task is important, its persistent notification should
// be canceled.
if (task.isImportant)
notificationManager.cancel(task.taskID, BaseWorker.NOTIFICATION_ID_TASK)
workManager.cancelUniqueWork(task.taskID)
}
suspend fun update(task: Task, attachmentList: List<Attachment> = emptyList()) {
tasks.update(task)
attachments.removeUsingTaskID(task.taskID)
if (attachmentList.isNotEmpty())
attachmentList.forEach { attachments.insert(it) }
TaskWidgetProvider.triggerRefresh(context)
// If we have a persistent notification,
// we should dismiss it when the user updates
// the task to finish
if (task.isFinished || !task.isImportant || task.dueDate?.isBeforeNow() == true)
notificationManager.cancel(task.taskID, BaseWorker.NOTIFICATION_ID_TASK)
// Check if notifications for tasks is turned on and if the task
// is not finished then reschedule the notification from
// WorkManager
if (preferenceManager.taskReminder && !task.isFinished) {
workManager.cancelUniqueWork(task.taskID)
val data = BaseWorker.convertTaskToData(task)
val request = OneTimeWorkRequest.Builder(TaskNotificationWorker::class.java)
.setInputData(data)
.addTag(task.taskID)
.build()
workManager.enqueueUniqueWork(
task.taskID, ExistingWorkPolicy.REPLACE,
request
)
}
}
suspend fun setFinished(taskID: String, status: Boolean) {
if (status)
tasks.setFinished(taskID, 1)
else tasks.setFinished(taskID, 0)
}
suspend fun addAttachment(attachment: Attachment) {
attachments.insert(attachment)
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/about/AboutFragment.kt
================================================
package com.isaiahvonrundstedt.fokus.features.about
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.browser.customtabs.CustomTabsIntent
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import com.isaiahvonrundstedt.fokus.BuildConfig
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import com.isaiahvonrundstedt.fokus.databinding.FragmentAboutBinding
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseFragment
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BasePreference
class AboutFragment : BaseFragment() {
private var _binding: FragmentAboutBinding? = null
private var controller: NavController? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAboutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setInsets(binding.root, binding.appBarLayout.toolbar)
with(binding.appBarLayout.toolbar) {
setTitle(R.string.activity_about)
setupNavigation(this, R.drawable.ic_outline_arrow_back_24) {
controller?.navigateUp()
}
}
}
override fun onStart() {
super.onStart()
controller = findNavController()
}
companion object {
const val ABOUT_ISSUE_URL = "https://github.com/icabetong/fokus-android/issues/new"
const val ABOUT_RELEASE_URL = "https://github.com/icabetong/fokus-android/releases"
const val ABOUT_DEVELOPER_EMAIL = "isaiahcollins_02@live.com"
class AboutFragment : BasePreference() {
private var controller: NavController? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.xml_about_main, rootKey)
}
override fun onStart() {
super.onStart()
controller = Navigation.findNavController(requireActivity(),
R.id.navigationHostFragment)
findPreference<Preference>(PreferenceManager.PREFERENCE_NOTICES)
?.setOnPreferenceClickListener {
controller?.navigate(R.id.navigation_notices)
true
}
findPreference<Preference>(PreferenceManager.PREFERENCE_TRANSLATE)
?.setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("mailto:${ABOUT_DEVELOPER_EMAIL}")
}
if (intent.resolveActivity(requireContext().packageManager) != null)
startActivity(intent)
true
}
findPreference<Preference>(PreferenceManager.PREFERENCE_REPORT_ISSUE)
?.setOnPreferenceClickListener {
CustomTabsIntent.Builder().build()
.launchUrl(requireContext(), Uri.parse(ABOUT_ISSUE_URL))
true
}
setPreferenceSummary(PreferenceManager.PREFERENCE_VERSION, BuildConfig.VERSION_NAME)
findPreference<Preference>(PreferenceManager.PREFERENCE_VERSION)
?.setOnPreferenceClickListener {
CustomTabsIntent.Builder().build()
.launchUrl(requireContext(), Uri.parse(ABOUT_RELEASE_URL))
true
}
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/about/LibrariesFragment.kt
================================================
package com.isaiahvonrundstedt.fokus.features.about
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.databinding.FragmentLibrariesBinding
import com.isaiahvonrundstedt.fokus.databinding.LayoutItemLibraryBinding
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseFragment
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
class LibrariesFragment : BaseFragment() {
private var _binding: FragmentLibrariesBinding? = null
private var controller: NavController? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLibrariesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setInsets(binding.root, binding.appBarLayout.toolbar, arrayOf(binding.recyclerView))
controller = Navigation.findNavController(view)
with(binding.appBarLayout.toolbar) {
setTitle(R.string.activity_open_source_licenses)
setupNavigation(this, R.drawable.ic_outline_arrow_back_24) {
controller?.navigateUp()
}
}
with(binding.recyclerView) {
layoutManager = LinearLayoutManager(context)
adapter = LibraryAdapter(Libs(context).libraries)
}
}
class LibraryAdapter(private val itemList: List<Library>) :
RecyclerView.Adapter<LibraryAdapter.LibraryViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryViewHolder {
val rowView: View = LayoutInflater.from(parent.context).inflate(
R.layout.layout_item_library,
parent, false
)
return LibraryViewHolder(rowView)
}
override fun onBindViewHolder(holder: LibraryViewHolder, position: Int) {
holder.onBind(itemList[position])
}
override fun getItemCount(): Int = itemList.size
class LibraryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = LayoutItemLibraryBinding.bind(itemView)
@Suppress("DEPRECATION")
fun onBind(library: Library) {
binding.nameTextView.text = library.libraryName
binding.versionTextView.text = library.libraryVersion
if (library.author.isNotEmpty())
binding.authorTextView.text = library.author
else binding.authorTextView.visibility = View.GONE
val license = library.licenses?.firstOrNull()
if (license != null) {
binding.licenseNameTextView.text = license.licenseName
binding.licenseDescriptionTextView.text =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(
license.licenseShortDescription,
Html.FROM_HTML_MODE_COMPACT
)
else Html.fromHtml(license.licenseShortDescription)
} else {
binding.licenseNameTextView.visibility = View.GONE
binding.licenseDescriptionTextView.visibility = View.GONE
}
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/about/NoticesFragment.kt
================================================
package com.isaiahvonrundstedt.fokus.features.about
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.browser.customtabs.CustomTabsIntent
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.preference.Preference
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.utils.PreferenceManager
import com.isaiahvonrundstedt.fokus.databinding.FragmentNoticesBinding
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseFragment
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BasePreference
class NoticesFragment : BaseFragment() {
private var _binding: FragmentNoticesBinding? = null
private var controller: NavController? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentNoticesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setInsets(binding.root, binding.appBarLayout.toolbar)
controller = Navigation.findNavController(view)
with(binding.appBarLayout.toolbar) {
setTitle(R.string.activity_notices)
setupNavigation(this, R.drawable.ic_outline_arrow_back_24) {
controller?.navigateUp()
}
}
}
companion object {
const val URL_LAUNCHER_ICON_BASE = "https://flaticon.com/authors/freepik"
const val URL_NOTIFICATION_SOUND =
"https://www.zapsplat.com/music/ui-alert-prompt-warm-wooden-mallet-style-notification-tone-generic-11/"
const val URL_USER_INTERFACE_ICONS = "https://heroicons.dev"
class NoticesFragment : BasePreference() {
private var controller: NavController? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.xml_about_notices, rootKey)
}
override fun onStart() {
super.onStart()
controller = Navigation.findNavController(requireView())
findPreference<Preference>(PreferenceManager.PREFERENCE_LIBRARIES)
?.setOnPreferenceClickListener {
controller?.navigate(R.id.navigation_libraries)
true
}
findPreference<Preference>(PreferenceManager.PREFERENCE_NOTIFICATION_SOUND)
?.setOnPreferenceClickListener {
CustomTabsIntent.Builder().build()
.launchUrl(requireContext(), Uri.parse(URL_NOTIFICATION_SOUND))
true
}
findPreference<Preference>(PreferenceManager.PREFERENCE_LAUNCHER_ICON)
?.setOnPreferenceClickListener {
CustomTabsIntent.Builder().build()
.launchUrl(requireContext(), Uri.parse(URL_LAUNCHER_ICON_BASE))
true
}
findPreference<Preference>(PreferenceManager.PREFERENCE_UI_ICONS)
?.setOnPreferenceClickListener {
CustomTabsIntent.Builder().build()
.launchUrl(requireContext(), Uri.parse(URL_USER_INTERFACE_ICONS))
true
}
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/Attachment.kt
================================================
package com.isaiahvonrundstedt.fokus.features.attachments
import android.content.Context
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.recyclerview.widget.DiffUtil
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.interfaces.Streamable
import com.isaiahvonrundstedt.fokus.components.json.JsonDataStreamer
import com.isaiahvonrundstedt.fokus.database.converter.DateTimeConverter
import com.isaiahvonrundstedt.fokus.features.task.Task
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
import okio.buffer
import okio.sink
import java.io.File
import java.net.URLConnection
import java.time.ZonedDateTime
import java.util.*
@Parcelize
@JsonClass(generateAdapter = true)
@Entity(
tableName = "attachments", foreignKeys = [ForeignKey(
entity = Task::class,
parentColumns = arrayOf("taskID"), childColumns = arrayOf("task"),
onDelete = ForeignKey.CASCADE
)
]
)
data class Attachment @JvmOverloads constructor(
@PrimaryKey
var attachmentID: String = UUID.randomUUID().toString(),
var name: String? = null,
var target: String? = null,
var task: String = "",
var type: Int = 0,
@TypeConverters(DateTimeConverter::class)
var dateAttached: ZonedDateTime? = ZonedDateTime.now()
) : Parcelable {
@DrawableRes
fun getIconResource(): Int {
return when (type) {
TYPE_WEBSITE_LINK -> R.drawable.ic_outline_link_24
else -> R.drawable.ic_outline_file_open_24
}
}
companion object {
const val TYPE_UNKNOWN = 0
const val TYPE_CONTENT_URI = 1
const val TYPE_IMPORTED_FILE = 2
const val TYPE_WEBSITE_LINK = 3
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Attachment>() {
override fun areItemsTheSame(oldItem: Attachment, newItem: Attachment): Boolean {
return oldItem.attachmentID == newItem.attachmentID
}
override fun areContentsTheSame(oldItem: Attachment, newItem: Attachment): Boolean {
return oldItem == newItem
}
}
fun isImage(path: String): Boolean {
val mimeType = URLConnection.guessContentTypeFromName(path)
return mimeType != null && mimeType.startsWith("image")
}
fun generateId(): String {
return UUID.randomUUID().toString()
}
fun getTargetDirectory(context: Context?): File {
return File(context?.getExternalFilesDir(null), Streamable.DIRECTORY_ATTACHMENTS)
}
fun toJsonFile(
items: List<Attachment>,
destination: File,
name: String = Streamable.FILE_NAME_ATTACHMENT
): File {
return File(destination, name).apply {
this.sink().buffer().use {
JsonDataStreamer.encodeToJson(items, Attachment::class.java)?.also { json ->
it.write(json.toByteArray())
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/AttachmentOptionSheet.kt
================================================
package com.isaiahvonrundstedt.fokus.features.attachments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import androidx.recyclerview.widget.LinearLayoutManager
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.databinding.LayoutSheetOptionsBinding
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseBottomSheet
import com.isaiahvonrundstedt.fokus.features.shared.adapters.MenuAdapter
class AttachmentOptionSheet(manager: FragmentManager) : BaseBottomSheet(manager),
MenuAdapter.MenuItemListener {
private var _binding: LayoutSheetOptionsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = LayoutSheetOptionsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.menuTitleView.text = getString(R.string.dialog_new_attachment)
with(binding.recyclerView) {
layoutManager = LinearLayoutManager(context)
adapter = MenuAdapter(
activity, R.menu.menu_attachment,
this@AttachmentOptionSheet
)
}
}
override fun onItemSelected(id: Int) {
setFragmentResult(
REQUEST_KEY, bundleOf(
EXTRA_OPTION to id
)
)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
companion object {
const val REQUEST_KEY = "request:attachment"
const val EXTRA_OPTION = "extra:option"
fun show(manager: FragmentManager) {
AttachmentOptionSheet(manager)
.show()
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/attach/AttachToTaskActivity.kt
================================================
package com.isaiahvonrundstedt.fokus.features.attachments.attach
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.custom.ItemDecoration
import com.isaiahvonrundstedt.fokus.components.extensions.android.createToast
import com.isaiahvonrundstedt.fokus.databinding.ActivityAttachToTaskBinding
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseActivity
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseAdapter
import com.isaiahvonrundstedt.fokus.features.task.TaskPackage
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AttachToTaskActivity : BaseActivity(), BaseAdapter.SelectListener {
private lateinit var binding: ActivityAttachToTaskBinding
private val attachAdapter = AttachToTaskAdapter(this)
private val viewModel: AttachToTaskViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAttachToTaskBinding.inflate(layoutInflater)
setContentView(binding.root)
setPersistentActionBar(binding.appBarLayout.toolbar)
setToolbarTitle(R.string.sharing_attach_to_task)
intent?.also {
viewModel.subject = it.getStringExtra(Intent.EXTRA_SUBJECT)
viewModel.url = it.getStringExtra(Intent.EXTRA_TEXT)
}
with(binding.recyclerView) {
addItemDecoration(ItemDecoration(context))
layoutManager = LinearLayoutManager(context)
adapter = attachAdapter
}
}
override fun onStart() {
super.onStart()
binding.titleView.text = viewModel.subject
binding.summaryView.text = viewModel.url
viewModel.tasks.observe(this) {
attachAdapter.submitList(it)
}
}
override fun <T> onItemSelected(t: T) {
if (t is TaskPackage) {
viewModel.addAttachment(t.task.taskID)
createToast(R.string.feedback_attachment_added)
finish()
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/attach/AttachToTaskAdapter.kt
================================================
package com.isaiahvonrundstedt.fokus.features.attachments.attach
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.isaiahvonrundstedt.fokus.databinding.LayoutItemTaskSendBinding
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseAdapter
import com.isaiahvonrundstedt.fokus.features.task.TaskPackage
class AttachToTaskAdapter(private val selectListener: SelectListener) :
BaseAdapter<TaskPackage, AttachToTaskAdapter.TaskViewHolder>(TaskPackage.DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
val binding = LayoutItemTaskSendBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
return TaskViewHolder(binding.root, selectListener)
}
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
holder.onBind(getItem(position))
}
class TaskViewHolder(
itemView: View,
private val selectListener: SelectListener
) : BaseAdapter.BaseViewHolder(itemView) {
private val binding = LayoutItemTaskSendBinding.bind(itemView)
override fun <T> onBind(data: T) {
if (data is TaskPackage) {
binding.titleView.text = data.task.name
binding.summaryView.text = data.task.formatDueDate(itemView.context)
binding.addButton.setOnClickListener {
selectListener.onItemSelected(data)
}
}
}
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/attach/AttachToTaskViewModel.kt
================================================
package com.isaiahvonrundstedt.fokus.features.attachments.attach
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isaiahvonrundstedt.fokus.database.repository.TaskRepository
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.task.TaskPackage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AttachToTaskViewModel @Inject constructor(
private val repository: TaskRepository
) : ViewModel() {
val tasks: LiveData<List<TaskPackage>> = repository.fetchLiveData()
val isEmpty: LiveData<Boolean> = Transformations.map(tasks) { it.isNullOrEmpty() }
var attachment = Attachment()
var subject: String? = null
set(value) {
field = value
attachment.name = value
}
var url: String? = null
set(value) {
field = value
attachment.target = value
}
init {
attachment.type = Attachment.TYPE_WEBSITE_LINK
}
fun addAttachment(taskID: String) = viewModelScope.launch {
attachment.task = taskID
repository.addAttachment(attachment)
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/core/activities/MainActivity.kt
================================================
package com.isaiahvonrundstedt.fokus.features.core.activities
import android.Manifest
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.navigation.NavController
import androidx.navigation.findNavController
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.components.extensions.android.createSnackbar
import com.isaiahvonrundstedt.fokus.components.extensions.android.getParcelableListExtra
import com.isaiahvonrundstedt.fokus.components.utils.NotificationChannelManager
import com.isaiahvonrundstedt.fokus.databinding.ActivityMainBinding
import com.isaiahvonrundstedt.fokus.features.attachments.Attachment
import com.isaiahvonrundstedt.fokus.features.event.Event
import com.isaiahvonrundstedt.fokus.features.event.editor.EventEditorFragment
import com.isaiahvonrundstedt.fokus.features.notifications.task.TaskReminderWorker
import com.isaiahvonrundstedt.fokus.features.schedule.Schedule
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseActivity
import com.isaiahvonrundstedt.fokus.features.subject.Subject
import com.isaiahvonrundstedt.fokus.features.subject.editor.SubjectEditorFragment
import com.isaiahvonrundstedt.fokus.features.task.Task
import com.isaiahvonrundstedt.fokus.features.task.editor.TaskEditorFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : BaseActivity() {
private var controller: NavController? = null
private lateinit var binding: ActivityMainBinding
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private val notificationManager: NotificationManager by lazy {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
controller = findNavController(R.id.navigationHostFragment)
requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { enabled ->
if (enabled) createSnackbar(R.string.notifications_are_now_enabled_for_this_app)
else createSnackbar(R.string.notifications_are_not_enabled_for_this_app)
}
intent?.also { intent ->
when (intent.action) {
ACTION_WIDGET_TASK -> {
val task: Task? = intent.getParcelableExtra(EXTRA_TASK)
val subject: Subject? = intent.getParcelableExtra(EXTRA_SUBJECT)
val attachments: List<Attachment>? =
intent.getParcelableListExtra(EXTRA_ATTACHMENTS)
val args = bundleOf(
TaskEditorFragment.EXTRA_TASK to task?.let { Task.toBundle(it) },
TaskEditorFragment.EXTRA_SUBJECT to subject?.let { Subject.toBundle(it) },
TaskEditorFragment.EXTRA_ATTACHMENTS to attachments
)
controller?.navigate(R.id.navigation_editor_task, args)
}
ACTION_WIDGET_EVENT -> {
val event: Event? = intent.getParcelableExtra(EXTRA_EVENT)
val subject: Subject? = intent.getParcelableExtra(EXTRA_SUBJECT)
val args = bundleOf(
EventEditorFragment.EXTRA_EVENT to event?.let { Event.toBundle(it) },
EventEditorFragment.EXTRA_SUBJECT to subject?.let { Subject.toBundle(it) }
)
controller?.navigate(R.id.navigation_editor_event, args)
}
ACTION_WIDGET_SUBJECT -> {
val subject: Subject? = intent.getParcelableExtra(EXTRA_SUBJECT)
val schedules: List<Schedule>? = intent.getParcelableListExtra(EXTRA_SCHEDULES)
val args = bundleOf(
SubjectEditorFragment.EXTRA_SUBJECT to subject?.let { Subject.toBundle(it) },
SubjectEditorFragment.EXTRA_SCHEDULE to schedules
)
controller?.navigate(R.id.navigation_editor_subject, args)
}
ACTION_SHORTCUT_TASK -> {
controller?.navigate(R.id.navigation_editor_task)
}
ACTION_SHORTCUT_EVENT -> {
controller?.navigate(R.id.navigation_editor_event)
}
ACTION_SHORTCUT_SUBJECT -> {
controller?.navigate(R.id.navigation_editor_subject)
}
ACTION_NAVIGATION_TASK -> {
//controller?.navigate(R.id.navigation_tasks)
}
ACTION_NAVIGATION_EVENT -> {
//controller?.navigate(R.id.navigation_events)
}
ACTION_NAVIGATION_SUBJECT -> {
//controller?.navigate(R.id.navigation_subjects)
}
}
}
TaskReminderWorker.reschedule(this.applicationContext)
}
override fun onStart() {
super.onStart()
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
with(NotificationChannelManager(this)) {
register(
NotificationChannelManager.CHANNEL_ID_GENERIC,
NotificationManager.IMPORTANCE_DEFAULT
)
register(
NotificationChannelManager.CHANNEL_ID_TASK,
groupID = NotificationChannelManager.CHANNEL_GROUP_ID_REMINDERS
)
register(
NotificationChannelManager.CHANNEL_ID_EVENT,
groupID = NotificationChannelManager.CHANNEL_GROUP_ID_REMINDERS
)
register(
NotificationChannelManager.CHANNEL_ID_CLASS,
groupID = NotificationChannelManager.CHANNEL_GROUP_ID_REMINDERS
)
}
}
}
}
override fun onSupportNavigateUp(): Boolean = controller?.navigateUp() ?: false
companion object {
const val ACTION_SHORTCUT_TASK = "action:shortcut:task"
const val ACTION_SHORTCUT_EVENT = "action:shortcut:event"
const val ACTION_SHORTCUT_SUBJECT = "action:shortcut:subject"
const val ACTION_WIDGET_TASK = "action:widget:task"
const val ACTION_WIDGET_EVENT = "action:widget:event"
const val ACTION_WIDGET_SUBJECT = "action:widget:subject"
const val ACTION_NAVIGATION_TASK = "action:navigation:task"
const val ACTION_NAVIGATION_EVENT = "action:navigation:event"
const val ACTION_NAVIGATION_SUBJECT = "action:navigation:subject"
const val EXTRA_TASK = "extra:task"
const val EXTRA_EVENT = "extra:event"
const val EXTRA_SUBJECT = "extra:subject"
const val EXTRA_ATTACHMENTS = "extra:attachments"
const val EXTRA_SCHEDULES = "extra:schedules"
}
}
================================================
FILE: app/src/main/java/com/isaiahvonrundstedt/fokus/features/core/fragment/RootFragment.kt
================================================
package com.isaiahvonrundstedt.fokus.features.core.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.GravityCompat
import androidx.core.view.doOnPreDraw
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.isaiahvonrundstedt.fokus.R
import com.isaiahvonrundstedt.fokus.databinding.FragmentRootBinding
import com.isaiahvonrundstedt.fokus.databinding.LayoutNavigationHeaderBinding
import com.isaiahvonrundstedt.fokus.features.shared.abstracts.BaseFragment
import java.time.LocalTime
class RootFragment: BaseFragment() {
private var _binding: FragmentRootBinding? = null
private var _headerBinding: LayoutNavigationHeaderBinding? = null
private var controller: NavController? = null
private var mainController: NavController? = null
private val binding get() = _binding!!
private val headerBinding get() = _headerBinding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentRootBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroy() {
_binding = null
_headerBinding = null
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mainController = requireActivity().supportFragmentManager
.findFragmentById(R.id.navigationHostFragment)?.findNavController()
/**
* Get this fragment's NavController
* to control what fragment will show up
* depending on what directions is on the
* NavigationViewModel
*/
controller = (childFragmentManager.findFragmentById(R.id.fragmentContainerView)
as? NavHostFragment)?.navController
if (binding.navigationView.headerCount > 0) {
_headerBinding = LayoutNavigationHeaderBinding
.bind(binding.navigationView.getHeaderView(0))
headerBinding.menuTitleView.text = when (LocalTime.now().hour) {
in 0..6 -> getString(R.string.greeting_default)
in 7..12 -> getString(R.string.greeting_mo
gitextract_ht_tjfbb/ ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── schemas/ │ │ └── com.isaiahvonrundstedt.fokus.database.AppDatabase/ │ │ └── 8.json │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── isaiahvonrundstedt/ │ │ └── fokus/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── isaiahvonrundstedt/ │ │ │ └── fokus/ │ │ │ ├── Fokus.kt │ │ │ ├── components/ │ │ │ │ ├── custom/ │ │ │ │ │ ├── ItemDecoration.kt │ │ │ │ │ └── ItemSwipeCallback.kt │ │ │ │ ├── enums/ │ │ │ │ │ └── SortDirection.kt │ │ │ │ ├── extensions/ │ │ │ │ │ ├── android/ │ │ │ │ │ │ ├── AppCompatExtensions.kt │ │ │ │ │ │ ├── IntentExtensions.kt │ │ │ │ │ │ ├── TextViewExtensions.kt │ │ │ │ │ │ └── UriExtensions.kt │ │ │ │ │ └── jdk/ │ │ │ │ │ ├── CalendarExtensions.kt │ │ │ │ │ ├── ListExtensions.kt │ │ │ │ │ └── TimeExtensions.kt │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── Streamable.kt │ │ │ │ │ └── Swipeable.kt │ │ │ │ ├── json/ │ │ │ │ │ ├── JsonDataStreamer.kt │ │ │ │ │ └── Metadata.kt │ │ │ │ ├── modules/ │ │ │ │ │ ├── DatabaseModule.kt │ │ │ │ │ ├── ExternalModule.kt │ │ │ │ │ └── InternalModule.kt │ │ │ │ ├── preference/ │ │ │ │ │ └── InformationHolder.kt │ │ │ │ ├── receiver/ │ │ │ │ │ └── LocalizationReceiver.kt │ │ │ │ ├── service/ │ │ │ │ │ ├── BackupRestoreService.kt │ │ │ │ │ ├── DataExporterService.kt │ │ │ │ │ ├── DataImporterService.kt │ │ │ │ │ ├── FileImporterService.kt │ │ │ │ │ └── NotificationActionService.kt │ │ │ │ ├── utils/ │ │ │ │ │ ├── DataArchiver.kt │ │ │ │ │ ├── NotificationChannelManager.kt │ │ │ │ │ ├── PermissionManager.kt │ │ │ │ │ └── PreferenceManager.kt │ │ │ │ └── views/ │ │ │ │ ├── RadioButtonCompat.kt │ │ │ │ ├── ReactiveTextColorSwitch.kt │ │ │ │ └── TwoLineRadioButton.kt │ │ │ ├── database/ │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── converter/ │ │ │ │ │ ├── ColorConverter.kt │ │ │ │ │ └── DateTimeConverter.kt │ │ │ │ ├── dao/ │ │ │ │ │ ├── AttachmentDAO.kt │ │ │ │ │ ├── EventDAO.kt │ │ │ │ │ ├── LogDAO.kt │ │ │ │ │ ├── ScheduleDAO.kt │ │ │ │ │ ├── SubjectDAO.kt │ │ │ │ │ └── TaskDAO.kt │ │ │ │ └── repository/ │ │ │ │ ├── EventRepository.kt │ │ │ │ ├── LogRepository.kt │ │ │ │ ├── SubjectRepository.kt │ │ │ │ └── TaskRepository.kt │ │ │ └── features/ │ │ │ ├── about/ │ │ │ │ ├── AboutFragment.kt │ │ │ │ ├── LibrariesFragment.kt │ │ │ │ └── NoticesFragment.kt │ │ │ ├── attachments/ │ │ │ │ ├── Attachment.kt │ │ │ │ ├── AttachmentOptionSheet.kt │ │ │ │ └── attach/ │ │ │ │ ├── AttachToTaskActivity.kt │ │ │ │ ├── AttachToTaskAdapter.kt │ │ │ │ └── AttachToTaskViewModel.kt │ │ │ ├── core/ │ │ │ │ ├── activities/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ ├── fragment/ │ │ │ │ │ └── RootFragment.kt │ │ │ │ └── worker/ │ │ │ │ └── ActionWorker.kt │ │ │ ├── event/ │ │ │ │ ├── Event.kt │ │ │ │ ├── EventAdapter.kt │ │ │ │ ├── EventFragment.kt │ │ │ │ ├── EventPackage.kt │ │ │ │ ├── EventViewModel.kt │ │ │ │ ├── archived/ │ │ │ │ │ ├── ArchivedEventAdapter.kt │ │ │ │ │ ├── ArchivedEventFragment.kt │ │ │ │ │ └── ArchivedEventViewModel.kt │ │ │ │ ├── editor/ │ │ │ │ │ ├── EventEditorContainer.kt │ │ │ │ │ ├── EventEditorFragment.kt │ │ │ │ │ └── EventEditorViewModel.kt │ │ │ │ └── widget/ │ │ │ │ ├── EventWidgetProvider.kt │ │ │ │ ├── EventWidgetRemoteViewFactory.kt │ │ │ │ └── EventWidgetService.kt │ │ │ ├── log/ │ │ │ │ ├── Log.kt │ │ │ │ ├── LogAdapter.kt │ │ │ │ ├── LogFragment.kt │ │ │ │ └── LogViewModel.kt │ │ │ ├── notifications/ │ │ │ │ ├── NotificationWorker.kt │ │ │ │ ├── event/ │ │ │ │ │ ├── EventNotificationScheduler.kt │ │ │ │ │ └── EventNotificationWorker.kt │ │ │ │ ├── subject/ │ │ │ │ │ ├── ClassNotificationScheduler.kt │ │ │ │ │ └── ClassNotificationWorker.kt │ │ │ │ └── task/ │ │ │ │ ├── TaskNotificationScheduler.kt │ │ │ │ ├── TaskNotificationWorker.kt │ │ │ │ └── TaskReminderWorker.kt │ │ │ ├── schedule/ │ │ │ │ ├── Schedule.kt │ │ │ │ ├── ScheduleEditor.kt │ │ │ │ ├── picker/ │ │ │ │ │ ├── SchedulePickerAdapter.kt │ │ │ │ │ └── SchedulePickerSheet.kt │ │ │ │ └── viewer/ │ │ │ │ ├── ScheduleViewerAdapter.kt │ │ │ │ └── ScheduleViewerSheet.kt │ │ │ ├── settings/ │ │ │ │ ├── BackupFragment.kt │ │ │ │ └── SettingsFragment.kt │ │ │ ├── shared/ │ │ │ │ ├── abstracts/ │ │ │ │ │ ├── BaseActivity.kt │ │ │ │ │ ├── BaseAdapter.kt │ │ │ │ │ ├── BaseBasicAdapter.kt │ │ │ │ │ ├── BaseBottomSheet.kt │ │ │ │ │ ├── BaseEditorFragment.kt │ │ │ │ │ ├── BaseFragment.kt │ │ │ │ │ ├── BasePickerFragment.kt │ │ │ │ │ ├── BasePreference.kt │ │ │ │ │ ├── BaseService.kt │ │ │ │ │ ├── BaseViewerFragment.kt │ │ │ │ │ └── BaseWorker.kt │ │ │ │ └── adapters/ │ │ │ │ └── MenuAdapter.kt │ │ │ ├── subject/ │ │ │ │ ├── Subject.kt │ │ │ │ ├── SubjectAdapter.kt │ │ │ │ ├── SubjectFragment.kt │ │ │ │ ├── SubjectPackage.kt │ │ │ │ ├── SubjectViewModel.kt │ │ │ │ ├── archived/ │ │ │ │ │ ├── ArchivedSubjectAdapter.kt │ │ │ │ │ ├── ArchivedSubjectFragment.kt │ │ │ │ │ └── ArchivedSubjectViewModel.kt │ │ │ │ ├── editor/ │ │ │ │ │ ├── SubjectEditorContainer.kt │ │ │ │ │ ├── SubjectEditorFragment.kt │ │ │ │ │ └── SubjectEditorViewModel.kt │ │ │ │ ├── picker/ │ │ │ │ │ ├── SubjectPickerAdapter.kt │ │ │ │ │ ├── SubjectPickerFragment.kt │ │ │ │ │ └── SubjectPickerViewModel.kt │ │ │ │ └── widget/ │ │ │ │ ├── SubjectWidgetProvider.kt │ │ │ │ ├── SubjectWidgetRemoteViewFactory.kt │ │ │ │ └── SubjectWidgetService.kt │ │ │ ├── task/ │ │ │ │ ├── Task.kt │ │ │ │ ├── TaskAdapter.kt │ │ │ │ ├── TaskFragment.kt │ │ │ │ ├── TaskPackage.kt │ │ │ │ ├── TaskViewModel.kt │ │ │ │ ├── archived/ │ │ │ │ │ ├── ArchivedTaskAdapter.kt │ │ │ │ │ ├── ArchivedTaskFragment.kt │ │ │ │ │ └── ArchivedTaskViewModel.kt │ │ │ │ ├── editor/ │ │ │ │ │ ├── TaskEditorContainer.kt │ │ │ │ │ ├── TaskEditorFragment.kt │ │ │ │ │ └── TaskEditorViewModel.kt │ │ │ │ └── widget/ │ │ │ │ ├── TaskWidgetProvider.kt │ │ │ │ ├── TaskWidgetRemoteViewFactory.kt │ │ │ │ └── TaskWidgetService.kt │ │ │ └── viewer/ │ │ │ └── ImageViewer.kt │ │ └── res/ │ │ ├── anim/ │ │ │ ├── anim_fade_in.xml │ │ │ ├── anim_slide_down.xml │ │ │ └── anim_slide_up.xml │ │ ├── color/ │ │ │ ├── color_text_input_stroke.xml │ │ │ ├── selector_chip_background.xml │ │ │ ├── selector_chip_stroke_color.xml │ │ │ └── selector_chip_text_color.xml │ │ ├── drawable/ │ │ │ ├── ic_hero_sort_ascending_24.xml │ │ │ ├── ic_hero_sort_descending_24.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_launcher_monochrome.xml │ │ │ ├── ic_outline_access_time_24.xml │ │ │ ├── ic_outline_add_24.xml │ │ │ ├── ic_outline_archive_24.xml │ │ │ ├── ic_outline_arrow_back_24.xml │ │ │ ├── ic_outline_attach_file_24.xml │ │ │ ├── ic_outline_balance_24.xml │ │ │ ├── ic_outline_calendar_month_24.xml │ │ │ ├── ic_outline_celebration_24.xml │ │ │ ├── ic_outline_check_24.xml │ │ │ ├── ic_outline_checklist_24.xml │ │ │ ├── ic_outline_close_24.xml │ │ │ ├── ic_outline_code_24.xml │ │ │ ├── ic_outline_color_lens_24.xml │ │ │ ├── ic_outline_confirmation_number_24.xml │ │ │ ├── ic_outline_date_range_24.xml │ │ │ ├── ic_outline_delete_24.xml │ │ │ ├── ic_outline_edit_note_24.xml │ │ │ ├── ic_outline_event_24.xml │ │ │ ├── ic_outline_event_busy_24.xml │ │ │ ├── ic_outline_event_repeat_24.xml │ │ │ ├── ic_outline_file_download_24.xml │ │ │ ├── ic_outline_file_open_24.xml │ │ │ ├── ic_outline_file_upload_24.xml │ │ │ ├── ic_outline_filter_alt_24.xml │ │ │ ├── ic_outline_info_24.xml │ │ │ ├── ic_outline_lightbulb_24.xml │ │ │ ├── ic_outline_link_24.xml │ │ │ ├── ic_outline_location_on_24.xml │ │ │ ├── ic_outline_menu_24.xml │ │ │ ├── ic_outline_more_vert_24.xml │ │ │ ├── ic_outline_music_note_24.xml │ │ │ ├── ic_outline_notes_24.xml │ │ │ ├── ic_outline_notifications_active_24.xml │ │ │ ├── ic_outline_numbers_24.xml │ │ │ ├── ic_outline_open_in_new_24.xml │ │ │ ├── ic_outline_person_2_24.xml │ │ │ ├── ic_outline_priority_high_24.xml │ │ │ ├── ic_outline_query_stats_24.xml │ │ │ ├── ic_outline_save_24.xml │ │ │ ├── ic_outline_science_24.xml │ │ │ ├── ic_outline_sensor_door_24.xml │ │ │ ├── ic_outline_settings_24.xml │ │ │ ├── ic_outline_share_24.xml │ │ │ ├── ic_outline_translate_24.xml │ │ │ ├── ic_outline_verified_24.xml │ │ │ ├── ic_outline_wb_sunny_24.xml │ │ │ ├── selector_checkbox.xml │ │ │ ├── shape_bottom_sheet.xml │ │ │ ├── shape_calendar_current_day.xml │ │ │ ├── shape_calendar_selected_day.xml │ │ │ ├── shape_cascade_background.xml │ │ │ ├── shape_color_holder.xml │ │ │ ├── shape_color_holder_chip.xml │ │ │ ├── shape_color_holder_vertical.xml │ │ │ ├── shape_icon_background.xml │ │ │ ├── shape_widget_background.xml │ │ │ ├── shortcut_icon_base_event.xml │ │ │ ├── shortcut_icon_base_subject.xml │ │ │ ├── shortcut_icon_base_task.xml │ │ │ ├── shortcut_icon_event.xml │ │ │ ├── shortcut_icon_subject.xml │ │ │ ├── shortcut_icon_task.xml │ │ │ ├── toggle_checked_to_unchecked.xml │ │ │ ├── toggle_unchecked_to_checked.xml │ │ │ ├── vector_checked.xml │ │ │ └── vector_unchecked.xml │ │ ├── layout/ │ │ │ ├── activity_attach_to_task.xml │ │ │ ├── activity_container_event.xml │ │ │ ├── activity_container_subject.xml │ │ │ ├── activity_container_task.xml │ │ │ ├── activity_main.xml │ │ │ ├── fragment_about.xml │ │ │ ├── fragment_archived_event.xml │ │ │ ├── fragment_archived_subject.xml │ │ │ ├── fragment_archived_task.xml │ │ │ ├── fragment_backup.xml │ │ │ ├── fragment_editor_event.xml │ │ │ ├── fragment_editor_subject.xml │ │ │ ├── fragment_editor_task.xml │ │ │ ├── fragment_event.xml │ │ │ ├── fragment_libraries.xml │ │ │ ├── fragment_logs.xml │ │ │ ├── fragment_notices.xml │ │ │ ├── fragment_picker_subject.xml │ │ │ ├── fragment_root.xml │ │ │ ├── fragment_settings.xml │ │ │ ├── fragment_subject.xml │ │ │ ├── fragment_task.xml │ │ │ ├── layout_appbar.xml │ │ │ ├── layout_appbar_editor.xml │ │ │ ├── layout_appbar_viewer.xml │ │ │ ├── layout_calendar_day.xml │ │ │ ├── layout_calendar_week_days.xml │ │ │ ├── layout_dialog_input_attachment.xml │ │ │ ├── layout_item_add.xml │ │ │ ├── layout_item_archived_event.xml │ │ │ ├── layout_item_archived_subject.xml │ │ │ ├── layout_item_archived_task.xml │ │ │ ├── layout_item_event.xml │ │ │ ├── layout_item_library.xml │ │ │ ├── layout_item_log.xml │ │ │ ├── layout_item_menu.xml │ │ │ ├── layout_item_schedule.xml │ │ │ ├── layout_item_subject.xml │ │ │ ├── layout_item_subject_picker.xml │ │ │ ├── layout_item_subject_single.xml │ │ │ ├── layout_item_task.xml │ │ │ ├── layout_item_task_send.xml │ │ │ ├── layout_item_widget.xml │ │ │ ├── layout_navigation_header.xml │ │ │ ├── layout_preference_info.xml │ │ │ ├── layout_sheet_options.xml │ │ │ ├── layout_sheet_schedule.xml │ │ │ ├── layout_sheet_schedule_editor.xml │ │ │ ├── layout_viewer_image.xml │ │ │ ├── layout_widget_events.xml │ │ │ ├── layout_widget_progress.xml │ │ │ ├── layout_widget_subjects.xml │ │ │ └── layout_widget_tasks.xml │ │ ├── menu/ │ │ │ ├── menu_add.xml │ │ │ ├── menu_attachment.xml │ │ │ ├── menu_editor.xml │ │ │ ├── menu_events.xml │ │ │ ├── menu_logs.xml │ │ │ ├── menu_share.xml │ │ │ ├── menu_subjects.xml │ │ │ ├── menu_tasks.xml │ │ │ └── navigation_main.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── navigation/ │ │ │ ├── navigation_container_event.xml │ │ │ ├── navigation_container_subject.xml │ │ │ ├── navigation_container_task.xml │ │ │ ├── navigation_main.xml │ │ │ └── navigation_root.xml │ │ ├── values/ │ │ │ ├── array.xml │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimen.xml │ │ │ ├── integer.xml │ │ │ ├── strings.xml │ │ │ ├── themes.xml │ │ │ └── values.xml │ │ ├── values-ar/ │ │ │ └── strings.xml │ │ ├── values-de/ │ │ │ └── strings.xml │ │ ├── values-es/ │ │ │ └── strings.xml │ │ ├── values-fr/ │ │ │ └── strings.xml │ │ ├── values-hdpi/ │ │ │ └── dimen.xml │ │ ├── values-id/ │ │ │ └── strings.xml │ │ ├── values-night/ │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── values-night-v23/ │ │ │ └── themes.xml │ │ ├── values-night-v27/ │ │ │ └── themes.xml │ │ ├── values-ru/ │ │ │ └── strings.xml │ │ ├── values-tr/ │ │ │ └── strings.xml │ │ ├── values-v23/ │ │ │ └── themes.xml │ │ ├── values-v27/ │ │ │ └── themes.xml │ │ └── xml/ │ │ ├── xml_about_main.xml │ │ ├── xml_about_notices.xml │ │ ├── xml_launcher_shortcuts.xml │ │ ├── xml_provider_paths.xml │ │ ├── xml_settings_backups.xml │ │ ├── xml_settings_main.xml │ │ ├── xml_widget_events.xml │ │ ├── xml_widget_subjects.xml │ │ └── xml_widget_tasks.xml │ └── test/ │ └── java/ │ └── com/ │ └── isaiahvonrundstedt/ │ └── fokus/ │ ├── ExampleUnitTest.kt │ └── features/ │ ├── attachments/ │ │ └── AttachmentTests.kt │ ├── event/ │ │ └── EventUnitTest.kt │ ├── schedule/ │ │ └── ScheduleTests.kt │ └── task/ │ └── TaskUnitTest.kt ├── build.gradle ├── fastlane/ │ └── metadata/ │ └── android/ │ ├── de/ │ │ └── short_description.txt │ └── en-US/ │ ├── full_description.txt │ └── short_description.txt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle
Condensed preview — 335 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,046K chars).
[
{
"path": ".gitignore",
"chars": 260,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea/*\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/tasks.xml\n/.idea/works"
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "Copyright 2023, Isaiah Collins Abetong\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of "
},
{
"path": "README.md",
"chars": 3140,
"preview": "# Fokus - To Do app tailored specifically for students\r\n[
About this extraction
This page contains the full source code of the icabetong/fokus-android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 335 files (937.2 KB), approximately 223.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.