From aba200fc4a6b0a393844ed1f78424594a334fa10 Mon Sep 17 00:00:00 2001 From: ken-kentan Date: Sun, 7 Oct 2018 22:20:54 +0900 Subject: [PATCH 01/35] first commit of androidx version --- .gitignore | 7 +- .gitmodules | 3 - LICENSE | 674 ---------------- README.md | 23 - app/build.gradle | 70 +- app/licenses.yml | 465 ----------- app/proguard-rules.pro | 3 +- .../ExampleInstrumentedTest.kt | 8 +- app/src/main/AndroidManifest.xml | 63 +- .../main/assets/terms_and_privacy_policy.html | 87 --- .../studentportalplus/StudentPortalPlus.kt | 11 +- .../data/PortalRepository.kt | 401 ++++++---- .../studentportalplus/data/UserRepository.kt | 9 + .../data/component/ClassColor.kt | 17 +- .../data/component/ClassWeek.kt | 36 + .../data/component/ClassWeekType.kt | 47 -- .../data/component/CreatedDateType.kt | 12 - ...{LectureAttendType.kt => LectureAttend.kt} | 3 +- .../data/component/LectureOrderType.kt | 8 - .../data/component/LectureQuery.kt | 31 +- .../data/component/NoticeQuery.kt | 54 +- .../data/component/NotifyContent.kt | 7 - .../data/component/NotifyType.kt | 17 - .../data/component/PortalContent.kt | 7 + .../data/component/PortalData.kt | 14 + .../data/component/PortalDataSet.kt | 4 +- .../data/component/PortalDataType.kt | 13 - .../data/component/ShibbolethData.kt | 4 - .../studentportalplus/data/dao/BaseDao.kt | 19 + .../data/dao/DatabaseMigrationHelper.kt | 67 ++ .../data/dao/DatabaseOpenHelper.kt | 94 +-- .../data/dao/LectureCancellationDao.kt | 143 +--- .../data/dao/LectureInformationDao.kt | 128 +--- .../studentportalplus/data/dao/MyClassDao.kt | 47 +- .../studentportalplus/data/dao/NoticeDao.kt | 88 +-- .../studentportalplus/data/model/Lecture.kt | 24 +- .../data/model/LectureCancellation.kt | 39 +- .../data/model/LectureInformation.kt | 44 +- .../studentportalplus/data/model/MyClass.kt | 39 +- .../studentportalplus/data/model/Notice.kt | 32 +- .../studentportalplus/data/model/User.kt | 6 + .../data/parser/BaseParser.kt | 9 +- .../data/parser/LectureAttendParser.kt | 13 +- .../data/parser/LectureCancellationParser.kt | 65 +- .../data/parser/LectureInformationParser.kt | 66 +- .../data/parser/MyClassParser.kt | 128 ++-- .../data/parser/NoticeParser.kt | 42 +- .../data/parser/ParseException.kt | 1 - .../data/shibboleth/ShibbolethClient.kt | 34 +- .../data/shibboleth/ShibbolethData.kt | 6 + .../data/shibboleth/ShibbolethDataProvider.kt | 66 +- .../data/shibboleth/ShibbolethException.kt | 5 +- .../studentportalplus/di/ActivityModule.kt | 18 +- .../kentan/studentportalplus/di/AppModule.kt | 24 +- .../studentportalplus/di/FragmentModule.kt | 15 +- .../notification/DeviceStatusReceiver.kt | 2 +- .../notification/NotificationController.kt | 181 +++-- .../notification/NotificationType.kt | 5 + .../notification/RetryActionService.kt | 8 +- .../notification/SyncScheduler.kt | 68 +- .../notification/SyncWorker.kt | 71 +- .../ui/LectureCancellationActivity.kt | 161 ---- .../ui/LectureInformationActivity.kt | 169 ---- .../studentportalplus/ui/LoginActivity.kt | 196 ----- .../studentportalplus/ui/MainActivity.kt | 276 ------- .../studentportalplus/ui/MyClassActivity.kt | 113 --- .../studentportalplus/ui/NoticeActivity.kt | 136 ---- .../studentportalplus/ui/SettingsActivity.kt | 309 -------- .../studentportalplus/ui/SingleLiveData.kt | 46 ++ .../studentportalplus/ui/ViewModelFactory.kt | 62 ++ .../studentportalplus/ui/WelcomeActivity.kt | 62 -- .../DashboardLectureCancellationAdapter.kt | 102 --- .../DashboardLectureInformationAdapter.kt | 102 --- .../ui/adapter/DashboardMyClassAdapter.kt | 91 --- .../ui/adapter/DashboardNoticeAdapter.kt | 103 --- .../ui/adapter/LectureCancellationAdapter.kt | 45 -- .../ui/adapter/LectureInformationAdapter.kt | 46 -- .../ui/adapter/MyClassAdapter.kt | 152 ---- .../ui/adapter/NoticeAdapter.kt | 70 -- .../ui/dashboard/DashboardFragment.kt | 168 ++++ .../ui/dashboard/DashboardViewModel.kt | 70 ++ .../ui/dashboard/LectureAdapter.kt | 75 ++ .../ui/dashboard/MyClassAdapter.kt | 70 ++ .../ui/dashboard/NoticeAdapter.kt | 76 ++ .../ui/fragment/DashboardFragment.kt | 159 ---- .../fragment/LectureCancellationFragment.kt | 159 ---- .../ui/fragment/LectureInformationFragment.kt | 160 ---- .../ui/fragment/NoticeFragment.kt | 159 ---- .../ui/fragment/TimetableFragment.kt | 176 ----- .../ui/lecturecancel/LectureCancelAdapter.kt | 44 ++ .../ui/lecturecancel/LectureCancelFragment.kt | 132 ++++ .../lecturecancel/LectureCancelViewModel.kt | 56 ++ .../detail/LectureCancelDetailActivity.kt | 104 +++ .../detail/LectureCancelDetailViewModel.kt | 101 +++ .../ui/lectureinfo/LectureInfoAdapter.kt | 44 ++ .../ui/lectureinfo/LectureInfoFragment.kt | 132 ++++ .../ui/lectureinfo/LectureInfoViewModel.kt | 55 ++ .../detail/LectureInfoDetailActivity.kt | 104 +++ .../detail/LectureInfoDetailViewModel.kt | 105 +++ .../ui/login/LoginActivity.kt | 117 +++ .../ui/login/LoginViewModel.kt | 106 +++ .../ui/login/ValidationResult.kt | 11 + .../studentportalplus/ui/main/FragmentType.kt | 25 + .../studentportalplus/ui/main/MainActivity.kt | 207 +++++ .../ui/main/MainViewModel.kt | 123 +++ .../studentportalplus/ui/main/MapHelper.kt | 36 + .../myclass/detail/MyClassDetailActivity.kt | 111 +++ .../myclass/detail/MyClassDetailViewModel.kt | 69 ++ .../ui/myclass/edit/MyClassEditActivity.kt | 143 ++-- .../ui/myclass/edit/MyClassEditNavigator.kt | 12 - .../ui/myclass/edit/MyClassEditViewModel.kt | 228 +++--- .../ui/myclass/edit/ValidationResult.kt | 7 + .../ui/notice/NoticeAdapter.kt | 46 ++ .../ui/notice/NoticeFragment.kt | 132 ++++ .../ui/notice/NoticeViewModel.kt | 59 ++ .../ui/notice/detail/NoticeDetailActivity.kt | 92 +++ .../ui/notice/detail/NoticeDetailViewModel.kt | 84 ++ .../ui/setting/GeneralPreferenceFragment.kt | 187 +++++ .../NotificationTypePreferenceFragment.kt | 30 + .../ui/setting/SettingsActivity.kt | 41 + .../SimilarSubjectPreferenceFragment.kt | 33 + .../TimetableShortcutActivity.kt | 19 +- .../ui/span/CustomTabsUrlSpan.kt | 41 - .../studentportalplus/ui/span/CustomTitle.kt | 44 -- .../ui/timetable/MyClassAdapter.kt | 161 ++++ .../ui/timetable/TimetableFragment.kt | 171 +++++ .../ui/timetable/TimetableViewModel.kt | 119 +++ .../viewmodel/DashboardFragmentViewModel.kt | 43 -- .../LectureCancellationFragmentViewModel.kt | 64 -- .../viewmodel/LectureCancellationViewModel.kt | 78 -- .../LectureInformationFragmentViewModel.kt | 64 -- .../viewmodel/LectureInformationViewModel.kt | 83 -- .../ui/viewmodel/MainViewModel.kt | 47 -- .../ui/viewmodel/MyClassViewModel.kt | 50 -- .../ui/viewmodel/NoticeFragmentViewModel.kt | 53 -- .../ui/viewmodel/NoticeViewModel.kt | 60 -- .../viewmodel/TimetableFragmentViewModel.kt | 100 --- .../ui/viewmodel/ViewModelFactory.kt | 57 -- .../ui/{ => web}/WebActivity.kt | 20 +- .../ui/welcome/WelcomeActivity.kt | 60 ++ .../ui/welcome/WelcomeViewModel.kt | 28 + .../studentportalplus/ui/widget/MapView.kt | 45 -- .../MyClassThresholdSamplePreference.kt | 68 -- .../studentportalplus/util/AnimationHelper.kt | 58 -- .../util/DataBindingHelper.kt | 181 ++++- .../kentan/studentportalplus/util/Helper.kt | 37 + .../kentan/studentportalplus/util/Helpers.kt | 88 --- .../studentportalplus/util/Murmur3.java | 6 + .../util/SharedPreferencesHelper.kt | 55 +- .../view/text/CustomTabsUrlSpan.kt | 37 + .../text/LinkTransformationMethod.kt} | 21 +- .../widget/CheckableFloatingActionButton.kt | 29 + .../view/widget/CustomTabsTextView.kt | 60 ++ .../view/widget/DividerItemDecoration.kt | 61 ++ .../widget/FavoriteFloatingActionButton.kt | 41 + .../LectureAttendFloatingActionButton.kt | 49 ++ .../widget/SimilarSubjectSamplePreference.kt | 77 ++ app/src/main/res/animator/fade_in.xml | 7 - app/src/main/res/animator/fade_out.xml | 7 - app/src/main/res/color/drawer_item_icon.xml | 2 +- .../drawable-v24/ic_launcher_foreground.xml | 34 + app/src/main/res/drawable/chip.xml | 23 - app/src/main/res/drawable/ic_account.xml | 12 +- app/src/main/res/drawable/ic_add.xml | 12 +- app/src/main/res/drawable/ic_check_accent.xml | 5 + app/src/main/res/drawable/ic_close_black.xml | 12 +- app/src/main/res/drawable/ic_delete.xml | 4 +- app/src/main/res/drawable/ic_edit.xml | 4 +- .../res/drawable/ic_launcher_background.xml | 170 ++++ app/src/main/res/drawable/ic_lock_off.xml | 4 +- app/src/main/res/drawable/ic_lock_on.xml | 4 +- ...enu_filter_list.xml => ic_menu_filter.xml} | 0 app/src/main/res/drawable/ic_menu_setting.xml | 6 +- app/src/main/res/drawable/ic_palette.xml | 5 + app/src/main/res/drawable/ic_pref_info.xml | 5 + ...c_check.xml => ic_pref_lecture_cancel.xml} | 4 +- .../res/drawable/ic_pref_lecture_info.xml | 9 + app/src/main/res/drawable/ic_pref_login.xml | 5 + app/src/main/res/drawable/ic_pref_notice.xml | 12 + .../res/drawable/ic_pref_similar_subject.xml | 5 + app/src/main/res/drawable/ic_pref_sync.xml | 5 + app/src/main/res/drawable/ic_refresh.xml | 5 - app/src/main/res/drawable/ic_retry.xml | 9 + app/src/main/res/drawable/ic_share.xml | 12 +- app/src/main/res/drawable/ic_view_day.xml | 5 - app/src/main/res/drawable/ic_view_list.xml | 5 + app/src/main/res/drawable/ic_view_week.xml | 4 +- app/src/main/res/drawable/ic_warning.xml | 12 +- .../layout/activity_lecture_cancel_detail.xml | 63 ++ .../layout/activity_lecture_cancellation.xml | 48 -- .../layout/activity_lecture_info_detail.xml | 63 ++ .../layout/activity_lecture_information.xml | 48 -- app/src/main/res/layout/activity_login.xml | 186 +++-- app/src/main/res/layout/activity_main.xml | 53 +- app/src/main/res/layout/activity_my_class.xml | 67 -- .../res/layout/activity_my_class_detail.xml | 62 ++ .../res/layout/activity_my_class_edit.xml | 415 +++++----- app/src/main/res/layout/activity_notice.xml | 73 -- .../res/layout/activity_notice_detail.xml | 92 +++ app/src/main/res/layout/activity_web.xml | 24 +- app/src/main/res/layout/activity_welcome.xml | 111 +-- app/src/main/res/layout/app_bar_main.xml | 41 +- .../res/layout/content_lecture_cancel.xml | 147 ++++ .../layout/content_lecture_cancellation.xml | 132 ---- .../main/res/layout/content_lecture_info.xml | 146 ++++ .../layout/content_lecture_information.xml | 132 ---- app/src/main/res/layout/content_my_class.xml | 41 +- app/src/main/res/layout/content_notice.xml | 89 --- .../main/res/layout/content_notice_detail.xml | 110 +++ .../main/res/layout/dialog_lecture_filter.xml | 173 ++--- .../main/res/layout/dialog_notice_filter.xml | 173 ++--- .../main/res/layout/fragment_dashboard.xml | 530 ++++++------- app/src/main/res/layout/fragment_lecture.xml | 32 - app/src/main/res/layout/fragment_list.xml | 23 + app/src/main/res/layout/fragment_notice.xml | 33 - .../main/res/layout/fragment_timetable.xml | 723 +++++++++--------- app/src/main/res/layout/grid_my_class.xml | 101 --- .../main/res/layout/grid_my_class_empty.xml | 41 - .../main/res/layout/item_empty_my_class.xml | 45 ++ .../main/res/layout/item_grid_my_class.xml | 117 +++ app/src/main/res/layout/item_lecture.xml | 149 ++-- .../main/res/layout/item_list_my_class.xml | 95 +++ app/src/main/res/layout/item_notice.xml | 83 ++ .../main/res/layout/item_small_lecture.xml | 80 ++ .../main/res/layout/item_small_my_class.xml | 104 +++ app/src/main/res/layout/item_small_notice.xml | 74 ++ app/src/main/res/layout/list_my_class.xml | 84 -- app/src/main/res/layout/list_notice.xml | 71 -- .../main/res/layout/list_small_lecture.xml | 60 -- .../main/res/layout/list_small_my_class.xml | 83 -- app/src/main/res/layout/list_small_notice.xml | 56 -- app/src/main/res/layout/nav_header_main.xml | 80 +- .../preference_subject_similar_sample.xml | 137 ++++ .../res/layout/sample_my_class_threshold.xml | 84 -- app/src/main/res/menu/popup_switch_layout.xml | 4 +- app/src/main/res/menu/save.xml | 2 +- app/src/main/res/menu/search_and_filter.xml | 10 +- app/src/main/res/menu/share.xml | 2 + app/src/main/res/values-v21/styles.xml | 17 +- app/src/main/res/values-v23/styles.xml | 4 +- app/src/main/res/values/arrays.xml | 36 +- app/src/main/res/values/attrs.xml | 13 + app/src/main/res/values/colors.xml | 11 +- app/src/main/res/values/dimens.xml | 9 +- app/src/main/res/values/strings.xml | 392 ++++------ app/src/main/res/values/styles.xml | 26 +- app/src/main/res/values/text_apperance.xml | 6 + app/src/main/res/xml-v25/shortcuts.xml | 12 +- app/src/main/res/xml-v26/pref_general.xml | 115 +++ app/src/main/res/xml-v26/preferences.xml | 118 --- app/src/main/res/xml/pref_general.xml | 122 +++ .../main/res/xml/pref_my_class_threshold.xml | 24 - .../main/res/xml/pref_notification_type.xml | 33 + app/src/main/res/xml/pref_notify_contents.xml | 30 - app/src/main/res/xml/pref_similar_subject.xml | 26 + app/src/main/res/xml/preferences.xml | 126 --- .../studentportalplus/ExampleUnitTest.kt | 3 +- build.gradle | 7 +- colorpicker | 1 - gradle.properties | 9 +- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 72 +- gradlew.bat | 14 +- settings.gradle | 2 +- 265 files changed, 9165 insertions(+), 10125 deletions(-) delete mode 100644 .gitmodules delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 app/licenses.yml delete mode 100644 app/src/main/assets/terms_and_privacy_policy.html create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/UserRepository.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeek.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeekType.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/CreatedDateType.kt rename app/src/main/java/jp/kentan/studentportalplus/data/component/{LectureAttendType.kt => LectureAttend.kt} (90%) delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/LectureOrderType.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyContent.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyType.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/PortalContent.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/PortalData.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataType.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/component/ShibbolethData.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/dao/BaseDao.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseMigrationHelper.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/model/User.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethData.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/notification/NotificationType.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/LectureCancellationActivity.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/LectureInformationActivity.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/LoginActivity.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/MainActivity.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/MyClassActivity.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/NoticeActivity.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/SettingsActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/SingleLiveData.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/ViewModelFactory.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/WelcomeActivity.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureCancellationAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureInformationAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardMyClassAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardNoticeAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureCancellationAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureInformationAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/MyClassAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/adapter/NoticeAdapter.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/LectureAdapter.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/MyClassAdapter.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/NoticeAdapter.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/fragment/DashboardFragment.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureCancellationFragment.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureInformationFragment.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/fragment/NoticeFragment.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/fragment/TimetableFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelAdapter.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoAdapter.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/login/ValidationResult.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/main/FragmentType.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/main/MainActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/main/MainViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/main/MapHelper.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditNavigator.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/ValidationResult.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeAdapter.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailViewModel.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/setting/GeneralPreferenceFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/setting/NotificationTypePreferenceFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/setting/SettingsActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/setting/SimilarSubjectPreferenceFragment.kt rename app/src/main/java/jp/kentan/studentportalplus/ui/{widget => shortcut}/TimetableShortcutActivity.kt (55%) delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTabsUrlSpan.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTitle.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/timetable/MyClassAdapter.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableFragment.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/DashboardFragmentViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationFragmentViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationFragmentViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MainViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MyClassViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeFragmentViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/TimetableFragmentViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/ViewModelFactory.kt rename app/src/main/java/jp/kentan/studentportalplus/ui/{ => web}/WebActivity.kt (80%) create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeActivity.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeViewModel.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/widget/MapView.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/ui/widget/MyClassThresholdSamplePreference.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/util/AnimationHelper.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/util/Helper.kt delete mode 100644 app/src/main/java/jp/kentan/studentportalplus/util/Helpers.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/view/text/CustomTabsUrlSpan.kt rename app/src/main/java/jp/kentan/studentportalplus/{util/CustomTransformationMethod.kt => view/text/LinkTransformationMethod.kt} (63%) create mode 100644 app/src/main/java/jp/kentan/studentportalplus/view/widget/CheckableFloatingActionButton.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/view/widget/CustomTabsTextView.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/view/widget/DividerItemDecoration.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/view/widget/FavoriteFloatingActionButton.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/view/widget/LectureAttendFloatingActionButton.kt create mode 100644 app/src/main/java/jp/kentan/studentportalplus/view/widget/SimilarSubjectSamplePreference.kt delete mode 100644 app/src/main/res/animator/fade_in.xml delete mode 100644 app/src/main/res/animator/fade_out.xml create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 app/src/main/res/drawable/chip.xml create mode 100644 app/src/main/res/drawable/ic_check_accent.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml rename app/src/main/res/drawable/{ic_menu_filter_list.xml => ic_menu_filter.xml} (100%) create mode 100644 app/src/main/res/drawable/ic_palette.xml create mode 100644 app/src/main/res/drawable/ic_pref_info.xml rename app/src/main/res/drawable/{ic_check.xml => ic_pref_lecture_cancel.xml} (53%) create mode 100644 app/src/main/res/drawable/ic_pref_lecture_info.xml create mode 100644 app/src/main/res/drawable/ic_pref_login.xml create mode 100644 app/src/main/res/drawable/ic_pref_notice.xml create mode 100644 app/src/main/res/drawable/ic_pref_similar_subject.xml create mode 100644 app/src/main/res/drawable/ic_pref_sync.xml delete mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/drawable/ic_retry.xml delete mode 100644 app/src/main/res/drawable/ic_view_day.xml create mode 100644 app/src/main/res/drawable/ic_view_list.xml create mode 100644 app/src/main/res/layout/activity_lecture_cancel_detail.xml delete mode 100644 app/src/main/res/layout/activity_lecture_cancellation.xml create mode 100644 app/src/main/res/layout/activity_lecture_info_detail.xml delete mode 100644 app/src/main/res/layout/activity_lecture_information.xml delete mode 100644 app/src/main/res/layout/activity_my_class.xml create mode 100644 app/src/main/res/layout/activity_my_class_detail.xml delete mode 100644 app/src/main/res/layout/activity_notice.xml create mode 100644 app/src/main/res/layout/activity_notice_detail.xml create mode 100644 app/src/main/res/layout/content_lecture_cancel.xml delete mode 100644 app/src/main/res/layout/content_lecture_cancellation.xml create mode 100644 app/src/main/res/layout/content_lecture_info.xml delete mode 100644 app/src/main/res/layout/content_lecture_information.xml delete mode 100644 app/src/main/res/layout/content_notice.xml create mode 100644 app/src/main/res/layout/content_notice_detail.xml delete mode 100644 app/src/main/res/layout/fragment_lecture.xml create mode 100644 app/src/main/res/layout/fragment_list.xml delete mode 100644 app/src/main/res/layout/fragment_notice.xml delete mode 100644 app/src/main/res/layout/grid_my_class.xml delete mode 100644 app/src/main/res/layout/grid_my_class_empty.xml create mode 100644 app/src/main/res/layout/item_empty_my_class.xml create mode 100644 app/src/main/res/layout/item_grid_my_class.xml create mode 100644 app/src/main/res/layout/item_list_my_class.xml create mode 100644 app/src/main/res/layout/item_notice.xml create mode 100644 app/src/main/res/layout/item_small_lecture.xml create mode 100644 app/src/main/res/layout/item_small_my_class.xml create mode 100644 app/src/main/res/layout/item_small_notice.xml delete mode 100644 app/src/main/res/layout/list_my_class.xml delete mode 100644 app/src/main/res/layout/list_notice.xml delete mode 100644 app/src/main/res/layout/list_small_lecture.xml delete mode 100644 app/src/main/res/layout/list_small_my_class.xml delete mode 100644 app/src/main/res/layout/list_small_notice.xml create mode 100644 app/src/main/res/layout/preference_subject_similar_sample.xml delete mode 100644 app/src/main/res/layout/sample_my_class_threshold.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/text_apperance.xml create mode 100644 app/src/main/res/xml-v26/pref_general.xml delete mode 100644 app/src/main/res/xml-v26/preferences.xml create mode 100644 app/src/main/res/xml/pref_general.xml delete mode 100644 app/src/main/res/xml/pref_my_class_threshold.xml create mode 100644 app/src/main/res/xml/pref_notification_type.xml delete mode 100644 app/src/main/res/xml/pref_notify_contents.xml create mode 100644 app/src/main/res/xml/pref_similar_subject.xml delete mode 100644 app/src/main/res/xml/preferences.xml delete mode 160000 colorpicker diff --git a/.gitignore b/.gitignore index 329cb6b..fd45b12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ *.iml .gradle /local.properties -/.idea/workspace.xml +/.idea/caches/build_file_checksums.ser /.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml .DS_Store /build /captures .externalNativeBuild - -colorpicker/ -licenses.html \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 1ff2a63..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "colorpicker"] - path = colorpicker - url = https://android.googlesource.com/platform/frameworks/opt/colorpicker diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 94a9ed0..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md deleted file mode 100644 index 0412f20..0000000 --- a/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Student Portal+ -Android向けの学生ポータルサイトクライアント - -## 概要 -授業情報の確認のためにWebの学生ポータルサイトへログインする手間を省くために開発したものです。 -初回起動時にログインを行えば、以降は自動的に授業情報・休講情報などを取得・通知してくれます。 -時間割の自動生成機能もあります。 - -アプリは以下からインストール可能です。 - -**[Google Play](https://play.google.com/store/apps/details?id=jp.kentan.student_portal_plus)** - -## スクリーンショット - -![SS01](https://ken.kentan.jp/app/student_portal_plus/ss01.png) - -## ライセンス - -[GPLv3](https://github.com/ken-kentan/student-portal-plus/blob/master/LICENSE) - -## 開発者 - -[@ken_kentan](https://twitter.com/ken_kentan) \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 622ea7e..45315dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,18 +2,17 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' -apply plugin: 'com.cookpad.android.licensetools' android { - compileSdkVersion 27 + compileSdkVersion 28 defaultConfig { - applicationId "jp.kentan.student_portal_plus" - minSdkVersion 19 - targetSdkVersion 27 - versionCode 21 + applicationId "jp.kentan.student_portal_plus_dev" + minSdkVersion 18 + targetSdkVersion 28 + versionCode 1 versionName "2.0.0-alpha" vectorDrawables.useSupportLibrary = true - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -23,66 +22,53 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - lintOptions { - disable 'GoogleAppIndexingWarning' - baseline file("lint-baseline.xml") - } dataBinding { enabled = true } } dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - // Android Support - implementation "com.android.support:support-v4:$support_version" - implementation "com.android.support:appcompat-v7:$support_version" - implementation "com.android.support:cardview-v7:$support_version" - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - implementation "com.android.support:design:$support_version" - implementation "com.android.support:support-vector-drawable:$support_version" - implementation "com.android.support:customtabs:$support_version" + implementation fileTree(dir: 'libs', include: ['*.jar']) - // DataBinding - kapt 'com.android.databinding:compiler:3.1.4' - - // Android Jetpack - implementation 'androidx.core:core-ktx:0.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.30.0' + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.preference:preference:1.0.0' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.browser:browser:1.0.0' + implementation 'androidx.core:core-ktx:1.0.0' - // Architecture Components - implementation 'android.arch.lifecycle:extensions:1.1.1' + // Android Architecture Components + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' - implementation 'android.arch.work:work-runtime-ktx:1.0.0-alpha08' + // WorkManager + implementation 'android.arch.work:work-runtime-ktx:1.0.0-alpha09' // Google Dagger def dagger_version = '2.16' implementation "com.google.dagger:dagger:$dagger_version" implementation "com.google.dagger:dagger-android:$dagger_version" implementation "com.google.dagger:dagger-android-support:$dagger_version" + implementation 'androidx.vectordrawable:vectordrawable:1.0.0-alpha1' kapt "com.google.dagger:dagger-compiler:$dagger_version" kapt "com.google.dagger:dagger-android-processor:$dagger_version" - // Anko - def anko_version = '0.10.5' - implementation "org.jetbrains.anko:anko:$anko_version" - implementation "org.jetbrains.anko:anko-design:$anko_version" - // OkHttp - implementation 'com.squareup.okhttp3:okhttp:3.10.0' + implementation 'com.squareup.okhttp3:okhttp:3.11.0' implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' // jsoup - implementation 'org.jsoup:jsoup:1.11.2' + implementation 'org.jsoup:jsoup:1.11.3' - // ColorPicker - implementation project(':colorpicker') + // Anko + implementation 'org.jetbrains.anko:anko-sqlite:0.10.6' - // for Test testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + + androidTestImplementation 'androidx.test:runner:1.1.0-beta01' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-beta01' } kotlin { experimental { diff --git a/app/licenses.yml b/app/licenses.yml deleted file mode 100644 index a170f80..0000000 --- a/app/licenses.yml +++ /dev/null @@ -1,465 +0,0 @@ -- artifact: com.android.support:support-v4:+ - name: Android Support Library v4 - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: com.android.support:support-compat:+ - name: Android Support Library compat - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - skip: true -- artifact: com.android.support:support-core-utils:+ - name: Android Support Library core utils - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: com.android.support:support-core-ui:+ - name: Android Support Library core UI - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: com.android.support:support-media-compat:+ - name: Android Support Library media compat - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - skip: true -- artifact: com.android.support:support-fragment:+ - name: Android Support Library fragment - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - -- artifact: com.android.support:appcompat-v7:+ - name: Android AppCompat Library v7 - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: com.android.support:cardview-v7:+ - name: Android Support CardView v7 - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: com.android.support:recyclerview-v7:+ - name: Android Support RecyclerView v7 - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - -- artifact: com.android.support:support-vector-drawable:+ - name: Android Support VectorDrawable - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - -- artifact: com.android.support:animated-vector-drawable:+ - name: Android Support AnimatedVectorDrawable - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - skip: true - -- artifact: com.android.support:support-annotations:+ - name: Android Support Library Annotations - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - -- artifact: com.android.support:design:+ - name: Android Design Support Library - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - -- artifact: com.android.support:customtabs:+ - name: Android Support Custom Tabs - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - -- artifact: com.android.support.constraint:constraint-layout:+ - name: Android ConstraintLayout - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://tools.android.com - skip: true -- artifact: com.android.support.constraint:constraint-layout-solver:+ - name: Android ConstraintLayout Solver - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://tools.android.com - skip: true - -- artifact: com.android.support:transition:+ - name: Android Transition Support Library - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html - skip: true - -- artifact: androidx.core:core-ktx:+ - name: Android KTX Core - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/android/android-ktx - -- artifact: android.arch.core:common:+ - name: Android Arch-Common - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.core:runtime:+ - name: Android Arch-Runtime - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true - -- artifact: android.arch.lifecycle:common:+ - name: Android Lifecycle-Common - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.lifecycle:runtime:+ - name: Android Lifecycle Runtime - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.lifecycle:extensions:+ - name: Android Lifecycle Extensions - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ -- artifact: android.arch.lifecycle:viewmodel:+ - name: Android Lifecycle ViewModel - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.lifecycle:livedata:+ - name: Android Lifecycle LiveData - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.lifecycle:livedata-core:+ - name: Android Lifecycle LiveData Core - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true - -- artifact: android.arch.persistence.room:common:+ - name: Android Room-Common - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.persistence.room:runtime:+ - name: Android Room-Runtime - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.persistence:db:+ - name: Android DB - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: android.arch.persistence:db-framework:+ - name: Android Support SQLite - Framework Implementation - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true - -- artifact: android.arch.work:work-runtime:+ - name: Android WorkManager Runtime - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ -- artifact: android.arch.work:work-runtime-ktx:+ - name: Android WorkManager Kotlin Extensions - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ -- artifact: android.arch.work:work-firebase:+ - name: Android WorkManager Firebase - copyrightHolder: The Android Open Source Project - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/ - skip: true -- artifact: com.firebase:firebase-jobdispatcher:+ - name: firebase-jobdispatcher - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - skip: true - -- artifact: javax.inject:javax.inject:+ - name: javax.inject - copyrightHolder: The JSR 330 Expert Group. - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://code.google.com/p/atinject/ - -- artifact: com.google.dagger:dagger:+ - name: Dagger - copyrightHolder: Google - license: Apache 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/google/dagger -- artifact: com.google.dagger:dagger-android:+ - name: Dagger Android - copyrightHolder: Google - license: Apache 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/google/dagger -- artifact: com.google.dagger:dagger-android-support:+ - name: Dagger Android Support - copyrightHolder: Google - license: Apache 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/google/dagger - -- artifact: org.jetbrains.anko:anko:+ - name: Anko - copyrightHolder: JetBrains - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko -- artifact: org.jetbrains.anko:anko-coroutines:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-support-v4-commons:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-appcompat-v7-commons:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-sdk25:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-appcompat-v7-coroutines:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-sqlite:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-support-v4:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-commons:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-appcompat-v7:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-design:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true -- artifact: org.jetbrains.anko:anko-sdk25-coroutines:+ - name: anko - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt - url: https://github.com/JetBrains/anko - skip: true - -- artifact: org.jetbrains.kotlin:kotlin-stdlib:+ - name: org.jetbrains.kotlin:kotlin-stdlib - copyrightHolder: JetBrains - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk7:+ - name: org.jetbrains.kotlin:kotlin-stdlib-jdk7 - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ - skip: true -- artifact: org.jetbrains.kotlin:kotlin-stdlib-common:+ - name: org.jetbrains.kotlin:kotlin-stdlib-common - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ - skip: true -- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core:+ - name: kotlinx-coroutines-core - copyrightHolder: JetBrains - license: The Apache License, Version 2.0 -- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-android:+ - name: kotlinx-coroutines-android - copyrightHolder: #COPYRIGHT_HOLDER# - license: #LICENSE# - skip: true - -- artifact: org.jsoup:jsoup:+ - name: jsoup Java HTML Parser - copyrightHolder: Jonathan Hedley - license: The MIT License - licenseUrl: https://jsoup.org/license - url: https://jsoup.org/ - -- artifact: org.jetbrains:annotations:+ - name: IntelliJ IDEA Annotations - copyrightHolder: JetBrains - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://www.jetbrains.org - -- artifact: com.squareup.okhttp3:okhttp:+ - name: OkHttp - copyrightHolder: Square, Inc. - license: The Apache Software License, Version 2.0 -- artifact: com.github.franmontiel:PersistentCookieJar:+ - name: franmontiel/PersistentCookieJar - copyrightHolder: Francisco José Montiel Navarro - license: Apache License 2.0 - licenseUrl: https://api.github.com/licenses/apache-2.0 - url: https://github.com/franmontiel/PersistentCookieJar -- artifact: com.squareup.okio:okio:+ - name: Okio - copyrightHolder: #COPYRIGHT_HOLDER# - license: #LICENSE# - skip: true - -- artifact: org.apache.lucene:lucene-suggest:+ - name: Apache Lucene Suggest - copyrightHolder: The Apache Software Foundation - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://lucene.apache.org/ - forceGenerate: true -- artifact: org.apache.hive:hive-storage-api:+ - name: Apache Hive Storage API - copyrightHolder: The Apache Software Foundation - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://hive.apache.org/ - forceGenerate: true -#- artifact: co.hanken:orkney:+ -# name: Orkney -# copyrightHolder: Hanken Design Co. -# license: SIL Open Font License v1.10 -# licenseUrl: https://www.fontsquirrel.com/license/orkney -# url: https://www.fontsquirrel.com/fonts/orkney -# forceGenerate: true - - -- artifact: com.google.android.gms:play-services-basement-license:+ - name: play-services-basement-license - copyrightHolder: #COPYRIGHT_HOLDER# - license: Android Software Development Kit License - licenseUrl: https://developer.android.com/studio/terms.html - skip: true -- artifact: com.google.android.gms:play-services-tasks-license:+ - name: play-services-tasks-license - copyrightHolder: #COPYRIGHT_HOLDER# - license: Android Software Development Kit License - licenseUrl: https://developer.android.com/studio/terms.html - skip: true -- artifact: com.google.android.gms:play-services-basement:+ - name: play-services-basement - copyrightHolder: #COPYRIGHT_HOLDER# - license: Android Software Development Kit License - licenseUrl: https://developer.android.com/studio/terms.html - skip: true -- artifact: com.google.android.gms:play-services-tasks:+ - name: play-services-tasks - copyrightHolder: #COPYRIGHT_HOLDER# - license: Android Software Development Kit License - licenseUrl: https://developer.android.com/studio/terms.html - skip: true -- artifact: com.google.android.gms:play-services-base:+ - name: play-services-base - copyrightHolder: #COPYRIGHT_HOLDER# - license: Android Software Development Kit License - licenseUrl: https://developer.android.com/studio/terms.html - skip: true -- artifact: com.google.android.gms:play-services-base-license:+ - name: play-services-base-license - copyrightHolder: #COPYRIGHT_HOLDER# - license: Android Software Development Kit License - licenseUrl: https://developer.android.com/studio/terms.html - skip: true \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 8c20fe6..6208f94 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,7 +19,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile - -assumenosideeffects public class android.util.Log { public static *** v(...); public static *** d(...); @@ -39,4 +38,4 @@ -dontwarn org.conscrypt.** -dontwarn androidx.work.** # A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase +-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase \ No newline at end of file diff --git a/app/src/androidTest/java/jp/kentan/studentportalplus/ExampleInstrumentedTest.kt b/app/src/androidTest/java/jp/kentan/studentportalplus/ExampleInstrumentedTest.kt index 40c17be..690a5ac 100644 --- a/app/src/androidTest/java/jp/kentan/studentportalplus/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/jp/kentan/studentportalplus/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package jp.kentan.studentportalplus -import android.support.test.InstrumentationRegistry -import android.support.test.runner.AndroidJUnit4 - +import androidx.test.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 36f84a3..ae2be8a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -10,59 +11,67 @@ android:name=".StudentPortalPlus" android:allowBackup="false" android:hardwareAccelerated="true" - android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" - android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:supportsRtl="false" + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + android:name=".ui.main.MainActivity" + android:label="@string/app_name" + android:launchMode="singleTop" + android:theme="@style/AppTheme.NoActionBar"> - - - - - - - - - + android:name=".ui.login.LoginActivity" + android:label="@string/title_activity_login" /> + - - + + + + + + + + + + + android:name=".notification.RetryActionService" /> - - - - - Student Portal+ 利用規約・プライバシーポリシー - - - -

Student Portal+ 利用規約

- -

目的

-

この利用規約(以下、「本規約」といいます。)は、K² Studio. (以下、「開発元」といいます。) が開発した、 Student Portal+(以下、「当アプリ」といいます。)が提供するサービス(以下、「本サービス」といいます。)の利用条件を定めるものです。利用ユーザーの皆さま(以下、「ユーザー」といいます。)には、本規約に従って、本サービスをご利用いただきます。

- -

適用

-

本規約は、ユーザーと開発元との間の本サービスの利用に関わる一切の関係に適用されるものとします。

- -

著作権

-

当アプリに関する著作権および知的財産権は、開発元に帰属します。
- 当サービスが提供する情報に関する著作権および知的財産権は、情報の取得元に帰属します。

- -

禁止事項

-

ユーザーは、当サービスの利用にあたり、以下の行為を行ってはならないものとします。 -

    -
  • 当サービスまたは第三者の著作権、その他の知的財産権を侵害する行為、またはそのおそれがある行為
  • -
  • 他のユーザーまたは第三者に不利益を与える行為、またはそのおそれがある行為
  • -
  • 当サービスの運用を妨げる行為、またはそのおそれがある行為
  • -
  • 当サービスに含まれるソフトウェア情報および著作物について、リバースエンジニアリング、逆コンパイル、または逆アセンブルする行為
  • -
  • 当サービスを提供するにあたり使用しているインターネットサーバー(以下、「対象設備」といいます。)に不正アクセスし、または蓄積された情報を不正に書き換えるもしくは消去する行為
  • -
  • 対象設備に必要以上の負荷をかける行為
-

- -

本サービスの提供の停止等

-

当アプリは、必要と判断した場合、ユーザーに事前に通知することなく本サービスの全部または一部の提供を停止または中断することができるものとします。
- 当アプリは、本サービスの提供の停止または中断により、ユーザーまたは第三者が被ったいかなる不利益または損害について、理由を問わず一切の責任を負わないものとします。

- -

免責事項

-

- ユーザーが自身の判断と責任において、以下を承諾の上で当サービスを利用するものとし、当アプリの使用に伴って生じたいかなる損失または損害について、開発元は一切の責任を負わないものとします。
-

    -
  • 当サービスの利用または当サービスを利用できなかったことに関してユーザーに生じたいかなる損害についても、一切の責任を負いません。
  • -
  • 当サービスの動作および使用機器への適合性について一切保証しません。
  • -
  • 当サービスの利用により取得された情報の正確性、信頼性、その他一切の事項について保証しません。
  • -
  • アクセス過多、その他予期せぬ原因による当サービスの速度低下や障害等によって生じた一切の責任を負いません。
  • -
  • 当サービスの変更、中断、停止、または終了によって生じたいかなる損害について、一切の責任を負いません。
-

- -

利用規約の変更

-

当アプリは、必要と判断した場合、本規約を変更することができるものとします。改定後の利用規約は、当サービスに掲示された時点より効力を発するものとします。

- -

準拠法

-

本規約の解釈にあたっては、日本法を準拠法とします。


- -

Student Portal+ プライバシーポリシー

-

目的

-

当サービスおけるプライバシー情報の取扱いについて、以下のとおりプライバシーポリシー(以下、「本ポリシー」といいます。)を定めます。

- -

プライバシー情報

-

-

    -
  • プライバシー情報のうち「個人情報」とは、個人情報保護法にいう「個人情報」を指すものとし、生存する個人に関する情報であって、当該情報に含まれる氏名、生年月日、住所、電話番号、連絡先その他の記述等により特定の個人を識別できる情報を指します。
  • -
  • プライバシー情報のうち「履歴情報および特性情報」とは、上記に定める「個人情報」以外のものをいい、ご利用いただいたサービスや、ユーザーが検索された検索キーワード、ご利用日時、ご利用の方法、ご利用環境、性別、年齢、ユーザーのIPアドレス、クッキー情報、端末の個体識別情報などを指します。

- -

個人情報を取得・利用する目的

-

当アプリは 当サービスを提供するために、ポータルサイトから休講通知や授業関連連絡などの情報(以下、「ポータルデータ」といいます。)の収集のため、ユーザーのユーザーネームとパスワード(以下、「シボレス認証データ」といいます。)を当アプリを通して取得します。

- -

シボレス認証データの管理

-

ユーザーから取得したシボレス認証データ(氏名、ユーザーネーム、パスワード)は適切に暗号化され、入力された端末にのみ保存されます。これが上記の目的以外に利用されることはありません。

- -

個人情報の第三者提供

-

当アプリは、法令に基づく場合または国の機関もしくは地方公共団体またはその委託を受けた者が法令の定める事務を遂行することに対して協力する必要がある場合であって、本人の同意を得ることにより当該事務の遂行に支障を及ぼすおそれがある場合を除いて、あらかじめユーザーの同意を得ることなく、第三者に個人情報を提供することはありません。ただし、個人情報保護法その他の法令で認められる場合を除きます。

- -

プライバシーポリシーの変更

-

本ポリシーの内容は、ユーザーに通知することなく、変更することができるものとします。改定後の利用規約は、当サービスに掲示された時点より効力を発するものとします。

- - diff --git a/app/src/main/java/jp/kentan/studentportalplus/StudentPortalPlus.kt b/app/src/main/java/jp/kentan/studentportalplus/StudentPortalPlus.kt index 1f6c527..b16d06e 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/StudentPortalPlus.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/StudentPortalPlus.kt @@ -1,6 +1,6 @@ package jp.kentan.studentportalplus - +import androidx.appcompat.app.AppCompatDelegate import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule import dagger.android.support.DaggerApplication @@ -10,8 +10,7 @@ import jp.kentan.studentportalplus.di.FragmentModule import jp.kentan.studentportalplus.notification.SyncWorker import javax.inject.Singleton - -open class StudentPortalPlus : DaggerApplication() { +class StudentPortalPlus : DaggerApplication() { val component: StudentPortalPlus.Component by lazy(LazyThreadSafetyMode.NONE) { DaggerStudentPortalPlus_Component.builder() @@ -19,6 +18,12 @@ open class StudentPortalPlus : DaggerApplication() { .build() } + + override fun onCreate() { + super.onCreate() + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) + } + override fun applicationInjector(): AndroidInjector { return component } diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/PortalRepository.kt b/app/src/main/java/jp/kentan/studentportalplus/data/PortalRepository.kt index d59f5af..e71219f 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/PortalRepository.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/PortalRepository.kt @@ -1,12 +1,16 @@ package jp.kentan.studentportalplus.data -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MediatorLiveData -import android.arch.lifecycle.MutableLiveData import android.content.Context import android.content.SharedPreferences import android.util.Log -import jp.kentan.studentportalplus.data.component.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import jp.kentan.studentportalplus.data.component.LectureQuery +import jp.kentan.studentportalplus.data.component.NoticeQuery +import jp.kentan.studentportalplus.data.component.PortalData +import jp.kentan.studentportalplus.data.component.PortalDataSet import jp.kentan.studentportalplus.data.dao.* import jp.kentan.studentportalplus.data.model.* import jp.kentan.studentportalplus.data.parser.LectureCancellationParser @@ -15,198 +19,321 @@ import jp.kentan.studentportalplus.data.parser.MyClassParser import jp.kentan.studentportalplus.data.parser.NoticeParser import jp.kentan.studentportalplus.data.shibboleth.ShibbolethClient import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider -import jp.kentan.studentportalplus.util.getMyClassThreshold +import jp.kentan.studentportalplus.util.getSimilarSubjectThresholdFloat +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.async +import kotlinx.coroutines.experimental.launch import org.jetbrains.anko.defaultSharedPreferences +class PortalRepository( + private val context: Context, + shibbolethDataProvider: ShibbolethDataProvider +) { + private val client = ShibbolethClient(context, shibbolethDataProvider) -class PortalRepository(private val context: Context, shibbolethDataProvider: ShibbolethDataProvider) { + private val similarSubjectThresholdListener: SharedPreferences.OnSharedPreferenceChangeListener - private companion object { - const val TAG = "PortalRepository" - } - - private val shibbolethClient = ShibbolethClient(context, shibbolethDataProvider) - - private val preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener - - private val noticeParser = NoticeParser() - private val lectureInfoParser = LectureInformationParser() + private val noticeParser = NoticeParser() + private val lectureInfoParser = LectureInformationParser() private val lectureCancelParser = LectureCancellationParser() - private val myClassParser = MyClassParser() + private val myClassParser = MyClassParser() - private val noticeDao = NoticeDao(context.database) - private val lectureInfoDao : LectureInformationDao - private val lectureCancelDao : LectureCancellationDao - private val myClassDao = MyClassDao(context.database) + private val noticeDao = NoticeDao(context.database) + private val lectureInfoDao: LectureInformationDao + private val lectureCancelDao: LectureCancellationDao + private val myClassDao = MyClassDao(context.database) - private val _noticeList = MutableLiveData>() - private val _lectureInformationList = MutableLiveData>() - private val _lectureCancellationList = MutableLiveData>() - private val _myClassList = MutableLiveData>() - private val _portalDataSet = MutableLiveData() + private val _portalDataSet = MutableLiveData() + private val noticeList = MutableLiveData>() + private val lectureInfoList = MutableLiveData>() + private val lectureCancelList = MutableLiveData>() + private val _myClassList = MutableLiveData>() - val noticeList: LiveData> = _noticeList + val portalDataSet: LiveData + get() = _portalDataSet - val lectureInformationList: LiveData> = _lectureInformationList - - val lectureCancellationList: LiveData> = _lectureCancellationList - - val myClassList: LiveData> = _myClassList - - val portalDataSet: LiveData = _portalDataSet + val myClassList: LiveData> + get() = _myClassList val subjectList: LiveData> by lazy { return@lazy MediatorLiveData>().apply { - addSource(_lectureInformationList) { list -> - if (list != null) { - value = list.map { it.subject } - .plus(value.orEmpty()) - .distinct() - } + addSource(lectureInfoList) { list -> + value = list.asSequence() + .map { it.subject } + .plus(value.orEmpty()) + .distinct().toList() } - addSource(_lectureCancellationList) { list -> - if (list != null) { - value = list.map { it.subject } - .plus(value.orEmpty()) - .distinct() - } + addSource(lectureCancelList) { list -> + value = list.asSequence() + .map { it.subject } + .plus(value.orEmpty()) + .distinct().toList() } addSource(_myClassList) { list -> - if (list != null) { - value = list.map { it.subject } - .plus(value.orEmpty()) - .distinct() - } + value = list.asSequence() + .map { it.subject } + .plus(value.orEmpty()) + .distinct().toList() } } } - init { - // Setup with my_class_threshold - val threshold = context.defaultSharedPreferences.getMyClassThreshold() + val threshold = context.defaultSharedPreferences.getSimilarSubjectThresholdFloat() lectureInfoDao = LectureInformationDao(context.database, threshold) lectureCancelDao = LectureCancellationDao(context.database, threshold) - // Update if MyClassThreshold changed - preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { pref, key -> - if (key == "my_class_threshold") { - val th = pref.getMyClassThreshold() + similarSubjectThresholdListener = SharedPreferences.OnSharedPreferenceChangeListener { pref, key -> + if (key == "similar_subject_threshold") { + val th = pref.getSimilarSubjectThresholdFloat() - lectureInfoDao.myClassThreshold = th - lectureCancelDao.myClassThreshold = th + lectureInfoDao.similarThreshold = th + lectureCancelDao.similarThreshold = th - postValues( - lectureInfoList = lectureInfoDao.getAll(), - lectureCancelList = lectureCancelDao.getAll() - ) + GlobalScope.launch { + postValues( + lectureInfoList = lectureInfoDao.getAll(), + lectureCancelList = lectureCancelDao.getAll() + ) + } } } - context.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + context.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(similarSubjectThresholdListener) } - fun loadFromDb() { + @Throws(Exception::class) + fun sync() = GlobalScope.async { + val noticeList = noticeParser.parse(PortalData.NOTICE.fetchDocument()) + val lectureInfoList = lectureInfoParser.parse(PortalData.LECTURE_INFO.fetchDocument()) + val lectureCancelList = lectureCancelParser.parse(PortalData.LECTURE_CANCEL.fetchDocument()) + val myClassList = myClassParser.parse(PortalData.MY_CLASS.fetchDocument()) + + myClassDao.updateAll(myClassList) + + val updatedNoticeList = noticeDao.updateAll(noticeList) + val updatedLectureInfoList = lectureInfoDao.updateAll(lectureInfoList) + val updatedLectureCancelList = lectureCancelDao.updateAll(lectureCancelList) + + loadFromDb().await() + + return@async mapOf( + PortalData.NOTICE to updatedNoticeList, + PortalData.LECTURE_INFO to updatedLectureInfoList, + PortalData.LECTURE_CANCEL to updatedLectureCancelList + ) + } + + fun loadFromDb() = GlobalScope.async { postValues( - myClassDao.getAll(), - lectureInfoDao.getAll(), - lectureCancelDao.getAll(), - noticeDao.getAll() + noticeList = noticeDao.getAll(), + lectureInfoList = lectureInfoDao.getAll(), + lectureCancelList = lectureCancelDao.getAll(), + myClassList = myClassDao.getAll() ) } - @Throws(Exception::class) - fun sync(): Map> { - val noticeList = noticeParser.parse(fetchDocument(PortalDataType.NOTICE)) - val lectureInfoList = lectureInfoParser.parse(fetchDocument(PortalDataType.LECTURE_INFORMATION)) - val lectureCancelList = lectureCancelParser.parse(fetchDocument(PortalDataType.LECTURE_CANCELLATION)) - val myCLassList = myClassParser.parse(fetchDocument(PortalDataType.MY_CLASS)) + fun getNoticeList(query: NoticeQuery): LiveData> { + val result = MediatorLiveData>() + + result.addSource(noticeList) { list -> + GlobalScope.launch { + result.postValue( + list.filter { notice -> + if (query.isUnread && notice.isRead) { + return@filter false + } + if (query.isRead && !notice.isRead) { + return@filter false + } + if (query.isFavorite && !notice.isFavorite) { + return@filter false + } + if (query.dateRange != NoticeQuery.DateRange.ALL) { + return@filter notice.createdDate.time >= query.dateRange.time + } + if (query.keywordList.isNotEmpty()) { + return@filter query.keywordList.any { notice.title.contains(it, true) } + } + + return@filter true + } + ) + } + } + + return result + } + + fun getLectureInfoList(query: LectureQuery): LiveData> { + val result = MediatorLiveData>() + + result.addSource(lectureInfoList) { list -> + GlobalScope.launch { + val filtered = list.filter { lecture -> + if (query.isUnread && lecture.isRead) { + return@filter false + } + if (query.isRead && !lecture.isRead) { + return@filter false + } + if (query.isAttend && !lecture.attend.isAttend()) { + return@filter false + } + if (query.keywordList.isNotEmpty()) { + return@filter query.keywordList.any { + lecture.subject.contains(it, true) || lecture.instructor.contains(it, true) + } + } + + return@filter true + } - myClassDao.updateAll(myCLassList) + result.postValue( + if (query.order == LectureQuery.Order.ATTEND_CLASS) { + filtered.sortedBy { !it.attend.isAttend() } + } else { + filtered + } + ) + } + } - val newNoticeList = noticeDao.updateAll(noticeList) - val newLectureInfoList = lectureInfoDao.updateAll(lectureInfoList) - val newLectureCancelList = lectureCancelDao.updateAll(lectureCancelList) + return result + } - loadFromDb() + fun getLectureCancelList(query: LectureQuery): LiveData> { + val result = MediatorLiveData>() + + result.addSource(lectureCancelList) { list -> + GlobalScope.launch { + val filtered = list.filter { lecture -> + if (query.isUnread && lecture.isRead) { + return@filter false + } + if (query.isRead && !lecture.isRead) { + return@filter false + } + if (query.isAttend && !lecture.attend.isAttend()) { + return@filter false + } + if (query.keywordList.isNotEmpty()) { + return@filter query.keywordList.any { + lecture.subject.contains(it, true) || lecture.instructor.contains(it, true) + } + } + + return@filter true + } - return mapOf( - PortalDataType.NOTICE to newNoticeList, - PortalDataType.LECTURE_INFORMATION to newLectureInfoList, - PortalDataType.LECTURE_CANCELLATION to newLectureCancelList - ) + result.postValue( + if (query.order == LectureQuery.Order.ATTEND_CLASS) { + filtered.sortedBy { !it.attend.isAttend() } + } else { + filtered + } + ) + } + } + + return result } - fun getLectureInformationById(id: Long) = _lectureInformationList.value?.find { it.id == id } ?: lectureInfoDao.get(id) + fun getMyClassSubjectList() = myClassDao.getSubjectList() - fun getLectureCancellationById(id: Long) = _lectureCancellationList.value?.find { it.id == id } ?: lectureCancelDao.get(id) + fun getNotice(id: Long): LiveData = Transformations.map(noticeList) { list -> + list.find { it.id == id } + } - fun getNoticeById(id: Long) = _noticeList.value?.find { it.id == id } ?: noticeDao.get(id) + fun getLectureInfo(id: Long): LiveData = Transformations.map(lectureInfoList) { list -> + list.find { it.id == id } + } - fun getMyClassById(id: Long) = _myClassList.value?.find { it.id == id } + fun getLectureCancel(id: Long): LiveData = Transformations.map(lectureCancelList) { list -> + list.find { it.id == id } + } - fun getMyClassSubjectList() = myClassDao.getSubjectList() + fun getMyClass(id: Long, isAllowNullOnlyFirst: Boolean = false): LiveData { + val result = MediatorLiveData() - fun searchNotices(query: NoticeQuery) = noticeDao.search(query) + var isFirst = true - fun searchLectureInformation(query: LectureQuery) = lectureInfoDao.search(query) + result.addSource(_myClassList) { list -> + val data = list.find { it.id == id } - fun searchLectureCancellations(query: LectureQuery) = lectureCancelDao.search(query) + if (!isAllowNullOnlyFirst || isFirst || data != null) { + result.value = data + } - fun update(data: Notice): Boolean { + isFirst = false + } + + return result + } + + fun getMyClassWithSync(id: Long) = _myClassList.value?.find { it.id == id } + + fun updateNotice(data: Notice) = GlobalScope.async { if (noticeDao.update(data) > 0) { postValues(noticeList = noticeDao.getAll()) - return true + return@async true } - return false + + return@async false } - fun update(data: LectureInformation) { + fun updateLectureInfo(data: LectureInformation) = GlobalScope.async { if (lectureInfoDao.update(data) > 0) { postValues(lectureInfoList = lectureInfoDao.getAll()) + return@async true } + + return@async false } - fun update(data: LectureCancellation) { + fun updateLectureCancel(data: LectureCancellation) = GlobalScope.async { if (lectureCancelDao.update(data) > 0) { postValues(lectureCancelList = lectureCancelDao.getAll()) + return@async true } + + return@async false } - fun update(data: MyClass): Boolean { + fun updateMyClass(data: MyClass) = GlobalScope.async { if (myClassDao.update(data) > 0) { - postValues(myClassDao.getAll()) - return true + postValues( + myClassDao.getAll(), + lectureInfoDao.getAll(), + lectureCancelDao.getAll()) + return@async true } - return false - } - fun add(data: MyClass): Boolean { - if (myClassDao.add(listOf(data)) > 0) { - postValues(myClassDao.getAll()) - return true - } - return false + return@async false } - fun delete(subject: String): Boolean { - if (myClassDao.delete(subject) > 0) { - postValues(myClassDao.getAll()) - return true + fun addMyClass(data: MyClass) = GlobalScope.async { + if (myClassDao.insert(listOf(data)) > 0) { + postValues( + myClassDao.getAll(), + lectureInfoDao.getAll(), + lectureCancelDao.getAll()) + return@async true } - return false + + return@async false } - fun addToMyClass(data: Lecture): Boolean { + fun addToMyClass(data: Lecture) = GlobalScope.async { try { val list = myClassParser.parse(data) - myClassDao.add(list) + myClassDao.insert(list) } catch (e: Exception) { - Log.e(TAG, "Failed to add to MyClass", e) - return false + Log.e("PortalRepository", "Failed to add to MyClass", e) + return@async false } postValues( @@ -215,15 +342,15 @@ class PortalRepository(private val context: Context, shibbolethDataProvider: Shi lectureCancelDao.getAll() ) - return true + return@async true } - fun deleteFromMyClass(data: Lecture): Boolean { + fun deleteFromMyClass(subject: String) = GlobalScope.async { try { - myClassDao.delete(data.subject) + myClassDao.delete(subject) } catch (e: Exception) { - Log.e(TAG, "Failed to delete from MyClass", e) - return false + Log.e("PortalRepository", "Failed to delete from MyClass", e) + return@async false } postValues( @@ -232,21 +359,19 @@ class PortalRepository(private val context: Context, shibbolethDataProvider: Shi lectureCancelDao.getAll() ) - return true + return@async true } - fun deleteAll(onDeleted: (success: Boolean) -> Unit) { - val success = context.deleteDatabase(context.database.databaseName) - if (success) { + fun deleteAll() = GlobalScope.async { + val isSuccess = context.deleteDatabase(context.database.databaseName) + + if (isSuccess) { postValues(emptyList(), emptyList(), emptyList(), emptyList()) } - onDeleted(success) + return@async isSuccess } - @Throws(Exception::class) - private fun fetchDocument(type: PortalDataType) = shibbolethClient.fetch(type.url) - private fun postValues( myClassList: List? = null, lectureInfoList: List? = null, @@ -258,25 +383,25 @@ class PortalRepository(private val context: Context, shibbolethDataProvider: Shi if (myClassList != null) { postCount++ - _myClassList.postValue(myClassList) + this._myClassList.postValue(myClassList) set = set.copy(myClassList = myClassList) } if (lectureInfoList != null) { postCount++ - _lectureInformationList.postValue(lectureInfoList) + this.lectureInfoList.postValue(lectureInfoList) set = set.copy(lectureInfoList = lectureInfoList) } if (lectureCancelList != null) { postCount++ - _lectureCancellationList.postValue(lectureCancelList) + this.lectureCancelList.postValue(lectureCancelList) set = set.copy(lectureCancelList = lectureCancelList) } if (noticeList != null) { postCount++ - _noticeList.postValue(noticeList) + this.noticeList.postValue(noticeList) set = set.copy(noticeList = noticeList) } @@ -285,6 +410,8 @@ class PortalRepository(private val context: Context, shibbolethDataProvider: Shi _portalDataSet.postValue(set) } - Log.d(TAG, "posted $postCount lists") + Log.d("PortalRepository", "posted $postCount lists") } + + private fun PortalData.fetchDocument() = client.fetch(url) } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/UserRepository.kt b/app/src/main/java/jp/kentan/studentportalplus/data/UserRepository.kt new file mode 100644 index 0000000..d1ca740 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/UserRepository.kt @@ -0,0 +1,9 @@ +package jp.kentan.studentportalplus.data + +import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider + +class UserRepository( + private val provider: ShibbolethDataProvider +) { + fun getUser() = provider.getUser() +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassColor.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassColor.kt index 9d23fb0..ac39c6f 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassColor.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassColor.kt @@ -5,14 +5,15 @@ import android.graphics.Color class ClassColor { companion object { val DEFAULT = Color.parseColor("#4FC3F7") - val ALL = intArrayOf( - Color.parseColor("#4FC3F7"), Color.parseColor("#03A9F4"), Color.parseColor("#0288D1"), Color.parseColor("#01579B"), //Light Blue 300 500 700 900 - Color.parseColor("#1B5E20"), Color.parseColor("#388E3C"), Color.parseColor("#4CAF50"), Color.parseColor("#81C784"), //Green 900 700 500 300 - Color.parseColor("#E57373"), Color.parseColor("#F44336"), Color.parseColor("#D32F2F"), Color.parseColor("#B71C1C"), //Red 300 500 700 900 - Color.parseColor("#E65100"), Color.parseColor("#F57C00"), Color.parseColor("#FF9800"), Color.parseColor("#FFB74D"), //Orange 900 700 500 300 - Color.parseColor("#BA68C8"), Color.parseColor("#9C27B0"), Color.parseColor("#7B1FA2"), Color.parseColor("#4A148C"), //Purple 300 500 700 900 - Color.parseColor("#3E2723"), Color.parseColor("#5D4037"), Color.parseColor("#795548"), Color.parseColor("#A1887F") //Brown 900 700 500 300 - ) + val ALL by lazy { + intArrayOf( + Color.parseColor("#4FC3F7"), Color.parseColor("#03A9F4"), Color.parseColor("#0288D1"), Color.parseColor("#01579B"), //Light Blue 300 500 700 900 + Color.parseColor("#1B5E20"), Color.parseColor("#388E3C"), Color.parseColor("#4CAF50"), Color.parseColor("#81C784"), //Green 900 700 500 300 + Color.parseColor("#E57373"), Color.parseColor("#F44336"), Color.parseColor("#D32F2F"), Color.parseColor("#B71C1C"), //Red 300 500 700 900 + Color.parseColor("#E65100"), Color.parseColor("#F57C00"), Color.parseColor("#FF9800"), Color.parseColor("#FFB74D"), //Orange 900 700 500 300 + Color.parseColor("#BA68C8"), Color.parseColor("#9C27B0"), Color.parseColor("#7B1FA2"), Color.parseColor("#4A148C"), //Purple 300 500 700 900 + Color.parseColor("#3E2723"), Color.parseColor("#5D4037"), Color.parseColor("#795548"), Color.parseColor("#A1887F")) //Brown 900 700 500 300 + } val size = ALL.size } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeek.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeek.kt new file mode 100644 index 0000000..e4ecb65 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeek.kt @@ -0,0 +1,36 @@ +package jp.kentan.studentportalplus.data.component + +enum class ClassWeek( + val code: Int, + val displayName: String +) { + MONDAY (1, "月"), + TUESDAY (2, "火"), + WEDNESDAY(3, "水"), + THURSDAY (4, "木"), + FRIDAY (5, "金"), + SATURDAY (6, "土"), + SUNDAY (7, "日"), + INTENSIVE(8, "集中"), + UNKNOWN (0, "-"); + + val fullDisplayName = displayName + if (code in 1..7) "曜日" else "" + + override fun toString() = fullDisplayName + + fun hasPeriod() = code in 1..7 + + companion object { + private val ENUMS = values() + + val TIMETABLE = arrayListOf(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY) + + fun valueOf(code: Int) = + ENUMS.find { it.code == code } + ?: throw IllegalArgumentException("Invalid ClassWeek code: $code") + + fun valueOfSimilar(name: String) = + ENUMS.find { name == it.displayName || name == it.fullDisplayName } + ?: throw IllegalArgumentException("Invalid ClassWeek name: $name") + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeekType.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeekType.kt deleted file mode 100644 index a3e21f3..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/ClassWeekType.kt +++ /dev/null @@ -1,47 +0,0 @@ -package jp.kentan.studentportalplus.data.component - - -/** - * Class week type - * - * https://portal.student.kit.ac.jp/ead/?c=attend_course - */ -enum class ClassWeekType(val code: Int, val displayName: String) { - MONDAY (1, "月"), - TUESDAY (2, "火"), - WEDNESDAY(3, "水"), - THURSDAY (4, "木"), - FRIDAY (5, "金"), - SATURDAY (6, "土"), - SUNDAY (7, "日"), - INTENSIVE(8, "集中"), - UNKNOWN (9, "-"); - - val fullDisplayName = displayName + if (code in 1..7) "曜日" else "" - - override fun toString() = fullDisplayName - - fun hasPeriod() = code in 1..7 - - companion object { - private val ENUMS = values() - - fun valueOf(code: Int): ClassWeekType { - if (code in 1..9) { - return ENUMS[code-1] - } - - throw Exception("Invalid ClassWeekType code: $code") - } - - fun valueOfSimilar(name: String): ClassWeekType { - ENUMS.forEach { - if (name == it.displayName || name == it.fullDisplayName) { - return it - } - } - - throw Exception("Invalid ClassWeekType name: $name") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/CreatedDateType.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/CreatedDateType.kt deleted file mode 100644 index af40ff7..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/CreatedDateType.kt +++ /dev/null @@ -1,12 +0,0 @@ -package jp.kentan.studentportalplus.data.component - - -enum class CreatedDateType(private val string: String){ - ALL("全期間"), - DAY("今日"), - WEEK("今週"), - MONTH("今月"), - YEAR("今年"); - - override fun toString() = string -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureAttendType.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureAttend.kt similarity index 90% rename from app/src/main/java/jp/kentan/studentportalplus/data/component/LectureAttendType.kt rename to app/src/main/java/jp/kentan/studentportalplus/data/component/LectureAttend.kt index 3a06353..f321100 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureAttendType.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureAttend.kt @@ -1,7 +1,6 @@ package jp.kentan.studentportalplus.data.component - -enum class LectureAttendType { +enum class LectureAttend { PORTAL, // ポータル取得 USER, // ユーザー登録 SIMILAR, // 類似 diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureOrderType.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureOrderType.kt deleted file mode 100644 index 5dc38b8..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureOrderType.kt +++ /dev/null @@ -1,8 +0,0 @@ -package jp.kentan.studentportalplus.data.component - -enum class LectureOrderType(private val string: String) { - UPDATED_DATE("最終更新日"), - ATTEND_CLASS("受講科目優先"); - - override fun toString() = string -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureQuery.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureQuery.kt index 1d2da57..e3e01ec 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureQuery.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/LectureQuery.kt @@ -1,29 +1,32 @@ package jp.kentan.studentportalplus.data.component -import jp.kentan.studentportalplus.data.dao.escapeQuery data class LectureQuery( - val keywords: String?, - val order : LectureOrderType, - val isUnread: Boolean, - val hasRead : Boolean, - val isAttend: Boolean + val keyword: String? = null, + val order: Order = Order.UPDATED_DATE, + val isUnread: Boolean = false, + val isRead: Boolean = false, + val isAttend: Boolean = false ) { + val keywordList: List by lazy { - if (keywords == null || keywords.isEmpty()) { + if (keyword == null || keyword.isBlank()) { emptyList() } else { - keywords.split(' ') + keyword.split(' ') .mapNotNull { val trim = it.trim() - if (trim.isNotEmpty()) trim.escapeQuery() else null + if (trim.isNotBlank()) trim else null } } } - companion object { - val DEFAULT = LectureQuery(null, LectureOrderType.UPDATED_DATE, true, true, true) - } -} + enum class Order( + private val displayName: String + ) { + UPDATED_DATE("最終更新日"), + ATTEND_CLASS("受講科目優先"); -fun LectureQuery.isDefault() = (this == LectureQuery.DEFAULT) \ No newline at end of file + override fun toString() = displayName + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/NoticeQuery.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/NoticeQuery.kt index 0499ca8..a8b43da 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/NoticeQuery.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/NoticeQuery.kt @@ -1,29 +1,55 @@ package jp.kentan.studentportalplus.data.component -import jp.kentan.studentportalplus.data.dao.escapeQuery +import java.util.* data class NoticeQuery( - val keywords : String?, - val type : CreatedDateType, - val isUnread : Boolean, - val hasRead : Boolean, - val isFavorite: Boolean + val keyword: String? = null, + val dateRange: DateRange = DateRange.ALL, + val isUnread: Boolean = false, + val isRead: Boolean = false, + val isFavorite: Boolean = false ) { + val keywordList: List by lazy { - if (keywords == null || keywords.isEmpty()) { + if (keyword == null || keyword.isBlank()) { emptyList() } else { - keywords.split(' ') + keyword.split(' ') .mapNotNull { val trim = it.trim() - if (trim.isNotEmpty()) trim.escapeQuery() else null + if (trim.isNotBlank()) trim else null } } } - companion object { - val DEFAULT = NoticeQuery(null, CreatedDateType.ALL, true, true, true) - } -} + enum class DateRange( + private val displayName: String + ) { + ALL("全期間"), + DAY("今日"), + WEEK("今週"), + MONTH("今月"), + YEAR("今年"); + + val time: Long + get() { + val calendar = Calendar.getInstance().apply { + clear(Calendar.MINUTE) + clear(Calendar.SECOND) + clear(Calendar.MILLISECOND) + set(Calendar.HOUR_OF_DAY, 0) + } -fun NoticeQuery.isDefault() = (this == NoticeQuery.DEFAULT) \ No newline at end of file + when(this) { + WEEK -> { calendar.set(Calendar.DAY_OF_WEEK , calendar.firstDayOfWeek) } + MONTH -> { calendar.set(Calendar.DAY_OF_MONTH, 1) } + YEAR -> { calendar.set(Calendar.DAY_OF_YEAR , 1) } + else -> {} + } + + return calendar.timeInMillis + } + + override fun toString() = displayName + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyContent.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyContent.kt deleted file mode 100644 index e5426aa..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyContent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package jp.kentan.studentportalplus.data.component - -data class NotifyContent( - val title: String, - val text : String?, - val id : Long // for Database -) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyType.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyType.kt deleted file mode 100644 index df361b3..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/NotifyType.kt +++ /dev/null @@ -1,17 +0,0 @@ -package jp.kentan.studentportalplus.data.component - -import android.content.SharedPreferences - -enum class NotifyType( - val displayName: String -) { - ALL("すべて"), - ATTEND("受講科目・類似科目"), - NOT("通知しない"); - - companion object { - fun getBy(preferences: SharedPreferences, key: String): NotifyType { - return valueOf(preferences.getString(key, ALL.name)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalContent.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalContent.kt new file mode 100644 index 0000000..e2446f9 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalContent.kt @@ -0,0 +1,7 @@ +package jp.kentan.studentportalplus.data.component + +data class PortalContent( + val id: Long, + val title: String, + val text: String? +) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalData.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalData.kt new file mode 100644 index 0000000..df12457 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalData.kt @@ -0,0 +1,14 @@ +package jp.kentan.studentportalplus.data.component + +import androidx.annotation.StringRes +import jp.kentan.studentportalplus.R + +enum class PortalData( + val url: String, + @StringRes val nameResId: Int +) { + NOTICE("https://portal.student.kit.ac.jp", R.string.name_notice), + LECTURE_INFO("https://portal.student.kit.ac.jp/ead/?c=lecture_information", R.string.name_lecture_info), + LECTURE_CANCEL("https://portal.student.kit.ac.jp/ead/?c=lecture_cancellation", R.string.name_lecture_cancel), + MY_CLASS("https://portal.student.kit.ac.jp/ead/?c=attend_course", R.string.name_my_class) +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataSet.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataSet.kt index d2a81ca..1691f8e 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataSet.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataSet.kt @@ -6,8 +6,8 @@ import jp.kentan.studentportalplus.data.model.MyClass import jp.kentan.studentportalplus.data.model.Notice data class PortalDataSet( - val myClassList: List = emptyList(), val lectureInfoList: List = emptyList(), val lectureCancelList: List = emptyList(), - val noticeList: List = emptyList() + val noticeList: List = emptyList(), + val myClassList: List = emptyList() ) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataType.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataType.kt deleted file mode 100644 index 0248d2b..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/PortalDataType.kt +++ /dev/null @@ -1,13 +0,0 @@ -package jp.kentan.studentportalplus.data.component - - -enum class PortalDataType( - val url: String, - val displayName: String, - val notifyTypeKey: String -) { - NOTICE("https://portal.student.kit.ac.jp", "最新情報", "notify_type_notice"), - LECTURE_INFORMATION("https://portal.student.kit.ac.jp/ead/?c=lecture_information", "授業関連連絡", "notify_type_lecture_info"), - LECTURE_CANCELLATION("https://portal.student.kit.ac.jp/ead/?c=lecture_cancellation", "休講情報", "notify_type_lecture_cancel"), - MY_CLASS("https://portal.student.kit.ac.jp/ead/?c=attend_course", "受講情報", "notify_type_my_class") -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/component/ShibbolethData.kt b/app/src/main/java/jp/kentan/studentportalplus/data/component/ShibbolethData.kt deleted file mode 100644 index dea8b1a..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/data/component/ShibbolethData.kt +++ /dev/null @@ -1,4 +0,0 @@ -package jp.kentan.studentportalplus.data.component - - -data class ShibbolethData(val username: String, val password: String) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/dao/BaseDao.kt b/app/src/main/java/jp/kentan/studentportalplus/data/dao/BaseDao.kt new file mode 100644 index 0000000..a5c8c2e --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/dao/BaseDao.kt @@ -0,0 +1,19 @@ +package jp.kentan.studentportalplus.data.dao + +import android.database.sqlite.SQLiteStatement + +abstract class BaseDao { + + protected fun SQLiteStatement.bindStringOrNull(index: Int, value: String?) { + if (value != null) { + bindString(index, value) + } else { + bindNull(index) + } + } + + fun String.escapeQuery() = + replace("'", "''").replace("%", "\$%").replace("_", "\$_") + + protected fun Boolean.toLong() = if (this) 1L else 0L +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseMigrationHelper.kt b/app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseMigrationHelper.kt new file mode 100644 index 0000000..1efd3c2 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseMigrationHelper.kt @@ -0,0 +1,67 @@ +package jp.kentan.studentportalplus.data.dao + +import android.database.sqlite.SQLiteDatabase +import androidx.core.database.getStringOrNull +import jp.kentan.studentportalplus.data.component.ClassWeek +import org.jetbrains.anko.db.dropTable +import org.jetbrains.anko.db.insert +import org.jetbrains.anko.db.select +import org.jetbrains.anko.db.transaction + +class DatabaseMigrationHelper { + companion object { + + fun upgradeVersion3From2(db: SQLiteDatabase, createTablesIfNotExist:(SQLiteDatabase) -> Unit) { + db.transaction { + db.execSQL("ALTER TABLE my_class RENAME TO tmp_my_class") + + db.dropTable("news", true) + db.dropTable("lecture_info", true) + db.dropTable("lecture_cancel", true) + db.dropTable("my_class", true) + + db.execSQL("DELETE FROM sqlite_sequence") + + createTablesIfNotExist(db) + + db.select("tmp_my_class").exec { + while (moveToNext()) { + val week: ClassWeek = getLong(2).let { + return@let ClassWeek.valueOf(it.toInt() + 1) + } + val period = getLong(3).let { if (it > 0) it else 0 } + val scheduleCode = getLong(9).toString() + val credit = getLong(8) + val category = getStringOrNull(7) ?: "" + val subject = getString(4) + val isUser = getLong(11) == 1L + val instructor = getStringOrNull(5).let { it ?: return@let "" + return@let if (isUser) { it } else { it.replace(' ', ' ') } + } + val location = getStringOrNull(6)?.let { + val trim = it.trim() + return@let if (trim.isBlank()) null else trim + } + + db.insert("my_class", + "_id" to null, + "week" to week.code, + "period" to period, + "schedule_code" to scheduleCode, + "credit" to credit, + "category" to category, + "subject" to subject, + "instructor" to instructor, + "user" to isUser.toLong(), + "color" to getLong(10), + "location" to location) + } + } + + db.dropTable("tmp_my_class") + } + } + + private fun Boolean.toLong() = if (this) 1L else 0L + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseOpenHelper.kt b/app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseOpenHelper.kt index ad6ac48..f48ce0f 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseOpenHelper.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/dao/DatabaseOpenHelper.kt @@ -2,16 +2,7 @@ package jp.kentan.studentportalplus.data.dao import android.content.Context import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteStatement -import androidx.core.database.getLong -import androidx.core.database.getString -import androidx.core.database.getStringOrNull -import jp.kentan.studentportalplus.data.component.ClassWeekType -import jp.kentan.studentportalplus.util.Murmur3 -import jp.kentan.studentportalplus.util.toLong import org.jetbrains.anko.db.* -import java.text.SimpleDateFormat -import java.util.* /** * DatabaseOpenHelper @@ -30,12 +21,6 @@ class DatabaseOpenHelper(context: Context) : ManagedSQLiteOpenHelper(context, "p } return instance!! } - - - private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.JAPAN) - - fun toString(date: Date): String = DATE_FORMAT.format(date) - fun toDate(date: String): Date = DATE_FORMAT.parse(date) } override fun onCreate(db: SQLiteDatabase) { @@ -43,54 +28,8 @@ class DatabaseOpenHelper(context: Context) : ManagedSQLiteOpenHelper(context, "p } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion == 2) { - db.transaction { - db.execSQL("ALTER TABLE my_class RENAME TO tmp_my_class") - - db.dropTable("news", true) - db.dropTable("lecture_info", true) - db.dropTable("lecture_cancel", true) - db.dropTable("my_class", true) - - db.execSQL("DELETE FROM sqlite_sequence") - - createTablesIfNotExist(db) - - db.select("tmp_my_class").exec { - while (moveToNext()) { - val week: ClassWeekType = getLong("day_of_week").let { - return@let ClassWeekType.valueOf(it.toInt() + 1) - } - val period = getLong("period").let { if (it > 0) it else 0 } - val scheduleCode = getLong("timetable_number").toString() - val credit = getLong("credits") - val category = getStringOrNull("type") ?: "" - val subject = getString("subject") - val isUser = getLong("registered_by_user") == 1L - val instructor = getStringOrNull("instructor").let { it ?: return@let "" - return@let if (isUser) { it } else { it.replace(' ', ' ') } - } - - val hashStr = week.name + period + scheduleCode + credit + category + subject + instructor + isUser - - db.insert("my_class", - "_id" to null, - "hash" to Murmur3.hash64(hashStr.toByteArray()), - "week" to week.code, - "period" to period, - "schedule_code" to scheduleCode, - "credit" to credit, - "category" to category, - "subject" to subject, - "instructor" to instructor, - "user" to isUser.toLong(), - "color" to getLong("color_rgb"), - "location" to getStringOrNull("place")) - } - } - - db.dropTable("tmp_my_class") - } + if (oldVersion == 2 && newVersion == 3) { + DatabaseMigrationHelper.upgradeVersion3From2(db, ::createTablesIfNotExist) } } @@ -98,7 +37,7 @@ class DatabaseOpenHelper(context: Context) : ManagedSQLiteOpenHelper(context, "p db.createTable(NoticeDao.TABLE_NAME, true, "_id" to INTEGER + PRIMARY_KEY + AUTOINCREMENT, "hash" to INTEGER + NOT_NULL + UNIQUE, - "created_date" to TEXT + NOT_NULL, + "created_date" to INTEGER + NOT_NULL, "in_charge" to TEXT + NOT_NULL, "category" to TEXT + NOT_NULL, "title" to TEXT + NOT_NULL, @@ -120,8 +59,8 @@ class DatabaseOpenHelper(context: Context) : ManagedSQLiteOpenHelper(context, "p "category" to TEXT + NOT_NULL, "detail_text" to TEXT + NOT_NULL, "detail_html" to TEXT + NOT_NULL, - "created_date" to TEXT + NOT_NULL, - "updated_date" to TEXT + NOT_NULL, + "created_date" to INTEGER + NOT_NULL, + "updated_date" to INTEGER + NOT_NULL, "read" to INTEGER + NOT_NULL + DEFAULT("0")) db.createTable(LectureCancellationDao.TABLE_NAME, true, @@ -130,12 +69,12 @@ class DatabaseOpenHelper(context: Context) : ManagedSQLiteOpenHelper(context, "p "grade" to TEXT + NOT_NULL, "subject" to TEXT + NOT_NULL, "instructor" to TEXT + NOT_NULL, - "cancel_date" to TEXT + NOT_NULL, + "cancel_date" to INTEGER + NOT_NULL, "week" to TEXT + NOT_NULL, "period" to TEXT + NOT_NULL, "detail_text" to TEXT + NOT_NULL, "detail_html" to TEXT + NOT_NULL, - "created_date" to TEXT + NOT_NULL, + "created_date" to INTEGER + NOT_NULL, "read" to INTEGER + NOT_NULL + DEFAULT("0")) db.createTable(MyClassDao.TABLE_NAME, true, @@ -159,22 +98,3 @@ class DatabaseOpenHelper(context: Context) : ManagedSQLiteOpenHelper(context, "p */ val Context.database: DatabaseOpenHelper get() = DatabaseOpenHelper.getInstance(applicationContext) - -fun SQLiteStatement.bindStringOrNull(index: Int, value: String?) { - if (value != null) { - this.bindString(index, value) - } else { - this.bindNull(index) - } -} - -fun String.escapeQuery() = - this.replace("'", "''").replace("%", "\$%").replace("_", "\$_") - -fun StringBuilder.appendIfNotEmpty(string: String): StringBuilder{ - if (this.isNotEmpty()) { - this.append(string) - } - - return this -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureCancellationDao.kt b/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureCancellationDao.kt index 39221d6..b7762e1 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureCancellationDao.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureCancellationDao.kt @@ -1,24 +1,20 @@ package jp.kentan.studentportalplus.data.dao -import jp.kentan.studentportalplus.data.component.LectureAttendType -import jp.kentan.studentportalplus.data.component.LectureOrderType -import jp.kentan.studentportalplus.data.component.LectureQuery -import jp.kentan.studentportalplus.data.component.NotifyContent +import jp.kentan.studentportalplus.data.component.LectureAttend +import jp.kentan.studentportalplus.data.component.PortalContent import jp.kentan.studentportalplus.data.model.LectureCancellation import jp.kentan.studentportalplus.data.parser.LectureAttendParser import jp.kentan.studentportalplus.data.parser.LectureCancellationParser import jp.kentan.studentportalplus.util.JaroWinklerDistance -import jp.kentan.studentportalplus.util.toLong import org.jetbrains.anko.db.SqlOrderDirection import org.jetbrains.anko.db.delete import org.jetbrains.anko.db.select import org.jetbrains.anko.db.update - class LectureCancellationDao( private val database: DatabaseOpenHelper, - var myClassThreshold: Float -) { + var similarThreshold: Float +) : BaseDao() { companion object { const val TABLE_NAME = "lecture_cancel" @@ -32,106 +28,23 @@ class LectureCancellationDao( fun getAll(): List = database.use { val myClassList = select(MyClassDao.TABLE_NAME, "subject, user").parseList(LECTURE_ATTEND_PARSER) - val lectureCancelList = select(TABLE_NAME) - .orderBy("DATE(created_date)", SqlOrderDirection.DESC) + return@use select(TABLE_NAME) + .orderBy("created_date", SqlOrderDirection.DESC) .orderBy("subject") .parseList(PARSER) - - return@use lectureCancelList.map { - val subject = it.subject - var attend = LectureAttendType.NOT - - for (i in myClassList) { - if (i.first == subject) { - attend = i.second - break - } else if (STRING_DISTANCE.getDistance(i.first, subject) >= 0.8f) { - attend = LectureAttendType.SIMILAR - } - } - - it.copy(attend = attend) - } + .map { it.copy(attend = myClassList.calcLectureAttend(it.subject)) } } - fun get(id: Long): LectureCancellation? = database.use { - val myClassList = select(MyClassDao.TABLE_NAME, "subject, user").parseList(LECTURE_ATTEND_PARSER) - - val data = select(TABLE_NAME) - .whereArgs("_id=$id") - .limit(1) - .parseOpt(PARSER) ?: return@use null - - data.copy(attend = myClassList.analyzeAttendType(data.subject)) - } - - fun search(query: LectureQuery) = database.use { - val myClassList = select(MyClassDao.TABLE_NAME, "subject, user").parseList(LECTURE_ATTEND_PARSER) - - val where = StringBuilder() - - if (!query.isAttend) { - if (!query.isUnread && !query.hasRead) { - return@use emptyList() - } else if (!query.isUnread) { - where.append("read=1") - } else if (!query.hasRead) { - where.append("read=0") - } - } - - if (query.keywordList.isNotEmpty()) { - where.appendIfNotEmpty(" AND ") - where.append('(') - - // Subject - query.keywordList.forEach { where.append("subject LIKE '%$it%' AND ") } - where.delete(where.length-5, where.length) - - where.append(") OR (") - - // Instructor - query.keywordList.forEach { where.append("instructor LIKE '%$it%' AND ") } - where.delete(where.length-5, where.length) - - where.append(") ") - } - - val lectureInfoList = select(TABLE_NAME) - .whereArgs(where.toString()) - .orderBy("DATE(created_date)", SqlOrderDirection.DESC) - .orderBy("subject") - .parseList(PARSER) - - val result = lectureInfoList.mapNotNull { - val type = myClassList.analyzeAttendType(it.subject) - - if (query.isAttend && !type.isAttend()) { - if (!query.isUnread && !query.hasRead) { - return@mapNotNull null - } else if (!query.hasRead && it.isRead) { - return@mapNotNull null - } - - it.copy(attend = type) - } else if (!query.isAttend && type.isAttend()) { - null - } else { - it.copy(attend = type) - } - } - - return@use if (query.order == LectureOrderType.ATTEND_CLASS) { - result.sortedBy { !it.attend.isAttend() } - } else { - result - } + fun update(data: LectureCancellation): Int = database.use { + update(TABLE_NAME, "read" to data.isRead.toLong()) + .whereArgs("_id = ${data.id}") + .exec() } fun updateAll(list: List) = database.use { beginTransaction() - val notifyDataList = mutableListOf() + val updatedContentList = mutableListOf() var st = compileStatement("INSERT OR IGNORE INTO $TABLE_NAME VALUES(?,?,?,?,?,?,?,?,?,?,?,?);") @@ -142,17 +55,17 @@ class LectureCancellationDao( st.bindString(3, it.grade) st.bindString(4, it.subject) st.bindString(5, it.instructor) - st.bindString(6, DatabaseOpenHelper.toString(it.cancelDate)) + st.bindLong(6, it.cancelDate.time) st.bindString(7, it.week) st.bindString(8, it.period) st.bindString(9, it.detailText) st.bindString(10, it.detailHtml) - st.bindString(11, DatabaseOpenHelper.toString(it.createdDate)) + st.bindLong(11, it.createdDate.time) st.bindLong(12, it.isRead.toLong()) val id = st.executeInsert() if (id > 0) { - notifyDataList.add(NotifyContent(it.subject, it.detailText, id)) + updatedContentList.add(PortalContent(id, it.subject, it.detailText)) } st.clearBindings() } @@ -177,26 +90,20 @@ class LectureCancellationDao( setTransactionSuccessful() endTransaction() - return@use notifyDataList - } - - fun update(data: LectureCancellation): Int = database.use { - update(TABLE_NAME, "read" to data.isRead.toLong()) - .whereArgs("_id = ${data.id}") - .exec() + return@use updatedContentList } - private fun List>.analyzeAttendType(subject: String): LectureAttendType { - var type = LectureAttendType.NOT + private fun List>.calcLectureAttend(subject: String): LectureAttend { + // If match subject + firstOrNull { it.first == subject }?.run { + return second + } - forEach { - if (it.first == subject) { - return it.second - } else if (STRING_DISTANCE.getDistance(it.first, subject) >= myClassThreshold) { - type = LectureAttendType.SIMILAR - } + // If similar + if (any { STRING_DISTANCE.getDistance(it.first, subject) >= similarThreshold }) { + return LectureAttend.SIMILAR } - return type + return LectureAttend.NOT } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureInformationDao.kt b/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureInformationDao.kt index fba1aa2..4101504 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureInformationDao.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/dao/LectureInformationDao.kt @@ -1,24 +1,20 @@ package jp.kentan.studentportalplus.data.dao -import jp.kentan.studentportalplus.data.component.LectureAttendType -import jp.kentan.studentportalplus.data.component.LectureOrderType -import jp.kentan.studentportalplus.data.component.LectureQuery -import jp.kentan.studentportalplus.data.component.NotifyContent +import jp.kentan.studentportalplus.data.component.LectureAttend +import jp.kentan.studentportalplus.data.component.PortalContent import jp.kentan.studentportalplus.data.model.LectureInformation import jp.kentan.studentportalplus.data.parser.LectureAttendParser import jp.kentan.studentportalplus.data.parser.LectureInformationParser import jp.kentan.studentportalplus.util.JaroWinklerDistance -import jp.kentan.studentportalplus.util.toLong import org.jetbrains.anko.db.SqlOrderDirection import org.jetbrains.anko.db.delete import org.jetbrains.anko.db.select import org.jetbrains.anko.db.update - class LectureInformationDao( private val database: DatabaseOpenHelper, - var myClassThreshold: Float -) { + var similarThreshold: Float +) : BaseDao() { companion object { const val TABLE_NAME = "lecture_info" @@ -33,92 +29,22 @@ class LectureInformationDao( val myClassList = select(MyClassDao.TABLE_NAME, "subject, user").parseList(LECTURE_ATTEND_PARSER) select(TABLE_NAME) - .orderBy("DATE(updated_date)", SqlOrderDirection.DESC) + .orderBy("updated_date", SqlOrderDirection.DESC) .orderBy("subject") .parseList(PARSER) - .map { - it.copy(attend = myClassList.analyzeAttendType(it.subject)) - } - } - - fun get(id: Long): LectureInformation? = database.use { - val myClassList = select(MyClassDao.TABLE_NAME, "subject, user").parseList(LECTURE_ATTEND_PARSER) - - val data = select(TABLE_NAME) - .whereArgs("_id=$id") - .limit(1) - .parseOpt(PARSER) ?: return@use null - - data.copy(attend = myClassList.analyzeAttendType(data.subject)) + .map { it.copy(attend = myClassList.calcLectureAttend(it.subject)) } } - fun search(query: LectureQuery) = database.use { - val myClassList = select(MyClassDao.TABLE_NAME, "subject, user").parseList(LECTURE_ATTEND_PARSER) - - val where = StringBuilder() - - if (!query.isAttend) { - if (!query.isUnread && !query.hasRead) { - return@use emptyList() - } else if (!query.isUnread) { - where.append("read=1") - } else if (!query.hasRead) { - where.append("read=0") - } - } - - if (query.keywordList.isNotEmpty()) { - where.appendIfNotEmpty(" AND ") - where.append('(') - - // Subject - query.keywordList.forEach { where.append("subject LIKE '%$it%' AND ") } - where.delete(where.length-5, where.length) - - where.append(") OR (") - - // Instructor - query.keywordList.forEach { where.append("instructor LIKE '%$it%' AND ") } - where.delete(where.length-5, where.length) - - where.append(") ") - } - - val lectureInfoList = select(TABLE_NAME) - .whereArgs(where.toString()) - .orderBy("DATE(updated_date)", SqlOrderDirection.DESC) - .orderBy("subject") - .parseList(PARSER) - - val result = lectureInfoList.mapNotNull { - val type = myClassList.analyzeAttendType(it.subject) - - if (query.isAttend && !type.isAttend()) { - if (!query.isUnread && !query.hasRead) { - return@mapNotNull null - } else if (!query.hasRead && it.isRead) { - return@mapNotNull null - } - - it.copy(attend = type) - } else if (!query.isAttend && type.isAttend()) { - null - } else { - it.copy(attend = type) - } - } - - return@use if (query.order == LectureOrderType.ATTEND_CLASS) { - result.sortedBy { !it.attend.isAttend() } - } else { - result - } + fun update(data: LectureInformation): Int = database.use { + update(TABLE_NAME, "read" to data.isRead.toLong()) + .whereArgs("_id = ${data.id}") + .exec() } fun updateAll(list: List) = database.use { beginTransaction() - val notifyDataList = mutableListOf() + val updatedContentList = mutableListOf() var st = compileStatement("INSERT OR IGNORE INTO $TABLE_NAME VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?);") @@ -135,13 +61,13 @@ class LectureInformationDao( st.bindString(9, it.category) st.bindString(10, it.detailText) st.bindString(11, it.detailHtml) - st.bindString(12, DatabaseOpenHelper.toString(it.createdDate)) - st.bindString(13, DatabaseOpenHelper.toString(it.updatedDate)) + st.bindLong(12, it.createdDate.time) + st.bindLong(13, it.updatedDate.time) st.bindLong(14, it.isRead.toLong()) val id = st.executeInsert() if (id > 0) { - notifyDataList.add(NotifyContent(it.subject, it.detailText, id)) + updatedContentList.add(PortalContent(id, it.subject, it.detailText)) } st.clearBindings() } @@ -166,26 +92,20 @@ class LectureInformationDao( setTransactionSuccessful() endTransaction() - return@use notifyDataList + return@use updatedContentList } - fun update(data: LectureInformation): Int = database.use { - update(TABLE_NAME, "read" to data.isRead.toLong()) - .whereArgs("_id = ${data.id}") - .exec() - } - - private fun List>.analyzeAttendType(subject: String): LectureAttendType { - var type = LectureAttendType.NOT + private fun List>.calcLectureAttend(subject: String): LectureAttend { + // If match subject + firstOrNull { it.first == subject }?.run { + return second + } - forEach { - if (it.first == subject) { - return it.second - } else if (STRING_DISTANCE.getDistance(it.first, subject) >= myClassThreshold) { - type = LectureAttendType.SIMILAR - } + // If similar + if (any { STRING_DISTANCE.getDistance(it.first, subject) >= similarThreshold }) { + return LectureAttend.SIMILAR } - return type + return LectureAttend.NOT } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/dao/MyClassDao.kt b/app/src/main/java/jp/kentan/studentportalplus/data/dao/MyClassDao.kt index 70e2255..c410520 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/dao/MyClassDao.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/dao/MyClassDao.kt @@ -2,15 +2,15 @@ package jp.kentan.studentportalplus.data.dao import jp.kentan.studentportalplus.data.model.MyClass import jp.kentan.studentportalplus.data.parser.MyClassParser -import jp.kentan.studentportalplus.util.toLong import org.jetbrains.anko.db.* - -class MyClassDao(private val database: DatabaseOpenHelper) { +class MyClassDao( + private val database: DatabaseOpenHelper +) : BaseDao() { companion object { const val TABLE_NAME = "my_class" - private val PARSER = MyClassParser() + val PARSER = MyClassParser() } fun getAll(): List = database.use { @@ -30,11 +30,21 @@ class MyClassDao(private val database: DatabaseOpenHelper) { }) } - fun get(id: Long): MyClass? = database.use { - select(TABLE_NAME) - .whereArgs("_id=$id") - .limit(1) - .parseOpt(PARSER) + fun update(data: MyClass) = database.use { + update(TABLE_NAME, + "hash" to data.hash, + "week" to data.week.code, + "period" to data.period, + "schedule_code" to data.scheduleCode, + "credit" to data.credit, + "category" to data.category, + "subject" to data.subject, + "instructor" to data.instructor, + "user" to data.isUser, + "color" to data.color, + "location" to data.location) + .whereArgs("_id=${data.id}") + .exec() } fun updateAll(list: List) = database.use { @@ -77,24 +87,7 @@ class MyClassDao(private val database: DatabaseOpenHelper) { } } - fun update(data: MyClass) = database.use { - update(TABLE_NAME, - "hash" to data.hash, - "week" to data.week.code, - "period" to data.period, - "schedule_code" to data.scheduleCode, - "credit" to data.credit, - "category" to data.category, - "subject" to data.subject, - "instructor" to data.instructor, - "user" to data.isUser, - "color" to data.color, - "location" to data.location) - .whereArgs("_id=${data.id}") - .exec() - } - - fun add(list: List) = database.use { + fun insert(list: List) = database.use { var count = 0 transaction { diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/dao/NoticeDao.kt b/app/src/main/java/jp/kentan/studentportalplus/data/dao/NoticeDao.kt index c04bc65..a900a1d 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/dao/NoticeDao.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/dao/NoticeDao.kt @@ -1,20 +1,17 @@ package jp.kentan.studentportalplus.data.dao -import jp.kentan.studentportalplus.data.component.CreatedDateType -import jp.kentan.studentportalplus.data.component.NoticeQuery -import jp.kentan.studentportalplus.data.component.NotifyContent +import jp.kentan.studentportalplus.data.component.PortalContent import jp.kentan.studentportalplus.data.model.Notice import jp.kentan.studentportalplus.data.parser.NoticeParser -import jp.kentan.studentportalplus.util.toLong import org.jetbrains.anko.collections.forEachReversedByIndex import org.jetbrains.anko.db.SqlOrderDirection import org.jetbrains.anko.db.delete import org.jetbrains.anko.db.select import org.jetbrains.anko.db.update -import java.util.* - -class NoticeDao(private val database: DatabaseOpenHelper) { +class NoticeDao( + private val database: DatabaseOpenHelper +) : BaseDao() { companion object { const val TABLE_NAME = "notice" @@ -22,71 +19,22 @@ class NoticeDao(private val database: DatabaseOpenHelper) { } fun getAll(): List = database.use { - select(TABLE_NAME).orderBy("created_date", SqlOrderDirection.DESC).orderBy("_id", SqlOrderDirection.DESC).parseList(PARSER) - } - - fun get(id: Long): Notice? = database.use { - select(TABLE_NAME).whereArgs("_id=$id").limit(1).parseOpt(PARSER) - } - - fun search(query: NoticeQuery) = database.use { - val where = StringBuilder() - - if (query.isFavorite) { - if (!query.isUnread || !query.hasRead) { - when { - query.isUnread -> where.append("(read=0 OR favorite=1)") - query.hasRead -> where.append("(read=1 OR favorite=1)") - else -> where.append("favorite=1") - } - } - } else { - if (query.isUnread && query.hasRead) { - where.append("favorite=0") - } else if (query.isUnread) { - where.append("(read=0 AND favorite=0)") - } else if (query.hasRead) { - where.append("(read=1 AND favorite=0)") - } else { - where.append("(read=0 AND read=1 AND favorite=0)") - } - } - - if (query.type != CreatedDateType.ALL) { - where.appendIfNotEmpty(" AND ") - - val calendar = Calendar.getInstance() - when(query.type) { - CreatedDateType.WEEK -> { calendar.set(Calendar.DAY_OF_WEEK , calendar.firstDayOfWeek) } - CreatedDateType.MONTH -> { calendar.set(Calendar.DAY_OF_MONTH, 1) } - CreatedDateType.YEAR -> { calendar.set(Calendar.DAY_OF_YEAR , 1) } - else -> {} - } - - where.append("created_date>=DATE('${DatabaseOpenHelper.toString(calendar.time)}')") - } - - if (query.keywordList.isNotEmpty()) { - where.appendIfNotEmpty(" AND ") - - query.keywordList.forEach { - where.append("title LIKE '%${it.escapeQuery()}%' AND ") - } - - where.delete(where.length-5, where.length) - } - select(TABLE_NAME) - .whereArgs(where.toString()) .orderBy("created_date", SqlOrderDirection.DESC) .orderBy("_id", SqlOrderDirection.DESC) .parseList(PARSER) } + fun update(data: Notice): Int = database.use { + update(TABLE_NAME, "favorite" to data.isFavorite.toLong(), "read" to data.isRead.toLong()) + .whereArgs("_id = ${data.id}") + .exec() + } + fun updateAll(list: List) = database.use { beginTransaction() - val notifyDataList = mutableListOf() + val updatedContentList = mutableListOf() var st = compileStatement("INSERT OR IGNORE INTO $TABLE_NAME VALUES(?,?,?,?,?,?,?,?,?,?,?);") @@ -94,19 +42,19 @@ class NoticeDao(private val database: DatabaseOpenHelper) { list.forEachReversedByIndex { st.bindNull(1) st.bindLong(2, it.hash) - st.bindString(3, DatabaseOpenHelper.toString(it.createdDate)) + st.bindLong(3, it.createdDate.time) st.bindString(4, it.inCharge) st.bindString(5, it.category) st.bindString(6, it.title) st.bindStringOrNull(7, it.detailText) st.bindStringOrNull(8, it.detailHtml) st.bindStringOrNull(9, it.link) - st.bindLong(10, it.hasRead.toLong()) + st.bindLong(10, it.isRead.toLong()) st.bindLong(11, it.isFavorite.toLong()) val id = st.executeInsert() if (id > 0) { - notifyDataList.add(NotifyContent(it.title, it.detailText ?: it.link, id)) + updatedContentList.add(PortalContent(id, it.title, it.detailText ?: it.link)) } st.clearBindings() } @@ -131,12 +79,6 @@ class NoticeDao(private val database: DatabaseOpenHelper) { setTransactionSuccessful() endTransaction() - return@use notifyDataList - } - - fun update(data: Notice): Int = database.use { - update(TABLE_NAME, "favorite" to data.isFavorite.toLong(), "read" to data.hasRead.toLong()) - .whereArgs("_id = ${data.id}") - .exec() + return@use updatedContentList } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/model/Lecture.kt b/app/src/main/java/jp/kentan/studentportalplus/data/model/Lecture.kt index 7bef63f..6916e9c 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/model/Lecture.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/model/Lecture.kt @@ -1,15 +1,17 @@ package jp.kentan.studentportalplus.data.model -import jp.kentan.studentportalplus.data.component.LectureAttendType -import java.util.Date +import jp.kentan.studentportalplus.data.component.LectureAttend +import java.util.* abstract class Lecture( - open val subject : String, - open val instructor: String, - open val week : String, - open val period : String, - open val detail : String, - open val date : Date, - open val isRead : Boolean, - open val attend : LectureAttendType -) \ No newline at end of file + open val detail: String, + open val date: Date +) { + abstract val id: Long + abstract val subject: String + abstract val instructor: String + abstract val week: String + abstract val period: String + abstract val isRead: Boolean + abstract val attend: LectureAttend +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureCancellation.kt b/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureCancellation.kt index 8cf0c86..bd94923 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureCancellation.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureCancellation.kt @@ -1,32 +1,33 @@ package jp.kentan.studentportalplus.data.model -import android.support.v7.util.DiffUtil -import jp.kentan.studentportalplus.data.component.LectureAttendType +import androidx.recyclerview.widget.DiffUtil +import jp.kentan.studentportalplus.data.component.LectureAttend +import jp.kentan.studentportalplus.util.Murmur3 import java.util.* data class LectureCancellation( - val id : Long = -1, - val hash : Long, // Murmur3.hash64(grade + subject + instructor + cancelDate + week + period + detailHtml + createdDate) - val grade : String, // 学部名など - override val subject : String, // 授業科目名 - override val instructor : String, // 担当教員名 - val cancelDate : Date, // 休講日 - override val week : String, // 曜日 - override val period : String, // 時限 - val detailText : String, // 概要(Text) - val detailHtml : String, // 概要(Html) - val createdDate: Date, // 初回掲示日 - override val isRead : Boolean = false, - override val attend : LectureAttendType = LectureAttendType.UNKNOWN -) : Lecture(subject, instructor, week, period, detailText, createdDate, isRead, attend) { + override val id: Long = -1, + val grade: String, // 学部名など + override val subject: String, // 授業科目名 + override val instructor: String, // 担当教員名 + val cancelDate: Date, // 休講日 + override val week: String, // 曜日 + override val period: String, // 時限 + val detailText: String, // 概要(Text) + val detailHtml: String, // 概要(Html) + val createdDate: Date, // 初回掲示日 + override val isRead: Boolean = false, + override val attend: LectureAttend = LectureAttend.UNKNOWN, + val hash: Long = Murmur3.hash64("$grade$subject$instructor$cancelDate$week$period$detailHtml$createdDate") +) : Lecture(detailText, createdDate) { companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: LectureCancellation?, newItem: LectureCancellation?): Boolean { - return oldItem?.id == newItem?.id + override fun areItemsTheSame(oldItem: LectureCancellation, newItem: LectureCancellation): Boolean { + return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: LectureCancellation?, newItem: LectureCancellation?): Boolean { + override fun areContentsTheSame(oldItem: LectureCancellation, newItem: LectureCancellation): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureInformation.kt b/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureInformation.kt index 6f092c7..3fc0350 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureInformation.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/model/LectureInformation.kt @@ -1,34 +1,34 @@ package jp.kentan.studentportalplus.data.model -import android.support.v7.util.DiffUtil -import jp.kentan.studentportalplus.data.component.LectureAttendType +import androidx.recyclerview.widget.DiffUtil +import jp.kentan.studentportalplus.data.component.LectureAttend +import jp.kentan.studentportalplus.util.Murmur3 import java.util.* - data class LectureInformation( - val id : Long = -1, - val hash : Long, // Murmur3.hash64(grade + semester + subject + instructor + week + period + category + detailHtml + createdDate + updatedDate) - val grade : String, // 学部名など - val semester : String, // 学期 - override val subject : String, // 授業科目名 - override val instructor : String, // 担当教員名 - override val week : String, // 曜日 - override val period : String, // 時限 - val category : String, // 分類 - val detailText : String, // 連絡事項(Text) - val detailHtml : String, // 連絡事項(Html) - val createdDate: Date, // 初回掲示日 - val updatedDate: Date, // 最終更新日 - override val isRead : Boolean = false, - override val attend : LectureAttendType = LectureAttendType.UNKNOWN -) : Lecture(subject, instructor, week, period, detailText, updatedDate, isRead, attend) { + override val id: Long = -1, + val grade: String, // 学部名など + val semester: String, // 学期 + override val subject: String, // 授業科目名 + override val instructor: String, // 担当教員名 + override val week: String, // 曜日 + override val period: String, // 時限 + val category: String, // 分類 + val detailText: String, // 連絡事項(Text) + val detailHtml: String, // 連絡事項(Html) + val createdDate: Date, // 初回掲示日 + val updatedDate: Date, // 最終更新日 + override val isRead: Boolean = false, + override val attend: LectureAttend = LectureAttend.UNKNOWN, + val hash: Long = Murmur3.hash64("$grade$semester$subject$instructor$week$period$category$detailHtml$createdDate$updatedDate") +) : Lecture(detailText, updatedDate) { companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: LectureInformation?, newItem: LectureInformation?): Boolean { - return oldItem?.id == newItem?.id + override fun areItemsTheSame(oldItem: LectureInformation, newItem: LectureInformation): Boolean { + return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: LectureInformation?, newItem: LectureInformation?): Boolean { + override fun areContentsTheSame(oldItem: LectureInformation, newItem: LectureInformation): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/model/MyClass.kt b/app/src/main/java/jp/kentan/studentportalplus/data/model/MyClass.kt index 9d9828d..350b51c 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/model/MyClass.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/model/MyClass.kt @@ -1,35 +1,34 @@ package jp.kentan.studentportalplus.data.model -import android.support.v7.util.DiffUtil +import androidx.recyclerview.widget.DiffUtil import jp.kentan.studentportalplus.data.component.ClassColor -import jp.kentan.studentportalplus.data.component.ClassWeekType +import jp.kentan.studentportalplus.data.component.ClassWeek +import jp.kentan.studentportalplus.util.Murmur3 data class MyClass( - val id : Long = -1, - val hash : Long, // Murmur3.hash64(week + period + scheduleCode + credit + category + subject + instructor + isUser) - val week : ClassWeekType, // 週 - val period : Int, // 時限 - val scheduleCode: String, // 時間割コード - val credit : Int, // 単位 - val category : String, // カテゴリ - val subject : String, // 授業科目名 - val instructor : String, // 担当教員名 - val isUser : Boolean, // true: LectureAttendType.USER, false: LectureAttendType.PORTAL - val color : Int = ClassColor.DEFAULT, - val location : String? = null + val id: Long = -1, + val week: ClassWeek, // 週 + val period: Int, // 時限 + val scheduleCode: String, // 時間割コード + val credit: Int, // 単位 + val category: String, // カテゴリ + val subject: String, // 授業科目名 + val instructor: String, // 担当教員名 + val isUser: Boolean, // true: LectureAttend.USER, false: LectureAttend.PORTAL + val color: Int = ClassColor.DEFAULT, + val location: String? = null, + val hash: Long = Murmur3.hash64("$week$period$scheduleCode$credit$category$subject$instructor$isUser") ) { companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MyClass?, newItem: MyClass?): Boolean { - return oldItem?.id == newItem?.id + override fun areItemsTheSame(oldItem: MyClass, newItem: MyClass): Boolean { + return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: MyClass?, newItem: MyClass?): Boolean { - return oldItem == newItem + override fun areContentsTheSame(oldItem: MyClass, newItem: MyClass): Boolean { + return oldItem == newItem } } } - - fun match(period: Int, week: ClassWeekType) = (period == this.period) && (week == this.week) } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/model/Notice.kt b/app/src/main/java/jp/kentan/studentportalplus/data/model/Notice.kt index 620b7bd..8292efb 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/model/Notice.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/model/Notice.kt @@ -1,29 +1,29 @@ package jp.kentan.studentportalplus.data.model -import android.support.v7.util.DiffUtil +import androidx.recyclerview.widget.DiffUtil +import jp.kentan.studentportalplus.util.Murmur3 import java.util.* - data class Notice( - val id : Long = -1, - val hash : Long, // Murmur3.hash64(createdDateStr + inCharge + category + title + detailHtml + link) - val createdDate: Date, // 掲示日 - val inCharge : String, // 発信課 - val category : String, // カテゴリ - val title : String, // お知らせ > タイトル - val detailText : String?, // お知らせ > 詳細(Text) - val detailHtml : String?, // お知らせ > 詳細(Html) - val link : String?, // お知らせ > リンク - val hasRead : Boolean = false, - val isFavorite : Boolean = false + val id: Long = -1, + val createdDate: Date, // 掲示日 + val inCharge: String, // 発信課 + val category: String, // カテゴリ + val title: String, // お知らせ > タイトル + val detailText: String?, // お知らせ > 詳細(Text) + val detailHtml: String?, // お知らせ > 詳細(Html) + val link: String?, // お知らせ > リンク + val isRead: Boolean = false, + val isFavorite: Boolean = false, + val hash: Long = Murmur3.hash64("$createdDate$inCharge$category$title$detailHtml$link") ) { companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Notice?, newItem: Notice?): Boolean { - return oldItem?.id == newItem?.id + override fun areItemsTheSame(oldItem: Notice, newItem: Notice): Boolean { + return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: Notice?, newItem: Notice?): Boolean { + override fun areContentsTheSame(oldItem: Notice, newItem: Notice): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/model/User.kt b/app/src/main/java/jp/kentan/studentportalplus/data/model/User.kt new file mode 100644 index 0000000..9b7fc63 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/model/User.kt @@ -0,0 +1,6 @@ +package jp.kentan.studentportalplus.data.model + +data class User( + val name: String, + val username: String +) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/parser/BaseParser.kt b/app/src/main/java/jp/kentan/studentportalplus/data/parser/BaseParser.kt index 6ea59b9..de8ce01 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/parser/BaseParser.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/parser/BaseParser.kt @@ -1,10 +1,3 @@ package jp.kentan.studentportalplus.data.parser -import org.jsoup.nodes.Document - - -abstract class BaseParser { - - @Throws(Exception::class) - abstract fun parse(document: Document): List -} \ No newline at end of file +abstract class BaseParser \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureAttendParser.kt b/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureAttendParser.kt index 6fdaa61..deb357d 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureAttendParser.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureAttendParser.kt @@ -1,16 +1,13 @@ package jp.kentan.studentportalplus.data.parser -import jp.kentan.studentportalplus.data.component.LectureAttendType +import jp.kentan.studentportalplus.data.component.LectureAttend import org.jetbrains.anko.db.RowParser -class LectureAttendParser : RowParser> { +class LectureAttendParser : RowParser> { - override fun parseRow(columns: Array): Pair { - return if (columns[1] is Long) { - Pair(columns[0] as String, if ((columns[1] as Long) == 1L) LectureAttendType.USER else LectureAttendType.PORTAL) - } else { - Pair(columns[0] as String, LectureAttendType.valueOf(columns[1] as String)) - } + override fun parseRow(columns: Array): Pair { + return Pair(columns[0] as String, if ((columns[1] as Long) == 1L) LectureAttend.USER else LectureAttend.PORTAL) } + } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureCancellationParser.kt b/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureCancellationParser.kt index 33ba973..9106bbc 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureCancellationParser.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureCancellationParser.kt @@ -1,65 +1,52 @@ package jp.kentan.studentportalplus.data.parser import android.util.Log -import jp.kentan.studentportalplus.data.dao.DatabaseOpenHelper import jp.kentan.studentportalplus.data.model.LectureCancellation -import jp.kentan.studentportalplus.util.Murmur3 -import jp.kentan.studentportalplus.util.toShortString +import jp.kentan.studentportalplus.util.formatYearMonthDay import org.jetbrains.anko.db.RowParser import org.jsoup.nodes.Document import java.text.SimpleDateFormat import java.util.* - class LectureCancellationParser : BaseParser(), RowParser { private companion object { - const val TAG = "LectureCancelParser" val DATE_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.JAPAN) } @Throws(Exception::class) - override fun parse(document: Document): List { - val resultList = mutableListOf() - - val trElements = document.select("tr[class~=gen_tbl1_(odd|even)]") - - trElements.forEach{ + fun parse(document: Document): List { + val resultList = document.select("tr[class~=gen_tbl1_(odd|even)]").mapNotNull { val tdElements = it.select("td") if (tdElements.size < 9) { - return@forEach + return@mapNotNull null } - val grade = tdElements[1].text() - val subject = tdElements[2].text() - val instructor = tdElements[3].text() - val cancelDateStr = tdElements[4].text() - val cancelDate = cancelDateStr.toDate() - val week = tdElements[5].text() - val period = tdElements[6].text() - val detailText = cancelDate.toShortString() + " — " + instructor - val detailHtml = tdElements[7].html() + val grade = tdElements[1].text() + val subject = tdElements[2].text() + val instructor = tdElements[3].text() + val cancelDateStr = tdElements[4].text() + val cancelDate = cancelDateStr.toDate() + val week = tdElements[5].text() + val period = tdElements[6].text() + val detailText = cancelDate.formatYearMonthDay() + " — " + instructor + val detailHtml = tdElements[7].html() val createdDateStr = tdElements[8].text() - val hashStr = grade + subject + instructor + cancelDateStr + week + period + detailHtml + createdDateStr - - resultList.add( - LectureCancellation( - hash = Murmur3.hash64(hashStr.toByteArray()), - grade = grade, - subject = subject, - instructor = instructor, - cancelDate = cancelDate, - week = week, - period = period, - detailText = detailText, - detailHtml = detailHtml, - createdDate = createdDateStr.toDate() - ) + return@mapNotNull LectureCancellation( + grade = grade, + subject = subject, + instructor = instructor, + cancelDate = cancelDate, + week = week, + period = period, + detailText = detailText, + detailHtml = detailHtml, + createdDate = createdDateStr.toDate() ) } - Log.d(TAG, "Parsed ${resultList.size} LectureCancellationData") + Log.d("LectureCancelParser", "Parsed ${resultList.size} LectureCancellation") return resultList } @@ -70,12 +57,12 @@ class LectureCancellationParser : BaseParser(), RowParser { grade = columns[2] as String, subject = columns[3] as String, instructor = columns[4] as String, - cancelDate = DatabaseOpenHelper.toDate(columns[5] as String), + cancelDate = Date(columns[5] as Long), week = columns[6] as String, period = columns[7] as String, detailText = columns[8] as String, detailHtml = columns[9] as String, - createdDate = DatabaseOpenHelper.toDate(columns[10] as String), + createdDate = Date(columns[10] as Long), isRead = (columns[11] as Long) == 1L ) diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureInformationParser.kt b/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureInformationParser.kt index 3287c46..7f30c37 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureInformationParser.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/parser/LectureInformationParser.kt @@ -2,8 +2,6 @@ package jp.kentan.studentportalplus.data.parser import android.util.Log import jp.kentan.studentportalplus.data.model.LectureInformation -import jp.kentan.studentportalplus.data.dao.DatabaseOpenHelper -import jp.kentan.studentportalplus.util.Murmur3 import org.jetbrains.anko.db.RowParser import org.jsoup.nodes.Document import java.text.SimpleDateFormat @@ -13,55 +11,45 @@ import java.util.* class LectureInformationParser : BaseParser(), RowParser { private companion object { - const val TAG = "LectureInfoParser" val DATE_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.JAPAN) } @Throws(Exception::class) - override fun parse(document: Document): List { - val resultList = mutableListOf() - - val trElements = document.select("tr[class~=gen_tbl1_(odd|even)]") - - trElements.forEach{ + fun parse(document: Document): List { + val resultList = document.select("tr[class~=gen_tbl1_(odd|even)]").mapNotNull { val tdElements = it.select("td") if (tdElements.size < 11) { - return@forEach + return@mapNotNull null } - val grade = tdElements[1].text() - val semester = tdElements[2].text() - val subject = tdElements[3].text() - val instructor = tdElements[4].text() - val week = tdElements[5].text() - val period = tdElements[6].text() - val category = tdElements[7].text() - val detailText = tdElements[8].text() - val detailHtml = tdElements[8].html() + val grade = tdElements[1].text() + val semester = tdElements[2].text() + val subject = tdElements[3].text() + val instructor = tdElements[4].text() + val week = tdElements[5].text() + val period = tdElements[6].text() + val category = tdElements[7].text() + val detailText = tdElements[8].text() + val detailHtml = tdElements[8].html() val createdDateStr = tdElements[9].text() val updatedDateStr = tdElements[10].text() - val hashStr = grade + semester + subject + instructor + week + period + category + detailHtml + createdDateStr + updatedDateStr - - resultList.add( - LectureInformation( - hash = Murmur3.hash64(hashStr.toByteArray()), - grade = grade, - semester = semester, - subject = subject, - instructor = instructor, - week = week, - period = period, - category = category, - detailText = detailText, - detailHtml = detailHtml, - createdDate = createdDateStr.toDate(), - updatedDate = updatedDateStr.toDate() - ) + return@mapNotNull LectureInformation( + grade = grade, + semester = semester, + subject = subject, + instructor = instructor, + week = week, + period = period, + category = category, + detailText = detailText, + detailHtml = detailHtml, + createdDate = createdDateStr.toDate(), + updatedDate = updatedDateStr.toDate() ) } - Log.d(TAG, "Parsed ${resultList.size} LectureInformation") + Log.d("LectureInfoParser", "Parsed ${resultList.size} LectureInformation") return resultList } @@ -78,8 +66,8 @@ class LectureInformationParser : BaseParser(), RowParser { category = columns[8] as String, detailText = columns[9] as String, detailHtml = columns[10] as String, - createdDate = DatabaseOpenHelper.toDate(columns[11] as String), - updatedDate = DatabaseOpenHelper.toDate(columns[12] as String), + createdDate = Date(columns[11] as Long), + updatedDate = Date(columns[12] as Long), isRead = (columns[13] as Long) == 1L ) diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/parser/MyClassParser.kt b/app/src/main/java/jp/kentan/studentportalplus/data/parser/MyClassParser.kt index 57f9095..88b2244 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/parser/MyClassParser.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/parser/MyClassParser.kt @@ -1,32 +1,23 @@ package jp.kentan.studentportalplus.data.parser import android.util.Log -import jp.kentan.studentportalplus.data.component.ClassWeekType -import jp.kentan.studentportalplus.data.component.LectureAttendType +import jp.kentan.studentportalplus.data.component.ClassWeek +import jp.kentan.studentportalplus.data.component.LectureAttend import jp.kentan.studentportalplus.data.model.Lecture import jp.kentan.studentportalplus.data.model.MyClass -import jp.kentan.studentportalplus.util.Murmur3 -import jp.kentan.studentportalplus.util.toIntOrNull import org.jetbrains.anko.db.RowParser import org.jsoup.nodes.Document - class MyClassParser : BaseParser(), RowParser { - private companion object { - const val TAG = "MyClassParser" - } - @Throws(Exception::class) - override fun parse(document: Document): List { + fun parse(document: Document): List { val resultList = mutableListOf() - val trElements = document.select("table#enroll_data_tbl tr[class~=gen_tbl1_(odd|even)]") - var weekCode = 0 // Timetable (Per week) - trElements.forEach{ + document.select("table#enroll_data_tbl tr[class~=gen_tbl1_(odd|even)]").forEach{ val tdElements = it.select("td") if (tdElements.size != 7) { throw ParseException("Unknown attend_course layout") @@ -44,18 +35,16 @@ class MyClassParser : BaseParser(), RowParser { return@forEachIndexed } - val week = ClassWeekType.valueOf(weekCode) - val period = index + 1 + val week = ClassWeek.valueOf(weekCode) + val period = index + 1 val scheduleCode = pElement.selectFirst("a").text() - resultList.add(toMyClass(week, period, scheduleCode, lineList)) + resultList.add(createMyClass(week, period, scheduleCode, lineList)) } } // Intensive part - val td2Elements = document.select("table#enroll_data_tbl2 tr td") - - td2Elements.forEach { + document.select("table#enroll_data_tbl2 tr td").forEach { val lineList = it.html().split("
", limit = 5) if (lineList.size < 5) { @@ -64,41 +53,25 @@ class MyClassParser : BaseParser(), RowParser { val scheduleCode = it.selectFirst("a").text() - resultList.add(toMyClass(ClassWeekType.INTENSIVE, 0, scheduleCode, lineList)) + resultList.add(createMyClass(ClassWeek.INTENSIVE, 0, scheduleCode, lineList)) } - Log.d(TAG, "Parsed ${resultList.size} MyClass") + Log.d("MyClassParser", "Parsed ${resultList.size} MyClass") return resultList } - override fun parseRow(columns: Array) = MyClass( - id = columns[0] as Long, - hash = columns[1] as Long, - week = ClassWeekType.valueOf((columns[2] as Long).toInt()), - period = (columns[3] as Long).toInt(), - scheduleCode = columns[4] as String, - credit = (columns[5] as Long).toInt(), - category = columns[6] as String, - subject = columns[7] as String, - instructor = columns[8] as String, - isUser = (columns[9] as Long) == 1L, - color = (columns[10] as Long).toInt(), - location = columns[11] as String? - ) - @Throws(Exception::class) fun parse(data: Lecture): List { - val list = mutableListOf() - - val week = ClassWeekType.valueOfSimilar(data.week) + val week = ClassWeek.valueOfSimilar(data.week) val periodList = mutableListOf() + if (data.period.length >= 3) { - val first = data.period[0] - val last = data.period[data.period.lastIndex] + val first = data.period.find { it.isDigit() } ?: throw ParseException("First period not found.") + val last = data.period.findLast { it.isDigit() } ?: throw ParseException("Last period not found.") - val periodFirst = first.toIntOrNull() ?: throw ParseException("Invalid period: $first") - val periodLast = last.toIntOrNull() ?: throw ParseException("Invalid period: $last") + val periodFirst = first.toString().toInt() + val periodLast = last.toString().toInt() if (periodFirst > periodLast) { throw ParseException("Invalid period range: $first to $last") @@ -108,55 +81,58 @@ class MyClassParser : BaseParser(), RowParser { periodList.add(p) } } else { - val period = data.period.toIntOrNull() ?: 0 + val period = data.period.find { it.isDigit() }?.toString()?.toInt() ?: 0 periodList.add(period) } - if (data.attend != LectureAttendType.PORTAL && data.attend != LectureAttendType.USER) { - throw ParseException("Invalid lecture attend type: ${data.attend.name}") + if (data.attend != LectureAttend.PORTAL && data.attend != LectureAttend.USER) { + throw ParseException("Not supported LectureAttend: ${data.attend.name}") } - periodList.forEach { - val isUser = (data.attend == LectureAttendType.USER) - val hashStr = week.name + it + 0 + data.subject + data.instructor + isUser - - list.add( - MyClass( - hash = Murmur3.hash64(hashStr.toByteArray()), - week = week, - period = it, - scheduleCode = "", - credit = 0, - category = "", - subject = data.subject, - instructor = data.instructor, - isUser = isUser - ) + return periodList.map { period -> + MyClass( + week = week, + period = period, + scheduleCode = "", + credit = 0, + category = "", + subject = data.subject, + instructor = data.instructor, + isUser = data.attend == LectureAttend.USER ) } - - return list } - private fun toMyClass(week: ClassWeekType, period: Int, scheduleCode: String, lineList: List): MyClass { - val credit = lineList[1].filter { it.isDigit() }.toIntOrNull() ?: throw ParseException("Invalid credit: ${lineList[1]}") - val category = lineList[2].trim() - val subject = lineList[3].trim() - val instructor = lineList[4].trim() - val isUser = false + override fun parseRow(columns: Array) = MyClass( + id = columns[0] as Long, + hash = columns[1] as Long, + week = ClassWeek.valueOf((columns[2] as Long).toInt()), + period = (columns[3] as Long).toInt(), + scheduleCode = columns[4] as String, + credit = (columns[5] as Long).toInt(), + category = columns[6] as String, + subject = columns[7] as String, + instructor = columns[8] as String, + isUser = (columns[9] as Long) == 1L, + color = (columns[10] as Long).toInt(), + location = columns[11] as String? + ) - val hashStr = week.name + period + scheduleCode + credit + category + subject + instructor + isUser + private fun createMyClass(week: ClassWeek, period: Int, scheduleCode: String, lineList: List): MyClass { + val credit = lineList[1] + .filter { it.isDigit() } + .toIntOrNull() + ?: throw ParseException("Invalid credit: ${lineList[1]}") return MyClass( - hash = Murmur3.hash64(hashStr.toByteArray()), week = week, period = period, scheduleCode = scheduleCode, credit = credit, - category = category, - subject = subject, - instructor = instructor, - isUser = isUser + category = lineList[2].trim(), + subject = lineList[3].trim(), + instructor = lineList[4].trim(), + isUser = false ) } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/parser/NoticeParser.kt b/app/src/main/java/jp/kentan/studentportalplus/data/parser/NoticeParser.kt index 6ba4db7..4a89cd2 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/parser/NoticeParser.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/parser/NoticeParser.kt @@ -2,15 +2,12 @@ package jp.kentan.studentportalplus.data.parser import android.util.Log import jp.kentan.studentportalplus.data.model.Notice -import jp.kentan.studentportalplus.data.dao.DatabaseOpenHelper -import jp.kentan.studentportalplus.util.Murmur3 import org.jetbrains.anko.db.RowParser import org.jsoup.nodes.Document import java.text.SimpleDateFormat import java.util.* - -class NoticeParser : BaseParser(), RowParser { +class NoticeParser : RowParser { private companion object { const val TAG = "NoticeParser" @@ -18,15 +15,11 @@ class NoticeParser : BaseParser(), RowParser { } @Throws(Exception::class) - override fun parse(document: Document): List { - val resultList = mutableListOf() - - val dlElements = document.select("dl") - - dlElements.forEach{ + fun parse(document: Document): List { + val resultList = document.select("dl").mapNotNull { val ddElements = it.select("dd") if (ddElements.size < 4) { - return@forEach + return@mapNotNull null } val createdDateStr = ddElements[0].text() @@ -34,7 +27,7 @@ class NoticeParser : BaseParser(), RowParser { val category = ddElements[2].text() val noticeElement = ddElements[3] - val hrefElement = noticeElement.selectFirst("a") + val hrefElement = noticeElement.selectFirst("a") val title = if (hrefElement != null) { hrefElement.text() @@ -48,19 +41,14 @@ class NoticeParser : BaseParser(), RowParser { val detailHtml = infoElement?.html() val link = hrefElement?.attr("href") - val hashStr = createdDateStr + inCharge + category + title + detailHtml + link - - resultList.add( - Notice( - hash = Murmur3.hash64(hashStr.toByteArray()), - createdDate = createdDateStr.toDate(), - inCharge = inCharge, - category = category, - title = title, - detailText = detailText, - detailHtml = detailHtml, - link = link - ) + return@mapNotNull Notice( + createdDate = createdDateStr.toDate(), + inCharge = inCharge, + category = category, + title = title, + detailText = detailText, + detailHtml = detailHtml, + link = link ) } @@ -72,14 +60,14 @@ class NoticeParser : BaseParser(), RowParser { override fun parseRow(columns: Array) = Notice( id = columns[0] as Long, hash = columns[1] as Long, - createdDate = DatabaseOpenHelper.toDate(columns[2] as String), + createdDate = Date(columns[2] as Long), inCharge = columns[3] as String, category = columns[4] as String, title = columns[5] as String, detailText = columns[6] as String?, detailHtml = columns[7] as String?, link = columns[8] as String?, - hasRead = (columns[9] as Long) == 1L, + isRead = (columns[9] as Long) == 1L, isFavorite = (columns[10] as Long) == 1L ) diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/parser/ParseException.kt b/app/src/main/java/jp/kentan/studentportalplus/data/parser/ParseException.kt index 064c55d..08424e8 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/parser/ParseException.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/parser/ParseException.kt @@ -1,4 +1,3 @@ package jp.kentan.studentportalplus.data.parser - class ParseException(message: String) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethClient.kt b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethClient.kt index d2f4859..6c6cc39 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethClient.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethClient.kt @@ -3,26 +3,20 @@ package jp.kentan.studentportalplus.data.shibboleth import android.content.Context import android.os.Build import android.util.Log -import androidx.core.content.edit import com.franmontiel.persistentcookiejar.PersistentCookieJar import com.franmontiel.persistentcookiejar.cache.SetCookieCache import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor -import jp.kentan.studentportalplus.data.component.ShibbolethData +import jp.kentan.studentportalplus.util.updateShibbolethLastLoginDate import okhttp3.* -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.doFromSdk import org.jsoup.Jsoup import org.jsoup.nodes.Document import java.security.KeyStore -import java.text.SimpleDateFormat -import java.util.* import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager - class ShibbolethClient( private val context: Context, private val shibbolethDataProvider: ShibbolethDataProvider @@ -43,12 +37,10 @@ class ShibbolethClient( val SESSION_FORM_PARAMS = listOf("shib_idp_ls_supported", "_eventId_proceed") val LOGIN_FORM_PARAMS = listOf(INPUT_NAME_USERNAME, INPUT_NAME_PASSWORD) - - val LOGIN_DATE_FORMAT = SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss", Locale.JAPAN) } private val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context)) - private val httpClient: OkHttpClient + private val client: OkHttpClient init { val builder = OkHttpClient.Builder() @@ -61,13 +53,13 @@ class ShibbolethClient( enableTls12(builder) - val spec = setupConnectionSpec() + val spec = createConnectionSpec() builder.connectionSpecs(spec) - httpClient = builder.build() + client = builder.build() } - private fun setupConnectionSpec(): List { + private fun createConnectionSpec(): List { val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.TLS_1_2) .allEnabledCipherSuites() @@ -81,7 +73,7 @@ class ShibbolethClient( } private fun enableTls12(builder: OkHttpClient.Builder): OkHttpClient.Builder { - doFromSdk(Build.VERSION_CODES.KITKAT_WATCH) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { return builder } @@ -141,8 +133,8 @@ class ShibbolethClient( .url(url) .build() - val response = httpClient.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") - val body = response.body() ?: throw ShibbolethException("Empty response body") + val response = client.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") + val body = response.body() ?: throw ShibbolethException("Empty response body") if (!response.isSuccessful) { throw ShibbolethException("Error HTTP status code: ${response.code()} ${response.message()}") @@ -157,9 +149,7 @@ class ShibbolethClient( document = passLoginPage(document, username, password) document = passSamlResponsePage(document) - context.defaultSharedPreferences.edit { - putString("shibboleth_last_login_date", LOGIN_DATE_FORMAT.format(Date())) - } + context.updateShibbolethLastLoginDate() } Log.d(TAG, "Fetched: ${document.title()}") @@ -186,7 +176,7 @@ class ShibbolethClient( .post(requestBody) .build() - val response = httpClient.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") + val response = client.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") val body = response.body() ?: throw ShibbolethException("Empty response body") if (!response.isSuccessful) { @@ -220,7 +210,7 @@ class ShibbolethClient( .post(requestBody) .build() - val response = httpClient.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") + val response = client.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") val body = response.body() ?: throw ShibbolethException("Empty response body") if (!response.isSuccessful) { @@ -258,7 +248,7 @@ class ShibbolethClient( .post(requestBody) .build() - val response = httpClient.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") + val response = client.newCall(request)?.execute() ?: throw ShibbolethException("Empty response") val body = response.body() ?: throw ShibbolethException("Empty response body") if (!response.isSuccessful) { diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethData.kt b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethData.kt new file mode 100644 index 0000000..e36a0a8 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethData.kt @@ -0,0 +1,6 @@ +package jp.kentan.studentportalplus.data.shibboleth + +data class ShibbolethData( + val username: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethDataProvider.kt b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethDataProvider.kt index d28f55f..5d7c503 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethDataProvider.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethDataProvider.kt @@ -2,9 +2,6 @@ package jp.kentan.studentportalplus.data.shibboleth -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MediatorLiveData -import android.arch.lifecycle.MutableLiveData import android.content.Context import android.content.SharedPreferences import android.os.Build @@ -14,19 +11,23 @@ import android.security.keystore.KeyProperties import android.util.Base64 import android.util.Log import androidx.core.content.edit -import jp.kentan.studentportalplus.data.component.ShibbolethData -import org.jetbrains.anko.coroutines.experimental.bg +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import jp.kentan.studentportalplus.data.model.User +import kotlinx.coroutines.experimental.launch import java.math.BigInteger -import java.nio.charset.StandardCharsets +import java.nio.charset.Charset import java.security.KeyPairGenerator import java.security.KeyStore -import java.security.interfaces.RSAPublicKey import java.util.* import javax.crypto.Cipher import javax.security.auth.x500.X500Principal -class ShibbolethDataProvider(private val context: Context) { +class ShibbolethDataProvider( + private val context: Context +) { private companion object { const val TAG = "ShibbolethDataProvider" @@ -38,10 +39,12 @@ class ShibbolethDataProvider(private val context: Context) { const val KEY_NAME = "name" const val KEY_USERNAME = "username" const val KEY_PASSWORD = "password" + + val UTF_8: Charset = Charset.forName("UTF-8") } private val preferences: SharedPreferences = context.getSharedPreferences("shibboleth", Context.MODE_PRIVATE) - private val userLiveData = MutableLiveData>() // (name, username) + private val userLiveData = MutableLiveData() private lateinit var keyStore: KeyStore init { @@ -94,20 +97,19 @@ class ShibbolethDataProvider(private val context: Context) { generator.generateKeyPair() } - private fun encryptString(text: String, enableRetry: Boolean = true): String? { + private fun encrypt(text: String, enableRetry: Boolean = true): String? { if (text.isEmpty()) { Log.e(TAG, "Empty decrypt text") return null } try { - val privateKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry - val publicKey = privateKeyEntry.certificate.publicKey as RSAPublicKey + val publicKey = keyStore.getCertificate(KEY_ALIAS).publicKey val cipher = Cipher.getInstance(CIPHER_TYPE, CIPHER_PROVIDER) cipher.init(Cipher.ENCRYPT_MODE, publicKey) - val bytes = cipher.doFinal(text.toByteArray(StandardCharsets.UTF_8)) + val bytes = cipher.doFinal(text.toByteArray(UTF_8)) return Base64.encodeToString(bytes, Base64.DEFAULT) } catch (e: Exception) { @@ -117,7 +119,7 @@ class ShibbolethDataProvider(private val context: Context) { keyStore.deleteEntry(KEY_ALIAS) createKeyIfNeed(keyStore) - return encryptString(text, false) + return encrypt(text, false) } else { Log.e(TAG, "Failed to encrypt", e) } @@ -126,21 +128,21 @@ class ShibbolethDataProvider(private val context: Context) { return null } - private fun decryptString(text: String?): String? { + private fun decrypt(text: String?): String? { if (text.isNullOrEmpty()) { Log.e(TAG, "Empty encrypt text") return null } try { - val privateKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry + val privateKey = keyStore.getKey(KEY_ALIAS, null) val cipher = Cipher.getInstance(CIPHER_TYPE) - cipher.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) + cipher.init(Cipher.DECRYPT_MODE, privateKey) val bytes = cipher.doFinal(Base64.decode(text, Base64.DEFAULT)) - return String(bytes, StandardCharsets.UTF_8) + return String(bytes, UTF_8) } catch (e: Exception) { Log.e(TAG, "Failed to decrypt", e) } @@ -150,40 +152,40 @@ class ShibbolethDataProvider(private val context: Context) { fun save(name: String, data: ShibbolethData) { preferences.edit { - putString(KEY_NAME , encryptString(name)) - putString(KEY_USERNAME, encryptString(data.username)) - putString(KEY_PASSWORD, encryptString(data.password)) + putString(KEY_NAME , encrypt(name)) + putString(KEY_USERNAME, encrypt(data.username)) + putString(KEY_PASSWORD, encrypt(data.password)) } // may be call in background thread - userLiveData.postValue(Pair(name, data.username)) + userLiveData.postValue(User(name, data.username)) } - fun getUsername() = decryptString(preferences.getString(KEY_USERNAME, null)) + fun getUsername() = decrypt(preferences.getString(KEY_USERNAME, null)) - fun getUser(): LiveData> { - val result = MediatorLiveData>() + fun getUser(): LiveData { + val result = MediatorLiveData() result.addSource(userLiveData) { result.value = it } - bg { + launch { result.postValue( - Pair( - decryptString(preferences.getString(KEY_NAME, null)) ?: "", - decryptString(preferences.getString(KEY_USERNAME, null)) ?: "") + User( + name = decrypt(preferences.getString(KEY_NAME, null)) ?: "", + username = decrypt(preferences.getString(KEY_USERNAME, null)) ?: "") ) } return result } - @Throws(Exception::class) + @Throws(ShibbolethAuthenticationException::class) fun get() = ShibbolethData( - username = decryptString(preferences.getString(KEY_USERNAME, null)) + username = decrypt(preferences.getString(KEY_USERNAME, null)) ?: throw ShibbolethAuthenticationException("ユーザー名の復号に失敗しました"), - password = decryptString(preferences.getString(KEY_PASSWORD, null)) + password = decrypt(preferences.getString(KEY_PASSWORD, null)) ?: throw ShibbolethAuthenticationException("パスワードの復号に失敗しました") ) diff --git a/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethException.kt b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethException.kt index 32f1175..4782461 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethException.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/data/shibboleth/ShibbolethException.kt @@ -1,6 +1,5 @@ package jp.kentan.studentportalplus.data.shibboleth +open class ShibbolethException(override val message: String) : Exception(message) -class ShibbolethException(override val message: String) : Exception(message) - -class ShibbolethAuthenticationException(override val message: String) : Exception(message) \ No newline at end of file +class ShibbolethAuthenticationException(override val message: String) : ShibbolethException(message) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/di/ActivityModule.kt b/app/src/main/java/jp/kentan/studentportalplus/di/ActivityModule.kt index 7e2c619..f47ff39 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/di/ActivityModule.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/di/ActivityModule.kt @@ -2,8 +2,13 @@ package jp.kentan.studentportalplus.di import dagger.Module import dagger.android.ContributesAndroidInjector -import jp.kentan.studentportalplus.ui.* +import jp.kentan.studentportalplus.ui.lecturecancel.detail.LectureCancelDetailActivity +import jp.kentan.studentportalplus.ui.lectureinfo.detail.LectureInfoDetailActivity +import jp.kentan.studentportalplus.ui.login.LoginActivity +import jp.kentan.studentportalplus.ui.main.MainActivity +import jp.kentan.studentportalplus.ui.myclass.detail.MyClassDetailActivity import jp.kentan.studentportalplus.ui.myclass.edit.MyClassEditActivity +import jp.kentan.studentportalplus.ui.notice.detail.NoticeDetailActivity @Module abstract class ActivityModule { @@ -15,17 +20,18 @@ abstract class ActivityModule { abstract fun contributeLoginActivity(): LoginActivity @ContributesAndroidInjector - abstract fun contributeLectureInformationActivity(): LectureInformationActivity + abstract fun contributeLectureInfoDetailActivity(): LectureInfoDetailActivity @ContributesAndroidInjector - abstract fun contributeLectureCancellationActivity(): LectureCancellationActivity + abstract fun contributeLectureCancelDetailActivity(): LectureCancelDetailActivity @ContributesAndroidInjector - abstract fun contributeNoticeActivity(): NoticeActivity + abstract fun contributeMyClassDetailActivity(): MyClassDetailActivity @ContributesAndroidInjector - abstract fun contributeMyClassActivity(): MyClassActivity + abstract fun contributeMyClassEditActivity(): MyClassEditActivity @ContributesAndroidInjector - abstract fun contributeMyClassEditActivity(): MyClassEditActivity + abstract fun contributeNoticeDetailActivity(): NoticeDetailActivity + } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/di/AppModule.kt b/app/src/main/java/jp/kentan/studentportalplus/di/AppModule.kt index 5bc988c..1ff88a1 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/di/AppModule.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/di/AppModule.kt @@ -5,33 +5,33 @@ import android.content.Context import dagger.Module import dagger.Provides import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.UserRepository import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import org.jetbrains.anko.defaultSharedPreferences +import jp.kentan.studentportalplus.ui.ViewModelFactory import javax.inject.Singleton - @Module -class AppModule(app: Application) { +class AppModule( + private val app: Application +) { - private val context = app.applicationContext - private val shibbolethDataProvider = ShibbolethDataProvider(context) - private val portalRepository = PortalRepository(app.applicationContext, shibbolethDataProvider) + private val shibbolethDataProvider = ShibbolethDataProvider(app) + private val portalRepository = PortalRepository(app, shibbolethDataProvider) + private val userRepository = UserRepository(shibbolethDataProvider) @Provides @Singleton - fun provideContext(): Context = context + fun provideContext(): Context = app @Provides @Singleton - fun provideShibbolethDataProvider() = shibbolethDataProvider + fun providePortalRepository(): PortalRepository = portalRepository @Provides @Singleton - fun providePortalRepository() = portalRepository + fun provideUserRepository(): UserRepository = userRepository @Provides @Singleton - fun provideViewModelFactory() = ViewModelFactory(context.defaultSharedPreferences, portalRepository, shibbolethDataProvider) - + fun provideViewModelFactory() = ViewModelFactory(app, portalRepository, userRepository, shibbolethDataProvider) } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/di/FragmentModule.kt b/app/src/main/java/jp/kentan/studentportalplus/di/FragmentModule.kt index 7fb531e..84fa66f 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/di/FragmentModule.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/di/FragmentModule.kt @@ -2,8 +2,12 @@ package jp.kentan.studentportalplus.di import dagger.Module import dagger.android.ContributesAndroidInjector -import jp.kentan.studentportalplus.ui.SettingsActivity -import jp.kentan.studentportalplus.ui.fragment.* +import jp.kentan.studentportalplus.ui.dashboard.DashboardFragment +import jp.kentan.studentportalplus.ui.lecturecancel.LectureCancelFragment +import jp.kentan.studentportalplus.ui.lectureinfo.LectureInfoFragment +import jp.kentan.studentportalplus.ui.notice.NoticeFragment +import jp.kentan.studentportalplus.ui.setting.GeneralPreferenceFragment +import jp.kentan.studentportalplus.ui.timetable.TimetableFragment @Module abstract class FragmentModule { @@ -15,14 +19,15 @@ abstract class FragmentModule { abstract fun contributeTimetableFragment(): TimetableFragment @ContributesAndroidInjector - abstract fun contributeLectureInformationFragment(): LectureInformationFragment + abstract fun contributeLectureInfoFragment(): LectureInfoFragment @ContributesAndroidInjector - abstract fun contributeLectureCancellationFragment(): LectureCancellationFragment + abstract fun contributeLectureCancelFragment(): LectureCancelFragment @ContributesAndroidInjector abstract fun contributeNoticeFragment(): NoticeFragment @ContributesAndroidInjector - abstract fun contributePreferencesFragment(): SettingsActivity.PreferencesFragment + abstract fun contributeGeneralPreferenceFragment(): GeneralPreferenceFragment + } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/notification/DeviceStatusReceiver.kt b/app/src/main/java/jp/kentan/studentportalplus/notification/DeviceStatusReceiver.kt index 847bb25..cc3ba08 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/notification/DeviceStatusReceiver.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/notification/DeviceStatusReceiver.kt @@ -7,7 +7,7 @@ import android.content.Intent class DeviceStatusReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) { - SyncScheduler.scheduleIfNeed(context) + SyncScheduler(context).scheduleIfNeeded() } } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/notification/NotificationController.kt b/app/src/main/java/jp/kentan/studentportalplus/notification/NotificationController.kt index f31909c..162d95b 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/notification/NotificationController.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/notification/NotificationController.kt @@ -4,34 +4,37 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context +import android.content.ContextWrapper +import android.content.Intent import android.graphics.BitmapFactory -import android.graphics.Typeface.BOLD +import android.graphics.Typeface import android.os.Build -import android.support.v4.app.NotificationCompat -import android.support.v4.app.NotificationManagerCompat -import android.support.v4.content.ContextCompat import android.text.Spannable import android.text.style.StyleSpan +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import androidx.core.text.set import androidx.core.text.toSpannable import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.NotifyContent -import jp.kentan.studentportalplus.data.component.PortalDataType -import jp.kentan.studentportalplus.ui.* -import jp.kentan.studentportalplus.util.enabledNotificationLed -import jp.kentan.studentportalplus.util.enabledNotificationVibration +import jp.kentan.studentportalplus.data.component.PortalContent +import jp.kentan.studentportalplus.data.component.PortalData +import jp.kentan.studentportalplus.ui.lecturecancel.detail.LectureCancelDetailActivity +import jp.kentan.studentportalplus.ui.lectureinfo.detail.LectureInfoDetailActivity +import jp.kentan.studentportalplus.ui.login.LoginActivity +import jp.kentan.studentportalplus.ui.main.FragmentType +import jp.kentan.studentportalplus.ui.main.MainActivity +import jp.kentan.studentportalplus.ui.notice.detail.NoticeDetailActivity import jp.kentan.studentportalplus.util.getNotificationId +import jp.kentan.studentportalplus.util.isEnabledNotificationLed +import jp.kentan.studentportalplus.util.isEnabledNotificationVibration import jp.kentan.studentportalplus.util.setNotificationId -import org.jetbrains.anko.clearTop +import org.jetbrains.anko.clearTask import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.intentFor import org.jetbrains.anko.newTask -class NotificationController( - private val context: Context -) { +class NotificationController(context: Context): ContextWrapper(context) { companion object { const val NEWLY_CHANNEL_ID = "0_newly_channel" //新着通知 @@ -42,42 +45,44 @@ class NotificationController( private const val ERROR_NOTIFICATION_ID = -1 private const val INBOX_LINE_LIMIT = 4 - private val CAN_USE_VECTOR_DRAWABLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - private val CAN_USE_NOTIFICATION_SUMMARY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - private val CAN_USE_NOTIFICATION_CHANNEL = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + private val CAN_USE_VECTOR_DRAWABLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + private val CAN_USE_SUMMARY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N private val VIBRATION_PATTERN = longArrayOf(0, 300, 300, 300) private val SMALL_APP_ICON = if (CAN_USE_VECTOR_DRAWABLE) R.drawable.ic_menu_dashboard else R.mipmap.ic_notification_app private val SMALL_ICON_MAP = if (CAN_USE_VECTOR_DRAWABLE) { mapOf( - PortalDataType.LECTURE_INFORMATION to R.drawable.ic_menu_lecture_info, - PortalDataType.LECTURE_CANCELLATION to R.drawable.ic_menu_lecture_cancel, - PortalDataType.NOTICE to R.drawable.ic_menu_notice + PortalData.LECTURE_INFO to R.drawable.ic_menu_lecture_info, + PortalData.LECTURE_CANCEL to R.drawable.ic_menu_lecture_cancel, + PortalData.NOTICE to R.drawable.ic_menu_notice ) } else { mapOf( - PortalDataType.LECTURE_INFORMATION to R.mipmap.ic_notification_lecture_info, - PortalDataType.LECTURE_CANCELLATION to R.mipmap.ic_notification_lecture_cancel, - PortalDataType.NOTICE to R.mipmap.ic_notification_notice + PortalData.LECTURE_INFO to R.mipmap.ic_notification_lecture_info, + PortalData.LECTURE_CANCEL to R.mipmap.ic_notification_lecture_cancel, + PortalData.NOTICE to R.mipmap.ic_notification_notice ) } + fun setupChannel(context: Context) { - if (!CAN_USE_NOTIFICATION_CHANNEL) { return } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return } val color = ContextCompat.getColor(context, R.color.colorAccent) - val newlyChannel = NotificationChannel(NEWLY_CHANNEL_ID, "新着通知", NotificationManager.IMPORTANCE_DEFAULT) - newlyChannel.enableLights(true) - newlyChannel.lightColor = color - newlyChannel.vibrationPattern = VIBRATION_PATTERN - newlyChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + val newlyChannel = NotificationChannel(NEWLY_CHANNEL_ID, context.getString(R.string.name_newly_channel), NotificationManager.IMPORTANCE_DEFAULT).apply { + enableLights(true) + lightColor = color + vibrationPattern = VIBRATION_PATTERN + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + } - val appChannel = NotificationChannel(APP_CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW) - appChannel.enableLights(true) - appChannel.lightColor = color - appChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + val appChannel = NotificationChannel(APP_CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW).apply { + enableLights(true) + lightColor = color + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + } val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannels(listOf(newlyChannel, appChannel)) @@ -86,8 +91,8 @@ class NotificationController( private val notificationManager = NotificationManagerCompat.from(context) - private val enabledVibration = context.defaultSharedPreferences.enabledNotificationVibration() - private val enabledLed = context.defaultSharedPreferences.enabledNotificationLed() + private val isEnabledVibration = context.defaultSharedPreferences.isEnabledNotificationVibration() + private val isEnabledLed = context.defaultSharedPreferences.isEnabledNotificationLed() private val accentColor = ContextCompat.getColor(context, R.color.colorAccent) private val largeIcon by lazy(LazyThreadSafetyMode.NONE) { BitmapFactory.decodeResource(context.resources, R.mipmap.ic_notification_large) } @@ -99,25 +104,27 @@ class NotificationController( setupChannel(context) } - fun notify(type: PortalDataType, contentList: List) { + fun notify(type: PortalData, contentList: List) { if (contentList.isEmpty()) { return } val smallIcon = SMALL_ICON_MAP[type] ?: SMALL_APP_ICON - var id = context.defaultSharedPreferences.getNotificationId() + val name = getString(type.nameResId) + var id = defaultSharedPreferences.getNotificationId() - if (CAN_USE_NOTIFICATION_SUMMARY) { + if (CAN_USE_SUMMARY) { createSummaryNotification() + val builder = NotificationCompat.Builder(applicationContext, NEWLY_CHANNEL_ID) + contentList.forEach { - val builder = NotificationCompat.Builder(context, NEWLY_CHANNEL_ID) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(smallIcon) .setGroup(GROUP_KEY) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setGroupSummary(false) - .setSubText(type.displayName) + .setSubText(name) .setContentTitle(it.title) .setContentText(it.text) .setContentIntent(it.createIntent(type, id)) @@ -128,9 +135,9 @@ class NotificationController( if (++id >= Int.MAX_VALUE) { id = 1 } } } else { - val title = if (contentList.size > 1) "${contentList.size}件の${type.displayName}" else type.displayName + val title = if (contentList.size > 1) getString(R.string.title_content_inbox, contentList.size, name) else name - val builder = NotificationCompat.Builder(context, NEWLY_CHANNEL_ID) + val builder = NotificationCompat.Builder(applicationContext, NEWLY_CHANNEL_ID) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setPriority(NotificationCompat.PRIORITY_HIGH) .setLargeIcon(largeIcon) @@ -143,9 +150,9 @@ class NotificationController( .setAutoCancel(true) if (isFirstNotify) { - builder.setVibrate(if (enabledVibration) VIBRATION_PATTERN else longArrayOf(0)) + builder.setVibrate(if (isEnabledVibration) VIBRATION_PATTERN else longArrayOf(0)) - if (enabledLed) { + if (isEnabledLed) { builder.setLights(accentColor, 1000, 2000) } } @@ -156,32 +163,36 @@ class NotificationController( } isFirstNotify = false - context.defaultSharedPreferences.setNotificationId(id) + defaultSharedPreferences.setNotificationId(id) } fun notifyError(message: String, isRequireLogin: Boolean = false) { - val activity = if (isRequireLogin) { - LoginActivity.createIntent(context, shouldLaunchMainActivity = true) + val intent = if (isRequireLogin) { + LoginActivity.createIntent(this, true) } else { - context.intentFor() + MainActivity.createIntent(this) } - val intent = PendingIntent.getActivity(context, ERROR_NOTIFICATION_ID, activity, FLAG_UPDATE_CURRENT) - val builder = NotificationCompat.Builder(context, APP_CHANNEL_ID) + val pendingIntent = PendingIntent.getActivity(this, ERROR_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) + + val builder = NotificationCompat.Builder(applicationContext, APP_CHANNEL_ID) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_ERROR) .setPriority(NotificationCompat.PRIORITY_HIGH) - .setColor(ContextCompat.getColor(context, R.color.red_600)) + .setColor(ContextCompat.getColor(this, R.color.red_600)) .setSmallIcon(SMALL_APP_ICON) - .setSubText("同期失敗") - .setContentTitle(if (isRequireLogin) "再ログインが必要です" else "エラー") + .setSubText(getString(R.string.sub_text_sync_error)) + .setContentTitle(getString(if (isRequireLogin) R.string.title_require_login else R.string.title_error)) .setContentText(message) - .setContentIntent(intent) + .setContentIntent(pendingIntent) .setAutoCancel(true) if (CAN_USE_VECTOR_DRAWABLE) { - val retryService = PendingIntent.getService(context, ERROR_NOTIFICATION_ID, context.intentFor(), FLAG_UPDATE_CURRENT) - val retryAction = NotificationCompat.Action.Builder(R.drawable.ic_refresh, "再試行", retryService).build() + val retryService = PendingIntent.getService(this, + ERROR_NOTIFICATION_ID, + Intent(this, RetryActionService::class.java), + PendingIntent.FLAG_UPDATE_CURRENT) + val retryAction = NotificationCompat.Action.Builder(R.drawable.ic_retry, getString(R.string.name_retry), retryService).build() builder.addAction(retryAction) } @@ -194,55 +205,57 @@ class NotificationController( } private fun createSummaryNotification() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !isFirstNotify) { + if (!CAN_USE_SUMMARY || !isFirstNotify) { return } - val summary = NotificationCompat.Builder(context, NEWLY_CHANNEL_ID) + val builder = NotificationCompat.Builder(applicationContext, NEWLY_CHANNEL_ID) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(SMALL_APP_ICON) .setColor(accentColor) .setGroup(GROUP_KEY) .setGroupSummary(true) - .setVibrate(if (enabledVibration) VIBRATION_PATTERN else longArrayOf(0)) - .setLights(accentColor, 1000, 2000) + .setVibrate(if (isEnabledVibration) VIBRATION_PATTERN else longArrayOf(0)) .setOnlyAlertOnce(true) .setAutoCancel(true) - if (enabledLed) { - summary.setLights(accentColor, 1000, 2000) + if (isEnabledLed) { + builder.setLights(accentColor, 1000, 2000) } - notificationManager.notify(SUMMARY_NOTIFICATION_ID, summary.build()) + notificationManager.notify(SUMMARY_NOTIFICATION_ID, builder.build()) } - private fun NotifyContent.createIntent(type: PortalDataType, code: Int): PendingIntent? { + private fun PortalContent.createIntent(type: PortalData, code: Int): PendingIntent? { + val context = this@NotificationController + val intent = when (type) { - PortalDataType.LECTURE_INFORMATION -> context.intentFor("id" to id) - PortalDataType.LECTURE_CANCELLATION -> context.intentFor("id" to id) - PortalDataType.NOTICE -> context.intentFor("id" to id) + PortalData.LECTURE_INFO -> LectureInfoDetailActivity.createIntent(context, id) + PortalData.LECTURE_CANCEL -> LectureCancelDetailActivity.createIntent(context, id) + PortalData.NOTICE -> NoticeDetailActivity.createIntent(context, id) else -> return null } - return PendingIntent.getActivity(context, code, intent.clearTop().newTask(), FLAG_UPDATE_CURRENT) + return PendingIntent.getActivity(context, code, intent.clearTask().newTask(), PendingIntent.FLAG_UPDATE_CURRENT) } - private fun PortalDataType.createIntent(id: Int): PendingIntent { - val fragmentType = when (this) { - PortalDataType.NOTICE -> MainActivity.FragmentType.NOTICE - PortalDataType.LECTURE_INFORMATION -> MainActivity.FragmentType.LECTURE_INFO - PortalDataType.LECTURE_CANCELLATION -> MainActivity.FragmentType.LECTURE_CANCEL - PortalDataType.MY_CLASS -> MainActivity.FragmentType.DASHBOARD // not support - } + private fun PortalData.createIntent(id: Int): PendingIntent { + val context = this@NotificationController - val intent = context.intentFor(MainActivity.FRAGMENT_TYPE to fragmentType.name).clearTop().newTask() + val fragment = when (this) { + PortalData.NOTICE -> FragmentType.NOTICE + PortalData.LECTURE_INFO -> FragmentType.LECTURE_INFO + PortalData.LECTURE_CANCEL -> FragmentType.LECTURE_CANCEL + PortalData.MY_CLASS -> FragmentType.DASHBOARD // not support + } - return PendingIntent.getActivity(context, id, intent, FLAG_UPDATE_CURRENT) + val intent = MainActivity.createIntent(context, fragment = fragment) + return PendingIntent.getActivity(context, id, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - private fun List.createInboxStyle(type: PortalDataType): NotificationCompat.InboxStyle { + private fun List.createInboxStyle(type: PortalData): NotificationCompat.InboxStyle { val inboxStyle = NotificationCompat.InboxStyle() - .setBigContentTitle(type.displayName) + .setBigContentTitle(getString(type.nameResId)) take(INBOX_LINE_LIMIT).forEach { inboxStyle.addLine(it.toInboxStyleText()) @@ -250,15 +263,15 @@ class NotificationController( val moreContentSize = size - INBOX_LINE_LIMIT if (moreContentSize > 0) { - inboxStyle.setSummaryText("他${moreContentSize}件") + inboxStyle.setSummaryText(getString(R.string.summary_text_inbox, moreContentSize)) } return inboxStyle } - private fun NotifyContent.toInboxStyleText(): Spannable { + private fun PortalContent.toInboxStyleText(): Spannable { val text = "$title $text".toSpannable() - text[0..title.length] = StyleSpan(BOLD) + text[0..title.length] = StyleSpan(Typeface.BOLD) return text } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/notification/NotificationType.kt b/app/src/main/java/jp/kentan/studentportalplus/notification/NotificationType.kt new file mode 100644 index 0000000..7c3c317 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/notification/NotificationType.kt @@ -0,0 +1,5 @@ +package jp.kentan.studentportalplus.notification + +enum class NotificationType { + ALL, ATTEND, NOT; +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/notification/RetryActionService.kt b/app/src/main/java/jp/kentan/studentportalplus/notification/RetryActionService.kt index d322a34..4fe4e8d 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/notification/RetryActionService.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/notification/RetryActionService.kt @@ -3,6 +3,7 @@ package jp.kentan.studentportalplus.notification import android.app.Service import android.content.Intent import android.os.IBinder +import android.util.Log import androidx.work.Data import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager @@ -16,7 +17,12 @@ class RetryActionService : Service() { .setInputData(Data.Builder().putBoolean(SyncWorker.IGNORE_MIDNIGHT, true).build()) .build() - WorkManager.getInstance()?.enqueue(syncWorkRequest) + try { + WorkManager.getInstance() + .enqueue(syncWorkRequest) + } catch (e: IllegalStateException) { + Log.e("RetryActionService", "Failed to enqueue a SyncWorker", e) + } return START_NOT_STICKY } diff --git a/app/src/main/java/jp/kentan/studentportalplus/notification/SyncScheduler.kt b/app/src/main/java/jp/kentan/studentportalplus/notification/SyncScheduler.kt index 805abd0..c3c9ced 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/notification/SyncScheduler.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/notification/SyncScheduler.kt @@ -3,55 +3,59 @@ package jp.kentan.studentportalplus.notification import android.content.Context import android.util.Log import androidx.work.* -import jp.kentan.studentportalplus.util.enabledSync import jp.kentan.studentportalplus.util.getSyncIntervalMinutes +import jp.kentan.studentportalplus.util.isEnabledSync import org.jetbrains.anko.defaultSharedPreferences import java.util.concurrent.TimeUnit +class SyncScheduler( + private val context: Context +) { -class SyncScheduler { companion object { private const val TAG = "SyncScheduler" + } - fun scheduleIfNeed(context: Context) { - if (context.defaultSharedPreferences.enabledSync()) { - enqueueUniquePeriodicWork(context, ExistingPeriodicWorkPolicy.KEEP) - } + fun scheduleIfNeeded() { + if (context.defaultSharedPreferences.isEnabledSync()) { + enqueueUniquePeriodicWork(ExistingPeriodicWorkPolicy.KEEP) } + } + + fun schedule() { + enqueueUniquePeriodicWork(ExistingPeriodicWorkPolicy.REPLACE) + } - fun schedule(context: Context) { - enqueueUniquePeriodicWork(context, ExistingPeriodicWorkPolicy.REPLACE) + fun cancel() { + try { + WorkManager.getInstance() + .cancelUniqueWork(SyncWorker.NAME) + } catch (e: IllegalStateException) { + Log.e(TAG, "Failed to cancel SyncWorker", e) + return } - private fun enqueueUniquePeriodicWork(context: Context, workPolicy: ExistingPeriodicWorkPolicy) { - val intervalMinutes = context.defaultSharedPreferences.getSyncIntervalMinutes() + Log.d(TAG, "Cancelled a unique SyncWorker") + } - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() + private fun enqueueUniquePeriodicWork(workPolicy: ExistingPeriodicWorkPolicy) { + val intervalMinutes = context.defaultSharedPreferences.getSyncIntervalMinutes() - val syncWorkRequest = PeriodicWorkRequestBuilder(intervalMinutes, TimeUnit.MINUTES, intervalMinutes / 2, TimeUnit.MINUTES) - .setConstraints(constraints) - .build() + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() - try { - val workManager = WorkManager.getInstance() - workManager.enqueueUniquePeriodicWork(SyncWorker.NAME, workPolicy, syncWorkRequest) - } catch (e: IllegalStateException) { - Log.e(TAG, "Failed to enqueue SyncWorker", e) - } - } + val syncWorkRequest = PeriodicWorkRequestBuilder(intervalMinutes, TimeUnit.MINUTES, intervalMinutes / 2, TimeUnit.MINUTES) + .setConstraints(constraints) + .build() - fun cancel() { - try { - val workManager = WorkManager.getInstance() - workManager.cancelUniqueWork(SyncWorker.NAME) - } catch (e: IllegalStateException) { - Log.e(TAG, "Failed to cancel SyncWorker", e) - return - } + try { + WorkManager.getInstance() + .enqueueUniquePeriodicWork(SyncWorker.NAME, workPolicy, syncWorkRequest) - Log.d(TAG, "Cancelled SyncWorker") + Log.d(TAG, "Enqueued a unique SyncWorker") + } catch (e: IllegalStateException) { + Log.e(TAG, "Failed to enqueue SyncWorker", e) } } } \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/notification/SyncWorker.kt b/app/src/main/java/jp/kentan/studentportalplus/notification/SyncWorker.kt index eeede30..264bf87 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/notification/SyncWorker.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/notification/SyncWorker.kt @@ -1,40 +1,39 @@ package jp.kentan.studentportalplus.notification +import android.content.Context import android.util.Log import androidx.work.Worker +import androidx.work.WorkerParameters import jp.kentan.studentportalplus.StudentPortalPlus import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.NotifyContent -import jp.kentan.studentportalplus.data.component.NotifyType -import jp.kentan.studentportalplus.data.component.PortalDataType -import jp.kentan.studentportalplus.data.component.PortalDataType.* +import jp.kentan.studentportalplus.data.component.PortalContent +import jp.kentan.studentportalplus.data.component.PortalData import jp.kentan.studentportalplus.data.shibboleth.ShibbolethAuthenticationException import jp.kentan.studentportalplus.util.JaroWinklerDistance -import jp.kentan.studentportalplus.util.enabledDetailError -import jp.kentan.studentportalplus.util.getMyClassThreshold +import jp.kentan.studentportalplus.util.getNotificationType +import jp.kentan.studentportalplus.util.getSimilarSubjectThresholdFloat +import jp.kentan.studentportalplus.util.isEnabledDetailError +import kotlinx.coroutines.experimental.runBlocking import org.jetbrains.anko.defaultSharedPreferences import java.io.PrintWriter import java.io.StringWriter import java.util.* import javax.inject.Inject -class SyncWorker : Worker() { +class SyncWorker(context : Context, params : WorkerParameters) : Worker(context, params) { companion object { const val NAME = "sync_worker" - const val IGNORE_MIDNIGHT = "ignore_midnight" private const val TAG = "SyncWorker" - private val STRING_DISTANCE = JaroWinklerDistance() - - private val JST = TimeZone.getTimeZone("Asia/Tokyo") } @Inject lateinit var repository: PortalRepository - private val preferences by lazy { applicationContext.defaultSharedPreferences } + private val preferences = context.defaultSharedPreferences + private val stringDistance = JaroWinklerDistance() override fun doWork(): Result { if (isInMidnight() && !inputData.getBoolean(IGNORE_MIDNIGHT, false)) { @@ -44,31 +43,32 @@ class SyncWorker : Worker() { (applicationContext as StudentPortalPlus).component.inject(this) - val notification = NotificationController(applicationContext) - notification.cancelErrorNotification() + val controller = NotificationController(applicationContext) + controller.cancelErrorNotification() // Sync try { - val newDataMap = repository.sync() + val updatedContentsMap = runBlocking { repository.sync().await() } val subjectList = repository.getMyClassSubjectList() - val threshold = preferences.getMyClassThreshold() + val threshold = preferences.getSimilarSubjectThresholdFloat() - val lectureInfoList = newDataMap.getBy(LECTURE_INFORMATION, subjectList, threshold) - val lectureCancelList = newDataMap.getBy(LECTURE_CANCELLATION, subjectList, threshold) - val noticeList = newDataMap.getBy(NOTICE) - - notification.notify(LECTURE_INFORMATION, lectureInfoList) - notification.notify(LECTURE_CANCELLATION, lectureCancelList) - notification.notify(NOTICE, noticeList) + val lectureInfoList = updatedContentsMap.getBy(PortalData.LECTURE_INFO, subjectList, threshold) + val lectureCancelList = updatedContentsMap.getBy(PortalData.LECTURE_CANCEL, subjectList, threshold) + val noticeList = updatedContentsMap.getBy(PortalData.NOTICE) + controller.apply { + notify(PortalData.LECTURE_INFO, lectureInfoList) + notify(PortalData.LECTURE_CANCEL, lectureCancelList) + notify(PortalData.NOTICE, noticeList) + } } catch (e: ShibbolethAuthenticationException) { - notification.notifyError(e.message, true) + controller.notifyError(e.message, true) } catch (e: Exception) { Log.e(TAG, "Failed to sync", e) - if (preferences.enabledDetailError()) { - notification.notifyError(e.stackTraceToString()) + if (preferences.isEnabledDetailError()) { + controller.notifyError(e.stackTraceToString()) } } @@ -76,21 +76,22 @@ class SyncWorker : Worker() { } private fun isInMidnight(): Boolean { - return Calendar.getInstance(JST).get(Calendar.HOUR_OF_DAY) !in 5..22 + val timeZone = TimeZone.getTimeZone("Asia/Tokyo") + return Calendar.getInstance(timeZone).get(Calendar.HOUR_OF_DAY) !in 5..22 } - private fun Map>.getBy(type: PortalDataType, subjects: List = emptyList(), threshold: Float = 0f): List { + private fun Map>.getBy(type: PortalData, subjects: List = emptyList(), threshold: Float = 0f): List { val list = this[type] ?: return emptyList() - return when (NotifyType.getBy(preferences, type.notifyTypeKey)) { - NotifyType.ALL -> list - NotifyType.ATTEND -> { - list.filter { - val subject = it.title - return@filter subjects.any { it == subject || STRING_DISTANCE.getDistance(it, subject) >= threshold } + return when (preferences.getNotificationType(type)) { + NotificationType.ALL -> list + NotificationType.ATTEND -> { + list.filter { content -> + val subject = content.title + return@filter subjects.any { it == subject || stringDistance.getDistance(it, subject) >= threshold } } } - NotifyType.NOT -> emptyList() + NotificationType.NOT -> emptyList() } } diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/LectureCancellationActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/LectureCancellationActivity.kt deleted file mode 100644 index 71eec52..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/LectureCancellationActivity.kt +++ /dev/null @@ -1,161 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.v7.app.AlertDialog -import android.support.v7.app.AppCompatActivity -import android.view.Menu -import android.view.MenuItem -import android.view.animation.AnticipateOvershootInterpolator -import dagger.android.AndroidInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.LectureAttendType -import jp.kentan.studentportalplus.data.model.LectureCancellation -import jp.kentan.studentportalplus.ui.viewmodel.LectureCancellationViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.util.CustomTransformationMethod -import jp.kentan.studentportalplus.util.htmlToSpanned -import jp.kentan.studentportalplus.util.indefiniteSnackbar -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.android.synthetic.main.activity_lecture_cancellation.* -import kotlinx.android.synthetic.main.content_lecture_cancellation.* -import org.jetbrains.anko.design.snackbar -import org.jetbrains.anko.longToast -import org.jetbrains.anko.startActivity -import javax.inject.Inject - -class LectureCancellationActivity : AppCompatActivity() { - - private companion object { - const val ROTATION_FROM = 0f - const val ROTATION_TO = 135f - const val DURATION = 800L - val INTERPOLATOR = AnticipateOvershootInterpolator() - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private val viewModel by lazy(LazyThreadSafetyMode.NONE) { - ViewModelProvider(this, viewModelFactory).get(LectureCancellationViewModel::class.java) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_lecture_cancellation) - AndroidInjection.inject(this) - - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - viewModel.lectureCancel.observe(this, Observer { data -> - if (data == null) { - longToast(getString(R.string.error_not_found, getString(R.string.name_lecture_cancel))) - finish() - return@Observer - } - - toolbar_layout.title = data.subject - initView(data) - - if (data.attend == LectureAttendType.PORTAL) { - fab.hide() - } else { - fab.show() - } - - fab.setOnClickListener { - val type = viewModel.getCurrentAttendType() ?: return@setOnClickListener - - if (type == LectureAttendType.USER) { - showConfirmationDialog(data.subject) - } else { - viewModel.onClickAttendToUser { success -> - if (success) { - fab.animate() - .rotation(ROTATION_TO) - .setDuration(DURATION) - .setInterpolator(INTERPOLATOR) - .withLayer() - .start() - - snackbar(it, R.string.msg_register_class) - } else { - indefiniteSnackbar(layout, - getString(R.string.error_add, getString(R.string.name_attend_lecture)), - getString(R.string.action_close)) - } - } - } - } - }) - - viewModel.lectureCancelId.value = intent.getLongExtra("id", 0) - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.share, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.action_share -> viewModel.onClickShare(this) - android.R.id.home -> { - finish() - startActivity() - } - } - - return super.onOptionsItemSelected(item) - } - - private fun initView(data: LectureCancellation) { - toolbar_layout.title = data.subject - - subject.text = data.subject - instructor.text = data.instructor - grade_week_period.text = - getString(R.string.text_grade_week_period, - data.grade, - data.week.formatWeek(), - data.period.formatPeriod()) - cancel_date.text = data.cancelDate.toShortString() - detail.text = data.detailHtml.htmlToSpanned() - detail.transformationMethod = CustomTransformationMethod(this) - date.text = getString(R.string.text_created_date_lecture_cancel, data.createdDate.toShortString()) - - fab.rotation = if (data.attend == LectureAttendType.USER) ROTATION_TO else ROTATION_FROM - } - - private fun showConfirmationDialog(subject: String) { - val builder = AlertDialog.Builder(this) - builder.setTitle(R.string.title_confirmation) - builder.setMessage(getString(R.string.text_unregister_confirm, subject).htmlToSpanned()) - builder.setPositiveButton(R.string.action_yes) { _, _ -> - viewModel.onClickAttendToNot { success -> - if (success) { - snackbar(layout, R.string.msg_unregister_class) - - fab.animate() - .rotation(ROTATION_FROM) - .setDuration(DURATION) - .setInterpolator(INTERPOLATOR) - .withLayer() - .start() - } else { - indefiniteSnackbar(layout - , getString(R.string.error_remove, getString(R.string.name_attend_lecture)), - getString(R.string.action_close)) - } - } - } - builder.setNegativeButton(R.string.action_no, null) - builder.show() - } - - private fun String.formatWeek() = this.replace("曜日", "曜") - - private fun String.formatPeriod() = if (this != "-") this + "限" else "" -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/LectureInformationActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/LectureInformationActivity.kt deleted file mode 100644 index 0602901..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/LectureInformationActivity.kt +++ /dev/null @@ -1,169 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.v7.app.AlertDialog -import android.support.v7.app.AppCompatActivity -import android.view.Menu -import android.view.MenuItem -import android.view.animation.AnticipateOvershootInterpolator -import dagger.android.AndroidInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.LectureAttendType -import jp.kentan.studentportalplus.data.model.LectureInformation -import jp.kentan.studentportalplus.ui.viewmodel.LectureInformationViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.util.CustomTransformationMethod -import jp.kentan.studentportalplus.util.htmlToSpanned -import jp.kentan.studentportalplus.util.indefiniteSnackbar -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.android.synthetic.main.activity_lecture_information.* -import kotlinx.android.synthetic.main.content_lecture_information.* -import org.jetbrains.anko.design.snackbar -import org.jetbrains.anko.longToast -import org.jetbrains.anko.startActivity -import javax.inject.Inject - - -class LectureInformationActivity : AppCompatActivity() { - - private companion object { - const val ROTATION_FROM = 0f - const val ROTATION_TO = 135f - const val DURATION = 800L - val INTERPOLATOR = AnticipateOvershootInterpolator() - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private val viewModel by lazy(LazyThreadSafetyMode.NONE) { - ViewModelProvider(this, viewModelFactory).get(LectureInformationViewModel::class.java) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_lecture_information) - AndroidInjection.inject(this) - - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - viewModel.lectureInfo.observe(this, Observer { data -> - if (data == null) { - longToast(getString(R.string.error_not_found, getString(R.string.name_lecture_info))) - finish() - return@Observer - } - - toolbar_layout.title = data.subject - initView(data) - - if (data.attend == LectureAttendType.PORTAL) { - fab.hide() - } else { - fab.show() - } - - fab.setOnClickListener { - val type = viewModel.getCurrentAttendType() ?: return@setOnClickListener - - if (type == LectureAttendType.USER) { - showConfirmationDialog(data.subject) - } else { - viewModel.onClickAttendToUser { success -> - if (success) { - fab.animate() - .rotation(ROTATION_TO) - .setDuration(DURATION) - .setInterpolator(INTERPOLATOR) - .withLayer() - .start() - - snackbar(it, R.string.msg_register_class) - } else { - indefiniteSnackbar(it, - getString(R.string.error_add, getString(R.string.name_attend_lecture)), - getString(R.string.action_close)) - } - } - } - } - }) - - viewModel.lectureInfoId.value = intent.getLongExtra("id", 0) - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.share, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.action_share -> viewModel.onClickShare(this) - android.R.id.home -> { - finish() - startActivity() - } - } - - return super.onOptionsItemSelected(item) - } - - private fun initView(data: LectureInformation) { - subject.text = data.subject - instructor.text = data.instructor - semester_week_period.text = - getString(R.string.text_semester_week_period, - data.grade, - data.semester.formatSemester(), - data.week.formatWeek(), - data.period.formatPeriod()) - category.text = data.category - detail.text = data.detailHtml.htmlToSpanned() - detail.transformationMethod = CustomTransformationMethod(this) - date.text = getString(R.string.text_created_date_lecture_info, data.createdDate.toShortString()) - - if (data.createdDate != data.updatedDate) { - date.append(getString(R.string.text_updated_date_lecture_info, data.updatedDate.toShortString())) - } - - fab.rotation = if (data.attend == LectureAttendType.USER) ROTATION_TO else ROTATION_FROM - } - - private fun showConfirmationDialog(subject: String) { - val builder = AlertDialog.Builder(this) - builder.setTitle(R.string.title_confirmation) - builder.setMessage(getString(R.string.text_unregister_confirm, subject).htmlToSpanned()) - builder.setPositiveButton(R.string.action_yes) { _, _ -> - viewModel.onClickAttendToNot { success -> - if (success) { - snackbar(layout, R.string.msg_unregister_class) - - fab.animate() - .rotation(ROTATION_FROM) - .setDuration(DURATION) - .setInterpolator(AnticipateOvershootInterpolator()) - .withLayer() - .start() - } else { - indefiniteSnackbar(layout, - getString(R.string.error_remove, getString(R.string.name_attend_lecture)), - getString(R.string.action_close)) - } - } - } - builder.setNegativeButton(R.string.action_no, null) - builder.show() - } - - private fun String.formatSemester() = if (this == "前" || this == "後") this + "学期" else this.hyphenToWhitespace() - - private fun String.formatWeek() = this.replace("曜日", "曜") - - private fun String.formatPeriod() = if (this != "-") this + "限" else "" - - private fun String.hyphenToWhitespace() = if (this == "-") " " else this -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/LoginActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/LoginActivity.kt deleted file mode 100644 index 268f82e..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/LoginActivity.kt +++ /dev/null @@ -1,196 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.support.v7.app.AppCompatActivity -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.TextView -import dagger.android.AndroidInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.shibboleth.ShibbolethClient -import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider -import jp.kentan.studentportalplus.util.customTitle -import jp.kentan.studentportalplus.util.hideSoftInput -import jp.kentan.studentportalplus.util.setFirstLaunch -import kotlinx.android.synthetic.main.activity_login.* -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.clearTask -import org.jetbrains.anko.coroutines.experimental.bg -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.intentFor -import org.jetbrains.anko.newTask -import javax.inject.Inject - - -class LoginActivity : AppCompatActivity() { - - companion object { - private const val EXTRA_SHOULD_LAUNCH_MAIN_ACTIVITY = "should_launch_main_activity" - - fun createIntent(context: Context, shouldLaunchMainActivity: Boolean = false) - = Intent(context, LoginActivity::class.java).apply { - putExtra(EXTRA_SHOULD_LAUNCH_MAIN_ACTIVITY, shouldLaunchMainActivity) - } - } - - @Inject - lateinit var shibbolethDataProvider: ShibbolethDataProvider - - private var loginJob: Job? = null - private var shouldLaunchMainActivity: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) - - AndroidInjection.inject(this) - - customTitle = getString(R.string.title_activity_login) - - // Set up the login form. - populateUsername() - password.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ -> - if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { - attemptLogin() - return@OnEditorActionListener true - } - false - }) - - login_button.setOnClickListener { attemptLogin() } - - shouldLaunchMainActivity = intent.getBooleanExtra(EXTRA_SHOULD_LAUNCH_MAIN_ACTIVITY, false) - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - supportActionBar?.setDisplayHomeAsUpEnabled(true) - return true - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - loginJob?.cancel() - finish() - return super.onOptionsItemSelected(item) - } - - override fun onBackPressed() { - loginJob?.cancel() - super.onBackPressed() - } - - private fun populateUsername() { - val usernameStr = shibbolethDataProvider.getUsername() ?: return - - username.setText(usernameStr) - password.requestFocus() - } - - private fun attemptLogin() { - // Prevent multi jobs - loginJob?.cancel() - - // Reset errors. - username.error = null - password.error = null - - // Store values at the time of the login attempt. - val usernameStr = username.text.toString() - val passwordStr = password.text.toString() - - var focusView: View? = null - - // Check for a valid password. - if (TextUtils.isEmpty(passwordStr)) { - password.error = getString(R.string.error_field_required) - focusView = password - } else if (!isPasswordValid(passwordStr)) { - password.error = getString(R.string.error_invalid_password) - focusView = password - } - - // Check for a valid username. - if (TextUtils.isEmpty(usernameStr)) { - username.error = getString(R.string.error_field_required) - focusView = username - } else if (!isUsernameValid(usernameStr)) { - username.error = getString(R.string.error_invalid_username) - focusView = username - } - - if (focusView != null) { - focusView.requestFocus() - return - } - - hideSoftInput() - - loginJob = launchLoginJob(usernameStr, passwordStr) - } - - private fun isUsernameValid(username: String): Boolean = username.startsWith('b') || username.startsWith('m') || username.startsWith('d') - - private fun isPasswordValid(password: String): Boolean = password.length in 8..24 - - private fun launchLoginJob(username: String, password: String) = launch(UI) { - showProgress(true) - - val client = ShibbolethClient(this@LoginActivity, shibbolethDataProvider) - - val result = bg { - client.auth(username, password) - } - - val (isSuccess, message) = result.await() - - showProgress(false) - - if (isSuccess) { - defaultSharedPreferences.setFirstLaunch(false) - - if (shouldLaunchMainActivity) { - launchMainActivity() - } - finish() - } else { - error_text.visibility = View.VISIBLE - error_text.text = message ?: getString(R.string.error_unknown) - } - } - - private fun launchMainActivity() { - startActivity(intentFor(MainActivity.REQUIRE_SYNC to true).newTask().clearTask()) - } - - private fun showProgress(isShow: Boolean) { - val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() - - login_form.visibility = if (isShow) View.GONE else View.VISIBLE - login_form.animate() - .setDuration(shortAnimTime) - .alpha((if (isShow) 0 else 1).toFloat()) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - login_form.visibility = if (isShow) View.GONE else View.VISIBLE - } - }) - - login_progress.visibility = if (isShow) View.VISIBLE else View.GONE - login_progress.animate() - .setDuration(shortAnimTime) - .alpha((if (isShow) 1 else 0).toFloat()) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - login_progress.visibility = if (isShow) View.VISIBLE else View.GONE - } - }) - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/MainActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/MainActivity.kt deleted file mode 100644 index d4f3db3..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/MainActivity.kt +++ /dev/null @@ -1,276 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.design.widget.NavigationView -import android.support.design.widget.Snackbar -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentTransaction -import android.support.v4.view.GravityCompat -import android.support.v7.app.ActionBarDrawerToggle -import android.support.v7.app.AppCompatActivity -import android.view.MenuItem -import dagger.android.AndroidInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.notification.SyncScheduler -import jp.kentan.studentportalplus.ui.fragment.* -import jp.kentan.studentportalplus.ui.span.CustomTitle -import jp.kentan.studentportalplus.ui.viewmodel.MainViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.ui.widget.MapView -import jp.kentan.studentportalplus.util.enabledDetailError -import jp.kentan.studentportalplus.util.isFirstLaunch -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.app_bar_main.* -import kotlinx.android.synthetic.main.nav_header_main.view.* -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.* -import javax.inject.Inject - -class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { - - companion object { - const val REQUIRE_SYNC = "require_sync" - const val FRAGMENT_TYPE = "fragment_type" - } - - enum class FragmentType{DASHBOARD, TIMETABLE, LECTURE_INFO, LECTURE_CANCEL, NOTICE} - - private var fragmentType = FragmentType.DASHBOARD - - private val fragmentMap by lazy(LazyThreadSafetyMode.NONE) { - mapOf( - FragmentType.DASHBOARD to DashboardFragment.newInstance(), - FragmentType.TIMETABLE to TimetableFragment.newInstance(), - FragmentType.LECTURE_INFO to LectureInformationFragment.newInstance(), - FragmentType.LECTURE_CANCEL to LectureCancellationFragment.newInstance(), - FragmentType.NOTICE to NoticeFragment.newInstance()) - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private val viewModel by lazy(LazyThreadSafetyMode.NONE) { - ViewModelProvider(this, viewModelFactory).get(MainViewModel::class.java) - } - - private var isReadyFinish = false - private var snackbarFinish: Snackbar? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - setSupportActionBar(toolbar) - - if (defaultSharedPreferences.isFirstLaunch()) { - launchWelcomeActivity() - return - } - - AndroidInjection.inject(this) - - val toggle = ActionBarDrawerToggle(this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) - drawer_layout.addDrawerListener(toggle) - toggle.syncState() - - nav_view.setNavigationItemSelectedListener(this) - - setupSwipeRefresh() - setupAccountHeader() - - val fragment = when { - intent.hasExtra(FRAGMENT_TYPE) -> { - val type = FragmentType.valueOf(intent.getStringExtra(FRAGMENT_TYPE)) - intent.removeExtra(FRAGMENT_TYPE) - - fragmentMap[type] - } - supportFragmentManager.fragments.isEmpty() -> DashboardFragment.newInstance() - else -> null - } - - if (fragment != null) { - supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, fragment) - .commit() - } - - if (intent.getBooleanExtra(REQUIRE_SYNC, false)) { - viewModel.sync() - } - - SyncScheduler.scheduleIfNeed(this) - - viewModel.load() - } - - override fun onBackPressed() { - if (drawer_layout.isDrawerOpen(GravityCompat.START)) { - drawer_layout.closeDrawer(GravityCompat.START) - } else { - if (supportFragmentManager.backStackEntryCount <= 0) { - if (isReadyFinish) { - snackbarFinish?.dismiss() - snackbarFinish = null - - finish() - } else { - isReadyFinish = true - - val snackbar = Snackbar.make(swipe_refresh_layout, R.string.msg_back_to_exit, Snackbar.LENGTH_LONG) - .addCallback(object : Snackbar.Callback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - isReadyFinish = false - super.onDismissed(transientBottomBar, event) - } - }) - snackbar.show() - - snackbarFinish = snackbar - } - - return - } - - super.onBackPressed() - } - } - - override fun onNavigationItemSelected(item: MenuItem): Boolean { - drawer_layout.closeDrawer(GravityCompat.START) - - launch(UI) { - delay(300) - - when (item.itemId) { - R.id.nav_dashboard -> { switchFragment(FragmentType.DASHBOARD) } - R.id.nav_timetable -> { switchFragment(FragmentType.TIMETABLE) } - R.id.nav_lecture_info -> { switchFragment(FragmentType.LECTURE_INFO) } - R.id.nav_lecture_cancel -> { switchFragment(FragmentType.LECTURE_CANCEL) } - R.id.nav_notice -> { switchFragment(FragmentType.NOTICE) } - R.id.nav_campus_map -> { MapView.open(this@MainActivity, MapView.Type.CAMPUS) } - R.id.nav_room_map -> { MapView.open(this@MainActivity, MapView.Type.ROOM) } - R.id.nav_setting -> { startActivity() } - } - } - - return true - } - - override fun onAttachFragment(fragment: Fragment?) { - if (nav_view != null) { - when (fragment) { - is DashboardFragment -> { - fragmentType = FragmentType.DASHBOARD - - title = CustomTitle(this, getString(R.string.title_dashboard_fragment)) - nav_view.menu.findItem(R.id.nav_dashboard).isChecked = true - } - is TimetableFragment -> { - fragmentType = FragmentType.TIMETABLE - - title = CustomTitle(this, getString(R.string.title_timetable_fragment)) - nav_view.menu.findItem(R.id.nav_timetable).isChecked = true - } - is LectureInformationFragment -> { - fragmentType = FragmentType.LECTURE_INFO - - title = CustomTitle(this, getString(R.string.title_lecture_info_fragment)) - nav_view.menu.findItem(R.id.nav_lecture_info).isChecked = true - } - is LectureCancellationFragment -> { - fragmentType = FragmentType.LECTURE_CANCEL - - title = CustomTitle(this, getString(R.string.title_lecture_cancel_fragment)) - nav_view.menu.findItem(R.id.nav_lecture_cancel).isChecked = true - } - is NoticeFragment -> { - fragmentType = FragmentType.NOTICE - - title = CustomTitle(this, getString(R.string.title_notice_fragment)) - nav_view.menu.findItem(R.id.nav_notice).isChecked = true - } - } - } - - super.onAttachFragment(fragment) - } - - private fun setupSwipeRefresh() { - swipe_refresh_layout.setProgressBackgroundColorSchemeResource(R.color.colorAccent) - swipe_refresh_layout.setColorSchemeResources(R.color.grey_100) - swipe_refresh_layout.setOnRefreshListener { viewModel.sync() } - - viewModel.syncResult.observe(this, Observer { - val (result, message) = it ?: return@Observer - - val snackbar = Snackbar.make(swipe_refresh_layout, R.string.error_unknown, Snackbar.LENGTH_INDEFINITE) - when (result) { - MainViewModel.SyncResult.AUTH_ERROR -> { - if (message == null) { - snackbar.setText(R.string.msg_request_shibboleth_data) - } else { - snackbar.setText("$message\n${getString(R.string.msg_request_shibboleth_data)}") - } - - snackbar.setAction(R.string.action_login) { - startActivity(LoginActivity.createIntent(this, shouldLaunchMainActivity = true)) - } - } - MainViewModel.SyncResult.UNKNOWN_ERROR -> { - if (message != null && defaultSharedPreferences.enabledDetailError()) { - snackbar.setText(message) - } else { - snackbar.setText(R.string.error_failed_to_sync) - } - - snackbar.setAction(R.string.action_close) { snackbar.dismiss() } - } - MainViewModel.SyncResult.SUCCESS -> return@Observer - } - - snackbar.show() - }) - - viewModel.isSyncing.observe(this, Observer { - swipe_refresh_layout.isRefreshing = it ?: false - }) - } - - private fun setupAccountHeader() { - viewModel.getUser().observe(this@MainActivity, Observer { - it?.let { - val header = nav_view.getHeaderView(0) - val (name, username) = it - - header.name.text = name - header.student_number.text = username - } - }) - } - - fun switchFragment(type: FragmentType) { - if (fragmentType == type) { - return - } - - supportFragmentManager.beginTransaction() - .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - .replace(R.id.fragment_container, fragmentMap[type]) - .addToBackStack(null) - .commit() - - fragmentType = type - } - - private fun launchWelcomeActivity() { - val intent = intentFor().newTask().clearTask() - - startActivity(intent) - - finish() - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/MyClassActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/MyClassActivity.kt deleted file mode 100644 index f0ae3ee..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/MyClassActivity.kt +++ /dev/null @@ -1,113 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.content.Context -import android.content.Intent -import android.databinding.DataBindingUtil -import android.os.Bundle -import android.support.v7.app.AlertDialog -import android.support.v7.app.AppCompatActivity -import android.view.Menu -import android.view.MenuItem -import dagger.android.AndroidInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.databinding.ActivityMyClassBinding -import jp.kentan.studentportalplus.ui.myclass.edit.MyClassEditActivity -import jp.kentan.studentportalplus.ui.viewmodel.MyClassViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.util.CustomTransformationMethod -import jp.kentan.studentportalplus.util.htmlToSpanned -import jp.kentan.studentportalplus.util.indefiniteSnackbar -import org.jetbrains.anko.longToast -import javax.inject.Inject - -class MyClassActivity : AppCompatActivity() { - - companion object { - private const val EXTRA_DATA_ID = "id" - - fun createIntent(context: Context, id: Long): Intent { - return Intent(context, MyClassActivity::class.java).apply { - putExtra(EXTRA_DATA_ID, id) - } - } - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private val viewModel by lazy(LazyThreadSafetyMode.NONE) { - ViewModelProvider(this, viewModelFactory).get(MyClassViewModel::class.java) - } - - private lateinit var binding: ActivityMyClassBinding - - private val customTransformationMethod = CustomTransformationMethod(this) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = DataBindingUtil.setContentView(this, R.layout.activity_my_class) - - AndroidInjection.inject(this) - - setSupportActionBar(binding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - viewModel.myClass.observe(this, Observer { data -> - if (data == null) { - longToast(getString(R.string.error_not_found, getString(R.string.name_my_class))) - finish() - return@Observer - } - - binding.myClass = data - binding.setOnEditClickListener { - startActivity(MyClassEditActivity.createIntent(this, data.id)) - } - binding.content.syllabus.transformationMethod = customTransformationMethod - - invalidateOptionsMenu() - }) - - viewModel.setId(intent.getLongExtra(EXTRA_DATA_ID, 0)) - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - if (viewModel.canDelete) { - menuInflater.inflate(R.menu.delete, menu) - } - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { - android.R.id.home -> finish() - R.id.action_delete -> showDeleteDialog() - } - - return super.onOptionsItemSelected(item) - } - - private fun showDeleteDialog() { - if (!viewModel.canDelete) { - return - } - - AlertDialog.Builder(this) - .setTitle(R.string.title_delete) - .setMessage(getString(R.string.text_delete_confirm, viewModel.subject).htmlToSpanned()) - .setPositiveButton(R.string.action_yes) { _, _ -> - viewModel.delete { success -> - if (success) { - finish() - } else { - indefiniteSnackbar(binding.fab, getString(R.string.error_delete), getString(R.string.action_close)) - } - } - } - .setNegativeButton(R.string.action_no, null) - .show() - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/NoticeActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/NoticeActivity.kt deleted file mode 100644 index aaa13ba..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/NoticeActivity.kt +++ /dev/null @@ -1,136 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.v7.app.AppCompatActivity -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.animation.OvershootInterpolator -import android.widget.TextView -import dagger.android.AndroidInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.Notice -import jp.kentan.studentportalplus.ui.viewmodel.NoticeViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.util.CustomTransformationMethod -import jp.kentan.studentportalplus.util.htmlToSpanned -import jp.kentan.studentportalplus.util.indefiniteSnackbar -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.android.synthetic.main.activity_notice.* -import kotlinx.android.synthetic.main.content_notice.* -import org.jetbrains.anko.design.snackbar -import org.jetbrains.anko.find -import org.jetbrains.anko.longToast -import org.jetbrains.anko.startActivity -import javax.inject.Inject - -class NoticeActivity : AppCompatActivity() { - - private companion object { - const val ROTATION_FROM = 0f - const val ROTATION_TO = 144f - const val DURATION = 800L - val INTERPOLATOR = OvershootInterpolator() - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private val viewModel by lazy(LazyThreadSafetyMode.NONE) { - ViewModelProvider(this, viewModelFactory).get(NoticeViewModel::class.java) - } - - private val customTransformationMethod = CustomTransformationMethod(this) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_notice) - AndroidInjection.inject(this) - - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - initFab() - - viewModel.notice.observe(this, Observer { data -> - if (data == null) { - longToast(getString(R.string.error_not_found, getString(R.string.name_notice))) - finish() - return@Observer - } - - toolbar_layout.title = data.title - initView(data) - }) - - viewModel.noticeId.value = intent.getLongExtra("id", 0) - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.share, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.action_share -> viewModel.onClickShare(this) - android.R.id.home -> { - finish() - startActivity() - } - } - - return super.onOptionsItemSelected(item) - } - - private fun initView(data: Notice) { - fab.rotation = if (data.isFavorite) ROTATION_TO else ROTATION_FROM - fab.setImageResource(if (data.isFavorite) R.drawable.ic_star else R.drawable.ic_star_border) - - in_charge.text = data.inCharge - category.text = data.category - - find(R.id.title).text = data.title - - if (data.detailHtml != null) { - detail.text = data.detailHtml.htmlToSpanned() - detail.transformationMethod = customTransformationMethod - } else { - detail_header.visibility = View.GONE - detail.visibility = View.GONE - } - - if (data.link != null) { - link.text = data.link - link.transformationMethod = customTransformationMethod - } else { - link_header.visibility = View.GONE - link.visibility = View.GONE - } - - date.text = getString(R.string.text_created_date_notice, data.createdDate.toShortString()) - } - - private fun initFab() { - fab.setOnClickListener { - viewModel.onClickFavorite{ success: Boolean, favorite: Boolean -> - if (success) { - fab.setImageResource(if (favorite) R.drawable.ic_star else R.drawable.ic_star_border) - fab.animate() - .rotation(if (favorite) ROTATION_TO else ROTATION_FROM) - .setDuration(DURATION) - .setInterpolator(INTERPOLATOR) - .start() - - snackbar(it, if (favorite) R.string.msg_set_favorite else R.string.msg_reset_favorite) - } else { - indefiniteSnackbar(it, - getString(R.string.error_update, getString(R.string.name_favorite)), - getString(R.string.action_close)) - } - } - } - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/SettingsActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/SettingsActivity.kt deleted file mode 100644 index 658c543..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/SettingsActivity.kt +++ /dev/null @@ -1,309 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.app.FragmentTransaction -import android.content.Intent -import android.content.SharedPreferences -import android.os.Bundle -import android.preference.ListPreference -import android.preference.Preference -import android.preference.PreferenceFragment -import android.preference.PreferenceScreen -import android.support.v7.app.AlertDialog -import android.support.v7.app.AppCompatActivity -import android.view.MenuItem -import dagger.android.AndroidInjection -import jp.kentan.studentportalplus.BuildConfig -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.NotifyType -import jp.kentan.studentportalplus.notification.NotificationController -import jp.kentan.studentportalplus.notification.SyncScheduler -import jp.kentan.studentportalplus.ui.span.CustomTitle -import jp.kentan.studentportalplus.ui.widget.MyClassThresholdSamplePreference -import jp.kentan.studentportalplus.util.enabledSync -import jp.kentan.studentportalplus.util.getSyncIntervalMinutes -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.coroutines.experimental.bg -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.longToast -import javax.inject.Inject - - -class SettingsActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - title = CustomTitle(this, getString(R.string.title_activity_settings)) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - if (savedInstanceState == null) { - fragmentManager - .beginTransaction() - .add(android.R.id.content, PreferencesFragment()) - .commit() - } - - NotificationController.setupChannel(this) - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - onBackPressed() - return true - } - - override fun onBackPressed() { - val count = fragmentManager.backStackEntryCount - - if (count > 0) { - fragmentManager.popBackStackImmediate() - return - } - - super.onBackPressed() - } - - class PreferencesFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { - - @Inject - lateinit var portalRepository: PortalRepository - - private lateinit var shibbolethLastLoginDate: Preference - private lateinit var syncInterval: ListPreference - private lateinit var notifyContents: Preference - private var notifyVibration: Preference? = null - private var notifyLed: Preference? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - addPreferencesFromResource(R.xml.preferences) - - AndroidInjection.inject(this) - - val screen = preferenceScreen - - shibbolethLastLoginDate = screen.findPreference("shibboleth_last_login_date") - syncInterval = screen.findPreference("sync_interval") as ListPreference - notifyContents = screen.findPreference("notify_contents") - notifyVibration = screen.findPreference("enabled_notification_vibration") - notifyLed = screen.findPreference("enabled_notification_led") - - screen.findPreference("notification_settings")?.setOnPreferenceClickListener { - val intent = Intent("android.settings.CHANNEL_NOTIFICATION_SETTINGS") - intent.putExtra("android.provider.extra.APP_PACKAGE", activity.packageName) - intent.putExtra("android.provider.extra.CHANNEL_ID", NotificationController.NEWLY_CHANNEL_ID) - startActivity(intent) - - return@setOnPreferenceClickListener true - } - - notifyContents.setOnPreferenceClickListener { - fragmentManager - .beginTransaction() - .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - .replace(android.R.id.content, NotifyContentsFragment()) - .addToBackStack(null) - .commit() - - return@setOnPreferenceClickListener true - } - - screen.findPreference("my_class_threshold").setOnPreferenceClickListener { - fragmentManager - .beginTransaction() - .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - .replace(android.R.id.content, MyClassThresholdFragment()) - .addToBackStack(null) - .commit() - - return@setOnPreferenceClickListener true - } - - - val enabled = defaultSharedPreferences.enabledSync() - - syncInterval.isEnabled = enabled - notifyContents.isEnabled = enabled - notifyVibration?.isEnabled = enabled - notifyLed?.isEnabled = enabled - - setupSummary(screen) - } - - override fun onPreferenceTreeClick(preferenceScreen: PreferenceScreen, pref: Preference): Boolean { - when (pref.key) { - "reset" -> { - AlertDialog.Builder(activity) - .setIcon(R.drawable.ic_warning) - .setTitle("ポータルデータ消去") - .setMessage(R.string.msg_warn_reset) - .setPositiveButton(R.string.action_yes) { _, _ -> - bg { - portalRepository.deleteAll { success -> - launch(UI) { - if (success) { - longToast("すべてのポータルデータを消去しました") - } else { - longToast("消去に失敗しました") - } - } - } - } - } - .setNegativeButton(R.string.action_no, null) - .show() - } - "share" -> { - val intent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, getString(R.string.text_share_app)) - } - startActivity(intent) - } - "terms" -> { - startActivity(WebActivity.createIntent(activity, "Terms", getString(R.string.url_terms))) - } - "oss_license" -> { - startActivity(WebActivity.createIntent(activity, "Licenses", getString(R.string.url_licenses))) - } - } - return super.onPreferenceTreeClick(preferenceScreen, pref) - } - - override fun onSharedPreferenceChanged(pref: SharedPreferences, key: String) { - when (key) { - "shibboleth_last_login_date" -> { - shibbolethLastLoginDate.summary = pref.getString("shibboleth_last_login_date", "なし") - } - "enabled_sync" -> { - val enable = pref.getBoolean("enabled_sync", true) - - syncInterval.isEnabled = enable - notifyContents.isEnabled = enable - notifyVibration?.isEnabled = enable - notifyLed?.isEnabled = enable - - if (enable) { - SyncScheduler.schedule(activity) - } else { - SyncScheduler.cancel() - } - } - "sync_interval" -> { - val interval = pref.getSyncIntervalMinutes() - - syncInterval.summary = if (interval >= 60) { - "${interval / 60}時間毎に更新する" - } else { - "${interval}分毎に更新する" - } - - SyncScheduler.schedule(activity) - } - } - } - - override fun onResume() { - super.onResume() - defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) - } - - override fun onPause() { - super.onPause() - defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) - } - - private fun setupSummary(screen: PreferenceScreen) { - val pref = defaultSharedPreferences - - screen.findPreference("shibboleth_last_login_date").summary = pref.getString("shibboleth_last_login_date", "なし") - screen.findPreference("version").summary = BuildConfig.VERSION_NAME - - val interval = pref.getSyncIntervalMinutes() - syncInterval.summary = if (interval >= 60) { - "${interval / 60}時間毎に更新する" - } else { - "${interval}分毎に更新する" - } - } - } - - class NotifyContentsFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { - - private lateinit var lectureInfo: Preference - private lateinit var lectureCancel: Preference - private lateinit var notice: Preference - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - addPreferencesFromResource(R.xml.pref_notify_contents) - - val screen = preferenceScreen - val pref = defaultSharedPreferences - - lectureInfo = screen.findPreference("notify_type_lecture_info") - lectureCancel = screen.findPreference("notify_type_lecture_cancel") - notice = screen.findPreference("notify_type_notice") - - lectureInfo.summary = NotifyType.valueOf(pref.getString("notify_type_lecture_info", NotifyType.ALL.name)).displayName - lectureCancel.summary = NotifyType.valueOf(pref.getString("notify_type_lecture_cancel", NotifyType.ALL.name)).displayName - notice.summary = NotifyType.valueOf(pref.getString("notify_type_notice", NotifyType.ALL.name)).displayName - - } - - override fun onSharedPreferenceChanged(pref: SharedPreferences, key: String) { - - val preference = when (key) { - "notify_type_lecture_info" -> lectureInfo - "notify_type_lecture_cancel" -> lectureCancel - "notify_type_notice" -> notice - else -> return - } - - preference.summary = NotifyType.valueOf(pref.getString(key, NotifyType.ALL.name)).displayName - } - - override fun onResume() { - super.onResume() - defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) - } - - override fun onPause() { - super.onPause() - defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) - } - } - - - class MyClassThresholdFragment : PreferenceFragment() { - - private lateinit var threshold: Preference - private lateinit var sample: MyClassThresholdSamplePreference - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - addPreferencesFromResource(R.xml.pref_my_class_threshold) - - val screen = preferenceScreen - - threshold = screen.findPreference("my_class_threshold") - sample = screen.findPreference("my_class_threshold_sample") as MyClassThresholdSamplePreference - - val currentPercent = defaultSharedPreferences.getString("my_class_threshold", "80").toIntOrNull() ?: 80 - - threshold.summary = if (currentPercent < 100) "$currentPercent%%以上" else "100%%" - sample.updateThreshold(currentPercent) - - threshold.setOnPreferenceChangeListener { _, newValue -> - val percent = newValue.toString().toIntOrNull() ?: 80 - - threshold.summary = if (percent < 100) "$newValue%%以上" else "100%%" - sample.updateThreshold(percent) - - return@setOnPreferenceChangeListener true - } - } - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/SingleLiveData.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/SingleLiveData.kt new file mode 100644 index 0000000..465bf1e --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/SingleLiveData.kt @@ -0,0 +1,46 @@ +package jp.kentan.studentportalplus.ui + +import android.util.Log +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + + +class SingleLiveData : MutableLiveData() { + + private val pending = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") + } + + // Observe the internal MutableLiveData + super.observe(owner, Observer { + if (pending.compareAndSet(true, false)) { + observer.onChanged(it) + } + }) + } + + @MainThread + override fun setValue(t: T?) { + pending.set(true) + super.setValue(t) + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + fun call() { + value = null + } + + companion object { + private const val TAG = "SingleLiveData" + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/ViewModelFactory.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/ViewModelFactory.kt new file mode 100644 index 0000000..fd08dc3 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/ViewModelFactory.kt @@ -0,0 +1,62 @@ +package jp.kentan.studentportalplus.ui + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.UserRepository +import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider +import jp.kentan.studentportalplus.ui.dashboard.DashboardViewModel +import jp.kentan.studentportalplus.ui.lecturecancel.LectureCancelViewModel +import jp.kentan.studentportalplus.ui.lecturecancel.detail.LectureCancelDetailViewModel +import jp.kentan.studentportalplus.ui.lectureinfo.LectureInfoViewModel +import jp.kentan.studentportalplus.ui.lectureinfo.detail.LectureInfoDetailViewModel +import jp.kentan.studentportalplus.ui.login.LoginViewModel +import jp.kentan.studentportalplus.ui.main.MainViewModel +import jp.kentan.studentportalplus.ui.myclass.detail.MyClassDetailViewModel +import jp.kentan.studentportalplus.ui.myclass.edit.MyClassEditViewModel +import jp.kentan.studentportalplus.ui.notice.NoticeViewModel +import jp.kentan.studentportalplus.ui.notice.detail.NoticeDetailViewModel +import jp.kentan.studentportalplus.ui.timetable.TimetableViewModel +import org.jetbrains.anko.defaultSharedPreferences + +class ViewModelFactory( + private val context: Application, + private val portalRepository: PortalRepository, + private val userRepository: UserRepository, + private val shibbolethDataProvider: ShibbolethDataProvider +) : ViewModelProvider.AndroidViewModelFactory(context) { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class) = + with(modelClass) { + when { + isAssignableFrom(MainViewModel::class.java) -> + MainViewModel(portalRepository, userRepository) + isAssignableFrom(DashboardViewModel::class.java) -> + DashboardViewModel(portalRepository) + isAssignableFrom(TimetableViewModel::class.java) -> + TimetableViewModel(context.defaultSharedPreferences, portalRepository) + isAssignableFrom(LectureInfoViewModel::class.java) -> + LectureInfoViewModel(portalRepository) + isAssignableFrom(LectureCancelViewModel::class.java) -> + LectureCancelViewModel(portalRepository) + isAssignableFrom(NoticeViewModel::class.java) -> + NoticeViewModel(portalRepository) + isAssignableFrom(LectureInfoDetailViewModel::class.java) -> + LectureInfoDetailViewModel(context, portalRepository) + isAssignableFrom(LectureCancelDetailViewModel::class.java) -> + LectureCancelDetailViewModel(context, portalRepository) + isAssignableFrom(NoticeDetailViewModel::class.java) -> + NoticeDetailViewModel(context, portalRepository) + isAssignableFrom(MyClassDetailViewModel::class.java) -> + MyClassDetailViewModel(portalRepository) + isAssignableFrom(MyClassEditViewModel::class.java) -> + MyClassEditViewModel(portalRepository) + isAssignableFrom(LoginViewModel::class.java) -> + LoginViewModel(context, shibbolethDataProvider) + + else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + } + } as T +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/WelcomeActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/WelcomeActivity.kt deleted file mode 100644 index c854937..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/WelcomeActivity.kt +++ /dev/null @@ -1,62 +0,0 @@ -package jp.kentan.studentportalplus.ui - -import android.os.Bundle -import android.support.v7.app.AppCompatActivity -import android.webkit.WebView -import android.webkit.WebViewClient -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.util.customTitle -import kotlinx.android.synthetic.main.activity_welcome.* -import org.jetbrains.anko.longToast - - -class WelcomeActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_welcome) - - customTitle = getString(R.string.title_activity_welcome) - - setupWebView() - - shibboleth_button.setOnClickListener { - if (!agree_checkbox.isChecked) { - longToast(getString(R.string.error_not_agree_to_terms)) - return@setOnClickListener - } - - startActivity(LoginActivity.createIntent(this, shouldLaunchMainActivity = true)) - } - } - - override fun onPause() { - web_view.onPause() - web_view.pauseTimers() - super.onPause() - } - - override fun onResume() { - super.onResume() - web_view.resumeTimers() - web_view.onResume() - } - - override fun onDestroy() { - web_view.destroy() - super.onDestroy() - } - - private fun setupWebView() { - web_view.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - view ?: return - - if (!view.title.contains(getString(R.string.title_terms))) { - web_view.loadUrl(getString(R.string.url_terms_local)) - } - } - } - web_view.loadUrl(getString(R.string.url_terms)) - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureCancellationAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureCancellationAdapter.kt deleted file mode 100644 index 713a4fe..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureCancellationAdapter.kt +++ /dev/null @@ -1,102 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.graphics.Typeface -import android.support.v7.util.DiffUtil -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.LectureCancellation -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.android.synthetic.main.list_small_lecture.view.* - -/** - * LectureCancellationAdapter on MainThread - * - * @see LectureCancellationAdapter - */ -class DashboardLectureCancellationAdapter( - private val context: Context, - private val maxItemCount: Int, - private val onClick: (data: LectureCancellation) -> Unit = {}) : -RecyclerView.Adapter() { - - private var currentList: List = emptyList() - private var isOverMaxItemCount: Boolean = false - - init { - setHasStableIds(true) - } - - override fun getItemCount() = currentList.size - - override fun getItemId(position: Int) = currentList[position].id - - fun submitList(list: List) { - val oldList = currentList - val newList = list.take(maxItemCount) - - isOverMaxItemCount = list.size > maxItemCount - - val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int { return oldList.size } - - override fun getNewListSize(): Int { return newList.size } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return LectureCancellation.DIFF_CALLBACK.areItemsTheSame( - oldList[oldItemPosition], newList[newItemPosition] - ) - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return LectureCancellation.DIFF_CALLBACK.areContentsTheSame( - currentList[oldItemPosition], newList[newItemPosition] - ) - } - }) - - currentList = newList - - result.dispatchUpdatesTo(this) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutInflater = LayoutInflater.from(context) - val view = layoutInflater.inflate(R.layout.list_small_lecture, parent, false) - - return ViewHolder(view, onClick) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.separator.visibility = if (!isOverMaxItemCount && position >= itemCount-1) View.GONE else View.VISIBLE - holder.bindTo(currentList[position]) - } - - class ViewHolder( - private val view: View, - private val onClick: (data: LectureCancellation) -> Unit) : RecyclerView.ViewHolder(view) { - - val separator: View = view.separator - - fun bindTo(data: LectureCancellation) { - view.date.text = data.createdDate.toShortString() - view.subject.text = data.subject - view.detail.text = data.detailText - - if (data.isRead) { - view.date.typeface = Typeface.DEFAULT - view.subject.typeface = Typeface.DEFAULT - view.detail.typeface = Typeface.DEFAULT - } else { - view.date.typeface = Typeface.DEFAULT_BOLD - view.subject.typeface = Typeface.DEFAULT_BOLD - view.detail.typeface = Typeface.DEFAULT_BOLD - } - - view.setOnClickListener { onClick(data) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureInformationAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureInformationAdapter.kt deleted file mode 100644 index 6e62c05..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardLectureInformationAdapter.kt +++ /dev/null @@ -1,102 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.graphics.Typeface -import android.support.v7.util.DiffUtil -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.LectureInformation -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.android.synthetic.main.list_small_lecture.view.* - -/** - * LectureInformationAdapter on MainThread - * - * @see LectureInformationAdapter - */ -class DashboardLectureInformationAdapter( - private val context: Context, - private val maxItemCount: Int, - private val onClick: (data: LectureInformation) -> Unit = {}) : -RecyclerView.Adapter() { - - private var currentList: List = emptyList() - private var isOverMaxItemCount: Boolean = false - - init { - setHasStableIds(true) - } - - override fun getItemCount() = currentList.size - - override fun getItemId(position: Int) = currentList[position].id - - fun submitList(list: List) { - val oldList = currentList - val newList = list.take(maxItemCount) - - isOverMaxItemCount = list.size > maxItemCount - - val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int { return oldList.size } - - override fun getNewListSize(): Int { return newList.size } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return LectureInformation.DIFF_CALLBACK.areItemsTheSame( - oldList[oldItemPosition], newList[newItemPosition] - ) - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return LectureInformation.DIFF_CALLBACK.areContentsTheSame( - currentList[oldItemPosition], newList[newItemPosition] - ) - } - }) - - currentList = newList - - result.dispatchUpdatesTo(this) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutInflater = LayoutInflater.from(context) - val view = layoutInflater.inflate(R.layout.list_small_lecture, parent, false) - - return ViewHolder(view, onClick) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.separator.visibility = if (!isOverMaxItemCount && position >= itemCount-1) View.GONE else View.VISIBLE - holder.bindTo(currentList[position]) - } - - class ViewHolder( - private val view: View, - private val onClick: (data: LectureInformation) -> Unit) : RecyclerView.ViewHolder(view) { - - val separator: View = view.separator - - fun bindTo(data: LectureInformation) { - view.date.text = data.updatedDate.toShortString() - view.subject.text = data.subject - view.detail.text = data.detailText - - if (data.isRead) { - view.date.typeface = Typeface.DEFAULT - view.subject.typeface = Typeface.DEFAULT - view.detail.typeface = Typeface.DEFAULT - } else { - view.date.typeface = Typeface.DEFAULT_BOLD - view.subject.typeface = Typeface.DEFAULT_BOLD - view.detail.typeface = Typeface.DEFAULT_BOLD - } - - view.setOnClickListener { onClick(data) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardMyClassAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardMyClassAdapter.kt deleted file mode 100644 index 812e7d9..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardMyClassAdapter.kt +++ /dev/null @@ -1,91 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.support.v7.util.DiffUtil -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.MyClass -import kotlinx.android.synthetic.main.list_small_my_class.view.* -import org.jetbrains.anko.backgroundColor - -/** - * MyClassAdapter on MainThread - * - * @see MyClassAdapter - */ -class DashboardMyClassAdapter( - private val context: Context, - private val onClick: (data: MyClass) -> Unit = {} -) : RecyclerView.Adapter() { - - private var currentList: List = emptyList() - - init { - setHasStableIds(true) - } - - override fun getItemCount() = currentList.size - - override fun getItemId(position: Int) = currentList[position].id - - fun submitList(newList: List) { - val oldList = currentList - - val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int { return oldList.size } - - override fun getNewListSize(): Int { return newList.size } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return MyClass.DIFF_CALLBACK.areItemsTheSame( - oldList[oldItemPosition], newList[newItemPosition] - ) - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return MyClass.DIFF_CALLBACK.areContentsTheSame( - currentList[oldItemPosition], newList[newItemPosition] - ) - } - }) - - currentList = newList - - result.dispatchUpdatesTo(this) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutInflater = LayoutInflater.from(context) - val view = layoutInflater.inflate(R.layout.list_small_my_class, parent, false) - - return ViewHolder(view, onClick) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.separator.visibility = if (position >= itemCount-1) View.GONE else View.VISIBLE - holder.bindTo(currentList[position]) - } - - class ViewHolder( - private val view: View, - private val onClick: (data: MyClass) -> Unit - ) : RecyclerView.ViewHolder(view) { - - val separator: View = view.separator - - fun bindTo(data: MyClass) { - view.color_header.backgroundColor = data.color - view.subject.text = data.subject - view.instructor.text = data.instructor - view.location.text = data.location - view.period.text = data.period.toString() - - view.instructor.visibility = if (data.instructor.isNotBlank()) View.VISIBLE else View.GONE - - view.setOnClickListener { onClick(data) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardNoticeAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardNoticeAdapter.kt deleted file mode 100644 index 90f1468..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/DashboardNoticeAdapter.kt +++ /dev/null @@ -1,103 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.graphics.Typeface -import android.support.v7.util.DiffUtil -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.Notice -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.android.synthetic.main.list_notice.view.* - -/** - * NoticeAdapter on MainThread - * - * @see NoticeAdapter - */ -class DashboardNoticeAdapter( - private val context: Context, - private val maxItemCount: Int, - private val onClick: (data: Notice) -> Unit, - private val onClickFavorite: (data: Notice) -> Unit -) : RecyclerView.Adapter() { - - private var currentList: List = emptyList() - - init { - setHasStableIds(true) - } - - override fun getItemCount() = currentList.size - - override fun getItemId(position: Int) = currentList[position].id - - fun submitList(list: List) { - val oldList = currentList - val newList = list.take(maxItemCount) - - val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int { return oldList.size } - - override fun getNewListSize(): Int { return newList.size } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return Notice.DIFF_CALLBACK.areItemsTheSame( - oldList[oldItemPosition], newList[newItemPosition] - ) - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return Notice.DIFF_CALLBACK.areContentsTheSame( - currentList[oldItemPosition], newList[newItemPosition] - ) - } - }) - - currentList = newList - - result.dispatchUpdatesTo(this) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutInflater = LayoutInflater.from(context) - val view = layoutInflater.inflate(R.layout.list_small_notice, parent, false) - - return ViewHolder(view, onClick, onClickFavorite) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bindTo(currentList[position]) - } - - class ViewHolder( - private val view: View, - private val onClick: (data: Notice) -> Unit, - private val onClickFavorite: (data: Notice) -> Unit - ) : RecyclerView.ViewHolder(view) { - - fun bindTo(data: Notice) { - view.date.text = data.createdDate.toShortString() - view.subject.text = data.title - - if (data.isFavorite) { - view.favorite_icon.setImageResource(R.drawable.ic_favorite_on) - } else { - view.favorite_icon.setImageResource(R.drawable.ic_favorite_off) - } - - if (data.hasRead) { - view.date.typeface = Typeface.DEFAULT - view.subject.typeface = Typeface.DEFAULT - } else { - view.date.typeface = Typeface.DEFAULT_BOLD - view.subject.typeface = Typeface.DEFAULT_BOLD - } - - view.setOnClickListener { onClick(data) } - view.favorite_icon.setOnClickListener { onClickFavorite(data) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureCancellationAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureCancellationAdapter.kt deleted file mode 100644 index 10b4d65..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureCancellationAdapter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.databinding.DataBindingUtil -import android.support.v7.recyclerview.extensions.ListAdapter -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.ViewGroup -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.LectureCancellation -import jp.kentan.studentportalplus.databinding.ItemLectureBinding - - -class LectureCancellationAdapter( - private val context: Context, - private val onClick: (data: LectureCancellation) -> Unit = {}) : - ListAdapter(LectureCancellation.DIFF_CALLBACK) { - - init { - setHasStableIds(true) - } - - override fun getItemId(position: Int) = getItem(position).id - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(context) - val binding = DataBindingUtil.inflate(inflater, R.layout.item_lecture, parent, false) - - return ViewHolder(binding, onClick) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - class ViewHolder( - private val binding: ItemLectureBinding, - private val onClick: (data: LectureCancellation) -> Unit) : RecyclerView.ViewHolder(binding.root) { - - fun bind(data: LectureCancellation) = binding.apply { - lecture = data - setOnClickListener { onClick(data) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureInformationAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureInformationAdapter.kt deleted file mode 100644 index 54abf40..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/LectureInformationAdapter.kt +++ /dev/null @@ -1,46 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.databinding.DataBindingUtil -import android.support.v7.recyclerview.extensions.ListAdapter -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.ViewGroup -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.LectureInformation -import jp.kentan.studentportalplus.databinding.ItemLectureBinding - - -class LectureInformationAdapter( - private val context: Context, - private val onClick: (data: LectureInformation) -> Unit = {} -) : ListAdapter(LectureInformation.DIFF_CALLBACK) { - - init { - setHasStableIds(true) - } - - override fun getItemId(position: Int) = getItem(position).id - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(context) - val binding = DataBindingUtil.inflate(inflater, R.layout.item_lecture, parent, false) - - return ViewHolder(binding, onClick) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - class ViewHolder( - private val binding: ItemLectureBinding, - private val onClick: (data: LectureInformation) -> Unit - ) : RecyclerView.ViewHolder(binding.root) { - - fun bind(data: LectureInformation) = binding.apply { - lecture = data - setOnClickListener { onClick(data) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/MyClassAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/MyClassAdapter.kt deleted file mode 100644 index 0716696..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/MyClassAdapter.kt +++ /dev/null @@ -1,152 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.support.v7.recyclerview.extensions.ListAdapter -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.ClassWeekType -import jp.kentan.studentportalplus.data.model.MyClass -import kotlinx.android.synthetic.main.grid_my_class.view.* -import org.jetbrains.anko.backgroundColor -import org.jetbrains.anko.find -import org.jetbrains.anko.sdk25.coroutines.onClick -import java.util.* - - -class MyClassAdapter( - private val context: Context, - private var viewType: Int, - private val onClick: (data: MyClass) -> Unit, - private val onAddClick: (period: Int, week: ClassWeekType) -> Unit -) : ListAdapter(MyClass.DIFF_CALLBACK) { - - companion object { - private const val TYPE_EMPTY = -1 - const val TYPE_GRID = 0 - const val TYPE_LIST = 1 - - private val PERIOD_MINUTES = intArrayOf(8 * 60 + 50, 10 * 60 + 30, 12 * 60 + 50, 14 * 60 + 30, 16 * 60 + 10, 17 * 60 + 50, 19 * 60 + 30) - } - - init { - setHasStableIds(true) - - if (viewType != TYPE_GRID && viewType != TYPE_LIST) { - throw IllegalArgumentException("Invalid ViewType: $viewType") - } - } - - fun setViewType(viewType: Int) { - if (viewType != TYPE_GRID && viewType != TYPE_LIST) { - throw IllegalArgumentException("Invalid ViewType: $viewType") - } - this.viewType = viewType - submitList(null) - } - - override fun getItemId(position: Int) = getItem(position).id - - override fun getItemViewType(position: Int) = if (getItemId(position) < 0) TYPE_EMPTY else viewType - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutInflater = LayoutInflater.from(context) - - val layoutId = when (viewType) { - TYPE_EMPTY -> R.layout.grid_my_class_empty - TYPE_GRID -> R.layout.grid_my_class - TYPE_LIST -> R.layout.list_my_class - else -> R.layout.list_my_class - } - - val view = layoutInflater.inflate(layoutId, parent, false) - - return ViewHolder(view, viewType, onClick, onAddClick) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bindTo(getItem(position)) - - if (viewType == TYPE_GRID) { - holder.setPercent(getGuidelinePercent(position)) - } - } - - private fun getGuidelinePercent(position: Int): Float { - val week = position % 5 + 2 - val period = position / 5 - - val now = Calendar.getInstance() - val todayWeek = now.get(Calendar.DAY_OF_WEEK) - - if (week < todayWeek || todayWeek == Calendar.SUNDAY) { - return 1f - } else if (week == todayWeek) { - val minutes = now.get(Calendar.MINUTE) + now.get(Calendar.HOUR_OF_DAY) * 60 - - val diff = (minutes - PERIOD_MINUTES[period]) - - if (diff > 0) { - return Math.min(diff / 90f, 1f) - } - } - - return 0f - } - - class ViewHolder( - private val view: View, - private val viewType: Int, - private val onClick: (data: MyClass) -> Unit, - private val onAddClick: (period: Int, week: ClassWeekType) -> Unit - ) : RecyclerView.ViewHolder(view) { - - fun bindTo(data: MyClass) { - if (viewType == TYPE_EMPTY) { - view.find(R.id.add_button).setOnClickListener { onAddClick(data.period, data.week) } - return - } - when (viewType) { - TYPE_LIST -> { - view.find(R.id.user_icon).setImageResource( - if (data.isUser) R.drawable.ic_lock_off else R.drawable.ic_lock_on - ) - view.find(R.id.color_header).backgroundColor = data.color - view.find(R.id.day_and_period).text = formatDayAndPeriod(data) - } - TYPE_GRID -> { - view.location.text = data.location - view.layout.backgroundColor = data.color - } - } - - view.subject.text = data.subject - view.instructor.text = data.instructor - - view.layout.onClick { onClick(data) } - } - - fun setPercent(ratio: Float) { - if (ratio > 0f) { - view.guideline.setGuidelinePercent(ratio) - view.mask_group.visibility = View.VISIBLE - } else { - view.mask_group.visibility = View.GONE - } - } - - private companion object { - fun formatDayAndPeriod(data: MyClass): String { - return if (data.week.hasPeriod()) { - data.week.displayName + data.period - } else { - data.week.displayName - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/NoticeAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/NoticeAdapter.kt deleted file mode 100644 index 3d12baa..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/adapter/NoticeAdapter.kt +++ /dev/null @@ -1,70 +0,0 @@ -package jp.kentan.studentportalplus.ui.adapter - -import android.content.Context -import android.graphics.Typeface -import android.support.v7.recyclerview.extensions.ListAdapter -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.model.Notice -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.android.synthetic.main.list_notice.view.* - - -class NoticeAdapter( - private val context: Context, - private val onClick: (data: Notice) -> Unit, - private val onClickFavorite: (data: Notice) -> Unit -) : ListAdapter(Notice.DIFF_CALLBACK) { - - init { - setHasStableIds(true) - } - - override fun getItemId(position: Int) = getItem(position).id - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutInflater = LayoutInflater.from(context) - - val view = layoutInflater.inflate(R.layout.list_notice, parent, false) - - return ViewHolder(view, onClick, onClickFavorite) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bindTo(getItem(position)) - } - - class ViewHolder( - private val view: View, - private val onClick: (data: Notice) -> Unit, - private val onClickFavorite: (data: Notice) -> Unit - ) : RecyclerView.ViewHolder(view) { - - fun bindTo(data: Notice) { - view.date.text = data.createdDate.toShortString() - view.subject.text = data.title - - if (data.isFavorite) { - view.favorite_icon.setImageResource(R.drawable.ic_favorite_on) - } else { - view.favorite_icon.setImageResource(R.drawable.ic_favorite_off) - } - - if (data.hasRead) { - view.date.typeface = Typeface.DEFAULT - view.subject.typeface = Typeface.DEFAULT - } else { - view.date.typeface = Typeface.DEFAULT_BOLD - view.subject.typeface = Typeface.DEFAULT_BOLD - } - - view.setOnClickListener { onClick(data) } - view.favorite_icon.setOnClickListener{ onClickFavorite(data) } - - view.instructor.text = data.detailText ?: data.link - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardFragment.kt new file mode 100644 index 0000000..fe115b5 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardFragment.kt @@ -0,0 +1,168 @@ +package jp.kentan.studentportalplus.ui.dashboard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.RecyclerView +import androidx.transition.ChangeBounds +import androidx.transition.Fade +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet +import dagger.android.support.AndroidSupportInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.model.Lecture +import jp.kentan.studentportalplus.databinding.FragmentDashboardBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.lecturecancel.detail.LectureCancelDetailActivity +import jp.kentan.studentportalplus.ui.lectureinfo.detail.LectureInfoDetailActivity +import jp.kentan.studentportalplus.ui.main.FragmentType +import jp.kentan.studentportalplus.ui.main.MainViewModel +import jp.kentan.studentportalplus.ui.myclass.detail.MyClassDetailActivity +import jp.kentan.studentportalplus.ui.notice.detail.NoticeDetailActivity +import javax.inject.Inject + +class DashboardFragment : Fragment() { + + companion object { + private val TRANSITION by lazy(LazyThreadSafetyMode.NONE) { + TransitionSet() + .setOrdering(TransitionSet.ORDERING_SEQUENTIAL) + .addTransition(ChangeBounds()) + .addTransition(Fade(Fade.IN)) + } + + fun newInstance() = DashboardFragment() + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var binding: FragmentDashboardBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_dashboard, container, false) + binding.setLifecycleOwner(this) + + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + AndroidSupportInjection.inject(this) + + val provider = ViewModelProvider(requireActivity(), viewModelFactory) + + val viewModel = provider.get(DashboardViewModel::class.java) + val mainViewModel = provider.get(MainViewModel::class.java) + + binding.viewModel = viewModel + binding.mainViewModel = mainViewModel + + val myClassAdapter = MyClassAdapter(layoutInflater, viewModel::onMyClassClick) + val lectureInfoAdapter = LectureAdapter(layoutInflater, viewModel::onLectureInfoClick) + val lectureCancelAdapter = LectureAdapter(layoutInflater, viewModel::onLectureCancelClick) + val noticeAdapter = NoticeAdapter(layoutInflater, viewModel::onNoticeItemClick, viewModel::onNoticeFavoriteClick) + + binding.apply { + timetableRecyclerView.setup(myClassAdapter) + lectureInfoRecyclerView.setup(lectureInfoAdapter) + lectureCancelRecyclerView.setup(lectureCancelAdapter) + noticeRecyclerView.setup(noticeAdapter) + } + + viewModel.subscribe(myClassAdapter, lectureInfoAdapter, lectureCancelAdapter, noticeAdapter) + + // Call MainViewModel::onAttachFragment + mainViewModel.onAttachFragment(FragmentType.DASHBOARD) + } + + private fun DashboardViewModel.subscribe( + myClassAdapter: MyClassAdapter, + lectureInfoAdapter: LectureAdapter, + lectureCancelAdapter: LectureAdapter, + noticeAdapter: NoticeAdapter + ) { + val fragment = this@DashboardFragment + + portalDataSet.observe(fragment, Observer { set -> + TransitionManager.beginDelayedTransition(binding.layout, TRANSITION) + + myClassAdapter.submitList(set.myClassList) + lectureInfoAdapter.submitList(set.lectureInfoList.take(DashboardViewModel.MAX_ITEM_SIZE), set.lectureInfoList.isInvisibleLastDivider()) + lectureCancelAdapter.submitList(set.lectureCancelList.take(DashboardViewModel.MAX_ITEM_SIZE), set.lectureCancelList.isInvisibleLastDivider()) + noticeAdapter.submitList(set.noticeList) + + if (set.myClassList.isNotEmpty()) { + val week = set.myClassList.first().week + binding.timetableHeader.text = getString(R.string.name_timetable, week.fullDisplayName) + binding.timetableCard.isVisible = true + } else { + binding.timetableCard.isVisible = false + } + + updateCardView( + binding.lectureInfoHeader, + binding.lectureInfoNote, + binding.lectureInfoButton, + R.string.name_lecture_info, + set.lectureInfoList.size) + + updateCardView( + binding.lectureCancelHeader, + binding.lectureCancelNote, + binding.lectureCancelButton, + R.string.name_lecture_cancel, + set.lectureCancelList.size) + }) + + startMyClassDetailActivity.observe(fragment, Observer { id -> + startActivity(MyClassDetailActivity.createIntent(requireContext(), id)) + }) + + startLectureInfoActivity.observe(fragment, Observer { id -> + startActivity(LectureInfoDetailActivity.createIntent(requireContext(), id)) + }) + startLectureCancelActivity.observe(fragment, Observer { id -> + startActivity(LectureCancelDetailActivity.createIntent(requireContext(), id)) + }) + startNoticeDetailActivity.observe(fragment, Observer { id -> + startActivity(NoticeDetailActivity.createIntent(requireContext(), id)) + }) + } + + private fun RecyclerView.setup(adapter: RecyclerView.Adapter<*>) { + isNestedScrollingEnabled = false + setAdapter(adapter) + setHasFixedSize(false) + } + + private fun updateCardView(header: TextView, note: TextView, button: TextView, titleId: Int, itemCount: Int) { + header.text = getString(titleId) + + when { + itemCount <= 0 -> { + note.isVisible = true + button.isVisible = false + } + itemCount > DashboardViewModel.MAX_ITEM_SIZE -> { + header.append(getString(R.string.text_more_item, itemCount - DashboardViewModel.MAX_ITEM_SIZE)) + + note.isVisible = false + button.isVisible = true + } + else -> { // 1-3 + button.isVisible = false + note.isVisible = false + } + } + } + + private fun List.isInvisibleLastDivider() = size <= DashboardViewModel.MAX_ITEM_SIZE +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..5920c92 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/DashboardViewModel.kt @@ -0,0 +1,70 @@ +package jp.kentan.studentportalplus.ui.dashboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.component.ClassWeek +import jp.kentan.studentportalplus.data.component.PortalDataSet +import jp.kentan.studentportalplus.data.model.MyClass +import jp.kentan.studentportalplus.data.model.Notice +import jp.kentan.studentportalplus.ui.SingleLiveData +import java.util.* + +class DashboardViewModel( + private val portalRepository: PortalRepository +) : ViewModel() { + + companion object { + const val MAX_ITEM_SIZE = 3 + } + + val portalDataSet: LiveData = Transformations.map(portalRepository.portalDataSet) { set -> + return@map PortalDataSet( + myClassList = set.myClassList.toTodayTimetable(), + lectureInfoList = set.lectureInfoList.filter { it.attend.isAttend() }, + lectureCancelList = set.lectureCancelList.filter { it.attend.isAttend() }, + noticeList = set.noticeList.take(MAX_ITEM_SIZE) + ) + } + + val startMyClassDetailActivity = SingleLiveData() + val startLectureInfoActivity = SingleLiveData() + val startLectureCancelActivity = SingleLiveData() + val startNoticeDetailActivity = SingleLiveData() + + fun onMyClassClick(id: Long) { + startMyClassDetailActivity.value = id + } + + fun onLectureInfoClick(id: Long) { + startLectureInfoActivity.value = id + } + + fun onLectureCancelClick(id: Long) { + startLectureCancelActivity.value = id + } + + fun onNoticeItemClick(id: Long) { + startNoticeDetailActivity.value = id + } + + fun onNoticeFavoriteClick(data: Notice) { + portalRepository.updateNotice(data.copy(isFavorite = !data.isFavorite)) + } + + private fun List.toTodayTimetable(): List { + val calender = Calendar.getInstance() + + val hour = calender.get(Calendar.HOUR_OF_DAY) + var dayOfWeek = calender.get(Calendar.DAY_OF_WEEK) + + // 午後8時以降は明日の時間割 + if (hour >= 20) { dayOfWeek++ } + + // 土、日は月に + val week = if (dayOfWeek in Calendar.MONDAY..Calendar.FRIDAY) ClassWeek.valueOf(dayOfWeek-1) else ClassWeek.MONDAY + + return filter { it.week == week } + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/LectureAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/LectureAdapter.kt new file mode 100644 index 0000000..d510f33 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/LectureAdapter.kt @@ -0,0 +1,75 @@ +package jp.kentan.studentportalplus.ui.dashboard + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.model.Lecture +import jp.kentan.studentportalplus.databinding.ItemSmallLectureBinding + +class LectureAdapter( + private val layoutInflater: LayoutInflater, + private val onClick: (Long) -> Unit +) : RecyclerView.Adapter() { + + private var currentList: List = emptyList() + private var isInvisibleLastDivider = false + + init { + setHasStableIds(true) + } + + fun submitList(newList: List, isInvisibleLastDivider: Boolean) { + val oldList = currentList + + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].id == newList[newItemPosition].id + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] && + newList.lastIndex != newItemPosition // Update last divider + } + }) + + currentList = newList + this.isInvisibleLastDivider = isInvisibleLastDivider + + result.dispatchUpdatesTo(this) + } + + override fun getItemCount() = currentList.size + + override fun getItemId(position: Int) = currentList[position].id + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ItemSmallLectureBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_small_lecture, parent, false) + + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(currentList[position], currentList.lastIndex <= position) + } + + inner class ViewHolder( + private val binding: ItemSmallLectureBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: Lecture, isLastItem: Boolean) { + binding.apply { + setData(data) + layout.setOnClickListener { onClick(data.id) } + divider.isVisible = !(isLastItem && isInvisibleLastDivider) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/MyClassAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/MyClassAdapter.kt new file mode 100644 index 0000000..b55a183 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/MyClassAdapter.kt @@ -0,0 +1,70 @@ +package jp.kentan.studentportalplus.ui.dashboard + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.model.MyClass +import jp.kentan.studentportalplus.databinding.ItemSmallMyClassBinding + +class MyClassAdapter( + private val layoutInflater: LayoutInflater, + private val onClick: (Long) -> Unit +) : RecyclerView.Adapter() { + + private var currentList: List = emptyList() + + init { + setHasStableIds(true) + } + + fun submitList(newList: List) { + val oldList = currentList + + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].id == newList[newItemPosition].id + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] + } + }) + + currentList = newList + + result.dispatchUpdatesTo(this) + } + + override fun getItemCount() = currentList.size + + override fun getItemId(position: Int) = currentList[position].id + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ItemSmallMyClassBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_small_my_class, parent, false) + + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(currentList[position], currentList.lastIndex <= position) + } + + inner class ViewHolder( + private val binding: ItemSmallMyClassBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: MyClass, isLastItem: Boolean) { + binding.data = data + binding.layout.setOnClickListener { onClick(data.id) } + binding.divider.isVisible = !isLastItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/NoticeAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/NoticeAdapter.kt new file mode 100644 index 0000000..25ea3ab --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/dashboard/NoticeAdapter.kt @@ -0,0 +1,76 @@ +package jp.kentan.studentportalplus.ui.dashboard + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.model.Notice +import jp.kentan.studentportalplus.databinding.ItemSmallNoticeBinding + +class NoticeAdapter( + private val layoutInflater: LayoutInflater, + private val onClick: (Long) -> Unit, + private val onFavoriteClick: (Notice) -> Unit +) : RecyclerView.Adapter() { + + private var currentList: List = emptyList() + + init { + setHasStableIds(true) + } + + fun submitList(newList: List) { + val oldList = currentList + + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return Notice.DIFF_CALLBACK.areItemsTheSame( + oldList[oldItemPosition], newList[newItemPosition] + ) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return Notice.DIFF_CALLBACK.areContentsTheSame( + oldList[oldItemPosition], newList[newItemPosition] + ) + } + }) + + currentList = newList + + result.dispatchUpdatesTo(this) + } + + override fun getItemCount() = currentList.size + + override fun getItemId(position: Int) = currentList[position].id + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ItemSmallNoticeBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_small_notice, parent, false) + + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(currentList[position]) + } + + inner class ViewHolder( + private val binding: ItemSmallNoticeBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: Notice) { + binding.apply { + setData(data) + layout.setOnClickListener { onClick(data.id) } + favoriteIcon.setOnClickListener { onFavoriteClick(data) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/DashboardFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/DashboardFragment.kt deleted file mode 100644 index 08e9fe3..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/DashboardFragment.kt +++ /dev/null @@ -1,159 +0,0 @@ -package jp.kentan.studentportalplus.ui.fragment - -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.transition.TransitionManager -import android.support.v4.app.Fragment -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import dagger.android.support.AndroidSupportInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.ui.* -import jp.kentan.studentportalplus.ui.adapter.DashboardLectureCancellationAdapter -import jp.kentan.studentportalplus.ui.adapter.DashboardLectureInformationAdapter -import jp.kentan.studentportalplus.ui.adapter.DashboardMyClassAdapter -import jp.kentan.studentportalplus.ui.adapter.DashboardNoticeAdapter -import jp.kentan.studentportalplus.ui.viewmodel.DashboardFragmentViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import kotlinx.android.synthetic.main.fragment_dashboard.* -import org.jetbrains.anko.support.v4.startActivity -import javax.inject.Inject - - -class DashboardFragment : Fragment() { - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_dashboard, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - AndroidSupportInjection.inject(this) - - val context = requireContext() - val activity = requireActivity() as MainActivity - - val viewModel = ViewModelProvider(activity, viewModelFactory).get(DashboardFragmentViewModel::class.java) - - val myClassAdapter = DashboardMyClassAdapter(context) { - val intent = MyClassActivity.createIntent(requireContext(), it.id) - startActivity(intent) - } - - val lectureInfoAdapter = DashboardLectureInformationAdapter(context , MAX_LIST_SIZE) { - startActivity("id" to it.id) - } - - val lectureCancelAdapter = DashboardLectureCancellationAdapter(context, MAX_LIST_SIZE) { - startActivity("id" to it.id) - } - - val noticeAdapter = DashboardNoticeAdapter(context, MAX_LIST_SIZE, onClick = { - startActivity("id" to it.id) - }, onClickFavorite = { - viewModel.onClickNoticeFavorite(it) - }) - - viewModel.portalDataSet.observe(this, Observer { set -> - if (set == null) { - return@Observer - } - - TransitionManager.beginDelayedTransition(dashboard_layout) - - if (set.myClassList.isNotEmpty()) { - timetable_card_view.visibility = View.VISIBLE - timetable_recycler_view.visibility = View.VISIBLE - timetable_header.text = getString(R.string.name_timetable, set.myClassList.first().week.fullDisplayName) - } else { - timetable_card_view.visibility = View.GONE - timetable_recycler_view.visibility = View.GONE - } - myClassAdapter.submitList(set.myClassList) - - updateCardView( - lecture_info_header, - lecture_info_note, - lecture_info_button, - R.string.name_lecture_info, - set.lectureInfoList.size) - - lectureInfoAdapter.submitList(set.lectureInfoList) - - updateCardView( - lecture_cancel_header, - lecture_cancel_note, - lecture_cancel_button, - R.string.name_lecture_cancel, - set.lectureCancelList.size) - - lectureCancelAdapter.submitList(set.lectureCancelList) - - noticeAdapter.submitList(set.noticeList) - }) - - initRecyclerView(timetable_recycler_view, myClassAdapter) - initRecyclerView(lecture_info_recycler_view, lectureInfoAdapter) - initRecyclerView(lecture_cancel_recycler_view, lectureCancelAdapter) - initRecyclerView(notice_recycler_view, noticeAdapter) - - lecture_info_note.text = getString(R.string.text_no_data, getString(R.string.name_lecture_info)) - lecture_cancel_note.text = getString(R.string.text_no_data, getString(R.string.name_lecture_cancel)) - - lecture_info_button.setOnClickListener { - activity.switchFragment(MainActivity.FragmentType.LECTURE_INFO) - } - lecture_cancel_button.setOnClickListener { - activity.switchFragment(MainActivity.FragmentType.LECTURE_CANCEL) - } - notice_button.setOnClickListener { - activity.switchFragment(MainActivity.FragmentType.NOTICE) - } - - activity.onAttachFragment(this) - } - - private fun initRecyclerView(view: RecyclerView, adapter: RecyclerView.Adapter<*>?) { - view.layoutManager = LinearLayoutManager(context) - view.adapter = adapter - view.isNestedScrollingEnabled = false - view.setHasFixedSize(false) - } - - private fun updateCardView(header: TextView, text: TextView, button: TextView, titleId: Int, itemCount: Int) { - header.text = getString(titleId) - - when { - itemCount <= 0 -> { - text.visibility = View.VISIBLE - button.visibility = View.GONE - } - itemCount > MAX_LIST_SIZE -> { - header.append(getString(R.string.text_more_item, itemCount - MAX_LIST_SIZE)) - - text.visibility = View.GONE - button.visibility = View.VISIBLE - } - else -> { - text.visibility = View.GONE - button.visibility = View.GONE - } - } - } - - companion object { - @JvmStatic - fun newInstance() = DashboardFragment() - - private const val MAX_LIST_SIZE = 3 - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureCancellationFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureCancellationFragment.kt deleted file mode 100644 index 5a1f0ed..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureCancellationFragment.kt +++ /dev/null @@ -1,159 +0,0 @@ -package jp.kentan.studentportalplus.ui.fragment - -import android.annotation.SuppressLint -import android.app.AlertDialog -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.v4.app.Fragment -import android.support.v4.content.ContextCompat -import android.support.v4.view.AsyncLayoutInflater -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.support.v7.widget.SearchView -import android.view.* -import android.widget.ArrayAdapter -import android.widget.CompoundButton -import dagger.android.support.AndroidSupportInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.LectureOrderType -import jp.kentan.studentportalplus.ui.LectureCancellationActivity -import jp.kentan.studentportalplus.ui.adapter.LectureCancellationAdapter -import jp.kentan.studentportalplus.ui.viewmodel.LectureCancellationFragmentViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.util.animateFadeInDelay -import kotlinx.android.synthetic.main.dialog_lecture_filter.view.* -import kotlinx.android.synthetic.main.fragment_lecture.* -import org.jetbrains.anko.support.v4.startActivity -import javax.inject.Inject - - -class LectureCancellationFragment : Fragment() { - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private lateinit var viewModel: LectureCancellationFragmentViewModel - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_lecture, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - AndroidSupportInjection.inject(this) - - setHasOptionsMenu(true) - - val context = requireContext() - val activity = requireActivity() - - viewModel = ViewModelProvider(activity, viewModelFactory).get(LectureCancellationFragmentViewModel::class.java) - - val adapter = LectureCancellationAdapter(context) { - startActivity("id" to it.id) - } - - viewModel.getResults().observe(this, Observer { - adapter.submitList(it) - - if (it == null || it.isEmpty()) { - note.animateFadeInDelay(context) - } else { - note.alpha = 0f - note.visibility = View.GONE - } - }) - - note.text = getString(R.string.msg_not_found, getString(R.string.name_lecture_cancel)) - - initRecyclerView(recycler_view, adapter) - - activity.onAttachFragment(this) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.search_and_filter, menu) - - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - - val keywords = viewModel.query.keywords - if (!keywords.isNullOrBlank()) { - searchItem.expandActionView() - searchView.setQuery(keywords, false) - searchView.clearFocus() - } - - searchView.queryHint = getString(R.string.hint_query_subject_and_instructor) - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ - override fun onQueryTextSubmit(query: String?) = true - - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.query = viewModel.query.copy(keywords = newText) - return true - } - }) - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - if (item?.itemId == R.id.action_list_filter) { - showFilterDialog() - } - return super.onOptionsItemSelected(item) - } - - private fun initRecyclerView(view: RecyclerView, adapter: RecyclerView.Adapter<*>?) { - view.layoutManager = LinearLayoutManager(context) - view.adapter = adapter - view.setHasFixedSize(true) - } - - @SuppressLint("InflateParams") - private fun showFilterDialog() { - val context = requireContext() - - val changeListener = CompoundButton.OnCheckedChangeListener{ button, checked -> - val color = ContextCompat.getColor(context, if (checked) R.color.chip_checked_text else R.color.chip_unchecked_text) - button.setTextColor(color) - } - - AsyncLayoutInflater(context).inflate(R.layout.dialog_lecture_filter, null) { view, _, _ -> - view.unread_check_box.setOnCheckedChangeListener(changeListener) - view.read_check_box.setOnCheckedChangeListener(changeListener) - view.attend_check_box.setOnCheckedChangeListener(changeListener) - - view.order_spinner.adapter = - ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, LectureOrderType.values()) - - val query = viewModel.query - view.order_spinner.setSelection(query.order.ordinal) - view.unread_check_box.isChecked = query.isUnread - view.read_check_box.isChecked = query.hasRead - view.attend_check_box.isChecked = query.isAttend - - AlertDialog.Builder(context) - .setView(view) - .setTitle(R.string.title_filter_dialog) - .setPositiveButton(R.string.action_apply) { _, _ -> - val order = view.order_spinner.selectedItem as LectureOrderType - - viewModel.query = viewModel.query.copy( - order = order, - isUnread = view.unread_check_box.isChecked, - hasRead = view.read_check_box.isChecked, - isAttend = view.attend_check_box.isChecked - ) - } - .setNegativeButton(R.string.action_cancel, null) - .create() - .show() - } - } - - companion object { - @JvmStatic - fun newInstance() = LectureCancellationFragment() - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureInformationFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureInformationFragment.kt deleted file mode 100644 index b8c75e9..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/LectureInformationFragment.kt +++ /dev/null @@ -1,160 +0,0 @@ -package jp.kentan.studentportalplus.ui.fragment - -import android.annotation.SuppressLint -import android.app.AlertDialog -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.v4.app.Fragment -import android.support.v4.content.ContextCompat -import android.support.v4.view.AsyncLayoutInflater -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.support.v7.widget.SearchView -import android.view.* -import android.widget.ArrayAdapter -import android.widget.CompoundButton -import dagger.android.support.AndroidSupportInjection - -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.LectureOrderType -import jp.kentan.studentportalplus.ui.LectureInformationActivity -import jp.kentan.studentportalplus.ui.adapter.LectureInformationAdapter -import jp.kentan.studentportalplus.ui.viewmodel.LectureInformationFragmentViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.util.animateFadeInDelay -import kotlinx.android.synthetic.main.dialog_lecture_filter.view.* -import kotlinx.android.synthetic.main.fragment_lecture.* -import org.jetbrains.anko.support.v4.startActivity -import javax.inject.Inject - - -class LectureInformationFragment : Fragment() { - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private lateinit var viewModel: LectureInformationFragmentViewModel - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_lecture, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - AndroidSupportInjection.inject(this) - - setHasOptionsMenu(true) - - val context = requireContext() - val activity = requireActivity() - - viewModel = ViewModelProvider(activity, viewModelFactory).get(LectureInformationFragmentViewModel::class.java) - - val adapter = LectureInformationAdapter(context) { - startActivity("id" to it.id) - } - - viewModel.getResults().observe(this, Observer { - adapter.submitList(it) - - if (it == null || it.isEmpty()) { - note.animateFadeInDelay(context) - } else { - note.alpha = 0f - note.visibility = View.GONE - } - }) - - note.text = getString(R.string.msg_not_found, getString(R.string.name_lecture_info)) - - initRecyclerView(recycler_view, adapter) - - activity.onAttachFragment(this) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.search_and_filter, menu) - - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - - val keywords = viewModel.query.keywords - if (!keywords.isNullOrBlank()) { - searchItem.expandActionView() - searchView.setQuery(keywords, false) - searchView.clearFocus() - } - - searchView.queryHint = getString(R.string.hint_query_subject_and_instructor) - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ - override fun onQueryTextSubmit(query: String?) = true - - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.query = viewModel.query.copy(keywords = newText) - return true - } - }) - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - if (item?.itemId == R.id.action_list_filter) { - showFilterDialog() - } - return super.onOptionsItemSelected(item) - } - - private fun initRecyclerView(view: RecyclerView, adapter: RecyclerView.Adapter<*>?) { - view.layoutManager = LinearLayoutManager(context) - view.adapter = adapter - view.setHasFixedSize(true) - } - - @SuppressLint("InflateParams") - private fun showFilterDialog() { - val context = requireContext() - - val changeListener = CompoundButton.OnCheckedChangeListener{ button, checked -> - val color = ContextCompat.getColor(context, if (checked) R.color.chip_checked_text else R.color.chip_unchecked_text) - button.setTextColor(color) - } - - AsyncLayoutInflater(context).inflate(R.layout.dialog_lecture_filter, null) { view, _, _ -> - view.unread_check_box.setOnCheckedChangeListener(changeListener) - view.read_check_box.setOnCheckedChangeListener(changeListener) - view.attend_check_box.setOnCheckedChangeListener(changeListener) - - view.order_spinner.adapter = - ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, LectureOrderType.values()) - - val query = viewModel.query - view.order_spinner.setSelection(query.order.ordinal) - view.unread_check_box.isChecked = query.isUnread - view.read_check_box.isChecked = query.hasRead - view.attend_check_box.isChecked = query.isAttend - - AlertDialog.Builder(context) - .setView(view) - .setTitle(R.string.title_filter_dialog) - .setPositiveButton(R.string.action_apply) { _, _ -> - val order = view.order_spinner.selectedItem as LectureOrderType - - viewModel.query = viewModel.query.copy( - order = order, - isUnread = view.unread_check_box.isChecked, - hasRead = view.read_check_box.isChecked, - isAttend = view.attend_check_box.isChecked - ) - } - .setNegativeButton(R.string.action_cancel, null) - .create() - .show() - } - } - - companion object { - @JvmStatic - fun newInstance() = LectureInformationFragment() - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/NoticeFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/NoticeFragment.kt deleted file mode 100644 index 2aeb930..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/NoticeFragment.kt +++ /dev/null @@ -1,159 +0,0 @@ -package jp.kentan.studentportalplus.ui.fragment - -import android.annotation.SuppressLint -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.os.Bundle -import android.support.v4.app.Fragment -import android.support.v4.content.ContextCompat -import android.support.v4.view.AsyncLayoutInflater -import android.support.v7.app.AlertDialog -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.support.v7.widget.SearchView -import android.view.* -import android.widget.ArrayAdapter -import android.widget.CompoundButton -import dagger.android.support.AndroidSupportInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.CreatedDateType -import jp.kentan.studentportalplus.ui.NoticeActivity -import jp.kentan.studentportalplus.ui.adapter.NoticeAdapter -import jp.kentan.studentportalplus.ui.viewmodel.NoticeFragmentViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import jp.kentan.studentportalplus.util.animateFadeInDelay -import kotlinx.android.synthetic.main.dialog_notice_filter.view.* -import kotlinx.android.synthetic.main.fragment_notice.* -import org.jetbrains.anko.support.v4.startActivity -import javax.inject.Inject - - -class NoticeFragment : Fragment() { - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private lateinit var viewModel: NoticeFragmentViewModel - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_notice, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - AndroidSupportInjection.inject(this) - - setHasOptionsMenu(true) - - val context = requireContext() - val activity = requireActivity() - - viewModel = ViewModelProvider(activity, viewModelFactory).get(NoticeFragmentViewModel::class.java) - - val adapter = NoticeAdapter(context, { - startActivity("id" to it.id) - }, { - viewModel.update(it.copy(isFavorite = !it.isFavorite)) - }) - - viewModel.getResults().observe(this, Observer { - adapter.submitList(it) - - if (it == null || it.isEmpty()) { - note.animateFadeInDelay(context) - } else { - note.alpha = 0f - note.visibility = View.GONE - } - }) - - note.text = getString(R.string.msg_not_found, getString(R.string.name_notice)) - - initRecyclerView(recycler_view, adapter) - - activity.onAttachFragment(this) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.search_and_filter, menu) - - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - searchView.queryHint = getString(R.string.hint_query_title) - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ - override fun onQueryTextSubmit(query: String?) = true - - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.query = viewModel.query.copy(keywords = newText) - return true - } - }) - - val keywords = viewModel.query.keywords - if (!keywords.isNullOrBlank()) { - searchItem.expandActionView() - searchView.setQuery(keywords, false) - searchView.clearFocus() - } - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - if (item?.itemId == R.id.action_list_filter) { - showFilterDialog() - } - return super.onOptionsItemSelected(item) - } - - private fun initRecyclerView(view: RecyclerView, adapter: RecyclerView.Adapter<*>?) { - view.layoutManager = LinearLayoutManager(context) - view.adapter = adapter - view.setHasFixedSize(true) - } - - @SuppressLint("InflateParams") - private fun showFilterDialog() { - val context = requireContext() - - val changeListener = CompoundButton.OnCheckedChangeListener{ button, checked -> - val color = ContextCompat.getColor(context, if (checked) R.color.chip_checked_text else R.color.chip_unchecked_text) - button.setTextColor(color) - } - - AsyncLayoutInflater(context).inflate(R.layout.dialog_notice_filter, null) { view, _, _ -> - - view.unread_check_box.setOnCheckedChangeListener(changeListener) - view.read_check_box.setOnCheckedChangeListener(changeListener) - view.favorite_check_box.setOnCheckedChangeListener(changeListener) - - view.created_date_spinner.adapter = - ArrayAdapter(context, android.R.layout.simple_list_item_1, CreatedDateType.values()) - - val query = viewModel.query - view.created_date_spinner.setSelection(query.type.ordinal) - view.unread_check_box.isChecked = query.isUnread - view.read_check_box.isChecked = query.hasRead - view.favorite_check_box.isChecked = query.isFavorite - - AlertDialog.Builder(context) - .setView(view) - .setTitle(R.string.title_filter_dialog) - .setPositiveButton(R.string.action_apply) { _, _ -> - viewModel.query = viewModel.query.copy( - type = view.created_date_spinner.selectedItem as CreatedDateType, - isUnread = view.unread_check_box.isChecked, - hasRead = view.read_check_box.isChecked, - isFavorite = view.favorite_check_box.isChecked - ) - } - .setNegativeButton(R.string.action_cancel, null) - .create() - .show() - } - } - - companion object { - @JvmStatic - fun newInstance() = NoticeFragment() - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/TimetableFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/TimetableFragment.kt deleted file mode 100644 index c655b94..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/fragment/TimetableFragment.kt +++ /dev/null @@ -1,176 +0,0 @@ -package jp.kentan.studentportalplus.ui.fragment - -import android.annotation.SuppressLint -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModelProvider -import android.graphics.Typeface -import android.os.Bundle -import android.support.transition.TransitionManager -import android.support.v4.app.Fragment -import android.support.v7.view.menu.MenuBuilder -import android.support.v7.widget.GridLayoutManager -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.PopupMenu -import android.view.* -import android.widget.TextView -import androidx.core.content.edit -import dagger.android.support.AndroidSupportInjection -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.ClassWeekType -import jp.kentan.studentportalplus.ui.MyClassActivity -import jp.kentan.studentportalplus.ui.adapter.MyClassAdapter -import jp.kentan.studentportalplus.ui.myclass.edit.MyClassEditActivity -import jp.kentan.studentportalplus.ui.viewmodel.TimetableFragmentViewModel -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import kotlinx.android.synthetic.main.fragment_timetable.* -import org.jetbrains.anko.find -import org.jetbrains.anko.support.v4.defaultSharedPreferences -import org.jetbrains.anko.support.v4.startActivity -import org.jetbrains.anko.textColorResource -import javax.inject.Inject - -class TimetableFragment : Fragment() { - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private lateinit var viewModel: TimetableFragmentViewModel - private lateinit var adapter: MyClassAdapter - private lateinit var layoutType: TimetableFragmentViewModel.LayoutType - - private val weekViewMap: Map by lazy { - mapOf( - ClassWeekType.MONDAY to monday_header, - ClassWeekType.TUESDAY to tuesday_header, - ClassWeekType.WEDNESDAY to wednesday_header, - ClassWeekType.THURSDAY to thursday_header, - ClassWeekType.FRIDAY to friday_header - ) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timetable, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - AndroidSupportInjection.inject(this) - - setHasOptionsMenu(true) - - val context = requireContext() - val activity = requireActivity() - - layoutType = TimetableFragmentViewModel.LayoutType.valueOf( - defaultSharedPreferences.getString("timetable_layout", TimetableFragmentViewModel.LayoutType.WEEK.name) - ) - if (layoutType == TimetableFragmentViewModel.LayoutType.WEEK) { - grid_layout.visibility = View.VISIBLE - list_recycler_view.visibility = View.GONE - } else { - grid_layout.visibility = View.GONE - list_recycler_view.visibility = View.VISIBLE - } - - viewModel = ViewModelProvider(activity, viewModelFactory).get(TimetableFragmentViewModel::class.java) - viewModel.setViewType(layoutType) - - adapter = MyClassAdapter(context, layoutType.viewType, { - startActivity(MyClassActivity.createIntent(context, it.id)) - }, { period: Int, week: ClassWeekType -> - startActivity(MyClassEditActivity.createIntent(context, week, period)) - }) - - viewModel.getResults().observe(this, Observer { - note.visibility = if (it == null || it.isEmpty()) View.VISIBLE else View.GONE - - val today = viewModel.getWeek() - weekViewMap.forEach { (week, view) -> - if (week == today) { - view.typeface = Typeface.DEFAULT_BOLD - view.textColorResource = R.color.colorAccent - } else { - view.typeface = Typeface.DEFAULT - view.textColorResource = R.color.colorPrimary - } - } - - if (layoutType == TimetableFragmentViewModel.LayoutType.WEEK) { - TransitionManager.beginDelayedTransition(grid_layout) - } - - adapter.submitList(it) - }) - - // Initialize RecyclerViews - grid_recycler_view.layoutManager = GridLayoutManager(context, 5) - grid_recycler_view.adapter = adapter - grid_recycler_view.isNestedScrollingEnabled = false - grid_recycler_view.setHasFixedSize(true) - grid_recycler_view.itemAnimator = null - - list_recycler_view.layoutManager = LinearLayoutManager(context) - list_recycler_view.adapter = adapter - list_recycler_view.setHasFixedSize(true) - - activity.onAttachFragment(this) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.timetable, menu) - } - - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.action_add -> startActivity("period" to 1, "week" to ClassWeekType.MONDAY) - R.id.action_switch_layout -> showLayoutSelectPopup() - } - return super.onOptionsItemSelected(item) - } - - @SuppressLint("RestrictedApi") - private fun showLayoutSelectPopup() { - val anchor: View = requireActivity().find(R.id.action_switch_layout) - - val popup = PopupMenu(requireContext(), anchor) - popup.menuInflater.inflate(R.menu.popup_switch_layout, popup.menu) - popup.setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.action_view_week -> { switchViewType(TimetableFragmentViewModel.LayoutType.WEEK) } - R.id.action_view_day -> { switchViewType(TimetableFragmentViewModel.LayoutType.DAY) } - } - return@setOnMenuItemClickListener true - } - popup.show() - - (popup.menu as MenuBuilder).setOptionalIconsVisible(true) - } - - private fun switchViewType(type: TimetableFragmentViewModel.LayoutType) { - if (type == layoutType) { - return - } - layoutType = type - - if (type == TimetableFragmentViewModel.LayoutType.WEEK) { - grid_layout.visibility = View.VISIBLE - list_recycler_view.visibility = View.GONE - } else { - grid_layout.visibility = View.GONE - list_recycler_view.visibility = View.VISIBLE - } - - adapter.setViewType(type.viewType) - viewModel.setViewType(type) - - defaultSharedPreferences.edit { - putString("timetable_layout", type.name) - } - } - - companion object { - @JvmStatic - fun newInstance() = TimetableFragment() - } -} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelAdapter.kt new file mode 100644 index 0000000..c61cf20 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelAdapter.kt @@ -0,0 +1,44 @@ +package jp.kentan.studentportalplus.ui.lecturecancel + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.model.LectureCancellation +import jp.kentan.studentportalplus.databinding.ItemLectureBinding + +class LectureCancelAdapter( + private val layoutInflater: LayoutInflater, + private val onClick: (Long) -> Unit +) : ListAdapter(LectureCancellation.DIFF_CALLBACK) { + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int) = getItem(position).id + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ItemLectureBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_lecture, parent, false) + + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: ItemLectureBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: LectureCancellation) { + binding.apply { + setData(data) + layout.setOnClickListener { onClick(data.id) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelFragment.kt new file mode 100644 index 0000000..3691011 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelFragment.kt @@ -0,0 +1,132 @@ +package jp.kentan.studentportalplus.ui.lecturecancel + +import android.os.Bundle +import android.view.* +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.DividerItemDecoration +import dagger.android.support.AndroidSupportInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.component.LectureQuery +import jp.kentan.studentportalplus.databinding.DialogLectureFilterBinding +import jp.kentan.studentportalplus.databinding.FragmentListBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.lecturecancel.detail.LectureCancelDetailActivity +import jp.kentan.studentportalplus.ui.main.FragmentType +import jp.kentan.studentportalplus.ui.main.MainViewModel +import javax.inject.Inject + +class LectureCancelFragment : Fragment() { + + companion object { + fun newInstance() = LectureCancelFragment() + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var binding: FragmentListBinding + private lateinit var viewModel: LectureCancelViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_list, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + AndroidSupportInjection.inject(this) + + setHasOptionsMenu(true) + + val provider = ViewModelProvider(requireActivity(), viewModelFactory) + + viewModel = provider.get(LectureCancelViewModel::class.java) + + val adapter = LectureCancelAdapter(layoutInflater, viewModel::onClick) + + binding.recyclerView.apply { + setAdapter(adapter) + setHasFixedSize(true) + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + + viewModel.subscribe(adapter) + + // Call MainViewModel::onAttachFragment + provider.get(MainViewModel::class.java) + .onAttachFragment(FragmentType.LECTURE_CANCEL) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.search_and_filter, menu) + + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.queryHint = getString(R.string.hint_query_subject_and_instructor) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ + override fun onQueryTextSubmit(query: String?) = true + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.onQueryTextChange(newText) + return true + } + }) + + val keyword = viewModel.query.keyword + if (!keyword.isNullOrBlank()) { + searchItem.expandActionView() + searchView.setQuery(keyword, false) + searchView.clearFocus() + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + if (item?.itemId == R.id.action_filter) { + showFilterDialog() + } + return super.onOptionsItemSelected(item) + } + + private fun showFilterDialog() { + val context = requireContext() + + val binding: DialogLectureFilterBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.dialog_lecture_filter, binding.root as ViewGroup, false) + + binding.apply { + orderSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, LectureQuery.Order.values()) + query = this@LectureCancelFragment.viewModel.query + } + + AlertDialog.Builder(context) + .setView(binding.root) + .setTitle(R.string.title_filter_dialog) + .setPositiveButton(R.string.action_apply) { _, _ -> + viewModel.onFilterApplyClick( + binding.orderSpinner.selectedItem as LectureQuery.Order, + binding.unreadChip.isChecked, + binding.readChip.isChecked, + binding.attendChip.isChecked + ) + } + .setNegativeButton(R.string.action_cancel, null) + .create() + .show() + } + + private fun LectureCancelViewModel.subscribe(adapter: LectureCancelAdapter) { + val fragment = this@LectureCancelFragment + + lectureCancelList.observe(fragment, Observer { adapter.submitList(it) }) + startDetailActivity.observe(fragment, Observer { id -> + startActivity(LectureCancelDetailActivity.createIntent(requireContext(), id)) + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelViewModel.kt new file mode 100644 index 0000000..9fea633 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/LectureCancelViewModel.kt @@ -0,0 +1,56 @@ +package jp.kentan.studentportalplus.ui.lecturecancel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.component.LectureQuery +import jp.kentan.studentportalplus.data.model.LectureCancellation +import jp.kentan.studentportalplus.ui.SingleLiveData + +class LectureCancelViewModel( + private val portalRepository: PortalRepository +) : ViewModel() { + + + private val queryLiveData = MutableLiveData() + var query = LectureQuery() + private set + + val lectureCancelList: LiveData> = Transformations.switchMap(queryLiveData) { + portalRepository.getLectureCancelList(it) + } + + val startDetailActivity = SingleLiveData() + + init { + queryLiveData.value = query + } + + fun onClick(id: Long) { + startDetailActivity.value = id + } + + fun onQueryTextChange(text: String?) { + query = query.copy(keyword = text) + + queryLiveData.value = query + } + + fun onFilterApplyClick( + order: LectureQuery.Order, + isUnread: Boolean, + isRead: Boolean, + isAttend: Boolean + ) { + query = query.copy( + order = order, + isUnread = isUnread, + isRead = isRead, + isAttend = isAttend + ) + + queryLiveData.value = query + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailActivity.kt new file mode 100644 index 0000000..44fc8e5 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailActivity.kt @@ -0,0 +1,104 @@ +package jp.kentan.studentportalplus.ui.lecturecancel.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.text.parseAsHtml +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.snackbar.Snackbar +import dagger.android.AndroidInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.databinding.ActivityLectureCancelDetailBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import javax.inject.Inject + +class LectureCancelDetailActivity : AppCompatActivity() { + + companion object { + private const val EXTRA_ID = "ID" + + fun createIntent(context: Context, id: Long) = + Intent(context, LectureCancelDetailActivity::class.java).apply { + putExtra(EXTRA_ID, id) + } + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + private val viewModel by lazy(LazyThreadSafetyMode.NONE) { + ViewModelProvider(this, viewModelFactory).get(LectureCancelDetailViewModel::class.java) + } + + private lateinit var binding: ActivityLectureCancelDetailBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_lecture_cancel_detail) + + AndroidInjection.inject(this) + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.setLifecycleOwner(this) + binding.viewModel = viewModel + + viewModel.subscribe() + viewModel.onActivityCreated(intent.getLongExtra(EXTRA_ID, -1)) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.share, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.action_share -> viewModel.onShareClick() + android.R.id.home -> finish() + } + + return super.onOptionsItemSelected(item) + } + + private fun LectureCancelDetailViewModel.subscribe() { + val activity = this@LectureCancelDetailActivity + + showAttendNotDialog.observe(activity, Observer { subject -> + AlertDialog.Builder(activity) + .setTitle(R.string.title_confirm) + .setMessage(getString(R.string.text_unregister_confirm, subject).parseAsHtml()) + .setPositiveButton(R.string.action_yes) { _, _ -> + viewModel.onAttendNotClick(subject) + } + .setNegativeButton(R.string.action_no, null) + .show() + }) + + snackbar.observe(activity, Observer { resId -> + Snackbar.make(binding.root, resId, Snackbar.LENGTH_SHORT) + .show() + }) + + indefiniteSnackbar.observe(activity, Observer { resId -> + val snackbar = Snackbar.make(binding.root, resId, Snackbar.LENGTH_INDEFINITE) + + snackbar.setAction(R.string.action_close) { snackbar.dismiss() } + .show() + }) + + share.observe(activity, Observer { startActivity(it) }) + + errorNotFound.observe(activity, Observer { + Toast.makeText(activity, R.string.error_not_found, Toast.LENGTH_LONG).show() + finish() + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailViewModel.kt new file mode 100644 index 0000000..64bb09b --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lecturecancel/detail/LectureCancelDetailViewModel.kt @@ -0,0 +1,101 @@ +package jp.kentan.studentportalplus.ui.lecturecancel.detail + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.* +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.component.LectureAttend +import jp.kentan.studentportalplus.data.model.LectureCancellation +import jp.kentan.studentportalplus.ui.SingleLiveData +import jp.kentan.studentportalplus.util.formatYearMonthDay +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.launch +import org.jsoup.Jsoup + +class LectureCancelDetailViewModel( + private val context: Application, + private val portalRepository: PortalRepository +) : AndroidViewModel(context) { + + private val idLiveData = MutableLiveData() + + val lectureCancel: LiveData = Transformations.switchMap(idLiveData) { id -> + portalRepository.getLectureCancel(id) + } + val showAttendNotDialog = SingleLiveData() + val snackbar = SingleLiveData() + val indefiniteSnackbar = SingleLiveData() + val share = SingleLiveData() + val errorNotFound = SingleLiveData() + + private val lectureCancelObserver = Observer { data -> + if (data == null) { + errorNotFound.value = Unit + } else if (!data.isRead) { + portalRepository.updateLectureCancel(data.copy(isRead = true)) + } + } + + init { + lectureCancel.observeForever(lectureCancelObserver) + } + + fun onActivityCreated(id: Long) { + idLiveData.value = id + } + + fun onAttendClick(data: LectureCancellation) { + if (data.attend.canAttend()) { + GlobalScope.launch { + val isSuccess = portalRepository.addToMyClass(data.copy(attend = LectureAttend.USER)).await() + + if (isSuccess) { + snackbar.postValue(R.string.msg_register_class) + } else { + indefiniteSnackbar.postValue(R.string.error_update) + } + } + } else { + showAttendNotDialog.value = data.subject + } + } + + fun onAttendNotClick(subject: String) { + GlobalScope.launch { + val isSuccess = portalRepository.deleteFromMyClass(subject).await() + + if (isSuccess) { + snackbar.postValue(R.string.msg_unregister_class) + } else { + indefiniteSnackbar.postValue(R.string.error_update) + } + } + } + + fun onShareClick() { + val data = lectureCancel.value ?: return + + val text = context.getString(R.string.share_lecture_cancel, + data.subject, + data.instructor, + data.week, + data.period, + data.cancelDate.formatYearMonthDay(), + Jsoup.parse(data.detailHtml).text(), + data.createdDate.formatYearMonthDay()) + + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, data.subject) + putExtra(Intent.EXTRA_TEXT, text.toString()) + } + + share.value = Intent.createChooser(intent, null) + } + + override fun onCleared() { + lectureCancel.removeObserver(lectureCancelObserver) + super.onCleared() + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoAdapter.kt new file mode 100644 index 0000000..a7126b1 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoAdapter.kt @@ -0,0 +1,44 @@ +package jp.kentan.studentportalplus.ui.lectureinfo + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.model.LectureInformation +import jp.kentan.studentportalplus.databinding.ItemLectureBinding + +class LectureInfoAdapter( + private val layoutInflater: LayoutInflater, + private val onClick: (Long) -> Unit +) : ListAdapter(LectureInformation.DIFF_CALLBACK) { + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int) = getItem(position).id + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ItemLectureBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_lecture, parent, false) + + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: ItemLectureBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: LectureInformation) { + binding.apply { + setData(data) + layout.setOnClickListener { onClick(data.id) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoFragment.kt new file mode 100644 index 0000000..2096d27 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoFragment.kt @@ -0,0 +1,132 @@ +package jp.kentan.studentportalplus.ui.lectureinfo + +import android.os.Bundle +import android.view.* +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.DividerItemDecoration +import dagger.android.support.AndroidSupportInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.component.LectureQuery +import jp.kentan.studentportalplus.databinding.DialogLectureFilterBinding +import jp.kentan.studentportalplus.databinding.FragmentListBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.lectureinfo.detail.LectureInfoDetailActivity +import jp.kentan.studentportalplus.ui.main.FragmentType +import jp.kentan.studentportalplus.ui.main.MainViewModel +import javax.inject.Inject + +class LectureInfoFragment : Fragment() { + + companion object { + fun newInstance() = LectureInfoFragment() + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var binding: FragmentListBinding + private lateinit var viewModel: LectureInfoViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_list, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + AndroidSupportInjection.inject(this) + + setHasOptionsMenu(true) + + val provider = ViewModelProvider(requireActivity(), viewModelFactory) + + viewModel = provider.get(LectureInfoViewModel::class.java) + + val adapter = LectureInfoAdapter(layoutInflater, viewModel::onClick) + + binding.recyclerView.apply { + setAdapter(adapter) + setHasFixedSize(true) + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + + viewModel.subscribe(adapter) + + // Call MainViewModel::onAttachFragment + provider.get(MainViewModel::class.java) + .onAttachFragment(FragmentType.LECTURE_INFO) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.search_and_filter, menu) + + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.queryHint = getString(R.string.hint_query_subject_and_instructor) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ + override fun onQueryTextSubmit(query: String?) = true + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.onQueryTextChange(newText) + return true + } + }) + + val keyword = viewModel.query.keyword + if (!keyword.isNullOrBlank()) { + searchItem.expandActionView() + searchView.setQuery(keyword, false) + searchView.clearFocus() + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + if (item?.itemId == R.id.action_filter) { + showFilterDialog() + } + return super.onOptionsItemSelected(item) + } + + private fun showFilterDialog() { + val context = requireContext() + + val binding: DialogLectureFilterBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.dialog_lecture_filter, binding.root as ViewGroup, false) + + binding.apply { + orderSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, LectureQuery.Order.values()) + query = this@LectureInfoFragment.viewModel.query + } + + AlertDialog.Builder(context) + .setView(binding.root) + .setTitle(R.string.title_filter_dialog) + .setPositiveButton(R.string.action_apply) { _, _ -> + viewModel.onFilterApplyClick( + binding.orderSpinner.selectedItem as LectureQuery.Order, + binding.unreadChip.isChecked, + binding.readChip.isChecked, + binding.attendChip.isChecked + ) + } + .setNegativeButton(R.string.action_cancel, null) + .create() + .show() + } + + private fun LectureInfoViewModel.subscribe(adapter: LectureInfoAdapter) { + val fragment = this@LectureInfoFragment + + lectureInfoList.observe(fragment, Observer { adapter.submitList(it) }) + startDetailActivity.observe(fragment, Observer { id -> + startActivity(LectureInfoDetailActivity.createIntent(requireContext(), id)) + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoViewModel.kt new file mode 100644 index 0000000..dd70030 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/LectureInfoViewModel.kt @@ -0,0 +1,55 @@ +package jp.kentan.studentportalplus.ui.lectureinfo + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.component.LectureQuery +import jp.kentan.studentportalplus.data.model.LectureInformation +import jp.kentan.studentportalplus.ui.SingleLiveData + +class LectureInfoViewModel( + private val portalRepository: PortalRepository +) : ViewModel() { + + private val queryLiveData = MutableLiveData() + var query = LectureQuery() + private set + + val lectureInfoList: LiveData> = Transformations.switchMap(queryLiveData) { + portalRepository.getLectureInfoList(it) + } + + val startDetailActivity = SingleLiveData() + + init { + queryLiveData.value = query + } + + fun onClick(id: Long) { + startDetailActivity.value = id + } + + fun onQueryTextChange(text: String?) { + query = query.copy(keyword = text) + + queryLiveData.value = query + } + + fun onFilterApplyClick( + order: LectureQuery.Order, + isUnread: Boolean, + isRead: Boolean, + isAttend: Boolean + ) { + query = query.copy( + order = order, + isUnread = isUnread, + isRead = isRead, + isAttend = isAttend + ) + + queryLiveData.value = query + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailActivity.kt new file mode 100644 index 0000000..f9d2035 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailActivity.kt @@ -0,0 +1,104 @@ +package jp.kentan.studentportalplus.ui.lectureinfo.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.text.parseAsHtml +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.snackbar.Snackbar +import dagger.android.AndroidInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.databinding.ActivityLectureInfoDetailBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import javax.inject.Inject + +class LectureInfoDetailActivity : AppCompatActivity() { + + companion object { + private const val EXTRA_ID = "ID" + + fun createIntent(context: Context, id: Long) = + Intent(context, LectureInfoDetailActivity::class.java).apply { + putExtra(EXTRA_ID, id) + } + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + private val viewModel by lazy(LazyThreadSafetyMode.NONE) { + ViewModelProvider(this, viewModelFactory).get(LectureInfoDetailViewModel::class.java) + } + + private lateinit var binding: ActivityLectureInfoDetailBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_lecture_info_detail) + + AndroidInjection.inject(this) + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.setLifecycleOwner(this) + binding.viewModel = viewModel + + viewModel.subscribe() + viewModel.onActivityCreated(intent.getLongExtra(EXTRA_ID, -1)) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.share, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.action_share -> viewModel.onShareClick() + android.R.id.home -> finish() + } + + return super.onOptionsItemSelected(item) + } + + private fun LectureInfoDetailViewModel.subscribe() { + val activity = this@LectureInfoDetailActivity + + showAttendNotDialog.observe(activity, Observer { subject -> + AlertDialog.Builder(activity) + .setTitle(R.string.title_confirm) + .setMessage(getString(R.string.text_unregister_confirm, subject).parseAsHtml()) + .setPositiveButton(R.string.action_yes) { _, _ -> + viewModel.onAttendNotClick(subject) + } + .setNegativeButton(R.string.action_no, null) + .show() + }) + + snackbar.observe(activity, Observer { resId -> + Snackbar.make(binding.root, resId, Snackbar.LENGTH_SHORT) + .show() + }) + + indefiniteSnackbar.observe(activity, Observer { resId -> + val snackbar = Snackbar.make(binding.root, resId, Snackbar.LENGTH_INDEFINITE) + + snackbar.setAction(R.string.action_close) { snackbar.dismiss() } + .show() + }) + + share.observe(activity, Observer { startActivity(it) }) + + errorNotFound.observe(activity, Observer { + Toast.makeText(activity, R.string.error_not_found, Toast.LENGTH_LONG).show() + finish() + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailViewModel.kt new file mode 100644 index 0000000..6a46d0f --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/lectureinfo/detail/LectureInfoDetailViewModel.kt @@ -0,0 +1,105 @@ +package jp.kentan.studentportalplus.ui.lectureinfo.detail + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.* +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.component.LectureAttend +import jp.kentan.studentportalplus.data.model.LectureInformation +import jp.kentan.studentportalplus.ui.SingleLiveData +import jp.kentan.studentportalplus.util.formatYearMonthDay +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.launch + +class LectureInfoDetailViewModel( + private val context: Application, + private val portalRepository: PortalRepository +) : AndroidViewModel(context) { + + private val idLiveData = MutableLiveData() + + val lectureInfo: LiveData = Transformations.switchMap(idLiveData) { id -> + portalRepository.getLectureInfo(id) + } + val showAttendNotDialog = SingleLiveData() + val snackbar = SingleLiveData() + val indefiniteSnackbar = SingleLiveData() + val share = SingleLiveData() + val errorNotFound = SingleLiveData() + + private val lectureInfoObserver = Observer { data -> + if (data == null) { + errorNotFound.value = Unit + } else if (!data.isRead) { + portalRepository.updateLectureInfo(data.copy(isRead = true)) + } + } + + init { + lectureInfo.observeForever(lectureInfoObserver) + } + + fun onActivityCreated(id: Long) { + idLiveData.value = id + } + + fun onAttendClick(data: LectureInformation) { + if (data.attend.canAttend()) { + GlobalScope.launch { + val isSuccess = portalRepository.addToMyClass(data.copy(attend = LectureAttend.USER)).await() + + if (isSuccess) { + snackbar.postValue(R.string.msg_register_class) + } else { + indefiniteSnackbar.postValue(R.string.error_update) + } + } + } else { + showAttendNotDialog.value = data.subject + } + } + + fun onAttendNotClick(subject: String) { + GlobalScope.launch { + val isSuccess = portalRepository.deleteFromMyClass(subject).await() + + if (isSuccess) { + snackbar.postValue(R.string.msg_unregister_class) + } else { + indefiniteSnackbar.postValue(R.string.error_update) + } + } + } + + fun onShareClick() { + val data = lectureInfo.value ?: return + + val text = StringBuilder( + context.getString(R.string.share_lecture_info, + data.subject, + data.instructor, + data.week, + data.period, + data.category, + data.detailText, + data.createdDate.formatYearMonthDay())) + + if (data.createdDate != data.updatedDate) { + text.append(context.getString(R.string.share_updated_date, data.updatedDate.formatYearMonthDay())) + } + + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, data.subject) + putExtra(Intent.EXTRA_TEXT, text.toString()) + } + + share.value = Intent.createChooser(intent, null) + } + + override fun onCleared() { + lectureInfo.removeObserver(lectureInfoObserver) + super.onCleared() + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginActivity.kt new file mode 100644 index 0000000..68db7f1 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginActivity.kt @@ -0,0 +1,117 @@ +package jp.kentan.studentportalplus.ui.login + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import dagger.android.AndroidInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.databinding.ActivityLoginBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.main.MainActivity +import jp.kentan.studentportalplus.util.hideSoftInput +import jp.kentan.studentportalplus.util.setAuthenticatedUser +import org.jetbrains.anko.defaultSharedPreferences +import javax.inject.Inject + +class LoginActivity : AppCompatActivity() { + + companion object { + private const val EXTRA_IS_LAUNCH_MAIN_ACTIVITY = "IS_LAUNCH_MAIN_ACTIVITY" + + fun createIntent(context: Context, isLaunchMainActivity: Boolean) = + Intent(context, LoginActivity::class.java).apply { + putExtra(EXTRA_IS_LAUNCH_MAIN_ACTIVITY, isLaunchMainActivity) + } + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var binding: ActivityLoginBinding + + private val viewModel by lazy(LazyThreadSafetyMode.NONE) { + ViewModelProvider(this, viewModelFactory).get(LoginViewModel::class.java) + } + + private var isLaunchMainActivity: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_login) + + AndroidInjection.inject(this) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + isLaunchMainActivity = intent.getBooleanExtra(EXTRA_IS_LAUNCH_MAIN_ACTIVITY, false) + + binding.setLifecycleOwner(this) + binding.password.setOnEditorActionListener { _, id, _ -> + if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { + viewModel.onLoginClick() + return@setOnEditorActionListener true + } + false + } + binding.viewModel = viewModel + + viewModel.subscribe() + viewModel.onActivityCreated() + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + finish() + return super.onOptionsItemSelected(item) + } + + override fun finish() { + viewModel.cancelLogin() + super.finish() + } + + private fun LoginViewModel.subscribe() { + val activity = this@LoginActivity + + loginSuccess.observe(activity, Observer { + activity.defaultSharedPreferences.setAuthenticatedUser(true) + + if (isLaunchMainActivity) { + startActivity(MainActivity.createIntent(activity, isSync = true)) + } + finish() + }) + + validation.observe(activity, Observer { result -> + var focusView: View? = null + + if (result.isEmptyUsername) { + binding.usernameLayout.error = getString(R.string.error_field_required) + focusView = binding.username + } else if (result.isInvalidUsername) { + binding.usernameLayout.error = getString(R.string.error_invalid_username) + focusView = binding.username + } + + if (result.isEmptyPassword) { + binding.passwordLayout.error = getString(R.string.error_field_required) + focusView = focusView ?: binding.password + } else if (result.isInvalidPassword) { + binding.passwordLayout.error = getString(R.string.error_invalid_password) + focusView = focusView ?: binding.password + } + + focusView?.requestFocus() + }) + + hideSoftInput.observe(activity, Observer { + activity.hideSoftInput() + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginViewModel.kt new file mode 100644 index 0000000..19f963a --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/login/LoginViewModel.kt @@ -0,0 +1,106 @@ +package jp.kentan.studentportalplus.ui.login + +import android.app.Application +import androidx.databinding.Observable +import androidx.databinding.ObservableBoolean +import androidx.databinding.ObservableField +import androidx.lifecycle.AndroidViewModel +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.shibboleth.ShibbolethClient +import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider +import jp.kentan.studentportalplus.ui.SingleLiveData +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.async +import kotlinx.coroutines.experimental.launch + +class LoginViewModel( + private val context: Application, + private val provider: ShibbolethDataProvider +) : AndroidViewModel(context) { + + val loading = ObservableBoolean() + val username = ObservableField() + val password = ObservableField() + val message = ObservableField() + + val isEnabledErrorUsername = SingleLiveData() + val isEnabledErrorPassword = SingleLiveData() + + val loginSuccess = SingleLiveData() + val validation = SingleLiveData() + val hideSoftInput = SingleLiveData() + + private var loginJob: Job? = null + + init { + username.setErrorCancelCallback(isEnabledErrorUsername) + password.setErrorCancelCallback(isEnabledErrorPassword) + } + + fun onActivityCreated() { + val username = provider.getUsername() ?: return + + this.username.set(username) + } + + fun cancelLogin() { + loginJob?.cancel() + } + + fun onLoginClick() { + val username = username.get() ?: "" + val password = password.get() ?: "" + + var result = ValidationResult() + + if (password.isEmpty()) { + result = result.copy(isEmptyPassword = true) + } else if (!password.isValidPassword()) { + result = result.copy(isInvalidPassword = true) + } + + if (username.isEmpty()) { + result = result.copy(isEmptyUsername = true) + } else if (!username.isValidUsername()) { + result = result.copy(isInvalidUsername = true) + } + + if (result.isError) { + validation.value = result + return + } + + login(username, password) + } + + private fun login(username: String, password: String) { + hideSoftInput.value = Unit + loginJob?.cancel() + + loginJob = GlobalScope.launch { + loading.set(true) + + val (isSuccess, errorMessage) = async { + ShibbolethClient(this@LoginViewModel.context, provider).auth(username, password) + }.await() + + if (isSuccess) { + loginSuccess.postValue(Unit) + } else { + message.set(errorMessage ?: this@LoginViewModel.context.getString(R.string.error_unknown)) + loading.set(false) + } + } + } + + private fun ObservableField.setErrorCancelCallback(error: SingleLiveData) { + addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { error.value = false } + }) + } + + private fun String.isValidUsername() = startsWith('b') || startsWith('m') || startsWith('d') + + private fun String.isValidPassword() = length in 8..24 +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/login/ValidationResult.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/login/ValidationResult.kt new file mode 100644 index 0000000..67aad4d --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/login/ValidationResult.kt @@ -0,0 +1,11 @@ +package jp.kentan.studentportalplus.ui.login + +data class ValidationResult( + val isEmptyUsername: Boolean = false, + val isInvalidUsername: Boolean = false, + val isEmptyPassword: Boolean = false, + val isInvalidPassword: Boolean = false +) { + val isError: Boolean + get() = isEmptyUsername || isInvalidUsername || isEmptyPassword || isInvalidPassword +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/main/FragmentType.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/main/FragmentType.kt new file mode 100644 index 0000000..f0b2778 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/main/FragmentType.kt @@ -0,0 +1,25 @@ +package jp.kentan.studentportalplus.ui.main + +import androidx.annotation.StringRes +import jp.kentan.studentportalplus.R + +enum class FragmentType( + @StringRes val titleResId: Int, + val menuItemId: Int +) { + DASHBOARD( + R.string.title_fragment_dashboard, + R.id.nav_dashboard), + TIMETABLE( + R.string.title_fragment_timetable, + R.id.nav_timetable), + LECTURE_INFO( + R.string.title_fragment_lecture_info, + R.id.nav_lecture_info), + LECTURE_CANCEL( + R.string.title_fragment_lecture_cancel, + R.id.nav_lecture_cancel), + NOTICE( + R.string.title_fragment_notice, + R.id.nav_notice) +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/main/MainActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/main/MainActivity.kt new file mode 100644 index 0000000..025a9a4 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/main/MainActivity.kt @@ -0,0 +1,207 @@ +package jp.kentan.studentportalplus.ui.main + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.TextView +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.GravityCompat +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.FragmentTransaction +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.snackbar.Snackbar +import dagger.android.AndroidInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.databinding.ActivityMainBinding +import jp.kentan.studentportalplus.notification.SyncScheduler +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.dashboard.DashboardFragment +import jp.kentan.studentportalplus.ui.lecturecancel.LectureCancelFragment +import jp.kentan.studentportalplus.ui.lectureinfo.LectureInfoFragment +import jp.kentan.studentportalplus.ui.login.LoginActivity +import jp.kentan.studentportalplus.ui.notice.NoticeFragment +import jp.kentan.studentportalplus.ui.setting.SettingsActivity +import jp.kentan.studentportalplus.ui.timetable.TimetableFragment +import jp.kentan.studentportalplus.ui.welcome.WelcomeActivity +import jp.kentan.studentportalplus.util.isAuthenticatedUser +import jp.kentan.studentportalplus.util.isEnabledDetailError +import org.jetbrains.anko.defaultSharedPreferences +import javax.inject.Inject + +class MainActivity : AppCompatActivity() { + + companion object { + private const val EXTRA_IS_SYNC = "IS_SYNC" + private const val EXTRA_FRAGMENT = "FRAGMENT" + + fun createIntent(context: Context, isSync: Boolean = false, fragment: FragmentType? = null) = + Intent(context, MainActivity::class.java).apply { + putExtra(EXTRA_IS_SYNC, isSync) + if (fragment != null) { putExtra(EXTRA_FRAGMENT, fragment.name) } + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var binding: ActivityMainBinding + private lateinit var viewModel: MainViewModel + + private var finishSnackbar: Snackbar? = null + + private val fragmentMap by lazy(LazyThreadSafetyMode.NONE) { + mapOf( + FragmentType.DASHBOARD to DashboardFragment.newInstance(), + FragmentType.TIMETABLE to TimetableFragment.newInstance(), + FragmentType.LECTURE_INFO to LectureInfoFragment.newInstance(), + FragmentType.LECTURE_CANCEL to LectureCancelFragment.newInstance(), + FragmentType.NOTICE to NoticeFragment.newInstance() + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (!defaultSharedPreferences.isAuthenticatedUser()) { + startWelcomeActivity() + return + } + + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + AndroidInjection.inject(this) + + setSupportActionBar(binding.appBar.toolbar) + + viewModel = ViewModelProvider(this, viewModelFactory).get(MainViewModel::class.java) + + binding.setLifecycleOwner(this) + binding.apply { + val toggle = ActionBarDrawerToggle( + this@MainActivity, drawerLayout, appBar.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) + drawerLayout.addDrawerListener(toggle) + toggle.syncState() + + appBar.refreshLayout.setProgressBackgroundColorSchemeResource(R.color.colorAccent) + appBar.refreshLayout.setColorSchemeResources(R.color.grey_100) + + viewModel = this@MainActivity.viewModel + } + + val fragment = when { + intent.hasExtra(EXTRA_FRAGMENT) -> { + val name = intent.getStringExtra(EXTRA_FRAGMENT) + fragmentMap[FragmentType.valueOf(name)] + } + supportFragmentManager.fragments.isEmpty() -> DashboardFragment.newInstance() + else -> null + } + + if (fragment != null) { + supportFragmentManager.beginTransaction() + .replace(R.id.container, fragment) + .commit() + } + + SyncScheduler(this).scheduleIfNeeded() + + viewModel.subscribe() + viewModel.onActivityCreated(intent.getBooleanExtra(EXTRA_IS_SYNC, false)) + } + + override fun onBackPressed() { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START) + } else { + if (supportFragmentManager.backStackEntryCount <= 0) { + if (viewModel.canFinish()) { + finishSnackbar?.dismiss() + finish() + } + return + } + + super.onBackPressed() + } + } + + private fun MainViewModel.subscribe() { + val activity = this@MainActivity + + replaceFragment.observe(activity, Observer { + val newFragment = fragmentMap[it] ?: return@Observer + + supportFragmentManager.beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .replace(R.id.container, newFragment) + .addToBackStack(null) + .commit() + }) + + attachFragment.observe(activity, Observer { it.attach() }) + + user.observe(activity, Observer { user -> + binding.navView.getHeaderView(0).apply { + findViewById(R.id.name).text = user.name + findViewById(R.id.username).text = user.username + } + }) + + closeDrawer.observe(activity, Observer { + binding.drawerLayout.closeDrawer(GravityCompat.START) + }) + + openMap.observe(activity, Observer { MapHelper.open(activity, it) }) + + startSettingsActivity.observe(activity, Observer { + startActivity(Intent(activity, SettingsActivity::class.java)) + }) + + indefiniteSnackbar.observe(activity, Observer { (message, isAuthError) -> + val snackbar = Snackbar.make(binding.root, R.string.error_unknown, Snackbar.LENGTH_INDEFINITE) + + if (isAuthError) { + if (message == null) { + snackbar.setText(R.string.msg_request_shibboleth_data) + } else { + snackbar.setText("$message\n${getString(R.string.msg_request_shibboleth_data)}") + } + + snackbar.setAction(R.string.action_login) { + startActivity(LoginActivity.createIntent(activity, true)) + } + } else { + if (message != null && defaultSharedPreferences.isEnabledDetailError()) { + snackbar.setText(message) + } else { + snackbar.setText(R.string.error_failed_to_sync) + } + + snackbar.setAction(R.string.action_close) { snackbar.dismiss() } + } + + snackbar.show() + }) + + finishSnackbar.observe(activity, Observer { callback -> + val snackbar = Snackbar.make(binding.root, getString(R.string.msg_back_to_exit), Snackbar.LENGTH_LONG) + .addCallback(callback) + + snackbar.show() + + activity.finishSnackbar = snackbar + }) + } + + private fun startWelcomeActivity() { + startActivity(WelcomeActivity.createIntent(this)) + } + + private fun FragmentType.attach() { + title = getString(titleResId) + binding.navView.menu.findItem(menuItemId).isChecked = true + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/main/MainViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/main/MainViewModel.kt new file mode 100644 index 0000000..518105b --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/main/MainViewModel.kt @@ -0,0 +1,123 @@ +package jp.kentan.studentportalplus.ui.main + +import android.util.Log +import android.view.MenuItem +import androidx.databinding.ObservableBoolean +import androidx.lifecycle.ViewModel +import com.google.android.material.navigation.NavigationView +import com.google.android.material.snackbar.Snackbar +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.UserRepository +import jp.kentan.studentportalplus.data.shibboleth.ShibbolethAuthenticationException +import jp.kentan.studentportalplus.ui.SingleLiveData +import kotlinx.coroutines.experimental.Dispatchers +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.android.Main +import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.experimental.launch + + +class MainViewModel( + private val portalRepository: PortalRepository, + userRepository: UserRepository +) : ViewModel(), NavigationView.OnNavigationItemSelectedListener { + + val attachFragment = SingleLiveData() + val replaceFragment = SingleLiveData() + val startSettingsActivity = SingleLiveData() + + val user = userRepository.getUser() + val isSyncing = ObservableBoolean() + val openMap = SingleLiveData() + val closeDrawer = SingleLiveData() + val indefiniteSnackbar = SingleLiveData>() + val finishSnackbar = SingleLiveData() + + private var currentFragment = FragmentType.DASHBOARD + private var canFinish = false + + fun onActivityCreated(isSync: Boolean) { + portalRepository.loadFromDb() + + if (isSync) { onRefresh() } + } + + fun onRefresh() { + isSyncing.set(true) + + GlobalScope.launch { + try { + portalRepository.sync().await() + } catch (e: Exception) { + indefiniteSnackbar.postValue(Pair(e.message, e is ShibbolethAuthenticationException)) + Log.e(javaClass.simpleName, "Failed to refresh", e) + } + isSyncing.set(false) + } + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + closeDrawer.value = Unit + + GlobalScope.launch(Dispatchers.Main) { + delay(300) + + when (item.itemId) { + R.id.nav_dashboard -> submitFragmentIfNeeded(FragmentType.DASHBOARD) + R.id.nav_timetable -> submitFragmentIfNeeded(FragmentType.TIMETABLE) + R.id.nav_lecture_info -> submitFragmentIfNeeded(FragmentType.LECTURE_INFO) + R.id.nav_lecture_cancel -> submitFragmentIfNeeded(FragmentType.LECTURE_CANCEL) + R.id.nav_notice -> submitFragmentIfNeeded(FragmentType.NOTICE) + R.id.nav_campus_map -> openMap.value = MapHelper.Type.CAMPUS + R.id.nav_room_map -> openMap.value = MapHelper.Type.ROOM + R.id.nav_setting -> startSettingsActivity.value = Unit + } + } + + return true + } + + fun onLectureInfoButtonClick() { + submitFragmentIfNeeded(FragmentType.LECTURE_INFO) + } + + fun onLectureCancelButtonClick() { + submitFragmentIfNeeded(FragmentType.LECTURE_CANCEL) + } + + fun onNoticeButtonClick() { + submitFragmentIfNeeded(FragmentType.NOTICE) + } + + fun onAttachFragment(type: FragmentType) { + currentFragment = type + attachFragment.value = type + } + + fun canFinish(): Boolean { + if (!canFinish) { + canFinish = true + + finishSnackbar.value = object : Snackbar.Callback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + canFinish = false + super.onDismissed(transientBottomBar, event) + } + } + + return false + } + + return true + } + + private fun submitFragmentIfNeeded(type: FragmentType) { + if (currentFragment != type) { + currentFragment = type + + replaceFragment.value = type + attachFragment.value = type + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/main/MapHelper.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/main/MapHelper.kt new file mode 100644 index 0000000..6f98a1c --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/main/MapHelper.kt @@ -0,0 +1,36 @@ +package jp.kentan.studentportalplus.ui.main + +import android.content.Context +import androidx.core.net.toUri +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.util.CustomTabsHelper +import jp.kentan.studentportalplus.util.buildCustomTabsIntent +import jp.kentan.studentportalplus.util.isEnabledPdfOpenWithGdocs +import org.jetbrains.anko.defaultSharedPreferences + +class MapHelper { + + enum class Type { CAMPUS, ROOM } + + companion object { + fun open(context: Context, type: Type) { + val url = when (type) { + Type.CAMPUS -> { + context.getString(R.string.url_campus_map) + } + Type.ROOM -> { + if (context.defaultSharedPreferences.isEnabledPdfOpenWithGdocs()) { + context.getString(R.string.url_gdocs, context.getString(R.string.url_room_map)) + } else { + context.getString(R.string.url_room_map) + } + } + } + + context.buildCustomTabsIntent().run { + intent.`package` = CustomTabsHelper.getPackageNameToUse(context) + launchUrl(context, url.toUri()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailActivity.kt new file mode 100644 index 0000000..9380788 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailActivity.kt @@ -0,0 +1,111 @@ +package jp.kentan.studentportalplus.ui.myclass.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.text.parseAsHtml +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.snackbar.Snackbar +import dagger.android.AndroidInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.databinding.ActivityMyClassDetailBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.myclass.edit.MyClassEditActivity +import javax.inject.Inject + +class MyClassDetailActivity : AppCompatActivity() { + + companion object { + private const val EXTRA_ID = "ID" + + fun createIntent(context: Context, id: Long) = + Intent(context, MyClassDetailActivity::class.java).apply { + putExtra(EXTRA_ID, id) + } + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + private val viewModel by lazy(LazyThreadSafetyMode.NONE) { + ViewModelProvider(this, viewModelFactory).get(MyClassDetailViewModel::class.java) + } + + private lateinit var binding: ActivityMyClassDetailBinding + + private var isEnabledOptionMenu = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_my_class_detail) + + AndroidInjection.inject(this) + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.setLifecycleOwner(this) + binding.viewModel = viewModel + + viewModel.subscribe() + viewModel.onActivityCreated(intent.getLongExtra(EXTRA_ID, -1)) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + if (isEnabledOptionMenu) { + menuInflater.inflate(R.menu.delete, menu) + } + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.action_delete -> viewModel.onDeleteClick() + android.R.id.home -> finish() + } + + return super.onOptionsItemSelected(item) + } + + private fun MyClassDetailViewModel.subscribe() { + val activity = this@MyClassDetailActivity + + startEditActivity.observe(activity, Observer { id -> + startActivity(MyClassEditActivity.createIntent(activity, id)) + }) + + finishActivity.observe(activity, Observer { finish() }) + + enabledDeleteOptionMenu.observe(activity, Observer { isEnabled -> + isEnabledOptionMenu = isEnabled + invalidateOptionsMenu() + }) + + showDeleteDialog.observe(activity, Observer { subject -> + AlertDialog.Builder(activity) + .setTitle(R.string.title_delete) + .setMessage(getString(R.string.text_delete_confirm, subject).parseAsHtml()) + .setPositiveButton(R.string.action_yes) { _, _ -> viewModel.onDeleteConfirmClick(subject) } + .setNegativeButton(R.string.action_no, null) + .show() + }) + + errorDelete.observe(activity, Observer { _ -> + val snackbar = Snackbar.make(binding.root, R.string.error_delete, Snackbar.LENGTH_INDEFINITE) + + snackbar.setAction(R.string.action_close) { snackbar.dismiss() } + .show() + }) + + errorNotFound.observe(activity, Observer { + Toast.makeText(activity, R.string.error_not_found, Toast.LENGTH_LONG).show() + finish() + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailViewModel.kt new file mode 100644 index 0000000..96e6779 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/detail/MyClassDetailViewModel.kt @@ -0,0 +1,69 @@ +package jp.kentan.studentportalplus.ui.myclass.detail + +import androidx.lifecycle.* +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.model.MyClass +import jp.kentan.studentportalplus.ui.SingleLiveData +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.launch + +class MyClassDetailViewModel( + private val portalRepository: PortalRepository +) : ViewModel() { + + private val idLiveData = MutableLiveData() + + val myClass: LiveData = Transformations.switchMap(idLiveData) { id -> + portalRepository.getMyClass(id, true) + } + + private val myClassObserver = Observer { data -> + if (data == null) { + errorNotFound.value = Unit + } else { + enabledDeleteOptionMenu.value = data.isUser + } + } + + val startEditActivity = SingleLiveData() + val finishActivity = SingleLiveData() + val enabledDeleteOptionMenu = SingleLiveData() + val showDeleteDialog = SingleLiveData() + val errorDelete = SingleLiveData() + val errorNotFound = SingleLiveData() + + init { + myClass.observeForever(myClassObserver) + } + + fun onActivityCreated(id: Long) { + idLiveData.value = id + } + + fun onEditClick(data: MyClass) { + startEditActivity.value = data.id + } + + fun onDeleteClick() { + val data = myClass.value ?: return + + showDeleteDialog.value = data.subject + } + + fun onDeleteConfirmClick(subject: String) { + GlobalScope.launch { + val isSuccess = portalRepository.deleteFromMyClass(subject).await() + + if (isSuccess) { + finishActivity.postValue(Unit) + } else { + errorDelete.postValue(Unit) + } + } + } + + override fun onCleared() { + myClass.removeObserver(myClassObserver) + super.onCleared() + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditActivity.kt index 13640ea..875a9ae 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditActivity.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditActivity.kt @@ -1,52 +1,45 @@ package jp.kentan.studentportalplus.ui.myclass.edit -import android.arch.lifecycle.ViewModelProvider import android.content.Context import android.content.Intent -import android.databinding.DataBindingUtil import android.os.Bundle -import android.support.v7.app.AlertDialog -import android.support.v7.app.AppCompatActivity import android.view.Menu import android.view.MenuItem import android.view.View -import com.android.colorpicker.ColorPickerDialog -import com.android.colorpicker.ColorPickerSwatch +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import dagger.android.AndroidInjection import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.ClassColor -import jp.kentan.studentportalplus.data.component.ClassWeekType +import jp.kentan.studentportalplus.data.component.ClassWeek import jp.kentan.studentportalplus.databinding.ActivityMyClassEditBinding -import jp.kentan.studentportalplus.ui.span.CustomTitle -import jp.kentan.studentportalplus.ui.viewmodel.ViewModelFactory -import org.jetbrains.anko.longToast +import jp.kentan.studentportalplus.ui.ViewModelFactory import javax.inject.Inject -class MyClassEditActivity : AppCompatActivity(), MyClassEditNavigator { +class MyClassEditActivity : AppCompatActivity() { companion object { - private const val EXTRA_ID = "id" - private const val EXTRA_WEEK = "week" - private const val EXTRA_PERIOD = "period" - private const val EXTRA_MODE = "mode" + private const val EXTRA_ID = "ID" + private const val EXTRA_WEEK = "WEEK" + private const val EXTRA_PERIOD = "PERIOD" fun createIntent(context: Context, id: Long) = Intent(context, MyClassEditActivity::class.java).apply { putExtra(EXTRA_ID, id) - putExtra(EXTRA_MODE, MyClassEditViewModel.Mode.EDIT) } - fun createIntent(context: Context, week: ClassWeekType, period: Int) = + fun createIntent(context: Context, week: ClassWeek, period: Int) = Intent(context, MyClassEditActivity::class.java).apply { putExtra(EXTRA_WEEK, week) putExtra(EXTRA_PERIOD, period) - putExtra(EXTRA_MODE, MyClassEditViewModel.Mode.ADD) } } @Inject lateinit var viewModelFactory: ViewModelFactory - private val viewModel by lazy(LazyThreadSafetyMode.NONE) { ViewModelProvider(this, viewModelFactory).get(MyClassEditViewModel::class.java) } @@ -55,15 +48,11 @@ class MyClassEditActivity : AppCompatActivity(), MyClassEditNavigator { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_my_class_edit) AndroidInjection.inject(this) - val mode = intent.getSerializableExtra(EXTRA_MODE) as MyClassEditViewModel.Mode - supportActionBar?.apply { - title = CustomTitle(this@MyClassEditActivity, mode.title) setHomeAsUpIndicator(R.drawable.ic_close_black) setDisplayHomeAsUpEnabled(true) } @@ -71,17 +60,16 @@ class MyClassEditActivity : AppCompatActivity(), MyClassEditNavigator { binding.setLifecycleOwner(this) binding.viewModel = viewModel - when (mode) { - MyClassEditViewModel.Mode.EDIT -> viewModel.startEdit( - intent.getLongExtra(EXTRA_ID, 0) - ) - MyClassEditViewModel.Mode.ADD -> viewModel.startAdd( - intent.getSerializableExtra(EXTRA_WEEK) as ClassWeekType, - intent.getIntExtra(EXTRA_PERIOD, 0) + viewModel.subscribe() + + if (intent.hasExtra(EXTRA_ID)) { + viewModel.onActivityCreated(intent.getLongExtra(EXTRA_ID, -1)) + } else { + viewModel.onActivityCreated( + intent.getSerializableExtra(EXTRA_WEEK) as ClassWeek, + intent.getIntExtra(EXTRA_PERIOD, 1) ) } - - viewModel.navigator = this } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -91,61 +79,60 @@ class MyClassEditActivity : AppCompatActivity(), MyClassEditNavigator { override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { + R.id.action_save -> viewModel.onClickSave() android.R.id.home -> finish() - R.id.action_save -> viewModel.save() } return super.onOptionsItemSelected(item) } override fun finish() { - if (viewModel.hasEdit()) { - AlertDialog.Builder(this) - .setTitle(R.string.title_confirmation) - .setMessage(R.string.text_discard_confirm) - .setPositiveButton(R.string.action_yes) { _, _ -> super.finish() } - .setNegativeButton(R.string.action_no, null) - .show() - return - } - super.finish() + viewModel.onFinish() } - override fun onErrorValidation(isSubject: Boolean, isCredit: Boolean, isScheduleCode: Boolean) { - var focusView: View? = null + private fun MyClassEditViewModel.subscribe() { + val activity = this@MyClassEditActivity - if (isSubject) { - binding.subjectEdit.error = getString(R.string.error_field_required) - focusView = binding.subjectEdit - } - if (isCredit) { - binding.creditEdit.error = getString(R.string.error_invalid_credit) - focusView = focusView ?: binding.creditEdit - } - if (isScheduleCode) { - binding.scheduleCodeEdit.error = getString(R.string.error_invalid_schedule_code) - focusView = focusView ?: binding.scheduleCodeEdit - } - - focusView?.requestFocus() - } - - override fun onMyClassSaved(success: Boolean) { - if (success) { - super.finish() - } else { - longToast(getString(R.string.error_update, getString(R.string.name_attend_lecture))) - } - } + title.observe(activity, Observer { resId -> + setTitle(resId) + }) - override fun openColorPickerDialog(listener: ColorPickerSwatch.OnColorSelectedListener) { - val dialog = ColorPickerDialog.newInstance( - R.string.title_color_picker, - ClassColor.ALL, - viewModel.color.get(), - 4, - ClassColor.size) + finishActivity.observe(activity, Observer { super.finish() }) - dialog.setOnColorSelectedListener(listener) - dialog.show(fragmentManager, "ColorPickerDialog") + showFinishConfirmDialog.observe(activity, Observer { + AlertDialog.Builder(activity) + .setTitle(R.string.title_confirm) + .setMessage(R.string.text_discard_confirm) + .setPositiveButton(R.string.action_yes) { _, _ -> super.finish() } + .setNegativeButton(R.string.action_no, null) + .show() + }) + + validation.observe(activity, Observer { result -> + var focusView: View? = null + + if (result.isSubject) { + binding.subjectLayout.error = getString(R.string.error_field_required) + focusView = binding.subject + } + if (result.isCredit) { + binding.creditLayout.error = getString(R.string.error_invalid_credit) + focusView = focusView ?: binding.credit + } + if (result.isScheduleCode) { + binding.scheduleCodeLayout.error = getString(R.string.error_invalid_schedule_code) + focusView = focusView ?: binding.scheduleCode + } + + focusView?.requestFocus() + }) + + errorSaveFailed.observe(activity, Observer { + Toast.makeText(activity, R.string.error_update, Toast.LENGTH_SHORT).show() + }) + + errorNotFound.observe(activity, Observer { + Toast.makeText(activity, R.string.error_not_found, Toast.LENGTH_LONG).show() + finish() + }) } } diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditNavigator.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditNavigator.kt deleted file mode 100644 index aab813d..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditNavigator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package jp.kentan.studentportalplus.ui.myclass.edit - -import com.android.colorpicker.ColorPickerSwatch - -interface MyClassEditNavigator { - - fun onErrorValidation(isSubject: Boolean, isCredit: Boolean, isScheduleCode: Boolean) - - fun onMyClassSaved(success: Boolean) - - fun openColorPickerDialog(listener: ColorPickerSwatch.OnColorSelectedListener) -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditViewModel.kt index 30547bb..be2321f 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditViewModel.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/MyClassEditViewModel.kt @@ -1,41 +1,30 @@ package jp.kentan.studentportalplus.ui.myclass.edit -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.Transformations -import android.arch.lifecycle.ViewModel -import android.databinding.ObservableBoolean -import android.databinding.ObservableField -import android.databinding.ObservableInt -import android.databinding.adapters.AdapterViewBindingAdapter -import com.android.colorpicker.ColorPickerSwatch +import androidx.databinding.Observable +import androidx.databinding.ObservableBoolean +import androidx.databinding.ObservableField +import androidx.databinding.ObservableInt +import androidx.databinding.adapters.AdapterViewBindingAdapter +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import jp.kentan.studentportalplus.R import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.ClassWeekType +import jp.kentan.studentportalplus.data.component.ClassWeek import jp.kentan.studentportalplus.data.model.MyClass -import jp.kentan.studentportalplus.util.Murmur3 -import kotlinx.coroutines.experimental.android.UI +import jp.kentan.studentportalplus.ui.SingleLiveData +import kotlinx.coroutines.experimental.GlobalScope import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.coroutines.experimental.bg class MyClassEditViewModel( - private val repository: PortalRepository + private val portalRepository: PortalRepository ) : ViewModel() { - enum class Mode(val title: String) { - EDIT("Edit"), - ADD("Add") - } - - val subjects: LiveData> = Transformations.map(repository.subjectList) { it.sorted() } - - val weekEntries = ClassWeekType.values().map { it.fullDisplayName } - val periodEntries = (1..7).map { "${it}限" } - - val isUser = ObservableBoolean() - val enabledPeriod = ObservableBoolean() - - val color = ObservableInt() + val title = MutableLiveData() val subject = ObservableField() + val color = ObservableInt() val instructor = ObservableField() val location = ObservableField() val week = ObservableInt() @@ -44,95 +33,108 @@ class MyClassEditViewModel( val credit = ObservableField() val scheduleCode = ObservableField() + val subjects: LiveData> = Transformations.map(portalRepository.subjectList) { it.sorted() } + + val weekEntries = ClassWeek.values().map { it.fullDisplayName } + val periodEntries = (1..7).map { "${it}限" } + + val isUserMode = ObservableBoolean(true) + val isEnabledPeriod = ObservableBoolean(true) + + val isEnabledErrorSubject = SingleLiveData() + val isEnabledErrorCredit = SingleLiveData() + val isEnabledErrorScheduleCode = SingleLiveData() + + val finishActivity = SingleLiveData() + val showFinishConfirmDialog = SingleLiveData() + val validation = SingleLiveData() + val errorSaveFailed = SingleLiveData() + val errorNotFound = SingleLiveData() + val onWeekItemSelected = AdapterViewBindingAdapter.OnItemSelected { _, _, position: Int, _ -> - val week = ClassWeekType.values().getOrNull(position) ?: ClassWeekType.UNKNOWN - val enabled = (week != ClassWeekType.INTENSIVE) && (week != ClassWeekType.UNKNOWN) + val week = ClassWeek.values()[position] + val isDisabled = week == ClassWeek.INTENSIVE || week == ClassWeek.UNKNOWN - enabledPeriod.set(isUser.get() && enabled) + isEnabledPeriod.set(isUserMode.get() && !isDisabled) } - var navigator: MyClassEditNavigator? = null + private var isInitialized = false + private var isUpdateMode = true + private lateinit var originalData: MyClass + + init { + subject.setErrorCancelCallback(isEnabledErrorSubject) + credit.setErrorCancelCallback(isEnabledErrorCredit) + scheduleCode.setErrorCancelCallback(isEnabledErrorScheduleCode) + } - private val weekType: ClassWeekType - get() = ClassWeekType.values().getOrNull(week.get()) ?: ClassWeekType.UNKNOWN + fun onActivityCreated(id: Long) { + isUpdateMode = true + title.value = R.string.title_my_class_edit - private lateinit var originalData: MyClass + if (isInitialized) { return } + isInitialized = true - @Throws(Exception::class) - fun startEdit(id: Long) { - if (::originalData.isInitialized) { + val data = portalRepository.getMyClassWithSync(id) ?: let { + errorNotFound.value = Unit return } - val data = repository.getMyClassById(id) ?: - throw IllegalStateException("data_ not found") - - originalData = data setData(data) + originalData = data } - fun startAdd(week: ClassWeekType, period: Int) { - if (::originalData.isInitialized) { - return - } + fun onActivityCreated(week: ClassWeek, period: Int) { + isUpdateMode = false + title.value = R.string.title_my_class_add + + if (isInitialized) { return } + isInitialized = true val data = MyClass( - hash = 0, week = week, period = period, scheduleCode = "", - credit = 2, + credit = 0, category = "", subject = "", instructor = "", isUser = true ) - originalData = data setData(data) + originalData = data } - private fun setData(data: MyClass) { - isUser.set(data.isUser) - enabledPeriod.set(data.isUser) - color.set(data.color) - subject.set(data.subject) - instructor.set(data.instructor) - location.set(data.location) - week.set(data.week.ordinal) - period.set(if (data.period in 1..7) data.period - 1 else 0) - category.set(data.category) - credit.set(data.credit.toString()) - scheduleCode.set(data.scheduleCode) - } - - fun save() { + fun onClickSave() { val subject = subject.get().trimOrEmpty() - val instructor = instructor.get().trimOrEmpty() - val location = location.get().trimOrEmpty() - val week = weekType - val period = if (week.hasPeriod()) period.get() + 1 else 0 - val category = category.get().trimOrEmpty() val credit = credit.get().trimOrEmpty().toIntOrNull() ?: 0 val scheduleCode = scheduleCode.get().trimOrEmpty() + // Validation val isErrorSubject = subject.isBlank() - val isErrorCredit = credit !in 1..10 + val isErrorCredit = credit !in 0..10 val isErrorScheduleCode = scheduleCode.isNotScheduleCode() if (isErrorSubject || isErrorCredit || isErrorScheduleCode) { - navigator?.onErrorValidation(isErrorSubject, isErrorCredit, isErrorScheduleCode) + validation.value = ValidationResult(isErrorSubject, isErrorCredit, isErrorScheduleCode) return } - val hashStr = week.name + period + scheduleCode + credit + category + subject + instructor + isUser.get() - val data = originalData.copy( - hash = Murmur3.hash64(hashStr.toByteArray()), + val instructor = instructor.get().trimOrEmpty() + val location = location.get().trimOrNull() + val weekType = ClassWeek.values()[week.get()] + val period = if (weekType.hasPeriod()) period.get() + 1 else 0 + val category = category.get().trimOrEmpty() + + val data = MyClass( + id = originalData.id, + isUser = isUserMode.get(), subject = subject, instructor = instructor, - location = if (location.isNotBlank()) location else null, - week = week, + location = location, + week = weekType, period = period, category = category, credit = credit, @@ -140,44 +142,74 @@ class MyClassEditViewModel( color = color.get() ) - launch(UI) { - val success = bg { - if (data.id > 0) repository.update(data) else repository.add(data) - }.await() + GlobalScope.launch { + + val isSuccess = if (isUpdateMode) { + portalRepository.updateMyClass(data).await() + } else { + portalRepository.addMyClass(data).await() + } - navigator?.onMyClassSaved(success) + if (isSuccess) { + finishActivity.postValue(Unit) + } else { + errorSaveFailed.postValue(Unit) + } } } - fun hasEdit(): Boolean { + fun onFinish() { val data = originalData - val location = if (location.get().isNullOrEmpty()) null else location.get() + val dataLocation = data.run { location ?: "" } + val dataCredit: String? = data.run { if (credit > 0) credit.toString() else "" } + val period = period.get() + 1 - return (color.get() != data.color) || - (subject.get() != data.subject) || - (instructor.get() != data.instructor) || - (location != data.location) || - (weekType != data.week) || - (data.week.hasPeriod() && (period != data.period)) || - (category.get() != data.category) || - (credit.get() != data.credit.toString()) || - (scheduleCode.get() != data.scheduleCode) + val canFinish = (color.get() == data.color) && + (subject.get() == data.subject) && + (instructor.get() == data.instructor) && + (location.get() == dataLocation) && + (week.get() == data.week.ordinal) && + ((period == data.period || !data.week.hasPeriod())) && + (category.get() == data.category) && + (credit.get() == dataCredit) && + (scheduleCode.get() == data.scheduleCode) + + if (canFinish) { + finishActivity.value = Unit + } else { + showFinishConfirmDialog.value = Unit + } } - fun onClickColorButton() { - navigator?.openColorPickerDialog(ColorPickerSwatch.OnColorSelectedListener { selectedColor -> - color.set(selectedColor) - }) + private fun setData(data: MyClass) { + isUserMode.set(data.isUser) + + subject.set(data.subject) + color.set(data.color) + instructor.set(data.instructor) + location.set(data.location ?: "") + week.set(data.week.ordinal) + period.set(if (data.period in 1..7) data.period - 1 else 0) + category.set(data.category) + credit.set(if (data.credit > 0) data.credit.toString() else "") + scheduleCode.set(data.scheduleCode) } - override fun onCleared() { - navigator = null - super.onCleared() + private fun ObservableField.setErrorCancelCallback(error: SingleLiveData) { + addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { error.value = false } + }) } private fun String?.trimOrEmpty(): String = this?.trim() ?: "" + private fun String?.trimOrNull(): String? { + val trim = this?.trim() + + return if (trim.isNullOrEmpty()) null else trim + } + private fun String.isNotScheduleCode(): Boolean { val code = toIntOrNull() ?: return !isEmpty() return code !in 10000000..1000000000 diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/ValidationResult.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/ValidationResult.kt new file mode 100644 index 0000000..4df2396 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/myclass/edit/ValidationResult.kt @@ -0,0 +1,7 @@ +package jp.kentan.studentportalplus.ui.myclass.edit + +data class ValidationResult( + val isSubject: Boolean, + val isCredit: Boolean, + val isScheduleCode: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeAdapter.kt new file mode 100644 index 0000000..32d8973 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeAdapter.kt @@ -0,0 +1,46 @@ +package jp.kentan.studentportalplus.ui.notice + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.model.Notice +import jp.kentan.studentportalplus.databinding.ItemNoticeBinding + +class NoticeAdapter( + private val layoutInflater: LayoutInflater, + private val onClick: (Long) -> Unit, + private val onFavoriteClick: (Notice) -> Unit +) : ListAdapter(Notice.DIFF_CALLBACK) { + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int) = getItem(position).id + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding: ItemNoticeBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_notice, parent, false) + + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class ViewHolder( + private val binding: ItemNoticeBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: Notice) { + binding.apply { + setData(data) + layout.setOnClickListener { onClick(data.id) } + favoriteIcon.setOnClickListener { onFavoriteClick(data) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeFragment.kt new file mode 100644 index 0000000..7985ccb --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeFragment.kt @@ -0,0 +1,132 @@ +package jp.kentan.studentportalplus.ui.notice + +import android.os.Bundle +import android.view.* +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.DividerItemDecoration +import dagger.android.support.AndroidSupportInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.component.NoticeQuery +import jp.kentan.studentportalplus.databinding.DialogNoticeFilterBinding +import jp.kentan.studentportalplus.databinding.FragmentListBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.main.FragmentType +import jp.kentan.studentportalplus.ui.main.MainViewModel +import jp.kentan.studentportalplus.ui.notice.detail.NoticeDetailActivity +import javax.inject.Inject + +class NoticeFragment : Fragment() { + + companion object { + fun newInstance() = NoticeFragment() + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var binding: FragmentListBinding + private lateinit var viewModel: NoticeViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_list, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + AndroidSupportInjection.inject(this) + + setHasOptionsMenu(true) + + val provider = ViewModelProvider(requireActivity(), viewModelFactory) + + viewModel = provider.get(NoticeViewModel::class.java) + + val adapter = NoticeAdapter(layoutInflater, viewModel::onClick, viewModel::onFavoriteClick) + + binding.recyclerView.apply { + setAdapter(adapter) + setHasFixedSize(true) + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + + viewModel.subscribe(adapter) + + // Call MainViewModel::onAttachFragment + provider.get(MainViewModel::class.java) + .onAttachFragment(FragmentType.NOTICE) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.search_and_filter, menu) + + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.queryHint = getString(R.string.hint_query_title) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ + override fun onQueryTextSubmit(query: String?) = true + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.onQueryTextChange(newText) + return true + } + }) + + val keyword = viewModel.query.keyword + if (!keyword.isNullOrBlank()) { + searchItem.expandActionView() + searchView.setQuery(keyword, false) + searchView.clearFocus() + } + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + if (item?.itemId == R.id.action_filter) { + showFilterDialog() + } + return super.onOptionsItemSelected(item) + } + + private fun showFilterDialog() { + val context = requireContext() + + val binding: DialogNoticeFilterBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.dialog_notice_filter, binding.root as ViewGroup, false) + + binding.apply { + dateRangeSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, NoticeQuery.DateRange.values()) + query = this@NoticeFragment.viewModel.query + } + + AlertDialog.Builder(context) + .setView(binding.root) + .setTitle(R.string.title_filter_dialog) + .setPositiveButton(R.string.action_apply) { _, _ -> + viewModel.onFilterApplyClick( + binding.dateRangeSpinner.selectedItem as NoticeQuery.DateRange, + binding.unreadChip.isChecked, + binding.readChip.isChecked, + binding.favoriteChip.isChecked + ) + } + .setNegativeButton(R.string.action_cancel, null) + .create() + .show() + } + + private fun NoticeViewModel.subscribe(adapter: NoticeAdapter) { + val fragment = this@NoticeFragment + + noticeList.observe(fragment, Observer { adapter.submitList(it) }) + startDetailActivity.observe(fragment, Observer { id -> + startActivity(NoticeDetailActivity.createIntent(requireContext(), id)) + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeViewModel.kt new file mode 100644 index 0000000..4ea2655 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/NoticeViewModel.kt @@ -0,0 +1,59 @@ +package jp.kentan.studentportalplus.ui.notice + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.component.NoticeQuery +import jp.kentan.studentportalplus.data.model.Notice +import jp.kentan.studentportalplus.ui.SingleLiveData + +class NoticeViewModel( + private val portalRepository: PortalRepository +) : ViewModel() { + + var query = NoticeQuery() + private set + + private val queryLiveData = MutableLiveData() + + val noticeList: LiveData> = Transformations.switchMap(queryLiveData) { + portalRepository.getNoticeList(it) + } + val startDetailActivity = SingleLiveData() + + init { + queryLiveData.value = query + } + + fun onClick(id: Long) { + startDetailActivity.value = id + } + + fun onFavoriteClick(data: Notice) { + portalRepository.updateNotice(data.copy(isFavorite = !data.isFavorite)) + } + + fun onQueryTextChange(text: String?) { + query = query.copy(keyword = text) + + queryLiveData .value = query + } + + fun onFilterApplyClick( + range: NoticeQuery.DateRange, + isUnread: Boolean, + isRead: Boolean, + isFavorite: Boolean + ) { + query = query.copy( + dateRange = range, + isUnread = isUnread, + isRead = isRead, + isFavorite = isFavorite + ) + + queryLiveData .value = query + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailActivity.kt new file mode 100644 index 0000000..82eb9b2 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailActivity.kt @@ -0,0 +1,92 @@ +package jp.kentan.studentportalplus.ui.notice.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.snackbar.Snackbar +import dagger.android.AndroidInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.databinding.ActivityNoticeDetailBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import javax.inject.Inject + +class NoticeDetailActivity : AppCompatActivity() { + + companion object { + private const val EXTRA_ID = "ID" + + fun createIntent(context: Context, id: Long) = + Intent(context, NoticeDetailActivity::class.java).apply { + putExtra(EXTRA_ID, id) + } + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + private val viewModel by lazy(LazyThreadSafetyMode.NONE) { + ViewModelProvider(this, viewModelFactory).get(NoticeDetailViewModel::class.java) + } + + private lateinit var binding: ActivityNoticeDetailBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_notice_detail) + + AndroidInjection.inject(this) + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.setLifecycleOwner(this) + binding.viewModel = viewModel + + viewModel.subscribe() + viewModel.onActivityCreated(intent.getLongExtra(EXTRA_ID, -1)) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.share, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.action_share -> viewModel.onShareClick() + android.R.id.home -> finish() + } + + return super.onOptionsItemSelected(item) + } + + private fun NoticeDetailViewModel.subscribe() { + val activity = this@NoticeDetailActivity + + snackbar.observe(activity, Observer { resId -> + Snackbar.make(binding.root, resId, Snackbar.LENGTH_SHORT) + .show() + }) + + indefiniteSnackbar.observe(activity, Observer { resId -> + val snackbar = Snackbar.make(binding.root, resId, Snackbar.LENGTH_INDEFINITE) + + snackbar.setAction(R.string.action_close) { snackbar.dismiss() } + .show() + }) + + share.observe(activity, Observer { startActivity(it) }) + + errorNotFound.observe(activity, Observer { + Toast.makeText(activity, R.string.error_not_found, Toast.LENGTH_LONG).show() + finish() + }) + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailViewModel.kt new file mode 100644 index 0000000..37da69a --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/notice/detail/NoticeDetailViewModel.kt @@ -0,0 +1,84 @@ +package jp.kentan.studentportalplus.ui.notice.detail + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.* +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.model.Notice +import jp.kentan.studentportalplus.ui.SingleLiveData +import jp.kentan.studentportalplus.util.formatYearMonthDay +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.launch + +class NoticeDetailViewModel( + private val context: Application, + private val portalRepository: PortalRepository +) : AndroidViewModel(context) { + + private val idLiveData = MutableLiveData() + + val notice: LiveData = Transformations.switchMap(idLiveData) { id -> + portalRepository.getNotice(id) + } + val snackbar = SingleLiveData() + val indefiniteSnackbar = SingleLiveData() + val share = SingleLiveData() + val errorNotFound = SingleLiveData() + + private val noticeObserver = Observer { data -> + if (data == null) { + errorNotFound.value = Unit + } else if (!data.isRead) { + portalRepository.updateNotice(data.copy(isRead = true)) + } + } + + init { + notice.observeForever(noticeObserver) + } + + fun onActivityCreated(id: Long) { + idLiveData.value = id + } + + fun onFavoriteClick(data: Notice) { + GlobalScope.launch { + val isFavorite = !data.isFavorite + val isSuccess = portalRepository.updateNotice(data.copy(isFavorite = isFavorite)).await() + + if (isSuccess) { + snackbar.postValue((if (isFavorite) R.string.msg_set_favorite else R.string.msg_reset_favorite)) + } else { + indefiniteSnackbar.postValue(R.string.error_update) + } + } + } + + fun onShareClick() { + val data = notice.value ?: return + + val text = StringBuilder(context.getString(R.string.share_title, data.title)) + + data.detailText?.let { + text.append(context.getString(R.string.share_detail, it)) + } + data.link?.let { + text.append(context.getString(R.string.share_link, it)) + } + text.append(context.getString(R.string.share_created_date, data.createdDate.formatYearMonthDay())) + + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, data.title) + putExtra(Intent.EXTRA_TEXT, text.toString()) + } + + share.value = Intent.createChooser(intent, null) + } + + override fun onCleared() { + notice.removeObserver(noticeObserver) + super.onCleared() + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/setting/GeneralPreferenceFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/GeneralPreferenceFragment.kt new file mode 100644 index 0000000..9b57be2 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/GeneralPreferenceFragment.kt @@ -0,0 +1,187 @@ +package jp.kentan.studentportalplus.ui.setting + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentTransaction +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.snackbar.Snackbar +import dagger.android.support.AndroidSupportInjection +import jp.kentan.studentportalplus.BuildConfig +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.notification.SyncScheduler +import jp.kentan.studentportalplus.ui.web.WebActivity +import jp.kentan.studentportalplus.util.formatYearMonthDayHms +import jp.kentan.studentportalplus.util.getShibbolethLastLoginDate +import jp.kentan.studentportalplus.util.getSyncIntervalMinutes +import jp.kentan.studentportalplus.util.isEnabledSync +import kotlinx.coroutines.experimental.Dispatchers +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.android.Main +import kotlinx.coroutines.experimental.launch +import org.jetbrains.anko.defaultSharedPreferences +import java.util.* +import javax.inject.Inject + +class GeneralPreferenceFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { + + @Inject + lateinit var portalRepository: PortalRepository + + private val syncScheduler by lazy(LazyThreadSafetyMode.NONE) { SyncScheduler(requireContext()) } + + private lateinit var shibbolethLastLoginDate: Preference + private lateinit var syncInterval: Preference + private lateinit var notificationType: Preference + private var isEnabledNotificationVibration: Preference? = null + private var isEnabledNotificationLed: Preference? = null + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_general) + + AndroidSupportInjection.inject(this) + + shibbolethLastLoginDate = findPreference("shibboleth_last_login_date") + syncInterval = findPreference("sync_interval_minutes") + notificationType = findPreference("notification_type") + isEnabledNotificationVibration = findPreference("is_enabled_notification_vibration") + isEnabledNotificationLed = findPreference("is_enabled_notification_led") + + val isEnabledSync = requireContext().defaultSharedPreferences.isEnabledSync() + setEnabledSync(isEnabledSync) + + notificationType.setOnPreferenceClickListener { + commitFragment(NotificationTypePreferenceFragment()) + return@setOnPreferenceClickListener true + } + + findPreference("similar_subject_threshold").setOnPreferenceClickListener { + commitFragment(SimilarSubjectPreferenceFragment()) + return@setOnPreferenceClickListener true + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + findPreference("notification_settings")?.setOnPreferenceClickListener { + val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) + putExtra(Settings.EXTRA_CHANNEL_ID, "NotificationController.NEWLY_CHANNEL_ID") + } + startActivity(intent) + + return@setOnPreferenceClickListener true + } + } + + setupSummary() + } + + override fun onSharedPreferenceChanged(preferences: SharedPreferences, key: String) { + when (key) { + "shibboleth_last_login_date" -> { + shibbolethLastLoginDate.summary = preferences.getFormatShibbolethLastLoginDate() + } + "is_enabled_sync" -> { + val isEnabled = preferences.isEnabledSync() + setEnabledSync(isEnabled) + + if (isEnabled) { + syncScheduler.schedule() + } else { + syncScheduler.cancel() + } + } + "sync_interval_minutes" -> { + syncInterval.summary = getString(R.string.pref_summary_sync_interval, + preferences.getSyncIntervalMinutes() / 60) + syncScheduler.schedule() + } + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + "reset" -> view?.run { + showDeleteConfirmDialog(this) + } + "share" -> { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, getString(R.string.share_app)) + } + startActivity(intent) + } + "terms" -> startActivity(WebActivity.createIntent(requireContext(), "Terms", getString(R.string.url_terms))) + "license" -> startActivity(WebActivity.createIntent(requireContext(), "Licenses", getString(R.string.url_licenses))) + } + return super.onPreferenceTreeClick(preference) + } + + override fun onResume() { + super.onResume() + requireContext().defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onDestroy() { + requireContext().defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + super.onDestroy() + } + + private fun commitFragment(fragment: PreferenceFragmentCompat) { + requireFragmentManager() + .beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .replace(android.R.id.content, fragment) + .addToBackStack(null) + .commit() + } + + private fun setupSummary() { + val pref = requireContext().defaultSharedPreferences + + shibbolethLastLoginDate.summary = pref.getFormatShibbolethLastLoginDate() + syncInterval.summary = getString(R.string.pref_summary_sync_interval, + pref.getSyncIntervalMinutes() / 60) + findPreference("version").summary = BuildConfig.VERSION_NAME + } + + private fun setEnabledSync(isEnabled: Boolean) { + syncInterval.isEnabled = isEnabled + notificationType.isEnabled = isEnabled + isEnabledNotificationVibration?.isEnabled = isEnabled + isEnabledNotificationLed?.isEnabled = isEnabled + } + + private fun showDeleteConfirmDialog(view: View) { + AlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_warning) + .setTitle("ポータルデータ消去") + .setMessage(R.string.msg_warn_reset) + .setPositiveButton(R.string.action_yes) { _, _ -> + GlobalScope.launch(Dispatchers.Main) { + val isSuccess = portalRepository.deleteAll().await() + + if (isSuccess) { + Snackbar.make(view, R.string.msg_delete_all, Snackbar.LENGTH_LONG).show() + } else { + val snackbar = Snackbar.make(view, R.string.error_delete, Snackbar.LENGTH_INDEFINITE) + + snackbar.setAction(R.string.action_close) { snackbar.dismiss() } + .show() + } + } + } + .setNegativeButton(R.string.action_no, null) + .show() + } + + private fun SharedPreferences.getFormatShibbolethLastLoginDate(): String { + val time = getShibbolethLastLoginDate() + return if (time <= 0) "unknown" else Date(time).formatYearMonthDayHms() + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/setting/NotificationTypePreferenceFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/NotificationTypePreferenceFragment.kt new file mode 100644 index 0000000..0c23d3a --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/NotificationTypePreferenceFragment.kt @@ -0,0 +1,30 @@ +package jp.kentan.studentportalplus.ui.setting + +import android.os.Bundle +import androidx.preference.ListPreference +import androidx.preference.PreferenceFragmentCompat +import jp.kentan.studentportalplus.R + +class NotificationTypePreferenceFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_notification_type) + + (findPreference("notification_type_lecture_info") as ListPreference).bindSummaryToValue() + (findPreference("notification_type_lecture_cancel") as ListPreference).bindSummaryToValue() + (findPreference("notification_type_notice") as ListPreference).bindSummaryToValue() + } + + private fun ListPreference.bindSummaryToValue() { + summary = entry + + setOnPreferenceChangeListener { preference, newValue -> + val listPreference = preference as ListPreference + val index = listPreference.findIndexOfValue(newValue.toString()) + + listPreference.summary = listPreference.entries[index] + + return@setOnPreferenceChangeListener true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/setting/SettingsActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/SettingsActivity.kt new file mode 100644 index 0000000..bc930ee --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/SettingsActivity.kt @@ -0,0 +1,41 @@ +package jp.kentan.studentportalplus.ui.setting + +import android.os.Bundle +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import jp.kentan.studentportalplus.notification.NotificationController + + +class SettingsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + if (supportFragmentManager.fragments.isEmpty()) { + supportFragmentManager + .beginTransaction() + .add(android.R.id.content, GeneralPreferenceFragment()) + .commit() + } + + NotificationController.setupChannel(this) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + onBackPressed() + return true + } + + override fun onBackPressed() { + val count = supportFragmentManager.backStackEntryCount + + if (count > 0) { + supportFragmentManager.popBackStackImmediate() + return + } + + super.onBackPressed() + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/setting/SimilarSubjectPreferenceFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/SimilarSubjectPreferenceFragment.kt new file mode 100644 index 0000000..bba7e52 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/setting/SimilarSubjectPreferenceFragment.kt @@ -0,0 +1,33 @@ +package jp.kentan.studentportalplus.ui.setting + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.util.getSimilarSubjectThreshold +import jp.kentan.studentportalplus.view.widget.SimilarSubjectSamplePreference +import org.jetbrains.anko.defaultSharedPreferences + +class SimilarSubjectPreferenceFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_similar_subject) + + val samplePreference = findPreference("similar_threshold_sample") as SimilarSubjectSamplePreference + + findPreference("similar_subject_threshold").apply { + requireContext().defaultSharedPreferences.getSimilarSubjectThreshold().let { percent -> + summary = if (percent < 100) "$percent%%以上" else "$percent%%" + } + + setOnPreferenceChangeListener { _, newValue -> + val percent = newValue.toString().toIntOrNull() ?: 80 + summary = if (percent < 100) "$percent%%以上" else "$percent%%" + + samplePreference.updateThreshold(percent) + + return@setOnPreferenceChangeListener true + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/widget/TimetableShortcutActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/shortcut/TimetableShortcutActivity.kt similarity index 55% rename from app/src/main/java/jp/kentan/studentportalplus/ui/widget/TimetableShortcutActivity.kt rename to app/src/main/java/jp/kentan/studentportalplus/ui/shortcut/TimetableShortcutActivity.kt index 53f13da..778a1ab 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/widget/TimetableShortcutActivity.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/shortcut/TimetableShortcutActivity.kt @@ -1,22 +1,19 @@ -package jp.kentan.studentportalplus.ui.widget +package jp.kentan.studentportalplus.ui.shortcut import android.os.Bundle -import android.support.v4.content.pm.ShortcutInfoCompat -import android.support.v4.content.pm.ShortcutManagerCompat -import android.support.v4.graphics.drawable.IconCompat -import android.support.v7.app.AppCompatActivity +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.ui.MainActivity -import org.jetbrains.anko.clearTop -import org.jetbrains.anko.intentFor -import org.jetbrains.anko.newTask - +import jp.kentan.studentportalplus.ui.main.FragmentType +import jp.kentan.studentportalplus.ui.main.MainActivity class TimetableShortcutActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val shortcutIntent = intentFor("fragment_type" to MainActivity.FragmentType.TIMETABLE.name).newTask().clearTop() + val shortcutIntent = MainActivity.createIntent(this, fragment = FragmentType.TIMETABLE) shortcutIntent.action = intent.action val shortcut = ShortcutInfoCompat.Builder(this, "shortcut_timetable") diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTabsUrlSpan.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTabsUrlSpan.kt deleted file mode 100644 index 06b3741..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTabsUrlSpan.kt +++ /dev/null @@ -1,41 +0,0 @@ -package jp.kentan.studentportalplus.ui.span - -import android.content.Context -import android.content.Intent -import android.support.customtabs.CustomTabsIntent -import android.support.v4.content.ContextCompat -import android.text.style.URLSpan -import android.view.View -import androidx.core.net.toUri -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.util.CustomTabsHelper -import jp.kentan.studentportalplus.util.enabledPdfOpenWithGdocs -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.longToast - -class CustomTabsUrlSpan(private val context: Context, url: String) : URLSpan(url) { - - override fun onClick(widget: View?) { - val isPdf = url.endsWith(".pdf", true) - val isRequireLogin = url.startsWith("https://portal.student.kit.ac.jp", true) - - var urlStr = url - if (isPdf && context.defaultSharedPreferences.enabledPdfOpenWithGdocs()) { - if (isRequireLogin) { - context.longToast(R.string.error_gdocs_require_login) - } else { - urlStr = context.getString(R.string.url_gdocs, url) - } - } - - val customTabs = CustomTabsIntent.Builder() - .setShowTitle(true) - .addDefaultShareMenuItem() - .setToolbarColor(ContextCompat.getColor(context, R.color.colorPrimary)) - .build() - - customTabs.intent.`package` = CustomTabsHelper.getPackageNameToUse(context) - customTabs.intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - customTabs.launchUrl(context, urlStr.toUri()) - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTitle.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTitle.kt deleted file mode 100644 index 1feb666..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/span/CustomTitle.kt +++ /dev/null @@ -1,44 +0,0 @@ -package jp.kentan.studentportalplus.ui.span - -import android.content.Context -import android.graphics.Typeface -import android.support.v4.content.res.ResourcesCompat -import android.text.SpannableString -import android.text.Spanned -import android.text.TextPaint -import android.text.style.MetricAffectingSpan -import jp.kentan.studentportalplus.R - - -/** - * Custom font(Orkney) spannable string - * @note https://www.fontsquirrel.com/fonts/orkney - */ -class CustomTitle(context: Context, title: String) : SpannableString(title) { - - private companion object { - var typefaceSpan: MetricAffectingSpan? = null - } - - init { - typefaceSpan = typefaceSpan ?: ResourcesCompat.getFont(context, R.font.orkney)?.toSpan() - - if (typefaceSpan != null) { - setSpan(typefaceSpan, 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - - /** - * Create MetricAffectingSpan from Typeface - */ - private fun Typeface.toSpan(): MetricAffectingSpan { - return object : MetricAffectingSpan() { - override fun updateMeasureState(p: TextPaint) { - p.typeface = this@toSpan - } - override fun updateDrawState(tp: TextPaint) { - tp.typeface = this@toSpan - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/MyClassAdapter.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/MyClassAdapter.kt new file mode 100644 index 0000000..e5fc6a6 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/MyClassAdapter.kt @@ -0,0 +1,161 @@ +package jp.kentan.studentportalplus.ui.timetable + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.component.ClassWeek +import jp.kentan.studentportalplus.data.model.MyClass +import jp.kentan.studentportalplus.databinding.ItemEmptyMyClassBinding +import jp.kentan.studentportalplus.databinding.ItemGridMyClassBinding +import jp.kentan.studentportalplus.databinding.ItemListMyClassBinding +import java.util.* +import kotlin.math.min + +class MyClassAdapter( + private val layoutInflater: LayoutInflater, + private val onClick: (Long) -> Unit, + private val onAddClick: (ClassWeek, Int) -> Unit +) : ListAdapter(MyClass.DIFF_CALLBACK) { + + private companion object { + const val GRID_TYPE = 0 + const val EMPTY_TYPE = 1 + const val LIST_TYPE = 2 + + val PERIOD_MINUTES = intArrayOf(8 * 60 + 50, 10 * 60 + 30, 12 * 60 + 50, 14 * 60 + 30, 16 * 60 + 10, 17 * 60 + 50, 19 * 60 + 30) + } + + var isGridLayout = true + set(value) { + submitList(null) + field = value + } + private val calender by lazy(LazyThreadSafetyMode.NONE) { Calendar.getInstance() } + + init { + setHasStableIds(true) + } + + fun updateCalender() { + calender.timeInMillis = System.currentTimeMillis() + } + + override fun getItemId(position: Int) = getItem(position).id + + override fun getItemViewType(position: Int) = if (!isGridLayout) { + LIST_TYPE + } else if (getItemId(position) >= 0) { + GRID_TYPE + } else { + EMPTY_TYPE + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyClassAdapter.ViewHolder { + when (viewType) { + GRID_TYPE -> { + val binding: ItemGridMyClassBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_grid_my_class, parent, false) + + return GridViewHolder(binding) + } + EMPTY_TYPE -> { + val binding: ItemEmptyMyClassBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_empty_my_class, parent, false) + + return EmptyViewHolder(binding) + } + LIST_TYPE -> { + val binding: ItemListMyClassBinding = + DataBindingUtil.inflate(layoutInflater, R.layout.item_list_my_class, parent, false) + + return ListViewHolder(binding) + } + else -> throw IllegalArgumentException("viewType($viewType) is not supported.") + } + } + + override fun onBindViewHolder(holder: MyClassAdapter.ViewHolder, position: Int) { + holder.bind(getItem(position)) + + if (isGridLayout) { + holder.setMask(calcMaskGuidelinePercent(position)) + } + } + + private fun calcMaskGuidelinePercent(position: Int): Float { + val day = position % 5 + 2 + + val dayOfWeek = calender.get(Calendar.DAY_OF_WEEK) + + if (day < dayOfWeek || dayOfWeek == Calendar.SUNDAY) { + return 1f + } else if (day == dayOfWeek) { + val period = position / 5 + val minutes = calender.get(Calendar.MINUTE) + calender.get(Calendar.HOUR_OF_DAY) * 60 + + val diff = (minutes - PERIOD_MINUTES[period]) + + if (diff > 0) { + return min(diff / 90f, 1f) + } + } + + return 0f + } + + inner class GridViewHolder( + private val binding: ItemGridMyClassBinding + ) : ViewHolder(binding.root) { + override fun bind(data: MyClass) { + binding.data = data + binding.layout.setOnClickListener { onClick(data.id) } + } + + override fun setMask(ratio: Float) { + if (ratio > 0f) { + binding.guideline.setGuidelinePercent(ratio) + binding.maskGroup.isVisible = true + } else { + binding.maskGroup.isVisible = false + } + } + } + + inner class EmptyViewHolder( + private val binding: ItemEmptyMyClassBinding + ) : ViewHolder(binding.root) { + override fun bind(data: MyClass) { + binding.layout.setOnClickListener { onAddClick(data.week, data.period) } + } + + override fun setMask(ratio: Float) { + if (ratio > 0f) { + binding.guideline.setGuidelinePercent(ratio) + binding.maskGroup.isVisible = true + } else { + binding.maskGroup.isVisible = false + } + } + } + + inner class ListViewHolder( + private val binding: ItemListMyClassBinding + ) : ViewHolder(binding.root) { + override fun bind(data: MyClass) { + binding.data = data + binding.layout.setOnClickListener { onClick(data.id) } + } + + override fun setMask(ratio: Float) { } + } + + abstract class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract fun bind(data: MyClass) + abstract fun setMask(ratio: Float) + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableFragment.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableFragment.kt new file mode 100644 index 0000000..2296bb3 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableFragment.kt @@ -0,0 +1,171 @@ +package jp.kentan.studentportalplus.ui.timetable + +import android.annotation.SuppressLint +import android.graphics.Typeface +import android.os.Bundle +import android.view.* +import android.widget.TextView +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.PopupMenu +import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.transition.TransitionManager +import dagger.android.support.AndroidSupportInjection +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.data.component.ClassWeek +import jp.kentan.studentportalplus.databinding.FragmentTimetableBinding +import jp.kentan.studentportalplus.ui.ViewModelFactory +import jp.kentan.studentportalplus.ui.main.FragmentType +import jp.kentan.studentportalplus.ui.main.MainViewModel +import jp.kentan.studentportalplus.ui.myclass.detail.MyClassDetailActivity +import jp.kentan.studentportalplus.ui.myclass.edit.MyClassEditActivity +import jp.kentan.studentportalplus.util.setGridTimetableLayout +import org.jetbrains.anko.defaultSharedPreferences +import javax.inject.Inject + +class TimetableFragment : Fragment() { + + companion object { + fun newInstance() = TimetableFragment() + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var binding: FragmentTimetableBinding + private lateinit var viewModel: TimetableViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_timetable, container, false) + binding.setLifecycleOwner(this) + + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + AndroidSupportInjection.inject(this) + + setHasOptionsMenu(true) + + val provider = ViewModelProvider(requireActivity(), viewModelFactory) + + viewModel = provider.get(TimetableViewModel::class.java) + + val adapter = MyClassAdapter(layoutInflater, viewModel::onClick, viewModel::onAddClick) + + binding.viewModel = viewModel + binding.gridRecyclerView.apply { + setAdapter(adapter) + isNestedScrollingEnabled = false + setHasFixedSize(true) + itemAnimator = null + } + binding.listRecyclerView.apply { + setAdapter(adapter) + setHasFixedSize(true) + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + + viewModel.subscribe(adapter) + + // Call MainViewModel::onAttachFragment + provider.get(MainViewModel::class.java) + .onAttachFragment(FragmentType.TIMETABLE) + } + + override fun onResume() { + super.onResume() + viewModel.onFragmentResume() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.timetable, menu) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.action_add -> viewModel.onAddClick(ClassWeek.MONDAY, 1) + R.id.action_switch_layout -> showLayoutSelectPopup() + } + return super.onOptionsItemSelected(item) + } + + private fun TimetableViewModel.subscribe(adapter: MyClassAdapter) { + val fragment = this@TimetableFragment + + // Should call before adapter::submitList + isGridLayout.observe(fragment, Observer { isGrid -> + requireActivity().defaultSharedPreferences.setGridTimetableLayout(isGrid) + + adapter.isGridLayout = isGrid + }) + + myClassList.observe(fragment, Observer { list -> + if (adapter.isGridLayout) { + TransitionManager.beginDelayedTransition(binding.gridRecyclerView) + } + adapter.updateCalender() + adapter.submitList(list) + }) + + dayOfWeek.observe(fragment, Observer { updateWeekHeaders(it) }) + + notifyDataSetChanged.observe(fragment, Observer { + adapter.updateCalender() + adapter.notifyDataSetChanged() + }) + + startDetailActivity.observe(fragment, Observer { id -> + startActivity(MyClassDetailActivity.createIntent(requireContext(), id)) + }) + + startAddActivity.observe(fragment, Observer { (week, period) -> + startActivity(MyClassEditActivity.createIntent(requireContext(), week, period)) + }) + } + + @SuppressLint("RestrictedApi") + private fun showLayoutSelectPopup() { + val anchor: View = requireActivity().findViewById(R.id.action_switch_layout) + + val popup = PopupMenu(requireContext(), anchor).apply { + menuInflater.inflate(R.menu.popup_switch_layout, menu) + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_view_week -> viewModel.onWeekLayoutClick() + R.id.action_view_list -> viewModel.onListLayoutClick() + } + return@setOnMenuItemClickListener true + } + show() + } + + (popup.menu as MenuBuilder).setOptionalIconsVisible(true) + } + + private fun updateWeekHeaders(today: ClassWeek) { + binding.apply { + mondayHeader.setToday(today == ClassWeek.MONDAY) + tuesdayHeader.setToday(today == ClassWeek.TUESDAY) + wednesdayHeader.setToday(today == ClassWeek.WEDNESDAY) + thursdayHeader.setToday(today == ClassWeek.THURSDAY) + fridayHeader.setToday(today == ClassWeek.FRIDAY) + } + } + + private fun TextView.setToday(isToday: Boolean) { + if (isToday) { + typeface = Typeface.DEFAULT_BOLD + setTextColor(ContextCompat.getColor(context, R.color.colorAccent)) + } else { + typeface = Typeface.DEFAULT + setTextColor(ContextCompat.getColor(context, R.color.colorPrimary)) + } + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableViewModel.kt new file mode 100644 index 0000000..b20f213 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/timetable/TimetableViewModel.kt @@ -0,0 +1,119 @@ +package jp.kentan.studentportalplus.ui.timetable + +import android.content.SharedPreferences +import androidx.lifecycle.* +import jp.kentan.studentportalplus.data.PortalRepository +import jp.kentan.studentportalplus.data.component.ClassWeek +import jp.kentan.studentportalplus.data.model.MyClass +import jp.kentan.studentportalplus.ui.SingleLiveData +import jp.kentan.studentportalplus.util.isGridTimetableLayout +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.launch +import java.util.* + +class TimetableViewModel( + preferences: SharedPreferences, + private val portalRepository: PortalRepository +) : ViewModel() { + + companion object { + private val EMPTY_ITEM = MyClass( + hash = 0, + week = ClassWeek.UNKNOWN, + period = 1, + scheduleCode = "", + credit = 0, + category = "", + subject = "", + instructor = "", + isUser = false + ) + } + + val isGridLayout = MutableLiveData() + + val myClassList: LiveData> = Transformations.switchMap(isGridLayout) { isGrid -> + dayOfWeek.value = getDayOfWeek() + + if (isGrid) { + val result = MediatorLiveData>() + + result.addSource(portalRepository.myClassList) { list -> + GlobalScope.launch { + result.postValue(list.toWeekTimetable()) + } + } + + return@switchMap result + } else { + return@switchMap portalRepository.myClassList + } + } + + val dayOfWeek = MutableLiveData() + val notifyDataSetChanged = SingleLiveData() + val startDetailActivity = SingleLiveData() + val startAddActivity = SingleLiveData>() + + init { + isGridLayout.value = preferences.isGridTimetableLayout() + } + + fun onFragmentResume() { + if (isGridLayout.value == true) { + dayOfWeek.value = getDayOfWeek() + notifyDataSetChanged.value = Unit + } + } + + fun onClick(id: Long) { + startDetailActivity.value = id + } + + fun onAddClick(week: ClassWeek, period: Int) { + startAddActivity.value = week to period + } + + fun onWeekLayoutClick() { + if (isGridLayout.value != true) { + isGridLayout.value = true + } + } + + fun onListLayoutClick() { + if (isGridLayout.value != false) { + isGridLayout.value = false + } + } + + private fun getDayOfWeek(): ClassWeek { + val day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) + + return if (day in Calendar.MONDAY..Calendar.FRIDAY) { + ClassWeek.valueOf(day-1) + } else { + ClassWeek.UNKNOWN + } + } + + private fun List.toWeekTimetable(): List { + val list = mutableListOf() + + // id for empty + var emptyId: Long = -1 + + for (period in 1..7) { + for (week in ClassWeek.TIMETABLE) { + val myClass = find { it.period == period && it.week == week } + + if (myClass != null) { + list.add(myClass) + } else { + list.add(EMPTY_ITEM.copy(id = emptyId--, week = week, period = period)) + } + } + } + + return list + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/DashboardFragmentViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/DashboardFragmentViewModel.kt deleted file mode 100644 index 74bbc6e..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/DashboardFragmentViewModel.kt +++ /dev/null @@ -1,43 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.Transformations -import android.arch.lifecycle.ViewModel -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.ClassWeekType -import jp.kentan.studentportalplus.data.component.PortalDataSet -import jp.kentan.studentportalplus.data.model.MyClass -import jp.kentan.studentportalplus.data.model.Notice -import org.jetbrains.anko.coroutines.experimental.bg -import java.util.* - - -class DashboardFragmentViewModel(private val repository: PortalRepository) : ViewModel() { - - val portalDataSet: LiveData = Transformations.map(repository.portalDataSet) { - it.copy( - myClassList = toTodayTimetable(it.myClassList).second, - lectureInfoList = it.lectureInfoList.filter { it.attend.isAttend() }, - lectureCancelList = it.lectureCancelList.filter { it.attend.isAttend() } - ) - } - - fun onClickNoticeFavorite(data: Notice) { - bg {repository.update(data.copy(isFavorite = !data.isFavorite))} - } - - private fun toTodayTimetable(list: List): Pair> { - val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) - var dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - - // 午後8時以降は明日の時間割 - if (hour >= 20) { - dayOfWeek++ - } - - // 土、日は月に - val week = if (dayOfWeek in Calendar.MONDAY..Calendar.FRIDAY) ClassWeekType.valueOf(dayOfWeek-1) else ClassWeekType.MONDAY - - return Pair(week.fullDisplayName, list.filter { it.week == week }) - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationFragmentViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationFragmentViewModel.kt deleted file mode 100644 index 8fb6458..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationFragmentViewModel.kt +++ /dev/null @@ -1,64 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MediatorLiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.ViewModel -import android.content.SharedPreferences -import androidx.core.content.edit -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.LectureOrderType -import jp.kentan.studentportalplus.data.component.LectureQuery -import jp.kentan.studentportalplus.data.component.isDefault -import jp.kentan.studentportalplus.data.model.LectureCancellation -import org.jetbrains.anko.coroutines.experimental.bg - - -class LectureCancellationFragmentViewModel( - private val preferences: SharedPreferences, - private val repository: PortalRepository -) : ViewModel() { - - private val lectureCancellationList = repository.lectureCancellationList - private val results = MediatorLiveData>() - private val _query = MutableLiveData() - - private val defaultQuery by lazy { - val order = LectureOrderType.valueOf( - preferences.getString("lecture_cancel_order_type", LectureOrderType.UPDATED_DATE.name) - ) - LectureQuery.DEFAULT.copy(order = order) - } - - var query: LectureQuery - set(value) { - if (value != _query.value) { - _query.value = value - - preferences.edit { putString("lecture_cancel_order_type", value.order.name) } - } - } - get() = _query.value ?: defaultQuery - - init { - results.addSource(lectureCancellationList) { - loadFromRepository(query) - } - - results.addSource(_query) { - loadFromRepository(it) - } - } - - fun getResults(): LiveData> = results - - private fun loadFromRepository(query: LectureQuery?) { - if (query == null || query.isDefault()) { - results.value = lectureCancellationList.value - } else{ - bg { - results.postValue(repository.searchLectureCancellations(query)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationViewModel.kt deleted file mode 100644 index 1e2943a..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureCancellationViewModel.kt +++ /dev/null @@ -1,78 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.Transformations -import android.arch.lifecycle.ViewModel -import android.content.Context -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.LectureAttendType -import jp.kentan.studentportalplus.data.model.LectureCancellation -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.coroutines.experimental.bg -import org.jetbrains.anko.share -import org.jsoup.Jsoup - -class LectureCancellationViewModel(private val repository: PortalRepository) : ViewModel() { - - val lectureCancelId = MutableLiveData() - val lectureCancel: LiveData = Transformations.map(lectureCancelId) { id -> findLectureCancelById(id) } - - fun getCurrentAttendType() = findLectureCancelById(lectureCancelId.value)?.attend - - fun onClickShare(context: Context) { - val data = lectureCancel.value ?: return - val text = context.getString(R.string.text_share_lecture_cancel, - data.subject, - data.instructor, - data.week, - data.period, - data.cancelDate.toShortString(), - Jsoup.parse(data.detailHtml).text(), - data.createdDate.toShortString()) - - context.share(text, data.subject) - } - - fun onClickAttendToUser(onUpdated: (isSuccess: Boolean) -> Unit) { - val data = findLectureCancelById(lectureCancelId.value) ?: return - - if (!data.attend.canAttend()) { - return - } - - launch(UI) { - val success = bg { repository.addToMyClass(data.copy(attend = LectureAttendType.USER)) }.await() - onUpdated(success) - } - } - - fun onClickAttendToNot(onUpdated: (isSuccess: Boolean) -> Unit) { - val data = findLectureCancelById(lectureCancelId.value) ?: return - - // Allow only LectureAttendType.USER - if (data.attend != LectureAttendType.USER) { - return - } - - launch(UI) { - val success = bg { repository.deleteFromMyClass(data.copy(attend = LectureAttendType.NOT)) }.await() - onUpdated(success) - } - } - - private fun findLectureCancelById(id: Long?): LectureCancellation? { - return if (id == null || id < 1) { - null - } else { - repository.getLectureCancellationById(id)?.apply { - if (!this.isRead) { - bg { repository.update(this.copy(isRead = true)) } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationFragmentViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationFragmentViewModel.kt deleted file mode 100644 index 5738863..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationFragmentViewModel.kt +++ /dev/null @@ -1,64 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MediatorLiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.ViewModel -import android.content.SharedPreferences -import androidx.core.content.edit -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.LectureOrderType -import jp.kentan.studentportalplus.data.component.LectureQuery -import jp.kentan.studentportalplus.data.component.isDefault -import jp.kentan.studentportalplus.data.model.LectureInformation -import org.jetbrains.anko.coroutines.experimental.bg - - -class LectureInformationFragmentViewModel( - private val preferences: SharedPreferences, - private val repository: PortalRepository -) : ViewModel() { - - private val lectureInformationList = repository.lectureInformationList - private val results = MediatorLiveData>() - private val _query = MutableLiveData() - - private val defaultQuery by lazy { - val order = LectureOrderType.valueOf( - preferences.getString("lecture_info_order_type", LectureOrderType.UPDATED_DATE.name) - ) - LectureQuery.DEFAULT.copy(order = order) - } - - var query: LectureQuery - set(value) { - if (value != _query.value) { - _query.value = value - - preferences.edit { putString("lecture_info_order_type", value.order.name) } - } - } - get() = _query.value ?: defaultQuery - - init { - results.addSource(lectureInformationList) { - loadFromRepository(query) - } - - results.addSource(_query) { - loadFromRepository(it) - } - } - - fun getResults(): LiveData> = results - - private fun loadFromRepository(query: LectureQuery?) { - if (query == null || query.isDefault()) { - results.value = lectureInformationList.value - } else{ - bg { - results.postValue(repository.searchLectureInformation(query)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationViewModel.kt deleted file mode 100644 index 4c953a5..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/LectureInformationViewModel.kt +++ /dev/null @@ -1,83 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.Transformations -import android.arch.lifecycle.ViewModel -import android.content.Context -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.LectureAttendType -import jp.kentan.studentportalplus.data.model.LectureInformation -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.coroutines.experimental.bg -import org.jetbrains.anko.share - -class LectureInformationViewModel(private val repository: PortalRepository) : ViewModel() { - - val lectureInfoId = MutableLiveData() - val lectureInfo: LiveData = Transformations.map(lectureInfoId) { id -> findLectureInfoById(id)} - - fun getCurrentAttendType() = findLectureInfoById(lectureInfoId.value)?.attend - - fun onClickShare(context: Context) { - val data = findLectureInfoById(lectureInfoId.value) ?: return - - val text = StringBuilder( - context.getString(R.string.text_share_lecture_info, - data.subject, - data.instructor, - data.week, - data.period, - data.category, - data.detailText, - data.createdDate.toShortString())) - - if (data.createdDate != data.updatedDate) { - text.append(context.getString(R.string.text_share_updated_date, data.updatedDate.toShortString())) - } - - context.share(text.toString(), data.subject) - } - - fun onClickAttendToUser(onUpdated: (isSuccess: Boolean) -> Unit) { - val data = findLectureInfoById(lectureInfoId.value) ?: return - - if (!data.attend.canAttend()) { - return - } - - launch(UI) { - val success = bg { repository.addToMyClass(data.copy(attend = LectureAttendType.USER)) }.await() - onUpdated(success) - } - } - - fun onClickAttendToNot(onUpdated: (isSuccess: Boolean) -> Unit) { - val data = findLectureInfoById(lectureInfoId.value) ?: return - - // Allow only LectureAttendType.USER - if (data.attend != LectureAttendType.USER) { - return - } - - launch(UI) { - val success = bg { repository.deleteFromMyClass(data.copy(attend = LectureAttendType.NOT)) }.await() - onUpdated(success) - } - } - - private fun findLectureInfoById(id: Long?): LectureInformation? { - return if (id == null || id < 1) { - null - } else { - repository.getLectureInformationById(id)?.apply { - if (!this.isRead) { - bg { repository.update(this.copy(isRead = true)) } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MainViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MainViewModel.kt deleted file mode 100644 index 7cd4438..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MainViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.Transformations -import android.arch.lifecycle.ViewModel -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.shibboleth.ShibbolethAuthenticationException -import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider -import org.jetbrains.anko.coroutines.experimental.bg - -class MainViewModel( - private val repository: PortalRepository, - private val shibbolethDataProvider: ShibbolethDataProvider -) : ViewModel() { - - private val _isSyncing = MutableLiveData() - private val _syncResult = MutableLiveData>() - - val isSyncing: LiveData = Transformations.map(_isSyncing) { it } - val syncResult: LiveData> = Transformations.map(_syncResult) { it } - - enum class SyncResult { SUCCESS, AUTH_ERROR, UNKNOWN_ERROR } - - fun load() { - bg { repository.loadFromDb() } - } - - fun sync() { - _isSyncing.value = true - - bg { - try { - repository.sync() - _syncResult.postValue(Pair(SyncResult.SUCCESS, null)) - } catch (e: ShibbolethAuthenticationException) { - _syncResult.postValue(Pair(SyncResult.AUTH_ERROR, e.message)) - } catch (e: Exception) { - _syncResult.postValue(Pair(SyncResult.UNKNOWN_ERROR, e.message)) - } finally { - _isSyncing.postValue(false) - } - } - } - - fun getUser() = shibbolethDataProvider.getUser() -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MyClassViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MyClassViewModel.kt deleted file mode 100644 index 9f0156d..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/MyClassViewModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MediatorLiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.ViewModel -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.model.MyClass -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.coroutines.experimental.bg - -class MyClassViewModel( - private val repository: PortalRepository -) : ViewModel() { - - private val myClassSource = repository.myClassList - - private val myClassId = MutableLiveData() - val myClass: LiveData by lazy(LazyThreadSafetyMode.NONE) { - MediatorLiveData().apply { - addSource(myClassId) { id -> - value = myClassSource.value?.find { it.id == id } - } - addSource(myClassSource) { list -> - val id = myClassId.value ?: return@addSource - value = list?.find { it.id == id } - } - } - } - - val subject: String - get() = myClass.value?.subject ?: "" - - val canDelete: Boolean - get() = myClass.value?.isUser == true - - fun setId(id: Long) { - myClassId.value = id - } - - fun delete(onDeleted: (isSuccess: Boolean) -> Unit) { - val data = myClass.value ?: return - - launch(UI) { - val success = bg { repository.delete(data.subject) }.await() - onDeleted(success) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeFragmentViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeFragmentViewModel.kt deleted file mode 100644 index 4351eb1..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeFragmentViewModel.kt +++ /dev/null @@ -1,53 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MediatorLiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.ViewModel -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.NoticeQuery -import jp.kentan.studentportalplus.data.component.isDefault -import jp.kentan.studentportalplus.data.model.Notice -import org.jetbrains.anko.coroutines.experimental.bg - - -class NoticeFragmentViewModel(private val repository: PortalRepository) : ViewModel() { - - private val noticeList = repository.noticeList - private val results = MediatorLiveData>() - private val _query = MutableLiveData() - - var query: NoticeQuery - set(value) { - if (value != _query.value) { - _query.value = value - } - } - get() = _query.value ?: NoticeQuery.DEFAULT - - init { - results.addSource(noticeList) { - loadFromRepository(query) - } - - results.addSource(_query) { - loadFromRepository(it) - } - } - - fun getResults(): LiveData> = results - - fun update(data: Notice) = bg { - repository.update(data) - } - - private fun loadFromRepository(query: NoticeQuery?) { - if (query == null || query.isDefault()) { - results.value = noticeList.value - } else{ - bg { - results.postValue(repository.searchNotices(query)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeViewModel.kt deleted file mode 100644 index 1b1f5dc..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/NoticeViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.Transformations -import android.arch.lifecycle.ViewModel -import android.content.Context -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.model.Notice -import jp.kentan.studentportalplus.util.toShortString -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.launch -import org.jetbrains.anko.coroutines.experimental.bg -import org.jetbrains.anko.share - -class NoticeViewModel(private val repository: PortalRepository) : ViewModel() { - - val noticeId = MutableLiveData() - val notice: LiveData = Transformations.map(noticeId) { id -> findNoticeById(id)} - - - fun onClickShare(context: Context) { - val data = notice.value ?: return - - val text = StringBuilder(context.getString(R.string.text_share_title, data.title)) - - if (data.detailText.isNullOrEmpty()) { - text.append(context.getString(R.string.text_share_detail, data.detailText)) - } - if (data.link.isNullOrEmpty()) { - text.append(context.getString(R.string.text_share_link, data.link)) - } - text.append(context.getString(R.string.text_share_created_date, data.createdDate.toShortString())) - - context.share(text.toString(), data.title) - } - - fun onClickFavorite(onUpdated: (isSuccess: Boolean, isFavorite: Boolean) -> Unit) { - val data = findNoticeById(noticeId.value) ?: return - val favorite = !data.isFavorite - - launch(UI) { - val success = bg { repository.update(data.copy(isFavorite = favorite)) }.await() - onUpdated(success, favorite) - } - } - - private fun findNoticeById(id: Long?): Notice? { - return if (id == null || id < 1) { - null - } else { - repository.getNoticeById(id)?.apply { - if (!this.hasRead) { - bg { repository.update(this.copy(hasRead = true)) } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/TimetableFragmentViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/TimetableFragmentViewModel.kt deleted file mode 100644 index 6717ae4..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/TimetableFragmentViewModel.kt +++ /dev/null @@ -1,100 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MediatorLiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.ViewModel -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.component.ClassWeekType -import jp.kentan.studentportalplus.data.model.MyClass -import jp.kentan.studentportalplus.ui.adapter.MyClassAdapter -import org.jetbrains.anko.coroutines.experimental.bg -import java.util.* - - -class TimetableFragmentViewModel(repository: PortalRepository) : ViewModel() { - - enum class LayoutType(val viewType: Int){ - WEEK(MyClassAdapter.TYPE_GRID), - DAY(MyClassAdapter.TYPE_LIST) - } - - private companion object { - val DUMMY = MyClass( - id = -1, - hash = -1, - week = ClassWeekType.UNKNOWN, - period = 1, - scheduleCode = "", - credit = 0, - category = "", - subject = "", - instructor = "", - isUser = false, - location = null) - } - - private val source = repository.myClassList - private val results = MediatorLiveData>() - private val layout = MutableLiveData() - - init { - results.addSource(source) { - layout.value?.let { loadFromRepository(it) } - } - results.addSource(layout) { - it?.let { loadFromRepository(it) } - } - } - - fun getResults(): LiveData> = results - - fun setViewType(type: LayoutType) { - layout.value = type - } - - fun getWeek(): ClassWeekType? { - val intWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - - return if (intWeek in Calendar.MONDAY..Calendar.FRIDAY) { - ClassWeekType.valueOf(intWeek-1) - } else { - null - } - } - - private fun loadFromRepository(type: LayoutType) { - val list = source.value ?: return - - if (type == LayoutType.WEEK) { - bg { results.postValue(normalize(list)) } - } else { - results.value = list - } - } - - /** - * Normalize MyClass list to 7*5 timetable - */ - private fun normalize(rawList: List): List { - val list = mutableListOf() - - // id for dummy - var uniqueId: Long = -1 - - for (period in 1..7) { - for (week in 1..5) { - val weekType = ClassWeekType.valueOf(week) - val myClass = rawList.find { it.match(period, weekType) } - - if (myClass != null) { - list.add(myClass) - } else { - list.add(DUMMY.copy(id = uniqueId--, week = weekType, period = period)) - } - } - } - - return list - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/ViewModelFactory.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/ViewModelFactory.kt deleted file mode 100644 index 528a7d1..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/viewmodel/ViewModelFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package jp.kentan.studentportalplus.ui.viewmodel - -import android.arch.lifecycle.ViewModel -import android.arch.lifecycle.ViewModelProvider -import android.content.SharedPreferences -import jp.kentan.studentportalplus.data.PortalRepository -import jp.kentan.studentportalplus.data.shibboleth.ShibbolethDataProvider -import jp.kentan.studentportalplus.ui.myclass.edit.MyClassEditViewModel - - -class ViewModelFactory( - private val sharedPreferences: SharedPreferences, - private val portalRepository: PortalRepository, - private val shibbolethDataProvider: ShibbolethDataProvider -) : ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class) = - with(modelClass) { - when { - isAssignableFrom(MainViewModel::class.java) -> - MainViewModel(portalRepository, shibbolethDataProvider) - - isAssignableFrom(DashboardFragmentViewModel::class.java) -> - DashboardFragmentViewModel(portalRepository) - - isAssignableFrom(TimetableFragmentViewModel::class.java) -> - TimetableFragmentViewModel(portalRepository) - - isAssignableFrom(LectureInformationFragmentViewModel::class.java) -> - LectureInformationFragmentViewModel(sharedPreferences, portalRepository) - - isAssignableFrom(LectureCancellationFragmentViewModel::class.java) -> - LectureCancellationFragmentViewModel(sharedPreferences, portalRepository) - - isAssignableFrom(NoticeFragmentViewModel::class.java) -> - NoticeFragmentViewModel(portalRepository) - - isAssignableFrom(LectureInformationViewModel::class.java) -> - LectureInformationViewModel(portalRepository) - - isAssignableFrom(LectureCancellationViewModel::class.java) -> - LectureCancellationViewModel(portalRepository) - - isAssignableFrom(NoticeViewModel::class.java) -> - NoticeViewModel(portalRepository) - - isAssignableFrom(MyClassViewModel::class.java) -> - MyClassViewModel(portalRepository) - - isAssignableFrom(MyClassEditViewModel::class.java) -> - MyClassEditViewModel(portalRepository) - - else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") - } - } as T -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/WebActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/web/WebActivity.kt similarity index 80% rename from app/src/main/java/jp/kentan/studentportalplus/ui/WebActivity.kt rename to app/src/main/java/jp/kentan/studentportalplus/ui/web/WebActivity.kt index 8af5770..42043a6 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/WebActivity.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/web/WebActivity.kt @@ -1,23 +1,21 @@ -package jp.kentan.studentportalplus.ui +package jp.kentan.studentportalplus.ui.web import android.content.Context import android.content.Intent -import android.databinding.DataBindingUtil -import android.support.v7.app.AppCompatActivity import android.os.Bundle -import android.util.Log import android.view.MenuItem import android.webkit.WebSettings import android.webkit.WebView +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil import jp.kentan.studentportalplus.R import jp.kentan.studentportalplus.databinding.ActivityWebBinding -import jp.kentan.studentportalplus.ui.span.CustomTitle class WebActivity : AppCompatActivity() { companion object { - private const val EXTRA_TITLE = "title" - private const val EXTRA_URL = "url" + private const val EXTRA_TITLE = "EXTRA_TITLE" + private const val EXTRA_URL = "EXTRA_URL" fun createIntent(context: Context, title: String, url: String) = Intent(context, WebActivity::class.java).apply { @@ -39,13 +37,11 @@ class WebActivity : AppCompatActivity() { val url = intent.getStringExtra(EXTRA_URL) if (title == null || url == null) { - Log.w("WebActivity", "Invalid intent.") finish() return } - setTitle(CustomTitle(this, title)) - + setTitle(title) webView.apply { settings.cacheMode = WebSettings.LOAD_NO_CACHE settings.setAppCacheEnabled(false) @@ -54,7 +50,7 @@ class WebActivity : AppCompatActivity() { } override fun onOptionsItemSelected(item: MenuItem?): Boolean { - onBackPressed() + finish() return true } @@ -78,4 +74,4 @@ class WebActivity : AppCompatActivity() { webView.destroy() super.onDestroy() } -} +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeActivity.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeActivity.kt new file mode 100644 index 0000000..eef484a --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeActivity.kt @@ -0,0 +1,60 @@ +package jp.kentan.studentportalplus.ui.welcome + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.databinding.ActivityWelcomeBinding +import jp.kentan.studentportalplus.ui.login.LoginActivity + +class WelcomeActivity : AppCompatActivity() { + + companion object { + fun createIntent(context: Context) = + Intent(context, WelcomeActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + } + + private lateinit var binding: ActivityWelcomeBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome) + + val viewModel = ViewModelProviders.of(this).get(WelcomeViewModel::class.java) + + binding.setLifecycleOwner(this) + binding.viewModel = viewModel.apply { + startLoginActivity.observe(this@WelcomeActivity, Observer { + val intent = LoginActivity.createIntent(this@WelcomeActivity, true) + startActivity(intent) + }) + } + binding.webView.apply { + webViewClient = viewModel.getWebViewClient() + loadUrl(getString(R.string.url_terms)) + } + } + + override fun onPause() { + binding.webView.onPause() + super.onPause() + } + + override fun onResume() { + super.onResume() + binding.webView.onResume() + } + + override fun onDestroy() { + binding.webView.destroy() + super.onDestroy() + } +} diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeViewModel.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeViewModel.kt new file mode 100644 index 0000000..5f2cae5 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/ui/welcome/WelcomeViewModel.kt @@ -0,0 +1,28 @@ +package jp.kentan.studentportalplus.ui.welcome + +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.databinding.ObservableBoolean +import androidx.lifecycle.ViewModel +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.ui.SingleLiveData + +class WelcomeViewModel : ViewModel() { + + val isCheckedAgree = ObservableBoolean() + val startLoginActivity = SingleLiveData() + + fun onClickShibboleth() { + startLoginActivity.value = Unit + } + + fun getWebViewClient() = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + view?.apply { + if (!title.contains(context.getString(R.string.title_terms))) { + view.loadUrl(context.getString(R.string.url_terms_local)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/widget/MapView.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/widget/MapView.kt deleted file mode 100644 index 8df71af..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/widget/MapView.kt +++ /dev/null @@ -1,45 +0,0 @@ -package jp.kentan.studentportalplus.ui.widget - -import android.content.Context -import android.support.customtabs.CustomTabsIntent -import android.support.v4.content.ContextCompat -import androidx.core.net.toUri -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.util.CustomTabsHelper -import jp.kentan.studentportalplus.util.enabledPdfOpenWithGdocs -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.newTask - -class MapView { - - enum class Type {CAMPUS, ROOM} - - companion object { - fun open(context: Context, type: Type) { - val urlStr = when (type) { - Type.CAMPUS -> { - context.getString(R.string.url_campus_map) - } - Type.ROOM -> { - if (context.defaultSharedPreferences.enabledPdfOpenWithGdocs()) { - context.getString(R.string.url_gdocs, context.getString(R.string.url_room_map)) - } else { - context.getString(R.string.url_room_map) - } - } - } - - val customTabs = CustomTabsIntent.Builder() - .setShowTitle(true) - .addDefaultShareMenuItem() - .setToolbarColor(ContextCompat.getColor(context, R.color.colorPrimary)) - .build() - - customTabs.run { - intent.`package` = CustomTabsHelper.getPackageNameToUse(context) - intent.newTask() - launchUrl(context, urlStr.toUri()) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/ui/widget/MyClassThresholdSamplePreference.kt b/app/src/main/java/jp/kentan/studentportalplus/ui/widget/MyClassThresholdSamplePreference.kt deleted file mode 100644 index 42f0f14..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/ui/widget/MyClassThresholdSamplePreference.kt +++ /dev/null @@ -1,68 +0,0 @@ -package jp.kentan.studentportalplus.ui.widget - -import android.annotation.SuppressLint -import android.content.Context -import android.preference.Preference -import android.util.AttributeSet -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.util.JaroWinklerDistance -import kotlinx.android.synthetic.main.sample_my_class_threshold.view.* -import org.jetbrains.anko.find -import org.jetbrains.anko.imageResource - -class MyClassThresholdSamplePreference(context: Context, attributeSet: AttributeSet) : Preference(context, attributeSet) { - - private companion object { - val SUBJECTS = arrayOf("ABC実験ma", "ABC実験ma~mc", "ABC実験 ガイダンス", "XYZ実験ma") - val STRING_DISTANCE = JaroWinklerDistance() - } - - private val iconViews = arrayOfNulls(4) - private var threshold: Float = 0.8f - - init { - widgetLayoutResource = R.layout.sample_my_class_threshold - } - - @SuppressLint("SetTextI18n") - override fun onBindView(view: View) { - super.onBindView(view) - - val itemList = listOf(view.lecture1, view.lecture2, view.lecture3, view.lecture4) - - var indexChar = 'A' - itemList.forEachIndexed { index, item -> - item.find(R.id.date).text = "2018/01/0${index+1}" - item.find(R.id.subject).text = SUBJECTS[index] - item.find(R.id.detail).text = "詳細テキスト${indexChar++}" - - iconViews[index] = item.find(R.id.attend_icon) - } - - updateIconView() - } - - private fun updateIconView() { - iconViews.forEachIndexed { index, icon -> - if (index <= 0) { - icon?.imageResource = R.drawable.ic_lecture_attend - return@forEachIndexed - } - - if (STRING_DISTANCE.getDistance(SUBJECTS[0], SUBJECTS[index]) >= threshold) { - icon?.imageResource = R.drawable.ic_lecture_attend_similar - } else { - icon?.imageResource = R.drawable.ic_lecture_attend_not - } - } - } - - fun updateThreshold(percent: Int) { - threshold = percent / 100f - - updateIconView() - } -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/util/AnimationHelper.kt b/app/src/main/java/jp/kentan/studentportalplus/util/AnimationHelper.kt deleted file mode 100644 index 9431f23..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/util/AnimationHelper.kt +++ /dev/null @@ -1,58 +0,0 @@ -package jp.kentan.studentportalplus.util - -import android.animation.Animator -import android.animation.AnimatorInflater -import android.content.Context -import android.view.View -import jp.kentan.studentportalplus.R - - -fun View.animateFadeIn(context: Context) { - if (visibility == View.VISIBLE) { - return - } - - val fadeIn = AnimatorInflater.loadAnimator(context, R.animator.fade_in) - - fadeIn.setTarget(this) - fadeIn.start() - - visibility = View.VISIBLE -} - -fun View.animateFadeInDelay(context: Context, delay: Long = 200) { - if (visibility == View.VISIBLE) { - return - } - - val fadeIn = AnimatorInflater.loadAnimator(context, R.animator.fade_in) - - fadeIn.setTarget(this) - fadeIn.startDelay = delay - fadeIn.start() - - visibility = View.VISIBLE -} - -fun View.animateFadeOut(context: Context) { - if (visibility != View.VISIBLE) { - return - } - - val fadeOut = AnimatorInflater.loadAnimator(context, R.animator.fade_out) - - fadeOut.addListener(object : Animator.AnimatorListener{ - override fun onAnimationRepeat(animation: Animator?) {} - - override fun onAnimationEnd(animation: Animator?) { - visibility = View.GONE - } - - override fun onAnimationCancel(animation: Animator?) {} - - override fun onAnimationStart(animation: Animator?) {} - }) - - fadeOut.setTarget(this) - fadeOut.start() -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/util/DataBindingHelper.kt b/app/src/main/java/jp/kentan/studentportalplus/util/DataBindingHelper.kt index acdc9a3..418d196 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/util/DataBindingHelper.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/util/DataBindingHelper.kt @@ -1,71 +1,174 @@ package jp.kentan.studentportalplus.util -import android.databinding.BindingAdapter import android.graphics.PorterDuff import android.graphics.Typeface import android.view.View -import android.widget.* +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.widget.AppCompatAutoCompleteTextView +import androidx.core.view.isVisible +import androidx.databinding.BindingAdapter +import com.google.android.material.button.MaterialButton +import com.google.android.material.navigation.NavigationView import jp.kentan.studentportalplus.R -import jp.kentan.studentportalplus.data.component.ClassWeekType -import jp.kentan.studentportalplus.data.component.LectureAttendType +import jp.kentan.studentportalplus.data.component.LectureAttend +import jp.kentan.studentportalplus.data.model.LectureCancellation +import jp.kentan.studentportalplus.data.model.LectureInformation +import jp.kentan.studentportalplus.data.model.MyClass +import jp.kentan.studentportalplus.data.model.Notice import java.util.* -@BindingAdapter("text") -fun setText(textView: TextView, text: String?) { - textView.text = if (text.isNullOrBlank()) "未入力" else text + +@BindingAdapter("isVisible") +fun setIsVisible(view: View, isVisible: Boolean) { + view.isVisible = isVisible } -@BindingAdapter("isRead") -fun setIsRead(textView: TextView, isRead: Boolean) { - textView.typeface = if (isRead) Typeface.DEFAULT else Typeface.DEFAULT_BOLD +@BindingAdapter("date") +fun setDate(view: TextView, date: Date?) { + view.text = date?.formatYearMonthDay() } -@BindingAdapter("attend") -fun setAttendType(imageView: ImageView, attendType: LectureAttendType) { - val resourceId = when (attendType) { - LectureAttendType.PORTAL, LectureAttendType.USER -> R.drawable.ic_lecture_attend - LectureAttendType.SIMILAR -> R.drawable.ic_lecture_attend_similar - else -> R.drawable.ic_lecture_attend_not +@BindingAdapter("noticeDate") +fun setNoticeDate(view: TextView, data: Notice?) { + view.text = if (data != null ) { + view.context.getString(R.string.text_created_date, data.createdDate.formatYearMonthDay()) + } else { + null + } +} + +@BindingAdapter("lectureInfoDate") +fun setLectureInfoDate(view: TextView, data: LectureInformation?) { + data ?: run { + view.text = null + return } - imageView.setImageResource(resourceId) + val context = view.context + + view.text = context.getString(R.string.text_lecture_info_created_date, data.createdDate.formatYearMonthDay()) + + if (data.createdDate != data.updatedDate) { + view.append(context.getString(R.string.text_lecture_info_updated_date, data.updatedDate.formatYearMonthDay())) + } } -@BindingAdapter("date") -fun setDateText(textView: TextView, date: Date) { - textView.text = date.formatToYearMonthDay() +@BindingAdapter("lectureCancelDate") +fun setLectureCancelDate(view: TextView, data: LectureCancellation?) { + view.text = if (data != null ) { + view.context.getString(R.string.text_lecture_cancel_created_date, data.createdDate.formatYearMonthDay()) + } else { + null + } } -@BindingAdapter("week", "period", requireAll = true) -fun setWeekPeriodText(textView: TextView, weekType: ClassWeekType, period: Int) { - textView.apply { - text = context.getString( - R.string.text_week_period, - weekType.fullDisplayName.replace("曜日", "曜"), - period.formatPeriod()) +@BindingAdapter("myClassWeekPeriod") +fun setMyClassWeekPeriod(view: TextView, data: MyClass?) { + data ?: run { + view.text = null + return + } + + val period: String = data.period.let { + if (it > 0) "${it}限" else "" } + + view.text = view.context.getString( + R.string.text_my_class_week_period, + data.week.fullDisplayName.replace("曜日", "曜"), + period) } -@BindingAdapter("syllabus") -fun setSyllabusText(textView: TextView, syllabus: String) { - textView.text = if (syllabus.isBlank()) "未入力" else "http://www.syllabus.kit.ac.jp/?c=detail&schedule_code=$syllabus" +@BindingAdapter("myClassDayPeriod") +fun setMyClassDayPeriod(view: TextView, data: MyClass?) { + data ?: run { + view.text = null + return + } + + view.text = if (data.week.hasPeriod()) data.week.displayName + data.period else data.week.displayName } -@BindingAdapter("credit") -fun setCreditText(textView: TextView, credit: Int) { - textView.text = if (credit > 0) credit.toString() else "" +@BindingAdapter("lectureInfoWeekPeriod") +fun setLectureInfoWeekPeriod(view: TextView, data: LectureInformation?) { + data ?: run { + view.text = null + return + } + + val semester: String = data.semester.let { + if (arrayOf("前", "後", "春", "秋").contains(it)) { "${it}学期" } else { it } + } + val period: String = data.period.let { + if (it != "-") "${it}限" else "" + } + + view.text = view.context.getString( + R.string.text_semester_week_period, + data.grade, + semester, + data.week.replace("曜日", "曜"), + period) } -@BindingAdapter("backgroundColor") -fun setBackgroundColor(button: Button, color: Int) { - button.background.setColorFilter(color, PorterDuff.Mode.MULTIPLY) +@BindingAdapter("lectureCancelWeekPeriod") +fun setLectureCancelWeekPeriod(view: TextView, data: LectureCancellation?) { + data ?: run { + view.text = null + return + } + + val period: String = data.period.let { + if (it != "-") "${it}限" else "" + } + + view.text = view.context.getString( + R.string.text_grade_week_period, + data.grade, + data.week.replace("曜日", "曜"), + period) +} + +@BindingAdapter("syllabus") +fun setSyllabusText(view: TextView, scheduleCode: String?) { + view.text = if (scheduleCode.isNullOrBlank()) "未入力" else "http://www.syllabus.kit.ac.jp/?c=detail&schedule_code=$scheduleCode" +} + +@BindingAdapter("bold") +fun setBold(view: TextView, isBold: Boolean) { + if (isBold) { + view.setTypeface(null, Typeface.BOLD) + } else { + view.setTypeface(null, Typeface.NORMAL) + } } -@BindingAdapter("adapterEntities") -fun setAdapterEntities(view: AutoCompleteTextView, list: List?) { +@BindingAdapter("entities") +fun setAdapterEntities(view: AppCompatAutoCompleteTextView, list: List?) { if (list != null) { view.setAdapter(ArrayAdapter(view.context, android.R.layout.simple_list_item_1, list)) } } -private fun Int.formatPeriod() = if (this > 0) "${this}限" else "" \ No newline at end of file +@BindingAdapter("attend") +fun setAttendType(view: ImageView, attendType: LectureAttend) { + val resourceId = when (attendType) { + LectureAttend.PORTAL, LectureAttend.USER -> R.drawable.ic_lecture_attend + LectureAttend.SIMILAR -> R.drawable.ic_lecture_attend_similar + else -> R.drawable.ic_lecture_attend_not + } + + view.setImageResource(resourceId) +} + +@BindingAdapter("onNavigationItemSelected") +fun setOnNavigationItemSelected(view: NavigationView, listener: NavigationView.OnNavigationItemSelectedListener?) { + view.setNavigationItemSelectedListener(listener) +} + +@BindingAdapter("backgroundColor") +fun setBackgroundColor(button: MaterialButton, color: Int) { + button.background.setColorFilter(color, PorterDuff.Mode.MULTIPLY) +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/util/Helper.kt b/app/src/main/java/jp/kentan/studentportalplus/util/Helper.kt new file mode 100644 index 0000000..3b9a0ea --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/util/Helper.kt @@ -0,0 +1,37 @@ +package jp.kentan.studentportalplus.util + +import android.app.Activity +import android.content.Context +import android.view.inputmethod.InputMethodManager +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.content.ContextCompat +import jp.kentan.studentportalplus.R +import java.text.SimpleDateFormat +import java.util.* + +private val DATE_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.JAPAN) +fun Date.formatYearMonthDay(): String = DATE_FORMAT.format(this) + +private val FULL_DATE_FORMAT = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPAN) +fun Date.formatYearMonthDayHms(): String = FULL_DATE_FORMAT.format(this) + +fun Activity.hideSoftInput() { + try { + val view = this.currentFocus ?: return + + val inputMethodManager = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + if (!inputMethodManager.isActive) { + return + } + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun Context.buildCustomTabsIntent(): CustomTabsIntent = CustomTabsIntent.Builder() + .setShowTitle(true) + .addDefaultShareMenuItem() + .setSecondaryToolbarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark)) + .setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary)) + .build() \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/util/Helpers.kt b/app/src/main/java/jp/kentan/studentportalplus/util/Helpers.kt deleted file mode 100644 index c1d5254..0000000 --- a/app/src/main/java/jp/kentan/studentportalplus/util/Helpers.kt +++ /dev/null @@ -1,88 +0,0 @@ -package jp.kentan.studentportalplus.util - -import android.app.Activity -import android.content.Context -import android.support.design.widget.Snackbar -import android.text.Spanned -import android.view.View -import android.view.inputmethod.InputMethodManager -import androidx.core.text.parseAsHtml -import jp.kentan.studentportalplus.ui.span.CustomTitle -import java.text.SimpleDateFormat -import java.util.* -import java.util.regex.Pattern - -/** - * Hide soft keyboard - */ -fun Activity.hideSoftInput() { - try { - val view = this.currentFocus ?: return - - val inputMethodManager = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - if (!inputMethodManager.isActive) { - return - } - inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) - } catch (e: Exception) { - e.printStackTrace() - } -} - -var Activity.customTitle: String - set(title) { - setTitle(CustomTitle(this, title)) - } - get() = title.toString() - -fun indefiniteSnackbar(view: View, message: String, actionMessage: String) { - val snackbar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE) - snackbar.setAction(actionMessage) { snackbar.dismiss() } - snackbar.show() -} - -fun Boolean.toLong() = if (this) 1L else 0L - -fun Char.toIntOrNull() = toString().toIntOrNull() - -/** - * Convert Date to short String(yyyy/MM/dd) - */ -private val DATE_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.JAPAN) - -@Deprecated("use Date.formatToYearMonthDay()") -fun Date.toShortString(): String? = DATE_FORMAT.format(this) - -fun Date.formatToYearMonthDay(): String = DATE_FORMAT.format(this) - -/** - * Convert String to Spanned - */ -fun String.htmlToSpanned(): Spanned{ - var html = this - - HTML_TAGS.forEach { (pattern, span) -> - html = pattern.matcher(html).replaceAll(span) - } - - return html.parseAsHtml() -} - -/** - * Support custom SPAN class - * https://portal.student.kit.ac.jp/css/common/wb_common.css - */ -private val HTML_TAGS by lazy { - mapOf( - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("(.*?)") to "\$3( \$1 )", - Pattern.compile("(.*?)") to "\$1", - Pattern.compile("([A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}?)") to " \$1 " - ) -} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/util/Murmur3.java b/app/src/main/java/jp/kentan/studentportalplus/util/Murmur3.java index 1a776f0..93c4422 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/util/Murmur3.java +++ b/app/src/main/java/jp/kentan/studentportalplus/util/Murmur3.java @@ -18,6 +18,8 @@ package jp.kentan.studentportalplus.util; +import kotlin.text.Charsets; + /** * Murmur3 is successor to Murmur2 fast non-crytographic hash algorithms. * @@ -131,6 +133,10 @@ public static long hash64(byte[] data) { return hash64(data, 0, data.length, DEFAULT_SEED); } + public static long hash64(String data) { + return hash64(data.getBytes(Charsets.UTF_8)); + } + public static long hash64(byte[] data, int offset, int length) { return hash64(data, offset, length, DEFAULT_SEED); } diff --git a/app/src/main/java/jp/kentan/studentportalplus/util/SharedPreferencesHelper.kt b/app/src/main/java/jp/kentan/studentportalplus/util/SharedPreferencesHelper.kt index 6e808fe..87f6197 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/util/SharedPreferencesHelper.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/util/SharedPreferencesHelper.kt @@ -1,29 +1,56 @@ package jp.kentan.studentportalplus.util +import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import jp.kentan.studentportalplus.data.component.PortalData +import jp.kentan.studentportalplus.notification.NotificationType +import org.jetbrains.anko.defaultSharedPreferences +import java.util.* -/** - * SharedPreferencesHelper - */ +fun SharedPreferences.isAuthenticatedUser() = getBoolean("is_authenticated_user", false) -fun SharedPreferences.enabledDetailError() = getBoolean("enabled_detail_error", false) +fun SharedPreferences.isEnabledDetailError() = getBoolean("is_enabled_detail_error", false) -fun SharedPreferences.enabledPdfOpenWithGdocs() = getBoolean("enabled_pdf_open_with_gdocs", true) +fun SharedPreferences.isEnabledPdfOpenWithGdocs() = getBoolean("is_enabled_pdf_open_with_gdocs", true) -fun SharedPreferences.isFirstLaunch() = getBoolean("is_first_launch", true) +fun SharedPreferences.isGridTimetableLayout() = getBoolean("is_grid_timetable_layout", true) -fun SharedPreferences.setFirstLaunch(isFirst: Boolean) = edit { putBoolean("is_first_launch", isFirst) } +fun SharedPreferences.isEnabledSync() = getBoolean("is_enabled_sync", true) -fun SharedPreferences.enabledSync() = getBoolean("enabled_sync", true) +fun SharedPreferences.isEnabledNotificationVibration() = getBoolean("is_enabled_notification_vibration", true) -fun SharedPreferences.getMyClassThreshold() = (getString("my_class_threshold", "80").toIntOrNull() ?: 80) / 100f +fun SharedPreferences.isEnabledNotificationLed() = getBoolean("is_enabled_notification_led", true) -fun SharedPreferences.getSyncIntervalMinutes() = getString("sync_interval", "60").toLongOrNull() ?: 60L +fun SharedPreferences.getShibbolethLastLoginDate() = getLong("shibboleth_last_login_date", -1) -// For notification -fun SharedPreferences.enabledNotificationVibration() = getBoolean("enabled_notification_vibration", true) -fun SharedPreferences.enabledNotificationLed() = getBoolean("enabled_notification_led", true) +fun SharedPreferences.getSyncIntervalMinutes() = getString("sync_interval_minutes", "60")?.toLongOrNull() ?: 60L + +fun SharedPreferences.getSimilarSubjectThreshold() = getString("similar_subject_threshold", "80")?.toIntOrNull() ?: 80 + +fun SharedPreferences.getSimilarSubjectThresholdFloat() = getSimilarSubjectThreshold() / 100f fun SharedPreferences.getNotificationId() = getInt("notification_id", 1) -fun SharedPreferences.setNotificationId(id: Int) = edit { putInt("notification_id", id) } \ No newline at end of file + +fun SharedPreferences.getNotificationType(type: PortalData): NotificationType { + val key = when (type) { + PortalData.NOTICE -> "notification_type_notice" + PortalData.LECTURE_INFO -> "notification_type_lecture_info" + PortalData.LECTURE_CANCEL -> "notification_type_lecture_cancel" + PortalData.MY_CLASS -> "notification_type_my_class" + } + + return NotificationType.valueOf(getString(key, null) ?: NotificationType.ALL.name) +} + +fun SharedPreferences.setAuthenticatedUser(isAuthenticated: Boolean) = edit { putBoolean("is_authenticated_user", isAuthenticated) } + +fun SharedPreferences.setGridTimetableLayout(isGrid: Boolean) = edit { putBoolean("is_grid_timetable_layout", isGrid) } + +fun SharedPreferences.setNotificationId(id: Int) = edit { putInt("notification_id", id) } + +fun Context.updateShibbolethLastLoginDate() { + defaultSharedPreferences.edit { + putLong("shibboleth_last_login_date", Date().time) + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/view/text/CustomTabsUrlSpan.kt b/app/src/main/java/jp/kentan/studentportalplus/view/text/CustomTabsUrlSpan.kt new file mode 100644 index 0000000..b8a1ae7 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/view/text/CustomTabsUrlSpan.kt @@ -0,0 +1,37 @@ +package jp.kentan.studentportalplus.view.text + +import android.content.Context +import android.text.style.URLSpan +import android.view.View +import androidx.core.net.toUri +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.util.CustomTabsHelper +import jp.kentan.studentportalplus.util.buildCustomTabsIntent +import jp.kentan.studentportalplus.util.isEnabledPdfOpenWithGdocs +import org.jetbrains.anko.defaultSharedPreferences +import org.jetbrains.anko.longToast + +class CustomTabsUrlSpan( + private val context: Context, url: String +) : URLSpan(url) { + + override fun onClick(widget: View?) { + val isPdf = url.endsWith(".pdf", true) + + var urlStr = url + if (isPdf && context.defaultSharedPreferences.isEnabledPdfOpenWithGdocs()) { + val isRequireLogin = url.startsWith("https://portal.student.kit.ac.jp", true) + + if (isRequireLogin) { + context.longToast(R.string.error_gdocs_require_login) + } else { + urlStr = context.getString(R.string.url_gdocs, url) + } + } + + context.buildCustomTabsIntent().run { + intent.`package` = CustomTabsHelper.getPackageNameToUse(context) + launchUrl(context, urlStr.toUri()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/util/CustomTransformationMethod.kt b/app/src/main/java/jp/kentan/studentportalplus/view/text/LinkTransformationMethod.kt similarity index 63% rename from app/src/main/java/jp/kentan/studentportalplus/util/CustomTransformationMethod.kt rename to app/src/main/java/jp/kentan/studentportalplus/view/text/LinkTransformationMethod.kt index 41d4bdf..21d7f5b 100644 --- a/app/src/main/java/jp/kentan/studentportalplus/util/CustomTransformationMethod.kt +++ b/app/src/main/java/jp/kentan/studentportalplus/view/text/LinkTransformationMethod.kt @@ -1,4 +1,4 @@ -package jp.kentan.studentportalplus.util +package jp.kentan.studentportalplus.view.text import android.content.Context import android.graphics.Rect @@ -9,9 +9,10 @@ import android.text.style.URLSpan import android.util.Patterns import android.view.View import android.widget.TextView -import jp.kentan.studentportalplus.ui.span.CustomTabsUrlSpan -class CustomTransformationMethod(private val context: Context) : TransformationMethod { +class LinkTransformationMethod( + private val context: Context +) : TransformationMethod { override fun getTransformation(source: CharSequence, view: View): CharSequence { if (view is TextView) { @@ -23,14 +24,14 @@ class CustomTransformationMethod(private val context: Context) : TransformationM val spans = text.getSpans(0, view.length(), URLSpan::class.java) spans.forEach { span -> val url = span.url - if (!Patterns.WEB_URL.matcher(url).matches()) { - return@forEach - } - val start = text.getSpanStart(span) - val end = text.getSpanEnd(span) - text.removeSpan(span) - text.setSpan(CustomTabsUrlSpan(context, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + if (Patterns.WEB_URL.matcher(url).matches()) { + val start = text.getSpanStart(span) + val end = text.getSpanEnd(span) + + text.removeSpan(span) + text.setSpan(CustomTabsUrlSpan(context, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } } return text } diff --git a/app/src/main/java/jp/kentan/studentportalplus/view/widget/CheckableFloatingActionButton.kt b/app/src/main/java/jp/kentan/studentportalplus/view/widget/CheckableFloatingActionButton.kt new file mode 100644 index 0000000..682aa13 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/view/widget/CheckableFloatingActionButton.kt @@ -0,0 +1,29 @@ +package jp.kentan.studentportalplus.view.widget + +import android.content.Context +import android.util.AttributeSet +import android.widget.Checkable +import com.google.android.material.floatingactionbutton.FloatingActionButton + +abstract class CheckableFloatingActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = com.google.android.material.R.attr.floatingActionButtonStyle +) : FloatingActionButton(context, attrs, defStyleAttr), Checkable { + + private var isChecked = false + + override fun isChecked() = isChecked + + override fun toggle() { + isChecked = !isChecked + onCheckedChange() + } + + override fun setChecked(checked: Boolean) { + isChecked = checked + onCheckedChange() + } + + abstract fun onCheckedChange() +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/view/widget/CustomTabsTextView.kt b/app/src/main/java/jp/kentan/studentportalplus/view/widget/CustomTabsTextView.kt new file mode 100644 index 0000000..2df28cd --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/view/widget/CustomTabsTextView.kt @@ -0,0 +1,60 @@ +package jp.kentan.studentportalplus.view.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.text.parseAsHtml +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.view.text.LinkTransformationMethod +import java.util.regex.Pattern + + +class CustomTabsTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle +) : AppCompatTextView(context, attrs, defStyleAttr) { + + private companion object { + /** + * Support custom SPAN class + * https://portal.student.kit.ac.jp/css/common/wb_common.css + */ + val HTML_TAGS by lazy(LazyThreadSafetyMode.NONE) { + mapOf( + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("(.*?)") to "\$3( \$1 )", + Pattern.compile("(.*?)") to "\$1", + Pattern.compile("([A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}?)") to " \$1 " + ) + } + } + + init { + context.obtainStyledAttributes(attrs, R.styleable.CustomTabsTextView, defStyleAttr, 0).apply { + setHtml(getString(R.styleable.CustomTabsTextView_html)) + recycle() + } + + transformationMethod = LinkTransformationMethod(context) + } + + fun setHtml(html: String?) { + var htmlText = html ?: run { + text = null + return + } + + HTML_TAGS.forEach { (pattern, span) -> + htmlText = pattern.matcher(htmlText).replaceAll(span) + } + + text = htmlText.parseAsHtml() + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/view/widget/DividerItemDecoration.kt b/app/src/main/java/jp/kentan/studentportalplus/view/widget/DividerItemDecoration.kt new file mode 100644 index 0000000..e346f3f --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/view/widget/DividerItemDecoration.kt @@ -0,0 +1,61 @@ +package jp.kentan.studentportalplus.view.widget + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import androidx.recyclerview.widget.RecyclerView + + +class DividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { + + private val bounds = Rect() + private val divider: Drawable + + init { + val attrs = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider)) + + divider = attrs.getDrawable(0) ?: throw IllegalArgumentException("@android:attr/listDivider was not set.") + attrs.recycle() + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + canvas.save() + + val left: Int + val right: Int + + if (parent.clipToPadding) { + left = parent.paddingLeft + right = parent.width - parent.paddingRight + canvas.clipRect(left, parent.paddingTop, right, parent.height - parent.paddingBottom) + } else { + left = 0 + right = parent.width + } + + val childCount = parent.childCount + for (i in 0..childCount - 2) { + val child = parent.getChildAt(i) + parent.getDecoratedBoundsWithMargins(child, bounds) + val bottom = bounds.bottom + Math.round(child.translationY) + val top = bottom - divider.intrinsicHeight + divider.setBounds(left, top, right, bottom) + divider.draw(canvas) + } + + canvas.restore() + } + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + + // Last position + if (parent.getChildAdapterPosition(view) == state.itemCount -1) { + outRect.set(0, 0, 0, 0) + return + } + + outRect.set(0, 0, 0, divider.intrinsicHeight) + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/view/widget/FavoriteFloatingActionButton.kt b/app/src/main/java/jp/kentan/studentportalplus/view/widget/FavoriteFloatingActionButton.kt new file mode 100644 index 0000000..7c10014 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/view/widget/FavoriteFloatingActionButton.kt @@ -0,0 +1,41 @@ +package jp.kentan.studentportalplus.view.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.animation.OvershootInterpolator +import jp.kentan.studentportalplus.R + +class FavoriteFloatingActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = com.google.android.material.R.attr.floatingActionButtonStyle +) : CheckableFloatingActionButton(context, attrs, defStyleAttr) { + + private companion object { + const val ROTATION_FROM = 0f + const val ROTATION_TO = 144f + const val DURATION = 800L + } + + private val interpolator = OvershootInterpolator() + private var isInitialized = false + + override fun onCheckedChange() { + val isFavorite = isChecked + + setImageResource(if (isFavorite) R.drawable.ic_star else R.drawable.ic_star_border) + + if (!isInitialized) { + rotation = if (isFavorite) ROTATION_TO else ROTATION_FROM + isInitialized = true + return + } + + animate() + .rotation(if (isFavorite) ROTATION_TO else ROTATION_FROM) + .setDuration(DURATION) + .setInterpolator(interpolator) + .start() + } + +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/view/widget/LectureAttendFloatingActionButton.kt b/app/src/main/java/jp/kentan/studentportalplus/view/widget/LectureAttendFloatingActionButton.kt new file mode 100644 index 0000000..4ccf6a9 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/view/widget/LectureAttendFloatingActionButton.kt @@ -0,0 +1,49 @@ +package jp.kentan.studentportalplus.view.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.animation.AnticipateOvershootInterpolator +import jp.kentan.studentportalplus.data.component.LectureAttend + +class LectureAttendFloatingActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = com.google.android.material.R.attr.floatingActionButtonStyle +) : CheckableFloatingActionButton(context, attrs, defStyleAttr) { + + private companion object { + const val ROTATION_FROM = 0f + const val ROTATION_TO = 135f + const val DURATION = 800L + } + + private val interpolator = AnticipateOvershootInterpolator() + private var isInitialized = false + + fun setAttend(attend: LectureAttend?) { + attend ?: return + + if (attend == LectureAttend.PORTAL) { + hide() + } else { + show() + isChecked = attend == LectureAttend.USER + } + } + + override fun onCheckedChange() { + val isAttend = isChecked + + if (!isInitialized) { + rotation = if (isAttend) ROTATION_TO else ROTATION_FROM + isInitialized = true + return + } + + animate() + .rotation(if (isAttend) ROTATION_TO else ROTATION_FROM) + .setDuration(DURATION) + .setInterpolator(interpolator) + .start() + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/kentan/studentportalplus/view/widget/SimilarSubjectSamplePreference.kt b/app/src/main/java/jp/kentan/studentportalplus/view/widget/SimilarSubjectSamplePreference.kt new file mode 100644 index 0000000..07115b7 --- /dev/null +++ b/app/src/main/java/jp/kentan/studentportalplus/view/widget/SimilarSubjectSamplePreference.kt @@ -0,0 +1,77 @@ +package jp.kentan.studentportalplus.view.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import jp.kentan.studentportalplus.R +import jp.kentan.studentportalplus.util.JaroWinklerDistance +import jp.kentan.studentportalplus.util.getSimilarSubjectThresholdFloat +import org.jetbrains.anko.defaultSharedPreferences +import org.jetbrains.anko.find + +class SimilarSubjectSamplePreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle +) : Preference(context, attrs, defStyleAttr) { + + private companion object { + val SUBJECTS = arrayOf("ABC実験ma", "ABC実験ma~mc", "ABC実験 ガイダンス", "XYZ実験ma") + } + + private val stringDistance = JaroWinklerDistance() + private val iconViews = arrayOfNulls(4) + private var threshold = context.defaultSharedPreferences.getSimilarSubjectThresholdFloat() + + init { + widgetLayoutResource = R.layout.preference_subject_similar_sample + } + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + holder.itemView.apply { + isClickable = false + + val itemList = listOf(find(R.id.lecture1), find(R.id.lecture2), find(R.id.lecture3), find(R.id.lecture4)) + + var indexChar = 'A' + itemList.forEachIndexed { index, item -> + item.find(R.id.date).text = "2018/01/0${index+1}" + item.find(R.id.subject).text = SUBJECTS[index] + item.find(R.id.detail).text = "詳細テキスト${indexChar++}" + + iconViews[index] = item.find(R.id.attend_image) + } + } + + updateIconView() + } + + private fun updateIconView() { + iconViews.forEachIndexed { index, icon -> + if (index <= 0) { + icon?.setImageResource(R.drawable.ic_lecture_attend) + return@forEachIndexed + } + + if (stringDistance.getDistance(SUBJECTS[0], SUBJECTS[index]) >= threshold) { + icon?.setImageResource(R.drawable.ic_lecture_attend_similar) + } else { + icon?.setImageResource(R.drawable.ic_lecture_attend_not) + } + } + } + + fun updateThreshold(percent: Int) { + threshold = percent / 100f + + updateIconView() + } +} \ No newline at end of file diff --git a/app/src/main/res/animator/fade_in.xml b/app/src/main/res/animator/fade_in.xml deleted file mode 100644 index 7cbaf8c..0000000 --- a/app/src/main/res/animator/fade_in.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/animator/fade_out.xml b/app/src/main/res/animator/fade_out.xml deleted file mode 100644 index 3758a2c..0000000 --- a/app/src/main/res/animator/fade_out.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/color/drawer_item_icon.xml b/app/src/main/res/color/drawer_item_icon.xml index 04cee9d..8a0a8ac 100644 --- a/app/src/main/res/color/drawer_item_icon.xml +++ b/app/src/main/res/color/drawer_item_icon.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/chip.xml b/app/src/main/res/drawable/chip.xml deleted file mode 100644 index c1ff83a..0000000 --- a/app/src/main/res/drawable/chip.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_account.xml b/app/src/main/res/drawable/ic_account.xml index ba703e0..f66b7c9 100644 --- a/app/src/main/res/drawable/ic_account.xml +++ b/app/src/main/res/drawable/ic_account.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml index e3979cd..b9b8eca 100644 --- a/app/src/main/res/drawable/ic_add.xml +++ b/app/src/main/res/drawable/ic_add.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_check_accent.xml b/app/src/main/res/drawable/ic_check_accent.xml new file mode 100644 index 0000000..0fc3973 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_accent.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_close_black.xml b/app/src/main/res/drawable/ic_close_black.xml index 0e33c59..d642c36 100644 --- a/app/src/main/res/drawable/ic_close_black.xml +++ b/app/src/main/res/drawable/ic_close_black.xml @@ -1,9 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml index 8bed121..1f2b6e7 100644 --- a/app/src/main/res/drawable/ic_delete.xml +++ b/app/src/main/res/drawable/ic_delete.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml index 46462b5..cb2e394 100644 --- a/app/src/main/res/drawable/ic_edit.xml +++ b/app/src/main/res/drawable/ic_edit.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_lock_off.xml b/app/src/main/res/drawable/ic_lock_off.xml index 7edfe01..c4efaaa 100644 --- a/app/src/main/res/drawable/ic_lock_off.xml +++ b/app/src/main/res/drawable/ic_lock_off.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/ic_lock_on.xml b/app/src/main/res/drawable/ic_lock_on.xml index d3777c6..2d2a500 100644 --- a/app/src/main/res/drawable/ic_lock_on.xml +++ b/app/src/main/res/drawable/ic_lock_on.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/ic_menu_filter_list.xml b/app/src/main/res/drawable/ic_menu_filter.xml similarity index 100% rename from app/src/main/res/drawable/ic_menu_filter_list.xml rename to app/src/main/res/drawable/ic_menu_filter.xml diff --git a/app/src/main/res/drawable/ic_menu_setting.xml b/app/src/main/res/drawable/ic_menu_setting.xml index 9aa0f3d..ace746c 100644 --- a/app/src/main/res/drawable/ic_menu_setting.xml +++ b/app/src/main/res/drawable/ic_menu_setting.xml @@ -1,11 +1,9 @@ + android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/> diff --git a/app/src/main/res/drawable/ic_palette.xml b/app/src/main/res/drawable/ic_palette.xml new file mode 100644 index 0000000..4d7a2df --- /dev/null +++ b/app/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_pref_info.xml b/app/src/main/res/drawable/ic_pref_info.xml new file mode 100644 index 0000000..dc7b049 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_info.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_pref_lecture_cancel.xml similarity index 53% rename from app/src/main/res/drawable/ic_check.xml rename to app/src/main/res/drawable/ic_pref_lecture_cancel.xml index c632cd1..2be6a45 100644 --- a/app/src/main/res/drawable/ic_check.xml +++ b/app/src/main/res/drawable/ic_pref_lecture_cancel.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> + android:fillColor="@color/icon" + android:pathData="M7,13c1.66,0 3,-1.34 3,-3S8.66,7 7,7s-3,1.34 -3,3 1.34,3 3,3zM19,7h-8v7L3,14L3,5L1,5v15h2v-3h18v3h2v-9c0,-2.21 -1.79,-4 -4,-4z"/> diff --git a/app/src/main/res/drawable/ic_pref_lecture_info.xml b/app/src/main/res/drawable/ic_pref_lecture_info.xml new file mode 100644 index 0000000..95a0224 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_lecture_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pref_login.xml b/app/src/main/res/drawable/ic_pref_login.xml new file mode 100644 index 0000000..b38b27e --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_login.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_pref_notice.xml b/app/src/main/res/drawable/ic_pref_notice.xml new file mode 100644 index 0000000..d7dea2c --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_notice.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_pref_similar_subject.xml b/app/src/main/res/drawable/ic_pref_similar_subject.xml new file mode 100644 index 0000000..321bd23 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_similar_subject.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_pref_sync.xml b/app/src/main/res/drawable/ic_pref_sync.xml new file mode 100644 index 0000000..a9fd889 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_sync.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml deleted file mode 100644 index f21de6c..0000000 --- a/app/src/main/res/drawable/ic_refresh.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_retry.xml b/app/src/main/res/drawable/ic_retry.xml new file mode 100644 index 0000000..fabb503 --- /dev/null +++ b/app/src/main/res/drawable/ic_retry.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index 9040666..1af7cfe 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -1,9 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/ic_view_day.xml b/app/src/main/res/drawable/ic_view_day.xml deleted file mode 100644 index 16170e6..0000000 --- a/app/src/main/res/drawable/ic_view_day.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_view_list.xml b/app/src/main/res/drawable/ic_view_list.xml new file mode 100644 index 0000000..0711abc --- /dev/null +++ b/app/src/main/res/drawable/ic_view_list.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_week.xml b/app/src/main/res/drawable/ic_view_week.xml index 931cf37..2dd91bb 100644 --- a/app/src/main/res/drawable/ic_view_week.xml +++ b/app/src/main/res/drawable/ic_view_week.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml index 844813e..20bef0d 100644 --- a/app/src/main/res/drawable/ic_warning.xml +++ b/app/src/main/res/drawable/ic_warning.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/layout/activity_lecture_cancel_detail.xml b/app/src/main/res/layout/activity_lecture_cancel_detail.xml new file mode 100644 index 0000000..330544f --- /dev/null +++ b/app/src/main/res/layout/activity_lecture_cancel_detail.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_lecture_cancellation.xml b/app/src/main/res/layout/activity_lecture_cancellation.xml deleted file mode 100644 index e9ec913..0000000 --- a/app/src/main/res/layout/activity_lecture_cancellation.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_lecture_info_detail.xml b/app/src/main/res/layout/activity_lecture_info_detail.xml new file mode 100644 index 0000000..c30e3a2 --- /dev/null +++ b/app/src/main/res/layout/activity_lecture_info_detail.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_lecture_information.xml b/app/src/main/res/layout/activity_lecture_information.xml deleted file mode 100644 index 37dff3c..0000000 --- a/app/src/main/res/layout/activity_lecture_information.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 46d48e8..52ef0a0 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -1,90 +1,132 @@ - + - - - - - + + + + + + - + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + app:isVisible="@{viewModel.loading}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + + + android:clipToPadding="false" + android:padding="16dp"> - + - + android:layout_marginTop="16dp" + android:hint="@string/hint_username" + app:errorEnabled="@{safeUnbox(viewModel.isEnabledErrorUsername)}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/info"> - + - + - + android:layout_marginTop="16dp" + android:hint="@string/hint_password" + app:errorEnabled="@{safeUnbox(viewModel.isEnabledErrorPassword)}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/username_layout" + app:passwordToggleEnabled="true"> - + - + -