From 3b14696b5dbd9b92bd7f841f78e0dff85c1cc3d1 Mon Sep 17 00:00:00 2001 From: Sebastian Seedorf Date: Sun, 6 Dec 2020 19:59:55 +0100 Subject: [PATCH] initial commit --- .gitignore | 9 + FUPlanner 2.iml | 19 + app/.gitignore | 1 + app/app.iml | 251 + app/build.gradle | 79 + app/demo.kts | 27 + app/demo.py | 28 + app/demo.ws.kts | 27 + app/proguard-rules.pro | 21 + .../fuplanner2/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 42 + app/src/main/ic_launcher-playstore.png | Bin 0 -> 43797 bytes .../de/sebse/fuplanner2/CustomApplication.kt | 15 + .../java/de/sebse/fuplanner2/MainActivity.kt | 146 + .../de/sebse/fuplanner2/StartupActivity.kt | 71 + .../AccountAuthenticatorAppCompatActivity.kt | 42 + .../de/sebse/fuplanner2/auth/AppAccounts.kt | 148 + .../de/sebse/fuplanner2/auth/FUAuthModule.kt | 84 + .../auth/FuplannerAccountActivity.kt | 116 + .../auth/FuplannerAccountAuthenticator.kt | 150 + .../auth/FuplannerAccountConstants.kt | 10 + .../fuplanner2/auth/FuplannerAccountHelper.kt | 58 + .../auth/FuplannerAccountService.kt | 14 + .../de/sebse/fuplanner2/auth/SamlReponse.kt | 3 + .../de/sebse/fuplanner2/auth/UserCookies.kt | 56 + .../sebse/fuplanner2/blackboard/Blackboard.kt | 145 + .../fuplanner2/blackboard/BlackboardInfo.kt | 24 + .../fuplanner2/blackboard/ExtAnnouncements.kt | 62 + .../sebse/fuplanner2/blackboard/ExtCourses.kt | 159 + .../sebse/fuplanner2/blackboard/ExtEvents.kt | 105 + .../sebse/fuplanner2/database/Announcement.kt | 92 + .../fuplanner2/database/AnnouncementDao.kt | 46 + .../sebse/fuplanner2/database/AppDatabase.kt | 84 + .../sebse/fuplanner2/database/Attachment.kt | 25 + .../de/sebse/fuplanner2/database/Cache.kt | 30 + .../de/sebse/fuplanner2/database/CacheDao.kt | 15 + .../sebse/fuplanner2/database/Converters.kt | 88 + .../de/sebse/fuplanner2/database/Course.kt | 101 + .../de/sebse/fuplanner2/database/CourseDao.kt | 56 + .../de/sebse/fuplanner2/database/Event.kt | 91 + .../de/sebse/fuplanner2/database/EventDao.kt | 49 + .../de/sebse/fuplanner2/database/Lecturer.kt | 26 + .../sebse/fuplanner2/database/Notification.kt | 23 + .../fuplanner2/database/NotificationDao.kt | 52 + .../java/de/sebse/fuplanner2/database/User.kt | 21 + .../de/sebse/fuplanner2/database/UserDao.kt | 41 + .../sebse/fuplanner2/network/CustomRequest.kt | 126 + .../de/sebse/fuplanner2/network/NetData.kt | 6 + .../de/sebse/fuplanner2/network/Requester.kt | 52 + .../java/de/sebse/fuplanner2/network/tools.kt | 17 + .../fuplanner2/preferences/AppPreferences.kt | 47 + .../fuplanner2/ui/courses/CoursesAdapter.kt | 66 + .../fuplanner2/ui/courses/CoursesFragment.kt | 70 + .../fuplanner2/ui/courses/CoursesViewModel.kt | 10 + .../fuplanner2/ui/details/DetailsAdapter.kt | 84 + .../fuplanner2/ui/details/DetailsFragment.kt | 89 + .../fuplanner2/ui/details/DetailsViewModel.kt | 16 + .../AnnouncementsAdapter.kt | 40 + .../AnnouncementsFragment.kt | 63 + .../AnnouncementsViewModel.kt | 18 + .../DescriptionFragment.kt | 44 + .../ui/details_events/EventsAdapter.kt | 41 + .../ui/details_events/EventsFragment.kt | 63 + .../ui/details_events/EventsViewModel.kt | 19 + .../fuplanner2/ui/gallery/GalleryFragment.kt | 31 + .../fuplanner2/ui/gallery/GalleryViewModel.kt | 13 + .../ui/notification/NotificationAdapter.kt | 87 + .../ui/notification/NotificationFragment.kt | 66 + .../ui/notification/NotificationViewModel.kt | 13 + .../ui/schedule/ScheduleFragment.kt | 216 + .../ui/schedule/ScheduleViewModel.kt | 38 + .../java/de/sebse/fuplanner2/ui/viewholder.kt | 131 + .../fuplanner2/utils/CustomTabsHelper.kt | 82 + .../sebse/fuplanner2/utils/notifications.kt | 203 + .../java/de/sebse/fuplanner2/utils/utils.kt | 181 + .../fuplanner2/whiteboard/ExtAnnouncements.kt | 57 + .../sebse/fuplanner2/whiteboard/ExtCourses.kt | 127 + .../sebse/fuplanner2/whiteboard/ExtEvents.kt | 54 + .../sebse/fuplanner2/whiteboard/Whiteboard.kt | 112 + .../fuplanner2/whiteboard/WhiteboardInfo.kt | 24 + .../worker/AbstractAccountWorker.kt | 53 + .../fuplanner2/worker/AnnouncementWorker.kt | 35 + .../sebse/fuplanner2/worker/CourseWorker.kt | 47 + .../de/sebse/fuplanner2/worker/EventWorker.kt | 43 + .../worker/FetchResourceException.kt | 11 + .../de/sebse/fuplanner2/worker/SyncWorker.kt | 74 + app/src/main/res/anim/slide_in_left.xml | 6 + app/src/main/res/anim/slide_in_right.xml | 6 + app/src/main/res/anim/slide_out_left.xml | 6 + app/src/main/res/anim/slide_out_right.xml | 6 + .../main/res/drawable-night/logo_campus.png | Bin 0 -> 92766 bytes .../drawable/ic_keyboard_arrow_left_white.xml | 9 + .../res/drawable/ic_keyboard_arrow_right.xml | 10 + .../ic_keyboard_arrow_right_white.xml | 9 + app/src/main/res/drawable/ic_logo_mono.xml | 11 + app/src/main/res/drawable/ic_mail.xml | 10 + app/src/main/res/drawable/ic_menu_canteen.xml | 9 + app/src/main/res/drawable/ic_menu_courses.xml | 9 + app/src/main/res/drawable/ic_menu_event.xml | 9 + .../res/drawable/ic_menu_notifications.xml | 9 + .../drawable/ic_menu_notifications_none.xml | 9 + app/src/main/res/drawable/logo_campus.png | Bin 0 -> 92112 bytes app/src/main/res/drawable/rounded_corner.xml | 16 + app/src/main/res/drawable/side_nav_bar.xml | 8 + app/src/main/res/layout/activity_login.xml | 72 + app/src/main/res/layout/activity_main.xml | 25 + app/src/main/res/layout/activity_startup.xml | 20 + app/src/main/res/layout/app_bar_main.xml | 25 + app/src/main/res/layout/content_main.xml | 20 + app/src/main/res/layout/fragment_canteen.xml | 22 + .../main/res/layout/fragment_description.xml | 10 + app/src/main/res/layout/fragment_recycler.xml | 16 + .../res/layout/fragment_refresh_recycler.xml | 16 + app/src/main/res/layout/fragment_schedule.xml | 32 + app/src/main/res/layout/list_all_caption.xml | 14 + app/src/main/res/layout/list_all_items.xml | 53 + app/src/main/res/layout/list_all_mails.xml | 45 + .../res/layout/list_courses_quicklinks.xml | 73 + .../res/layout/list_notification_item.xml | 53 + .../res/layout/nav_action_view_counter.xml | 17 + app/src/main/res/layout/nav_header_main.xml | 37 + .../main/res/layout/notification_fragment.xml | 13 + .../main/res/menu/activity_main_drawer.xml | 29 + app/src/main/res/menu/main.xml | 9 + app/src/main/res/menu/notification_menu.xml | 9 + app/src/main/res/menu/schedule_menu.xml | 19 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4791 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 4134 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4791 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2910 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2414 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2910 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 7197 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5969 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7197 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 11539 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 9852 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11539 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 16943 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 14075 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16943 bytes app/src/main/res/navigation/nav_graph.xml | 125 + app/src/main/res/values-night-v21/styles.xml | 12 + app/src/main/res/values-night/colors.xml | 9 + .../main/res/values-notnight-v23/styles.xml | 15 + app/src/main/res/values/array.xml | 7 + app/src/main/res/values/array_lang.xml | 7 + app/src/main/res/values/colors.xml | 48 + app/src/main/res/values/dimens.xml | 12 + app/src/main/res/values/drawables.xml | 5 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 78 + app/src/main/res/values/styles.xml | 54 + app/src/main/res/xml/auth_pref.xml | 10 + .../main/res/xml/fuplanner_authenticator.xml | 7 + .../de/sebse/fuplanner2/ExampleUnitTest.kt | 17 + app/tt.ods | Bin 0 -> 4335277 bytes app/your_file.txt | 66572 ++++++++++++++++ build.gradle | 29 + data/Icon-optimized.svg | 20 + data/Icon.png | Bin 0 -> 247249 bytes data/Icon.svg | 202 + data/Icon.xcf | Bin 0 -> 531581 bytes data/logo_campus.png | Bin 0 -> 304520 bytes data/logo_campus.xcf | Bin 0 -> 901489 bytes data/logo_campus_night.png | Bin 0 -> 353861 bytes data/monochrome-optimized.svg | 10 + data/monochrome.svg | 87 + gradle.properties | 21 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 + gradlew.bat | 84 + settings.gradle | 2 + 176 files changed, 73741 insertions(+) create mode 100644 .gitignore create mode 100644 FUPlanner 2.iml create mode 100644 app/.gitignore create mode 100644 app/app.iml create mode 100644 app/build.gradle create mode 100644 app/demo.kts create mode 100644 app/demo.py create mode 100644 app/demo.ws.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/de/sebse/fuplanner2/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/de/sebse/fuplanner2/CustomApplication.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/MainActivity.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/StartupActivity.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/AccountAuthenticatorAppCompatActivity.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/AppAccounts.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/FUAuthModule.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountActivity.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountAuthenticator.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountConstants.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountHelper.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountService.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/SamlReponse.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/auth/UserCookies.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/blackboard/Blackboard.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/blackboard/BlackboardInfo.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/blackboard/ExtAnnouncements.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/blackboard/ExtCourses.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/blackboard/ExtEvents.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Announcement.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/AnnouncementDao.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/AppDatabase.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Attachment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Cache.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/CacheDao.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Converters.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Course.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/CourseDao.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Event.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/EventDao.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/Notification.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/NotificationDao.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/User.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/database/UserDao.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/network/CustomRequest.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/network/NetData.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/network/Requester.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/network/tools.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/preferences/AppPreferences.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesAdapter.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesViewModel.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsViewModel.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsAdapter.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details_description/DescriptionFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsAdapter.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsViewModel.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryViewModel.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationAdapter.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationViewModel.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleViewModel.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/ui/viewholder.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/utils/CustomTabsHelper.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/utils/notifications.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/utils/utils.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtAnnouncements.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtCourses.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtEvents.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/whiteboard/Whiteboard.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/whiteboard/WhiteboardInfo.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/worker/AnnouncementWorker.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/worker/EventWorker.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/worker/FetchResourceException.kt create mode 100644 app/src/main/java/de/sebse/fuplanner2/worker/SyncWorker.kt create mode 100644 app/src/main/res/anim/slide_in_left.xml create mode 100644 app/src/main/res/anim/slide_in_right.xml create mode 100644 app/src/main/res/anim/slide_out_left.xml create mode 100644 app/src/main/res/anim/slide_out_right.xml create mode 100644 app/src/main/res/drawable-night/logo_campus.png create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_left_white.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_right.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_right_white.xml create mode 100644 app/src/main/res/drawable/ic_logo_mono.xml create mode 100644 app/src/main/res/drawable/ic_mail.xml create mode 100644 app/src/main/res/drawable/ic_menu_canteen.xml create mode 100644 app/src/main/res/drawable/ic_menu_courses.xml create mode 100644 app/src/main/res/drawable/ic_menu_event.xml create mode 100644 app/src/main/res/drawable/ic_menu_notifications.xml create mode 100644 app/src/main/res/drawable/ic_menu_notifications_none.xml create mode 100644 app/src/main/res/drawable/logo_campus.png create mode 100644 app/src/main/res/drawable/rounded_corner.xml create mode 100644 app/src/main/res/drawable/side_nav_bar.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_startup.xml create mode 100644 app/src/main/res/layout/app_bar_main.xml create mode 100644 app/src/main/res/layout/content_main.xml create mode 100644 app/src/main/res/layout/fragment_canteen.xml create mode 100644 app/src/main/res/layout/fragment_description.xml create mode 100644 app/src/main/res/layout/fragment_recycler.xml create mode 100644 app/src/main/res/layout/fragment_refresh_recycler.xml create mode 100644 app/src/main/res/layout/fragment_schedule.xml create mode 100644 app/src/main/res/layout/list_all_caption.xml create mode 100644 app/src/main/res/layout/list_all_items.xml create mode 100644 app/src/main/res/layout/list_all_mails.xml create mode 100644 app/src/main/res/layout/list_courses_quicklinks.xml create mode 100644 app/src/main/res/layout/list_notification_item.xml create mode 100644 app/src/main/res/layout/nav_action_view_counter.xml create mode 100644 app/src/main/res/layout/nav_header_main.xml create mode 100644 app/src/main/res/layout/notification_fragment.xml create mode 100644 app/src/main/res/menu/activity_main_drawer.xml create mode 100644 app/src/main/res/menu/main.xml create mode 100644 app/src/main/res/menu/notification_menu.xml create mode 100644 app/src/main/res/menu/schedule_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values-night-v21/styles.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-notnight-v23/styles.xml create mode 100644 app/src/main/res/values/array.xml create mode 100644 app/src/main/res/values/array_lang.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/drawables.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/auth_pref.xml create mode 100644 app/src/main/res/xml/fuplanner_authenticator.xml create mode 100644 app/src/test/java/de/sebse/fuplanner2/ExampleUnitTest.kt create mode 100644 app/tt.ods create mode 100644 app/your_file.txt create mode 100644 build.gradle create mode 100644 data/Icon-optimized.svg create mode 100644 data/Icon.png create mode 100644 data/Icon.svg create mode 100644 data/Icon.xcf create mode 100644 data/logo_campus.png create mode 100644 data/logo_campus.xcf create mode 100644 data/logo_campus_night.png create mode 100644 data/monochrome-optimized.svg create mode 100644 data/monochrome.svg create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ea113c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle +/local.properties +/.idea/* +/.idea - PC/* +.DS_Store +/build +/captures +.externalNativeBuild +app/release/* diff --git a/FUPlanner 2.iml b/FUPlanner 2.iml new file mode 100644 index 0000000..9c4a891 --- /dev/null +++ b/FUPlanner 2.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..023d1cb --- /dev/null +++ b/app/app.iml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..4edd8dc --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,79 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: 'androidx.navigation.safeargs.kotlin' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + + defaultConfig { + applicationId "de.sebse.fuplanner2" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + //minifyEnabled true + //shrinkResources true + resValue("string", "PORT_NUMBER", "8081") + } + } + + dataBinding { + enabled = true + } + +// To inline the bytecode built with JVM target 1.8 into +// bytecode that is being built with JVM target 1.6. (e.g. navArgs) + + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1' + implementation 'androidx.navigation:navigation-ui-ktx:2.2.1' + implementation 'androidx.fragment:fragment:1.2.4' + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + implementation 'com.android.volley:volley:1.1.1' + implementation 'androidx.room:room-runtime:2.2.5' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + kapt 'androidx.room:room-compiler:2.2.5' + implementation 'androidx.room:room-ktx:2.2.5' + implementation 'com.beust:klaxon:5.0.1' + implementation 'androidx.work:work-runtime-ktx:2.3.4' + implementation 'androidx.paging:paging-runtime:2.1.2' + implementation 'com.github.thellmund.android-week-view:core:4.1.5' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + debugImplementation 'com.amitshekhar.android:debug-db:1.0.6' +} diff --git a/app/demo.kts b/app/demo.kts new file mode 100644 index 0000000..9fc3ed7 --- /dev/null +++ b/app/demo.kts @@ -0,0 +1,27 @@ +import java.lang.StringBuilder +import java.net.HttpURLConnection + +// https://lms.fu-berlin.de/learn/api/v1/courses?fields=id,name,description,courseId,startDate,endDate +// paging.nextPage + +suspend fun sendGet(uri: String): String { + val url = URL(uri) + url + val str = StringBuilder() + with(url.openConnection() as HttpURLConnection) { + requestMethod = "GET" // optional default is GET + addRequestProperty("Cookie", "JSESSIONID=1C7D8F7562968A6693BDD5C5F049D35A; session_id=8286C021DFB6CCA0DB4089A1D31EB422; s_session_id=C2CB57E4AEEFBF6496A09F517CDBDE91; web_client_cache_guid=f6ec07e0-7c39-4d0e-b690-5abab8d6cc06; loginType=shibboleth; JSESSIONID=3929500B4C87181844342524E78BDC89; _shibsession_64656661756c7468747470733a2f2f6c6d732e66752d6265726c696e2e64652f73686962626f6c657468=_a144d509c3e7defaec6a5a7d23d5f00c") + println("\nSent 'GET' request to URL : $url; Response Code : $responseCode") + inputStream.bufferedReader().use { + it.lines().forEach { line -> + str.append(line); + } + } + } + return str.toString() +} + +launch { + val x = sendGet("https://lms.fu-berlin.de/learn/api/v1/courses?fields=id,name,description,courseId,startDate,endDate") + println(x) +} \ No newline at end of file diff --git a/app/demo.py b/app/demo.py new file mode 100644 index 0000000..bf094c4 --- /dev/null +++ b/app/demo.py @@ -0,0 +1,28 @@ +import urllib.request +import json +import urllib.parse + + +url = 'https://lms.fu-berlin.de/learn/api/v1/courses?fields=id,name,description,courseId,startDate,endDate' + +# now, with the below headers, we defined ourselves as a simpleton who is +# still using internet explorer. +headers = {} +headers['Cookie'] = "JSESSIONID=1C7D8F7562968A6693BDD5C5F049D35A; session_id=8286C021DFB6CCA0DB4089A1D31EB422; s_session_id=C2CB57E4AEEFBF6496A09F517CDBDE91; web_client_cache_guid=f6ec07e0-7c39-4d0e-b690-5abab8d6cc06; loginType=shibboleth; JSESSIONID=3929500B4C87181844342524E78BDC89; _shibsession_64656661756c7468747470733a2f2f6c6d732e66752d6265726c696e2e64652f73686962626f6c657468=_a144d509c3e7defaec6a5a7d23d5f00c" + + +with open('your_file.txt', 'w') as f: + f.write("description°courseId°name°id°startDate°endDate") + while url is not None: + req = urllib.request.Request(url, headers = headers) + resp = urllib.request.urlopen(req) + respData = resp.read().decode('utf-8') + j = json.loads(respData) + if len(j['results']) == 0: + url = None + else: + url = 'https://lms.fu-berlin.de' + urllib.parse.unquote_plus(j['paging']['nextPage']) + for result in j['results']: + data = result.get('description', "") + "°" + result.get('courseId', "") + "°" + result.get('name', "") + "°" + result.get('id', "") + "°" + result.get('startDate', "") + "°" + result.get('endDate', "") + "\n" + f.write(data) + print(url) diff --git a/app/demo.ws.kts b/app/demo.ws.kts new file mode 100644 index 0000000..9fdf56e --- /dev/null +++ b/app/demo.ws.kts @@ -0,0 +1,27 @@ +import java.lang.StringBuilder +import java.net.HttpURLConnection +import java.net.URL + +// https://lms.fu-berlin.de/learn/api/v1/courses?fields=id,name,description,courseId,startDate,endDate +// paging.nextPage + +fun sendGet(uri: String): String { + val url = URL(uri) + url + val str = StringBuilder() + with(url.openConnection() as HttpURLConnection) { + requestMethod = "GET" // optional default is GET + addRequestProperty("Cookie", "JSESSIONID=1C7D8F7562968A6693BDD5C5F049D35A; session_id=8286C021DFB6CCA0DB4089A1D31EB422; s_session_id=C2CB57E4AEEFBF6496A09F517CDBDE91; web_client_cache_guid=f6ec07e0-7c39-4d0e-b690-5abab8d6cc06; loginType=shibboleth; JSESSIONID=3929500B4C87181844342524E78BDC89; _shibsession_64656661756c7468747470733a2f2f6c6d732e66752d6265726c696e2e64652f73686962626f6c657468=_a144d509c3e7defaec6a5a7d23d5f00c") + println("\nSent 'GET' request to URL : $url; Response Code : $responseCode") + inputStream.bufferedReader().use { + it.lines().forEach { line -> + str.append(line); + } + } + } + return str.toString() +} + +val x = sendGet("https://lms.fu-berlin.de/learn/api/v1/courses?fields=id,name,description,courseId,startDate,endDate") +println(x) +val parsed = JSONObject(x) \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 diff --git a/app/src/androidTest/java/de/sebse/fuplanner2/ExampleInstrumentedTest.kt b/app/src/androidTest/java/de/sebse/fuplanner2/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..81e4365 --- /dev/null +++ b/app/src/androidTest/java/de/sebse/fuplanner2/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package de.sebse.fuplanner2 + +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("de.sebse.fuplanner2", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3a63c98 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..32514813e028236bec540c0b5d82ab1c3cc5d3ba GIT binary patch literal 43797 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 z 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 XPa~BQ){{vE{O;&KxLtP3HtK%?_M-S1 literal 0 HcmV?d00001 diff --git a/app/src/main/java/de/sebse/fuplanner2/CustomApplication.kt b/app/src/main/java/de/sebse/fuplanner2/CustomApplication.kt new file mode 100644 index 0000000..3fd0886 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/CustomApplication.kt @@ -0,0 +1,15 @@ +package de.sebse.fuplanner2 + +import android.app.Application +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.preferences.AppPreferences + +class CustomApplication: Application() { + override fun onCreate() { + super.onCreate() + AppDatabase.initialize(applicationContext) + AppPreferences.initialize(applicationContext) + AppAccounts.initialize(applicationContext) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/MainActivity.kt b/app/src/main/java/de/sebse/fuplanner2/MainActivity.kt new file mode 100644 index 0000000..e8b6f02 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/MainActivity.kt @@ -0,0 +1,146 @@ +package de.sebse.fuplanner2 + +import android.accounts.Account +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.lifecycle.* +import androidx.navigation.NavOptions +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.utils.console +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.app_bar_main.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + + +class MainActivity() : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var activityViewModel: MainActivityViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + setSupportActionBar(toolbar) + + val navController = findNavController(R.id.nav_host_fragment) + activityViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java) + + activityViewModel.user.observe(this) { + nav_view.getHeaderView(0).run { + findViewById(R.id.nav_header_title).text = getString(R.string.full_name, it?.firstName, it?.lastName) + findViewById(R.id.nav_header_subtitle).text = it?.email + } + } + activityViewModel.notificationCnt.observe(this) { + nav_view.menu.findItem(R.id.nav_notifications).apply { + icon = ContextCompat.getDrawable( + this@MainActivity, + if (it == 0) R.drawable.ic_menu_notifications_none else R.drawable.ic_menu_notifications + ) + actionView = if (it == 0) + null + else layoutInflater.inflate(R.layout.nav_action_view_counter, nav_view, false).apply { + findViewById(R.id.counterText).text = when (it) { + in 0..99 -> it.toString() + else -> "99+" + } + } + } + } + activityViewModel.latestSemester.observe(this) { + val courseOrder = nav_view.menu.findItem(R.id.nav_courses).order + var i = nav_view.menu.size() - 1 + while(i >= 0) { + val menuItem: MenuItem = nav_view.menu.getItem(i--) + if (menuItem.order / 100 == courseOrder / 100 && menuItem.order != courseOrder) { + nav_view.menu.removeItem(menuItem.itemId) + } + } + it.mapIndexed { index, course -> + val itemOrder = courseOrder / 100 * 100 + index + 2 + nav_view.menu.add(0, itemOrder, itemOrder, course.title).setOnMenuItemClickListener { + navController.navigate( + R.id.course_details, + bundleOf("courseId" to course.uid, "title" to course.title), + NavOptions.Builder().setPopUpTo(R.id.nav_courses, false).build() + ) + drawer_layout.closeDrawers() + false + } + } + } + + appBarConfiguration = AppBarConfiguration( + setOf(R.id.nav_courses, R.id.nav_canteen, R.id.nav_schedule, R.id.nav_notifications), + drawer_layout + ) + setupActionBarWithNavController(navController, appBarConfiguration) + nav_view.setupWithNavController(navController) + + if (intent.getBooleanExtra(EXTRA_OPEN_NOTIFICATIONS, false)) { + navController.navigate(R.id.nav_notifications) + intent.putExtra(EXTRA_OPEN_NOTIFICATIONS, false) + } + } + + override fun onStart() { + super.onStart() + val accounts = AppAccounts.getInstance() + val selectedAccount = accounts.selectedAccount + if (selectedAccount == null) { + val intent = Intent(this, StartupActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME + startActivity(intent) + finish() + return + } + activityViewModel.updateSelectedUser(selectedAccount) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + //menuInflater.inflate(R.menu.main, menu) + return true + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment) + return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() + } + + companion object { + const val EXTRA_OPEN_NOTIFICATIONS: String = "EXTRA_OPEN_NOTIFICATIONS" + const val EXTRA_NETWORK_ERROR = "EXTRA_NETWORK_ERROR" + const val EXTRA_UNSPECIFIED_ERROR = "EXTRA_UNSPECIFIED_ERROR" + } +} + +class MainActivityViewModel : ViewModel() { + val database = AppDatabase.getInstance() + val user: MutableLiveData = MutableLiveData().apply { + value = null + } + val notificationCnt: LiveData = database.notificationDao().getUnreadRowCount() + val latestSemester: LiveData> = database.courseDao().getLatestSemester() + + fun updateSelectedUser(account: Account) { + GlobalScope.launch { + user.postValue(database.userDao().findByUsername(account.name)) + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/StartupActivity.kt b/app/src/main/java/de/sebse/fuplanner2/StartupActivity.kt new file mode 100644 index 0000000..22870f6 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/StartupActivity.kt @@ -0,0 +1,71 @@ +package de.sebse.fuplanner2 + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.auth.FuplannerAccountActivity +import de.sebse.fuplanner2.auth.FuplannerAccountConstants +import de.sebse.fuplanner2.utils.Notifications + + +class StartupActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_startup) + } + + override fun onStart() { + super.onStart() + + // has accounts? -> false: goto AccountActivity (add=true) / exit for result + // get selected account -> false: select other + // refresh token -> false: goto AccountActivity (add=false, pre-fill username) / exit + // goto MainActivity + + + Notifications.init(applicationContext) + val accounts = AppAccounts.getInstance() + val selectedAccount = accounts.selectedAccount + if (selectedAccount == null) { + val intent = FuplannerAccountActivity.createIntent( + this, + true, + FuplannerAccountConstants.FU_ACC_TYPE + ) + startActivityForResult(intent, LAUNCH_LOGIN_RESULT) + } else { + accounts.refresh(selectedAccount) { + when (it) { + AppAccounts.RefreshResults.UNSPECIFIED_ERROR, + AppAccounts.RefreshResults.NETWORK_ERROR, + AppAccounts.RefreshResults.SUCCESS -> { + AppAccounts.getInstance().setPeriodicSync() + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME + if (it == AppAccounts.RefreshResults.NETWORK_ERROR) + intent.putExtra(MainActivity.EXTRA_NETWORK_ERROR, true) + if (it == AppAccounts.RefreshResults.UNSPECIFIED_ERROR) + intent.putExtra(MainActivity.EXTRA_UNSPECIFIED_ERROR, true) + startActivity(intent) + } + AppAccounts.RefreshResults.INVALID_PASSWORD -> { + val intent = FuplannerAccountActivity.createIntent( + this, + true, + accountType = selectedAccount.type, + accountName = selectedAccount.name + ) + startActivity(intent) + } + } + finish() + } + } + } + + companion object { + const val LAUNCH_LOGIN_RESULT = 111 + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/AccountAuthenticatorAppCompatActivity.kt b/app/src/main/java/de/sebse/fuplanner2/auth/AccountAuthenticatorAppCompatActivity.kt new file mode 100644 index 0000000..add6c3d --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/AccountAuthenticatorAppCompatActivity.kt @@ -0,0 +1,42 @@ +package de.sebse.fuplanner2.auth + +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + + +@SuppressLint("Registered") +open class AccountAuthenticatorAppCompatActivity : AppCompatActivity() { + private var mAccountAuthenticatorResponse: AccountAuthenticatorResponse? = null + private var mResultBundle: Bundle? = null + fun setAccountAuthenticatorResult(result: Bundle?) { + mResultBundle = result + } + + override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + mAccountAuthenticatorResponse = + intent.getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) + if (mAccountAuthenticatorResponse != null) { + mAccountAuthenticatorResponse!!.onRequestContinued() + } + } + + override fun finish() { + if (mAccountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (mResultBundle != null) { + mAccountAuthenticatorResponse!!.onResult(mResultBundle) + } else { + mAccountAuthenticatorResponse!!.onError( + AccountManager.ERROR_CODE_CANCELED, + "canceled" + ) + } + mAccountAuthenticatorResponse = null + } + super.finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/AppAccounts.kt b/app/src/main/java/de/sebse/fuplanner2/auth/AppAccounts.kt new file mode 100644 index 0000000..a083eca --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/AppAccounts.kt @@ -0,0 +1,148 @@ +package de.sebse.fuplanner2.auth + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.AuthenticatorException +import android.content.Context +import android.os.Bundle +import androidx.work.* +import de.sebse.fuplanner2.preferences.AppPreferences +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.worker.SyncWorker +import java.io.IOException +import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class AppAccounts(val context: Context) { + private val accountManager = AccountManager.get(context) + private val workManager = WorkManager.getInstance(context) + + val list: Array + get() = accountManager.getAccountsByType(FuplannerAccountConstants.FU_ACC_TYPE) + + var selectedAccount: Account? + get() { + val preferences = AppPreferences.getInstance() + val accountName = preferences.getString(PREF_SELECTED_ACCOUNT) + return getAccount(accountName) + ?.also { preferences.set(PREF_SELECTED_ACCOUNT, it.name ) } + } + set(value) { + val preferences = AppPreferences.getInstance() + value + ?.run { preferences.set(PREF_SELECTED_ACCOUNT, name ) } + ?: preferences.remove(PREF_SELECTED_ACCOUNT) + } + + fun hasAccounts(): Boolean { + return list.isNotEmpty() + } + + fun hasAccountName(name: String, accountType: String? = null): Boolean { + return accountManager.getAccountsByType(accountType ?: FuplannerAccountConstants.FU_ACC_TYPE).find { it.name == name } != null + } + + suspend fun refreshSuspended(account: Account): RefreshResults = suspendCoroutine { res -> refresh(account) { res.resume(it) } } + + fun refresh(account: Account, cb: (RefreshResults) -> Unit) { + val authToken: String? = accountManager.peekAuthToken(account, FuplannerAccountConstants.FU_ACC_TOKEN_TYPE) + accountManager.invalidateAuthToken(FuplannerAccountConstants.FU_ACC_TYPE, authToken) + accountManager.getAuthToken( + account, + FuplannerAccountConstants.FU_ACC_TOKEN_TYPE, + null,true, { + try { + it.result + cb(RefreshResults.SUCCESS) + } catch (e: AuthenticatorException) { + if (e.message == FuplannerAccountConstants.MSG_INVALID_CREDENTIALS) { + cb(RefreshResults.INVALID_PASSWORD) + } else { + console.error(e) + cb(RefreshResults.UNSPECIFIED_ERROR) + } + } catch (e: IOException) { + console.error(e) + cb(RefreshResults.NETWORK_ERROR) + } catch (e: Throwable) { + console.error(e) + cb(RefreshResults.UNSPECIFIED_ERROR) + } + }, null) + } + + fun setPassword(account: Account, passWd: String?) = accountManager.setPassword(account, passWd) + fun addAccountExplicitly(account: Account, password: String?, bundle: Bundle?) { + accountManager.addAccountExplicitly(account, password, bundle) + } + + fun setAuthToken(account: Account, tokenType: String?, authToken: String?) { + accountManager.setAuthToken(account, tokenType, authToken) + } + + fun getAccount(accountName: String?): Account? { + val account = list.find { it.name == accountName } + return account?.let { return it } + ?: list.getOrNull(0) + } + + fun setPeriodicSync() { + workManager.cancelAllWorkByTag(TAG_SYNC) + val preferences = AppPreferences.getInstance() + val syncPeriod = preferences.getLong(PREF_SYNC_PERIOD_MINUTES) ?: 15L + if (syncPeriod != null) { + list.forEach { + val syncWork = PeriodicWorkRequestBuilder(syncPeriod, TimeUnit.MINUTES) + .setInputData(workDataOf(SyncWorker.KEY_ACCOUNT_NAME to it.name)) + .setConstraints(Constraints.Builder() + //.setRequiresDeviceIdle(true) + .setRequiresStorageNotLow(true) + //.setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build() + ) + .addTag(TAG_SYNC) + .addTag(it.name) + .build() + workManager.enqueue(syncWork) + } + } else { + console.warn("No sync period set! Removed all accounts from syncing!") + } + } + + enum class RefreshResults { + SUCCESS, + INVALID_PASSWORD, + NETWORK_ERROR, + UNSPECIFIED_ERROR + } + + + companion object { + const val PREF_SELECTED_ACCOUNT = "PREF_SELECTED_ACCOUNT" + const val PREF_SYNC_PERIOD_MINUTES = "PREF_SYNC_PERIOD_MINUTES" + const val TAG_SYNC = "TAG_SYNC" + + private var sInstance: AppAccounts? = null + + fun initialize(context: Context): AppAccounts? { + if (sInstance == null) { + synchronized(AppAccounts::class.java) { + if (sInstance == null) { + sInstance = AppAccounts(context.applicationContext) + } + } + } + return sInstance + } + + fun getInstance(): AppAccounts { + sInstance?.let { + return it + } + throw NullPointerException("Please call initialize() before getting the instance.") + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/FUAuthModule.kt b/app/src/main/java/de/sebse/fuplanner2/auth/FUAuthModule.kt new file mode 100644 index 0000000..d89f7c0 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/FUAuthModule.kt @@ -0,0 +1,84 @@ +package de.sebse.fuplanner2.auth + +import android.content.Context +import com.android.volley.Header +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.network.NetData +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.network.tools.invalidPassword +import de.sebse.fuplanner2.network.tools.invalidResponse +import de.sebse.fuplanner2.utils.xml +import java.net.URI +import java.util.concurrent.TimeUnit +import kotlin.random.Random + + +abstract class FUAuthModule { + companion object { + // 7-10 days (random to reduce heap load) + val REFRESH_FREQUENCY_COURSES_MILLIS = TimeUnit.HOURS.toMillis(7*24) + Random.nextLong(TimeUnit.HOURS.toMillis(3*24)) + } + + abstract suspend fun isAvailable(ctx: Context, name: String): Boolean + abstract suspend fun login(ctx: Context, name: String, password: String, user: User) + + suspend fun doSaml(ctx: Context, samlUrl: String, name: String, password: String, user: User): SamlReponse { + val requester = Requester(ctx) + var response = requester.get(samlUrl, getCookies(user)) + updateCookies(user, response) + if (response.networkResponse.statusCode == 200) { + return parseResponse(response.body) + } else { + val relLocation = response.headers["Location"] + ?: throw invalidResponse(100110, "No IDP form location!") + val formUri = URI(samlUrl).resolve(relLocation).toString() + requester.head(formUri, getCookies(user)) + response = requester.post( + formUri, + cookies = getCookies(user), + data = hashMapOf("j_username" to name, "j_password" to password, "_eventId_proceed" to "") + ) + if (response.networkResponse.statusCode != 200) { + throw invalidPassword(100111, "Password or username invalid!") + } + } + return parseResponse(response.body) + } + + protected fun parseCookies(headers: List
): HashMap { + val result: HashMap = hashMapOf() + headers + .filter { it.name == "Set-Cookie" } + .forEach { + result[it.value.substringBefore("=")] = it.value.substringAfter("=").substringBefore(";") + } + return result + } + + private fun parseResponse(body: String): SamlReponse { + var matcher = "name=\"SAMLResponse\" value=\"(.*?)\"".toRegex().find(body) + val samlResponse = matcher?.groupValues?.let { + if (it.size >= 2) it[1] else null + } ?: throw invalidResponse(100100, "No SAML response found!") + matcher = "name=\"RelayState\" value=\"(.*?)\"".toRegex().find(body) + val relayState = matcher?.groupValues?.let { + if (it.size >= 2) it[1] else null + } ?: throw invalidResponse(100100, "No Relay State found!") + matcher = "form action=\"(.*?)\"".toRegex().find(body) + val url = matcher?.groupValues?.let { + if (it.size >= 2) it[1] else null + } ?: throw invalidResponse(100100, "No SAML Url found!") + return SamlReponse(xml.decode(url), xml.decode(relayState), xml.decode(samlResponse)) + } + + private fun updateCookies(user: User, response: NetData) { + val setCookies = parseCookies(response.networkResponse.allHeaders) + setCookies["JSESSIONID"]?.let { + user.cookies.idpJsessionId = it + } + } + + private fun getCookies(user: User): HashMap? { + return user.cookies.idpJsessionId?.let { key -> hashMapOf("JSESSIONID" to key) } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountActivity.kt b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountActivity.kt new file mode 100644 index 0000000..48f17f7 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountActivity.kt @@ -0,0 +1,116 @@ +package de.sebse.fuplanner2.auth + +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.EditText +import android.widget.TextView +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.utils.console +import kotlinx.coroutines.* + + +class FuplannerAccountActivity : AccountAuthenticatorAppCompatActivity() { + private val REQ_REGISTER = 11 + override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + setContentView(R.layout.activity_login) + + } + + @Suppress("UNUSED_PARAMETER") + fun login(view: View?) { + val userId = + (findViewById(R.id.user) as EditText).text.toString() + val passWd = + (findViewById(R.id.password) as EditText).text.toString() + findViewById(R.id.login).isEnabled = false + + val accountType = intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) + + val result = Intent() + GlobalScope.async { + // Check if account is already added + val accounts = AppAccounts.getInstance() + if (accounts.hasAccountName(userId, accountType)) { + throw IllegalArgumentException("Account already bound!") + } + + val authToken = FuplannerAccountHelper.authenticate(applicationContext, userId, passWd, addNewUser = intent.getBooleanExtra(FuplannerAccountConstants.KEY_ADD_ACCOUNT, false)) + val tokenType: String = FuplannerAccountHelper.getTokenType(userId) + val data = Bundle() + data.putString(AccountManager.KEY_ACCOUNT_NAME, userId) + data.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType) + data.putString(FuplannerAccountConstants.KEY_AUTH_TOKEN_TYPE, tokenType) + data.putString(AccountManager.KEY_AUTHTOKEN, authToken) + data.putString(FuplannerAccountConstants.KEY_PASSWORD, passWd) + result.putExtras(data) + }.invokeOnCompletion { + if (it !== null) { + console.error("An error occured!", it) + runOnUiThread { + findViewById(R.id.user).text.clear() + findViewById(R.id.user).setText("seedorf96", TextView.BufferType.EDITABLE) + findViewById(R.id.password).text.clear() + findViewById(R.id.password).setText("m&gcwBaT@", TextView.BufferType.EDITABLE) + findViewById(R.id.login).isEnabled = true + } + } else if (!this.isFinishing) { + this.setLoginResult(result) + } + } + } + + private fun setLoginResult(intent: Intent) { + val userId = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) + val passWd = intent.getStringExtra(FuplannerAccountConstants.KEY_PASSWORD) + val account = Account(userId, intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE)) + val accounts = AppAccounts.getInstance() + if (getIntent().getBooleanExtra(FuplannerAccountConstants.KEY_ADD_ACCOUNT, false)) { + val authtoken = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN) + val tokenType = + intent.getStringExtra(FuplannerAccountConstants.KEY_AUTH_TOKEN_TYPE) + accounts.addAccountExplicitly(account, passWd, null) + accounts.setAuthToken(account, tokenType, authtoken) + } else { + accounts.setPassword(account, passWd) + } + setAccountAuthenticatorResult(intent.extras) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent? + ) { + if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQ_REGISTER) { + setLoginResult(data) + } else super.onActivityResult(requestCode, resultCode, data) + } + + companion object { + fun createIntent(activityCtx: Context, addAccount: Boolean, + accountType: String? = null, + accountName: String? = null, + response: AccountAuthenticatorResponse? = null, + authTokenType: String? = null + ): Intent { + return Intent(activityCtx, FuplannerAccountActivity::class.java).run { + putExtra(FuplannerAccountConstants.KEY_ADD_ACCOUNT, addAccount) + if (response != null) + putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + if (accountName != null) + putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName) + putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType ?: FuplannerAccountConstants.FU_ACC_TYPE) + putExtra(FuplannerAccountConstants.KEY_AUTH_TOKEN_TYPE, authTokenType ?: FuplannerAccountConstants.FU_ACC_TOKEN_TYPE) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountAuthenticator.kt b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountAuthenticator.kt new file mode 100644 index 0000000..7d43267 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountAuthenticator.kt @@ -0,0 +1,150 @@ +package de.sebse.fuplanner2.auth + +import android.accounts.* +import android.accounts.AccountManager.KEY_BOOLEAN_RESULT +import android.content.Context +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import com.android.volley.VolleyError +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async + + +class FuplannerAccountAuthenticator(ctx: Context) : AbstractAccountAuthenticator(ctx) { + private val context: Context = ctx.applicationContext + override fun editProperties( + accountAuthenticatorResponse: AccountAuthenticatorResponse, + s: String + ): Bundle? { + return null + } + + @Throws(NetworkErrorException::class) + override fun addAccount( + response: AccountAuthenticatorResponse?, + accountType: String?, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle? + ): Bundle { + return Bundle().apply { + val intent = FuplannerAccountActivity.createIntent( + context, + true, + accountType, + response = response, + authTokenType = authTokenType + ) + putParcelable(AccountManager.KEY_INTENT, intent) + } + } + + @Throws(NetworkErrorException::class) + override fun confirmCredentials( + accountAuthenticatorResponse: AccountAuthenticatorResponse, + account: Account, + bundle: Bundle + ): Bundle? { + return null + } + + @Throws(NetworkErrorException::class) + override fun getAuthToken( + response: AccountAuthenticatorResponse, + account: Account, + authTokenType: String, + options: Bundle + ): Bundle? { + val accountManager = AccountManager.get(context) + var authToken = accountManager.peekAuthToken(account, authTokenType) + if (TextUtils.isEmpty(authToken)) { + val password = accountManager.getPassword(account) + if (password != null) { + val result = Bundle() + GlobalScope.async { + authToken = FuplannerAccountHelper.authenticate( + context, + account.name, + password, + false + ) + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authToken) + }.invokeOnCompletion { + if (it == null) { + response.onResult(result) + return@invokeOnCompletion + } + var message = it.message + val code = if (it is VolleyError) { + when (it.networkResponse.statusCode) { + 403 -> { // Auth failed / wrong credentials + message = FuplannerAccountConstants.MSG_INVALID_CREDENTIALS + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) + AccountManager.ERROR_CODE_BAD_AUTHENTICATION + else + AccountManager.ERROR_CODE_BAD_ARGUMENTS + } + 422 // Processing failed; MAYBE auth failed / wrong credentials + -> AccountManager.ERROR_CODE_CANCELED + 418 // Timeout + -> AccountManager.ERROR_CODE_NETWORK_ERROR + else // Other network error + -> AccountManager.ERROR_CODE_NETWORK_ERROR + } + } else { // Other undefined error + AccountManager.ERROR_CODE_CANCELED + } + response.onError(code, message) + } + return null + } else { + return Bundle().apply { + val intent = FuplannerAccountActivity.createIntent( + context, + true, + account.type, + accountName = account.name, + response = response, + authTokenType = authTokenType + ) + putParcelable(AccountManager.KEY_INTENT, intent) + } + } + } else { + // Token found and returned + val result = Bundle() + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authToken) + return result + } + } + + override fun getAuthTokenLabel(s: String): String { + return "full" + } + + @Throws(NetworkErrorException::class) + override fun updateCredentials( + accountAuthenticatorResponse: AccountAuthenticatorResponse, + account: Account, + s: String, + bundle: Bundle + ): Bundle? { + return null + } + + @Throws(NetworkErrorException::class) + override fun hasFeatures( + accountAuthenticatorResponse: AccountAuthenticatorResponse, + account: Account, + strings: Array + ): Bundle { + val result = Bundle() + result.putBoolean(KEY_BOOLEAN_RESULT, false) + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountConstants.kt b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountConstants.kt new file mode 100644 index 0000000..1285957 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountConstants.kt @@ -0,0 +1,10 @@ +package de.sebse.fuplanner2.auth + +object FuplannerAccountConstants { + const val MSG_INVALID_CREDENTIALS = "Invalid credentials provided!" + const val KEY_ADD_ACCOUNT = "key_add_account" + const val KEY_AUTH_TOKEN_TYPE = "key_auth_token_type" + const val KEY_PASSWORD = "key_password" + const val FU_ACC_TOKEN_TYPE = "fuauth" + const val FU_ACC_TYPE = "de.sebse.fuauth" +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountHelper.kt b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountHelper.kt new file mode 100644 index 0000000..c8bb86a --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountHelper.kt @@ -0,0 +1,58 @@ +package de.sebse.fuplanner2.auth + +import android.content.Context +import de.sebse.fuplanner2.blackboard.Blackboard +import de.sebse.fuplanner2.blackboard.BlackboardInfo +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.whiteboard.Whiteboard +import de.sebse.fuplanner2.whiteboard.WhiteboardInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +object FuplannerAccountHelper { + suspend fun authenticate( + ctx: Context, + name: String, + password: String, + addNewUser: Boolean + ): String { + val database = AppDatabase.getInstance() + if (addNewUser) { + console.warn("Previous user account will be removed!") + removeAccount(name) + } + val user = database.userDao().findByUsername(name) ?: User( + cookies = UserCookies(), + bbInfo = BlackboardInfo(), + wbInfo = WhiteboardInfo(), + userName = name + ) + + var available = Blackboard.isAvailable(ctx, name) + if (available) + Blackboard.login(ctx, name, password, user) + + available = Whiteboard.isAvailable(ctx, name) + if (available) { + Whiteboard.login(ctx, name, password, user) + } + + database.userDao().upsert(user) + + return user.toString() + } + + fun getTokenType(userId: String): String { + return FuplannerAccountConstants.FU_ACC_TOKEN_TYPE + } + + suspend fun removeAccount(userName: String) { + val database = AppDatabase.getInstance() + withContext(Dispatchers.Default) { + database.userDao().deleteByUserName(userName) + } + } + +} diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountService.kt b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountService.kt new file mode 100644 index 0000000..f3a03f0 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/FuplannerAccountService.kt @@ -0,0 +1,14 @@ +package de.sebse.fuplanner2.auth + +import android.app.Service +import android.content.Intent + +import android.os.IBinder + + +class FuplannerAccountService: Service() { + override fun onBind(intent: Intent?): IBinder? { + val authenticator = FuplannerAccountAuthenticator(this) + return authenticator.getIBinder() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/SamlReponse.kt b/app/src/main/java/de/sebse/fuplanner2/auth/SamlReponse.kt new file mode 100644 index 0000000..b7ad168 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/SamlReponse.kt @@ -0,0 +1,3 @@ +package de.sebse.fuplanner2.auth + +data class SamlReponse(val uri: String, val relayState: String, val samlResponse: String) diff --git a/app/src/main/java/de/sebse/fuplanner2/auth/UserCookies.kt b/app/src/main/java/de/sebse/fuplanner2/auth/UserCookies.kt new file mode 100644 index 0000000..21c7346 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/auth/UserCookies.kt @@ -0,0 +1,56 @@ +package de.sebse.fuplanner2.auth + +import org.json.JSONObject + +class UserCookies { + var idpJsessionId: String? = null + var wbJsessionId: String? = null + var wbShibKey: String? = null + var wbShibValue: String? = null + var bbJsessionId: String? = null + var bbSessionId: String? = null + var bbSSessionId: String? = null + var bbShibKey: String? = null + var bbShibValue: String? = null + + constructor() { } + + constructor(json: String) { + val obj = JSONObject(json) + idpJsessionId = obj.opt("idpJsessionId") as String? + wbJsessionId = obj.opt("wbJsessionId") as String? + wbShibKey = obj.opt("wbShibKey") as String? + wbShibValue = obj.opt("wbShibValue") as String? + bbJsessionId = obj.opt("bbJsessionId") as String? + bbSessionId = obj.opt("bbSessionId") as String? + bbSSessionId = obj.opt("bbSSessionId") as String? + bbShibKey = obj.opt("bbShibKey") as String? + bbShibValue = obj.opt("bbShibValue") as String? + } + + fun update(user: UserCookies) { + idpJsessionId = user.idpJsessionId?: idpJsessionId + wbJsessionId = user.wbJsessionId?: wbJsessionId + wbShibKey = user.wbShibKey?: wbShibKey + wbShibValue = user.wbShibValue?: wbShibValue + bbJsessionId = user.bbJsessionId?: bbJsessionId + bbSessionId = user.bbSessionId?: bbSessionId + bbSSessionId = user.bbSSessionId?: bbSSessionId + bbShibKey = user.bbShibKey?: bbShibKey + bbShibValue = user.bbShibValue?: bbShibValue + } + + override fun toString(): String { + val obj = JSONObject() + obj.put("idpJsessionId", idpJsessionId) + obj.put("wbJsessionId", wbJsessionId) + obj.put("wbShibKey", wbShibKey) + obj.put("wbShibValue", wbShibValue) + obj.put("bbJsessionId", bbJsessionId) + obj.put("bbSessionId", bbSessionId) + obj.put("bbSSessionId", bbSSessionId) + obj.put("bbShibKey", bbShibKey) + obj.put("bbShibValue", bbShibValue) + return obj.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/blackboard/Blackboard.kt b/app/src/main/java/de/sebse/fuplanner2/blackboard/Blackboard.kt new file mode 100644 index 0000000..fb4d0fb --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/blackboard/Blackboard.kt @@ -0,0 +1,145 @@ +package de.sebse.fuplanner2.blackboard + +import android.content.Context +import android.net.Uri +import com.beust.klaxon.JsonArray +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.auth.FUAuthModule +import de.sebse.fuplanner2.auth.SamlReponse +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.network.NetData +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.network.tools.invalidResponse +import java.net.URI +import java.util.concurrent.TimeUnit +import kotlin.random.Random + +object Blackboard: FUAuthModule() { + private const val LOGIN_URL = "https://lms.fu-berlin.de/lms-apps/login/sso/index.php" + private const val TEST_URL = "https://lms.fu-berlin.de/learn/api/public/v1/users?userName=%s&fields=id,userName,studentId,name,contact" + private const val RESTORE_SESSION_URI = "https://lms.fu-berlin.de" + internal const val MODULE_TYPE = 1 + + internal const val FETCH_COURSE_MEMBERSHIP = "https://lms.fu-berlin.de/learn/api/public/v1/users/%s/courses?limit=200&fields=courseId" // userId + internal const val FETCH_COURSE_DETAILS = "https://lms.fu-berlin.de/learn/api/v1/courses/%s?fields=displayName,description,courseId,startDate,endDate" // courseId + internal const val FETCH_COURSE_USERS = "https://lms.fu-berlin.de/learn/api/public/v1/courses/%s/users?fields=courseRoleId,userId&limit=200" // courseId + internal const val FETCH_USER_DETAILS = "https://lms.fu-berlin.de/learn/api/public/v1/users/%s?fields=name,userName" // lecturerId + + internal const val FETCH_VV_COURSE_ID = "https://www.fu-berlin.de/vv/de/search?utf8=✓&query=%s" + internal const val FETCH_VV_COURSE = "https://www.fu-berlin.de/vv/de/lv/%s" + // 14-21 days (random to reduce heap load) + val REFRESH_FREQUENCY_EVENT_CACHE_MILLIS = TimeUnit.HOURS.toMillis(14*24) + + Random.nextLong(TimeUnit.HOURS.toMillis(21*24)) + + + internal const val FETCH_ANNOUNCEMENT_LIST = "https://lms.fu-berlin.de/learn/api/v1/courses/%s/announcements?fields=id,title,body.rawText,startDateRestriction,creatorUserId" // courseId + + override suspend fun isAvailable(ctx: Context, name: String): Boolean { + return true + } + + override suspend fun login(ctx: Context, name: String, password: String, user: User) { + val requester = Requester(ctx) + var response = requester.head(LOGIN_URL, cookies = getCookies(user, shib = true)) + val samlUri = response.headers["Location"] + ?: throw invalidResponse(101100, "Location header not set!") + + if (!samlUri.startsWith(RESTORE_SESSION_URI)) { + val samlResponse: SamlReponse = doSaml(ctx, samlUri, name, password, user) + + // Shib-Session-Cookie + response = requester.post(samlResponse.uri, cookies = null, data = hashMapOf( + "RelayState" to samlResponse.relayState, + "SAMLResponse" to samlResponse.samlResponse + )) + updateCookies(user, response) + // Finish BB + response = requester.get( + response.networkResponse.headers["Location"] ?: throw invalidResponse(101101, "No Location header to finish Blackboard"), + getCookies(user, shib = true) + ) + } + // Start Session + response = requester.get( + response.networkResponse.headers["Location"] ?: throw invalidResponse(101102, "No Location header to start Blackboard session"), + getCookies(user, shib = true) + ) + + updateCookies(user, response) + // Set API JSESSION + response = requester.get(String.format(TEST_URL, Uri.encode(name)), getCookies(user, shib = true)) + updateCookies(user, response) + user.bbInfo.id = (Parser.default().parse(StringBuilder(response.body)) as JsonObject) + .array("result") + ?.find { entry -> entry["userName"] == name } + ?.string("id") + + (Parser.default().parse(StringBuilder(response.body)) as JsonObject) + .array("results") + ?.find { entry -> entry["userName"] == name } + ?.run { + user.bbInfo.id = string("id") ?: user.wbInfo.id + user.email = obj("contact")?.string("email") ?: user.email + user.matNumber = string("studentId")?.toIntOrNull() ?: user.matNumber + user.firstName = obj("name")?.string("given") ?: user.firstName + user.lastName = obj("name")?.string("family") ?: user.lastName + } + } + + private fun updateCookies(user: User, response: NetData) { + val setCookies = parseCookies(response.networkResponse.allHeaders) + setCookies["JSESSIONID"]?.let { + user.cookies.bbJsessionId = it + } + setCookies["session_id"]?.let { + user.cookies.bbSessionId = it + } + setCookies["s_session_id"]?.let { + user.cookies.bbSSessionId = it + } + setCookies + .filter{ (key, _) -> key.startsWith("_shibsession_") } + .forEach { (key, value) -> + user.cookies.bbShibKey = key + user.cookies.bbShibValue = value + return@forEach + } + } + + internal fun getCookies(user: User, shib: Boolean = true): HashMap? { + val cookies = user.cookies.bbJsessionId?.let { key -> hashMapOf("JSESSIONID" to key) } ?: hashMapOf() + user.cookies.bbSessionId?.let { key -> cookies["session_id"] = key } + user.cookies.bbSSessionId?.let { key -> cookies["s_session_id"] = key } + if (shib && user.cookies.bbShibValue != null) { + user.cookies.bbShibKey?.let { cookies[it] = user.cookies.bbShibValue ?: "" } + } + return cookies + } + + internal fun isAvailable(user: User): Boolean { + return user.cookies.bbJsessionId != null && user.cookies.bbSessionId != null && user.cookies.bbSSessionId != null + } + + internal suspend fun requestList( + requester: Requester, user: User, + entryUri: String, + arrayPath: ((JsonObject) -> JsonArray?), + nextPage: ((JsonObject) -> String?) + ): Array { + var data: Array = arrayOf() + var url: String = entryUri + do { + val json = requester.get(url, getCookies(user)).let { + Parser.default().parse(StringBuilder(it.body)) as JsonObject + } + val next = nextPage(json) ?: break + if (next.isEmpty()) break + url = URI(url).resolve(next).toString() + val results = arrayPath(json)?.toTypedArray() ?: arrayOf() + if (results.isEmpty()) break + data += results + } while (true) + return data + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/blackboard/BlackboardInfo.kt b/app/src/main/java/de/sebse/fuplanner2/blackboard/BlackboardInfo.kt new file mode 100644 index 0000000..65c7c06 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/blackboard/BlackboardInfo.kt @@ -0,0 +1,24 @@ +package de.sebse.fuplanner2.blackboard + +import org.json.JSONObject + +class BlackboardInfo { + var id: String? = null + + constructor() { } + + constructor(json: String) { + val obj = JSONObject(json) + id = obj.opt("id") as String? + } + + fun update(user: BlackboardInfo) { + id = user.id?: id + } + + override fun toString(): String { + val obj = JSONObject() + obj.put("id", id) + return obj.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtAnnouncements.kt b/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtAnnouncements.kt new file mode 100644 index 0000000..ce4be21 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtAnnouncements.kt @@ -0,0 +1,62 @@ +package de.sebse.fuplanner2.blackboard + +import android.content.Context +import com.android.volley.VolleyError +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.auth.FUAuthModule.Companion.REFRESH_FREQUENCY_COURSES_MILLIS +import de.sebse.fuplanner2.database.* +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.utils.* +import de.sebse.fuplanner2.worker.FetchResourceErrorType +import de.sebse.fuplanner2.worker.FetchResourceException + + +suspend fun Blackboard.getAnnouncements(ctx: Context, database: AppDatabase, user: User, course: Course): UpdateResult { + if (!isAvailable(user)) return UpdateResult() + if (course.moduleType != MODULE_TYPE) return UpdateResult() + val courseId = course.uid ?: return UpdateResult() + + val requester = Requester(ctx) + val stored = database.announcementDao().getAll2(courseId) + + try {val data = requester.get( + FETCH_ANNOUNCEMENT_LIST.format(course.internalId), + getCookies(user) + ) + val json = Parser.default().parse(StringBuilder(data.body)) as JsonObject + val new = json.array("results")?.pmap { obj -> + val id: String = obj.string("id") ?: return@pmap null + val title: String = obj.string("title") ?: "" + val body: String = obj.obj("body")?.string("rawText") ?: "" + val createdOn: Long = obj.string("startDateRestriction")?.dateStringToLong("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") ?: 0 + val createdBy: String = obj.string("creatorUserId")?.let { lecturerUserId -> + Cache.getCache(database, lecturerUserId, "Blackboard_Users", REFRESH_FREQUENCY_COURSES_MILLIS) { + requester.get( + FETCH_USER_DETAILS.format(lecturerUserId), + getCookies(user) + ).body + }.let { Parser.default().parse(StringBuilder(it)) as JsonObject } + .let { + val given = it.obj("name")?.string("given") + val family = it.obj("name")?.string("family") + if (given != null && family != null) + "$given $family" + else + null + } + } ?: "" + val attachments: List = listOf() + Announcement(null, courseId, System.currentTimeMillis(), id, title, body, createdOn, createdBy, attachments) + }?.filterNotNull() ?: listOf() + val result = updateResultOf(stored, new) + database.announcementDao().run { + delete(result.removed) + upsert(result.values) + } + return result + } catch (e: VolleyError) { + val statusCode = e.networkResponse.statusCode + throw FetchResourceException(FetchResourceErrorType.ERR_NETWORK_ERROR, statusCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtCourses.kt b/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtCourses.kt new file mode 100644 index 0000000..e8f71e2 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtCourses.kt @@ -0,0 +1,159 @@ +package de.sebse.fuplanner2.blackboard + +import android.content.Context +import com.android.volley.VolleyError +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.auth.FUAuthModule.Companion.REFRESH_FREQUENCY_COURSES_MILLIS +import de.sebse.fuplanner2.database.* +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.utils.pmap +import de.sebse.fuplanner2.utils.updateResultOf +import de.sebse.fuplanner2.whiteboard.Whiteboard +import de.sebse.fuplanner2.worker.FetchResourceErrorType +import de.sebse.fuplanner2.worker.FetchResourceException +import java.lang.StringBuilder + +suspend fun Blackboard.getCourseByLocationRef(stored: List, requester: Requester, user: User, blockedLvNumbers: Set, database: AppDatabase, locationRef: String): Course? { + val course = stored.find { it.internalId == locationRef }?.apply { + if (lastRefreshed > System.currentTimeMillis() - REFRESH_FREQUENCY_COURSES_MILLIS) + return this + } + return getCourseByCourse(requester, user, blockedLvNumbers, database, locationRef, course) +} + +suspend fun Blackboard.getCourseByCourse(requester: Requester, user: User, blockedLvNumbers: Set, database: AppDatabase, locationRef: String, course: Course?): Course? { + + val userId = user.uid ?: return null + + val jsonSite = Cache.getCache(database, locationRef, "Blackboard_Courses", REFRESH_FREQUENCY_COURSES_MILLIS) { + requester.get( + FETCH_COURSE_DETAILS.format(locationRef), + getCookies(user) + ).body + }.let { Parser.default().parse(StringBuilder(it)) as JsonObject } + + val uid = course?.uid + val moduleType = course?.moduleType ?: MODULE_TYPE + val (isSummerSemester, year) = jsonSite.string("courseId")?.let { courseId -> + val groups = Regex("([0-9]{2})([WS])\$").find(courseId)?.groupValues + val type: String? = groups?.getOrNull(2) + val year: String? = groups?.getOrNull(1) + val isSS = type == "S" + val yearInt = if (type != null && year != null) year.toIntOrNull(10) else null + Pair(isSS, yearInt) + } ?: Pair(false, null) + val (lvNumbers, type) = jsonSite.string("courseId")?.let { courseId -> + val groups = Regex("^[A-Z0-9a-z-]+_([A-Z0-9a-z-]*)_([A-Z0-9a-z-]+)_[0-9]{2}[WS]\$").find(courseId)?.groupValues + val type: String? = groups?.getOrNull(1) + val lv: String? = groups?.getOrNull(2) + Pair(if (lv == null) setOf() else setOf(lv), if (type.isNullOrEmpty()) "Sonstiges" else type) + } ?: Pair(setOf(), "Sonstiges") + if (lvNumbers.intersect(blockedLvNumbers).any()) return null + val lecturers = this.requestList( + requester, user, FETCH_COURSE_USERS.format(locationRef), + {json -> json.array("results")}, + {json -> json.obj("paging")?.string("nextPage")} + ).filter { it.string("courseRoleId") == "Instructor" && it.string("userId")?.isNotEmpty() ?: false } + .map { lecturer -> + val lecturerUserId = lecturer.string("userId") ?: return@map null + Cache.getCache(database, lecturerUserId, "Blackboard_Users", REFRESH_FREQUENCY_COURSES_MILLIS) { + requester.get( + FETCH_USER_DETAILS.format(lecturerUserId), + getCookies(user) + ).body + }.let { Parser.default().parse(StringBuilder(it)) as JsonObject } + .let { Lecturer( + it.obj("name")?.string("given") ?: "", + it.obj("name")?.string("family") ?: "", + it.string("userName") + "@zedat.fu-berlin.de", + true + ) } + }.filterNotNull() + return Course( + uid, + userId, + System.currentTimeMillis(), + isSummerSemester, + year, + lvNumbers.toHashSet(), + jsonSite.string("displayName") ?: "", + type, + jsonSite.string("description") ?: "", + locationRef, + moduleType, + lecturers + ) +} + +suspend fun Blackboard.getCourses(ctx: Context, database: AppDatabase, user: User): UpdateResult { + + if (!isAvailable(user)) return UpdateResult() + val userId = user.uid ?: return UpdateResult() + + val requester = Requester(ctx) + val stored = database.courseDao().getAllByType(userId, MODULE_TYPE) + val blockedLvNumbers = database.courseDao() + .getAllByType(userId, Whiteboard.MODULE_TYPE) + .map { it.lvNumber.toSet() } + .reduce { a, b -> a.union(b) } + + try { + val data = this.requestList( + requester, user, FETCH_COURSE_MEMBERSHIP.format(user.bbInfo.id), + {json -> json.array("results")}, + {json -> json.obj("paging")?.string("nextPage")} + ) + val new = data.mapNotNull { it.string("courseId") }.pmap { + getCourseByLocationRef(stored, requester, user, blockedLvNumbers, database, locationRef = it) + }.filterNotNull() + + val result = updateResultOf(stored, new) + database.courseDao().run { + delete(result.removed) + upsert(result.values) + } + return result + } catch (e: VolleyError) { + if (e.networkResponse.statusCode == 401) + throw FetchResourceException( + FetchResourceErrorType.ERR_AUTHORIZATION + ) + throw FetchResourceException( + FetchResourceErrorType.ERR_NETWORK_ERROR + ) + } +} + +suspend fun Blackboard.getCourse(ctx: Context, database: AppDatabase, user: User, courseId: Long): UpdateResult { + + if (!isAvailable(user)) return UpdateResult() + + val requester = Requester(ctx) + val blockedLvNumbers = database.courseDao() + .getAllByType(user.uid!!, Whiteboard.MODULE_TYPE) + .map { it.lvNumber.toSet() } + .reduce { a, b -> a.union(b) } + + try { + database.courseDao().run { + val old = getCourseById2(courseId) + if (old.moduleType == MODULE_TYPE) { + getCourseByCourse(requester, user, blockedLvNumbers, database, old.internalId, old)?.also { + upsert(it) + return updateResultOf(listOf(old), listOf(it)) + } + } + return UpdateResult() + } + } catch (e: VolleyError) { + if (e.networkResponse.statusCode == 401) + throw FetchResourceException( + FetchResourceErrorType.ERR_AUTHORIZATION + ) + throw FetchResourceException( + FetchResourceErrorType.ERR_NETWORK_ERROR + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtEvents.kt b/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtEvents.kt new file mode 100644 index 0000000..bc4aff8 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/blackboard/ExtEvents.kt @@ -0,0 +1,105 @@ +package de.sebse.fuplanner2.blackboard + +import android.content.Context +import com.android.volley.VolleyError +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.database.* +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.utils.* +import de.sebse.fuplanner2.worker.FetchResourceErrorType +import de.sebse.fuplanner2.worker.FetchResourceException + + +suspend fun Blackboard.getEvents(ctx: Context, database: AppDatabase, user: User, course: Course): UpdateResult { + if (!isAvailable(user)) return UpdateResult() + if (course.moduleType != MODULE_TYPE) return UpdateResult() + val courseId = course.uid ?: return UpdateResult() + + val requester = Requester(ctx) + val stored = database.eventDao().getAll2(courseId) + + val latestSemester = database.courseDao().getLatestSemesterName() + if (course.isSummerSemester != latestSemester.semester || course.year != latestSemester.year) + return updateResultOf(stored, stored) + + try { + val new = course.lvNumber.map { lvNumber -> + val vvNumber = Cache.getCache(database, lvNumber, "Blackboard_VVNumber", REFRESH_FREQUENCY_EVENT_CACHE_MILLIS) { + requester.head( + FETCH_VV_COURSE_ID.format(lvNumber), + null + ).let { + Regex("lv/([0-9]+)\\?") + .find(it.headers["Location"] ?: "") + ?.groups + ?.get(1) + ?.value + } ?: "" + } + + val body = requester + .get(FETCH_VV_COURSE.format(vvNumber), null) + .body + Regex("") + .findAll(body) + .map event@{ + val (start, duration) = Regex("[A-Z][a-z], [0-9]{2}\\.[0-9]{2}\\.[0-9]{4} [0-9]{2}:[0-9]{2} - [0-9]{2}:[0-9]{2}") + .find(it.value) + ?.value + ?.let { dateString -> + val start = dateString.substring(4, 20) + .dateStringToLong("dd.MM.yyyy HH:mm") + ?: return@event null + val duration = (0L + + (dateString.substring(23, 25).toLongOrNull(10) ?: return@event null) * 60 + - (dateString.substring(15, 17).toLongOrNull(10) ?: return@event null) * 60 + + (dateString.substring(26, 28).toLongOrNull(10) ?: return@event null) * 1 + - (dateString.substring(18, 20).toLongOrNull(10) ?: return@event null) * 1) + Pair(start, duration * 60000) + } ?: return@event null + val title = Regex("
([^<]*?)
") + .find(it.value) + ?.groups + ?.get(1) + ?.value + ?.trim() + ?: course.title + val location = Regex("
[^~]*?
") + .findAll(it.value) + .map { + if (it.value.contains("Räume:")) + Regex("([^~]*?)

") + .findAll(it.value) + .map { it.groups.get(1)?.value?.trim() } + .filterNotNull() + .toList() + else + listOf() + } + .flatten() + .joinToString(separator = ", ") + val type = Regex("([^<]*)") + .find(it.value) + ?.groups + ?.get(1) + ?.value + ?.trim() + ?: "Class section - Lecture" + Event(null, courseId, System.currentTimeMillis(), title, duration, start, location, type) + } + .toList() + .filterNotNull() + }.flatten() + + val result = updateResultOf(stored, new) + database.eventDao().run { + delete(result.removed) + upsert(result.values) + } + return result + } catch (e: VolleyError) { + val statusCode = e.networkResponse.statusCode + throw FetchResourceException(FetchResourceErrorType.ERR_NETWORK_ERROR, statusCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Announcement.kt b/app/src/main/java/de/sebse/fuplanner2/database/Announcement.kt new file mode 100644 index 0000000..d018d9c --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Announcement.kt @@ -0,0 +1,92 @@ +package de.sebse.fuplanner2.database + +import android.content.Context +import androidx.navigation.NavController +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.Index +import androidx.room.PrimaryKey +import com.beust.klaxon.JsonObject +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.ui.notification.NotificationFragmentDirections +import de.sebse.fuplanner2.utils.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Entity( + foreignKeys = [ + ForeignKey(onDelete = CASCADE, entity = Course::class, parentColumns = ["uid"], childColumns = ["courseId"]) + ], + indices = [ + Index("courseId"), + Index("id", unique = true) + ] +) +data class Announcement ( + @PrimaryKey(autoGenerate = true) var uid: Long? = null, + val courseId: Long, + val lastRefreshed: Long, + val id: String, + val title: String, + val body: String, + val createdOn: Long, + val createdBy: String, + var attachments: List +): Updatable { + override fun getIdentifier(): String = + id + + override fun getHash(): Int = + hashCodeOf(title, body, createdOn, createdBy, attachments) + + override fun getData(): JsonObject = + JsonObject(mapOf("title" to title, "uid" to uid)) + + override fun getNotificationEntityType(): Notifications.CourseUpdateEntity = + Notifications.CourseUpdateEntity.ANNOUNCEMENT + + companion object : UpdatableCompanion { + override fun notificationText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? = when (type) { + Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.not_course_update_announcement_removed, data.string("title")) + Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.not_course_update_announcement_updated, data.string("title")) + Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.not_course_update_announcement_added, data.string("title")) + } + + override fun adapterText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? = when (type) { + Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_announcement_removed, data.string("title")) + Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_announcement_updated, data.string("title")) + Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_announcement_added, data.string("title")) + } + + override fun adapterCallback( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject, + navController: NavController + ): () -> Unit { + return { + GlobalScope.launch { + data.long("uid") + ?.let { AppDatabase.getInstance().announcementDao().getAnnouncementById(it) } + ?.let { AppDatabase.getInstance().courseDao().getCourseById2(it.courseId) } + ?.let { + withContext(Dispatchers.Main) { + navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title)) + } + } + } + } + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/AnnouncementDao.kt b/app/src/main/java/de/sebse/fuplanner2/database/AnnouncementDao.kt new file mode 100644 index 0000000..fae3638 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/AnnouncementDao.kt @@ -0,0 +1,46 @@ +package de.sebse.fuplanner2.database + +import androidx.paging.DataSource +import androidx.room.* +import de.sebse.fuplanner2.utils.console + +@Dao +interface AnnouncementDao { + @Query("SELECT * FROM announcement WHERE courseId = :courseId ORDER BY createdOn ASC") + fun getAll1(courseId: Long): DataSource.Factory + + @Query("SELECT * FROM announcement WHERE courseId = :courseId ORDER BY createdOn ASC") + fun getAll2(courseId: Long): List + + @Query("SELECT * FROM announcement WHERE uid = :announcementId LIMIT 1") + fun getAnnouncementById(announcementId: Long): Announcement + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(announcement: Announcement): Long + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(announcement: Announcement) + + @Transaction + fun upsert(announcement: Announcement) { + val id = insert(announcement) + if (id == -1L) { + update(announcement) + } else { + announcement.uid = id + } + } + + @Transaction + fun upsert(announcements: List) { + announcements.forEach { announcement -> + upsert(announcement) + } + } + + @Delete + fun delete(announcement: Announcement) + + @Delete + fun delete(announcements: List) +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/database/AppDatabase.kt b/app/src/main/java/de/sebse/fuplanner2/database/AppDatabase.kt new file mode 100644 index 0000000..59996ed --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/AppDatabase.kt @@ -0,0 +1,84 @@ +package de.sebse.fuplanner2.database + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.sqlite.db.SupportSQLiteDatabase + + +@Database( + entities = [ + User::class, Course::class, Cache::class, + Notification::class, Event::class, Announcement::class + ], + version = 1 +) +@TypeConverters(Converters::class) +abstract class AppDatabase: RoomDatabase() { + abstract fun userDao(): UserDao + abstract fun courseDao(): CourseDao + abstract fun cacheDao(): CacheDao + abstract fun notificationDao(): NotificationDao + abstract fun eventDao(): EventDao + abstract fun announcementDao(): AnnouncementDao + + private val mIsDatabaseCreated = MutableLiveData() + + companion object { + @VisibleForTesting + val DATABASE_NAME = "app-db" + + private var sInstance: AppDatabase? = null + + fun initialize(context: Context): AppDatabase? { + if (sInstance == null) { + synchronized(AppDatabase::class.java) { + if (sInstance == null) { + sInstance = buildDatabase(context.applicationContext) + sInstance?.updateDatabaseCreated(context.applicationContext) + } + } + } + return sInstance + } + + fun getInstance(): AppDatabase { + sInstance?.let { + return it + } + throw NullPointerException("Please call initialize() before getting the instance.") + } + + private fun buildDatabase( + appContext: Context + ): AppDatabase { + return Room.databaseBuilder(appContext, AppDatabase::class.java, DATABASE_NAME) + .addCallback(object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + val database = getInstance() + database.setDatabaseCreated() + } + }).build() + } + } + + private fun updateDatabaseCreated(context: Context) { + if (context.getDatabasePath(DATABASE_NAME).exists()) { + setDatabaseCreated() + } + } + + private fun setDatabaseCreated() { + mIsDatabaseCreated.postValue(true) + } + + fun getDatabaseCreated(): LiveData { + return mIsDatabaseCreated + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Attachment.kt b/app/src/main/java/de/sebse/fuplanner2/database/Attachment.kt new file mode 100644 index 0000000..ea449f8 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Attachment.kt @@ -0,0 +1,25 @@ +package de.sebse.fuplanner2.database + +import com.beust.klaxon.Klaxon +import com.beust.klaxon.KlaxonException + + +data class Attachment ( + val url: String, + val name: String, + val mimeType: String +) { + fun toJsonString(): String { + return Klaxon().toJsonString(this) + } + + companion object { + fun fromString(json: String): Attachment? { + return try { + Klaxon().parse(json) + } catch (e: KlaxonException) { + null + } + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Cache.kt b/app/src/main/java/de/sebse/fuplanner2/database/Cache.kt new file mode 100644 index 0000000..9c320ea --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Cache.kt @@ -0,0 +1,30 @@ +package de.sebse.fuplanner2.database + +import androidx.room.Entity + +@Entity(primaryKeys = ["uid", "type"]) +data class Cache ( + val uid: String, + val type: String, + val lastRefreshed: Long, + val cache: String +) { + companion object { + suspend fun getCache(database: AppDatabase, uid: String, type: String, refreshInterval: Long, onCreateNew: (suspend () -> String)): String { + val cacheDao = database.cacheDao() + val cache = cacheDao.getCache(uid, type)?.let { + if (System.currentTimeMillis() <= it.lastRefreshed + refreshInterval) { + it + } else { + cacheDao.delete(it) + null + } + } ?: run { + val c = Cache(uid, type, System.currentTimeMillis(), onCreateNew()) + cacheDao.insert(c) + c + } + return cache.cache + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/CacheDao.kt b/app/src/main/java/de/sebse/fuplanner2/database/CacheDao.kt new file mode 100644 index 0000000..1b05bd3 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/CacheDao.kt @@ -0,0 +1,15 @@ +package de.sebse.fuplanner2.database + +import androidx.room.* + +@Dao +interface CacheDao { + @Query("SELECT * FROM cache WHERE uid = :uid AND type = :type") + fun getCache(uid: String, type: String): Cache? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(cache: Cache) + + @Delete() + fun delete(cache: Cache) +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Converters.kt b/app/src/main/java/de/sebse/fuplanner2/database/Converters.kt new file mode 100644 index 0000000..855b6cc --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Converters.kt @@ -0,0 +1,88 @@ +package de.sebse.fuplanner2.database + +import androidx.room.TypeConverter +import de.sebse.fuplanner2.auth.UserCookies +import de.sebse.fuplanner2.blackboard.BlackboardInfo +import de.sebse.fuplanner2.whiteboard.WhiteboardInfo +import java.util.* + +class Converters { + private val delimiter = "~@@~" + + @TypeConverter + fun fromUserCookies(value: UserCookies?): String? { + return value?.toString() + } + @TypeConverter + fun toUserCookies(value: String?): UserCookies? { + return value?.let { UserCookies(it) } + } + + + + @TypeConverter + fun fromBlackboardInfo(value: BlackboardInfo?): String? { + return value?.toString() + } + @TypeConverter + fun toBlackboardInfo(value: String?): BlackboardInfo? { + return value?.let { BlackboardInfo(it) } + } + + + + @TypeConverter + fun fromWhiteboardInfo(value: WhiteboardInfo?): String? { + return value?.toString() + } + @TypeConverter + fun toWhiteboardInfo(value: String?): WhiteboardInfo? { + return value?.let { WhiteboardInfo(it) } + } + + + + @TypeConverter + fun fromDate(value: Date?): Long? { + return value?.time + } + @TypeConverter + fun toDate(value: Long?): Date? { + return value?.let { Date(it) } + } + + + + @TypeConverter + fun fromStringSet(value: HashSet?): String? { + return value?.joinToString(separator = delimiter) + } + @TypeConverter + fun toStringSet(value: String?): HashSet? { + return value?.split(delimiter)?.toHashSet() + } + + + + @TypeConverter + fun fromLecturerList(value: List?): String? { + return value?.joinToString(separator = delimiter) { it.toJsonString() } + } + @TypeConverter + fun toLecturerList(value: String?): List? { + return value?.split(delimiter)?.mapNotNull { Lecturer.fromString(it) } + } + + + + @TypeConverter + fun fromAttachmentList(value: List?): String? { + return value?.joinToString(separator = delimiter) { it.toJsonString() } + } + @TypeConverter + fun toAttachmentList(value: String?): List? { + return value?.split(delimiter)?.mapNotNull { Attachment.fromString(it) } + } + + +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Course.kt b/app/src/main/java/de/sebse/fuplanner2/database/Course.kt new file mode 100644 index 0000000..1859224 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Course.kt @@ -0,0 +1,101 @@ +package de.sebse.fuplanner2.database + +import android.content.Context +import androidx.navigation.NavController +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.Index +import androidx.room.PrimaryKey +import com.beust.klaxon.JsonObject +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.ui.notification.NotificationFragmentDirections +import de.sebse.fuplanner2.utils.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Entity( + foreignKeys = [ + ForeignKey(onDelete = CASCADE, entity = User::class, parentColumns = ["uid"], childColumns = ["userId"]) + ], + indices = [ + Index("userId"), + Index("internalId", "moduleType", unique = true) + ] +) +data class Course ( + @PrimaryKey(autoGenerate = true) var uid: Long? = null, + val userId: Long, + val lastRefreshed: Long, + val isSummerSemester: Boolean, + val year: Int?, + val lvNumber: HashSet, + val title: String, + val type: String, + val description: String, + val internalId: String, + val moduleType: Int, + val lecturers: List +): Comparable, Updatable { + override fun getIdentifier(): String = + internalId + + override fun getHash(): Int = + hashCodeOf(isSummerSemester, year, lvNumber, title, type, description) + + override fun getData(): JsonObject = + JsonObject(mapOf("title" to title, "uid" to uid)) + + override fun getNotificationEntityType(): Notifications.CourseUpdateEntity = + Notifications.CourseUpdateEntity.COURSE + + override fun compareTo(other: Course): Int { + (year ?: Int.MIN_VALUE).compareTo(other.year ?: Int.MIN_VALUE).let { + if (it != 0) return it + return other.isSummerSemester.compareTo(isSummerSemester) + } + } + + companion object : UpdatableCompanion { + override fun notificationText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? = when (type) { + Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_removed, data.string("title")) + Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_updated, data.string("title")) + Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_added, data.string("title")) + } + + override fun adapterText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? = when (type) { + Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_removed, data.string("title")) + Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_updated, data.string("title")) + Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_added, data.string("title")) + } + + override fun adapterCallback( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject, + navController: NavController + ): () -> Unit { + return { + GlobalScope.launch { + data.long("uid") + ?.let { AppDatabase.getInstance().courseDao().getCourseById2(it) } + ?.let { + withContext(Dispatchers.Main) { + navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title)) + } + } + } + } + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/CourseDao.kt b/app/src/main/java/de/sebse/fuplanner2/database/CourseDao.kt new file mode 100644 index 0000000..a8dc86e --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/CourseDao.kt @@ -0,0 +1,56 @@ +package de.sebse.fuplanner2.database + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface CourseDao { + @Query("SELECT * FROM course ORDER BY year DESC, CASE WHEN isSummerSemester THEN 1 ELSE 0 END ASC, title ASC") + fun getAll(): LiveData> + + @Query("SELECT * FROM course INNER JOIN (SELECT year, CASE WHEN isSummerSemester THEN 1 ELSE 0 END AS semester FROM course GROUP BY year, isSummerSemester ORDER BY year DESC, semester ASC LIMIT 1) current WHERE current.year = course.year AND current.semester = CASE WHEN course.isSummerSemester THEN 1 ELSE 0 END ORDER BY title ASC") + fun getLatestSemester(): LiveData> + + @Query("SELECT year, CASE WHEN isSummerSemester THEN 1 ELSE 0 END AS semester FROM course GROUP BY year, isSummerSemester ORDER BY year DESC, semester ASC LIMIT 1") + fun getLatestSemesterName(): Semester + + @Query("SELECT * FROM course WHERE uid = :courseId") + fun getCourseById(courseId: Long): LiveData + + @Query("SELECT * FROM course WHERE uid = :courseId") + fun getCourseById2(courseId: Long): Course + + @Query("SELECT * FROM course WHERE userId = :userId AND (moduleType & :moduleType) == :moduleType") + fun getAllByType(userId: Long, moduleType: Int): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(course: Course): Long + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(course: Course) + + @Transaction + fun upsert(course: Course) { + val id = insert(course) + if (id == -1L) { + update(course) + } else { + course.uid = id + } + } + + @Transaction + fun upsert(courses: List) { + courses.forEach { course -> + upsert(course) + } + } + + @Delete + fun delete(course: Course) + + @Delete + fun delete(courses: List) +} + +data class Semester(val year: Int, val semester: Boolean) \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Event.kt b/app/src/main/java/de/sebse/fuplanner2/database/Event.kt new file mode 100644 index 0000000..cb8b80b --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Event.kt @@ -0,0 +1,91 @@ +package de.sebse.fuplanner2.database + +import android.content.Context +import androidx.navigation.NavController +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.Index +import androidx.room.PrimaryKey +import com.beust.klaxon.JsonObject +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.ui.notification.NotificationFragmentDirections +import de.sebse.fuplanner2.utils.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Entity( + foreignKeys = [ + ForeignKey(onDelete = CASCADE, entity = Course::class, parentColumns = ["uid"], childColumns = ["courseId"]) + ], + indices = [ + Index("courseId"), + Index("courseId", "startDateTime", "duration", "title", "location", unique = true) + ] +) +data class Event ( + @PrimaryKey(autoGenerate = true) var uid: Long? = null, + val courseId: Long, + val lastRefreshed: Long, + val title: String, + val duration: Long, + val startDateTime: Long, + val location: String?, + val type: String +): Updatable { + override fun getIdentifier(): String = + hashCodeOf(type, startDateTime, duration, title, location).toString() + + override fun getHash(): Int = + hashCodeOf(type, startDateTime, duration, title, location) + + override fun getData(): JsonObject = + JsonObject(mapOf("title" to title, "uid" to uid)) + + override fun getNotificationEntityType(): Notifications.CourseUpdateEntity = + Notifications.CourseUpdateEntity.EVENT + + companion object : UpdatableCompanion { + override fun notificationText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? = when (type) { + Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.not_course_update_event_removed, data.string("title")) + Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.not_course_update_event_updated, data.string("title")) + Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.not_course_update_event_added, data.string("title")) + } + + override fun adapterText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? = when (type) { + Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_event_removed, data.string("title")) + Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_event_updated, data.string("title")) + Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_event_added, data.string("title")) + } + + override fun adapterCallback( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject, + navController: NavController + ): () -> Unit { + return { + GlobalScope.launch { + data.long("uid") + ?.let { AppDatabase.getInstance().eventDao().getEventById(it) } + ?.let { AppDatabase.getInstance().courseDao().getCourseById2(it.courseId) } + ?.let { + withContext(Dispatchers.Main) { + navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title)) + } + } + } + } + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/EventDao.kt b/app/src/main/java/de/sebse/fuplanner2/database/EventDao.kt new file mode 100644 index 0000000..5f0558f --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/EventDao.kt @@ -0,0 +1,49 @@ +package de.sebse.fuplanner2.database + +import androidx.paging.DataSource +import androidx.room.* +import de.sebse.fuplanner2.utils.console + +@Dao +interface EventDao { + @Query("SELECT * FROM event WHERE startDateTime >= :start AND startDateTime <= :end") + fun getAllBetween(start: Long, end: Long): List + + @Query("SELECT * FROM event WHERE courseId = :courseId ORDER BY startDateTime ASC") + fun getAll1(courseId: Long): DataSource.Factory + + @Query("SELECT * FROM event WHERE courseId = :courseId ORDER BY startDateTime ASC") + fun getAll2(courseId: Long): List + + @Query("SELECT * FROM event WHERE uid = :eventId LIMIT 1") + fun getEventById(eventId: Long): Event + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(event: Event): Long + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(event: Event) + + @Transaction + fun upsert(event: Event) { + val id = insert(event) + if (id == -1L) { + update(event) + } else { + event.uid = id + } + } + + @Transaction + fun upsert(events: List) { + events.forEach { event -> + upsert(event) + } + } + + @Delete + fun delete(event: Event) + + @Delete + fun delete(events: List) +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt b/app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt new file mode 100644 index 0000000..a4b24b5 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt @@ -0,0 +1,26 @@ +package de.sebse.fuplanner2.database + +import com.beust.klaxon.Klaxon +import com.beust.klaxon.KlaxonException + + +data class Lecturer ( + val fistName: String, + val lastName: String, + val email: String, + val isResponsible: Boolean +) { + fun toJsonString(): String { + return Klaxon().toJsonString(this) + } + + companion object { + fun fromString(json: String): Lecturer? { + return try { + Klaxon().parse(json) + } catch (e: KlaxonException) { + null + } + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Notification.kt b/app/src/main/java/de/sebse/fuplanner2/database/Notification.kt new file mode 100644 index 0000000..44c02d7 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/Notification.kt @@ -0,0 +1,23 @@ +package de.sebse.fuplanner2.database + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser + +@Entity +data class Notification ( + @PrimaryKey(autoGenerate = true) val uid: Int? = null, + val created: Long, + val read: Boolean, + val channelId: String, + val data: String +) { + fun getJsonData(): JsonObject? { + return try { + Parser.default().parse(StringBuilder(this.data)) as JsonObject + } catch (e: Throwable) { + null + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/database/NotificationDao.kt b/app/src/main/java/de/sebse/fuplanner2/database/NotificationDao.kt new file mode 100644 index 0000000..963a981 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/NotificationDao.kt @@ -0,0 +1,52 @@ +package de.sebse.fuplanner2.database + +import androidx.lifecycle.LiveData +import androidx.paging.DataSource +import androidx.room.* + + +@Dao +interface NotificationDao { + @Query("SELECT * FROM notification ORDER BY created DESC") + fun getAllPaged(): DataSource.Factory + + @Query("SELECT * FROM notification WHERE channelId = :channelId AND read = 0") + fun loadAllUnreadByChannelId(channelId: String): List + + @Query("SELECT COUNT(uid) FROM notification WHERE read = 0") + fun getUnreadRowCount(): LiveData + + @Query("UPDATE notification SET read = 1") + fun setRead() + + @Query("UPDATE notification SET read = 1 WHERE uid = :notificationId") + fun setRead(notificationId: Int) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(notification: Notification): Long + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(notification: Notification) + + @Transaction + fun upsert(notification: Notification) { + val id = insert(notification) + if (id == -1L) { + update(notification) + } + } + + @Transaction + fun upsert(notifications: List) { + notifications.forEach { notification -> + upsert(notification) + } + } + + @Delete + fun delete(notification: Notification) + + @Query("DELETE FROM notification WHERE created < (STRFTIME('%s', 'now') * 1000 - 31536000000) AND read") + fun deleteOld() + +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/database/User.kt b/app/src/main/java/de/sebse/fuplanner2/database/User.kt new file mode 100644 index 0000000..009ab8f --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/User.kt @@ -0,0 +1,21 @@ +package de.sebse.fuplanner2.database + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import de.sebse.fuplanner2.auth.UserCookies +import de.sebse.fuplanner2.blackboard.BlackboardInfo +import de.sebse.fuplanner2.whiteboard.WhiteboardInfo + +@Entity +data class User ( + @PrimaryKey(autoGenerate = true) var uid: Long? = null, + @ColumnInfo(name = "cookies") val cookies: UserCookies, + @ColumnInfo(name = "bbInfo") val bbInfo: BlackboardInfo, + @ColumnInfo(name = "wbInfo") val wbInfo: WhiteboardInfo, + @ColumnInfo(name = "username") val userName: String, + @ColumnInfo(name = "first_name") var firstName: String? = null, + @ColumnInfo(name = "last_name") var lastName: String? = null, + @ColumnInfo(name = "mat_number") var matNumber: Int? = null, + @ColumnInfo(name = "email") var email: String? = null +) diff --git a/app/src/main/java/de/sebse/fuplanner2/database/UserDao.kt b/app/src/main/java/de/sebse/fuplanner2/database/UserDao.kt new file mode 100644 index 0000000..75c0608 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/database/UserDao.kt @@ -0,0 +1,41 @@ +package de.sebse.fuplanner2.database + +import androidx.room.* + +@Dao +interface UserDao { + @Query("SELECT * FROM user") + fun getAll(): List + + @Query("SELECT * FROM user WHERE uid IN (:userIds)") + fun loadAllByIds(userIds: IntArray): List + + @Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1") + fun findByName(first: String, last: String): User? + + @Query("SELECT * FROM user WHERE username LIKE :name LIMIT 1") + fun findByUsername(name: String): User? + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(user: User): Long + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(user: User) + + @Transaction + fun upsert(user: User) { + val id = insert(user) + if (id == -1L) { + update(user) + } else { + user.uid = id + } + } + + @Delete + fun delete(user: User) + + @Query("DELETE FROM user WHERE username = :userName") + fun deleteByUserName(userName: String) + +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/network/CustomRequest.kt b/app/src/main/java/de/sebse/fuplanner2/network/CustomRequest.kt new file mode 100644 index 0000000..83b73e6 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/network/CustomRequest.kt @@ -0,0 +1,126 @@ +package de.sebse.fuplanner2.network + +import android.os.Build +import androidx.annotation.GuardedBy +import com.android.volley.* +import com.android.volley.toolbox.HttpHeaderParser +import de.sebse.fuplanner2.utils.console +import java.io.UnsupportedEncodingException +import java.net.URLEncoder +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + + +class CustomRequest: Request { + /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */ + private val mLock = Any() + + @GuardedBy("mLock") + private var mListener: Response.Listener? = null + + private var mCookies: Map? + private var mData: Map? + + constructor( + method: Int, + url: String?, + cookies: Map?, + data: Map?, + listener: Response.Listener?, errorListener: Response.ErrorListener? + ) : super(method, url, errorListener) { + mListener = listener + mCookies = cookies + mData = data + } + + constructor( + method: Int, + url: String?, + cookies: Map?, + listener: Response.Listener?, errorListener: Response.ErrorListener? + ) : this(method, url, cookies, null, listener, errorListener) + + override fun cancel() { + super.cancel() + synchronized(mLock) { mListener = null } + } + + override fun deliverResponse(response: NetData) { + var listener: Response.Listener? + synchronized(mLock) { listener = mListener } + listener?.onResponse(response) + } + + override fun deliverError(error: VolleyError?) { + val volleyError = if (error == null) { + VolleyError(NetworkResponse(408, null, true, 0, null)) + } else if (error.networkResponse == null) { + val statusCode: Int = if (error is TimeoutError) 408 else 500 + VolleyError(NetworkResponse(statusCode, null, true, error.networkTimeMs, null)) + } else { + if (error.networkResponse.statusCode == 302) { + deliverResponse(parseNetworkResponse(error.networkResponse)?.result ?: NetData("", NetworkResponse(null))) + null + } else { + error + } + } + volleyError?.let { + console.error("RequestError", "${it.networkResponse.statusCode} on $method $url") + super.deliverError(it) + } + } + + override fun parseNetworkResponse(response: NetworkResponse): Response? { + val parsed: String = parse(response.data, response.headers) ?: "" + return Response.success(NetData(parsed, response), HttpHeaderParser.parseCacheHeaders(response)) + } + + override fun getHeaders(): MutableMap { + var params = + super.getHeaders() + mCookies?.let { cookieMap -> + params = params?.let { HashMap(it) } ?: HashMap() + val newStr = ArrayList() + for (key in cookieMap.keys) newStr.add("$key=${cookieMap[key]}") + params["Cookie"] = newStr.joinToString("; ") + } + + return params + } + + override fun getBodyContentType(): String? { + return "application/x-www-form-urlencoded" + } + + override fun getBody(): ByteArray? { + mData?.let { data -> + val sb = StringBuilder() + for (key in data.keys) { + if (sb.isNotEmpty()) { + sb.append('&') + } + try { + sb.append(URLEncoder.encode(key, "UTF-8")).append('=') + .append(URLEncoder.encode(data[key], "UTF-8")) + } catch (ignored: UnsupportedEncodingException) { + } + } + val requestBody = sb.toString() + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + requestBody.toByteArray(StandardCharsets.UTF_8) + } else { + requestBody.toByteArray() + } + } + return null + } + + private fun parse(body: ByteArray?, headers: Map): String? { + return if (body == null) null else try { + String(body, Charset.forName(HttpHeaderParser.parseCharset(headers))) + } catch (e: UnsupportedEncodingException) { + String(body) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/network/NetData.kt b/app/src/main/java/de/sebse/fuplanner2/network/NetData.kt new file mode 100644 index 0000000..9201f5d --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/network/NetData.kt @@ -0,0 +1,6 @@ +package de.sebse.fuplanner2.network +import com.android.volley.NetworkResponse + +data class NetData(val body: String, val networkResponse: NetworkResponse) { + val headers = networkResponse.headers +} diff --git a/app/src/main/java/de/sebse/fuplanner2/network/Requester.kt b/app/src/main/java/de/sebse/fuplanner2/network/Requester.kt new file mode 100644 index 0000000..571d260 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/network/Requester.kt @@ -0,0 +1,52 @@ +package de.sebse.fuplanner2.network + +import android.content.Context +import android.os.Build +import com.android.volley.Request +import com.android.volley.RequestQueue +import com.android.volley.Response +import com.android.volley.toolbox.HttpStack +import com.android.volley.toolbox.HurlStack +import com.android.volley.toolbox.Volley +import de.sebse.fuplanner2.utils.console +import kotlinx.coroutines.suspendCancellableCoroutine +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import java.security.AccessController.getContext +import java.security.KeyManagementException +import java.security.NoSuchAlgorithmException +import kotlin.coroutines.resume + + +class Requester(ctx: Context) { + private var requestQueue: RequestQueue = Volley.newRequestQueue(ctx, object : HurlStack() { + @Throws(IOException::class) + override fun createConnection(url: URL?): HttpURLConnection? { + val connection: HttpURLConnection = super.createConnection(url) + connection.instanceFollowRedirects = false + return connection + } + }) + + suspend fun get(url: String, cookies: Map?): NetData { + return suspendCancellableCoroutine { cont -> + val request = CustomRequest(Request.Method.GET, url, cookies, Response.Listener { response -> cont.resume(response) }, Response.ErrorListener { error -> cont.cancel(error) }) + requestQueue.add(request) + } + } + + suspend fun head(url: String, cookies: Map?): NetData { + return suspendCancellableCoroutine { cont -> + val request = CustomRequest(Request.Method.HEAD, url, cookies, Response.Listener { response -> cont.resume(response) }, Response.ErrorListener { error -> cont.cancel(error) }) + requestQueue.add(request) + } + } + + suspend fun post(url: String, cookies: Map?, data: Map?): NetData { + return suspendCancellableCoroutine { cont -> + val request = CustomRequest(Request.Method.POST, url, cookies, data, Response.Listener { response -> cont.resume(response) }, Response.ErrorListener { error -> cont.cancel(error) }) + requestQueue.add(request) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/network/tools.kt b/app/src/main/java/de/sebse/fuplanner2/network/tools.kt new file mode 100644 index 0000000..ec2a667 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/network/tools.kt @@ -0,0 +1,17 @@ +package de.sebse.fuplanner2.network + +import com.android.volley.NetworkResponse +import com.android.volley.VolleyError +import de.sebse.fuplanner2.utils.console + +object tools { + fun invalidResponse(uid: Int, status: String): VolleyError { + console.warn("InvalidResponse", "$uid - $status") + return VolleyError(NetworkResponse(422, null, true, 0, null)) + } + + fun invalidPassword(uid: Int, status: String): VolleyError { + console.warn("InvalidPassword", "$uid - $status") + return VolleyError(NetworkResponse(403, null, true, 0, null)) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/preferences/AppPreferences.kt b/app/src/main/java/de/sebse/fuplanner2/preferences/AppPreferences.kt new file mode 100644 index 0000000..4fc5d35 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/preferences/AppPreferences.kt @@ -0,0 +1,47 @@ +package de.sebse.fuplanner2.preferences + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.SharedPreferences + +class AppPreferences(val context: Context) { + private val preferences: SharedPreferences = context.getSharedPreferences(APP_PREFERENCES_SCREEN, MODE_PRIVATE) + + fun remove(key: String) = preferences.edit().remove(key).apply() + + fun getString(key: String): String? = preferences.getString(key, null) + fun set(key: String, value: String) = preferences.edit().putString(key, value).apply() + + fun getBoolean(key: String): Boolean? = if (preferences.contains(key)) preferences.getBoolean(key, false) else null + fun set(key: String, value: Boolean) = preferences.edit().putBoolean(key, value).apply() + + fun getInt(key: String): Int? = if (preferences.contains(key)) preferences.getInt(key, 0) else null + fun set(key: String, value: Int) = preferences.edit().putInt(key, value).apply() + + fun getLong(key: String): Long? = if (preferences.contains(key)) preferences.getLong(key, 0) else null + fun set(key: String, value: Long) = preferences.edit().putLong(key, value).apply() + + companion object { + const val APP_PREFERENCES_SCREEN = "app" + + private var sInstance: AppPreferences? = null + + fun initialize(context: Context): AppPreferences? { + if (sInstance == null) { + synchronized(AppPreferences::class.java) { + if (sInstance == null) { + sInstance = AppPreferences(context.applicationContext) + } + } + } + return sInstance + } + + fun getInstance(): AppPreferences { + sInstance?.let { + return it + } + throw NullPointerException("Please call initialize() before getting the instance.") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesAdapter.kt b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesAdapter.kt new file mode 100644 index 0000000..828a0e6 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesAdapter.kt @@ -0,0 +1,66 @@ +package de.sebse.fuplanner2.ui.courses + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.ui.CaptionHolder +import de.sebse.fuplanner2.ui.CustomHolder +import de.sebse.fuplanner2.ui.ListItemHolder +import de.sebse.fuplanner2.ui.ViewHolderGenerator +import de.sebse.fuplanner2.utils.cast +import java.util.* + +class CoursesAdapter(private val onclick: (Course) -> Unit) : RecyclerView.Adapter() { + + private val positionalData: ArrayList = arrayListOf() + var dataset: List = listOf() + set(value) { + field = value + positionalData.clear() + var last: Course? = null + dataset.forEach { + if (last?.let { last -> it < last } != false) + positionalData.add(Pair(it.isSummerSemester, it.year)) + positionalData.add(it) + last = it + } + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder { + return ViewHolderGenerator.getHolderByType(parent, viewType) + } + + override fun onBindViewHolder(holder: CustomHolder, position: Int) { + // val viewType = getItemViewType(position) + val res = holder.itemView.resources + when (holder) { + is CaptionHolder -> cast>(positionalData[position])?.let { (isSummer, year) -> + holder.string.text = when { + year == null -> res.getString(R.string.projects) + isSummer -> res.getString(R.string.summer_semester, year) + else -> res.getString(R.string.winter_semester, year, year+1) + } + } + is ListItemHolder -> cast(positionalData[position])?.let { + holder.title.text = it.title + holder.subLeft.text = it.lecturers.filter { lecturer -> lecturer.isResponsible }.joinToString { lecturer -> + res.getString(R.string.full_name, lecturer.fistName.substring(0, 1)+".", lecturer.lastName) + } + holder.subRight.text = it.type + holder.itemView.setOnClickListener { _ -> this.onclick(it) } + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (positionalData[position]) { + is Course -> ViewHolderGenerator.HolderType.ITEM.ordinal + else -> ViewHolderGenerator.HolderType.HEADER.ordinal + } + } + + // Return the size of your dataset (invoked by the layout manager) + override fun getItemCount() = positionalData.size +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt new file mode 100644 index 0000000..637a76b --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt @@ -0,0 +1,70 @@ +package de.sebse.fuplanner2.ui.courses + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME +import de.sebse.fuplanner2.worker.CourseWorker +import kotlinx.android.synthetic.main.fragment_refresh_recycler.view.* + +class CoursesFragment : Fragment() { + + private lateinit var coursesViewModel: CoursesViewModel + private lateinit var navController: NavController + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val viewManager = LinearLayoutManager(context) + val viewAdapter = CoursesAdapter(this::onClick) + coursesViewModel = ViewModelProvider(this).get(CoursesViewModel::class.java) + return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply { + recycler_view.apply { + //setHasFixedSize(true) + layoutManager = viewManager + adapter = viewAdapter + } + swipe_refresh_layout.setOnRefreshListener { + val work = OneTimeWorkRequestBuilder() + .setInputData(workDataOf( + KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name + )) + .build() + WorkManager.getInstance(context.applicationContext) + .enqueue(work) + WorkManager.getInstance(context.applicationContext) + .getWorkInfoByIdLiveData(work.id) + .observe(viewLifecycleOwner, Observer { + if (it.state.isFinished) + swipe_refresh_layout.isRefreshing = false + }) + } + coursesViewModel.text.observe(viewLifecycleOwner, Observer { + viewAdapter.dataset = it + }) + navController = findNavController() + } + } + + private fun onClick(course: Course) { + course.uid?.let { + navController.navigate(CoursesFragmentDirections.actionNavHomeToCourseDetails(it, course.title)) + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesViewModel.kt new file mode 100644 index 0000000..9a4c0e9 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesViewModel.kt @@ -0,0 +1,10 @@ +package de.sebse.fuplanner2.ui.courses + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course + +class CoursesViewModel : ViewModel() { + val text: LiveData> = AppDatabase.getInstance().courseDao().getAll() +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt new file mode 100644 index 0000000..c78dcae --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt @@ -0,0 +1,84 @@ +package de.sebse.fuplanner2.ui.details + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.database.Lecturer +import de.sebse.fuplanner2.ui.* +import de.sebse.fuplanner2.utils.cast +import java.util.* + +class DetailsAdapter(private val onQuickLink: (ButtonTypes) -> Unit, private val onMailTo: (Lecturer) -> Unit) : RecyclerView.Adapter() { + + enum class HeaderTypes { + BUTTONS, LECTURER, ANNOUNCEMENTS, ASSIGNMENTS, EVENTS; + } + + enum class ButtonTypes { + DESCRIPTION, RESOURCES, GRADEBOOK, ANNOUNCEMENTS, ASSIGNMENTS, EVENTS; + } + + private val positionalData: ArrayList = arrayListOf() + + var lecturers: List = listOf() + set(value) { + field = value + updatePositionalData() + } + + private fun updatePositionalData() { + positionalData.clear() + positionalData.add(HeaderTypes.BUTTONS) + positionalData.add(ViewHolderGenerator.HolderType.BUTTONS) + positionalData.add(HeaderTypes.LECTURER) + positionalData.addAll(lecturers.sortedBy { it.lastName }.sortedBy { !it.isResponsible }) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder { + return ViewHolderGenerator.getHolderByType(parent, viewType) + } + + override fun onBindViewHolder(holder: CustomHolder, position: Int) { + // val viewType = getItemViewType(position) + val res = holder.itemView.resources + when (holder) { + is CaptionHolder -> cast(positionalData[position])?.let { type -> + holder.string.text = when (type) { + HeaderTypes.BUTTONS -> res.getString(R.string.quick_links) + HeaderTypes.LECTURER -> res.getString(R.string.lecturers) + HeaderTypes.ANNOUNCEMENTS -> res.getString(R.string.announcements) + HeaderTypes.ASSIGNMENTS -> res.getString(R.string.assignments) + HeaderTypes.EVENTS -> res.getString(R.string.events) + } + } + is QuickLinksHolder -> { + holder.btnAnnouncements.setOnClickListener { this.onQuickLink(ButtonTypes.ANNOUNCEMENTS) } + holder.btnAssignments.setOnClickListener { this.onQuickLink(ButtonTypes.ASSIGNMENTS) } + holder.btnDescription.setOnClickListener { this.onQuickLink(ButtonTypes.DESCRIPTION) } + holder.btnEvents.setOnClickListener { this.onQuickLink(ButtonTypes.EVENTS) } + holder.btnGradebook.setOnClickListener { this.onQuickLink(ButtonTypes.GRADEBOOK) } + holder.btnResources.setOnClickListener { this.onQuickLink(ButtonTypes.RESOURCES) } + } + is MailHolder -> cast(positionalData[position])?.let { type -> + holder.title.text = res.getString(R.string.full_name, type.fistName, type.lastName) + holder.subLeft.text = type.email + holder.itemView.setOnClickListener { this.onMailTo(type) } + } + is ListItemHolder -> when (positionalData[position]) { + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (positionalData[position]) { + is HeaderTypes -> ViewHolderGenerator.HolderType.HEADER.ordinal + ViewHolderGenerator.HolderType.BUTTONS -> ViewHolderGenerator.HolderType.BUTTONS.ordinal + is Lecturer -> ViewHolderGenerator.HolderType.MAIL.ordinal + else -> ViewHolderGenerator.HolderType.ITEM.ordinal + } + } + + // Return the size of your dataset (invoked by the layout manager) + override fun getItemCount() = positionalData.size +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt new file mode 100644 index 0000000..e02da1d --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt @@ -0,0 +1,89 @@ +package de.sebse.fuplanner2.ui.details + +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.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.work.workDataOf +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.database.Lecturer +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.utils.enqueueOneTimeWork +import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME +import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_COURSE_ID +import de.sebse.fuplanner2.worker.CourseWorker +import kotlinx.android.synthetic.main.fragment_refresh_recycler.view.* + + +class DetailsFragment : Fragment() { + + private var title: String = "" + private val args: DetailsFragmentArgs by navArgs() + private val detailsViewModel: DetailsViewModel by viewModels { DetailsViewModelFactory(args.courseId) } + private lateinit var navController: NavController + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val viewManager = LinearLayoutManager(context) + val viewAdapter = DetailsAdapter(this::launchFragment, this::sendMail) + return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply { + recycler_view.apply { + setHasFixedSize(true) + + layoutManager = viewManager + adapter = viewAdapter + } + swipe_refresh_layout.setOnRefreshListener { + enqueueOneTimeWork(context.applicationContext) { + it.setInputData(workDataOf( + KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name, + KEY_COURSE_ID to args.courseId + )) + }.observe(viewLifecycleOwner, Observer { + if (it.state.isFinished) + swipe_refresh_layout.isRefreshing = false + }) + } + detailsViewModel.course.observe(viewLifecycleOwner, Observer { + viewAdapter.lecturers = it.lecturers + this@DetailsFragment.title = it.title + }) + navController = findNavController() + } + } + + private fun sendMail(lecturer: Lecturer) { + val intent = Intent(Intent.ACTION_SENDTO) + intent.type = "text/html" + intent.data = Uri.fromParts("mailto", lecturer.email, null) + intent.putExtra(Intent.EXTRA_SUBJECT, this.title) + intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.email_preview, lecturer.fistName, lecturer.lastName)) + startActivity(Intent.createChooser(intent, getString(R.string.send_email, lecturer.fistName, lecturer.lastName))) + } + + private fun launchFragment(btnType: DetailsAdapter.ButtonTypes) { + when (btnType) { + DetailsAdapter.ButtonTypes.DESCRIPTION -> + this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToDescriptionFragment(args.courseId, title)) + DetailsAdapter.ButtonTypes.RESOURCES -> TODO() + DetailsAdapter.ButtonTypes.GRADEBOOK -> TODO() + DetailsAdapter.ButtonTypes.ANNOUNCEMENTS -> + this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToCourseAnnouncements(args.courseId, title)) + DetailsAdapter.ButtonTypes.ASSIGNMENTS -> TODO() + DetailsAdapter.ButtonTypes.EVENTS -> + this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToCourseEvents(args.courseId, title)) + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsViewModel.kt new file mode 100644 index 0000000..c624c83 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsViewModel.kt @@ -0,0 +1,16 @@ +package de.sebse.fuplanner2.ui.details + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course + + +class DetailsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T = DetailsViewModel(courseId) as T +} + +class DetailsViewModel(courseId: Long) : ViewModel() { + val course: LiveData = AppDatabase.getInstance().courseDao().getCourseById(courseId) +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsAdapter.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsAdapter.kt new file mode 100644 index 0000000..ee2f375 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsAdapter.kt @@ -0,0 +1,40 @@ +package de.sebse.fuplanner2.ui.details_announcements + +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import de.sebse.fuplanner2.database.Announcement +import de.sebse.fuplanner2.ui.ListItemHolder +import de.sebse.fuplanner2.utils.toDateTimeString + +class AnnouncementsAdapter() : PagedListAdapter(EventDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemHolder { + return ListItemHolder.invoke(parent) + } + + override fun onBindViewHolder(holder: ListItemHolder, position: Int) { + val event = getItem(position) + val actCtx = holder.itemView.context + + event?.let { + holder.title.text = it.title + holder.subLeft.text = it.createdOn.toDateTimeString(actCtx) + holder.subRight.text = it.createdBy + } ?: run { + holder.clear() + holder.itemView.setOnClickListener(null) + } + } +} + +class EventDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Announcement, newItem: Announcement): Boolean { + return oldItem.uid == newItem.uid + } + + override fun areContentsTheSame(oldItem: Announcement, newItem: Announcement): Boolean { + return oldItem.getHash() == newItem.getHash() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsFragment.kt new file mode 100644 index 0000000..c04318b --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsFragment.kt @@ -0,0 +1,63 @@ +package de.sebse.fuplanner2.ui.details_announcements + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.work.workDataOf +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.utils.enqueueOneTimeWork +import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME +import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_COURSE_ID +import de.sebse.fuplanner2.worker.AnnouncementWorker +import kotlinx.android.synthetic.main.fragment_refresh_recycler.view.* + +class AnnouncementsFragment : Fragment() { + + private var title: String = "" + private val args: AnnouncementsFragmentArgs by navArgs() + private val announcementsViewModel: AnnouncementsViewModel by viewModels { AnnouncementsViewModelFactory(args.courseId) } + private lateinit var navController: NavController + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val viewManager = LinearLayoutManager(context) + val viewAdapter = AnnouncementsAdapter() + return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply { + console.log(findViewById(R.id.recycler_view), recycler_view) + recycler_view.apply { + setHasFixedSize(true) + + layoutManager = viewManager + adapter = viewAdapter + } + swipe_refresh_layout.setOnRefreshListener { + enqueueOneTimeWork(context.applicationContext) { + it.setInputData(workDataOf( + KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name, + KEY_COURSE_ID to args.courseId + )) + }.observe(viewLifecycleOwner, Observer { + if (it.state.isFinished) + swipe_refresh_layout.isRefreshing = false + }) + } + announcementsViewModel.events.observe(viewLifecycleOwner, Observer { + viewAdapter.submitList(it) + this@AnnouncementsFragment.title = args.title + }) + navController = findNavController() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt new file mode 100644 index 0000000..68e8a8a --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_announcements/AnnouncementsViewModel.kt @@ -0,0 +1,18 @@ +package de.sebse.fuplanner2.ui.details_announcements + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import de.sebse.fuplanner2.database.Announcement +import de.sebse.fuplanner2.database.AppDatabase + +class AnnouncementsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T = AnnouncementsViewModel(courseId) as T +} + +class AnnouncementsViewModel(courseId: Long) : ViewModel() { + private val factory = AppDatabase.getInstance().announcementDao().getAll1(courseId) + val events: LiveData> = LivePagedListBuilder(factory, 50).build() +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_description/DescriptionFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_description/DescriptionFragment.kt new file mode 100644 index 0000000..3968935 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_description/DescriptionFragment.kt @@ -0,0 +1,44 @@ +package de.sebse.fuplanner2.ui.details_description + +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebSettings +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.ui.details.DetailsViewModel +import de.sebse.fuplanner2.ui.details.DetailsViewModelFactory +import kotlinx.android.synthetic.main.fragment_description.view.* + + +class DescriptionFragment : Fragment() { + + private var title: String = "" + private val args: DescriptionFragmentArgs by navArgs() + private val detailsViewModel: DetailsViewModel by viewModels { DetailsViewModelFactory(args.courseId) } + private lateinit var navController: NavController + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_description, container, false).apply { + val nightModeFlags = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + description.settings.forceDark = WebSettings.FORCE_DARK_ON + } + detailsViewModel.course.observe(viewLifecycleOwner, Observer { + description.loadDataWithBaseURL("", it.description, "text/html", "UTF-8", "") + }) + navController = findNavController() + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsAdapter.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsAdapter.kt new file mode 100644 index 0000000..8eadaca --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsAdapter.kt @@ -0,0 +1,41 @@ +package de.sebse.fuplanner2.ui.details_events + +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import de.sebse.fuplanner2.database.Event +import de.sebse.fuplanner2.ui.* +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.utils.toDateTimeString + +class EventsAdapter() : PagedListAdapter(EventDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemHolder { + return ListItemHolder.invoke(parent) + } + + override fun onBindViewHolder(holder: ListItemHolder, position: Int) { + val event = getItem(position) + val actCtx = holder.itemView.context + + event?.let { + holder.title.text = it.title + holder.subLeft.text = it.startDateTime.toDateTimeString(actCtx) + holder.subRight.text = it.location + } ?: run { + holder.clear() + holder.itemView.setOnClickListener(null) + } + } +} + +class EventDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Event, newItem: Event): Boolean { + return oldItem.uid == newItem.uid + } + + override fun areContentsTheSame(oldItem: Event, newItem: Event): Boolean { + return oldItem.getHash() == newItem.getHash() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsFragment.kt new file mode 100644 index 0000000..ae63d50 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsFragment.kt @@ -0,0 +1,63 @@ +package de.sebse.fuplanner2.ui.details_events + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.work.workDataOf +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.utils.enqueueOneTimeWork +import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME +import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_COURSE_ID +import de.sebse.fuplanner2.worker.CourseWorker +import de.sebse.fuplanner2.worker.EventWorker +import kotlinx.android.synthetic.main.fragment_refresh_recycler.view.* + + +class EventsFragment : Fragment() { + + private var title: String = "" + private val args: EventsFragmentArgs by navArgs() + private val eventsViewModel: EventsViewModel by viewModels { EventsViewModelFactory(args.courseId) } + private lateinit var navController: NavController + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val viewManager = LinearLayoutManager(context) + val viewAdapter = EventsAdapter() + return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply { + recycler_view.apply { + setHasFixedSize(true) + + layoutManager = viewManager + adapter = viewAdapter + } + swipe_refresh_layout.setOnRefreshListener { + enqueueOneTimeWork(context.applicationContext) { + it.setInputData(workDataOf( + KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name, + KEY_COURSE_ID to args.courseId + )) + }.observe(viewLifecycleOwner, Observer { + if (it.state.isFinished) + swipe_refresh_layout.isRefreshing = false + }) + } + eventsViewModel.events.observe(viewLifecycleOwner, Observer { + viewAdapter.submitList(it) + this@EventsFragment.title = args.title + }) + navController = findNavController() + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsViewModel.kt new file mode 100644 index 0000000..d21cdfc --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details_events/EventsViewModel.kt @@ -0,0 +1,19 @@ +package de.sebse.fuplanner2.ui.details_events + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Event + + +class EventsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T = EventsViewModel(courseId) as T +} + +class EventsViewModel(courseId: Long) : ViewModel() { + private val factory = AppDatabase.getInstance().eventDao().getAll1(courseId) + val events: LiveData> = LivePagedListBuilder(factory, 50).build() +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryFragment.kt new file mode 100644 index 0000000..189e705 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryFragment.kt @@ -0,0 +1,31 @@ +package de.sebse.fuplanner2.ui.gallery + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import de.sebse.fuplanner2.R + +class GalleryFragment : Fragment() { + + private lateinit var galleryViewModel: GalleryViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + galleryViewModel = + ViewModelProviders.of(this).get(GalleryViewModel::class.java) + val root = inflater.inflate(R.layout.fragment_canteen, container, false) + val textView: TextView = root.findViewById(R.id.text_gallery) + galleryViewModel.text.observe(viewLifecycleOwner, Observer { + textView.text = it + }) + return root + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryViewModel.kt new file mode 100644 index 0000000..e293aee --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/gallery/GalleryViewModel.kt @@ -0,0 +1,13 @@ +package de.sebse.fuplanner2.ui.gallery + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class GalleryViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "This is gallery Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationAdapter.kt b/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationAdapter.kt new file mode 100644 index 0000000..ba2568b --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationAdapter.kt @@ -0,0 +1,87 @@ +package de.sebse.fuplanner2.ui.notification + +import android.view.View +import android.view.ViewGroup +import androidx.navigation.NavController +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import com.beust.klaxon.JsonObject +import de.sebse.fuplanner2.database.* +import de.sebse.fuplanner2.ui.NotificationHolder +import de.sebse.fuplanner2.utils.Notifications +import de.sebse.fuplanner2.utils.timeAgoString +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class NotificationAdapter(val navController: NavController): PagedListAdapter(NotificationDiffCallback()) { + + val notificationDao = AppDatabase.getInstance().notificationDao() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationHolder { + return NotificationHolder.invoke(parent) + } + + override fun onBindViewHolder(holder: NotificationHolder, position: Int) { + val notification = getItem(position) + val actCtx = holder.itemView.context + + notification?.getJsonData()?.let { json -> + when (notification.channelId) { + Notifications.COURSE_UPDATE_CHANNEL_ID -> { + val updateType = + Notifications.CourseUpdateType.values[json.int(Notifications.COURSE_UPDATE_TYPE_KEY) ?: 0] + val entityType = + Notifications.CourseUpdateEntity.values[json.int(Notifications.COURSE_UPDATE_ENTITY_KEY) ?: 0] + val data = json.obj(Notifications.COURSE_UPDATE_DATA_KEY) ?: JsonObject() + val adapterText = when (entityType) { + Notifications.CourseUpdateEntity.COURSE -> Course + Notifications.CourseUpdateEntity.ANNOUNCEMENT -> Announcement + Notifications.CourseUpdateEntity.ASSIGNMENT -> TODO() + Notifications.CourseUpdateEntity.GRADE -> TODO() + Notifications.CourseUpdateEntity.RESOURCE -> TODO() + Notifications.CourseUpdateEntity.EVENT -> Event + }.adapterText(actCtx, updateType, data) + val adapterCallback = when (entityType) { + Notifications.CourseUpdateEntity.COURSE -> Course + Notifications.CourseUpdateEntity.ANNOUNCEMENT -> Announcement + Notifications.CourseUpdateEntity.ASSIGNMENT -> TODO() + Notifications.CourseUpdateEntity.GRADE -> TODO() + Notifications.CourseUpdateEntity.RESOURCE -> TODO() + Notifications.CourseUpdateEntity.EVENT -> Event + }.adapterCallback(actCtx, updateType, data, navController) + Pair(adapterText, adapterCallback) + } + else -> null + } + }?.let { (adapterText, adapterCallback) -> + holder.string.text = adapterText + holder.timestamp.text = notification.created.timeAgoString(actCtx) + holder.itemView.setOnClickListener { + adapterCallback() + GlobalScope.launch { + notificationDao.setRead(notification.uid!!) + } + } + holder.read.visibility = if (notification.read) + View.INVISIBLE + else + View.VISIBLE + + } ?: run { + holder.clear() + holder.itemView.setOnClickListener(null) + holder.read.visibility = View.INVISIBLE + } + } +} + +class NotificationDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean { + return oldItem.uid == newItem.uid + } + + override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean { + return oldItem.data == newItem.data && oldItem.read == newItem.read + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationFragment.kt new file mode 100644 index 0000000..7ea107f --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationFragment.kt @@ -0,0 +1,66 @@ +package de.sebse.fuplanner2.ui.notification + +import android.os.Bundle +import android.view.* +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.database.AppDatabase +import kotlinx.android.synthetic.main.fragment_recycler.view.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + + +class NotificationFragment : Fragment() { + + private lateinit var viewModel: NotificationViewModel + private lateinit var navController: NavController + + init { + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.notification_menu, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + navController = findNavController() + val viewManager = LinearLayoutManager(context) + val viewAdapter = NotificationAdapter(navController) + viewModel = ViewModelProvider(this).get(NotificationViewModel::class.java) + + viewModel.text.observe(viewLifecycleOwner, Observer { + viewAdapter.submitList(it) + }) + + return inflater.inflate(R.layout.fragment_recycler, container, false).apply { + recycler_view.apply { + //setHasFixedSize(true) + layoutManager = viewManager + adapter = viewAdapter + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + return when (id) { + R.id.read_all -> { + GlobalScope.launch { + AppDatabase.getInstance().notificationDao().setRead() + } + true + } + else -> super.onOptionsItemSelected(item) + } + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationViewModel.kt new file mode 100644 index 0000000..f987ecf --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/notification/NotificationViewModel.kt @@ -0,0 +1,13 @@ +package de.sebse.fuplanner2.ui.notification + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Notification + +class NotificationViewModel : ViewModel() { + private val factory = AppDatabase.getInstance().notificationDao().getAllPaged() + val text: LiveData> = LivePagedListBuilder(factory, 50).build() +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt new file mode 100644 index 0000000..b72ddb2 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt @@ -0,0 +1,216 @@ +package de.sebse.fuplanner2.ui.schedule + +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.view.* +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import com.alamkanak.weekview.WeekView +import com.alamkanak.weekview.WeekViewDisplayable +import com.alamkanak.weekview.WeekViewEvent +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.database.Event +import de.sebse.fuplanner2.utils.getHtmlSpannedString +import de.sebse.fuplanner2.utils.toTimeString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.* +import kotlin.math.roundToInt +import kotlin.random.Random + +class ScheduleFragment : Fragment() { + + private val scheduleViewModel: ScheduleViewModel by viewModels { ScheduleViewModelFactory() } + private var currentDate = Calendar.getInstance() + private var weekView: WeekView? = null + + init { + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.schedule_menu, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + scheduleViewModel.eventModel.observe(viewLifecycleOwner, Observer { + weekView?.submit(it.map { ContextEvent(requireContext(), it) }) + weekView?.notifyDataSetChanged() + }) + + val navController = findNavController() + var alertCourse: Course? = null + val alertDialog = AlertDialog.Builder(requireContext()) + .setCancelable(true) + .setNegativeButton(R.string.close) { dialog, _ -> + dialog.cancel() + } + .setPositiveButton(R.string.view_course) { _, _ -> + alertCourse?.let { course -> + navController.navigate(ScheduleFragmentDirections.actionNavScheduleToCourseDetails(course.uid!!, course.title)) + } + } + .create() + + + return inflater.inflate(R.layout.fragment_schedule, container, false).apply { + weekView = findViewById>(R.id.weekView).apply { + setOnLoadMoreListener { start, end -> + scheduleViewModel.loadBetween(start.timeInMillis, end.timeInMillis) + } + + setOnRangeChangeListener { firstVisibleDate, _ -> + currentDate = firstVisibleDate.clone() as Calendar + } + + setOnEventClickListener { data, _ -> + alertDialog.run { + GlobalScope.launch { + val course: Course? = withContext(Dispatchers.IO) { + AppDatabase.getInstance().courseDao().getCourseById2(data.event.courseId) + } + @Suppress("BlockingMethodInNonBlockingContext") + val cs: CharSequence = SpannableStringBuilder().apply { + if (course != null) { + this.append(context.getHtmlSpannedString(R.string.dialog_course_br, course.title)) + } + if (!data.event.location.isNullOrBlank()) { + this.append(context.getHtmlSpannedString(R.string.dialog_location_br, data.event.location)) + } + this.append(context.getHtmlSpannedString( + R.string.dialog_time, + data.event.startDateTime.toTimeString(context), + (data.event.startDateTime + data.event.duration).toTimeString(context) + )) + } + withContext(Dispatchers.Main) { + alertCourse = course + setTitle(data.event.title) + setMessage(cs) + show() + } + } + } + } + + val todayDate = Calendar.getInstance() + todayDate.firstDayOfWeek = Calendar.SATURDAY + todayDate.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY) + todayDate.add(Calendar.DAY_OF_WEEK, 2) + goToDate(todayDate) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + return when (id) { + R.id.prev_week -> { + currentDate.firstDayOfWeek = Calendar.TUESDAY + currentDate.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY) + currentDate.add(Calendar.DAY_OF_WEEK, -1) + weekView?.goToDate(currentDate) + true + } + R.id.next_week -> { + currentDate.firstDayOfWeek = Calendar.MONDAY + currentDate.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY) + currentDate.add(Calendar.DAY_OF_WEEK, 7) + weekView?.goToDate(currentDate) + true + } + R.id.go_to_today -> { + weekView?.goToToday() + true + } + else -> super.onOptionsItemSelected(item) + } + } +} + +data class ContextEvent(val actCtx: Context, val event: Event): WeekViewDisplayable { + override fun toWeekViewEvent(): WeekViewEvent { + // Build the styling of the event, for instance background color and strike-through + val style = WeekViewEvent.Style.Builder() + .setBackgroundColor(getColor(actCtx, event.courseId)) + .setTextStrikeThrough(false) + .setTextColorResource(R.color.scheduleOtherText) + .build() + + // Build the WeekViewEvent via the Builder + return WeekViewEvent.Builder(this) + .setId(event.getHash().toLong()) + .setTitle(event.title) + .setStartTime(Calendar.getInstance().apply { timeInMillis = event.startDateTime }) + .setEndTime(Calendar.getInstance().apply { timeInMillis = event.startDateTime + event.duration }) + .setLocation(event.location ?: "") + .setAllDay(false) + .setStyle(style) + .build() + } + + @ColorInt + fun getColor(actCtx: Context, seed: Long): Int { + var h = Random(seed).nextInt(0xFFFF) + h = h * 360 / 0xffff + //int s = 0xff & encodedHash[2]; + //s = s * 100 / 0xffff; + //int v = 0xff & encodedHash[3]; + //v = v * 100 / 0xffff; + + // range for more beautiful colors + h = h / 30 * 30 + val (s, v) = + if (actCtx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) + Pair(100, 20)//Pair(100, 80) + else + Pair(60, 100) + val (r, g, b) = hsvToRgb(h / 360.0, s / 100.0, v / 100.0) + return rgbToColorInt(r, g, b) + } + + private fun hsvToRgb( + hue: Double, + saturation: Double, + value: Double + ): Triple { + val h = (hue * 6).toInt() + val f = hue * 6 - h + val p = value * (1 - saturation) + val q = value * (1 - f * saturation) + val t = value * (1 - (1 - f) * saturation) + return when (h) { + 0 -> Triple(value, t, p) + 1 -> Triple(q, value, p) + 2 -> Triple(p, value, t) + 3 -> Triple(p, q, value) + 4 -> Triple(t, p, value) + 5 -> Triple(value, p, q) + else -> Triple(0.0, 0.0, 0.0) + } + } + + @ColorInt + private fun rgbToColorInt( + r: Double, + g: Double, + b: Double + ): Int { + return 0xFF000000.toInt() + (r*0xFF).roundToInt() * 0x10000 + (g*0xFF).roundToInt() * 0x100 + (b*0xFF).roundToInt() + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleViewModel.kt b/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleViewModel.kt new file mode 100644 index 0000000..3f2bd76 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleViewModel.kt @@ -0,0 +1,38 @@ +package de.sebse.fuplanner2.ui.schedule + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Event +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ScheduleViewModelFactory(): ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T = ScheduleViewModel() as T +} + +class ScheduleViewModel() : ViewModel() { + private val values: HashMap = hashMapOf() + private val events: MutableLiveData> = MutableLiveData() + + val eventModel: LiveData> + get() { + return events + } + + fun loadBetween(start: Long, end: Long) { + GlobalScope.launch { + withContext(Dispatchers.IO) { + val data = AppDatabase.getInstance().eventDao().getAllBetween(start, end) + values.putAll(data.map { it.getHash() to it }) + events.postValue(values.values.toList()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/viewholder.kt b/app/src/main/java/de/sebse/fuplanner2/ui/viewholder.kt new file mode 100644 index 0000000..102e959 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/viewholder.kt @@ -0,0 +1,131 @@ +package de.sebse.fuplanner2.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import de.sebse.fuplanner2.R + +sealed class CustomHolder(base: View) : RecyclerView.ViewHolder(base) { + abstract fun clear() +} + +class CaptionHolder(base: View) : CustomHolder(base) { + companion object { + private const val LAYOUT = R.layout.list_all_caption + fun invoke(parent: ViewGroup) = CaptionHolder( + LayoutInflater.from(parent.context) + .inflate(LAYOUT, parent, false) + ) + } + val string: TextView = base.findViewById(R.id.string) + + override fun clear() { + string.text = "" + } +} + +class NotificationHolder(base: View) : CustomHolder(base) { + companion object { + private const val LAYOUT = R.layout.list_notification_item + fun invoke(parent: ViewGroup) = NotificationHolder( + LayoutInflater.from(parent.context) + .inflate(LAYOUT, parent, false) + ) + } + val string: TextView = base.findViewById(R.id.string) + val timestamp: TextView = base.findViewById(R.id.timestamp) + val read: ImageView = base.findViewById(R.id.read) + + override fun clear() { + string.text = "" + timestamp.text = "" + read.visibility = View.INVISIBLE + } +} + +class QuickLinksHolder(base: View) : CustomHolder(base) { + companion object { + private const val LAYOUT = R.layout.list_courses_quicklinks + fun invoke(parent: ViewGroup) = QuickLinksHolder( + LayoutInflater.from(parent.context) + .inflate(LAYOUT, parent, false) + ) + } + val btnDescription: Button = base.findViewById(R.id.btn_description) + val btnResources: Button = base.findViewById(R.id.btn_resources) + val btnGradebook: Button = base.findViewById(R.id.btn_gradebook) + val btnAnnouncements: Button = base.findViewById(R.id.btn_announcements) + val btnAssignments: Button = base.findViewById(R.id.btn_assignments) + val btnEvents: Button = base.findViewById(R.id.btn_events) + + override fun clear() { + btnDescription.setOnClickListener(null) + btnResources.setOnClickListener(null) + btnGradebook.setOnClickListener(null) + btnAnnouncements.setOnClickListener(null) + btnAssignments.setOnClickListener(null) + btnEvents.setOnClickListener(null) + } +} + +class MailHolder(base: View) : CustomHolder(base) { + companion object { + private const val LAYOUT = R.layout.list_all_mails + fun invoke(parent: ViewGroup) = MailHolder( + LayoutInflater.from(parent.context) + .inflate(LAYOUT, parent, false) + ) + } + val title: TextView = base.findViewById(R.id.title) + val subLeft: TextView = base.findViewById(R.id.sub_left) + val icon: ImageView = base.findViewById(R.id.icon) + + override fun clear() { + title.text = "" + subLeft.text = "" + icon.setImageDrawable(null) + } +} + +class ListItemHolder(base: View) : CustomHolder(base) { + companion object { + private const val LAYOUT = R.layout.list_all_items + fun invoke(parent: ViewGroup) = ListItemHolder( + LayoutInflater.from(parent.context) + .inflate(LAYOUT, parent, false) + ) + } + val title: TextView = base.findViewById(R.id.title) + val subLeft: TextView = base.findViewById(R.id.sub_left) + val subRight: TextView = base.findViewById(R.id.sub_right) + val topRight: TextView = base.findViewById(R.id.top_right) + + override fun clear() { + title.text = "" + subLeft.text = "" + subRight.text = "" + topRight.text = "" + } +} + +object ViewHolderGenerator { + enum class HolderType { + HEADER, BUTTONS, MAIL, ITEM; + companion object { + val values: List = values().toList() + } + } + + fun getHolderByType(parent: ViewGroup, viewType: Int): CustomHolder { + return when (HolderType.values[viewType]) { + HolderType.HEADER -> CaptionHolder.invoke(parent) + HolderType.ITEM -> ListItemHolder.invoke(parent) + HolderType.BUTTONS -> QuickLinksHolder.invoke(parent) + HolderType.MAIL -> MailHolder.invoke(parent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/utils/CustomTabsHelper.kt b/app/src/main/java/de/sebse/fuplanner2/utils/CustomTabsHelper.kt new file mode 100644 index 0000000..ba8c264 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/utils/CustomTabsHelper.kt @@ -0,0 +1,82 @@ +package de.sebse.fuplanner2.utils + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.text.TextUtils + +/** + * Created by akshaynandwana on + * 08, February, 2019 + **/ +/* object CustomTabHelper { + private var sPackageNameToUse: String? = null + private const val STABLE_PACKAGE = "com.android.chrome" + private const val BETA_PACKAGE = "com.chrome.beta" + private const val DEV_PACKAGE = "com.chrome.dev" + private const val LOCAL_PACKAGE = "com.google.android.apps.chrome" + + fun getPackageNameToUse(context: Context, url: String): String? { + + sPackageNameToUse?.let { + return it + } + + val pm = context.packageManager + + val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + val defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0) + var defaultViewHandlerPackageName: String? = null + + defaultViewHandlerInfo?.let { + defaultViewHandlerPackageName = it.activityInfo.packageName + } + + val resolvedActivityList = pm.queryIntentActivities(activityIntent, 0) + val packagesSupportingCustomTabs = ArrayList() + for (info in resolvedActivityList) { + val serviceIntent = Intent() + serviceIntent.action = CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION + serviceIntent.setPackage(info.activityInfo.packageName) + + pm.resolveService(serviceIntent, 0)?.let { + packagesSupportingCustomTabs.add(info.activityInfo.packageName) + } + } + + when { + packagesSupportingCustomTabs.isEmpty() -> sPackageNameToUse = null + packagesSupportingCustomTabs.size == 1 -> sPackageNameToUse = packagesSupportingCustomTabs[0] + !TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName) -> + sPackageNameToUse = defaultViewHandlerPackageName + packagesSupportingCustomTabs.contains(STABLE_PACKAGE) -> sPackageNameToUse = STABLE_PACKAGE + packagesSupportingCustomTabs.contains(BETA_PACKAGE) -> sPackageNameToUse = BETA_PACKAGE + packagesSupportingCustomTabs.contains(DEV_PACKAGE) -> sPackageNameToUse = DEV_PACKAGE + packagesSupportingCustomTabs.contains(LOCAL_PACKAGE) -> sPackageNameToUse = LOCAL_PACKAGE + } + return sPackageNameToUse + } + + private fun hasSpecializedHandlerIntents(context: Context, intent: Intent): Boolean { + try { + val pm = context.packageManager + val handlers = pm.queryIntentActivities( + intent, + PackageManager.GET_RESOLVED_FILTER) + if (handlers.size == 0) { + return false + } + for (resolveInfo in handlers) { + val filter = resolveInfo.filter ?: continue + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue + if (resolveInfo.activityInfo == null) continue + return true + } + } catch (e: RuntimeException) { + } + return false + } +} */ \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/utils/notifications.kt b/app/src/main/java/de/sebse/fuplanner2/utils/notifications.kt new file mode 100644 index 0000000..22d25be --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/utils/notifications.kt @@ -0,0 +1,203 @@ +package de.sebse.fuplanner2.utils + +//import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.StringRes +import androidx.core.app.NotificationCompat +import androidx.navigation.NavController +import com.beust.klaxon.JsonObject +import de.sebse.fuplanner2.MainActivity +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.database.* + +object Notifications { + + private const val COURSE_UPDATE_NOTIFICATION_ID: Int = 456987 + internal const val COURSE_UPDATE_CHANNEL_ID: String = "COURSE_UPDATE_CHANNEL_ID" + internal const val COURSE_UPDATE_TYPE_KEY: String = "COURSE_UPDATE_TYPE_KEY" + internal const val COURSE_UPDATE_DATA_KEY: String = "COURSE_UPDATE_DATA_KEY" + internal const val COURSE_UPDATE_ENTITY_KEY: String = "COURSE_UPDATE_ENTITY_KEY" + + enum class CourseUpdateType { + REMOVED, UPDATED, ADDED; + companion object { + val values: List = values().toList() + } + } + + enum class CourseUpdateEntity { + COURSE, ANNOUNCEMENT, ASSIGNMENT, GRADE, RESOURCE, EVENT; + companion object { + val values: List = values().toList() + } + } + + fun init(actCtx: Context) { + createNotificationChannel(actCtx, COURSE_UPDATE_CHANNEL_ID, R.string.channel_name, R.string.channel_description) + } + + private fun createNotificationChannel( + actCtx: Context, + channelId: String, + @StringRes channelName: Int, + @StringRes channelDescription: Int + ) { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = actCtx.getString(channelName) + val descriptionText = actCtx.getString(channelDescription) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(channelId, name, importance).apply { + description = descriptionText + } + // Register the channel with the system + (actCtx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + .createNotificationChannel(channel) + } + } + + fun courseUpdates(updates: UpdateResult, database: AppDatabase, actCtx: Context) { + val newNotifications = listOf( + CourseUpdateType.REMOVED to updates.removed, + CourseUpdateType.ADDED to updates.added, + CourseUpdateType.UPDATED to updates.updated + ).map { pair -> + pair.second.map { + Notification( + null, + System.currentTimeMillis(), + false, + COURSE_UPDATE_CHANNEL_ID, + JsonObject().apply { + put(COURSE_UPDATE_TYPE_KEY, pair.first.ordinal) + put(COURSE_UPDATE_DATA_KEY, it.getData()) + put(COURSE_UPDATE_ENTITY_KEY, it.getNotificationEntityType().ordinal) + }.toJsonString() + ) + } + }.flatten() + database.notificationDao().upsert(newNotifications) + if (newNotifications.isEmpty()) return + val unread = database.notificationDao().loadAllUnreadByChannelId(COURSE_UPDATE_CHANNEL_ID) + val pendingIntent: PendingIntent = Intent(actCtx, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(MainActivity.EXTRA_OPEN_NOTIFICATIONS, true) + }.let { + PendingIntent.getActivity(actCtx, 0, it, 0) + } + + val notification = NotificationCompat.Builder(actCtx, COURSE_UPDATE_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_logo_mono) + .setContentTitle(actCtx.getString(R.string.not_course_update_title)) + .setContentText(actCtx.getQuantityString(R.plurals.not_course_update_text, unread.size, unread.size)) + .setStyle(NotificationCompat.InboxStyle().also { inboxStyle -> + unread.forEach { not -> + not.getJsonData()?.let { json -> + val updateType = + CourseUpdateType.values[json.int(COURSE_UPDATE_TYPE_KEY) ?: 0] + val entityType = + CourseUpdateEntity.values[json.int(COURSE_UPDATE_ENTITY_KEY) ?: 0] + val data = json.obj(COURSE_UPDATE_DATA_KEY) ?: JsonObject() + when (entityType) { + CourseUpdateEntity.COURSE -> Course + CourseUpdateEntity.ANNOUNCEMENT -> Announcement + CourseUpdateEntity.ASSIGNMENT -> TODO() + CourseUpdateEntity.GRADE -> TODO() + CourseUpdateEntity.RESOURCE -> TODO() + CourseUpdateEntity.EVENT -> Event + }.notificationText(actCtx, updateType, data) + }?.let { inboxStyle.addLine(it) } + } + }) + .setContentIntent(pendingIntent) + .build() + (actCtx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + .notify(actCtx.packageName, COURSE_UPDATE_NOTIFICATION_ID, notification) + } +} + +data class UpdateResult( + val removed: ArrayList = arrayListOf(), + val added: ArrayList = arrayListOf(), + val updated: ArrayList = arrayListOf(), + val unmodified: ArrayList = arrayListOf() +) { + fun merge(other: UpdateResult): UpdateResult { + this.removed.addAll(other.removed) + this.added.addAll(other.added) + this.updated.addAll(other.updated) + this.unmodified.addAll(other.unmodified) + return this + } + + val values: List + get() { + return this.added + this.updated + this.unmodified + } +} + +fun mergeUpdatable(source: UpdateResult, plus: UpdateResult): UpdateResult { + source.removed.addAll(plus.removed) + source.added.addAll(plus.added) + source.updated.addAll(plus.updated) + source.unmodified.addAll(plus.unmodified) + return source +} + +interface Updatable { + fun getIdentifier(): String + fun getHash(): Int + fun getData(): JsonObject + fun getNotificationEntityType(): Notifications.CourseUpdateEntity +} + +interface UpdatableCompanion { + fun notificationText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? + fun adapterText( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject + ): CharSequence? + fun adapterCallback( + actCtx: Context, + type: Notifications.CourseUpdateType, + data: JsonObject, + navController: NavController + ): () -> Unit +} + +fun updateResultOf(old: List, new: List): UpdateResult { + val newIds = new + .map { it.getIdentifier() to Pair(it.getHash(), it) } + .toMap() + val oldIds = old.map { it.getIdentifier() }.toSet() + val removed: ArrayList = arrayListOf() + val added: ArrayList = arrayListOf() + val updated: ArrayList = arrayListOf() + val unmodified: ArrayList = arrayListOf() + old.forEach { + newIds[it.getIdentifier()]?.run { + if (it.getHash() == first) { + unmodified.add(second) + } else { + updated.add(second) + } + } ?: removed.add(it) + } + newIds.forEach { + if (!oldIds.contains(it.key)) + added.add(it.value.second) + } + return UpdateResult(removed, added, updated, unmodified) +} + diff --git a/app/src/main/java/de/sebse/fuplanner2/utils/utils.kt b/app/src/main/java/de/sebse/fuplanner2/utils/utils.kt new file mode 100644 index 0000000..2486cad --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/utils/utils.kt @@ -0,0 +1,181 @@ +@file:Suppress("unused") + +package de.sebse.fuplanner2.utils + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.content.Context +import android.os.Build +import android.text.Html +import android.text.Spanned +import android.text.format.DateFormat +import android.util.Log +import androidx.annotation.PluralsRes +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.work.* +import de.sebse.fuplanner2.R +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + + +object console { + fun log(vararg obj: Any?) { + largeLog({ tag, msg -> Log.d(tag, msg) }, "KTConsole", obj.joinToString(" ")) + } + + fun warn(vararg obj: Any?) { + largeLog({ tag, msg -> Log.w(tag, msg) }, "KTConsole", obj.joinToString(" ")) + } + + fun error(vararg obj: Any?) { + largeLog({ tag, msg -> Log.e(tag, msg) }, "KTConsole", obj.joinToString(" ")) + } + + private fun largeLog( + method: (String?, String) -> Unit, + tag: String, + content: String + ) { + if (content.length > 4000) { + method(tag, content.substring(0, 4000)) + largeLog(method, tag, content.substring(4000)) + } else { + method(tag, content) + } + } +} + +object xml { + fun decode(xml: String): String { + return if (Build.VERSION.SDK_INT >= 24) { + Html.fromHtml(xml , Html.FROM_HTML_MODE_LEGACY).toString() + } else { + @Suppress("DEPRECATION") + Html.fromHtml(xml).toString() + } + } +} + +inline fun enqueueOneTimeWork(appCtx: Context, workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder): LiveData { + val work = workBuilder(OneTimeWorkRequestBuilder()).build() + val workManager = WorkManager.getInstance(appCtx) + workManager.enqueue(work) + return workManager.getWorkInfoByIdLiveData(work.id) +} + +fun List.pmap(f: suspend (A) -> B): List = runBlocking { + map { async { f(it) } }.map { it.await() } +} + +fun hashCodeOf(vararg elements: Any?): Int { + var code = 0 + elements.forEach { code = code*31 + (it?.hashCode() ?: 0) } + return code +} + +inline fun cast(any: Any?) : T? = any as? T? + + +fun String.toHtmlSpan(): Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY) +} else { + @Suppress("DEPRECATION") + Html.fromHtml(this) +} + +fun Context.getHtmlSpannedString(@StringRes id: Int): Spanned = getString(id).toHtmlSpan() + +fun Context.getHtmlSpannedString(@StringRes id: Int, vararg formatArgs: Any?): Spanned = getString(id, *formatArgs).toHtmlSpan() + +fun Context.getQuantityString(@PluralsRes id: Int, quantity: Int): String = resources.getQuantityString(id, quantity) + +fun Context.getQuantityString(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any?): String = resources.getQuantityString(id, quantity, *formatArgs) + +fun Context.getQuantityHtmlSpannedString(@PluralsRes id: Int, quantity: Int): Spanned = getQuantityString(id, quantity).toHtmlSpan() + +fun Context.getQuantityHtmlSpannedString(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any?): Spanned = getQuantityString(id, quantity, *formatArgs).toHtmlSpan() + + + + +fun Long.timeAgoString(actCtx: Context): String { + val SECOND_MILLIS = 1000 + val MINUTE_MILLIS = 60 * SECOND_MILLIS + val HOUR_MILLIS = 60 * MINUTE_MILLIS + val DAY_MILLIS = 24 * HOUR_MILLIS + + val now = System.currentTimeMillis() + if (this > now || this <= 0) { + return "" + } + val diff = now - this + return when { + diff < MINUTE_MILLIS -> actCtx.getString(R.string.time_just_now) + diff < 60 * MINUTE_MILLIS -> actCtx.getQuantityString(R.plurals.time_minutes_ago, (diff / MINUTE_MILLIS).toInt(), diff / MINUTE_MILLIS) + diff < 24 * HOUR_MILLIS -> actCtx.getQuantityString(R.plurals.time_hours_ago, (diff / HOUR_MILLIS).toInt(), diff / HOUR_MILLIS) + else -> actCtx.getQuantityString(R.plurals.time_days_ago, (diff / DAY_MILLIS).toInt(), diff / DAY_MILLIS) + } +} + + +fun Long.toDateTimeString(context: Context): String? { + return toDateString(context, "dd.MM.yy hh:mm") +} + +fun Long.toTimeString(context: Context): String? { + return toDateString(context, "hh:mm") +} + +fun Long.toDateString(context: Context): String? { + return toDateString(context, "dd.MM.yy") +} + +fun Long.toDateString( + context: Context, + skeleton: String +): String? { + return this.toDateString(context, Locale.getDefault(), skeleton) +} + +@SuppressLint("SimpleDateFormat") +fun Long.toDateString( + context: Context, + locale: Locale, + skeleton: String +): String? { + val userSkeleton = if (DateFormat.is24HourFormat(context)) + skeleton.replace("h".toRegex(), "H") + else + skeleton.replace("H".toRegex(), "h") + return getDateFormat(locale, userSkeleton) + ?.let { SimpleDateFormat(it) } + ?.format(Date(this)) +} + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +fun getDateFormat(locale: Locale, skeleton: String): String? { + return DateFormat.getBestDateTimePattern(locale, skeleton) +} + +fun dateEquals(a: Long, b: Long): Boolean { + return a / 86400000 == b / 86400000 +} + +fun String.dateStringToLong(format: String): Long? { + return this.dateStringToLong(format, Locale.getDefault()) +} + +fun String.dateStringToLong(format: String, locale: Locale): Long? { + return try { + SimpleDateFormat(format, locale) + .parse(this) + ?.time + } catch (e: ParseException) { + e.printStackTrace() + null + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtAnnouncements.kt b/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtAnnouncements.kt new file mode 100644 index 0000000..c5da816 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtAnnouncements.kt @@ -0,0 +1,57 @@ +package de.sebse.fuplanner2.whiteboard + +import android.content.Context +import com.android.volley.VolleyError +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.database.* +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.utils.pmap +import de.sebse.fuplanner2.utils.updateResultOf +import de.sebse.fuplanner2.worker.FetchResourceErrorType +import de.sebse.fuplanner2.worker.FetchResourceException + + +suspend fun Whiteboard.getAnnouncements(ctx: Context, database: AppDatabase, user: User, course: Course): UpdateResult { + if (!isAvailable(user)) return UpdateResult() + if (course.moduleType != MODULE_TYPE) return UpdateResult() + val courseId = course.uid ?: return UpdateResult() + + val requester = Requester(ctx) + val stored = database.announcementDao().getAll2(courseId) + + try { + val data = requester.get( + FETCH_ANNOUNCEMENT_LIST.format(course.internalId), + getCookies(user) + ) + val json = Parser.default().parse(StringBuilder(data.body)) as JsonObject + val new = json.array("announcement_collection")?.pmap { obj -> + val id: String = obj.string("announcementId") ?: return@pmap null + val title: String = obj.string("title") ?: "" + val body: String = obj.string("body") ?: "" + val createdOn: Long = obj.long("createdOn") ?: 0 + val createdBy: String = obj.string("creatorUserId") ?: "" + val attachments: List = obj.array("attachments")?.map attach@{ + Attachment( + it.string("url") ?: return@attach null, + it.string("name") ?: return@attach null, + it.string("type") ?: return@attach null + ) + }?.filterNotNull() ?: listOf() + Announcement(null, courseId, System.currentTimeMillis(), id, title, body, createdOn, createdBy, attachments) + }?.filterNotNull() ?: listOf() + val result = updateResultOf(stored, new) + database.announcementDao().run { + delete(result.removed) + upsert(result.values) + } + return result + } catch (e: VolleyError) { + val statusCode = e.networkResponse.statusCode + if (statusCode == 401) + throw FetchResourceException(FetchResourceErrorType.ERR_AUTHORIZATION, statusCode) + throw FetchResourceException(FetchResourceErrorType.ERR_NETWORK_ERROR, statusCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtCourses.kt b/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtCourses.kt new file mode 100644 index 0000000..4cdfc97 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtCourses.kt @@ -0,0 +1,127 @@ +package de.sebse.fuplanner2.whiteboard + +import android.content.Context +import com.android.volley.VolleyError +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.auth.FUAuthModule +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.database.Lecturer +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.utils.pmap +import de.sebse.fuplanner2.utils.updateResultOf +import de.sebse.fuplanner2.worker.FetchResourceErrorType +import de.sebse.fuplanner2.worker.FetchResourceException + + + +suspend fun Whiteboard.getCourseByLocationRef(stored: List, requester: Requester, user: User, locationRef: String): Course { + val course = stored.find { it.internalId == locationRef }?.apply { + if (lastRefreshed > System.currentTimeMillis() - FUAuthModule.REFRESH_FREQUENCY_COURSES_MILLIS) + return this + } + return getCourseByCourse(requester, user, locationRef, course) +} + +suspend fun Whiteboard.getCourseByCourse(requester: Requester, user: User, locationRef: String, course: Course?): Course { + val jsonSite = requester + .get( + FETCH_COURSE_DETAILS.format(locationRef), + getCookies(user) + ) + .let { Parser.default().parse(StringBuilder(it.body)) as JsonObject } + val uid = course?.uid + val moduleType = course?.moduleType ?: MODULE_TYPE + val (isSummerSemester, year) = jsonSite.obj("props")?.string("term_eid")?.let semester@{termEid -> + val type: String? = Regex("^(SS|WS) ").find(termEid)?.groupValues?.getOrNull(1) + val year: String? = Regex("^(SS|WS) ([0-9]{2})").find(termEid)?.groupValues?.getOrNull(2) + val isSS = type == "SS" + val yearInt = if (type != null && year != null) year.toIntOrNull(10) else null + return@semester Pair(isSS, yearInt) + } ?: Pair(false, null) + val lecturers = jsonSite.obj("props")?.string("kvv_lecturers") + ?.split("#") + ?.mapNotNull { lecString -> + return@mapNotNull Regex("(.*?)\\|(.*?)\\|(.*?)\\|\\|(.*)") + .matchEntire(lecString) + ?.groupValues?.let lecturer@{ match -> + return@lecturer Lecturer(match[1], match[2], match[3], match[4] == "true") + } + } ?: listOf() + val lvNumbers = jsonSite + .obj("props")?.string("kvv_lvnumbers") + ?.split(" + ") + ?.toHashSet() ?: hashSetOf() + @Suppress("SpellCheckingInspection") + return Course( + uid, + user.uid!!, + System.currentTimeMillis(), + isSummerSemester, + year, + lvNumbers, + jsonSite.string("entityTitle") ?: "", + jsonSite.obj("props")?.string("kvv_coursetype") ?: "Sonstiges", + jsonSite.string("description") ?: "", + locationRef, + moduleType, + lecturers + ) +} + +suspend fun Whiteboard.getCourses(ctx: Context, database: AppDatabase, user: User): UpdateResult { + if (!isAvailable(user)) return UpdateResult() + val userId = user.uid ?: return UpdateResult() + val requester = Requester(ctx) + val stored = database.courseDao().getAllByType(userId, MODULE_TYPE) + + try { + val data = requester.get( + FETCH_COURSE_MEMBERSHIP, + getCookies(user) + ) + val json = Parser.default().parse(StringBuilder(data.body)) as JsonObject + val new = json.array("membership_collection")?.pmap { obj -> + val locationRef = obj.string("locationReference") ?: "" + getCourseByLocationRef(stored, requester, user, locationRef = locationRef) + } ?: listOf() + val result = updateResultOf(stored, new) + database.courseDao().run { + delete(result.removed) + upsert(result.values) + } + return result + } catch (e: VolleyError) { + val statusCode = e.networkResponse.statusCode + if (statusCode == 401) + throw FetchResourceException(FetchResourceErrorType.ERR_AUTHORIZATION, statusCode) + throw FetchResourceException(FetchResourceErrorType.ERR_NETWORK_ERROR, statusCode) + } +} + +suspend fun Whiteboard.getCourse(ctx: Context, database: AppDatabase, user: User, courseId: Long): UpdateResult { + if (!isAvailable(user)) return UpdateResult() + val requester = Requester(ctx) + + try { + database.courseDao().run { + val old = getCourseById2(courseId) + if (old.moduleType == MODULE_TYPE) { + getCourseByCourse(requester, user, old.internalId, old).also { + upsert(it) + return updateResultOf(listOf(old), listOf(it)) + } + } + return UpdateResult() + } + } catch (e: VolleyError) { + val statusCode = e.networkResponse.statusCode + if (statusCode == 401) + throw FetchResourceException(FetchResourceErrorType.ERR_AUTHORIZATION, statusCode) + throw FetchResourceException(FetchResourceErrorType.ERR_NETWORK_ERROR, statusCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtEvents.kt b/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtEvents.kt new file mode 100644 index 0000000..ed3f4af --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/whiteboard/ExtEvents.kt @@ -0,0 +1,54 @@ +package de.sebse.fuplanner2.whiteboard + +import android.content.Context +import com.android.volley.VolleyError +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.database.Event +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.utils.pmap +import de.sebse.fuplanner2.utils.updateResultOf +import de.sebse.fuplanner2.worker.FetchResourceErrorType +import de.sebse.fuplanner2.worker.FetchResourceException + + +suspend fun Whiteboard.getEvents(ctx: Context, database: AppDatabase, user: User, course: Course): UpdateResult { + if (!isAvailable(user)) return UpdateResult() + if (course.moduleType != MODULE_TYPE) return UpdateResult() + val courseId = course.uid ?: return UpdateResult() + + val requester = Requester(ctx) + val stored = database.eventDao().getAll2(courseId) + + try { + val data = requester.get( + FETCH_EVENT_LIST.format(course.internalId), + getCookies(user) + ) + val json = Parser.default().parse(StringBuilder(data.body)) as JsonObject + val new = json.array("calendar_collection")?.pmap { obj -> + val type: String = obj.string("type") ?: "Event" + val title: String = obj.string("title") ?: "Event" + val duration: Long = obj.long("duration") ?: return@pmap null + val firstTime: Long = obj.obj("firstTime")?.long("time") ?: return@pmap null + val location: String? = obj.string("location") + Event(null, courseId, System.currentTimeMillis(), title, duration, firstTime, location, type) + }?.filterNotNull() ?: listOf() + val result = updateResultOf(stored, new) + database.eventDao().run { + delete(result.removed) + upsert(result.values) + } + return result + } catch (e: VolleyError) { + val statusCode = e.networkResponse.statusCode + if (statusCode == 401) + throw FetchResourceException(FetchResourceErrorType.ERR_AUTHORIZATION, statusCode) + throw FetchResourceException(FetchResourceErrorType.ERR_NETWORK_ERROR, statusCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/whiteboard/Whiteboard.kt b/app/src/main/java/de/sebse/fuplanner2/whiteboard/Whiteboard.kt new file mode 100644 index 0000000..6db4f5b --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/whiteboard/Whiteboard.kt @@ -0,0 +1,112 @@ +package de.sebse.fuplanner2.whiteboard + +import android.content.Context +import android.net.Uri +import com.android.volley.VolleyError +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import de.sebse.fuplanner2.auth.FUAuthModule +import de.sebse.fuplanner2.auth.SamlReponse +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.network.NetData +import de.sebse.fuplanner2.network.Requester +import de.sebse.fuplanner2.network.tools.invalidResponse +import de.sebse.fuplanner2.utils.console + +object Whiteboard: FUAuthModule() { + private const val LOGIN_URL = "https://mycampus.imp.fu-berlin.de/sakai-login-tool/container" + private const val TEST_URL = "https://mycampus.imp.fu-berlin.de/direct/user/%s.json" + private const val RESTORE_ON_REDIRECT_TO = "/portal" + private const val REMOVE_COOKIE_ON_REDIRECT_TO = "/sakai-login-tool/container" + internal const val MODULE_TYPE = 2 + + internal const val FETCH_COURSE_MEMBERSHIP = "https://mycampus.imp.fu-berlin.de/direct/membership.json?_validateSession=" + internal const val FETCH_COURSE_DETAILS = "https://mycampus.imp.fu-berlin.de/direct%s.json?_validateSession=" + + internal const val FETCH_EVENT_LIST = "https://mycampus.imp.fu-berlin.de/direct/calendar%s.json?detailed=true&_validateSession=" + + internal const val FETCH_ANNOUNCEMENT_LIST = "https://mycampus.imp.fu-berlin.de/direct/announcement%s.json?n=999999&d=999999999&_validateSession=" + + override suspend fun isAvailable(ctx: Context, name: String): Boolean { + val requester = Requester(ctx) + try { + requester.get(String.format(TEST_URL, Uri.encode(name)), null) + } catch (e: VolleyError) { + return e.networkResponse.statusCode == 403 + } + return true + } + + override suspend fun login(ctx: Context, name: String, password: String, user: User) { + val requester = Requester(ctx) + val request = requester.get(LOGIN_URL, cookies = getCookies(user, shib = true)) + val samlUri = request.headers["Location"] + ?: throw invalidResponse(102100, "Location header not set!") + if (samlUri == RESTORE_ON_REDIRECT_TO) { + updateCookies(user, request) + return + } + if (samlUri == REMOVE_COOKIE_ON_REDIRECT_TO) { + if (delCookies(user)) + login(ctx, name, password, user) + return + } + val samlResponse: SamlReponse = doSaml(ctx, samlUri, name, password, user) + // Shib-Session-Cookie + var response = requester.post(samlResponse.uri, cookies = getCookies(user), data = hashMapOf( + "RelayState" to samlResponse.relayState, + "SAMLResponse" to samlResponse.samlResponse + )) + updateCookies(user, response) + // Finish BB & Start Session + response = requester.get( + response.networkResponse.headers["Location"] ?: throw invalidResponse(102101, "No Location header to finish Blackboard"), + getCookies(user, shib = true) + ) + + updateCookies(user, response) + + response = requester.get(String.format(TEST_URL, Uri.encode(name)), getCookies(user)) + (Parser.default().parse(StringBuilder(response.body)) as JsonObject).run { + user.wbInfo.id = string("id") ?: user.wbInfo.id + user.email = string("email") ?: user.email + user.matNumber = obj("props")?.string("zedat:matrikelnr")?.toIntOrNull() ?: user.matNumber + user.firstName = string("firstName") ?: user.firstName + user.lastName = string("lastName") ?: user.lastName + } + } + + private fun updateCookies(user: User, response: NetData) { + val setCookies = parseCookies(response.networkResponse.allHeaders) + setCookies["JSESSIONID"]?.let { + user.cookies.wbJsessionId = it + } + setCookies + .filter{ (key, _) -> key.startsWith("_shibsession_") } + .forEach { (key, value) -> + user.cookies.wbShibKey = key + user.cookies.wbShibValue = value + return@forEach + } + } + + internal fun getCookies(user: User, shib: Boolean = false): HashMap? { + val cookies = user.cookies.wbJsessionId?.let { key -> hashMapOf("JSESSIONID" to key) } ?: hashMapOf() + if (shib && user.cookies.wbShibValue != null) { + user.cookies.wbShibKey?.let { cookies[it] = user.cookies.wbShibValue ?: "" } + } + return cookies + } + + private fun delCookies(user: User): Boolean { + val isSuccessful = user.cookies.wbShibKey != null + user.cookies.wbJsessionId = null + user.cookies.wbShibKey = null + user.cookies.wbShibValue = null + return isSuccessful + } + + internal fun isAvailable(user: User): Boolean { + return user.cookies.wbJsessionId != null + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/whiteboard/WhiteboardInfo.kt b/app/src/main/java/de/sebse/fuplanner2/whiteboard/WhiteboardInfo.kt new file mode 100644 index 0000000..d8ddbfc --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/whiteboard/WhiteboardInfo.kt @@ -0,0 +1,24 @@ +package de.sebse.fuplanner2.whiteboard + +import org.json.JSONObject + +class WhiteboardInfo { + var id: String? = null + + constructor() { } + + constructor(json: String) { + val obj = JSONObject(json) + id = obj.opt("id") as String? + } + + fun update(user: WhiteboardInfo) { + id = user.id?: id + } + + override fun toString(): String { + val obj = JSONObject() + obj.put("id", id) + return obj.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt b/app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt new file mode 100644 index 0000000..775094e --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/worker/AbstractAccountWorker.kt @@ -0,0 +1,53 @@ +package de.sebse.fuplanner2.worker + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.User + +abstract class AbstractAccountWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + companion object { + const val KEY_ACCOUNT_NAME = AccountManager.KEY_ACCOUNT_NAME + const val KEY_COURSE_ID = "KEY_COURSE_ID" + const val OUT_ERROR_CODE = "OUT_CODE" + } + + enum class ErrorCodes { + ERR_NO_ACCOUNT_NAME, ERR_TOO_MANY_RETRIES, ERR_INVALID_PASSWORD, ERR_NETWORK_ERROR + } + + override suspend fun doWork(): Result { + if (runAttemptCount > 2) { + return Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_TOO_MANY_RETRIES.ordinal)) + } + val accountName = inputData.getString(KEY_ACCOUNT_NAME) + val accounts = AppAccounts.getInstance() + val account = accounts.getAccount(accountName) + ?: return Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_NO_ACCOUNT_NAME.ordinal)) + + val database = AppDatabase.getInstance() + database.userDao().findByUsername(account.name)?.let { + try { + val success = doActualWork(database, account, it) + return Result.success(success) + } catch (e: FetchResourceException) { + if (e.type == FetchResourceErrorType.ERR_AUTHORIZATION) + return Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_INVALID_PASSWORD.ordinal)) + if (e.type == FetchResourceErrorType.ERR_NETWORK_ERROR && e.statusCode >= 500) + return Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_NETWORK_ERROR.ordinal)) + return Result.retry() + } + } + return Result.success(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_NO_ACCOUNT_NAME.ordinal)) + } + + @Throws(FetchResourceException::class) + abstract suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data +} diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/AnnouncementWorker.kt b/app/src/main/java/de/sebse/fuplanner2/worker/AnnouncementWorker.kt new file mode 100644 index 0000000..7c416a2 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/worker/AnnouncementWorker.kt @@ -0,0 +1,35 @@ +package de.sebse.fuplanner2.worker + +import android.accounts.Account +import android.content.Context +import androidx.work.Data +import androidx.work.WorkerParameters +import de.sebse.fuplanner2.blackboard.Blackboard +import de.sebse.fuplanner2.blackboard.getAnnouncements +import de.sebse.fuplanner2.database.* +import de.sebse.fuplanner2.utils.Notifications +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.whiteboard.Whiteboard +import de.sebse.fuplanner2.whiteboard.getAnnouncements + +class AnnouncementWorker(context: Context, params: WorkerParameters) : AbstractAccountWorker(context, params) { + + companion object { + suspend fun work(appCtx: Context, database: AppDatabase, user: User, course: Course): UpdateResult { + return Whiteboard.getAnnouncements(appCtx, database, user, course) + .merge(Blackboard.getAnnouncements(appCtx, database, user, course)) + } + } + + override suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data { + inputData.getLong(KEY_COURSE_ID, -1) + .let { if (it == -1L) null else it } + ?.let { + database.courseDao().getCourseById2(it) + } + ?.let { + Notifications.courseUpdates(work(applicationContext, database, user, it), database, applicationContext) + } + return inputData + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt b/app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt new file mode 100644 index 0000000..998a5e3 --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/worker/CourseWorker.kt @@ -0,0 +1,47 @@ +package de.sebse.fuplanner2.worker + +import android.accounts.Account +import android.content.Context +import androidx.work.Data +import androidx.work.WorkerParameters +import de.sebse.fuplanner2.blackboard.Blackboard +import de.sebse.fuplanner2.blackboard.getCourse +import de.sebse.fuplanner2.blackboard.getCourses +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.utils.Notifications +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.whiteboard.Whiteboard +import de.sebse.fuplanner2.whiteboard.getCourse +import de.sebse.fuplanner2.whiteboard.getCourses + +class CourseWorker(context: Context, params: WorkerParameters) : AbstractAccountWorker(context, params) { + + companion object { + suspend fun work(appCtx: Context, database: AppDatabase, user: User, courseId: Long? = null): UpdateResult { + return if (courseId != null) { + Whiteboard.getCourse(appCtx, database, user, courseId) + .merge(Blackboard.getCourse(appCtx, database, user, courseId)) + } else { + Whiteboard.getCourses(appCtx, database, user) + .merge(Blackboard.getCourses(appCtx, database, user)) + } + } + } + + override suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data { + val courseId = inputData.getLong(KEY_COURSE_ID, -1) + .let { if (it == -1L) null else it } + val updates = work(applicationContext, database, user, courseId) + val latestSemester = database.courseDao().getLatestSemesterName() + updates.added.forEach { + if (it.isSummerSemester == latestSemester.semester && it.year == latestSemester.year) { + EventWorker.work(applicationContext, database, user, it) + } + } + Notifications.courseUpdates(updates, database, applicationContext) + return inputData + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/EventWorker.kt b/app/src/main/java/de/sebse/fuplanner2/worker/EventWorker.kt new file mode 100644 index 0000000..a5238fa --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/worker/EventWorker.kt @@ -0,0 +1,43 @@ +package de.sebse.fuplanner2.worker + +import android.accounts.Account +import android.content.Context +import androidx.work.Data +import androidx.work.WorkerParameters +import de.sebse.fuplanner2.blackboard.Blackboard +import de.sebse.fuplanner2.blackboard.getCourse +import de.sebse.fuplanner2.blackboard.getCourses +import de.sebse.fuplanner2.blackboard.getEvents +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.database.Event +import de.sebse.fuplanner2.database.User +import de.sebse.fuplanner2.utils.Notifications +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.utils.console +import de.sebse.fuplanner2.whiteboard.Whiteboard +import de.sebse.fuplanner2.whiteboard.getCourse +import de.sebse.fuplanner2.whiteboard.getCourses +import de.sebse.fuplanner2.whiteboard.getEvents + +class EventWorker(context: Context, params: WorkerParameters) : AbstractAccountWorker(context, params) { + + companion object { + suspend fun work(appCtx: Context, database: AppDatabase, user: User, course: Course): UpdateResult { + return Whiteboard.getEvents(appCtx, database, user, course) + .merge(Blackboard.getEvents(appCtx, database, user, course)) + } + } + + override suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data { + inputData.getLong(KEY_COURSE_ID, -1) + .let { if (it == -1L) null else it } + ?.let { + database.courseDao().getCourseById2(it) + } + ?.let { + Notifications.courseUpdates(work(applicationContext, database, user, it), database, applicationContext) + } + return inputData + } +} diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/FetchResourceException.kt b/app/src/main/java/de/sebse/fuplanner2/worker/FetchResourceException.kt new file mode 100644 index 0000000..521af2a --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/worker/FetchResourceException.kt @@ -0,0 +1,11 @@ +package de.sebse.fuplanner2.worker + +import java.lang.Exception + +class FetchResourceException(val type: FetchResourceErrorType, val statusCode: Int = 0): Exception() { + +} + +enum class FetchResourceErrorType { + ERR_AUTHORIZATION, ERR_NETWORK_ERROR +} diff --git a/app/src/main/java/de/sebse/fuplanner2/worker/SyncWorker.kt b/app/src/main/java/de/sebse/fuplanner2/worker/SyncWorker.kt new file mode 100644 index 0000000..bbf336b --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/worker/SyncWorker.kt @@ -0,0 +1,74 @@ +package de.sebse.fuplanner2.worker + +import android.accounts.AccountManager +import android.content.Context +import androidx.work.* +import de.sebse.fuplanner2.auth.AppAccounts +import de.sebse.fuplanner2.database.AppDatabase +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.utils.Notifications +import de.sebse.fuplanner2.utils.Updatable +import de.sebse.fuplanner2.utils.UpdateResult +import de.sebse.fuplanner2.utils.mergeUpdatable + +class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + companion object { + const val KEY_ACCOUNT_NAME = AccountManager.KEY_ACCOUNT_NAME + const val OUT_ERROR_CODE = "OUT_CODE" + } + + enum class ErrorCodes { + ERR_NO_ACCOUNT_NAME, ERR_TOO_MANY_RETRIES, ERR_INVALID_PASSWORD, ERR_NETWORK_ERROR + } + + override suspend fun doWork(): Result { + if (runAttemptCount > 2) { + return Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_TOO_MANY_RETRIES.ordinal)) + } + val accountName = inputData.getString(KEY_ACCOUNT_NAME) + val accounts = AppAccounts.getInstance() + val account = accounts.getAccount(accountName) + ?: return Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_NO_ACCOUNT_NAME.ordinal)) + + when (accounts.refreshSuspended(account)) { + AppAccounts.RefreshResults.UNSPECIFIED_ERROR, + AppAccounts.RefreshResults.NETWORK_ERROR -> { + return Result.retry() + } + AppAccounts.RefreshResults.SUCCESS -> { + val database = AppDatabase.getInstance() + database.userDao().findByUsername(account.name)?.also { + try { + val updates: UpdateResult = CourseWorker.work(applicationContext, database, it) + val courseUpdates = updates.updated + val courseCreations = updates.added + val notifications = mergeUpdatable(UpdateResult(), updates) + courseUpdates.forEach { course -> + mergeUpdatable(notifications, EventWorker.work(applicationContext, database, it, course)) + mergeUpdatable(notifications, AnnouncementWorker.work(applicationContext, database, it, course)) + } + val latestSemester = database.courseDao().getLatestSemesterName() + courseCreations.forEach { course -> + if (course.isSummerSemester == latestSemester.semester && course.year == latestSemester.year) { + EventWorker.work(applicationContext, database, it, course) + } + } + Notifications.courseUpdates(notifications, database, applicationContext) + return@doWork Result.success(workDataOf(KEY_ACCOUNT_NAME to accountName)) + } catch (e: FetchResourceException) { + if (e.type == FetchResourceErrorType.ERR_AUTHORIZATION) + return@doWork Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_INVALID_PASSWORD.ordinal)) + if (e.type == FetchResourceErrorType.ERR_NETWORK_ERROR && e.statusCode >= 500) + return@doWork Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_NETWORK_ERROR.ordinal)) + return@doWork Result.retry() + } + } + return Result.failure(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_NO_ACCOUNT_NAME.ordinal)) + } + AppAccounts.RefreshResults.INVALID_PASSWORD -> { + return Result.success(workDataOf(OUT_ERROR_CODE to ErrorCodes.ERR_INVALID_PASSWORD.ordinal)) + } + } + } +} diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..ce098ea --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..f5e3d9d --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..57c173e --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..5303feb --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/logo_campus.png b/app/src/main/res/drawable-night/logo_campus.png new file mode 100644 index 0000000000000000000000000000000000000000..61bded57b076bacb3080499d3960080a17a18f29 GIT binary patch literal 92766 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�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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_left_white.xml b/app/src/main/res/drawable/ic_keyboard_arrow_left_white.xml new file mode 100644 index 0000000..876aaca --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_left_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right.xml new file mode 100644 index 0000000..e98df31 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right_white.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right_white.xml new file mode 100644 index 0000000..e1e9f45 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_right_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_logo_mono.xml b/app/src/main/res/drawable/ic_logo_mono.xml new file mode 100644 index 0000000..b961edc --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_mono.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mail.xml b/app/src/main/res/drawable/ic_mail.xml new file mode 100644 index 0000000..917fef6 --- /dev/null +++ b/app/src/main/res/drawable/ic_mail.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_canteen.xml b/app/src/main/res/drawable/ic_menu_canteen.xml new file mode 100644 index 0000000..7375ec3 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_canteen.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_courses.xml b/app/src/main/res/drawable/ic_menu_courses.xml new file mode 100644 index 0000000..ff485cf --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_courses.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_event.xml b/app/src/main/res/drawable/ic_menu_event.xml new file mode 100644 index 0000000..22f1bb6 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_event.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_notifications.xml b/app/src/main/res/drawable/ic_menu_notifications.xml new file mode 100644 index 0000000..7009a67 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_notifications.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_notifications_none.xml b/app/src/main/res/drawable/ic_menu_notifications_none.xml new file mode 100644 index 0000000..a4543fd --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_notifications_none.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/logo_campus.png b/app/src/main/res/drawable/logo_campus.png new file mode 100644 index 0000000000000000000000000000000000000000..d411c38b6e83a97e09a6e2373d406a589af6b7d5 GIT binary patch literal 92112 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

B-YGpc5&19 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

J_j>H$Y%9qYh%*)QEbvB-F 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/rounded_corner.xml b/app/src/main/res/drawable/rounded_corner.xml new file mode 100644 index 0000000..f7d44ea --- /dev/null +++ b/app/src/main/res/drawable/rounded_corner.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/app/src/main/res/drawable/side_nav_bar.xml new file mode 100644 index 0000000..62cd0ef --- /dev/null +++ b/app/src/main/res/drawable/side_nav_bar.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..7a49197 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + +