Full Code of icabetong/fokus-android for AI

main fb57365edc3f cached
335 files
937.2 KB
223.8k tokens
1 requests
Download .txt
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
[![License](https://img.shields.io/github/license/icabetong/fokus-android)](https://www.gnu.org/licenses/gpl-3.0.en.html)
![Issues](https://img.shields.io/github/issues/icabetong/fokus-android)
![PRs](https://img.shields.io/github/issues-pr/icabetong/fokus-android)

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
Download .txt
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[![License](https://img.shields.io/github/license/icabetong/foku"
  },
  {
    "path": "app/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "chars": 4125,
    "preview": "plugins {\r\n    id 'com.android.application'\r\n    id 'kotlin-android'\r\n    id 'kotlin-kapt'\r\n    id 'kotlin-parcelize'\r\n "
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 827,
    "preview": "# Add project specific ProGuard rules here.\r\n# You can control the set of applied configuration files using the\r\n# progu"
  },
  {
    "path": "app/schemas/com.isaiahvonrundstedt.fokus.database.AppDatabase/8.json",
    "chars": 12698,
    "preview": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 8,\n    \"identityHash\": \"a5a325b51a216bf9c8987bdd216bf28b\",\n    \"e"
  },
  {
    "path": "app/src/androidTest/java/com/isaiahvonrundstedt/fokus/ExampleInstrumentedTest.kt",
    "chars": 715,
    "preview": "package com.isaiahvonrundstedt.fokus\r\n\r\nimport androidx.test.platform.app.InstrumentationRegistry\r\nimport androidx.test."
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 6730,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    xmlns:"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/Fokus.kt",
    "chars": 1078,
    "preview": "package com.isaiahvonrundstedt.fokus\n\nimport android.app.Application\nimport android.content.Context\nimport android.net.U"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/custom/ItemDecoration.kt",
    "chars": 227,
    "preview": "package com.isaiahvonrundstedt.fokus.components.custom\n\nimport android.content.Context\nimport androidx.recyclerview.widg"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/custom/ItemSwipeCallback.kt",
    "chars": 5400,
    "preview": "package com.isaiahvonrundstedt.fokus.components.custom\r\n\r\nimport android.content.Context\r\nimport android.content.res.Con"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/enums/SortDirection.kt",
    "chars": 445,
    "preview": "package com.isaiahvonrundstedt.fokus.components.enums\n\nenum class SortDirection {\n    ASCENDING, DESCENDING;\n\n    compan"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/AppCompatExtensions.kt",
    "chars": 1859,
    "preview": "package com.isaiahvonrundstedt.fokus.components.extensions.android\n\nimport android.content.Context\nimport android.conten"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/IntentExtensions.kt",
    "chars": 479,
    "preview": "package com.isaiahvonrundstedt.fokus.components.extensions.android\r\n\r\nimport android.content.Intent\r\nimport android.os.P"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/TextViewExtensions.kt",
    "chars": 1708,
    "preview": "package com.isaiahvonrundstedt.fokus.components.extensions.android\r\n\r\nimport android.graphics.Paint\r\nimport android.grap"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/android/UriExtensions.kt",
    "chars": 1014,
    "preview": "package com.isaiahvonrundstedt.fokus.components.extensions.android\r\n\r\nimport android.content.Context\r\nimport android.dat"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/jdk/CalendarExtensions.kt",
    "chars": 840,
    "preview": "package com.isaiahvonrundstedt.fokus.components.extensions.jdk\n\nimport java.time.LocalTime\nimport java.time.ZoneId\nimpor"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/jdk/ListExtensions.kt",
    "chars": 1144,
    "preview": "package com.isaiahvonrundstedt.fokus.components.extensions.jdk\n\nimport com.isaiahvonrundstedt.fokus.features.attachments"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/extensions/jdk/TimeExtensions.kt",
    "chars": 2413,
    "preview": "package com.isaiahvonrundstedt.fokus.components.extensions.jdk\n\nimport java.time.LocalDate\nimport java.time.LocalTime\nim"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/interfaces/Streamable.kt",
    "chars": 822,
    "preview": "package com.isaiahvonrundstedt.fokus.components.interfaces\n\nimport java.io.File\nimport java.io.InputStream\n\ninterface St"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/interfaces/Swipeable.kt",
    "chars": 132,
    "preview": "package com.isaiahvonrundstedt.fokus.components.interfaces\n\ninterface Swipeable {\n\n    fun onSwipe(position: Int, direct"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/json/JsonDataStreamer.kt",
    "chars": 2731,
    "preview": "package com.isaiahvonrundstedt.fokus.components.json\n\nimport android.net.Uri\nimport com.isaiahvonrundstedt.fokus.databas"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/json/Metadata.kt",
    "chars": 2202,
    "preview": "package com.isaiahvonrundstedt.fokus.components.json\n\nimport com.isaiahvonrundstedt.fokus.BuildConfig\nimport com.isaiahv"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/modules/DatabaseModule.kt",
    "chars": 3096,
    "preview": "package com.isaiahvonrundstedt.fokus.components.modules\n\nimport android.app.NotificationManager\nimport android.content.C"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/modules/ExternalModule.kt",
    "chars": 1053,
    "preview": "package com.isaiahvonrundstedt.fokus.components.modules\n\nimport android.app.NotificationManager\nimport android.content.C"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/modules/InternalModule.kt",
    "chars": 851,
    "preview": "package com.isaiahvonrundstedt.fokus.components.modules\n\nimport android.content.Context\nimport com.isaiahvonrundstedt.fo"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/preference/InformationHolder.kt",
    "chars": 1106,
    "preview": "package com.isaiahvonrundstedt.fokus.components.preference\r\n\r\nimport android.annotation.SuppressLint\r\nimport android.con"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/receiver/LocalizationReceiver.kt",
    "chars": 1589,
    "preview": "package com.isaiahvonrundstedt.fokus.components.receiver\r\n\r\nimport android.app.NotificationManager\r\nimport android.conte"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/BackupRestoreService.kt",
    "chars": 11408,
    "preview": "package com.isaiahvonrundstedt.fokus.components.service\n\nimport android.content.Intent\nimport android.net.Uri\nimport and"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/DataExporterService.kt",
    "chars": 5031,
    "preview": "package com.isaiahvonrundstedt.fokus.components.service\n\nimport android.content.Intent\nimport android.net.Uri\nimport and"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/DataImporterService.kt",
    "chars": 6104,
    "preview": "package com.isaiahvonrundstedt.fokus.components.service\n\nimport android.content.Intent\nimport android.os.IBinder\nimport "
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/FileImporterService.kt",
    "chars": 3676,
    "preview": "package com.isaiahvonrundstedt.fokus.components.service\n\nimport android.content.Intent\nimport android.net.Uri\nimport and"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/service/NotificationActionService.kt",
    "chars": 2132,
    "preview": "package com.isaiahvonrundstedt.fokus.components.service\r\n\r\nimport android.app.IntentService\r\nimport android.app.Notifica"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/DataArchiver.kt",
    "chars": 2629,
    "preview": "package com.isaiahvonrundstedt.fokus.components.utils\n\nimport android.content.Context\nimport android.net.Uri\nimport org."
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/NotificationChannelManager.kt",
    "chars": 2796,
    "preview": "package com.isaiahvonrundstedt.fokus.components.utils\n\nimport android.app.NotificationChannel\nimport android.app.Notific"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/PermissionManager.kt",
    "chars": 505,
    "preview": "package com.isaiahvonrundstedt.fokus.components.utils\n\nimport android.Manifest\nimport android.content.Context\nimport and"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/utils/PreferenceManager.kt",
    "chars": 10510,
    "preview": "package com.isaiahvonrundstedt.fokus.components.utils\n\nimport android.content.ContentResolver\nimport android.content.Con"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/views/RadioButtonCompat.kt",
    "chars": 1404,
    "preview": "package com.isaiahvonrundstedt.fokus.components.views\n\nimport android.content.Context\nimport android.os.Build\nimport and"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/views/ReactiveTextColorSwitch.kt",
    "chars": 973,
    "preview": "package com.isaiahvonrundstedt.fokus.components.views\n\nimport android.content.Context\nimport android.util.AttributeSet\ni"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/components/views/TwoLineRadioButton.kt",
    "chars": 4162,
    "preview": "package com.isaiahvonrundstedt.fokus.components.views\n\nimport android.content.Context\nimport android.os.Build\nimport and"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/AppDatabase.kt",
    "chars": 21230,
    "preview": "package com.isaiahvonrundstedt.fokus.database\r\n\r\nimport android.content.Context\r\nimport androidx.room.Database\r\nimport a"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/converter/ColorConverter.kt",
    "chars": 549,
    "preview": "package com.isaiahvonrundstedt.fokus.database.converter\r\n\r\nimport androidx.room.TypeConverter\r\nimport com.isaiahvonrunds"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/converter/DateTimeConverter.kt",
    "chars": 3897,
    "preview": "package com.isaiahvonrundstedt.fokus.database.converter\r\n\r\nimport android.content.Context\r\nimport android.text.format.Da"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/AttachmentDAO.kt",
    "chars": 571,
    "preview": "package com.isaiahvonrundstedt.fokus.database.dao\r\n\r\nimport androidx.room.*\r\nimport com.isaiahvonrundstedt.fokus.feature"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/EventDAO.kt",
    "chars": 1299,
    "preview": "package com.isaiahvonrundstedt.fokus.database.dao\r\n\r\nimport androidx.lifecycle.LiveData\r\nimport androidx.room.*\r\nimport "
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/LogDAO.kt",
    "chars": 601,
    "preview": "package com.isaiahvonrundstedt.fokus.database.dao\r\n\r\nimport androidx.lifecycle.LiveData\r\nimport androidx.room.*\r\nimport "
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/ScheduleDAO.kt",
    "chars": 689,
    "preview": "package com.isaiahvonrundstedt.fokus.database.dao\r\n\r\nimport androidx.room.*\r\nimport com.isaiahvonrundstedt.fokus.feature"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/SubjectDAO.kt",
    "chars": 1193,
    "preview": "package com.isaiahvonrundstedt.fokus.database.dao\r\n\r\nimport androidx.lifecycle.LiveData\r\nimport androidx.room.*\r\nimport "
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/dao/TaskDAO.kt",
    "chars": 1537,
    "preview": "package com.isaiahvonrundstedt.fokus.database.dao\r\n\r\nimport androidx.lifecycle.LiveData\r\nimport androidx.room.*\r\nimport "
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/EventRepository.kt",
    "chars": 3544,
    "preview": "package com.isaiahvonrundstedt.fokus.database.repository\r\n\r\nimport android.app.NotificationManager\r\nimport android.conte"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/LogRepository.kt",
    "chars": 645,
    "preview": "package com.isaiahvonrundstedt.fokus.database.repository\r\n\r\nimport androidx.lifecycle.LiveData\r\nimport com.isaiahvonrund"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/SubjectRepository.kt",
    "chars": 3808,
    "preview": "package com.isaiahvonrundstedt.fokus.database.repository\r\n\r\nimport android.content.Context\r\nimport androidx.lifecycle.Li"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/database/repository/TaskRepository.kt",
    "chars": 4887,
    "preview": "package com.isaiahvonrundstedt.fokus.database.repository\r\n\r\nimport android.app.NotificationManager\r\nimport android.conte"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/about/AboutFragment.kt",
    "chars": 4216,
    "preview": "package com.isaiahvonrundstedt.fokus.features.about\n\nimport android.content.Intent\nimport android.net.Uri\nimport android"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/about/LibrariesFragment.kt",
    "chars": 4013,
    "preview": "package com.isaiahvonrundstedt.fokus.features.about\n\nimport android.os.Build\nimport android.os.Bundle\nimport android.tex"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/about/NoticesFragment.kt",
    "chars": 3849,
    "preview": "package com.isaiahvonrundstedt.fokus.features.about\n\nimport android.content.Intent\nimport android.net.Uri\nimport android"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/Attachment.kt",
    "chars": 3316,
    "preview": "package com.isaiahvonrundstedt.fokus.features.attachments\r\n\r\nimport android.content.Context\r\nimport android.os.Parcelabl"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/AttachmentOptionSheet.kt",
    "chars": 2070,
    "preview": "package com.isaiahvonrundstedt.fokus.features.attachments\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\ni"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/attach/AttachToTaskActivity.kt",
    "chars": 2189,
    "preview": "package com.isaiahvonrundstedt.fokus.features.attachments.attach\n\nimport android.content.Intent\nimport android.os.Bundle"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/attach/AttachToTaskAdapter.kt",
    "chars": 1558,
    "preview": "package com.isaiahvonrundstedt.fokus.features.attachments.attach\n\nimport android.view.LayoutInflater\nimport android.view"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/attachments/attach/AttachToTaskViewModel.kt",
    "chars": 1325,
    "preview": "package com.isaiahvonrundstedt.fokus.features.attachments.attach\n\nimport androidx.lifecycle.LiveData\nimport androidx.lif"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/core/activities/MainActivity.kt",
    "chars": 7761,
    "preview": "package com.isaiahvonrundstedt.fokus.features.core.activities\r\n\r\nimport android.Manifest\r\nimport android.app.Notificatio"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/core/fragment/RootFragment.kt",
    "chars": 3214,
    "preview": "package com.isaiahvonrundstedt.fokus.features.core.fragment\n\nimport android.os.Bundle\nimport android.view.LayoutInflater"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/core/worker/ActionWorker.kt",
    "chars": 1265,
    "preview": "package com.isaiahvonrundstedt.fokus.features.core.worker\n\nimport android.content.Context\nimport androidx.hilt.work.Hilt"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/Event.kt",
    "chars": 6107,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event\r\n\r\nimport android.content.Context\r\nimport android.os.Bundle\r\nimport "
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/EventAdapter.kt",
    "chars": 3133,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event\r\n\r\nimport android.view.LayoutInflater\r\nimport android.view.View\r\nimp"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/EventFragment.kt",
    "chars": 13618,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event\r\n\r\nimport android.os.Bundle\r\nimport android.view.LayoutInflater\r\nimp"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/EventPackage.kt",
    "chars": 940,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event\n\nimport android.os.Parcelable\nimport androidx.recyclerview.widget.Di"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/EventViewModel.kt",
    "chars": 2046,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event\r\n\r\nimport androidx.lifecycle.*\r\nimport com.isaiahvonrundstedt.fokus."
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/archived/ArchivedEventAdapter.kt",
    "chars": 2529,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.archived\n\nimport android.view.LayoutInflater\nimport android.view.Vie"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/archived/ArchivedEventFragment.kt",
    "chars": 3224,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.archived\n\nimport android.os.Bundle\nimport android.view.LayoutInflate"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/archived/ArchivedEventViewModel.kt",
    "chars": 955,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.archived\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecyc"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/editor/EventEditorContainer.kt",
    "chars": 800,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.editor\n\nimport android.os.Bundle\nimport com.isaiahvonrundstedt.fokus"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/editor/EventEditorFragment.kt",
    "chars": 22762,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.editor\n\nimport android.content.BroadcastReceiver\nimport android.cont"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/editor/EventEditorViewModel.kt",
    "chars": 5909,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.editor\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/widget/EventWidgetProvider.kt",
    "chars": 3015,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.widget\n\nimport android.app.PendingIntent\nimport android.appwidget.Ap"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/widget/EventWidgetRemoteViewFactory.kt",
    "chars": 2342,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.widget\n\nimport android.content.Context\nimport android.content.Intent"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/event/widget/EventWidgetService.kt",
    "chars": 327,
    "preview": "package com.isaiahvonrundstedt.fokus.features.event.widget\n\nimport android.content.Intent\nimport android.widget.RemoteVi"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/log/Log.kt",
    "chars": 3755,
    "preview": "package com.isaiahvonrundstedt.fokus.features.log\r\n\r\nimport android.content.Context\r\nimport android.os.Parcelable\r\nimpor"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/log/LogAdapter.kt",
    "chars": 1794,
    "preview": "package com.isaiahvonrundstedt.fokus.features.log\r\n\r\nimport android.view.LayoutInflater\r\nimport android.view.View\r\nimpor"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/log/LogFragment.kt",
    "chars": 3917,
    "preview": "package com.isaiahvonrundstedt.fokus.features.log\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport an"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/log/LogViewModel.kt",
    "chars": 941,
    "preview": "package com.isaiahvonrundstedt.fokus.features.log\r\n\r\nimport androidx.lifecycle.LiveData\r\nimport androidx.lifecycle.Trans"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/NotificationWorker.kt",
    "chars": 1354,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications\n\nimport android.app.NotificationManager\nimport android.conte"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/event/EventNotificationScheduler.kt",
    "chars": 1653,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications.event\n\nimport android.content.Context\nimport androidx.hilt.w"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/event/EventNotificationWorker.kt",
    "chars": 2825,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications.event\n\nimport android.content.Context\nimport androidx.hilt.w"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/subject/ClassNotificationScheduler.kt",
    "chars": 1402,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications.subject\n\nimport android.content.Context\nimport androidx.hilt"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/subject/ClassNotificationWorker.kt",
    "chars": 3405,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications.subject\n\nimport android.content.Context\nimport androidx.hilt"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/task/TaskNotificationScheduler.kt",
    "chars": 1503,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications.task\n\nimport android.content.Context\nimport androidx.hilt.wo"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/task/TaskNotificationWorker.kt",
    "chars": 3212,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications.task\n\nimport android.content.Context\nimport androidx.hilt.wo"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/notifications/task/TaskReminderWorker.kt",
    "chars": 3883,
    "preview": "package com.isaiahvonrundstedt.fokus.features.notifications.task\n\nimport android.app.NotificationManager\nimport android."
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/schedule/Schedule.kt",
    "chars": 9974,
    "preview": "package com.isaiahvonrundstedt.fokus.features.schedule\r\n\r\nimport android.content.Context\r\nimport android.os.Parcelable\r\n"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/schedule/ScheduleEditor.kt",
    "chars": 11118,
    "preview": "package com.isaiahvonrundstedt.fokus.features.schedule\r\n\r\nimport android.os.Bundle\r\nimport android.text.format.DateForma"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/schedule/picker/SchedulePickerAdapter.kt",
    "chars": 2144,
    "preview": "package com.isaiahvonrundstedt.fokus.features.schedule.picker\n\nimport android.view.LayoutInflater\nimport android.view.Vi"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/schedule/picker/SchedulePickerSheet.kt",
    "chars": 2470,
    "preview": "package com.isaiahvonrundstedt.fokus.features.schedule.picker\n\nimport android.os.Bundle\nimport android.view.LayoutInflat"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/schedule/viewer/ScheduleViewerAdapter.kt",
    "chars": 1411,
    "preview": "package com.isaiahvonrundstedt.fokus.features.schedule.viewer\n\nimport android.view.LayoutInflater\nimport android.view.Vi"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/schedule/viewer/ScheduleViewerSheet.kt",
    "chars": 1357,
    "preview": "package com.isaiahvonrundstedt.fokus.features.schedule.viewer\n\nimport android.os.Bundle\nimport android.view.LayoutInflat"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/settings/BackupFragment.kt",
    "chars": 8214,
    "preview": "package com.isaiahvonrundstedt.fokus.features.settings\n\nimport android.content.BroadcastReceiver\nimport android.content."
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/settings/SettingsFragment.kt",
    "chars": 11681,
    "preview": "package com.isaiahvonrundstedt.fokus.features.settings\n\nimport android.content.Intent\nimport android.net.Uri\nimport andr"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseActivity.kt",
    "chars": 1946,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\r\n\r\nimport android.os.Build\r\nimport android.os.Bundle\r\nimp"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseAdapter.kt",
    "chars": 1732,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\r\n\r\nimport android.view.View\r\nimport androidx.recyclerview"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseBasicAdapter.kt",
    "chars": 729,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\n\nimport android.view.View\nimport androidx.recyclerview.wi"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseBottomSheet.kt",
    "chars": 1452,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\r\n\r\nimport android.app.Dialog\r\nimport android.os.Bundle\r\ni"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseEditorFragment.kt",
    "chars": 810,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\n\nimport android.os.Bundle\nimport android.view.animation.A"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseFragment.kt",
    "chars": 6790,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\r\n\r\nimport android.content.Context\r\nimport android.graphic"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BasePickerFragment.kt",
    "chars": 1954,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\n\nimport android.os.Bundle\nimport android.view.View\nimport"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BasePreference.kt",
    "chars": 357,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\r\n\r\nimport androidx.preference.Preference\r\nimport androidx"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseService.kt",
    "chars": 2804,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\n\nimport android.app.Notification\nimport android.app.Notif"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseViewerFragment.kt",
    "chars": 947,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\n\nimport android.os.Bundle\nimport android.view.View\nimport"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/abstracts/BaseWorker.kt",
    "chars": 10694,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.abstracts\r\n\r\nimport android.app.Notification\r\nimport android.app.No"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/shared/adapters/MenuAdapter.kt",
    "chars": 1974,
    "preview": "package com.isaiahvonrundstedt.fokus.features.shared.adapters\n\nimport android.app.Activity\nimport android.graphics.drawa"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/Subject.kt",
    "chars": 5660,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject\r\n\r\nimport android.graphics.Color\r\nimport android.graphics.drawable"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/SubjectAdapter.kt",
    "chars": 6443,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject\r\n\r\nimport android.view.LayoutInflater\r\nimport android.view.View\r\ni"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/SubjectFragment.kt",
    "chars": 10470,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject\r\n\r\nimport android.os.Bundle\r\nimport android.view.LayoutInflater\r\ni"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/SubjectPackage.kt",
    "chars": 1690,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject\n\nimport android.os.Parcelable\nimport androidx.recyclerview.widget."
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/SubjectViewModel.kt",
    "chars": 8817,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject\r\n\r\nimport androidx.lifecycle.*\r\nimport com.isaiahvonrundstedt.foku"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/archived/ArchivedSubjectAdapter.kt",
    "chars": 1788,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.archived\n\nimport android.view.LayoutInflater\nimport android.view.V"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/archived/ArchivedSubjectFragment.kt",
    "chars": 3250,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.archived\n\nimport android.os.Bundle\nimport android.view.LayoutInfla"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/archived/ArchivedSubjectViewModel.kt",
    "chars": 989,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.archived\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifec"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/editor/SubjectEditorContainer.kt",
    "chars": 810,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.editor\n\nimport android.os.Bundle\nimport com.isaiahvonrundstedt.fok"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/editor/SubjectEditorFragment.kt",
    "chars": 19918,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.editor\n\nimport android.app.Activity\nimport android.content.Broadca"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/editor/SubjectEditorViewModel.kt",
    "chars": 3920,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.editor\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecyc"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/picker/SubjectPickerAdapter.kt",
    "chars": 1921,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.picker\n\nimport android.view.LayoutInflater\nimport android.view.Vie"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/picker/SubjectPickerFragment.kt",
    "chars": 3598,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.picker\n\nimport android.os.Bundle\nimport android.view.LayoutInflate"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/picker/SubjectPickerViewModel.kt",
    "chars": 1280,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.picker\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecyc"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/widget/SubjectWidgetProvider.kt",
    "chars": 3036,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.widget\n\nimport android.app.PendingIntent\nimport android.appwidget."
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/widget/SubjectWidgetRemoteViewFactory.kt",
    "chars": 2623,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.widget\n\nimport android.content.Context\nimport android.content.Inte"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/subject/widget/SubjectWidgetService.kt",
    "chars": 334,
    "preview": "package com.isaiahvonrundstedt.fokus.features.subject.widget\n\nimport android.content.Intent\nimport android.widget.Remote"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/Task.kt",
    "chars": 6046,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task\r\n\r\nimport android.content.Context\r\nimport android.os.Bundle\r\nimport a"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/TaskAdapter.kt",
    "chars": 4591,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task\r\n\r\nimport android.view.LayoutInflater\r\nimport android.view.View\r\nimpo"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/TaskFragment.kt",
    "chars": 11628,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task\r\n\r\nimport android.graphics.Color\r\nimport android.media.RingtoneManage"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/TaskPackage.kt",
    "chars": 1192,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task\n\nimport android.os.Parcelable\nimport androidx.recyclerview.widget.Dif"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/TaskViewModel.kt",
    "chars": 7063,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task\r\n\r\nimport androidx.lifecycle.*\r\nimport com.isaiahvonrundstedt.fokus.c"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/archived/ArchivedTaskAdapter.kt",
    "chars": 3083,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.archived\n\nimport android.view.LayoutInflater\nimport android.view.View"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/archived/ArchivedTaskFragment.kt",
    "chars": 3212,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.archived\n\nimport android.os.Bundle\nimport android.view.LayoutInflater"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/archived/ArchivedTaskViewModel.kt",
    "chars": 930,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.archived\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycl"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/editor/TaskEditorContainer.kt",
    "chars": 795,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.editor\n\nimport android.os.Bundle\nimport com.isaiahvonrundstedt.fokus."
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/editor/TaskEditorFragment.kt",
    "chars": 37679,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.editor\n\nimport android.Manifest\nimport android.app.Activity\nimport an"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/editor/TaskEditorViewModel.kt",
    "chars": 6595,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.editor\n\nimport android.content.ClipboardManager\nimport androidx.lifec"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/widget/TaskWidgetProvider.kt",
    "chars": 3017,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.widget\n\nimport android.app.PendingIntent\nimport android.appwidget.App"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/widget/TaskWidgetRemoteViewFactory.kt",
    "chars": 2657,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.widget\n\nimport android.content.Context\nimport android.content.Intent\n"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/task/widget/TaskWidgetService.kt",
    "chars": 325,
    "preview": "package com.isaiahvonrundstedt.fokus.features.task.widget\n\nimport android.content.Intent\nimport android.widget.RemoteVie"
  },
  {
    "path": "app/src/main/java/com/isaiahvonrundstedt/fokus/features/viewer/ImageViewer.kt",
    "chars": 1769,
    "preview": "package com.isaiahvonrundstedt.fokus.features.viewer\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport"
  },
  {
    "path": "app/src/main/res/anim/anim_fade_in.xml",
    "chars": 326,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\r\n    <alpha\r\n  "
  },
  {
    "path": "app/src/main/res/anim/anim_slide_down.xml",
    "chars": 328,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <translate\n "
  },
  {
    "path": "app/src/main/res/anim/anim_slide_up.xml",
    "chars": 325,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <translate\n "
  },
  {
    "path": "app/src/main/res/color/color_text_input_stroke.xml",
    "chars": 429,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item "
  },
  {
    "path": "app/src/main/res/color/selector_chip_background.xml",
    "chars": 391,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item "
  },
  {
    "path": "app/src/main/res/color/selector_chip_stroke_color.xml",
    "chars": 366,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item "
  },
  {
    "path": "app/src/main/res/color/selector_chip_text_color.xml",
    "chars": 340,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item "
  },
  {
    "path": "app/src/main/res/drawable/ic_hero_sort_ascending_24.xml",
    "chars": 501,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_hero_sort_descending_24.xml",
    "chars": 504,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "chars": 743,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    android:width=\"108dp\"\r\n    android:height=\"108dp"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_monochrome.xml",
    "chars": 809,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    android:width=\"108dp\"\r\n    android:height=\"108dp"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_access_time_24.xml",
    "chars": 498,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_add_24.xml",
    "chars": 335,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_archive_24.xml",
    "chars": 598,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_arrow_back_24.xml",
    "chars": 390,
    "preview": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:tint=\"?attr/colorOnSurfaceVariant\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_attach_file_24.xml",
    "chars": 577,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_balance_24.xml",
    "chars": 697,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_calendar_month_24.xml",
    "chars": 550,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_celebration_24.xml",
    "chars": 1216,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_check_24.xml",
    "chars": 358,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_checklist_24.xml",
    "chars": 465,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_close_24.xml",
    "chars": 409,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_code_24.xml",
    "chars": 402,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_color_lens_24.xml",
    "chars": 1201,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_confirmation_number_24.xml",
    "chars": 657,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_date_range_24.xml",
    "chars": 520,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_delete_24.xml",
    "chars": 400,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_edit_note_24.xml",
    "chars": 508,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_event_24.xml",
    "chars": 488,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_event_busy_24.xml",
    "chars": 595,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_event_repeat_24.xml",
    "chars": 651,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_file_download_24.xml",
    "chars": 424,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_file_open_24.xml",
    "chars": 446,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_file_upload_24.xml",
    "chars": 419,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_filter_alt_24.xml",
    "chars": 483,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_info_24.xml",
    "chars": 462,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_lightbulb_24.xml",
    "chars": 633,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_link_24.xml",
    "chars": 497,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_location_on_24.xml",
    "chars": 586,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_menu_24.xml",
    "chars": 357,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_more_vert_24.xml",
    "chars": 484,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_music_note_24.xml",
    "chars": 480,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_notes_24.xml",
    "chars": 379,
    "preview": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:tint=\"?attr/colorOnSurfaceVariant\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_notifications_active_24.xml",
    "chars": 718,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_numbers_24.xml",
    "chars": 446,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_open_in_new_24.xml",
    "chars": 466,
    "preview": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:tint=\"?attr/colorOnSurfaceVariant\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_person_2_24.xml",
    "chars": 851,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_priority_high_24.xml",
    "chars": 427,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_query_stats_24.xml",
    "chars": 823,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_save_24.xml",
    "chars": 499,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_science_24.xml",
    "chars": 503,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_sensor_door_24.xml",
    "chars": 473,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_settings_24.xml",
    "chars": 2046,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_share_24.xml",
    "chars": 933,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_translate_24.xml",
    "chars": 625,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_verified_24.xml",
    "chars": 1088,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  },
  {
    "path": "app/src/main/res/drawable/ic_outline_wb_sunny_24.xml",
    "chars": 693,
    "preview": "<vector android:height=\"24dp\" android:tint=\"?attr/colorOnSurfaceVariant\"\n    android:viewportHeight=\"24\" android:viewpor"
  }
]

// ... and 135 more files (download for full content)

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.

Copied to clipboard!