Compare commits

...

171 Commits

Author SHA1 Message Date
Jannik 28520bee74
changelog: minor text fixes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-09-04 11:08:50 +02:00
Jannik 0c6a486dd9 Merge pull request 'version 0.6.0' (#46) from develop into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Reviewed-on: #46
2020-08-25 19:24:49 +02:00
Jannik ec15c79b63
update fastlane screenshot
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2020-08-25 12:05:22 +02:00
Jannik 4592d1984f
release version 0.6.0
continuous-integration/drone/push Build is passing Details
2020-08-25 11:55:55 +02:00
Jannik aae74cf9db „README.md“ ändern
continuous-integration/drone/push Build is passing Details
2020-08-25 11:06:38 +02:00
Jannik e19b0db39d
add missing licenses to license dialog
continuous-integration/drone/push Build is passing Details
2020-08-25 10:52:59 +02:00
Jannik 23fd52b0c5
fab: use plus as icon
continuous-integration/drone/push Build is passing Details
2020-08-24 20:22:46 +02:00
Jannik 62a62049c3
code clean up
continuous-integration/drone/push Build is passing Details
2020-08-23 14:27:41 +02:00
Jannik 849698779b
add icon as svg
continuous-integration/drone/push Build is passing Details
* recreate all launcher icons
2020-08-23 14:06:28 +02:00
Jannik a42a92a3c5
update constraintlayout
continuous-integration/drone/push Build is passing Details
* constraintlayout 2.0.0.rc1 -> 2.0.0
2020-08-22 20:35:44 +02:00
Jannik be30e2f968
enable optimisations and minifying for release builds
continuous-integration/drone/push Build is passing Details
* closes #19
* update gradle wrapper to version 6.6
2020-08-22 20:20:23 +02:00
Jannik c629b2aec2
fix login button
continuous-integration/drone/push Build is passing Details
* fix login button action in Onboarding
* add release changelog
* update description
* use new version code scheme
2020-08-21 19:03:28 +02:00
Jannik 99ba87c3f6
init fragment in onViewCreated()
continuous-integration/drone/push Build is passing Details
* closes #42
* show a info text if no mensa menus where found
* the refresh spinner respects the selecte theme now
2020-08-17 22:30:12 +02:00
Jannik 953b4825a9
check qispos status and show message if unavailable
continuous-integration/drone/push Build is passing Details
2020-08-16 21:30:18 +02:00
Jannik 2807843d25
fix grades divider shown when sub-subject was added
continuous-integration/drone/push Build is passing Details
2020-08-15 18:15:36 +02:00
Jannik 638c321798
grades ui polishing & sorting fix
continuous-integration/drone/push Build is passing Details
2020-08-15 18:13:29 +02:00
Jannik 42d938b0bc
fix background in fragment_grades
continuous-integration/drone/push Build is passing Details
* disable swipe in fragment_grades
2020-08-14 21:57:09 +02:00
Jannik 9d7504bbaf
add grades ui
continuous-integration/drone/push Build is passing Details
2020-08-14 21:41:43 +02:00
Jannik 8ecfe8f120
QISPOSParser [Part 2]
continuous-integration/drone/push Build is passing Details
* parse grades and store them in a HashMap per semester
2020-08-14 15:08:42 +02:00
Jannik ff0c4ad1a7
QISPOSParser [Part 1]
continuous-integration/drone/push Build is passing Details
* get the grades url from qispos
2020-08-13 21:01:21 +02:00
Jannik ea0caea91e
save email and password to encrypted preference
continuous-integration/drone/push Build is passing Details
2020-08-12 11:00:22 +02:00
Jannik 1f660bdd37
add login dialog
continuous-integration/drone/push Build is passing Details
* first step towards a working his online integration
* rework AddSubjectDialog
* activate fragment_on_login
2020-08-11 17:09:46 +02:00
Jannik be43d87b1a
fix more lint warnings
continuous-integration/drone/push Build is passing Details
2020-08-10 15:11:36 +02:00
Jannik dd5c7b3fb8
fix some lint warnings
continuous-integration/drone/push Build is passing Details
* remove unused resources
2020-08-10 14:51:40 +02:00
Jannik fb3dab6dc3
save additional subjects and load them on start
continuous-integration/drone/push Build is passing Details
* the app now saves all aditional subjects and lessons and loads them on start
* udpate gradle 6.4.0 -> 6.5.1
* update coroutines 1.3.7 -> 1.3.8
* update several androidx libraries
2020-08-10 14:31:17 +02:00
Jannik bcfdb83d14
rework CacheController and TCoRAPIController
continuous-integration/drone/push Build is passing Details
* all cache file stuff is now handled in the CacheController
* TCoRAPIController only handles API and Gson stuff
2020-08-07 12:14:50 +02:00
Jannik 8c55366ec0
update libs
continuous-integration/drone/push Build is passing Details
* gradle plugin 4.0.0 -> 4.0.1
2020-07-15 16:02:58 +02:00
Jannik 6f68fbb73c „README.md“ ändern
continuous-integration/drone/push Build is passing Details
2020-06-25 17:30:56 +02:00
Jannik 297dd77e79
use gradle wrapper for drone
continuous-integration/drone/push Build is passing Details
2020-06-25 17:25:58 +02:00
Jannik 6a70f26807
update .drone.yml
continuous-integration/drone/push Build is failing Details
2020-06-25 17:22:06 +02:00
Jannik f6b00a8d81
additional Subject are now added to the timetable 2020-06-12 00:51:50 +02:00
Jannik 69ce9fef5a
code clean up 2020-06-11 21:51:46 +02:00
Jannik 2d753851c0
more "Additional Lessons" work
* add TimetableController, this class will handle all timetable work (main + additional Subjects)
2020-06-11 21:41:05 +02:00
Jannik 6c0624c793
make the app more tolerant about wrong API Data 2020-06-11 18:39:19 +02:00
Jannik 7779296345
change default accent color, code clean up 2020-06-07 23:41:49 +02:00
Jannik 3e2ef0521e
change primary color to reflect mosad.xyz design guidelines
* minor style fix: nav_header_main set background to black and text color to white
2020-06-07 23:30:41 +02:00
Jannik bdfeb46faf
add "Manage Lessons" Dialog to Settings, minor style fixes
* initially select the current theme in the select theme dialog
* set the correct accent color for all dialogs (needs to be done manually for every dialog)
2020-06-07 14:59:04 +02:00
Jannik 18ca435764
update TCoRAPIController to API version 1.2.0 2020-06-06 20:56:45 +02:00
Jannik 948f330ebe
TimeTableFragment clean up 2020-06-05 20:56:37 +02:00
Jannik 72e9efb9e7
fix warings in TCoRAPIController 2020-06-05 19:17:49 +02:00
Jannik bea1b47396
complete & move AddLessonDialog to separate class 2020-06-05 16:12:53 +02:00
Jannik 34a68ff75d
add courses to addLesson Dialog, read lessonSubjects from tcor 2020-05-29 16:56:15 +02:00
Jannik 1ba3f1fa87
update gradle tool to 4.0.0 2020-05-28 23:27:16 +02:00
Jannik 48544aef2f
add a complete version of the addLesson Dialog 2020-05-15 18:58:57 +02:00
Jannik bc09e33147
update gradle, constraintlayout & coroutines
* gradle 6.2.1 -> 6.4
* constraintlayout 2.0.0-beta4 -> 2.0.0-beta6
* coroutines 1.3.5 -> 1.3.6
2020-05-14 18:27:26 +02:00
Jannik 6ec9b9f5e2
update commons-lang and coroutines
* commons-lang 3.9 -> 3.10
* coroutines 1.3.3 -> 1.3.5
2020-05-01 12:48:53 +02:00
Jannik a19173b499
update kotlin
* kotlin 1.3.70 -> 1.3.71
* gradle-plugin 3.6.1 -> 3.6.2
2020-04-02 17:15:34 +02:00
Jannik 85dc3181fd
fix compatibility with tcor version 1.2.2 and higher
* update kotlin 1.3.61 -> 1.3.70
2020-03-03 20:13:47 +01:00
Jannik e46c234b6c
fix OnboardingActivity is not closed on start of MainActivity 2020-02-27 12:13:46 +01:00
Jannik faa07966da
fix crash on first start, if tcor is not reachable 2020-02-27 12:09:24 +01:00
Jannik ccc0f0f2bc
Onboarding [Part 2]
* call the onboarding activity on first start
* reworked selectCourse(() to work outside of the SettingsFragment
* closes #36
2020-02-25 15:25:45 +01:00
Jannik e15bf95b85
update to gradle 6.2.1 and gradle android plugin 3.6.0 2020-02-25 11:12:22 +01:00
Jannik 01677c04ef
Onboarding [Part 1]
this Commit adds a Onboarding Activity, it is currently not usefull or fully working, see #36
2020-02-23 20:09:27 +01:00
Jannik 6e9c63d3d4 Merge branch 'develop' of Seil0/ProjectLaogai into master [Hotfix] 2020-02-18 15:24:08 +01:00
Jannik c343735b57
fix crash on first startup 2020-02-18 15:18:36 +01:00
Jannik 9c5274dc06 Merge branch 'develop' of Seil0/ProjectLaogai into master
release version 0.5.1
2020-02-05 14:15:04 +01:00
Jannik b186a2e96e
release version 0.5.1 2020-02-05 14:10:45 +01:00
Jannik 2cb4b72369
anko code removed, coroutines are now used for asynchronous functions
* constraintlayout 2.0.0-beta3 -> 2.0.0-beta4
* material-components-android 1.0.0 -> 1.1.0
* this is the first release candidate for version 0.5.1
2020-02-04 19:58:07 +01:00
Jannik 2d497d1a96
update gradle wrapper to version 6.0.1 2020-01-15 15:28:07 +01:00
Jannik 9d2de3fcb3
don't use deprecated Gson methods 2020-01-15 15:08:05 +01:00
Jannik bed3f5d978
added Timetable and Moodle shortcut, cleaned up activity init 2020-01-15 15:00:05 +01:00
Jannik 8f5a4dd1b3
reworked preference saving 2019-12-13 14:54:51 +01:00
Jannik 9907d083e9
added a Mensa shortcut
* added a Mensa shortcut, to directly display the Mensa-Screen
* kotlin 1.3.60 -> 1.3.61
2019-11-28 16:38:25 +01:00
Jannik d4860b2a32
updated some libs
* kotlin 1.3.50 -> 1.3.60
* androidx.constraintlayout 2.0.0-beta2 -> 2.0.0-beta3
2019-11-14 23:11:49 +01:00
Jannik 5ec2b53bce
added "Get it on Google Play" badge 2019-10-24 22:22:17 +02:00
Jannik 701a351e6e
update .drone.yml
continuous-integration/drone/push Build encountered an error Details
2019-10-17 19:21:15 +02:00
Jannik 5cad924b26
removed anko dependency from PreferenceController
continuous-integration/drone/push Build encountered an error Details
2019-10-17 19:19:47 +02:00
Jannik 605bf6248d release 0.5.0
Merge branch 'develop' of Seil0/ProjectLaogai into master
2019-09-30 21:28:54 +02:00
Jannik d5adc4df51
release 0.5.0 2019-09-30 21:25:38 +02:00
Jannik c23454f081
removed versionNameSuffix, f-droid does not like that 2019-09-28 15:01:19 +02:00
Jannik b240beacc9
turns out there is a current week too
* don't debug timetable cod on Sundays, since there are no lessons on Sunday
2019-09-25 22:46:58 +02:00
Jannik aa69d2242f
fixed course name not being updated on change
* minor GUI improvements
2019-09-23 22:44:03 +02:00
Jannik 6e6c9f71a0
reworked the way the timetable date is calculated
* cleaned up some timetable gui code
* close #33
2019-09-22 20:51:36 +02:00
Jannik 889c673c5d Update 'README.md' 2019-09-08 20:35:06 +02:00
Jannik 522f31e077
added new screenshots 2019-09-08 20:30:03 +02:00
Jannik b9ca18044c
drone use gradle test instead of assembleRelease
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
2019-09-07 21:10:51 +02:00
Jannik bd49e482e2
updated some libs
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
* appcompat 1.0.2 -> 1.1.0
* material-dialogs 3.1.0 -> 3.1.1
2019-09-07 21:06:41 +02:00
Jannik 2f5b2a6579
added drone ci
* updated gradle wrapper to gradle 5.6.2
2019-09-07 21:01:17 +02:00
Jannik 4ce37b3dcf
updated desfire legal notes 2019-08-30 14:17:07 +02:00
Jannik 7550fcfd22 Update 'README.md' 2019-08-30 14:12:17 +02:00
Jannik a23f7c9212
update to release 0.5.0 2019-08-30 14:09:59 +02:00
Jannik 75168c6688
some more logging and version 0.4.99 (RC1) 2019-08-25 22:45:23 +02:00
Jannik be916a74ab
the license dialog will now follow the theme too 2019-08-25 22:19:36 +02:00
Jannik 3e909ab68f
color the selected element in the NavigationView too 2019-08-23 20:25:20 +02:00
Jannik 5f2b3aa496
clean up the layout & polished Mensa credit dialog
* don't crash if the nfc tag has been removed between triggering intent and connecting to the tag
* closes #21
2019-08-23 19:19:35 +02:00
Jannik 6cc1671a52
don't crash on devices without nfc 2019-08-21 20:43:08 +02:00
Jannik b4ed1ca927
migrated all farebot code to kotlin 2019-08-19 12:42:56 +02:00
Jannik e74c307566
cleand up the nfc part, added foreground-dispatch
* cleaned up the strings.xml files
* fixed license dialog
2019-08-18 22:09:33 +02:00
Jannik 733b675ffa
first working implementation of the mensa card reader
* based on farebot's desfire protocol implementation
* see #21
2019-08-17 18:59:28 +02:00
Jannik 3a0a31f781
first bits of the nfc reader 2019-08-17 01:51:15 +02:00
Jannik f52151fbf1
the no lesson card now has the correct text color
* updated material-dialogs to version 3.1.0
2019-07-18 00:32:31 +02:00
Jannik e8cae7e807
update to gradle 5.5.1, update gradle plugin to 3.4.2 2019-07-18 00:15:57 +02:00
Jannik c6ac19bfae
fixed the Sunday-fix 2019-07-01 20:31:06 +02:00
Jannik a7abd48726
moved all cache checking code tho the CacheController
* added new sunday bug fix for the timetable, same as for the mensa
2019-06-26 15:00:43 +02:00
Jannik 9a22d9b737
use a ScrollView for the settings fragment 2019-06-26 14:11:32 +02:00
Jannik 2cc2ecf952
theme the dialogs and navView too, added a License dialog 2019-06-25 17:10:57 +02:00
Jannik ea70aedbd0
added the fastlane changelog for version 14 2019-06-25 13:09:17 +02:00
Jannik 98b3adbf3b
update to kotlin 1.3.40, added theme choose option
* added missing dividers in settings
* added a black theme
* closes #15
2019-06-25 12:07:18 +02:00
Jannik a055f59cc8
added a dark theme, see #15
* the app now needs sdkversion 23 now an min
2019-06-24 22:41:45 +02:00
Jannik 01db6bb451
don't use the faBtn in MensaFragment, use it in TimetableFragment 2019-06-08 23:14:55 +02:00
Jannik 4838c9406c
fastlane description spelling fixes 2019-06-06 23:36:55 +02:00
Jannik 23195f94e0
don't us deprecated tcor api requests 2019-06-03 12:05:33 +02:00
Jannik 5e766ec126
hide the mensa card value button 2019-06-03 10:59:32 +02:00
Jannik 9c1f95ca25
some lib updates & new sunday-bug fix 2019-06-02 19:09:36 +02:00
Jannik e99127a63a Update 'README.md' 2019-05-03 22:54:43 +02:00
Jannik f7fa96b1ae
use gradle 5.4.1
* fixes build issues if instant run is enabled
2019-05-03 22:45:53 +02:00
Jannik ac37bce145 Merge branch 'develop' of Seil0/ProjectLaogai into master 2019-05-02 14:43:20 +02:00
Jannik 6fd82254e0
updated description for f-droid 2019-05-02 14:42:28 +02:00
Jannik 0dd8ba9475
release 0.4.1 2019-05-01 01:04:09 +02:00
Jannik 770e9a255d Update 'README.md' 2019-05-01 00:46:47 +02:00
Jannik 4589badbfc
updated versionName to 0.4.1
* updated material-dialogs 2.6.0 -> 2.8.1
2019-05-01 00:36:33 +02:00
Jannik 74f75bfbde update to kotlin 1.3.31, gradle 5.1.1 & gradle plugin 3.4.0 2019-04-25 18:27:57 +01:00
Jannik a00c651bfd disable layout change animation 2019-04-08 18:39:43 +02:00
Jannik 77757ad9ca fixed mensa screen crash if day has no meals 2019-04-08 14:04:26 +02:00
Jannik fe111ac56b added refresh to timetable screen 2019-04-06 14:13:01 +02:00
Jannik 4971d0b1b8 added fastlane metadata 2019-04-05 15:05:29 +02:00
Jannik 2bd86ff6bb added fastlane metadata 2019-04-05 15:03:53 +02:00
Jannik dbaf496a79 minor clean up and refactoring 2019-04-03 20:37:17 +02:00
Jannik 77326a8ed6 added ability to refresh the mensa menus
* some minor clean ups
2019-04-03 19:21:51 +02:00
Jannik 9fc897e194 fixed initial accent color 2019-04-02 13:23:37 +02:00
Jannik f9ac219ec6 Merge remote-tracking branch 'origin/master' into develop 2019-04-02 13:22:32 +02:00
Jannik a87e57c80e removed versionNameSuffix 2019-04-01 18:03:26 +02:00
Jannik ff1d353cae removed versionNameSuffix 2019-04-01 18:03:03 +02:00
Jannik 6e5cf29eaa removed maven repo 2019-04-01 17:55:37 +02:00
Jannik ee388bf5a5 removed maven repo 2019-04-01 17:55:14 +02:00
Jannik 8aaf8e3647 release 0.4.0
* use Locale.getDefault() for the timetable screen
2019-04-01 10:14:00 +02:00
Jannik d645d75bbf fixed showing wrong meal of tomorrow on Wednesday 2019-03-27 19:29:54 +01:00
Jannik f97491addd added mensa sunday workaround
* update mensa blocking un sundays between 1500 and 2359
* updated material-dialogs 2.2.0 -> 2.6.0
2019-03-27 13:29:36 +01:00
Jannik e08790aaa4 show the next day with a lecture on Sundays too 2019-03-24 17:23:10 +01:00
Jannik 750a808fbe the homescreen now shows the next day with a lecture
* fixed getTomorrowWeekIndex() since it would return 7 which is out of the arrays bounds
* minor clean up
2019-03-23 21:07:33 +01:00
Jannik e51a80b78d home screen redesign 2019-03-22 21:59:32 +01:00
Jannik a4eaea2918 timetable and mensa gui redesign
* the timetable and mensa screen are now the same design as the settings, much cleaner and ready for themes
2019-03-20 21:33:55 +01:00
Jannik cd3136715f spelling 2019-03-20 17:55:30 +01:00
Jannik 15f1386b6e some minor gui fixes
* why was minifyEnabled = false ??????????
2019-03-19 19:51:58 +01:00
Jannik 3bace6c155 Update 'README.md' 2019-03-19 19:18:34 +01:00
Jannik 61716f8eb4 Update 'README.md' 2019-03-19 19:13:47 +01:00
Jannik a1410f7b80 sort of worked around the Sunday problem 2019-03-18 19:09:03 +01:00
Jannik 953185425b fixed menu formatting 2019-03-17 20:52:34 +01:00
Jannik a6d14044c2 added initial course selection
* some minor gui fixes
2019-03-17 18:38:31 +01:00
Jannik f3b7ff066d added the tcor api
* drastically improved loading times, closes #14
2019-03-17 18:12:36 +01:00
Jannik 040eb26dcf update app version to 0.3.90 2019-03-14 22:57:41 +01:00
Jannik 1a5d2b6561 fix homescreen 2019-03-14 08:04:09 +01:00
Jannik 92b60d660c updated gradle build tool to 3.3.2 2019-03-14 08:03:39 +01:00
Jannik a4eceeddc9 added 2nd week menus & option to disable buffet in the mensa tab
* the GUI now shows a second week for Mensa menus
* you can disable the buffet in the Mensa tab now
2019-03-10 13:37:13 +01:00
Jannik 3e3a80442e next weeks menu & accent color
* the MensaParser can now get the next weeks menus
* it's now possible to select a accent color
2019-03-09 22:15:53 +01:00
Jannik 5a1a07cd42 minor gui updates 2019-03-03 20:46:51 +01:00
Jannik dbfdaffe99 the app can now display multiple lessons per time slot
* display multiple lessons per slot if needed
* material dialogs 2.0.0 -> 2.0.3
2019-03-01 18:59:17 +01:00
Jannik 58eb217ab7 added a new timetable parser implementation
* added supports for multiple lessons per slot
* added support for lessons which take 2 slots
2019-02-28 14:52:40 +01:00
Jannik 24f920c05f Added PreferenceController
* all preferences and global variables are now inside the PreferenceController, this makes it much easier to extend the apps functionality
* CourseTTLink is now Course with courseLink and courseName as values
2019-02-17 15:05:03 +01:00
Jannik 6301308d76 Update 'README.md' 2019-02-16 17:38:24 +01:00
Jannik 8e205fa889 updated the gradle plugin to 3.3.1 2019-02-14 16:27:29 +01:00
Jannik e9bdcee443 some clean up
* removed unnecessary MainActivity dependencies
2019-02-14 16:25:23 +01:00
Jannik b214cfccb2 „README.md“ ändern 2019-02-13 21:16:17 +01:00
Jannik 404ddd58b8 „README.md“ ändern 2019-02-13 21:14:34 +01:00
Jannik 137ff7df0c „README.md“ ändern 2019-02-13 21:10:38 +01:00
Jannik b4071d7456 updated some libs
* material-dialogs 2.0.0-rc5 -> 2.0.0
2019-02-09 21:58:35 +01:00
Jannik 5e6e6cfde6 updated gradle plugin to 3.3.0 2019-01-15 18:55:06 +01:00
Jannik ffeb09a37f fixed a possible issue with the mensa parser 2019-01-04 23:58:54 +01:00
Jannik 75a457312d major bug fixes & version 0.3.2
* fixed a issue that prevented the app from showing the second weeks schedule, until now the app showed always the current week as next week
* minor color chooser improvements
* minor code clean up
2019-01-03 13:46:09 +01:00
Jannik 87bf614d28 added color saving
** WARNING this contains issues, many issues **
2019-01-03 01:45:28 +01:00
Jannik e69354af96 clean up 2018-12-18 11:49:46 +01:00
Jannik ec74a8e4f8 basic color selection 2018-12-14 15:02:19 +01:00
Jannik b49d16b1a1 updated some libs, meal tomorrow shows correct string if there's no meal 2018-12-02 00:05:23 +01:00
Jannik 70059b4b0c added moodle webview 2018-11-30 13:54:39 +01:00
Jannik 3d7f6f961a the menus text is now selectable
* fixed some formating errors at the mensa screen
2018-11-26 12:56:06 +01:00
Jannik f97e8b2b14 fixed text overlapping and some crashes 2018-11-26 11:17:07 +01:00
Jannik deaf139b70 updated some libraries 2018-11-22 18:07:30 +01:00
Jannik bf48bec16b minor fixes 2018-11-19 12:00:39 +01:00
Jannik e6c4096787 minor gui fixes, clean up 2018-11-18 21:28:43 +01:00
Jannik b083e44711 added a new icon, gui cleanup 2018-11-18 19:46:21 +01:00
Jannik 2df58311cb updated version number 2018-11-17 15:02:53 +01:00
143 changed files with 5925 additions and 1437 deletions

9
.drone.yml Normal file
View File

@ -0,0 +1,9 @@
kind: pipeline
name: default
steps:
- name: assembleRelease
image: nextcloudci/android10:android-56
commands:
- ./gradlew assembleRelease

View File

@ -1,12 +1,27 @@
# ProjectLaogai "hso App"
Some info about the app ...
[![Build Status](https://drone.mosad.xyz/api/badges/Seil0/ProjectLaogai/status.svg)](https://drone.mosad.xyz/Seil0/ProjectLaogai)
![fdroid version](https://img.shields.io/f-droid/v/org.mosad.seil0.projectlaogai.svg?style=flat-square)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0)
# Project Laogai
Project Laogai is an app to access the timetable, grades (qispos) and the canteen menu of Hochschule Offenburg. Laogai uses the TCoR-API fot timetables and the canteen menu, wich makes acessing them super fast. To get the grades from Qispos, Laogai will ask for your login data, wich are stored encrypted on your device.
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">](https://f-droid.org/packages/org.mosad.seil0.projectlaogai/)
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="75">](https://play.google.com/store/apps/details?id=org.mosad.seil0.projectlaogai)
## Features
* look up what you can eat in the mensa
* check your timetable
* probably many many bugs
* have your grades displayed directly in the app
* show the timetable of your course
* take a look at the canteen menu for the current and next week
* check the current balance of your mensa card
* open moodle directly in the app
Please report bugs and issues to support@mosad.xyz
## Screenshots
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_HomeScreen.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_HomeScreen.png)
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa.png)
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Timetable.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Timetable.png)
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Settings.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Settings.png)
[<img src="https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa_dark.png" width=180>](https://www.mosad.xyz/images/Project_Laogai/ProjectLaogai_Mensa_dark.png)
ProjectLaogai © 2018 mosad www.mosad.xyz, Project by [@Seil0](https://git.mosad.xyz/Seil0)
ProjectLaogai © 2019-2020 [@Seil0](https://git.mosad.xyz/Seil0), a [mosad.xyz](http://www.mosad.xyz) Project

View File

@ -1,43 +1,75 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
signingConfigs {
}
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "org.mosad.seil0.projectlaogai"
minSdkVersion 21
targetSdkVersion 28
versionCode 7
versionName "0.2.3"
minSdkVersion 23
targetSdkVersion 29
versionCode 6000 // 0006000
versionName "0.6.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
setProperty("archivesBaseName", "projectlaogai-$versionName")
}
buildTypes {
release {
minifyEnabled false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
res.srcDirs =
[
'src/main/res/layouts/activities',
'src/main/res/layouts/dialogs',
'src/main/res/layouts/fragments',
'src/main/res/layouts',
'src/main/res'
]
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
implementation 'androidx.core:core:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
implementation 'org.jsoup:jsoup:1.11.3'
implementation 'org.jetbrains.anko:anko-commons:0.10.7'
implementation 'com.afollestad.material-dialogs:core:2.0.0-beta5'
implementation 'com.afollestad.material-dialogs:color:2.0.0-beta5'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
implementation 'com.google.android.material:material:1.2.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.afollestad:aesthetic:1.0.0-beta05'
implementation 'com.afollestad.material-dialogs:core:3.3.0'
implementation 'com.afollestad.material-dialogs:color:3.3.0'
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation 'org.apache.commons:commons-lang3:3.11'
implementation 'org.jsoup:jsoup:1.13.1'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
}
static def buildTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}

View File

@ -15,7 +15,16 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-dontobfuscate
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.mosad.seil0.projectlaogai.util.** { <fields>; }
#Gson
-keepattributes Signature
-dontwarn sun.misc.**
#misc
-dontwarn java.lang.instrument.ClassFileTransformer

View File

@ -1,7 +1,7 @@
package org.mosad.seil0.projectlaogai
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import org.junit.runner.RunWith
@ -18,7 +18,7 @@ class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
val appContext = InstrumentationRegistry.getInstrumentation().context
assertEquals("org.mosad.seil0.projectlaogai", appContext.packageName)
}
}

View File

@ -2,31 +2,55 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mosad.seil0.projectlaogai">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.NFC" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_laogai_icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/ic_laogai_icon"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme.Light">
<activity
android:name=".SplashActivity"
android:label="@string/app_name"
android:theme="@style/SplashTheme"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".OnboardingActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.Light"
android:screenOrientation="portrait"
android:launchMode="singleTop">
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait">
android:theme="@style/AppTheme.Light"
android:screenOrientation="portrait"
android:launchMode="singleTop">
<!-- nfc stuff -->
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,227 @@
/**
* Utils.kt
*
* Copyright (C) 2011 Eric Butler
* Copyright (C) 2019 <seil0@mosad.xyz>
*
* Authors:
* Eric Butler <eric@codebutler.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.codebutler.farebot
import android.app.Activity
import android.app.AlertDialog
import android.util.Log
import android.view.WindowManager
import com.codebutler.farebot.card.desfire.DesfireException
import com.codebutler.farebot.card.desfire.DesfireFileSettings
import com.codebutler.farebot.card.desfire.DesfireProtocol
import org.w3c.dom.Node
import java.io.StringWriter
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.experimental.and
class Utils {
companion object {
private val TAG = Utils::class.java.name
@Suppress("unused")
fun showError(activity: Activity, ex: Exception) {
Log.e(activity.javaClass.name, ex.message, ex)
AlertDialog.Builder(activity)
.setMessage(getErrorMessage(ex))
.show()
}
@Suppress("unused")
fun showErrorAndFinish(activity: Activity, ex: Exception) {
try {
Log.e(activity.javaClass.name, getErrorMessage(ex))
ex.printStackTrace()
AlertDialog.Builder(activity)
.setMessage(getErrorMessage(ex))
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ -> activity.finish() }
.show()
} catch (unused: WindowManager.BadTokenException) {
/* Ignore... happens if the activity was destroyed */
}
}
@Throws(Exception::class)
fun getHexString(b: ByteArray): String {
var result = ""
for (i in b.indices) {
result += ((b[i] and 0xff.toByte()) + 0x100).toString(16).substring(1)
}
return result
}
@Suppress("unused")
fun getHexString(b: ByteArray, defaultResult: String): String {
return try {
getHexString(b)
} catch (ex: Exception) {
defaultResult
}
}
@Suppress("unused")
fun hexStringToByteArray(s: String): ByteArray {
if (s.length % 2 != 0) {
throw IllegalArgumentException("Bad input string: $s")
}
val len = s.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
i += 2
}
return data
}
@JvmOverloads
fun byteArrayToInt(b: ByteArray, offset: Int = 0, length: Int = b.size): Int {
return byteArrayToLong(b, offset, length).toInt()
}
fun byteArrayToLong(b: ByteArray, offset: Int, length: Int): Long {
if (b.size < length)
throw IllegalArgumentException("length must be less than or equal to b.length")
var value: Long = 0
for (i in 0 until length) {
val shift = (length - 1 - i) * 8
value += ((b[i + offset].toInt() and 0x000000FF).toLong() shl shift)
}
return value
}
@Suppress("unused")
fun byteArraySlice(b: ByteArray, offset: Int, length: Int): ByteArray {
val ret = ByteArray(length)
for (i in 0 until length)
ret[i] = b[offset + i]
return ret
}
@Suppress("unused")
@Throws(Exception::class)
fun xmlNodeToString(node: Node): String {
// The amount of code required to do simple things in Java is incredible.
val source = DOMSource(node)
val stringWriter = StringWriter()
val result = StreamResult(stringWriter)
val factory = TransformerFactory.newInstance()
val transformer = factory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
transformer.uriResolver = null
transformer.transform(source, result)
return stringWriter.buffer.toString()
}
fun getErrorMessage(ex: Throwable): String {
var errorMessage: String? = ex.localizedMessage
if (errorMessage == null)
errorMessage = ex.message
if (errorMessage == null)
errorMessage = ex.toString()
if (ex.cause != null) {
var causeMessage: String? = ex.cause!!.localizedMessage
if (causeMessage == null)
causeMessage = ex.cause!!.message
if (causeMessage == null)
causeMessage = ex.cause.toString()
errorMessage += ": $causeMessage"
}
return errorMessage
}
@Suppress("unused")
fun <T> findInList(list: List<T>, matcher: Matcher<T>): T? {
for (item in list) {
if (matcher.matches(item)) {
return item
}
}
return null
}
interface Matcher<T> {
fun matches(t: T): Boolean
}
fun selectAppFile(tag: DesfireProtocol, appID: Int, fileID: Int): DesfireFileSettings? {
try {
tag.selectApp(appID)
} catch (e: DesfireException) {
Log.w(TAG, "App not found")
return null
}
return try {
tag.getFileSettings(fileID)
} catch (e: DesfireException) {
Log.w(TAG, "File not found")
null
}
}
fun arrayContains(arr: IntArray, item: Int): Boolean {
for (i in arr)
if (i == item)
return true
return false
}
@Suppress("unused")
fun containsAppFile(tag: DesfireProtocol, appID: Int, fileID: Int): Boolean {
try {
tag.selectApp(appID)
} catch (e: DesfireException) {
Log.w(TAG, "App not found")
Log.w(TAG, e)
return false
}
return try {
arrayContains(tag.fileList, fileID)
} catch (e: DesfireException) {
Log.w(TAG, "File not found")
Log.w(TAG, e)
false
}
}
}
}

View File

@ -0,0 +1,9 @@
package com.codebutler.farebot.card.desfire
/**
* Created by Jakob Wenzel on 16.11.13.
*/
class DesfireException : Exception {
constructor(message: String) : super(message)
constructor(cause: Throwable) : super(cause)
}

View File

@ -0,0 +1,104 @@
/**
* DesfireFile.kt
*
* Copyright (C) 2011 Eric Butler
* Copyright (C) 2019 <seil0@mosad.xyz>
*
* Authors:
* Eric Butler <eric@codebutler.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.codebutler.farebot.card.desfire
import android.os.Parcel
import android.os.Parcelable
import com.codebutler.farebot.card.desfire.DesfireFileSettings.RecordDesfireFileSettings
import org.apache.commons.lang3.ArrayUtils
open class DesfireFile private constructor(val id: Int, private val fileSettings: DesfireFileSettings?, val data: ByteArray) :
Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(id)
if (this is InvalidDesfireFile) {
parcel.writeInt(1)
parcel.writeString(this.errorMessage)
} else {
parcel.writeInt(0)
parcel.writeParcelable(fileSettings, 0)
parcel.writeInt(data.size)
parcel.writeByteArray(data)
}
}
override fun describeContents(): Int {
return 0
}
class RecordDesfireFile(fileId: Int, fileSettings: DesfireFileSettings, fileData: ByteArray) :
DesfireFile(fileId, fileSettings, fileData) {
private val records: Array<DesfireRecord?>
init {
val settings = fileSettings as RecordDesfireFileSettings
val records = arrayOfNulls<DesfireRecord>(settings.curRecords)
for (i in 0 until settings.curRecords) {
val offset = settings.recordSize * i
records[i] = DesfireRecord(ArrayUtils.subarray(data, offset, offset + settings.recordSize))
}
this.records = records
}
}
class InvalidDesfireFile(fileId: Int, val errorMessage: String?) : DesfireFile(fileId, null, ByteArray(0))
companion object {
fun create(fileId: Int, fileSettings: DesfireFileSettings, fileData: ByteArray): DesfireFile {
return (fileSettings as? RecordDesfireFileSettings)?.let { RecordDesfireFile(fileId, it, fileData) }
?: DesfireFile(fileId, fileSettings, fileData)
}
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<DesfireFile> = object : Parcelable.Creator<DesfireFile> {
override fun createFromParcel(source: Parcel): DesfireFile {
val fileId = source.readInt()
val isError = source.readInt() == 1
return if (!isError) {
val fileSettings =
source.readParcelable<Parcelable>(DesfireFileSettings::class.java.classLoader) as DesfireFileSettings
val dataLength = source.readInt()
val fileData = ByteArray(dataLength)
source.readByteArray(fileData)
create(fileId, fileSettings, fileData)
} else {
InvalidDesfireFile(fileId, source.readString())
}
}
override fun newArray(size: Int): Array<DesfireFile?> {
return arrayOfNulls(size)
}
}
}
}

View File

@ -0,0 +1,248 @@
/**
* DesfireFileSettings.kt
*
* Copyright (C) 2011 Eric Butler
* Copyright (C) 2019 <seil0@mosad.xyz>
*
* Authors:
* Eric Butler <eric@codebutler.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.codebutler.farebot.card.desfire
import android.os.Parcel
import android.os.Parcelable
import com.codebutler.farebot.Utils
import org.apache.commons.lang3.ArrayUtils
import java.io.ByteArrayInputStream
abstract class DesfireFileSettings : Parcelable {
private val fileType: Byte
private val commSetting: Byte
private val accessRights: ByteArray
@Suppress("unused")
val fileTypeName: String
get() {
return when (fileType) {
STANDARD_DATA_FILE -> "Standard"
BACKUP_DATA_FILE -> "Backup"
VALUE_FILE -> "Value"
LINEAR_RECORD_FILE -> "Linear Record"
CYCLIC_RECORD_FILE -> "Cyclic Record"
else -> "Unknown"
}
}
private constructor(stream: ByteArrayInputStream) {
fileType = stream.read().toByte()
commSetting = stream.read().toByte()
accessRights = ByteArray(2)
stream.read(accessRights, 0, accessRights.size)
}
private constructor(fileType: Byte, commSetting: Byte, accessRights: ByteArray) {
this.fileType = fileType
this.commSetting = commSetting
this.accessRights = accessRights
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeByte(fileType)
parcel.writeByte(commSetting)
parcel.writeInt(accessRights.size)
parcel.writeByteArray(accessRights)
}
override fun describeContents(): Int {
return 0
}
class StandardDesfireFileSettings : DesfireFileSettings {
private val fileSize: Int
internal constructor(stream: ByteArrayInputStream) : super(stream) {
val buf = ByteArray(3)
stream.read(buf, 0, buf.size)
ArrayUtils.reverse(buf)
fileSize = Utils.byteArrayToInt(buf)
}
internal constructor(
fileType: Byte,
commSetting: Byte,
accessRights: ByteArray,
fileSize: Int
) : super(fileType, commSetting, accessRights) {
this.fileSize = fileSize
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
parcel.writeInt(fileSize)
}
}
class RecordDesfireFileSettings : DesfireFileSettings {
private val maxRecords: Int
val recordSize: Int
val curRecords: Int
constructor(stream: ByteArrayInputStream) : super(stream) {
var buf = ByteArray(3)
stream.read(buf, 0, buf.size)
ArrayUtils.reverse(buf)
recordSize = Utils.byteArrayToInt(buf)
buf = ByteArray(3)
stream.read(buf, 0, buf.size)
ArrayUtils.reverse(buf)
maxRecords = Utils.byteArrayToInt(buf)
buf = ByteArray(3)
stream.read(buf, 0, buf.size)
ArrayUtils.reverse(buf)
curRecords = Utils.byteArrayToInt(buf)
}
internal constructor(
fileType: Byte,
commSetting: Byte,
accessRights: ByteArray,
recordSize: Int,
maxRecords: Int,
curRecords: Int
) : super(fileType, commSetting, accessRights) {
this.recordSize = recordSize
this.maxRecords = maxRecords
this.curRecords = curRecords
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
parcel.writeInt(recordSize)
parcel.writeInt(maxRecords)
parcel.writeInt(curRecords)
}
}
class ValueDesfireFileSettings(stream: ByteArrayInputStream) : DesfireFileSettings(stream) {
private val lowerLimit: Int
private val upperLimit: Int
val value: Int
private val limitedCreditEnabled: Byte
init {
var buf = ByteArray(4)
stream.read(buf, 0, buf.size)
ArrayUtils.reverse(buf)
lowerLimit = Utils.byteArrayToInt(buf)
buf = ByteArray(4)
stream.read(buf, 0, buf.size)
ArrayUtils.reverse(buf)
upperLimit = Utils.byteArrayToInt(buf)
buf = ByteArray(4)
stream.read(buf, 0, buf.size)
ArrayUtils.reverse(buf)
value = Utils.byteArrayToInt(buf)
buf = ByteArray(1)
stream.read(buf, 0, buf.size)
limitedCreditEnabled = buf[0]
//http://www.skyetek.com/docs/m2/desfire.pdf
//http://neteril.org/files/M075031_desfire.pdf
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
parcel.writeInt(lowerLimit)
parcel.writeInt(upperLimit)
parcel.writeInt(value)
parcel.writeByte(limitedCreditEnabled)
}
}
class UnsupportedDesfireFileSettings(fileType: Byte) :
DesfireFileSettings(fileType, java.lang.Byte.MIN_VALUE, ByteArray(0))
companion object {
/* DesfireFile Types */
internal const val STANDARD_DATA_FILE = 0x00.toByte()
internal const val BACKUP_DATA_FILE = 0x01.toByte()
internal const val VALUE_FILE = 0x02.toByte()
internal const val LINEAR_RECORD_FILE = 0x03.toByte()
internal const val CYCLIC_RECORD_FILE = 0x04.toByte()
@Throws(DesfireException::class)
fun create(data: ByteArray): DesfireFileSettings {
val fileType = data[0]
val stream = ByteArrayInputStream(data)
return if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE)
StandardDesfireFileSettings(stream)
else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE)
RecordDesfireFileSettings(stream)
else if (fileType == VALUE_FILE)
ValueDesfireFileSettings(stream)
else
throw DesfireException("Unknown file type: " + Integer.toHexString(fileType.toInt()))
}
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<DesfireFileSettings> = object : Parcelable.Creator<DesfireFileSettings> {
override fun createFromParcel(source: Parcel): DesfireFileSettings {
val fileType = source.readByte()
val commSetting = source.readByte()
val accessRights = ByteArray(source.readInt())
source.readByteArray(accessRights)
if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE) {
val fileSize = source.readInt()
return StandardDesfireFileSettings(fileType, commSetting, accessRights, fileSize)
} else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE) {
val recordSize = source.readInt()
val maxRecords = source.readInt()
val curRecords = source.readInt()
return RecordDesfireFileSettings(
fileType,
commSetting,
accessRights,
recordSize,
maxRecords,
curRecords
)
} else {
return UnsupportedDesfireFileSettings(fileType)
}
}
override fun newArray(size: Int): Array<DesfireFileSettings?> {
return arrayOfNulls(size)
}
}
}
}

View File

@ -0,0 +1,181 @@
/**
* DesfireManufacturingData.kt
*
* Copyright (C) 2011 Eric Butler
* Copyright (C) 2019 <seil0@mosad.xyz>
*
* Authors:
* Eric Butler <eric@codebutler.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.codebutler.farebot.card.desfire
import android.os.Parcel
import android.os.Parcelable
import com.codebutler.farebot.Utils
import org.w3c.dom.Element
import java.io.ByteArrayInputStream
class DesfireManufacturingData : Parcelable {
private val hwVendorID: Int
private val hwType: Int
private val hwSubType: Int
private val hwMajorVersion: Int
private val hwMinorVersion: Int
private val hwStorageSize: Int
private val hwProtocol: Int
private val swVendorID: Int
private val swType: Int
private val swSubType: Int
private val swMajorVersion: Int
private val swMinorVersion: Int
private val swStorageSize: Int
private val swProtocol: Int
private val uid: Int
private val batchNo: Int
private val weekProd: Int
private val yearProd: Int
constructor(data: ByteArray) {
val stream = ByteArrayInputStream(data)
hwVendorID = stream.read()
hwType = stream.read()
hwSubType = stream.read()
hwMajorVersion = stream.read()
hwMinorVersion = stream.read()
hwStorageSize = stream.read()
hwProtocol = stream.read()
swVendorID = stream.read()
swType = stream.read()
swSubType = stream.read()
swMajorVersion = stream.read()
swMinorVersion = stream.read()
swStorageSize = stream.read()
swProtocol = stream.read()
// FIXME: This has fewer digits than what's contained in EXTRA_ID, why?
var buf = ByteArray(7)
stream.read(buf, 0, buf.size)
uid = Utils.byteArrayToInt(buf)
// FIXME: This is returning a negative number. Probably is unsigned.
buf = ByteArray(5)
stream.read(buf, 0, buf.size)
batchNo = Utils.byteArrayToInt(buf)
// FIXME: These numbers aren't making sense.
weekProd = stream.read()
yearProd = stream.read()
}
private constructor(element: Element) {
hwVendorID = Integer.parseInt(element.getElementsByTagName("hw-vendor-id").item(0).textContent)
hwType = Integer.parseInt(element.getElementsByTagName("hw-type").item(0).textContent)
hwSubType = Integer.parseInt(element.getElementsByTagName("hw-sub-type").item(0).textContent)
hwMajorVersion = Integer.parseInt(element.getElementsByTagName("hw-major-version").item(0).textContent)
hwMinorVersion = Integer.parseInt(element.getElementsByTagName("hw-minor-version").item(0).textContent)
hwStorageSize = Integer.parseInt(element.getElementsByTagName("hw-storage-size").item(0).textContent)
hwProtocol = Integer.parseInt(element.getElementsByTagName("hw-protocol").item(0).textContent)
swVendorID = Integer.parseInt(element.getElementsByTagName("sw-vendor-id").item(0).textContent)
swType = Integer.parseInt(element.getElementsByTagName("sw-type").item(0).textContent)
swSubType = Integer.parseInt(element.getElementsByTagName("sw-sub-type").item(0).textContent)
swMajorVersion = Integer.parseInt(element.getElementsByTagName("sw-major-version").item(0).textContent)
swMinorVersion = Integer.parseInt(element.getElementsByTagName("sw-minor-version").item(0).textContent)
swStorageSize = Integer.parseInt(element.getElementsByTagName("sw-storage-size").item(0).textContent)
swProtocol = Integer.parseInt(element.getElementsByTagName("sw-protocol").item(0).textContent)
uid = Integer.parseInt(element.getElementsByTagName("uid").item(0).textContent)
batchNo = Integer.parseInt(element.getElementsByTagName("batch-no").item(0).textContent)
weekProd = Integer.parseInt(element.getElementsByTagName("week-prod").item(0).textContent)
yearProd = Integer.parseInt(element.getElementsByTagName("year-prod").item(0).textContent)
}
private constructor(parcel: Parcel) {
hwVendorID = parcel.readInt()
hwType = parcel.readInt()
hwSubType = parcel.readInt()
hwMajorVersion = parcel.readInt()
hwMinorVersion = parcel.readInt()
hwStorageSize = parcel.readInt()
hwProtocol = parcel.readInt()
swVendorID = parcel.readInt()
swType = parcel.readInt()
swSubType = parcel.readInt()
swMajorVersion = parcel.readInt()
swMinorVersion = parcel.readInt()
swStorageSize = parcel.readInt()
swProtocol = parcel.readInt()
uid = parcel.readInt()
batchNo = parcel.readInt()
weekProd = parcel.readInt()
yearProd = parcel.readInt()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(hwVendorID)
parcel.writeInt(hwType)
parcel.writeInt(hwSubType)
parcel.writeInt(hwMajorVersion)
parcel.writeInt(hwMinorVersion)
parcel.writeInt(hwStorageSize)
parcel.writeInt(hwProtocol)
parcel.writeInt(swVendorID)
parcel.writeInt(swType)
parcel.writeInt(swSubType)
parcel.writeInt(swMajorVersion)
parcel.writeInt(swMinorVersion)
parcel.writeInt(swStorageSize)
parcel.writeInt(swProtocol)
parcel.writeInt(uid)
parcel.writeInt(batchNo)
parcel.writeInt(weekProd)
parcel.writeInt(yearProd)
}
override fun describeContents(): Int {
return 0
}
companion object {
@Suppress("unused")
fun fromXml(element: Element): DesfireManufacturingData {
return DesfireManufacturingData(element)
}
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<DesfireManufacturingData> =
object : Parcelable.Creator<DesfireManufacturingData> {
override fun createFromParcel(source: Parcel): DesfireManufacturingData {
return DesfireManufacturingData(source)
}
override fun newArray(size: Int): Array<DesfireManufacturingData?> {
return arrayOfNulls(size)
}
}
}
}

View File

@ -0,0 +1,214 @@
/**
* DesfireProtocol.kt
*
* Copyright (C) 2011 Eric Butler
* Copyright (C) 2019 <seil0@mosad.xyz>
*
* Authors:
* Eric Butler <eric@codebutler.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.codebutler.farebot.card.desfire
import android.nfc.tech.IsoDep
import com.codebutler.farebot.Utils
import java.io.ByteArrayOutputStream
import java.io.IOException
import org.apache.commons.lang3.ArrayUtils
import kotlin.experimental.and
class DesfireProtocol(private val mTagTech: IsoDep) {
@Suppress("unused")
val manufacturingData: DesfireManufacturingData
@Throws(DesfireException::class)
get() {
val respBuffer = sendRequest(GET_MANUFACTURING_DATA)
if (respBuffer.size != 28)
throw DesfireException("Invalid response")
return DesfireManufacturingData(respBuffer)
}
@Suppress("unused")
val appList: IntArray
@Throws(DesfireException::class)
get() {
val appDirBuf = sendRequest(GET_APPLICATION_DIRECTORY)
val appIds = IntArray(appDirBuf.size / 3)
var app = 0
while (app < appDirBuf.size) {
val appId = ByteArray(3)
System.arraycopy(appDirBuf, app, appId, 0, 3)
appIds[app / 3] = Utils.byteArrayToInt(appId)
app += 3
}
return appIds
}
val fileList: IntArray
@Throws(DesfireException::class)
get() {
val buf = sendRequest(GET_FILES)
val fileIds = IntArray(buf.size)
for (x in buf.indices) {
fileIds[x] = buf[x].toInt()
}
return fileIds
}
@Throws(DesfireException::class)
fun selectApp(appId: Int) {
val appIdBuff = ByteArray(3)
appIdBuff[0] = (appId and 0xFF0000 shr 16).toByte()
appIdBuff[1] = (appId and 0xFF00 shr 8).toByte()
appIdBuff[2] = (appId and 0xFF).toByte()
sendRequest(SELECT_APPLICATION, appIdBuff)
}
@Throws(DesfireException::class)
fun getFileSettings(fileNo: Int): DesfireFileSettings {
val data = sendRequest(GET_FILE_SETTINGS, byteArrayOf(fileNo.toByte()))
return DesfireFileSettings.create(data)
}
@Suppress("unused")
@Throws(DesfireException::class)
fun readFile(fileNo: Int): ByteArray {
return sendRequest(
READ_DATA,
byteArrayOf(
fileNo.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte()
)
)
}
@Suppress("unused")
@Throws(DesfireException::class)
fun readRecord(fileNum: Int): ByteArray {
return sendRequest(
READ_RECORD,
byteArrayOf(
fileNum.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte(),
0x0.toByte()
)
)
}
@Throws(DesfireException::class)
fun readValue(fileNum: Int): Int {
val buf = sendRequest(READ_VALUE, byteArrayOf(fileNum.toByte()))
ArrayUtils.reverse(buf)
return Utils.byteArrayToInt(buf)
}
@Throws(DesfireException::class)
private fun sendRequest(command: Byte, parameters: ByteArray? = null): ByteArray {
val output = ByteArrayOutputStream()
var recvBuffer: ByteArray
try {
recvBuffer = mTagTech.transceive(wrapMessage(command, parameters))
} catch (e: IOException) {
throw DesfireException(e)
}
while (true) {
if (recvBuffer[recvBuffer.size - 2] != 0x91.toByte())
throw DesfireException("Invalid response")
output.write(recvBuffer, 0, recvBuffer.size - 2)
val status = recvBuffer[recvBuffer.size - 1]
if (status == OPERATION_OK) {
break
} else if (status == ADDITIONAL_FRAME) {
try {
recvBuffer = mTagTech.transceive(wrapMessage(GET_ADDITIONAL_FRAME, null))
} catch (e: IOException) {
throw DesfireException(e)
}
} else if (status == PERMISSION_DENIED) {
throw DesfireException("Permission denied")
} else {
throw DesfireException("Unknown status code: " + Integer.toHexString((status and 0xFF.toByte()).toInt()))
}
}
return output.toByteArray()
}
@Throws(DesfireException::class)
private fun wrapMessage(command: Byte, parameters: ByteArray?): ByteArray {
val stream = ByteArrayOutputStream()
stream.write(0x90.toByte().toInt())
stream.write(command.toInt())
stream.write(0x00.toByte().toInt())
stream.write(0x00.toByte().toInt())
if (parameters != null) {
stream.write(parameters.size.toByte().toInt())
try {
stream.write(parameters)
} catch (e: IOException) {
throw DesfireException(e)
}
}
stream.write(0x00.toByte().toInt())
return stream.toByteArray()
}
companion object {
/* Commands */
internal const val GET_MANUFACTURING_DATA = 0x60.toByte()
internal const val GET_APPLICATION_DIRECTORY = 0x6A.toByte()
internal const val GET_ADDITIONAL_FRAME = 0xAF.toByte()
internal const val SELECT_APPLICATION = 0x5A.toByte()
internal const val READ_DATA = 0xBD.toByte()
internal const val READ_RECORD = 0xBB.toByte()
internal const val READ_VALUE = 0x6C.toByte()
internal const val GET_FILES = 0x6F.toByte()
internal const val GET_FILE_SETTINGS = 0xF5.toByte()
/* Status codes */
internal const val OPERATION_OK = 0x00.toByte()
internal const val PERMISSION_DENIED = 0x9D.toByte()
internal const val ADDITIONAL_FRAME = 0xAF.toByte()
}
}

View File

@ -0,0 +1,26 @@
/*
* DesfireRecord.kt
*
* Copyright (C) 2011 Eric Butler
* Copyright (C) 2019 <seil0@mosad.xyz>
*
* Authors:
* Eric Butler <eric@codebutler.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.codebutler.farebot.card.desfire
class DesfireRecord(val data: ByteArray)

View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
@ -22,53 +22,63 @@
package org.mosad.seil0.projectlaogai
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.nfc.NfcAdapter
import android.nfc.NfcManager
import android.nfc.tech.NfcA
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.aesthetic.Aesthetic
import com.afollestad.aesthetic.NavigationViewMode
import com.google.android.material.navigation.NavigationView
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar_main.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import org.mosad.seil0.projectlaogai.fragments.HomeFragment
import org.mosad.seil0.projectlaogai.fragments.MensaFragment
import org.mosad.seil0.projectlaogai.fragments.SettingsFragment
import org.mosad.seil0.projectlaogai.fragments.TimeTableFragment
import org.mosad.seil0.projectlaogai.hsoparser.*
import org.mosad.seil0.projectlaogai.controller.NFCMensaCard
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorAccent
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorPrimary
import org.mosad.seil0.projectlaogai.fragments.*
import kotlin.system.measureTimeMillis
/**
* TODO save the current fragment to show it when the app is restarted
*/
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
//TODO make toolbar and navbar global
private val mensaParser = MensaParser()
private val timeTableParser = TimeTableParser()
private var activeFragment: Fragment = HomeFragment() // the currently active fragment, home at the start
private val className = "MainActivity"
private var weekMenus = ArrayList<Meal>()
private var courseTTLinkList = ArrayList<CourseTTLink>()
private var timeTableCurrentWeek = arrayOf<Array<Lesson>>()
private var timeTableNextWeek = arrayOf<Array<Lesson>>()
private lateinit var course: CourseTTLink
private lateinit var adapter: NfcAdapter
private lateinit var pendingIntent: PendingIntent
private lateinit var intentFiltersArray: Array<IntentFilter>
private lateinit var techListsArray: Array<Array<String>>
private var useNFC = false
override fun onCreate(savedInstanceState: Bundle?) {
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
Aesthetic.attach(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
// load mensa and timetable
// load mensa, timetable and color
load()
//init home fragment TODO make a abstract fragment class
val homeFragment = HomeFragment()
homeFragment.setMainActivity(this)
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, homeFragment)
fragmentTransaction.commit()
initAesthetic()
initForegroundDispatch()
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
@ -77,12 +87,48 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
toggle.syncState()
nav_view.setNavigationItemSelectedListener(this)
// based on the intent we get, call readBalance or open a Fragment
when (intent.action) {
NfcAdapter.ACTION_TECH_DISCOVERED -> NFCMensaCard.readBalance(intent, this)
"org.mosad.seil0.projectlaogai.fragments.MensaFragment" -> activeFragment = MensaFragment()
"org.mosad.seil0.projectlaogai.fragments.TimeTableFragment" -> activeFragment = TimeTableFragment()
"org.mosad.seil0.projectlaogai.fragments.MoodleFragment" -> activeFragment = MoodleFragment()
}
// open the activeFragment, default is the HomeFragment
fragmentTransaction.replace(R.id.fragment_container, activeFragment)
fragmentTransaction.commit()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action)
NFCMensaCard.readBalance(intent, this)
}
override fun onResume() {
super.onResume()
Aesthetic.resume(this)
if(useNFC)
adapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray)
}
override fun onPause() {
super.onPause()
Aesthetic.pause(this)
if(useNFC)
adapter.disableForegroundDispatch(this)
}
override fun onBackPressed() {
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
drawer_layout.closeDrawer(GravityCompat.START)
} else {
// TODO only call on double tap
super.onBackPressed()
}
}
@ -105,147 +151,82 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here.
when (item.itemId) {
R.id.nav_home -> {
val homeFragment = HomeFragment()
homeFragment.setMainActivity(this)
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, homeFragment)
fragmentTransaction.commit()
}
R.id.nav_mensa -> {
val mensaFragment = MensaFragment()
mensaFragment.setMainActivity(this)
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, mensaFragment)
fragmentTransaction.commit()
}
R.id.nav_timetable -> {
val timeTableFragment = TimeTableFragment()
timeTableFragment.setMainActivity(this)
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, timeTableFragment)
fragmentTransaction.commit()
}
R.id.nav_moodle -> {
}
R.id.nav_email -> {
}
R.id.nav_settings -> {
val settingsFragment = SettingsFragment()
settingsFragment.setMainActivity(this)
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, settingsFragment)
fragmentTransaction.commit()
}
activeFragment = when(item.itemId) {
R.id.nav_home -> HomeFragment()
R.id.nav_mensa -> MensaFragment()
R.id.nav_timetable -> TimeTableFragment()
R.id.nav_moodle -> MoodleFragment()
R.id.nav_grades -> GradesFragment()
R.id.nav_settings -> SettingsFragment()
else -> HomeFragment()
}
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, activeFragment)
fragmentTransaction.commit()
drawer_layout.closeDrawer(GravityCompat.START)
return true
}
/**
* update the gui with the data of the new selected course
* save selected course and courseTTLink
* load the mensa menus of the current week
*/
fun updateCourse(course: CourseTTLink) {
println(course.course)
println(course.courseTTLink)
private fun load() {
val startupTime = measureTimeMillis {
Preferences.load(this) // load the settings, must be finished before doing anything else
CacheController(this) // load the cache
EncryptedPreferences.load(this)
}
Log.i(className, "startup completed in $startupTime ms")
}
this.course = course
// save new selected course
val sharedPref = getPreferences(MODE_PRIVATE) ?: return
with (sharedPref.edit()) {
putString(getString(R.string.save_key_course), course.course)
putString(getString(R.string.save_key_courseTTLink), course.courseTTLink.replace("http", "https"))
private fun initAesthetic() {
// If we haven't set any defaults, do that now
if (Aesthetic.isFirstTime) {
// set the default theme at the first app start
Aesthetic.config {
activityTheme(R.style.AppTheme_Light)
apply()
}
// show the onboarding activity
startActivity(Intent(this, OnboardingActivity::class.java))
finish()
}
Aesthetic.config {
colorPrimary(cColorPrimary)
colorPrimaryDark(cColorPrimary)
colorAccent(cColorAccent)
navigationViewMode(NavigationViewMode.SELECTED_ACCENT)
apply()
}
timeTableCurrentWeek = timeTableParser.getTimeTable(course.courseTTLink.replace("http", "https"))
// set theme color values
val out = TypedValue()
this.theme.resolveAttribute(R.attr.themePrimary, out, true)
Preferences.themePrimary = out.data
this.theme.resolveAttribute(R.attr.themeSecondary, out, true)
Preferences.themeSecondary = out.data
}
/**
* load the mensa menus of the current week
* TODO evaluate if we should use a timeout here
*/
private fun load() {
private fun initForegroundDispatch() {
val nfcManager = this.getSystemService(Context.NFC_SERVICE) as NfcManager
val nfcAdapter = nfcManager.defaultAdapter
// load saved course
val sharedPref = getPreferences(MODE_PRIVATE) ?: return
course = CourseTTLink(
sharedPref.getString(getString(R.string.save_key_courseTTLink),
"https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0")!!,
sharedPref.getString(getString(R.string.save_key_course), "AI3")!!
)
/**
* load mensa, course timetable and courselist from the swfr/hso website
* TODO make an API see https://git.mosad.xyz/Seil0/TheCitadelofRicks
*/
val time = measureTimeMillis {
/* getting the course list should be faster than the timetable,
* we need have time until the user opens the dialog
*/
doAsync {
courseTTLinkList = timeTableParser.getCourseTTLinks()
}
doAsync {
try {
timeTableNextWeek = timeTableParser.getTimeTable(course.courseTTLink.replace("week=0","week=1"))
} catch (e: Exception) {
e.stackTrace
}
}
val t1 = doAsync {
weekMenus = mensaParser.getMensaMenu()
}
val t2 = doAsync {
try {
timeTableCurrentWeek = timeTableParser.getTimeTable(course.courseTTLink)
} catch (e: Exception) {
uiThread {
MaterialDialog(this@MainActivity)
.title(R.string.error)
.message(R.string.no_tt_error)
.show()
}
e.stackTrace
}
}
t1.get()
t2.get()
if (nfcAdapter != null) {
useNFC = true
intentFiltersArray = arrayOf(IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply { addDataType("*/*") })
techListsArray = arrayOf(arrayOf(NfcA::class.java.name))
adapter = NfcAdapter.getDefaultAdapter(this)
pendingIntent = PendingIntent.getActivity(
this, 0,
Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0
)
}
println("Completed in $time ms")
}
fun getCourseTTLinkList(): ArrayList<CourseTTLink>{
return courseTTLinkList
}
fun getTimeTableCurrentWeek(): Array<Array<Lesson>> {
return timeTableCurrentWeek
}
fun getTimeTableNextWeek(): Array<Array<Lesson>> {
return timeTableNextWeek
}
fun getWeekMenu(): ArrayList<Meal>{
return weekMenus
}
fun getCourse(): CourseTTLink {
return course
}
}

View File

@ -0,0 +1,145 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.text.HtmlCompat
import androidx.viewpager.widget.ViewPager
import com.afollestad.materialdialogs.callbacks.onDismiss
import kotlinx.android.synthetic.main.activity_onboarding.*
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.fragments.SettingsFragment
import org.mosad.seil0.projectlaogai.onboarding.ViewPagerAdapter
class OnboardingActivity : AppCompatActivity() {
companion object {
val layouts = intArrayOf(R.layout.fragment_on_welcome, R.layout.fragment_on_course, R.layout.fragment_on_login)
}
private lateinit var viewPager: ViewPager
private lateinit var viewPagerAdapter: ViewPagerAdapter
private lateinit var linLayoutDots: LinearLayout
private lateinit var dots: Array<TextView>
private lateinit var editTextEmail: EditText
private lateinit var editTextPassword: EditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
viewPager = findViewById(R.id.viewPager)
linLayoutDots = findViewById(R.id.linLayout_Dots)
addBottomDots(0)
viewPagerAdapter = ViewPagerAdapter(this)
viewPager.adapter = viewPagerAdapter
viewPager.addOnPageChangeListener(viewPagerPageChangeListener)
// we don't use the skip button, instead we use the start button to skip the last fragment
btn_Skip.visibility = View.GONE
}
fun btnNextClick(@Suppress("UNUSED_PARAMETER")v: View) {
if (viewPager.currentItem < layouts.size - 1) {
viewPager.currentItem++
} else {
launchHomeScreen()
}
}
fun btnSkipClick(@Suppress("UNUSED_PARAMETER")v: View) {
launchHomeScreen()
}
fun btnSelectCourseClick(@Suppress("UNUSED_PARAMETER")v: View) {
SettingsFragment().selectCourse(this).show {
onDismiss {
btnNextClick(v) // show the next fragment
}
}
}
fun btnLoginClick(@Suppress("UNUSED_PARAMETER")v: View) {
editTextEmail = findViewById(R.id.editText_email)
editTextPassword = findViewById(R.id.editText_password)
// get login credentials from gui
val email = editTextEmail.text.toString()
val password = editTextPassword.text.toString()
// save the credentials
EncryptedPreferences.saveCredentials(email, password, this)
launchHomeScreen()
}
private fun addBottomDots(currentPage: Int) {
dots = Array(layouts.size) { TextView(this) }
linLayoutDots.removeAllViews()
dots.forEach {
it.text = HtmlCompat.fromHtml("&#8226;", HtmlCompat.FROM_HTML_MODE_LEGACY)
it.textSize = 35f
it.setTextColor(Color.parseColor("#cccccc"))
linLayoutDots.addView(it)
}
if (dots.isNotEmpty())
dots[currentPage].setTextColor(Color.parseColor("#000000"))
}
private fun launchHomeScreen() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private var viewPagerPageChangeListener: ViewPager.OnPageChangeListener = object : ViewPager.OnPageChangeListener {
override fun onPageSelected(position: Int) {
addBottomDots(position)
// changing the next button text to skip for the login fragment
if (position == layouts.size - 1) {
btn_Next.text = getString(R.string.skip)
btn_Next.visibility = View.VISIBLE
btn_Skip.visibility = View.GONE
} else {
btn_Next.visibility = View.GONE
btn_Skip.visibility = View.GONE
}
}
override fun onPageScrolled(arg0: Int, arg1: Float, arg2: Int) {}
override fun onPageScrollStateChanged(arg0: Int) {}
}
}

View File

@ -0,0 +1,106 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.controller
import android.content.Context
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.util.Log
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.codebutler.farebot.Utils
import com.codebutler.farebot.card.desfire.DesfireFileSettings
import com.codebutler.farebot.card.desfire.DesfireProtocol
import kotlinx.android.synthetic.main.dialog_mensa_credit.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import java.lang.Exception
class NFCMensaCard {
companion object {
private const val className = "NFCMensaCard"
private const val appId = 0x5F8415
private const val fileId = 1
/**
* read the current balance and last payment from the mensa card
* @param intent a nfc intent
* @param context the context to show the dialog in
*/
fun readBalance(intent: Intent, context: Context) {
val tag: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
val isoDep = IsoDep.get(tag)
try {
isoDep.connect()
val card = DesfireProtocol(isoDep)
val settings = Utils.selectAppFile(card, appId, fileId)
if (settings is DesfireFileSettings.ValueDesfireFileSettings) {
val data = try {
card.readValue(fileId)
} catch (ex: Exception) { 0 }
lookAtMe(context, data, settings.value).show()
}
} catch (ex: Exception) {
Log.w(className,"could not connect to tag", ex)
}
}
/**
* generate the values for current balance and last payment
* if the easter egg is active use schmeckles as currency
* 0.0000075 = 1.11 / 148 / 1000 (dollar / shm / card multiplier)
* @param context the context to access resources
* @param currentRaw the raw card value of the current balance
* @param lastRaw the raw card value of the last payment
* @return the message containing all values
*/
private fun lookAtMe(context: Context, currentRaw: Int, lastRaw: Int): MaterialDialog {
val dialog = MaterialDialog(context)
.customView(R.layout.dialog_mensa_credit)
val current = if (!Preferences.oGiants) {
String.format("%.2f €", (currentRaw.toFloat() / 1000))
} else {
String.format("%.4f shm", (currentRaw.toFloat() * 0.0000075))
}
val last = if (!Preferences.oGiants) {
String.format("%.2f €", (lastRaw.toFloat() / 1000))
} else {
String.format("%.4f shm", (lastRaw.toFloat() * 0.0000075))
}
dialog.txtView_current.text = current
dialog.txtView_last.text = context.resources.getString(R.string.mensa_last, last)
return dialog
}
}
}

View File

@ -0,0 +1,236 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.controller
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jsoup.HttpStatusException
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.util.GradeSubject
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
/**
* Parse the qispos site the get all needed data for the grades fragment
*/
class QISPOSParser(val context: Context) {
private val className = this.javaClass.name
private val baseURL = "https://notenverwaltung.hs-offenburg.de"
private val loginPath = "/qispos/rds?state=user&type=1&category=auth.login&startpage=portal.vm&breadCrumbSource=portal"
/**
* check if qispos is available
* @return a http status code, 999 if no HttpStatusException supplied
*/
fun checkQISPOSStatus(): Int {
return runBlocking {
withContext(Dispatchers.IO) {
val socketFactory = createSSLSocketFactory()
try {
val res = Jsoup.connect(baseURL + loginPath)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.execute()
res.statusCode()
} catch (exHttp: HttpStatusException) {
exHttp.statusCode
} catch (ex: Exception) {
Log.e(className, "Error while checking status", ex)
999
}
}
}
}
/**
* parse the html from readGrades()
* @return a SortedMap, each entry is a semester, each semester has a ArrayList with subjects
*/
fun parseGrades(): SortedMap<String, ArrayList<GradeSubject>> {
val gradesMap = HashMap<String, ArrayList<GradeSubject>>()
val gradesListHtml = readGrades()
gradesListHtml?.select("table > tbody > tr")?.forEach {
val row = it.select("td.tabelle1_alignleft,td.tabelle1_aligncenter,td.tabelle1_alignright")
// only real subjects will be selected
if(row.size >= 6 && row[0].text().length >=7) {
val subject = GradeSubject(
id = row[0].text(),
name = row[1].text(),
semester = row[2].text(),
grade = if (row[3].text().isNotEmpty()) row[3].text() else row[4].text(),
credits = row[5].text()
)
if (gradesMap.containsKey(subject.semester)) {
gradesMap[subject.semester]!!.add(subject)
} else {
gradesMap[subject.semester] = arrayListOf(subject)
}
}
}
// return the sorted map
return gradesMap.toSortedMap(compareBy<String>{
val oText = it.substringAfter(" ")
if (oText.contains("/")) {
oText.substringBefore("/").toInt() + 0.5
} else {
oText.toDouble()
}
}.thenBy { it })
}
/**
* read the grades html from qispos
* @return the grades list as html element or null
*/
private fun readGrades(): Element?{
val credentials = EncryptedPreferences.readCredentials(context)
val username = credentials.first.substringBefore("@")
val password = credentials.second
return runBlocking {
withContext(Dispatchers.IO) {
try {
val socketFactory = createSSLSocketFactory()
// login, asdf = username, fdsa = password, wtf
val list = mapOf(
Pair("asdf", username),
Pair("fdsa", password),
Pair("submit", "Anmelden")
)
// login and get session cookies
val res = Jsoup.connect(baseURL + loginPath)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.data(list)
.postDataCharset("UTF-8")
.execute()
Log.i(className, "login status is: ${res.statusMessage()}: ${res.statusCode()}")
val loginCookies = res.cookies()
// grades root document and url
val rootHtml =Jsoup.parse(res.body())
val gradesRootLink = rootHtml.select("li.menueListStyle > a.auflistung").last().attr("abs:href")
// parse grades url
val gradesHtml = Jsoup.connect(gradesRootLink)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.cookies(loginCookies)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.get()
val gradesNextLink = gradesHtml.select("li.treelist > a.regular").attr("abs:href")
val gradesNextHtml = Jsoup.connect(gradesNextLink)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.cookies(loginCookies)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.get()
val gradesListLink = gradesNextHtml.selectFirst("li.treelist > ul > li").selectFirst("a").attr("abs:href")
// get the grades list
val gradesListHtml = Jsoup.connect(gradesListLink)
.sslSocketFactory(socketFactory)
.followRedirects(true)
.cookies(loginCookies)
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
.get()
gradesListHtml.body()
} catch (ex: Exception) {
Log.e(className, "error while loading qispos", ex)
null
}
}
}
}
/**
* since the HS has a fucked up tls setup we need to work around that
*/
private fun createSSLSocketFactory(): SSLSocketFactory {
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
val caInput = context.resources.openRawResource(R.raw.notenverwaltung_hs_offenburg_de)
val ca = caInput.use {
cf.generateCertificate(it) as X509Certificate
}
// Create a KeyStore containing our trusted CAs
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType).apply {
load(null, null)
setCertificateEntry("ca", ca)
}
// Create a TrustManager that trusts the CAs inputStream our KeyStore
val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
init(keyStore)
}
// Create an SSLContext that uses our TrustManager
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, tmf.trustManagers, null)
}
return sslContext.socketFactory
}
}

View File

@ -0,0 +1,116 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.controller
import com.google.gson.Gson
import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.util.*
import java.net.URL
/**
* This Controller calls the tcor api,
* all functions return tcor api objects
*/
class TCoRAPIController {
companion object {
private const val tcorBaseURL = "https://tcor.mosad.xyz"
/**
* Get a array of all currently available courses at the tcor API.
* Read the json object from tcor api
*/
fun getCourseListNEW(): CoursesList {
val url = URL("$tcorBaseURL/courseList")
return Gson().fromJson(url.readText(), CoursesList().javaClass)
}
/**
* Get current and next weeks mensa menus from the tcor API.
* Read the json object from tcor api
*/
fun getMensaMenu(): MensaMenu {
val url = URL("$tcorBaseURL/mensamenu")
return Gson().fromJson(url.readText(), MensaMenu().javaClass)
}
/**
* Get the timetable for "courseName" at week "week"
* Read the json object from tcor api
* @param courseName the course name (e.g AI1)
* @param week the week to update (0 for the current and so on)
*/
fun getTimetable(courseName: String, week: Int): TimetableWeek {
val url = URL("$tcorBaseURL/timetable?course=$courseName&week=$week")
val timetableCW = Gson().fromJson(url.readText(), TimetableCourseWeek().javaClass)
return TimetableWeek(
timetableCW.meta.weekIndex,
timetableCW.meta.weekNumberYear,
timetableCW.timetable.days
)
}
/**
* Get all lessons for a course at one week (async)
* @param courseName the course name
* @param week the week to look up
*/
fun getSubjectListAsync(courseName: String, week: Int): Deferred<ArrayList<String>> {
val url = URL("$tcorBaseURL/subjectList?course=$courseName&week=$week")
return GlobalScope.async {
Gson().fromJson(url.readText(), ArrayList<String>()::class.java)
}
}
/**
* Get all occurrences of a lesson for a course at one week
* @param courseName the course name
* @param subject the subject to search for
* @param week the week to look up
*/
fun getLessons(courseName: String, subject: String, week: Int): ArrayList<Lesson> {
val url = URL("$tcorBaseURL/lessons?course=$courseName&subject=$subject&week=$week")
var array: ArrayList<Lesson>
runBlocking {
withContext(Dispatchers.Default) {
array = Gson().fromJson(
JsonParser.parseString(url.readText()).asJsonArray,
object : TypeToken<ArrayList<Lesson>>() {}.type
)
}
}
return array
}
}
}

View File

@ -0,0 +1,375 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.controller.cache
import android.content.Context
import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.coursesCacheTime
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.mensaCacheTime
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.timetableCacheTime
import org.mosad.seil0.projectlaogai.util.*
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
/**
* The cacheController reads and updates the cache files.
* It contains the courseList and mensaMenu object, all timetable objects
* are located in TimetableController.
*/
class CacheController(cont: Context) {
private val className = "CacheController"
private val context = cont
init {
val cal = Calendar.getInstance()
val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
val currentTime = System.currentTimeMillis() / 1000
// check if we need to update the mensa data before displaying it
cal.time = Date(mensaCacheTime * 1000)
// if a) it's monday and the last cache update was on sunday or b) the cache is older than 24hr, update blocking
if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - mensaMenu.meta.updateTime) > 86400) {
Log.i(className, "update mensa blocking")
GlobalScope.launch(Dispatchers.Default) { updateMensaMenu(context).join() }
}
// check if we need to update the timetable before displaying it
cal.time = Date(timetableCacheTime * 1000)
// if a) it`s monday and the last cache update was not on a sunday or b) the cache is older than 24hr, update blocking
if ((currentDay == Calendar.MONDAY && cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) || (currentTime - timetableCacheTime) > 86400) {
Log.i(className, "updating timetable after sunday!")
GlobalScope.launch(Dispatchers.Default) {
val threads = listOf(
updateTimetable(cCourse.courseName, 0, context),
updateTimetable(cCourse.courseName, 1, context)
)
threads.joinAll()
}
}
updateCourseList(context)
readStartCache(cCourse.courseName) // initially read values from cache
// check if an update is necessary, not blocking
if (currentTime - coursesCacheTime > 86400)
updateCourseList(context)
if (currentTime - mensaCacheTime > 10800)
updateMensaMenu(context)
if (currentTime - timetableCacheTime > 10800) {
TimetableController.update(context)
}
}
companion object {
private const val className = "CacheController"
var coursesList = ArrayList<Course>()
var mensaMenu = MensaMenu()
/**
* update the course list, async
*/
fun updateCourseList(context: Context): Job {
val file = File(context.filesDir, "courses.json")
var courseListUp = CoursesList()
return GlobalScope.launch(Dispatchers.IO) {
try {
courseListUp = TCoRAPIController.getCourseListNEW()
} catch (ex: Exception) {
Log.e(className, "could not load course list from tcor", ex)
}
// update coursesList array list
coursesList = courseListUp.courses
// save cache file and update time
save(file, Gson().toJson(courseListUp))
coursesCacheTime = System.currentTimeMillis() / 1000
Preferences.save(context)
}
}
/**
* update the mensa menu, async
*/
fun updateMensaMenu(context: Context): Job {
val file = File(context.filesDir, "mensa.json")
return GlobalScope.launch(Dispatchers.IO) {
try {
mensaMenu = TCoRAPIController.getMensaMenu()
} catch (ex: Exception) {
Log.e(className, "could not load mensa menu from tcor", ex)
}
// save cache file and update time
save(file, Gson().toJson(mensaMenu))
mensaCacheTime = System.currentTimeMillis() / 1000
Preferences.save(context)
}
}
/**
* update the timetable for a week, async
* @param courseName the course name (e.g AI1)
* @param week the week to update (0 for the current and so on)
*/
fun updateTimetable(courseName: String, week: Int, context: Context): Job {
val file = File(context.filesDir, "timetable-$courseName-$week.json")
var timetable = TimetableWeek()
// try to update timetable from tcor, async
return GlobalScope.launch(Dispatchers.IO) {
try {
timetable = TCoRAPIController.getTimetable(courseName, week)
} catch (ex: Exception) {
Log.e(className, "could not load timetable $courseName[$week] from tcor", ex)
}
// update timetable in TTC
if (TimetableController.timetable.size > week) {
TimetableController.timetable[week] = timetable
} else {
TimetableController.timetable.add(timetable)
}
// save cache file and update time
save(file, Gson().toJson(timetable))
timetableCacheTime = System.currentTimeMillis() / 1000
Preferences.save(context)
}
}
/**
* update all additional subject lessons, async
*/
fun updateAdditionalLessons(context: Context): Job {
val fileLessons = File(context.filesDir, "additional_lessons.json")
return GlobalScope.launch(Dispatchers.IO) {
TimetableController.subjectMap.forEach { (courseName, subjects) ->
// update all subjects for a course
subjects.forEach {subject ->
try {
TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson ->
TimetableController.addLesson(courseName, subject, lesson)
}
} catch (ex: Exception) {
Log.e(className, "could not load $courseName: $subject", ex)
}
}
}
save(fileLessons, Gson().toJson(TimetableController.lessonMap))
}
}
/**
* save changes in lessonMap and subjectMap,
* called on addSubject or removeSubject
*/
fun saveAdditionalSubjects(context: Context) {
val fileLessons = File(context.filesDir, "additional_lessons.json")
val fileSubjects = File(context.filesDir, "additional_subjects.json")
save(fileLessons, Gson().toJson(TimetableController.lessonMap))
save(fileSubjects, Gson().toJson(TimetableController.subjectMap))
}
private fun save(file: File, text: String) {
try {
val writer = BufferedWriter(FileWriter(file))
writer.write(text)
writer.close()
} catch (ex: Exception) {
Log.e(className, "failed to write file \"${file.absoluteFile}\"", ex)
}
}
}
/**
* read coursesList, mensa (current and next week), timetable (current and next week)
* @param courseName the course name (e.g AI1)
*/
private fun readStartCache(courseName: String) {
try {
readCoursesList()
} catch (ex : Exception) {
Log.e(className, "Error while reading the course list", ex)
}
try {
readMensa()
} catch (ex : Exception) {
Log.e(className, "Error while reading the mensa menu", ex)
}
try {
readTimetable(courseName, 0)
} catch (ex : Exception) {
Log.e(className, "Error while reading timetable week 0", ex)
}
try {
readTimetable(courseName, 1)
} catch (ex : Exception) {
Log.e(className, "Error while reading timetable week 1", ex)
}
try {
readAdditionalSubjects()
} catch (ex : Exception) {
Log.e(className, "Error while reading additional subjects", ex)
}
}
/**
* read the courses list from the cached file
* add them to the coursesList object
*/
private fun readCoursesList() {
val file = File(context.filesDir, "courses.json")
// make sure the file exists
if (!file.exists()) {
runBlocking { updateCourseList(context).join() }
return
}
coursesList = FileReader(file).use {
GsonBuilder().create().fromJson(BufferedReader(it).readLine(), CoursesList().javaClass).courses
}
}
/**
* get the MensaMenu object from the cached json,
* if cache is empty create the cache file
*/
private fun readMensa() {
val file = File(context.filesDir, "mensa.json")
// make sure the file exists
if (!file.exists()) {
runBlocking { updateMensaMenu(context).join() }
return
}
mensaMenu = FileReader(file).use {
GsonBuilder().create().fromJson(BufferedReader(it).readLine(), MensaMenu().javaClass)
}
}
/**
* read the weeks timetable from the cached file
* @param courseName the course name (e.g AI1)
* @param week the week to read (0 for the current and so on)
*/
private fun readTimetable(courseName: String, week: Int) {
val file = File(context.filesDir, "timetable-$courseName-$week.json")
// if the file does not exist, call updateTimetable blocking and return
if (!file.exists()) {
runBlocking { updateTimetable(courseName, week, context).join() }
return
}
val timetableObject = FileReader(file).use {
JsonParser.parseString(BufferedReader(it).readLine()).asJsonObject
}
// if its a TimetableCourseWeek object migrate to TimetableWeek TODO remove converting at version 0.8
val timetable = if(timetableObject.has("meta")) {
Log.i(Companion.className, "trying to migrate TimetableCourseWeek to TimetableWeek")
val timetableWC = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)
save(file, Gson().toJson(TimetableWeek(
timetableWC.meta.weekIndex,
timetableWC.meta.weekNumberYear,
timetableWC.timetable.days
)))
TimetableWeek(timetableWC.meta.weekIndex, timetableWC.meta.weekNumberYear, timetableWC.timetable.days)
} else {
Gson().fromJson(timetableObject, TimetableWeek().javaClass)
}
// update timetable in TTC
if (TimetableController.timetable.size > week) {
TimetableController.timetable[week] = timetable
} else {
TimetableController.timetable.add(timetable)
}
}
private fun readAdditionalSubjects() {
val fileLessons = File(context.filesDir, "additional_lessons.json")
val fileSubjects = File(context.filesDir, "additional_subjects.json")
// make sure the file exists
if (!fileLessons.exists() || !fileSubjects.exists()) {
return
}
// clear the maps before loading, just to be save
TimetableController.lessonMap.clear()
TimetableController.subjectMap.clear()
// read subjects and lessons from cache file
FileReader(fileLessons).use {
TimetableController.lessonMap.putAll(
GsonBuilder().create()
.fromJson(BufferedReader(it).readLine(), object : TypeToken<HashMap<String, Lesson>>() {}.type)
)
}
FileReader(fileSubjects).use {
TimetableController.subjectMap.putAll(
GsonBuilder().create()
.fromJson(BufferedReader(it).readLine(), HashMap<String, ArrayList<String>>().javaClass)
)
}
// add lessons to timetable
TimetableController.lessonMap.forEach { (_, lesson) ->
TimetableController.addLessonToTimetable(lesson)
}
}
}

View File

@ -0,0 +1,131 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.controller.cache
import android.content.Context
import kotlinx.coroutines.Job
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse
import org.mosad.seil0.projectlaogai.util.Lesson
import org.mosad.seil0.projectlaogai.util.TimetableWeek
/**
* The TimetableController contains the timetable, subjectMap
* and lessonMap objects. It also contains the additional subjects logic.
* All functions ro read or update cache files are located in the CacheController.
*
* TODO
* * add second week
* * add configurable week to addSubject() and removeSubject(), updateAdditionalLessons()
*/
class TimetableController {
companion object {
val timetable = ArrayList<TimetableWeek>()
val lessonMap = HashMap<String, Lesson>() // the key is courseName-subject-lessonID
val subjectMap = HashMap<String, ArrayList<String>>() // the key is courseName
/**
* update the main timetable and all additional subjects, async
*/
fun update(context: Context): List<Job> {
return listOf(
CacheController.updateTimetable(cCourse.courseName, 0, context),
CacheController.updateTimetable(cCourse.courseName, 1, context),
CacheController.updateAdditionalLessons(context)
)
}
/**
* add a subject to the subjectMap and all it's lessons
* to the lessonMap
* @param courseName course to which the subject belongs
* @param subject the subjects name
*/
fun addSubject(courseName: String, subject: String, context: Context) {
// add subject
if (subjectMap.containsKey(courseName)) {
subjectMap[courseName]?.add(subject)
} else {
subjectMap[courseName] = arrayListOf(subject)
}
// add concrete lessons
TCoRAPIController.getLessons(courseName, subject, 0).forEach { lesson ->
addLesson(courseName, subject, lesson)
}
CacheController.saveAdditionalSubjects(context)
}
/**
* remove a subject from the subjectMap and all it's lessons
* from the lessonMap
* @param courseName course to which the subject belongs
* @param subject the subjects name
*/
fun removeSubject(courseName: String, subject: String, context: Context) {
// remove subject
subjectMap[courseName]?.remove(subject)
// remove concrete lessons
val iterator = lessonMap.iterator()
while (iterator.hasNext()) {
val it = iterator.next()
if(it.key.contains("$courseName-$subject")) {
// remove the lesson from the lessons list
iterator.remove() // use iterator to remove, otherwise ConcurrentModificationException
// remove the lesson from the timetable
val id = it.value.lessonID.split(".")
if(id.size == 3)
timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].remove(it.value)
}
}
CacheController.saveAdditionalSubjects(context)
}
/**
* add a lesson to the lessonMap, also add it to the timetable
*/
fun addLesson(courseName: String, subject: String, lesson: Lesson) {
//the courseName, subject and lessonID, separator: -
val key = "$courseName-$subject-${lesson.lessonID}"
lessonMap[key] = lesson
addLessonToTimetable(lesson)
}
/**
* add a lesson to the timetable
*/
fun addLessonToTimetable(lesson: Lesson) {
val id = lesson.lessonID.split(".")
if(id.size == 3)
timetable[0].days[id[0].toInt()].timeslots[id[1].toInt()].add(lesson)
}
}
}

View File

@ -0,0 +1,110 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.controller.preferences
import android.content.Context
import android.content.SharedPreferences
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import org.mosad.seil0.projectlaogai.R
object EncryptedPreferences {
var email = ""
internal set
/**
* save user email and password to encrypted preference
*/
fun saveCredentials(email: String, password: String, context: Context) {
this.email = email
with (getEncryptedPreferences(context)?.edit()) {
this?.putString(context.getString(R.string.save_key_user_email), email)
this?.putString(context.getString(R.string.save_key_user_password), password)
this?.apply()
}
}
/**
* read user email and password from encrypted preference
* @return Pair(email, password)
*/
fun readCredentials(context: Context): Pair<String, String> {
return with (getEncryptedPreferences(context)) {
email = this?.getString(context.getString(R.string.save_key_user_email), "").toString()
Pair(
this?.getString(context.getString(R.string.save_key_user_email), "").toString(),
this?.getString(context.getString(R.string.save_key_user_password), "").toString()
)
}
}
/**
* initially load the stored values
*/
fun load(context: Context) {
with(getEncryptedPreferences(context)) {
email = this?.getString(
context.getString(R.string.save_key_user_email), ""
).toString()
}
}
/**
* create a encrypted shared preference
*/
private fun getEncryptedPreferences(context: Context): SharedPreferences? {
return try {
val spec = KeyGenParameterSpec.Builder(MasterKey.DEFAULT_MASTER_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(MasterKey.DEFAULT_AES_GCM_MASTER_KEY_SIZE)
.build()
val masterKey = MasterKey.Builder(context)
.setKeyGenParameterSpec(spec)
.build()
EncryptedSharedPreferences.create(
context,
context.getString(R.string.encrypted_preference_file_key),
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (ex: Exception) {
Log.e(javaClass.name, "Could not create encrypted shared preference.", ex)
null
}
}
}

View File

@ -0,0 +1,192 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.controller.preferences
import android.content.Context
import android.graphics.Color
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.util.Course
/**
* The PreferencesController class
* contains all preferences and global variables that exist in this app
*/
object Preferences {
var coursesCacheTime: Long = 0
var mensaCacheTime: Long = 0
var timetableCacheTime: Long = 0
var cColorPrimary: Int = Color.parseColor("#009688")
var cColorAccent: Int = Color.parseColor("#0096ff")
var cCourse = Course(
"https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0",
"AI3"
)
var cShowBuffet = true
var oGiants = false
// TODO move!
var themePrimary = 0
var themeSecondary = 0
// the save function
fun save(context: Context) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
// save the update times (cache)
with (sharedPref.edit()) {
putLong(context.getString(R.string.save_key_coursesCacheTime),
coursesCacheTime
)
putLong(context.getString(R.string.save_key_mensaCacheTime),
mensaCacheTime
)
putLong(context.getString(R.string.save_key_timetableCacheTime),
timetableCacheTime
)
apply()
}
}
/**
* save the course locally
*/
fun saveCourse(context: Context, course: Course) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putString(context.getString(R.string.save_key_course), course.courseName)
putString(context.getString(R.string.save_key_courseTTLink), course.courseLink)
apply()
}
cCourse = course
}
/**
* save the primary color
*/
fun saveColorPrimary(context: Context, colorPrimary: Int) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putInt(context.getString(R.string.save_key_colorPrimary),
colorPrimary
)
apply()
}
cColorPrimary = colorPrimary
}
/**
* save the accent color
*/
fun saveColorAccent(context: Context, colorAccent: Int) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putInt(context.getString(R.string.save_key_colorAccent),
colorAccent
)
apply()
}
cColorAccent = colorAccent
}
/**
* save showBuffet
*/
fun saveShowBuffet(context: Context, showBuffet: Boolean) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putBoolean(context.getString(R.string.save_key_showBuffet),
showBuffet
)
apply()
}
cShowBuffet = showBuffet
}
/**
* initially load the stored values
*/
fun load(context: Context) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
// load the update times (cache)
coursesCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_coursesCacheTime
), 0)
mensaCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_mensaCacheTime
), 0)
timetableCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_timetableCacheTime
), 0)
// load saved course
cCourse = Course(
sharedPref.getString(context.getString(R.string.save_key_courseTTLink),
"https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0"
)!!,
sharedPref.getString(context.getString(R.string.save_key_course), "AI3")!!
)
// load saved colors
cColorPrimary = sharedPref.getInt(context.getString(
R.string.save_key_colorPrimary
), cColorPrimary)
cColorAccent = sharedPref.getInt(context.getString(
R.string.save_key_colorAccent
), cColorAccent)
// load showBuffet
cShowBuffet = sharedPref.getBoolean(context.getString(
R.string.save_key_showBuffet
), true)
}
}

View File

@ -0,0 +1,200 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.fragments
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.android.synthetic.main.fragment_grades.*
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.QISPOSParser
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
import org.mosad.seil0.projectlaogai.uicomponents.GradeLinearLayout
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog
/**
* The grades fragment class
* contains all needed parts to display and the grades screen
*/
class GradesFragment : Fragment() {
private lateinit var refreshLayoutGrades: SwipeRefreshLayout
private lateinit var parser: QISPOSParser
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grades, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
refreshLayoutGrades = view.findViewById(R.id.refreshLayout_Grades)
refreshLayoutGrades.isEnabled = false // disable swipe
refreshLayoutGrades.setProgressBackgroundColorSchemeColor(Preferences.themeSecondary)
parser = QISPOSParser(context!!)// init the parser
if (checkCredentials() && checkQisposStatus()) {
GlobalScope.launch(Dispatchers.Default) {
addGrades()
}
}
}
/**
* check if the credentials are set, if not show a login dialog
*/
private fun checkCredentials(): Boolean {
val credentials = EncryptedPreferences.readCredentials(context!!)
var credentialsPresent = false
// if there is no password set, show the login dialog
if (credentials.first.isEmpty() || credentials.second.isEmpty()) {
LoginDialog(this.context!!)
.positiveButton {
EncryptedPreferences.saveCredentials(email, password, context)
addGrades()
}
.negativeButton {
txtView_Loading.text = resources.getString(R.string.credentials_missing)
}
.show {
email = EncryptedPreferences.email
password = ""
}
} else {
credentialsPresent = true
}
return credentialsPresent
}
/**
* check if qispos is available, if not show an error
*/
private fun checkQisposStatus(): Boolean {
val statusCode = parser.checkQISPOSStatus()
// show error if the status code is not 200
if (statusCode != 200) {
val infoText = resources.getString(when(statusCode) {
503 -> R.string.qispos_unavailable
else -> R.string.qispos_generic_error
})
val img = ResourcesCompat.getDrawable(resources, R.drawable.ic_error_outline_black_24dp, null)?.apply {
bounds = Rect(0, 0, 75, 75)
}
txtView_Loading?.apply {
text = infoText
setCompoundDrawables(null, null, null, img)
}
}
return statusCode == 200
}
// add the grades to the layout, async
private fun addGrades() = GlobalScope.launch(Dispatchers.Default) {
withContext(Dispatchers.Main) {
refreshLayout_Grades.isRefreshing = true
}
val grades = parser.parseGrades()
withContext(Dispatchers.Main) {
linLayout_Grades.removeAllViews() // clear layout
}
// for each semester add a new card
grades.forEach { semester ->
val usedSubjects = ArrayList<String>()
val semesterCard = DayCardView(context!!)
semesterCard.setDayHeading(semester.key)
// for each subject add a new linLayout
semester.value.forEachIndexed { index, subject ->
if (usedSubjects.contains(subject.name)) {
return@forEachIndexed
}
// get the first sub subjects
val subSubject = semester.value.firstOrNull {
it.name.contains(subject.name) && it.name != subject.name
}
// if sub subject is not null, add it to used subjects
subSubject?.let {
usedSubjects.add(it.name)
}
val subjectLayout = GradeLinearLayout(context).set {
subjectName = subject.name
grade = subject.grade
subSubjectName = subSubject?.name.toString()
subGrade = subSubject?.grade.toString()
}
// disable sub-subject if not set
if (subSubject == null)
subjectLayout.disableSubSubject()
// disable divider if last element
if (index == semester.value.lastIndex || semester.value.indexOf(subSubject) == semester.value.lastIndex)
subjectLayout.disableDivider()
semesterCard.getLinLayoutDay().addView(subjectLayout)
}
// without context we can't access the view
withContext(Dispatchers.Main) {
linLayout_Grades.addView(semesterCard)
}
}
val txtViewLegal = TextView(context).apply {
text = resources.getString(R.string.without_guarantee)
textAlignment = View.TEXT_ALIGNMENT_CENTER
}
// stop refreshing and show legal warning
withContext(Dispatchers.Main) {
linLayout_Grades.addView(txtViewLegal)
refreshLayout_Grades.isRefreshing = false
}
}
}

View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
@ -22,20 +22,25 @@
package org.mosad.seil0.projectlaogai.fragments
import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.android.synthetic.main.fragment_home.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import org.mosad.seil0.projectlaogai.MainActivity
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.hsoparser.*
import org.mosad.seil0.projectlaogai.uicomponents.LessonCardView
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.mensaMenu
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController
import org.mosad.seil0.projectlaogai.util.Meal
import org.mosad.seil0.projectlaogai.util.TimetableDay
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
import org.mosad.seil0.projectlaogai.uicomponents.MealLinearLayout
import org.mosad.seil0.projectlaogai.util.NotRetardedCalendar
import java.text.SimpleDateFormat
import java.util.*
/**
@ -44,67 +49,66 @@ import java.util.*
*/
class HomeFragment : Fragment() {
private lateinit var linLayoutTimeTable: LinearLayout
private var mainActivity = MainActivity()
private val className = "HomeFragment"
private val formatter = SimpleDateFormat("E dd.MM", Locale.getDefault())
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
val view: View = inflater.inflate(R.layout.fragment_home, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// init UI elements
linLayoutTimeTable = view.findViewById(R.id.linLayoutTimeTable)
//setText()
addCurrentMensaMenu()
addCurrentTimeTable()
// Inflate the layout for this fragment
return view
addMensaMenu()
addTimeTable()
}
/**
* add the current mensa meal to the home screens
*/
private fun addCurrentMensaMenu() {
doAsync {
val dayMenus: ArrayList<Meal>
val cal = Calendar.getInstance()
private fun addMensaMenu() = GlobalScope.launch(Dispatchers.Default) {
// TODO needs testing
if (cal.get(Calendar.HOUR_OF_DAY) < 15) {
dayMenus = MensaParser().getMensaMenuDay(mainActivity.getWeekMenu(), cal.get(Calendar.DAY_OF_WEEK))
} else {
dayMenus = MensaParser().getMensaMenuDay(mainActivity.getWeekMenu(), cal.get(Calendar.DAY_OF_WEEK) + 1)
uiThread {
// TODO Mensa closed today is showing
txtView_Menu1Heading.text = resources.getString(R.string.meal_1_tomorrow)
txtView_Menu2Heading.text = resources.getString(R.string.meal_2_tomorrow)
var dayMeals: ArrayList<Meal>
val cal = Calendar.getInstance()
val mensaCardView = DayCardView(context!!)
withContext(Dispatchers.Main) {
if (isAdded) {
if (cal.get(Calendar.HOUR_OF_DAY) < 15) {
dayMeals = mensaMenu.currentWeek.days[NotRetardedCalendar.getDayOfWeekIndex()].meals
mensaCardView.setDayHeading(activity!!.resources.getString(R.string.today_date, formatter.format(cal.time)))
} else {
dayMeals = mensaMenu.currentWeek.days[NotRetardedCalendar.getTomorrowWeekIndex()].meals
cal.add(Calendar.DATE, 1)
mensaCardView.setDayHeading(activity!!.resources.getString(R.string.tomorrow_date, formatter.format(cal.time)))
}
}
uiThread {
if (dayMenus.size >= 2) {
if (dayMeals.size >= 2) {
// get the index of the first meal, not a "Schneller Teller"
loop@ for ((i, meal) in dayMenus.withIndex()) {
if(meal.heading.contains("Essen")) {
for (part in dayMenus[i].parts) {
txtViewMenu1.append(part)
}
loop@ for ((i, meal) in dayMeals.withIndex()) {
if (meal.heading.contains("Essen")) {
for (part in dayMenus[i + 1].parts) {
txtViewMenu2.append(part)
}
val meal1Layout = MealLinearLayout(context)
meal1Layout.setMeal(dayMeals[i])
mensaCardView.getLinLayoutDay().addView(meal1Layout)
val meal2Layout = MealLinearLayout(context)
meal2Layout.setMeal(dayMeals[i + 1])
meal2Layout.disableDivider()
mensaCardView.getLinLayoutDay().addView(meal2Layout)
break@loop
}
}
} else {
txtViewMenu1.text = resources.getString(R.string.no_meal_today)
txtViewMenu2.text = resources.getString(R.string.no_meal_today)
mensaCardView.getLinLayoutDay().addView(getNoCard(resources.getString(R.string.mensa_closed)))
}
linLayout_Home.addView(mensaCardView)
}
}
}
@ -112,51 +116,72 @@ class HomeFragment : Fragment() {
/**
* add the current timetable to the home screen
*/
private fun addCurrentTimeTable() {
val dayIndex = NotRetardedCalendar().getDayOfWeekIndex()
private fun addTimeTable() = GlobalScope.launch(Dispatchers.Default) {
if (mainActivity.getTimeTableCurrentWeek().isNotEmpty() && dayIndex < 6) {
withContext(Dispatchers.Main) {
val timeTableDay = mainActivity.getTimeTableCurrentWeek()[dayIndex]
for (i in 0..5) {
val lessonCardView = LessonCardView(context!!, null)
lessonCardView.getTxtViewLesson().text = resources.getString(R.string.string_new_line, timeTableDay[i].lessonSubject)
lessonCardView.getTxtViewLesson().append(timeTableDay[i].lessonTeacher + "\n")
lessonCardView.getTxtViewLesson().append(timeTableDay[i].lessonRoom)
lessonCardView.getTxtViewTime().text = DataTypes().getTime()[i]
if(lessonCardView.getTxtViewLesson().text.length > 2)
linLayoutTimeTable.addView(lessonCardView)
if (isAdded && TimetableController.timetable.isNotEmpty()) {
try {
val dayCardView = findNextDay(NotRetardedCalendar.getDayOfWeekIndex())
linLayout_Home.addView(dayCardView)
} catch (ex: Exception) {
Log.e(className, "could not load timetable", ex) // TODO send feedback
}
}
// add a card if there is no lesson today
if (linLayoutTimeTable.childCount == 0) {
// TODO we could display the next day with a lecture
val noLessonCardView = LessonCardView(context!!, null)
noLessonCardView.getTxtViewLesson().text = resources.getString(R.string.no_lesson_today)
linLayoutTimeTable.addView(noLessonCardView)
}
} else {
if (dayIndex == 6) {
// if that's the case it's sunday
val noLessonCardView = LessonCardView(context!!, null)
noLessonCardView.getTxtViewLesson().text = resources.getString(R.string.no_lesson_today)
linLayoutTimeTable.addView(noLessonCardView)
} else {
MaterialDialog(context!!)
.title(R.string.error)
.message(R.string.gen_tt_error)
.show()
// TODO log the error and send feedback
}
}
}
fun setMainActivity(mainActivity: MainActivity) {
this.mainActivity = mainActivity
/**
* find the next day with a lesson
* start at week 0, startDayIndex and search every cached week until we find a) a day with a timetable
* or b) we find no timetable and add a no lesson card
* @param startDayIndex the day index you want to start searching
* @return a DayCardView with all lessons added
*/
private fun findNextDay(startDayIndex: Int): DayCardView {
val dayCardView = DayCardView(context!!)
var dayTimetable: TimetableDay? = null
var dayIndexSearch = startDayIndex
var weekIndexSearch = 0
while (dayTimetable == null && weekIndexSearch < TimetableController.timetable.size) {
for (dayIndex in dayIndexSearch..5) {
dayTimetable = TimetableController.timetable[weekIndexSearch].days[dayIndex]
// some wired calendar magic, calculate the correct date to be shown ((timetable week - current week * 7) + (dayIndex - dayIndex of current week)
val daysToAdd = ((TimetableController.timetable[weekIndexSearch].weekNumberYear - NotRetardedCalendar.getWeekOfYear())
* 7 + (dayIndex - NotRetardedCalendar.getDayOfWeekIndex()))
dayCardView.addTimetableDay(dayTimetable, daysToAdd)
// if there are no lessons don't show the dayCardView
if (dayCardView.getLinLayoutDay().childCount > 1)
return dayCardView
}
weekIndexSearch++
dayIndexSearch = 0
}
// there was no day found in the cached weeks, add no lesson card
dayCardView.setDayHeading(formatter.format(Calendar.getInstance().time))
dayCardView.getLinLayoutDay().addView(getNoCard(resources.getString(R.string.no_lesson_today))) // if there is no lecture at all show the no lesson card
return dayCardView
}
/**
* @param text the text to show on the card
* @return a TextView with the text and all needed parameters
*/
private fun getNoCard(text: String): TextView {
val noLesson = TextView(context)
noLesson.text = text
noLesson.textSize = 18.0F
noLesson.setTypeface(null, Typeface.BOLD)
noLesson.textAlignment = View.TEXT_ALIGNMENT_CENTER
noLesson.setPadding(7, 7, 7, 7)
return noLesson
}
}

View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
@ -23,18 +23,25 @@
package org.mosad.seil0.projectlaogai.fragments
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import org.mosad.seil0.projectlaogai.MainActivity
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_mensa.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.uicomponents.MensaDayCardView
import org.mosad.seil0.projectlaogai.uicomponents.MenuCardView
import java.util.*
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.mensaMenu
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cShowBuffet
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
import org.mosad.seil0.projectlaogai.uicomponents.MealLinearLayout
import org.mosad.seil0.projectlaogai.uicomponents.TextViewInfo
import org.mosad.seil0.projectlaogai.util.MensaWeek
import org.mosad.seil0.projectlaogai.util.NotRetardedCalendar
/**
* The mensa controller class
@ -42,76 +49,109 @@ import java.util.*
*/
class MensaFragment : Fragment() {
private lateinit var linLayoutMensaFragment: LinearLayout
private var mainActivity = MainActivity()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_mensa, container, false)
linLayoutMensaFragment = view.findViewById(R.id.linLayout_MensaFragment)
addCurrentWeek()
return view
return inflater.inflate(R.layout.fragment_mensa, container, false)
}
private fun addCurrentWeek() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
doAsync {
refreshLayout_Mensa.setProgressBackgroundColorSchemeColor(Preferences.themeSecondary)
uiThread {
refreshAction() // init actions
for(day in Calendar.getInstance().get(Calendar.DAY_OF_WEEK)..7) {
GlobalScope.launch(Dispatchers.Default) {
val dayCurrent = if(NotRetardedCalendar.getDayOfWeekIndex() == 6) 0 else NotRetardedCalendar.getDayOfWeekIndex()
addWeek(mensaMenu.currentWeek, dayCurrent).join()
val strDay: String = when(day) {
Calendar.MONDAY -> "Mon"
Calendar.TUESDAY -> "Die"
Calendar.WEDNESDAY -> "Mit"
Calendar.THURSDAY -> "Don"
Calendar.FRIDAY -> "Fre"
Calendar.SATURDAY -> "Sam"
else -> "TODAY" // the app will likely crash here
// add the next week
addWeek(mensaMenu.nextWeek, 0)
}
// show a info if there are no more menus
if (linLayout_Mensa.childCount == 0) {
val txtViewInfo = TextViewInfo(context!!).set {
txt = resources.getString(R.string.no_more_meals)
}
linLayout_Mensa.addView(txtViewInfo)
}
}
/**
* add all menus from dayStart to Friday for a given week
* @param menusWeek menu of type MensaWeek you want to add
* @param dayStart the first day of the week to add
*/
private fun addWeek(menusWeek: MensaWeek, dayStart: Int) = GlobalScope.launch(Dispatchers.Default) {
withContext(Dispatchers.Main) {
// only add the days dayStart to Fri since the mensa is closed on Sat/Sun
for (dayIndex in dayStart..4) {
var helpMeal = MealLinearLayout(context)
val dayCardView = DayCardView(context!!)
if(menusWeek.days[dayIndex].meals.isNotEmpty())
dayCardView.setDayHeading(menusWeek.days[dayIndex].meals[0].day)
for (meal in menusWeek.days[dayIndex].meals) {
val mealLayout = MealLinearLayout(context)
mealLayout.setMeal(meal)
if(meal.heading != "Buffet" || cShowBuffet) {
dayCardView.getLinLayoutDay().addView(mealLayout)
helpMeal = mealLayout
}
val cardViewMensaDay = MensaDayCardView(context!!, null)
var add = false
for (meal in mainActivity.getWeekMenu()) {
//println("Day: " + meal.day)
if (meal.day.contains(strDay)) {
val menuViewMenu = MenuCardView(context!!, null)
menuViewMenu.setMenuHeading(meal.heading)
for(part in meal.parts) {
menuViewMenu.getTxtViewMenu().append(part)
}
cardViewMensaDay.setDayHeading(meal.day) //TODO move this out of the first for loop, performance!!
cardViewMensaDay.getLinLayoutMensaDay().addView(menuViewMenu)
add = true
}
}
if(add)
linLayoutMensaFragment.addView(cardViewMensaDay)
}
// add a card if there are no more meals in this week
if(linLayoutMensaFragment.childCount == 0) {
val cardViewNoMoreFood = MensaDayCardView(context!!, null)
cardViewNoMoreFood.setDayHeading(resources.getString(R.string.no_more_food))
linLayoutMensaFragment.addView(cardViewNoMoreFood)
}
helpMeal.disableDivider()
if(dayCardView.getLinLayoutDay().childCount > 2)
linLayout_Mensa.addView(dayCardView)
}
}
}
/**
* initialize the refresh action
*/
private fun refreshAction() = GlobalScope.launch(Dispatchers.Default) {
withContext(Dispatchers.Main) {
// set the refresh listener
refreshLayout_Mensa.setOnRefreshListener {
updateMensaScreen()
}
}
}
fun setMainActivity(mainActivity: MainActivity) {
this.mainActivity = mainActivity
/**
* refresh the mensa cache and update the mensa screen
*/
private fun updateMensaScreen() = GlobalScope.launch(Dispatchers.Default) {
CacheController.updateMensaMenu(context!!).join() // blocking since we want the new data
withContext(Dispatchers.Main) {
// remove all menus from the layout
linLayout_Mensa.removeAllViews()
// add the refreshed menus
val dayCurrent = if (NotRetardedCalendar.getDayOfWeekIndex() == 6) 0 else NotRetardedCalendar.getDayOfWeekIndex()
addWeek(mensaMenu.currentWeek, dayCurrent).join()
// add the next week
addWeek(mensaMenu.nextWeek, 0)
refreshLayout_Mensa.isRefreshing = false
// show a info if there are no more menus
if (linLayout_Mensa.childCount == 0) {
val txtViewInfo = TextViewInfo(context!!).set {
txt = resources.getString(R.string.no_more_meals)
}
linLayout_Mensa.addView(txtViewInfo)
}
}
}
}

View File

@ -0,0 +1,59 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.fragments
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebSettings
import android.webkit.WebView
import org.mosad.seil0.projectlaogai.R
import android.webkit.WebViewClient
/**
* The moodle screen controller class
* contains all needed parts to display and the moodle screen
*/
class MoodleFragment : Fragment() {
private lateinit var webView: WebView
private lateinit var webSettings: WebSettings
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_moodle, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webView = view.findViewById(R.id.webView)
webView.loadUrl("https://elearning.hs-offenburg.de/moodle/")
webSettings = webView.settings
//webSettings.setJavaScriptEnabled(true) // Enable Javascript
webView.webViewClient = WebViewClient() // Force links and redirects to open in the WebView instead of in a browser
}
}

View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
@ -22,24 +22,43 @@
package org.mosad.seil0.projectlaogai.fragments
import android.graphics.Color
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.fragment.app.Fragment
import com.afollestad.aesthetic.Aesthetic
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.WhichButton
import com.afollestad.materialdialogs.actions.getActionButton
import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.color.colorChooser
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.listItems
import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import de.psdev.licensesdialog.LicensesDialog
import kotlinx.android.synthetic.main.fragment_settings.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import org.mosad.seil0.projectlaogai.MainActivity
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.BuildConfig
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.hsoparser.CourseTTLink
import java.util.ArrayList
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.coursesList
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorAccent
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorPrimary
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cCourse
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cShowBuffet
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.LoginDialog
import org.mosad.seil0.projectlaogai.util.DataTypes
import java.util.*
/**
* The settings controller class
@ -49,33 +68,68 @@ class SettingsFragment : Fragment() {
private lateinit var linLayoutUser: LinearLayout
private lateinit var linLayoutCourse: LinearLayout
private lateinit var linLayoutInfo: LinearLayout
private lateinit var linLayoutMainColor: LinearLayout
private lateinit var viewPrimaryColor: View
private lateinit var courseTTLinkList: ArrayList<CourseTTLink>
private var mainActivity = MainActivity()
private lateinit var linLayoutManageLessons: LinearLayout
private lateinit var linLayoutAbout: LinearLayout
private lateinit var linLayoutLicence: LinearLayout
private lateinit var linLayoutTheme: LinearLayout
private lateinit var linLayoutPrimaryColor: LinearLayout
private lateinit var linLayoutAccentColor: LinearLayout
private lateinit var switchBuffet: SwitchCompat
private lateinit var txtViewCourse: TextView
private var selectedTheme = DataTypes.Theme.Light
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_settings, container, false)
linLayoutUser = view.findViewById(R.id.linLayout_User)
linLayoutCourse = view.findViewById(R.id.linLayout_Course)
linLayoutInfo = view.findViewById(R.id.linLayout_Info)
linLayoutMainColor = view.findViewById(R.id.linLayout_MainColor)
viewPrimaryColor = view.findViewById(R.id.view_PrimaryColor)
initActions()
// Inflate the layout for this fragment
return view
return inflater.inflate(R.layout.fragment_settings, container, false)
}
/**
* initialize the settings gui
*/
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
txtView_Course.text = mainActivity.getCourse().course
linLayoutUser = view.findViewById(R.id.linLayout_User)
linLayoutCourse = view.findViewById(R.id.linLayout_Course)
linLayoutManageLessons = view.findViewById(R.id.linLayout_ManageLessons)
linLayoutAbout = view.findViewById(R.id.linLayout_About)
linLayoutLicence = view.findViewById(R.id.linLayout_Licence)
linLayoutTheme = view.findViewById(R.id.linLayout_Theme)
linLayoutPrimaryColor = view.findViewById(R.id.linLayout_PrimaryColor)
linLayoutAccentColor = view.findViewById(R.id.linLayout_AccentColor)
switchBuffet = view.findViewById(R.id.switch_buffet)
// if we call txtView_Course via KAE view binding it'll result in a NPE in the onDismissed call
txtViewCourse = view.findViewById(R.id.txtView_Course)
initActions()
txtView_User.text = EncryptedPreferences.email.ifEmpty { resources.getString(R.string.sample_user) }
txtView_Course.text = cCourse.courseName
txtView_AboutDesc.text = resources.getString(R.string.about_version, BuildConfig.VERSION_NAME, getString(R.string.build_time))
switch_buffet.isChecked = cShowBuffet // init switch
val outValue = TypedValue()
activity!!.theme.resolveAttribute(R.attr.themeName, outValue, true)
when(outValue.string) {
"light" -> {
switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryLight, activity!!.theme))
selectedTheme = DataTypes.Theme.Light
selectedTheme.string = resources.getString(R.string.themeLight)
}
"dark" -> {
switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryDark, activity!!.theme))
selectedTheme = DataTypes.Theme.Dark
selectedTheme.string = resources.getString(R.string.themeDark)
}
"black" -> {
switch_buffet.setTextColor(activity!!.resources.getColor(R.color.textPrimaryDark, activity!!.theme))
selectedTheme = DataTypes.Theme.Black
selectedTheme.string = resources.getString(R.string.themeBlack)
}
}
txtView_SelectedTheme.text = selectedTheme.string
}
/**
@ -84,61 +138,187 @@ class SettingsFragment : Fragment() {
private fun initActions() {
linLayoutUser.setOnClickListener {
// open a new dialog
LoginDialog(context!!)
.positiveButton {
EncryptedPreferences.saveCredentials(email, password, context)
}
.show {
email = EncryptedPreferences.email
password = ""
}
}
linLayoutUser.setOnLongClickListener {
Preferences.oGiants = true // enable easter egg
return@setOnLongClickListener true
}
linLayoutCourse.setOnClickListener {
// open a new dialog
val courseList = ArrayList<String>()
selectCourse(context!!).show {
onDismiss {
txtViewCourse.text = cCourse.courseName // update txtView after the dialog is dismissed
}
}
}
courseTTLinkList = mainActivity.getCourseTTLinkList()
courseTTLinkList.forEach { (_, course) ->
courseList.add(course)
linLayoutManageLessons.setOnClickListener {
val lessons = ArrayList<String>()
TimetableController.subjectMap.forEach { pair ->
pair.value.forEach {
lessons.add("${pair.key} - $it")
}
}
MaterialDialog(context!!).listItems(items = courseList){ _, index, text ->
txtView_Course.text = text // update txtView
MaterialDialog(context!!).show {
title(R.string.manage_lessons)
positiveButton(R.string.delete)
negativeButton(R.string.cancel)
getActionButton(WhichButton.POSITIVE).updateTextColor(cColorAccent)
getActionButton(WhichButton.NEGATIVE).updateTextColor(cColorAccent)
val dialog = MaterialDialog(context!!).cancelable(false)
listItemsMultiChoice(items = lessons) { _, _, items ->
items.forEach {
val list = it.split(" - ")
TimetableController.removeSubject(list[0], list[1], context)
}
}
}
}
linLayoutAbout.setOnClickListener {
// open a new info dialog
MaterialDialog(context!!)
.title(R.string.about_dialog_heading)
.message(R.string.about_dialog_text)
.show()
}
linLayoutLicence.setOnClickListener {
// do the theme magic, as the lib's theme support is broken
val outValue = TypedValue()
context!!.theme.resolveAttribute(R.attr.themeName, outValue, true)
val dialogCss = when (outValue.string) {
"light" -> R.string.license_dialog_style_light
else -> R.string.license_dialog_style_dark
}
val themeId = when (outValue.string) {
"light" -> R.style.AppTheme_Light
else -> R.style.LicensesDialogTheme_Dark
}
// open a new license dialog
LicensesDialog.Builder(context!!)
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
.setIncludeOwnLicense(true)
.setThemeResourceId(themeId)
.setNoticesCssStyle(dialogCss)
.build()
.show()
}
linLayoutTheme.setOnClickListener {
val themes = listOf(
resources.getString(R.string.themeLight),
resources.getString(R.string.themeDark),
resources.getString(R.string.themeBlack)
)
MaterialDialog(context!!).show {
title(R.string.theme)
listItemsSingleChoice(items = themes, initialSelection = selectedTheme.ordinal) { _, index, _ ->
Aesthetic.config {
when (index) {
0 -> activityTheme(R.style.AppTheme_Light)
1 -> activityTheme(R.style.AppTheme_Dark)
2 -> activityTheme(R.style.AppTheme_Black)
else -> activityTheme(R.style.AppTheme_Light)
}
apply()
}
}
}
}
linLayoutPrimaryColor.setOnClickListener {
// open a new color chooser dialog
MaterialDialog(context!!)
.colorChooser(DataTypes().primaryColors, allowCustomArgb = true, initialSelection = cColorPrimary) { _, color ->
view_PrimaryColor.setBackgroundColor(color)
Aesthetic.config {
colorPrimary(color)
colorPrimaryDark(color)
apply()
}
Preferences.saveColorPrimary(context!!, color)
}
.show {
title(R.string.primary_color)
positiveButton(R.string.select)
getActionButton(WhichButton.POSITIVE).updateTextColor(cColorAccent)
}
}
linLayoutAccentColor.setOnClickListener {
// open a new color chooser dialog
MaterialDialog(context!!)
.colorChooser(DataTypes().accentColors, allowCustomArgb = true, initialSelection = cColorAccent) { _, color ->
view_AccentColor.setBackgroundColor(color)
Aesthetic.config {
colorAccent(color)
apply()
}
Preferences.saveColorAccent(context!!, color)
}
.show{
title(R.string.accent_color)
positiveButton(R.string.select)
getActionButton(WhichButton.POSITIVE).updateTextColor(cColorAccent)
}
}
switchBuffet.setOnClickListener {
Preferences.saveShowBuffet(context!!, switchBuffet.isChecked)
}
}
fun selectCourse(context: Context) : MaterialDialog {
val courseNameList = ArrayList<String>()
coursesList.forEach { (_, courseName) ->
courseNameList.add(courseName)
}
// return a new course selection dialog
return MaterialDialog(context)
.title(R.string.select_course)
.listItems(items = courseNameList) { _, index, _ ->
val loadingDialog = MaterialDialog(context).cancelable(false)
.cancelOnTouchOutside(false)
.customView(R.layout.dialog_loading)
dialog.show()
loadingDialog.show()
doAsync {
mainActivity.updateCourse(courseTTLinkList[index])
GlobalScope.launch(Dispatchers.Default) {
Preferences.saveCourse(context, coursesList[index]) // save the course
uiThread {
dialog.dismiss()
// update current & next weeks timetable
val threads = listOf(
CacheController.updateTimetable(cCourse.courseName, 0, context),
CacheController.updateTimetable(cCourse.courseName, 1, context)
)
threads.joinAll() // blocking since we want the new data
withContext(Dispatchers.Main) {
loadingDialog.dismiss()
}
}
}
.show()
}
linLayoutInfo.setOnClickListener {
// open a new info dialog
MaterialDialog(context!!)
.title(R.string.about)
.message(R.string.about_text)
.show()
}
linLayoutMainColor.setOnClickListener {
// open a new color chooser dialog
val colors = intArrayOf(Color.parseColor("#3F51B5"), Color.RED, Color.GREEN, Color.BLUE)
MaterialDialog(context!!)
.title(R.string.primary_color)
.colorChooser(colors, initialSelection = Color.parseColor("#3F51B5")) { _, color ->
viewPrimaryColor.setBackgroundColor(color)
}
.positiveButton(R.string.select)
.show()
}
}
fun setMainActivity(mainActivity: MainActivity) {
this.mainActivity = mainActivity
}
}

View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
@ -22,23 +22,23 @@
package org.mosad.seil0.projectlaogai.fragments
import android.graphics.Color
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import org.mosad.seil0.projectlaogai.MainActivity
import android.widget.ScrollView
import androidx.fragment.app.Fragment
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.android.synthetic.main.fragment_timetable.*
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.hsoparser.DataTypes
import org.mosad.seil0.projectlaogai.hsoparser.NotRetardedCalendar
import org.mosad.seil0.projectlaogai.uicomponents.LessonCardView
import org.mosad.seil0.projectlaogai.uicomponents.MensaDayCardView
import java.text.SimpleDateFormat
import java.util.*
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController
import org.mosad.seil0.projectlaogai.controller.cache.TimetableController.Companion.timetable
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.uicomponents.dialogs.AddSubjectDialog
import org.mosad.seil0.projectlaogai.uicomponents.DayCardView
import org.mosad.seil0.projectlaogai.uicomponents.TextViewInfo
import org.mosad.seil0.projectlaogai.util.NotRetardedCalendar
/**
* The timetable controller class
@ -46,91 +46,105 @@ import java.util.*
*/
class TimeTableFragment : Fragment() {
private lateinit var linLayoutTTFragment: LinearLayout
private var mainActivity = MainActivity()
private lateinit var scrollViewTimetable: ScrollView
private lateinit var faBtnAddSubject: FloatingActionButton
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_timetable, container, false)
}
val view: View = inflater.inflate(R.layout.fragment_time_table, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
linLayoutTTFragment = view.findViewById(R.id.linLayout_TTFragment)
scrollViewTimetable = view.findViewById(R.id.scrollView_Timetable)
faBtnAddSubject = view.findViewById(R.id.faBtnAddSubject)
refreshLayout_Timetable.setProgressBackgroundColorSchemeColor(Preferences.themeSecondary)
addCurrentWeek()
initActions() // init actions
return view
if (timetable.size > 1 && timetable[0].days.isNotEmpty() && timetable[1].days.isNotEmpty()) {
initTimetable()
} else {
val txtViewInfo = TextViewInfo(context!!).set {
txt = resources.getString(R.string.timetable_generic_error)
}.showImage()
linLayout_Timetable.addView(txtViewInfo)
}
}
/**
* add the remaining days of the current week to the timetable screen
* initialize the actions
*/
private fun addCurrentWeek() {
val dayIndex = NotRetardedCalendar().getDayOfWeekIndex()
val formatter = SimpleDateFormat("E dd.MM", Locale.GERMANY) // TODO change to android call when min api is 24
val calendar = Calendar.getInstance()
private fun initActions() = GlobalScope.launch(Dispatchers.Main) {
doAsync {
refreshLayout_Timetable.setOnRefreshListener {
runBlocking { TimetableController.update(context!!).joinAll() }
reloadTimetableUI()
}
uiThread {
// show the AddLessonDialog if the ftaBtn is clicked
faBtnAddSubject.setOnClickListener {
AddSubjectDialog(context!!)
.positiveButton {
TimetableController.addSubject(selectedCourse, selectedSubject, context)
runBlocking { reloadTimetableUI() }
}.show()
}
// add current weeks days
for(day in dayIndex..5) {
val cardViewTimeTableDay = MensaDayCardView(context!!, null)
cardViewTimeTableDay.setDayHeading(formatter.format(calendar.time))
// for each lessen of the day
for((i, lesson) in mainActivity.getTimeTableCurrentWeek()[day].withIndex()) {
val lessonCardView = LessonCardView(context!!, null)
lessonCardView.setBackgroundColor(Color.TRANSPARENT)
lessonCardView.getTxtViewLesson().text = resources.getString(R.string.string_new_line, lesson.lessonSubject)
lessonCardView.getTxtViewLesson().append(lesson.lessonTeacher + "\n")
lessonCardView.getTxtViewLesson().append(lesson.lessonRoom)
lessonCardView.getTxtViewTime().text = DataTypes().getTime()[i]
if(lessonCardView.getTxtViewLesson().text.length > 2)
cardViewTimeTableDay.getLinLayoutMensaDay().addView(lessonCardView)
}
calendar.add(Calendar.DATE,1)
linLayoutTTFragment.addView(cardViewTimeTableDay)
}
// add next weeks days, max number = dayIndex, if timetable was loaded
if (mainActivity.getTimeTableNextWeek().isNotEmpty()) {
calendar.add(Calendar.DATE,1) // before this we are at a sunday (no lecture on sundays!)
for(day in 0..(dayIndex - 1)) {
val cardViewTimeTableDay = MensaDayCardView(context!!, null)
cardViewTimeTableDay.setDayHeading(formatter.format(calendar.time))
// for each lessen of the day
for((i, lesson) in mainActivity.getTimeTableNextWeek()[day].withIndex()) {
val lessonCardView = LessonCardView(context!!, null)
lessonCardView.setBackgroundColor(Color.TRANSPARENT)
lessonCardView.getTxtViewLesson().text = resources.getString(R.string.string_new_line, lesson.lessonSubject)
lessonCardView.getTxtViewLesson().append(lesson.lessonTeacher + "\n")
lessonCardView.getTxtViewLesson().append(lesson.lessonRoom)
lessonCardView.getTxtViewTime().text = DataTypes().getTime()[i]
if(lessonCardView.getTxtViewLesson().text.length > 2)
cardViewTimeTableDay.getLinLayoutMensaDay().addView(lessonCardView)
}
calendar.add(Calendar.DATE,1)
linLayoutTTFragment.addView(cardViewTimeTableDay)
}
}
// TODO if there is no lesson at one day , show a no lesson card
// hide the btnCardValue if the user is scrolling down
scrollViewTimetable.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
faBtnAddSubject.hide()
} else {
faBtnAddSubject.show()
}
}
}
/**
* add the current and next weeks lessons
*/
private fun initTimetable() = GlobalScope.launch(Dispatchers.Default) {
val currentDayIndex = NotRetardedCalendar.getDayOfWeekIndex()
addTimetableWeek(currentDayIndex, 5, 0).join() // add current week
addTimetableWeek(0, currentDayIndex - 1, 1) // add next week
}
private fun addTimetableWeek(dayBegin: Int, dayEnd: Int, week: Int) = GlobalScope.launch(Dispatchers.Main) {
for (dayIndex in dayBegin..dayEnd) {
val dayCardView = DayCardView(context!!)
// some wired calendar magic, calculate the correct date to be shown
// ((timetable week - current week * 7) + (dayIndex - dayIndex of current week)
val daysToAdd = ((timetable[week].weekNumberYear - NotRetardedCalendar.getWeekOfYear())
* 7 + (dayIndex - NotRetardedCalendar.getDayOfWeekIndex()))
dayCardView.addTimetableDay(timetable[week].days[dayIndex], daysToAdd)
// if there are no lessons don't show the dayCardView
if (dayCardView.getLinLayoutDay().childCount > 1)
linLayout_Timetable.addView(dayCardView)
}
}
fun setMainActivity(mainActivity: MainActivity) {
this.mainActivity = mainActivity
/**
* clear linLayout_Timetable, add the updated timetable
*/
private fun reloadTimetableUI() = GlobalScope.launch(Dispatchers.Default) {
withContext(Dispatchers.Main) {
// remove all lessons from the layout
linLayout_Timetable.removeAllViews()
// add the refreshed timetables
val dayIndex = NotRetardedCalendar.getDayOfWeekIndex()
addTimetableWeek(dayIndex, 5, 0).join() // add current week
addTimetableWeek(0, dayIndex - 1, 1) // add next week
refreshLayout_Timetable.isRefreshing = false
}
}
}

View File

@ -1,63 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.hsoparser
import java.util.*
class DataTypes {
val times = arrayOf("8.00 - 9.30", "9.45 - 11.15" ,"11.35 - 13.05", "14.00 -15.30", "15.45 - 17.15", "17.30 - 19.00")
init {
// do something
}
fun getTime(): Array<String> {
return times
}
}
class NotRetardedCalendar {
private val calendar = Calendar.getInstance()!!
fun getDayOfWeekIndex(): Int {
return when(calendar.get(Calendar.DAY_OF_WEEK)) {
Calendar.MONDAY -> 0
Calendar.TUESDAY -> 1
Calendar.WEDNESDAY -> 2
Calendar.THURSDAY -> 3
Calendar.FRIDAY -> 4
Calendar.SATURDAY -> 5
Calendar.SUNDAY -> 6
else -> 7
}
}
}
data class Lesson(val lessonSubject: String, val lessonTeacher: String, val lessonRoom:String, val lessonRemark: String)
data class CourseTTLink(val courseTTLink: String, val course: String)
data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String)

View File

@ -1,79 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.hsoparser
import org.jsoup.Jsoup
import java.util.*
class MensaParser {
private var mealList = ArrayList<Meal>()
init {
// do something
}
/**
* returns the mensa menu for the whole week
*/
fun getMensaMenu(): ArrayList<Meal> {
val menuHTML = Jsoup.connect("https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/").get()
menuHTML.select("#speiseplan-tabs").select("div.tab-content").select("div.menu-tagesplan").forEachIndexed { _, element ->
val day = element.select("h3").text()
for (i in 0 .. (element.select("div.row h4").size - 1)) {
try {
val heading = element.select("div.row h4")[i].text()
val parts = ArrayList<String>(element.select("div.row").select("div.menu-info")[i].html().substringBefore("<span").replace("<br>", "|").split("|"))
val additives = element.select("div.row").select("div.menu-info")[i].select("span.show-with-allergenes").text()
mealList.add(Meal(day, heading, parts, additives))
} catch (e: Exception) {
// catch
}
}
}
return mealList
}
/**
* return the mensa menu of a given day (Mon - Sat)
*/
fun getMensaMenuDay(mealList: ArrayList<Meal>, day: Int): ArrayList<Meal> {
val dayMenus = ArrayList<Meal>()
val strDay: String = when (day) {
Calendar.MONDAY -> "Mon"
Calendar.TUESDAY -> "Die"
Calendar.WEDNESDAY -> "Mit"
Calendar.THURSDAY -> "Don"
Calendar.FRIDAY -> "Fre"
Calendar.SATURDAY -> "Sam"
else -> "TODAY"
}
for (meal in mealList) {
if (meal.day.contains(strDay))
dayMenus.add(meal)
}
return dayMenus
}
}

View File

@ -1,94 +0,0 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.hsoparser
import org.jsoup.Jsoup
class TimeTableParser {
private val days = arrayOf("Monday", "Tuesday" ,"Wednesday", "Thursday", "Friday", "Saturday")
private var courseTTLinkList = ArrayList<CourseTTLink>()
private var timeTableWeek = arrayOf<Array<Lesson>>()
init {
// create the timetable array
for (i in 0..5) {
var timeTableDay = arrayOf<Lesson>()
for (j in 0..5) {
timeTableDay += Lesson("", "","","")
}
timeTableWeek += timeTableDay
}
}
/**
* get the timetable from the given url
* the timetable is organised per row not per column;
* Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on
*/
fun getTimeTable(courseTTURL: String): Array<Array<Lesson>> {
val scheduleHTML = Jsoup.connect(courseTTURL).get()
//val week = scheduleHTML.select("h1.timetable-caption").text()
//println("$week successful!\n")
scheduleHTML.select("table.timetable").select("td.lastcol").forEachIndexed { index, element ->
timeTableWeek[index % 6][index / 6] = Lesson(element.select("div.lesson-subject").text(), element.select("div.lesson-teacher").text(), element.select("div.lesson-room").text(), element.select("div.lesson-remark").text())
}
return timeTableWeek
}
/**
* parse all courses from the courses site at https://www.hs-offenburg.de/studium/vorlesungsplaene/
*/
fun getCourseTTLinks(): ArrayList<CourseTTLink> {
val courseHTML = Jsoup.connect("https://www.hs-offenburg.de/studium/vorlesungsplaene/").get()
courseHTML.select("ul.index-group").select("li.Class").select("a[href]").forEachIndexed { _, element ->
courseTTLinkList.add(CourseTTLink(element.attr("href"),element.text()))
}
return courseTTLinkList
}
fun printTimeTableWeek (timeTableWeek: Array<Array<Lesson>>) {
for (j in 0..5) print(days[j].padEnd(25 ,' ') + " | ")
println()
for (j in 0..5) print("-".padEnd(26 + (j.toFloat().div(j).toInt()), '-') + "+")
println()
for (i in 0..5) {
for (j in 0..5) print(timeTableWeek[j][i].lessonSubject.padEnd(25 ,' ').substring(0,25) + " | ")
println()
for (j in 0..5) print(timeTableWeek[j][i].lessonTeacher.padEnd(25 ,' ').substring(0,25) + " | ")
println()
for (j in 0..5) print(timeTableWeek[j][i].lessonRoom.padEnd(25 ,' ').substring(0,25) + " | ")
println()
for (j in 0..5) print("-".padEnd(26 + (j.toFloat().div(j).toInt()), '-') + "+")
println()
}
println()
}
}

View File

@ -0,0 +1,59 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.onboarding
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.viewpager.widget.PagerAdapter
import org.mosad.seil0.projectlaogai.OnboardingActivity.Companion.layouts
/**
* a PagerAdapter
*/
class ViewPagerAdapter(cont: Context) : PagerAdapter() {
private val context = cont
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view === `object`
}
override fun getCount(): Int {
return layouts.size
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(context).inflate(layouts[position], container, false)
container.addView(view)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
val view = `object` as View
container.removeView(view)
}
}

View File

@ -0,0 +1,89 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.uicomponents
import android.content.Context
import android.graphics.Color
import android.widget.LinearLayout
import androidx.cardview.widget.CardView
import kotlinx.android.synthetic.main.cardview_day.view.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.util.DataTypes
import org.mosad.seil0.projectlaogai.util.TimetableDay
import java.text.SimpleDateFormat
import java.util.*
class DayCardView(context: Context) : CardView(context) {
private val formatter = SimpleDateFormat("E dd.MM", Locale.getDefault())
init {
inflate(context, R.layout.cardview_day,this)
// workaround to prevent a white border
this.setBackgroundColor(Color.TRANSPARENT)
}
fun getLinLayoutDay() : LinearLayout {
return linLayout_Day
}
fun setDayHeading(heading: String) {
txtView_DayHeading.text = heading
}
/**
* add the lessons of one day to the dayCardView
* @param timetable a timetable containing the day (and it's lessons) to be added
*/
fun addTimetableDay(timetable: TimetableDay, daysToAdd: Int) {
var lastLesson = LessonLinearLayout(context)
// set the heading
val cal = Calendar.getInstance()
cal.add(Calendar.DATE, daysToAdd)
txtView_DayHeading.text = formatter.format(cal.time)
// for every timeslot of that timetable
timetable.timeslots.forEachIndexed { tsIndex, timeslot ->
timeslot.forEach { lesson ->
if (lesson.lessonSubject.isNotEmpty()) {
val lessonLayout = LessonLinearLayout(context)
lessonLayout.setLesson(lesson, DataTypes().times[tsIndex])
linLayout_Day.addView(lessonLayout)
if (lesson != timeslot.last()) {
lessonLayout.disableDivider()
}
lastLesson = lessonLayout
}
}
}
lastLesson.disableDivider() // disable the divider for the last lesson of the day
}
}

View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
@ -23,32 +23,36 @@
package org.mosad.seil0.projectlaogai.uicomponents
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import kotlinx.android.synthetic.main.linearlayout_grade.view.*
import org.mosad.seil0.projectlaogai.R
class MensaDayCardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : androidx.cardview.widget.CardView(context, attrs){
class GradeLinearLayout(context: Context?): LinearLayout(context) {
private var linLayoutMensaDay: LinearLayout
private var txtViewDayHeading: TextView
var subjectName = ""
var grade = ""
var subSubjectName = ""
var subGrade = ""
init {
inflate(context, R.layout.mensaday_cardview,this)
linLayoutMensaDay = findViewById(R.id.linLayout_MensaDay)
txtViewDayHeading = findViewById(R.id.txtView_DayHeading)
// workaround to prevent a white border
//this.setBackgroundColor(Color.TRANSPARENT)
inflate(context, R.layout.linearlayout_grade, this)
}
fun getLinLayoutMensaDay(): LinearLayout {
return linLayoutMensaDay
fun set(func: GradeLinearLayout.() -> Unit): GradeLinearLayout = apply {
func()
txtView_subject.text = subjectName
txtView_grade.text = grade
txtView_sub_subject.text = subSubjectName
txtView_sub_grade.text = subGrade
}
fun setDayHeading(heading: String) {
txtViewDayHeading.text = heading
fun disableDivider() {
divider_grade.visibility = View.GONE
}
fun disableSubSubject() {
linLayout_sub_subject.visibility = View.GONE
}
}

View File

@ -1,7 +1,7 @@
/**
* ProjectLaogai
*
* Copyright 2018 <seil0@mosad.xyz>
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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
@ -23,31 +23,27 @@
package org.mosad.seil0.projectlaogai.uicomponents
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.widget.TextView
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.linearlayout_lesson.view.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.util.Lesson
class LessonCardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : androidx.cardview.widget.CardView(context, attrs){
private var txtViewLesson: TextView
private var txtViewTime: TextView
class LessonLinearLayout(context: Context?) : LinearLayout(context) {
init {
inflate(context, R.layout.lesson_cardview,this)
txtViewLesson = findViewById(R.id.txtView_Lesson)
txtViewTime = findViewById(R.id.txtView_Time)
// workaround to prevent a white border
this.setBackgroundColor(Color.parseColor("#3F51B5"))
inflate(context, R.layout.linearlayout_lesson, this)
}
fun getTxtViewLesson(): TextView {
return txtViewLesson
fun setLesson(lesson: Lesson, time: String) {
txtView_lessonTime.text = time
txtView_lessonSubject.text = lesson.lessonSubject
txtView_lessonTeacher.text = lesson.lessonTeacher
txtView_lessonRoom.text = lesson.lessonRoom
}
fun getTxtViewTime(): TextView {
return txtViewTime
fun disableDivider() {
divider_lesson.visibility = View.GONE
}
}

View File

@ -0,0 +1,52 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.uicomponents
import android.content.Context
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.linearlayout_meal.view.*
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.util.Meal
class MealLinearLayout(context: Context?): LinearLayout(context) {
init {
inflate(context, R.layout.linearlayout_meal, this)
}
fun setMeal(meal: Meal) {
txtView_MealHeading.text = meal.heading
meal.parts.forEachIndexed { partIndex, part ->
txtView_Meal.append(part)
if(partIndex < (meal.parts.size - 1))
txtView_Meal.append("\n")
}
}
fun disableDivider() {
divider_meal.visibility = View.GONE
}
}

View File

@ -1,31 +0,0 @@
package org.mosad.seil0.projectlaogai.uicomponents
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.widget.TextView
import org.mosad.seil0.projectlaogai.R
class MenuCardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : androidx.cardview.widget.CardView(context, attrs){
private var txtViewMenuHeading: TextView
private var txtViewMenu: TextView
init {
inflate(context, R.layout.menu_cardview,this)
txtViewMenuHeading = findViewById(R.id.txtView_MenuHeading)
txtViewMenu = findViewById(R.id.txtView_Menu)
// workaround to prevent a white border
this.setBackgroundColor(Color.TRANSPARENT)
}
fun setMenuHeading(heading: String) {
txtViewMenuHeading.text = heading
}
fun getTxtViewMenu(): TextView {
return txtViewMenu
}
}

View File

@ -0,0 +1,63 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.uicomponents
import android.content.Context
import android.graphics.Rect
import android.view.View
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.res.ResourcesCompat
import org.mosad.seil0.projectlaogai.R
class TextViewInfo(context: Context?): AppCompatTextView(context!!) {
var txt = ""
var txtSize = 15F
var txtAlignment = View.TEXT_ALIGNMENT_CENTER
var drawable = R.drawable.ic_error_outline_black_24dp
var params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
init {
params.setMargins(0,200,0,0)
}
fun set(func: TextViewInfo.() -> Unit): TextViewInfo = apply {
func()
text = txt
layoutParams = params
textSize = txtSize
textAlignment = txtAlignment
}
fun showImage(): TextViewInfo = apply {
val img = ResourcesCompat.getDrawable(resources, drawable, context.theme)?.apply {
bounds = Rect(0, 0, 75, 75)
}
setCompoundDrawables(null, null, null, img)
}
}

View File

@ -0,0 +1,164 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.uicomponents.dialogs
import android.content.Context
import android.os.Build
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.WhichButton
import com.afollestad.materialdialogs.actions.getActionButton
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.bottomsheets.setPeekHeight
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import kotlinx.coroutines.runBlocking
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.TCoRAPIController
import org.mosad.seil0.projectlaogai.util.Course
import java.util.stream.Collectors
/**
* This class creates a new AddLessonDialog.
*/
class AddSubjectDialog(val context: Context) {
private val dialog = MaterialDialog(context, BottomSheet())
private val spinnerCourses: Spinner
private val spinnerSubjects: Spinner
private val subjectsList = ArrayList<String>()
private val courseNamesList = getCourseNames()
var selectedCourse = ""
var selectedSubject = ""
init {
dialog.title(R.string.add_lesson)
.message(R.string.add_lesson_desc)
.customView(R.layout.dialog_add_lesson)
.positiveButton(R.string.add)
.negativeButton(R.string.cancel)
.setPeekHeight(900)
spinnerCourses = dialog.getCustomView().findViewById(R.id.spinner_Courses)
spinnerSubjects = dialog.getCustomView().findViewById(R.id.spinner_Lessons)
// fix not working accent color
dialog.getActionButton(WhichButton.POSITIVE).updateTextColor(Preferences.cColorAccent)
dialog.getActionButton(WhichButton.NEGATIVE).updateTextColor(Preferences.cColorAccent)
initSpinners()
}
fun positiveButton(func: AddSubjectDialog.() -> Unit): AddSubjectDialog = apply {
dialog.positiveButton {
func()
}
}
fun show() {
dialog.show()
}
fun show(func: AddSubjectDialog.() -> Unit): AddSubjectDialog = apply {
func()
this.show()
}
private fun initSpinners() {
setArrayAdapter(spinnerCourses, courseNamesList)
val lessonsAdapter = setArrayAdapter(spinnerSubjects, subjectsList)
// don't call onItemSelected() on spinnerCourses.onItemSelectedListener
spinnerCourses.setSelection(0,false)
spinnerCourses.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) {
selectedCourse = parent.getItemAtPosition(pos).toString()
// TODO show loading dialog while loading
val lessonSubjects = runBlocking {
TCoRAPIController.getSubjectListAsync(parent.getItemAtPosition(pos).toString(), 0).await()
}
lessonsAdapter.clear()
lessonsAdapter.addAll(lessonSubjects)
lessonsAdapter.notifyDataSetChanged()
}
override fun onNothingSelected(parent: AdapterView<*>) {
// Another interface callback
}
}
// don't call onItemSelected() on spinnerCourses.onItemSelectedListener
spinnerSubjects.setSelection(0,false)
spinnerSubjects.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) {
selectedSubject = parent.getItemAtPosition(pos).toString()
}
override fun onNothingSelected(parent: AdapterView<*>) {
// Another interface callback
}
}
}
/**
* set a new ArrayAdapter for a spinner with a list
* @param spinner the spinner you wish to set the adapter for
* @param list the list to set the adapter to
*/
private fun setArrayAdapter(spinner: Spinner, list: List<String>): ArrayAdapter<String> {
return ArrayAdapter(context, android.R.layout.simple_spinner_item, list)
.also { adapter ->
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
}
}
/**
* get all course names from coursesList
* @return a list, containing all course names
*/
private fun getCourseNames(): List<String> {
val coursesNameList: List<String>
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
coursesNameList = CacheController.coursesList.stream().map(Course::courseName).collect(
Collectors.toList())
} else {
coursesNameList = ArrayList()
CacheController.coursesList.forEach { course ->
coursesNameList.add(course.courseName)
}
}
return coursesNameList
}
}

View File

@ -0,0 +1,87 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.uicomponents.dialogs
import android.content.Context
import android.widget.EditText
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.WhichButton
import com.afollestad.materialdialogs.actions.getActionButton
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.bottomsheets.setPeekHeight
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
class LoginDialog(val context: Context) {
private val dialog = MaterialDialog(context, BottomSheet())
private val editTextEmail: EditText
private val editTextPassword: EditText
var email = ""
var password = ""
init {
dialog.title(R.string.login_heading)
.message(R.string.login_desc_on)
.customView(R.layout.dialog_login)
.positiveButton(R.string.save)
.negativeButton(R.string.cancel)
.setPeekHeight(900)
editTextEmail = dialog.getCustomView().findViewById(R.id.editText_email)
editTextPassword = dialog.getCustomView().findViewById(R.id.editText_password)
// fix not working accent color
dialog.getActionButton(WhichButton.POSITIVE).updateTextColor(Preferences.cColorAccent)
dialog.getActionButton(WhichButton.NEGATIVE).updateTextColor(Preferences.cColorAccent)
}
fun positiveButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
dialog.positiveButton {
email = editTextEmail.text.toString()
password = editTextPassword.text.toString()
func()
}
}
fun negativeButton(func: LoginDialog.() -> Unit): LoginDialog = apply {
dialog.negativeButton {
func()
}
}
fun show(func: LoginDialog.() -> Unit): LoginDialog = apply {
func()
editTextEmail.setText(email)
editTextPassword.setText(password)
dialog.show()
}
}

View File

@ -0,0 +1,119 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.util
import android.graphics.Color
import kotlin.collections.ArrayList
class DataTypes {
val times = arrayOf("8.00 - 9.30", "9.45 - 11.15", "11.35 - 13.05", "14.00 -15.30", "15.45 - 17.15", "17.30 - 19.00")
val primaryColors = intArrayOf(
Color.parseColor("#E53935"),
Color.parseColor("#E91E63"),
Color.parseColor("#9C27B0"),
Color.parseColor("#673AB7"),
Color.parseColor("#3F51B5"),
Color.parseColor("#2196F3"),
Color.parseColor("#03A9F4"),
Color.parseColor("#00BCD4"),
Color.parseColor("#009688"),
Color.parseColor("#4CAF50"),
Color.parseColor("#8BC34A"),
Color.parseColor("#CDDC39"),
Color.parseColor("#FDD835"),
Color.parseColor("#FFB300"),
Color.parseColor("#FB8C00"),
Color.parseColor("#FF5722"),
Color.parseColor("#795548"),
Color.parseColor("#9E9E9E"),
Color.parseColor("#607B8B"),
Color.parseColor("#000000")
)
val accentColors = intArrayOf(
Color.parseColor("#FF1744"),
Color.parseColor("#F50057"),
Color.parseColor("#D500F9"),
Color.parseColor("#3F51B5"),
Color.parseColor("#3D5AFE"),
Color.parseColor("#2979FF"),
Color.parseColor("#0096FF"),
Color.parseColor("#00E5FF"),
Color.parseColor("#1DE9B6"),
Color.parseColor("#00E676"),
Color.parseColor("#C6FF00"),
Color.parseColor("#FFD600"),
Color.parseColor("#FFC400"),
Color.parseColor("#FF9100"),
Color.parseColor("#FF3D00"),
Color.parseColor("#000000")
)
enum class Theme(var string: String) {
Light("Light"),
Dark("Dark"),
Black("Black")
}
}
// data classes for the course part
data class Course(val courseLink: String, val courseName: String)
data class CoursesMeta(val updateTime: Long = 0, val totalCourses: Int = 0)
data class CoursesList(val meta: CoursesMeta = CoursesMeta(), val courses: ArrayList<Course> = ArrayList())
// data classes for the Mensa part
data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String)
data class Meals(val meals: ArrayList<Meal>)
data class MensaWeek(val days: Array<Meals> = Array(7) { Meals(ArrayList()) })
data class MensaMeta(val updateTime: Long = 0, val mensaName: String = "")
data class MensaMenu(val meta: MensaMeta = MensaMeta(), val currentWeek: MensaWeek = MensaWeek(), val nextWeek: MensaWeek = MensaWeek())
// data classes for the timetable part
data class Lesson(
val lessonID: String,
val lessonSubject: String,
val lessonTeacher: String,
val lessonRoom: String,
val lessonRemark: String
)
data class TimetableDay(val timeslots: Array<ArrayList<Lesson>> = Array(6) { ArrayList() })
data class TimetableWeek(val weekIndex: Int = 0, val weekNumberYear: Int = 0, val days: Array<TimetableDay> = Array(6) { TimetableDay() })
// data classes for the qispos part
data class GradeSubject(val id: String = "", val name: String = "", val semester: String = "", val grade: String = "", val credits: String = "")
// TCoR
data class TimetableWeekTCoR(val days: Array<TimetableDay> = Array(6) { TimetableDay() })
data class TimetableCourseMeta(val updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, val weekNumberYear: Int = 0, val link: String = "")
data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeekTCoR = TimetableWeekTCoR())

View File

@ -0,0 +1,64 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.util
import java.util.*
class NotRetardedCalendar {
companion object {
private val calendar = Calendar.getInstance()
fun getDayOfWeekIndex(): Int {
return when (calendar.get(Calendar.DAY_OF_WEEK)) {
Calendar.MONDAY -> 0
Calendar.TUESDAY -> 1
Calendar.WEDNESDAY -> 2
Calendar.THURSDAY -> 3
Calendar.FRIDAY -> 4
Calendar.SATURDAY -> 5
Calendar.SUNDAY -> 6
else -> 7
}
}
fun getTomorrowWeekIndex(): Int {
return when (calendar.get(Calendar.DAY_OF_WEEK)) {
Calendar.MONDAY -> 1
Calendar.TUESDAY -> 2
Calendar.WEDNESDAY -> 3
Calendar.THURSDAY -> 4
Calendar.FRIDAY -> 5
Calendar.SATURDAY -> 6
Calendar.SUNDAY -> 0
else -> 7
}
}
fun getWeekOfYear(): Int {
return when (calendar.get(Calendar.DAY_OF_WEEK)) {
Calendar.SUNDAY -> Calendar.getInstance().get(Calendar.WEEK_OF_YEAR) - 1
else -> Calendar.getInstance().get(Calendar.WEEK_OF_YEAR)
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0"/>
<item
android:color="#00000000"
android:offset="1.0"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1"/>
</vector>

View File

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@color/colorPrimary"/>
<item android:drawable="@android:color/black"/>
<item>
<item android:gravity="center" android:width="144dp" android:height="144dp">
<bitmap
android:gravity="center"
android:src="@drawable/ic_launcher"/>
android:gravity="fill_horizontal|fill_vertical"
android:src="@drawable/ic_splash_logo"/>
</item>
</layer-list>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,7h16v2H4V7zM4,13h16v-2H4V13zM4,17h7v-2H4V17zM4,21h7v-2H4V21zM15.41,18.17L14,16.75l-1.41,1.41L15.41,21L20,16.42L18.58,15L15.41,18.17zM4,3v2h16V3H4z"
android:fillColor="#000000"/>
</vector>

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M8.1,13.34l2.83,-2.83L3.91,3.5c-1.56,1.56 -1.56,4.09 0,5.66l4.19,4.18zM14.88,11.53c1.53,0.71 3.68,0.21 5.27,-1.38 1.91,-1.91 2.28,-4.65 0.81,-6.12 -1.46,-1.46 -4.2,-1.1 -6.12,0.81 -1.59,1.59 -2.09,3.74 -1.38,5.27L3.7,19.87l1.41,1.41L12,14.41l6.88,6.88 1.41,-1.41L13.41,13l1.47,-1.47z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z"/>
</vector>

View File

@ -5,5 +5,5 @@
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,3L4,3v10c0,2.21 1.79,4 4,4h6c2.21,0 4,-1.79 4,-4v-3h2c1.11,0 2,-0.9 2,-2L22,5c0,-1.11 -0.89,-2 -2,-2zM20,8h-2L18,5h2v3zM4,19h16v2L4,21z"/>
android:pathData="M5,13.18v4L12,21l7,-3.82v-4L12,17l-7,-3.82zM12,3L1,9l11,6 9,-4.91V17h2V9L12,3z"/>
</vector>

View File

@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
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"/>
android:fillColor="#FF000000"
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" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -1,9 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="@color/colorPrimary"
android:endColor="@color/colorPrimaryDark"
android:startColor="@color/colorPrimary"
android:type="linear"/>
</shape>

View File

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.HomeFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:layout_width="0dp"
android:layout_height="125dp"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:clickable="false"
android:maxHeight="125dp"
app:cardCornerRadius="15dp"
app:cardUseCompatPadding="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="@string/meal_1"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_Menu1Heading"
android:textStyle="bold" android:textAlignment="center" android:textSize="16sp"
android:typeface="sans" android:fontFamily="sans-serif" android:paddingBottom="5dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
android:typeface="sans" android:id="@+id/txtViewMenu1"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/cardView2"
android:layout_width="0dp"
android:layout_height="125dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
app:cardCornerRadius="15dp"
app:cardUseCompatPadding="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cardView">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="@string/meal_2"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_Menu2Heading"
android:textAlignment="center" android:textStyle="bold" android:textSize="16sp"
android:typeface="sans" android:fontFamily="sans-serif" android:paddingBottom="5dp"/>
<TextView
android:id="@+id/txtViewMenu2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
android:typeface="sans"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cardView2" android:id="@+id/scrollViewTimeTable"
android:background="@color/colorPrimary" android:paddingTop="6dp">
<LinearLayout
android:id="@+id/linLayoutTimeTable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cardView2">
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.MensaFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/linLayout_MensaFragment"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -1,147 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.SettingsFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="9dp" android:id="@+id/cardView_Info" app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="@string/info"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_Info"
android:typeface="sans"
android:textSize="18sp" android:textStyle="bold" android:fontFamily="sans-serif"
android:layout_margin="7dp" android:textColor="@color/colorPrimary"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:padding="7dp" android:id="@+id/linLayout_User">
<TextView
android:text="@string/sample_user"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_User"
android:textSize="16sp" android:textStyle="bold"
android:textColor="@android:color/primary_text_light"/>
<TextView
android:text="@string/user"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_UserDesc"/>
</LinearLayout>
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_margin="7dp"
android:id="@+id/linLayout_Course">
<TextView
android:text="@string/sample_course"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_Course"
android:textSize="16sp" android:textColor="@android:color/primary_text_light"
android:textStyle="bold"/>
<TextView
android:text="@string/course_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_CourseDesc"/>
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_margin="7dp"
android:id="@+id/linLayout_Info">
<TextView
android:text="@string/about_txtView"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_About"
android:textStyle="bold" android:textSize="16sp"
android:textColor="@android:color/primary_text_light"/>
<TextView
android:text="@string/version"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_Version"/>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/cardView_Settings" app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/linLayout_Settings">
<TextView
android:text="@string/settings"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_Settings"
android:typeface="sans"
android:textSize="18sp" android:textColor="@color/design_default_color_primary"
android:textStyle="bold" android:layout_margin="7dp" android:paddingTop="3dp"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_margin="7dp"
android:gravity="center_vertical|end"
android:clickable="true" android:id="@+id/linLayout_MainColor" android:focusable="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:text="@string/primary_color"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_PrimaryColor"
android:textSize="16sp" android:textStyle="bold"
android:textColor="@android:color/primary_text_light"/>
<TextView
android:text="@string/main_color_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_PrimaryColorDesc"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_margin="7dp"
android:gravity="end">
<View
android:layout_width="40dp"
android:layout_height="40dp" android:id="@+id/view_PrimaryColor"
android:background="@color/colorPrimary"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@android:color/background_light"
app:contentPadding="5dp" app:cardElevation="5dp"
app:cardUseCompatPadding="true" app:cardPreventCornerOverlap="false" app:cardCornerRadius="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/txtView_Lesson"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/a_lesson"
android:textColor="@android:color/primary_text_light"
android:textSize="14sp"
android:textStyle="bold"
android:typeface="sans"/>
<TextView
android:id="@+id/txtView_Time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="@string/a_time"/>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="9dp"
app:cardElevation="5dp"
app:cardBackgroundColor="@color/colorMensaDay"
app:cardUseCompatPadding="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/linLayout_MensaDay">
<TextView
android:text="@string/sample_date"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_DayHeading" android:textAlignment="center"
android:textSize="20sp" android:textStyle="bold" android:typeface="sans" android:paddingBottom="5dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:clickable="false"
android:maxHeight="125dp"
app:cardCornerRadius="15dp"
app:cardUseCompatPadding="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="@string/meal_1"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_MenuHeading"
android:textStyle="bold" android:textAlignment="center" android:textSize="16sp"
android:typeface="sans" android:fontFamily="sans-serif" android:paddingBottom="5dp"/>
<TextView
android:id="@+id/txtView_Menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
android:typeface="sans"/>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -20,7 +20,9 @@
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
android:background="?themeSecondary"
app:headerLayout="@layout/nav_header_main"
app:itemTextColor="?colorAccent"
app:menu="@menu/activity_main_drawer"/>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</androidx.viewpager.widget.ViewPager>
<LinearLayout
android:id="@+id/linLayout_Dots"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:gravity="center"
android:orientation="horizontal" />
<View
android:id="@+id/view"
android:layout_width="wrap_content"
android:layout_height="1dp"
android:layout_above="@id/linLayout_Dots"
android:alpha="0.5"
android:background="@android:color/white" />
<Button
android:id="@+id/btn_Next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:background="@null"
android:onClick="btnNextClick"
android:text="@string/next"
android:visibility="gone" />
<Button
android:id="@+id/btn_Skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:background="@null"
android:onClick="btnSkipClick"
android:text="@string/skip"
tools:visibility="gone" />
</RelativeLayout>

View File

@ -20,7 +20,7 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
android:background="?colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
app:cardElevation="5dp"
app:cardBackgroundColor="?themeSecondary"
>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" android:id="@+id/linLayout_Day">
<TextView
android:text="@string/sample_date"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/txtView_DayHeading" android:textSize="18sp"
android:textAlignment="center" android:textStyle="bold" android:textColor="?colorAccent"/>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linLayout_login"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<EditText
android:id="@+id/editText_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:ems="10"
android:hint="@string/email"
android:importantForAutofill="no"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/editText_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:ems="10"
android:hint="@string/password"
android:importantForAutofill="no"
android:inputType="textPassword" />
</LinearLayout>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linLayout_grade"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="7dp"
android:paddingTop="2dp"
android:paddingRight="7dp"
android:paddingBottom="3dp">
<LinearLayout
android:id="@+id/linLayout_subject"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/txtView_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sample_subject"
android:textSize="15sp" />
<TextView
android:id="@+id/txtView_grade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:text="@string/sample_grade"
android:textAlignment="textEnd"
android:textSize="15sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linLayout_sub_subject"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingStart="7dp"
android:paddingTop="3dp"
android:paddingEnd="0dp">
<TextView
android:id="@+id/txtView_sub_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sample_sub_subject" />
<TextView
android:id="@+id/txtView_sub_grade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:text="@string/sample_grade_state"
android:textAlignment="textEnd" />
</LinearLayout>
<View
android:id="@+id/divider_grade"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="3dp"
android:background="?dividerColor" />
</LinearLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linLayout_lesson"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="7dp"
android:paddingTop="2dp"
android:paddingRight="7dp"
android:paddingBottom="3dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/txtView_lessonSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="15sp" />
<TextView
android:id="@+id/txtView_lessonTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:text="@string/a_time"
android:textColor="@color/textSecondaryLight" />
</LinearLayout>
<TextView
android:id="@+id/txtView_lessonTeacher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp" />
<TextView
android:id="@+id/txtView_lessonRoom"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:id="@+id/divider_lesson"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="2dp"
android:background="?dividerColor" />
</LinearLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linLayout_Meal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="7dp"
android:paddingTop="2dp"
android:paddingRight="7dp"
android:paddingBottom="2dp">
<TextView
android:id="@+id/txtView_MealHeading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:text="@string/meal"
android:textAlignment="center"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_Meal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textSize="15sp" />
<View
android:id="@+id/divider_meal"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="3dp"
android:background="?dividerColor" />
</LinearLayout>

View File

@ -1,37 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_header_main"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
android:background="@android:color/black"
android:gravity="bottom"
android:orientation="vertical"
android:gravity="bottom">
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/app_name"
android:paddingTop="@dimen/nav_header_vertical_spacing"
app:srcCompat="@mipmap/ic_launcher_round"
android:contentDescription="@string/nav_header_desc"
android:id="@+id/imageView"/>
app:srcCompat="@mipmap/ic_laogai_icon" />
<TextView
android:id="@+id/txtView_nav_header_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="@string/nav_header_title"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_header_subtitle"
android:id="@+id/textView"/>
android:textColor="#ffffff" />
</LinearLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="license_dialog_style_light" translatable="false">
body {
background-color: #ffffff;
color: #000000;
font-family: sans-serif;
overflow-wrap: break-word;
}
pre {
background-color: #eeeeee;
padding: 1em;
white-space: pre-wrap;
}
</string>
<string name="license_dialog_style_dark" translatable="false">
body {
background-color: #303030;
color: #ffffff;
font-family: sans-serif;
overflow-wrap: break-word;
}
pre {
background-color: #424242;
padding: 1em;
white-space: pre-wrap;
}
li a {
color: #21a3df;
}
</string>
</resources>

View File

@ -0,0 +1,43 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="mensa"
android:enabled="true"
android:icon="@drawable/ic_local_dining_black_24dp"
android:shortcutShortLabel="@string/shortcut_mensa_short"
android:shortcutLongLabel="@string/shortcut_mensa_long"
android:shortcutDisabledMessage="@string/shortcut_mensa_disabled">
<intent
android:action="org.mosad.seil0.projectlaogai.fragments.MensaFragment"
android:targetPackage="org.mosad.seil0.projectlaogai"
android:targetClass="org.mosad.seil0.projectlaogai.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="timetable"
android:enabled="true"
android:icon="@drawable/ic_baseline_calendar_today_24dp"
android:shortcutShortLabel="@string/shortcut_timetable_short"
android:shortcutLongLabel="@string/shortcut_timetable_long"
android:shortcutDisabledMessage="@string/shortcut_timetable_disabled">
<intent
android:action="org.mosad.seil0.projectlaogai.fragments.TimeTableFragment"
android:targetPackage="org.mosad.seil0.projectlaogai"
android:targetClass="org.mosad.seil0.projectlaogai.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="moodle"
android:enabled="true"
android:icon="@drawable/ic_school_black_24dp"
android:shortcutShortLabel="@string/shortcut_moodle_short"
android:shortcutLongLabel="@string/shortcut_moodle_long"
android:shortcutDisabledMessage="@string/shortcut_moodle_disabled">
<intent
android:action="org.mosad.seil0.projectlaogai.fragments.MoodleFragment"
android:targetPackage="org.mosad.seil0.projectlaogai"
android:targetClass="org.mosad.seil0.projectlaogai.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linLayout_addlesson"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<TextView
android:id="@+id/txtView_Course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="7dp"
android:text="@string/courses"
android:textSize="16sp" />
<Spinner
android:id="@+id/spinner_Courses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/courses"
android:paddingBottom="10dp"
android:popupBackground="?themeSecondary"
android:spinnerMode="dropdown" />
<TextView
android:id="@+id/txtView_Lesson"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="7dp"
android:text="@string/lessons"
android:textSize="16sp" />
<Spinner
android:id="@+id/spinner_Lessons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/lessons"
android:paddingBottom="10dp"
android:popupBackground="?themeSecondary" />
</LinearLayout>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linLayout_mensacredit"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_Heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mensa_credit"
android:textAlignment="center"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_current"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:layout_marginBottom="7dp"
android:text="@string/mensa_current"
android:textAlignment="center"
android:textSize="20sp" />
<TextView
android:id="@+id/txtView_last"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mensa_last"
android:textAlignment="center"
android:textSize="16sp" />
</LinearLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.GradesFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout_Grades"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?themePrimary">
<ScrollView
android:id="@+id/scrollView_Grades"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linLayout_Grades"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_Loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="100dp"
android:padding="7dp"
android:text="@string/loading_from_hs"
android:textAlignment="center"
android:textSize="15sp" />
</LinearLayout>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>

View File

@ -3,7 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.TimeTableFragment">
tools:context=".fragments.HomeFragment"
android:background="?themePrimary">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -15,9 +16,11 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/linLayout_TTFragment">
android:layout_height="wrap_content"
android:id="@+id/linLayout_Home">
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.MensaFragment"
android:background="?themePrimary">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout_Mensa"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scrollView_Mensa"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linLayout_Mensa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="false"
android:orientation="vertical" />
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.MoodleFragment">
<!-- TODO: Update blank fragment layout -->
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/webView"/>
</FrameLayout>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imgCourse"
android:layout_width="256dp"
android:layout_height="256dp"
android:contentDescription="@string/timetable"
android:scaleType="fitCenter"
app:layout_constraintBottom_toTopOf="@+id/linearLayout2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_baseline_calendar_today_24dp" />
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="60dp"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/txtView_Course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/course_heading"
android:textAlignment="center"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_CourseDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:text="@string/course_desc_on"
android:textAlignment="center"
android:textSize="18sp" />
<Button
android:id="@+id/btnSelectCourse"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:onClick="btnSelectCourseClick"
android:text="@string/select_course"
android:textColor="#FFFFFFFF"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imgGrades"
android:layout_width="256dp"
android:layout_height="256dp"
android:layout_marginBottom="8dp"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_school_black_24dp" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="60dp"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/txtView_Grades"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_heading"
android:textAlignment="center"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_GradesDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:text="@string/login_desc_on"
android:textAlignment="center"
android:textSize="18sp" />
<EditText
android:id="@+id/editText_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:ems="10"
android:hint="@string/email"
android:importantForAutofill="no"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/editText_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:ems="10"
android:hint="@string/password"
android:importantForAutofill="no"
android:inputType="textPassword" />
<Button
android:id="@+id/btnLogin"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:onClick="btnLoginClick"
android:text="@string/login"
android:textColor="#FFFFFFFF"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/logo"
android:layout_width="256dp"
android:layout_height="256dp"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
app:layout_constraintBottom_toTopOf="@+id/linearLayout3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@mipmap/ic_laogai_icon_foreground" />
<LinearLayout
android:id="@+id/linearLayout3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="60dp"
android:orientation="vertical"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/txtAppName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAlignment="center"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtWelcome"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:text="@string/welcome"
android:textAlignment="center"
android:textSize="18sp" />
<Button
android:id="@+id/btnGetStarted"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:onClick="btnNextClick"
android:text="@string/get_started"
android:textColor="#FFFFFFFF"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,348 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?themePrimary"
tools:context=".fragments.SettingsFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/cardView_Info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="9dp"
app:cardBackgroundColor="?themeSecondary"
app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_Info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:fontFamily="sans-serif"
android:text="@string/info"
android:textColor="?colorAccent"
android:textSize="18sp"
android:textStyle="bold"
android:typeface="sans" />
<LinearLayout
android:id="@+id/linLayout_User"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="7dp">
<TextView
android:id="@+id/txtView_User"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sample_user"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_UserDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/user_desc" />
</LinearLayout>
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?dividerColor" />
<LinearLayout
android:id="@+id/linLayout_Course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_Course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sample_course"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_CourseDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/course_desc" />
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?dividerColor" />
<LinearLayout
android:id="@+id/linLayout_ManageLessons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_ManageLessons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/manage_lessons"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_ManageLessonsDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/manage_lessons_desc" />
</LinearLayout>
<View
android:id="@+id/divider7"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?dividerColor" />
<LinearLayout
android:id="@+id/linLayout_About"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="7dp"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_About"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/about_txtView"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_AboutDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/about_version" />
</LinearLayout>
<View
android:id="@+id/divider6"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?dividerColor" />
<LinearLayout
android:id="@+id/linLayout_Licence"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="7dp"
android:orientation="vertical">
<TextView
android:id="@+id/textView3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/licenses"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/cardView_Settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?themeSecondary"
app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:id="@+id/linLayout_Settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_Settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:paddingTop="3dp"
android:text="@string/settings"
android:textColor="?colorAccent"
android:textSize="18sp"
android:textStyle="bold"
android:typeface="sans" />
<LinearLayout
android:id="@+id/linLayout_Theme"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="7dp"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_Theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/theme"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_SelectedTheme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/themeLight" />
</LinearLayout>
<View
android:id="@+id/divider3"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?dividerColor" />
<LinearLayout
android:id="@+id/linLayout_PrimaryColor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical|end"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_PrimaryColor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/primary_color"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_PrimaryColorDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/primary_color_desc" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:gravity="end"
android:orientation="vertical">
<View
android:id="@+id/view_PrimaryColor"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?colorPrimary" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/divider4"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?dividerColor" />
<LinearLayout
android:id="@+id/linLayout_AccentColor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical|end"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_AccentColor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/accent_color"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_AccentColorDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/accent_color_desc" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:gravity="end"
android:orientation="vertical">
<View
android:id="@+id/view_AccentColor"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?colorAccent" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/divider5"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_buffet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:text="@string/show_buffet"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.TimeTableFragment"
android:background="?themePrimary">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/refreshLayout_Timetable">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" android:id="@+id/scrollView_Timetable">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="false"
android:id="@+id/linLayout_Timetable"/>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/faBtnAddSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center|end"
android:layout_marginEnd="11dp"
android:layout_marginBottom="11dp"
android:clickable="true"
android:elevation="7dp"
android:focusable="true"
android:src="@drawable/ic_add_black_24dp"
android:visibility="visible"
app:backgroundTint="?colorAccent"
app:fabSize="auto" />
</FrameLayout>

View File

@ -4,10 +4,13 @@
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item android:id="@+id/nav_home" android:title="@string/home" android:icon="@drawable/ic_baseline_home_24dp"/>
<item
android:id="@+id/nav_home"
android:title="@string/home"
android:icon="@drawable/ic_baseline_home_24dp"/>
<item
android:id="@+id/nav_mensa"
android:icon="@drawable/ic_free_breakfast_black_24dp"
android:icon="@drawable/ic_local_dining_black_24dp"
android:title="@string/mensa"/>
<item
android:id="@+id/nav_timetable"
@ -15,12 +18,12 @@
android:title="@string/timetable"/>
<item
android:id="@+id/nav_moodle"
android:icon="@drawable/ic_menu_slideshow"
android:icon="@drawable/ic_school_black_24dp"
android:title="@string/moodle"/>
<item
android:id="@+id/nav_email"
android:icon="@android:drawable/ic_dialog_email"
android:title="@string/e_mail"/>
android:id="@+id/nav_grades"
android:icon="@drawable/ic_grading_black_24dp"
android:title="@string/grades" />
<item
android:id="@+id/nav_settings"
android:icon="@drawable/ic_settings_black_24dp"

View File

@ -4,5 +4,5 @@
<item android:id="@+id/action_settings"
android:title="@string/settings"
android:orderInCategory="100"
app:showAsAction="never"/>
app:showAsAction="never" android:visible="false"/>
</menu>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_laogai_icon_background"/>
<foreground android:drawable="@mipmap/ic_laogai_icon_foreground"/>
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Some files were not shown because too many files have changed in this diff Show More