Compare commits

..

204 Commits

Author SHA1 Message Date
d123c773a0 Upload documentation artifact 2023-07-24 14:20:22 +00:00
e225fb0c43 Merge branch '136-final-documentation-adjustments' into 'master'
Resolve "Final documentation adjustments"

Closes #136

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!121
2020-06-29 22:43:49 +00:00
3b2959aff2 Fix some spelling mistakes 2020-06-30 00:31:12 +02:00
c35ebe2735 Add release apk 2020-06-29 14:41:48 +02:00
wiecktobi
8156081e04 Finish corrections 2020-06-29 12:48:07 +02:00
wiecktobi
d7492ad5a1 One change 2020-06-29 01:58:45 +02:00
wiecktobi
564e4feb86 First corrections 2020-06-28 17:31:31 +02:00
bd9a23a0b0 Merge branch '136-final-documentation-adjustments' into 'master'
Resolve "Final documentation adjustments"

Closes #136

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!120
2020-06-20 15:51:15 +00:00
50f8e36800 Update README, Add Clockify Report, Move artifacts to better location 2020-06-20 17:01:59 +02:00
6a00a258a2 Add Projectjournal 2020-06-20 16:54:58 +02:00
0d6df49493 Fix spelling for documentation, backend and frontend 2020-06-20 16:38:25 +02:00
4188c15b4b Final formatting changes
Reduce toc depth from two to one
Remove abstract
2020-06-20 16:08:33 +02:00
ee874ac51e Merge branch '116-fazit-und-ausblick-chapter' into 'master'
Resolve "Fazit und Ausblick Chapter"

Closes #116

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!113
2020-06-20 12:17:37 +00:00
9c145c0d9c Merge branch '112-frontend-chapter' into 'master'
Resolve "Frontend Chapter"

Closes #112

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!109
2020-06-20 12:17:35 +00:00
Tim Zieger
76530bc09f Change home picture and some text 2020-06-20 14:04:12 +02:00
4974ae0e10 Change size and indentation of listings, minor fixes 2020-06-20 14:04:12 +02:00
1d9740c9bc Add TODO content, fix section depth of Probleme und Lösungen 2020-06-20 14:04:12 +02:00
Tim Zieger
2152fa61f7 Add Problems 2020-06-20 14:04:11 +02:00
6feed2ec2a Add DummyDaten and Diagramme 2020-06-20 14:04:11 +02:00
Tim Zieger
605dcb7f51 Edit Frontend Documentation umsetzung 2020-06-20 14:04:11 +02:00
e0cab7f5b3 Add Technologiebeschreibung 2020-06-20 14:04:10 +02:00
Tim Zieger
6defc2c353 Add listing js Edit frontend documentation 2020-06-20 14:04:10 +02:00
6f42a97b9e Add Fazit und Ausblick 2020-06-20 14:00:59 +02:00
b79a997bf8 Merge branch '121-prepare-presentation' into 'master'
Resolve "Prepare presentation"

Closes #121

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!119
2020-06-20 11:58:38 +00:00
56bfd038e4 Add Presentation 2020-06-20 13:56:36 +02:00
9382624d3f Init LFS for pptx 2020-06-20 13:56:23 +02:00
8de701c19f Merge branch '135-statisticss-some-entries-missing' into 'master'
Resolve "Statisticss some entries missing"

Closes #135

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!117
2020-06-14 17:22:29 +00:00
1f7d044ccc Fix missing requests 2020-06-14 19:14:59 +02:00
fa82e80f13 Merge branch '134-fix-request-loop-on-statistics-page' into 'master'
Resolve "Fix request loop on statistics page"

Closes #134

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!116
2020-06-14 15:08:01 +00:00
f525316400 Fix endless loop bug 2020-06-14 17:00:02 +02:00
ad833f3831 Merge branch '132-frontend-improvements' into 'master'
Resolve "Frontend improvements"

Closes #132

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!114
2020-06-14 14:28:53 +00:00
d46411c4e4 Merge branch '133-fix-todays-entires-also-show-future-entries' into 'master'
Resolve "Fix todays entires also show future entries"

Closes #133

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!115
2020-06-14 14:26:04 +00:00
fffde13440 Add a bit of everything 2020-06-14 16:20:34 +02:00
7b4887ecac Update documentation to show right lines for record repository 2020-06-14 16:14:27 +02:00
ff1afd7c32 Don't show future entries on /today endpoint 2020-06-14 16:11:13 +02:00
Tobias Wieck
f9ce4d4db8 Merge branch '131-fix-recyclerview-entries' into 'master'
Resolve "Fix RecyclerView entries"

Closes #131

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!112
2020-06-11 18:54:58 +00:00
wiecktobi
d9ee892c76 Fix bug 2020-06-11 20:25:36 +02:00
7efd7ec891 Merge branch '130-to-large-request-in-pie-chart' into 'master'
Resolve "To large request in Pie chart"

Closes #130

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!111
2020-06-11 16:04:40 +00:00
99e5005831 Fix request size 2020-06-11 17:56:44 +02:00
e38a3c4ce0 Merge branch '111-backend-chapter' into 'master'
Resolve "Backend Chapter"

Closes #111

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!108
2020-06-11 15:41:05 +00:00
9255bce492 Correct spelling android chapter 2020-06-11 17:24:26 +02:00
d3a0f3ad64 Correct spelling einleitung chapter 2020-06-11 16:45:15 +02:00
89d64102bd Correct spelling backend chapter 2020-06-11 16:41:17 +02:00
9e4584cf12 Merge branch '129-month-sumary-time-record-count-too-small' into 'master'
Resolve "Month Sumary time record count too small"

Closes #129

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!110
2020-06-11 14:35:57 +00:00
8c7eb8431c Fix request size 2020-06-11 16:26:54 +02:00
dd110e04fb Correct spelling projektplanung chapter 2020-06-11 16:17:22 +02:00
814bd585c2 Add list of listings 2020-06-11 14:35:33 +02:00
cdb87ea4c9 Change ich in android chapter to wir 2020-06-11 14:35:33 +02:00
207f300e66 Fix duplicate label at code listing 2020-06-11 14:35:33 +02:00
95d5025c0a Add samples to the projections subsection 2020-06-11 14:35:32 +02:00
f7c6914f4d Write probleme und loesungen 2020-06-11 14:35:32 +02:00
41b53998de Write endpoints section 2020-06-11 14:35:31 +02:00
88bc4517fe Write Repositories and Projections 2020-06-11 14:35:31 +02:00
38db18a492 Add JSON formatting for listings 2020-06-11 14:35:30 +02:00
cc41db8f16 Add Geotime logo in titlepage 2020-06-11 14:35:30 +02:00
92da1e539e Autoformat backend project 2020-06-11 14:35:29 +02:00
d0cae8af87 Write Entities and JWT 2020-06-11 14:35:29 +02:00
1fce986a2d Write Backend Technologiebeschreibung 2020-06-11 14:35:28 +02:00
Tobias Wieck
f7ad0c43ea Merge branch '108-einleitung-chapter' into 'master'
Resolve "Einleitung Chapter"

Closes #108

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!103
2020-06-11 12:28:02 +00:00
Tobias Wieck
b2b884731a Merge branch '113-android-chapter' into 'master'
Resolve "Android Chapter"

Closes #113

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!104
2020-06-11 12:04:56 +00:00
wiecktobi
722f9cd465 Finish Android chapter 2020-06-11 13:48:12 +02:00
Tim Zieger
8f0f6c2d8f Merge branch '109-projektplanung-chapter' into 'master'
Resolve "Projektplanung Chapter"

Closes #109

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!102
2020-06-11 10:08:43 +00:00
Tim Zieger
40f7f218a2 Edit projektplanung chapter 2020-06-11 11:56:30 +02:00
wiecktobi
de3b086a45 Chapter 6.4 and small changes in the code 2020-06-10 23:07:10 +02:00
wiecktobi
089df551a7 Add Kotlin source code formatting 2020-06-10 16:01:05 +02:00
wiecktobi
dade003d84 Chapter 6.3.4 2020-06-10 15:42:37 +02:00
wiecktobi
5b2d6a7cfe Finish chapter 6.3.3 2020-06-10 15:42:37 +02:00
wiecktobi
1cc688bed0 Chapter 6.1 to 6.3.3 2020-06-10 15:21:59 +02:00
53d5321541 Merge branch '128-fix-timezone-for-database' into 'master'
Resolve "Adjust timezone for database"

Closes #128

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!107
2020-06-10 13:14:11 +00:00
6f996d2784 Add timezone env variable to database Dockerfile 2020-06-10 15:13:34 +02:00
25c9c23b0b Merge branch '114-application-stack-chapter' into 'master'
Resolve "Application Stack Chapter"

Closes #114

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!106
2020-06-09 18:01:53 +00:00
1f5d8deb66 Fix spelling mistakes 2020-06-09 19:51:16 +02:00
5d14d379db Chapter ApplicationStack 2020-06-09 19:44:06 +02:00
1d24f210f1 Merge branch '110-entwicklungsumgebung-chapter' into 'master'
Resolve "Entwicklungsumgebung Chapter"

Closes #110

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!105
2020-06-09 13:58:42 +00:00
6cdd8d9ce1 Correct spelling mistakes and sentence structure 2020-06-09 15:48:14 +02:00
62bac89e98 Chapter Entwicklungsumgebung 2020-06-09 15:14:24 +02:00
2a66c34356 Import listings package
Add listings language Docker and Docker-Compose
2020-06-09 15:13:32 +02:00
wiecktobi
acf9736f92 First version of introduction 2020-06-08 12:00:39 +02:00
8a5904d513 Merge branch '118-donut-diagram-for-time-balance-over-all-accounts' into 'master'
Resolve "Donut diagram for time balance over all accounts"

Closes #118

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!101
2020-06-06 16:46:50 +00:00
edd03c5a7e Add donut diagram for time by type paid for accounts with revenue calc display in tooltip 2020-06-06 18:39:30 +02:00
Tim Zieger
5e13d103d0 Merge branch '124-fill-about-page-with-content' into 'master'
Resolve "Fill about page with content"

Closes #124

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!100
2020-06-05 09:34:03 +00:00
Tim Zieger
d235c8ddc0 Fill about page with contend 2020-06-05 11:25:39 +02:00
Tobias Wieck
23aa33209a Merge branch '101-get-todays-entries' into 'master'
Resolve "Get todays entries"

Closes #101

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!98
2020-06-04 19:10:53 +00:00
wiecktobi
8e73cc6e2c Fix problems and finish app 2020-06-04 18:45:10 +02:00
wiecktobi
c304632d6e Query /today endpoint an display response 2020-06-03 23:49:00 +02:00
Tim Zieger
aab586a4c8 Merge branch '120-frontend-code-cleanup' into 'master'
Resolve "Frontend code cleanup"

Closes #120

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!99
2020-06-03 14:44:54 +00:00
Tim Zieger
6a42650c40 Cleanup frontend code 2020-06-03 16:32:36 +02:00
wiecktobi
08ffe71433 Add recycler view with hardcoded values 2020-06-03 01:22:09 +02:00
wiecktobi
2ed09692f8 Small changes for user with no data 2020-06-02 23:09:15 +02:00
wiecktobi
20ab2931f2 Change button text 2020-06-02 22:47:23 +02:00
wiecktobi
c11d57de2e Update version and fix issue to show only accounts belonging to the user 2020-06-02 22:04:38 +02:00
Tim Zieger
b0759a64b7 Merge branch '126-remove-create-buttons-if-logged-out' into 'master'
Resolve "Remove create buttons if logged out"

Closes #126

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!97
2020-06-02 10:26:23 +00:00
Tim Zieger
a80acb0797 Remove crate Butttons if logged out 2020-06-02 12:16:30 +02:00
Tim Zieger
b379a750dc Merge branch '127-query-all-users' into 'master'
Resolve "Query all users"

Closes #127

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!96
2020-06-02 09:43:43 +00:00
Tim Zieger
8c75cf414f Implement pages for admin view 2020-06-02 11:34:30 +02:00
Tim Zieger
5597afb4b0 Merge branch '123-query-all-pages-when-more-than-20-entries-are-present' into 'master'
Resolve "Query all pages when more than 20 entries are present"

Closes #123

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!95
2020-06-02 09:15:42 +00:00
Tim Zieger
e30aa5798a Remove sort funktion and Add pagin 2020-06-02 11:06:07 +02:00
Tim Zieger
3e772d989e Merge branch '119-change-fontsize-for-edit-views' into 'master'
Resolve "Change fontsize for edit views"

Closes #119

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!94
2020-06-01 10:11:26 +00:00
Tim Zieger
7954bf7c04 Change h tags to p with pt 2020-06-01 11:58:22 +02:00
Tim Zieger
e2c698fd08 Merge branch '122-fix-date-and-time-pickers' into 'master'
Resolve "Fix date and time pickers"

Closes #122

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!93
2020-06-01 09:25:12 +00:00
Tim Zieger
8e54997b87 Fix pickers 2020-06-01 11:15:29 +02:00
Tim Zieger
f2397c6c92 Merge branch '107-sort-timerecords-by-date' into 'master'
Resolve "Sort timerecords by date"

Closes #107

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!92
2020-06-01 08:53:21 +00:00
Tim Zieger
04c9b7b55f Implemented sort for records 2020-06-01 10:44:45 +02:00
daf542c527 Merge branch '117-change-date-calculation-of-the-today-endpoint' into 'master'
Resolve "Change date calculation of the /today endpoint"

Closes #117

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!91
2020-05-31 21:36:03 +00:00
0a0bc34e38 Fix today endpoint to only show entries for today
Add more sample data
2020-05-31 23:29:13 +02:00
207b22afa1 Merge branch '95-fill-diagrams-with-data' into 'master'
Resolve "Fill diagrams with data"

Closes #95

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!82
2020-05-31 19:17:36 +00:00
Tim Zieger
09d206c279 Merge branch '58-fill-home-with-content' into 'master'
Resolve "Fill Home with content"

Closes #58

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!84
2020-05-31 19:16:55 +00:00
Tim Zieger
b07e5cd357 Implemented fill home with contend 2020-05-31 20:55:25 +02:00
918cf67b1f Implement diagrams with backend data 2020-05-31 20:48:51 +02:00
9a8faa3f0d Fix bug in documentation build script 2020-05-30 22:25:26 +02:00
8f2b697f05 Merge branch 'documentation-pipeline' into 'master'
Add documentation build step to CI

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!89
2020-05-30 19:59:25 +00:00
179af69564 Add documentation build step to CI 2020-05-30 21:57:05 +02:00
ca4da8c993 Merge branch 'improve-mr-build-times' into 'master'
Change builds to only build the specific module

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!87
2020-05-30 19:29:17 +00:00
5060da28ca Change builds to only build the specific module 2020-05-30 21:23:24 +02:00
61fccccd33 Merge branch '93-table-of-contents' into 'master'
Resolve "Table of contents"

Closes #93

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!86
2020-05-30 19:15:12 +00:00
0c1550c7a7 Extract large sections into separate files 2020-05-30 20:50:22 +02:00
b602db1bd9 Split project into main sections 2020-05-30 20:39:33 +02:00
cf00b314d3 Remove old structure 2020-05-30 19:49:35 +02:00
bd1795fb7e Add samples for the documentation 2020-05-30 19:44:45 +02:00
f3952c1ea8 Merge branch '106-fix-record-endpoint-allbetweenanduser' into 'master'
Resolve "Fix Record Endpoint allBetweenAndUser"

Closes #106

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!85
2020-05-30 17:05:16 +00:00
17a759ce1e bugfix for allBetweenAndUser Endpoint of records 2020-05-30 18:46:39 +02:00
Tim Zieger
ea4493f955 Merge branch '105-dropdown-edit-create-timetrack' into 'master'
Resolve "Dropdown edit create timetrack"

Closes #105

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!83
2020-05-30 09:11:06 +00:00
Tim Zieger
ea53de257e dropdown and projection for account 2020-05-30 10:54:00 +02:00
Tim Zieger
f3b9a0879b Merge branch '96-date-and-time-picker-for-timetracks' into 'master'
Resolve "date and time picker for timetracks"

Closes #96

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!81
2020-05-30 07:54:30 +00:00
Tim Zieger
3c4cbaf126 pickers 2020-05-29 17:34:23 +02:00
Tim Zieger
f509ad0d9a Merge branch '103-frontend-crashes-if-something-is-null' into 'master'
Resolve "Frontend crashes if something is null"

Closes #103

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!80
2020-05-29 08:23:06 +00:00
Tim Zieger
04abba8c1c bug fixed chrash if null 2020-05-29 10:05:18 +02:00
Tobias Wieck
d1910ea10d Merge branch '100-change-timetrack-account' into 'master'
Resolve "Change timetrack account"

Closes #100

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!76
2020-05-28 19:16:22 +00:00
wiecktobi
8521832315 Query track endpoint and show start/enddate 2020-05-28 20:49:58 +02:00
613c5cf0ac Merge branch '104-update-backend-timezone' into 'master'
Resolve "Update backend timezone"

Closes #104

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!79
2020-05-28 18:13:08 +00:00
161f5c7054 Update timezone of jre docker image 2020-05-28 19:57:05 +02:00
77c6b85eec Merge branch '102-track-endpoint-crashes-if-something-is-null' into 'master'
Resolve "Track endpoint crashes if something is null"

Closes #102

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!78
2020-05-28 17:19:10 +00:00
75d2cb6670 Fix crash when all record fields are null 2020-05-28 19:03:10 +02:00
Tim Zieger
19d01b1502 Merge branch '97-account-link-and-admin' into 'master'
Resolve "Account link and admin"

Closes #97

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!77
2020-05-28 15:41:34 +00:00
Tim Zieger
5ace5c4317 account link and admin + remove role 2020-05-28 17:18:05 +02:00
wiecktobi
c52daee815 Read, save and display account name, description and revenue 2020-05-28 16:19:55 +02:00
wiecktobi
dad0594854 Set geofenc according to users location values 2020-05-28 11:33:11 +02:00
Tobias Wieck
2a1eb3feef Merge branch '98-fix-geofence' into 'master'
Resolve "Fix geofence"

Closes #98

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!75
2020-05-27 22:22:54 +00:00
wiecktobi
1fbed97fb2 Update input fields and buttons plus fix security gap at logout 2020-05-27 22:55:21 +02:00
wiecktobi
73187542cd Bug fix 2020-05-27 20:19:05 +02:00
Tim Zieger
08b996308f Merge branch '94-action-listener-for-butttons' into 'master'
Resolve "Action listener for Butttons"

Closes #94

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!74
2020-05-27 13:53:33 +00:00
Tim Zieger
95aaac09c8 event listener 2020-05-27 15:30:18 +02:00
Tim Zieger
8e2a8208f3 Merge branch '92-time-reccord-data-from-backend' into 'master'
Resolve "Time Reccord data from Backend"

Closes #92

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!73
2020-05-27 12:19:30 +00:00
Tim Zieger
f41a2af656 time record backend and replace selfuri 2020-05-27 13:01:18 +02:00
Tobias Wieck
5cd2defd3d Merge branch '51-logout-implementation' into 'master'
Resolve "Logout implementation"

Closes #51

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!72
2020-05-26 09:19:17 +00:00
wiecktobi
6bd11577e0 Logout functionality and small changes 2020-05-26 10:51:27 +02:00
Tim Zieger
57e8541c0e Merge branch '90-delete-user' into 'master'
Resolve "Delete User"

Closes #90

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!71
2020-05-26 08:33:33 +00:00
Tim Zieger
d5ef0004f6 delete user 2020-05-26 10:17:42 +02:00
Tobias Wieck
5908de5650 Merge branch '66-implement-functional-top-toolbar' into 'master'
Resolve "Implement functional Top Toolbar"

Closes #66

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!34
2020-05-25 22:16:37 +00:00
wiecktobi
6334b62e2b Override old JWT on login 2020-05-25 23:52:31 +02:00
wiecktobi
c124818fa6 Remove JWT data class
* Code formatting
* Static code analysis
2020-05-25 23:29:36 +02:00
wiecktobi
6907710f51 Create private file with JWT 2020-05-25 23:20:23 +02:00
wiecktobi
881d01535d Change app theme
* Toolbar with logout on home screen
* Add a Toolbar to login and register view
* Embed Settings view
2020-05-25 22:55:56 +02:00
50446e0654 Merge branch '91-backend-uses-cascaded-delete-for-entities' into 'master'
Resolve "Backend uses cascaded delete for entities"

Closes #91

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!70
2020-05-25 19:25:42 +00:00
4e560b9d36 Add cascading delete for connected entities 2020-05-25 21:10:25 +02:00
Tim Zieger
0d4eb694c9 Merge branch '89-remove-localhost' into 'master'
Resolve "Remove localhost"

Closes #89

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!69
2020-05-25 17:39:15 +00:00
Tim Zieger
8992dd028b no more localhost 2020-05-25 19:22:09 +02:00
b1320fb11e Merge branch '78-update-timetrack-accounts' into 'master'
Resolve "Update Timetrack Accounts"

Closes #86 and #78

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!65
2020-05-25 16:58:48 +00:00
214d5b9218 add timetrack account managing views and implement functionality 2020-05-25 18:38:34 +02:00
Tobias Wieck
0047556180 Merge branch '80-query-backend-endpoint-whoami' into 'master'
Resolve "Query backend endpoint whoami"

Closes #80

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!67
2020-05-25 12:46:03 +00:00
Tobias Wieck
52e862afbe Resolve "Query backend endpoint whoami" 2020-05-25 12:46:02 +00:00
b20f2e8a61 Merge branch '88-list-all-timetrack-accounts-for-a-given-user' into 'master'
Resolve "List all Timetrack accounts for a given user"

Closes #88

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!68
2020-05-25 12:26:33 +00:00
781ec85851 Implement to query accounts for a given user 2020-05-25 14:11:34 +02:00
Tim Zieger
83bd086792 Merge branch '85-implement-default-actions' into 'master'
Resolve "Implement default actions"

Closes #85

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!66
2020-05-25 10:30:45 +00:00
Tim Zieger
2a68777f67 implement default actions 2020-05-25 12:10:02 +02:00
Tim Zieger
edd7317aa4 Merge branch '79-update-users-properties-from-frontend' into 'master'
Resolve "Update users properties from frontend"

Closes #79

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!63
2020-05-24 10:02:51 +00:00
Tim Zieger
3a04ff8823 edit user 2020-05-24 10:46:03 +02:00
Tobias Wieck
70e3a60930 Merge branch '74-disable-start-button' into 'master'
Resolve "Disable start button"

Closes #74

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!64
2020-05-23 16:39:27 +00:00
Tobias Wieck
841a9545e6 Resolve "Disable start button" 2020-05-23 16:39:27 +00:00
Tim Zieger
4b2fe41d9f Merge branch '83-user-list-add-style' into 'master'
Resolve "User list add style"

Closes #83

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!62
2020-05-22 12:15:27 +00:00
Tim Zieger
736e571471 users same style as timetrack 2020-05-22 13:57:47 +02:00
Tim Zieger
7488b5bb03 Merge branch '77-list-of-users' into 'master'
Resolve "List of users"

Closes #77

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!60
2020-05-21 08:36:55 +00:00
Tim Zieger
030b8c47d3 list of Users 2020-05-21 10:21:32 +02:00
ed284da1ac Merge branch '81-custom-endpoints' into 'master'
Resolve "Custom endpoints"

Closes #81

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!59
2020-05-20 22:02:04 +00:00
5e161d9c9d Remove accounts references from User 2020-05-20 23:45:36 +02:00
5eb6efcffe Implement /track endpoint 2020-05-20 23:26:35 +02:00
9f8fd0af1e Implement custom record searches
Create record overview projection
Add today search with scoped principal
Add sample data for records
2020-05-20 23:26:35 +02:00
3310062138 Merge branch '76-rework-the-design-for-a-single-time-record' into 'master'
Resolve "Rework the design for a single time record"

Closes #76

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!61
2020-05-20 16:03:23 +00:00
a6c4cc48d9 add new TimeRecordItem and TimeRecord list design 2020-05-20 17:47:17 +02:00
Tim Zieger
3d1f2fd741 Merge branch '75-menu-depending-on-loginstate' into 'master'
Resolve "Menu depending on loginstate"

Closes #75

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!56
2020-05-19 13:40:56 +00:00
Tim Zieger
a1072f40b3 menu depending on loginstate 2020-05-19 15:16:09 +02:00
04d4d9563b Merge branch '50-create-record-rest-controller' into 'master'
Resolve "Create record rest controller"

Closes #50

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!58
2020-05-19 09:33:50 +00:00
24b6ff4618 Make UserController a BasePathAwareController 2020-05-19 10:50:33 +02:00
0f700d87b0 Adjust records endpoint name and rels, enable paging 2020-05-19 10:35:14 +02:00
Tobias Wieck
0aa257dbaf Merge branch '82-update-geofence-for-api-29' into 'master'
Resolve "Update geofence for API 29"

Closes #82

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!57
2020-05-18 21:13:15 +00:00
wiecktobi
65fc373f97 Added permission request 2020-05-18 21:52:52 +02:00
Tobias Wieck
10a1d3a3fa Merge branch '73-implement-geofence' into 'master'
Resolve "Implement geofence"

Closes #73

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!55
2020-05-18 15:29:41 +00:00
wiecktobi
5d366e3c8b Implemented Geofence and updated package structure 2020-05-18 17:06:01 +02:00
Tobias Wieck
4c34ac3497 Merge branch '72-bug-new-retrofit-verion' into 'master'
Resolve "Bug: New retrofit verion"

Closes #72

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!54
2020-05-18 09:18:25 +00:00
Tim Zieger
5d98afb5c4 Merge branch '62-register-communication' into 'master'
Resolve "Register Communication"

Closes #62

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!53
2020-05-18 09:17:49 +00:00
wiecktobi
b673c0767f Fixed 2020-05-18 11:00:20 +02:00
Tim Zieger
5085cd45ce register communication 2020-05-18 10:55:02 +02:00
af3d5686d7 Merge branch 'fix-backend-uri' into 'master'
Important Fix: Backend uri

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!52
2020-05-17 19:36:21 +00:00
8a79d863f6 Important Fix: Backend uri 2020-05-17 21:21:40 +02:00
Tim Zieger
fc813f14db Merge branch '61-login-communication' into 'master'
Resolve "Login Communication"

Closes #61

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!50
2020-05-17 19:05:55 +00:00
Tim Zieger
efa7587702 login communication 2020-05-17 20:33:45 +02:00
6e4200cea6 Merge branch '68-statistic-overview-view' into 'master'
Resolve "Statistic Overview view"

Closes #68

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!51
2020-05-17 17:57:50 +00:00
4c81923977 add Statistics Page - move WeekSummary into Statistics 2020-05-17 17:32:50 +02:00
Tobias Wieck
3dd71a11eb Merge branch '67-read-geo-information' into 'master'
Resolve "Read geo-information"

Closes #67

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!42
2020-05-17 14:27:17 +00:00
Tobias Wieck
3c42d55881 Merge branch '64-login-functionality-with-retrofit' into 'master'
Resolve "Login functionality with Retrofit"

Closes #64

See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!41
2020-05-17 14:13:36 +00:00
wiecktobi
9326b88973 Working login function with token storage 2020-05-17 15:58:42 +02:00
wiecktobi
b4d6b5dd0c Changed to read header 2020-05-11 20:59:32 +02:00
wiecktobi
859fd1d5ec Get the actual location and update it 2020-05-11 18:00:33 +02:00
wiecktobi
e6834a18e6 Created Login with Retrofit and store Token (with exceptions) 2020-05-11 14:24:01 +02:00
140 changed files with 15427 additions and 1473 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.pptx filter=lfs diff=lfs merge=lfs -text
*.apk filter=lfs diff=lfs merge=lfs -text

View File

@ -8,6 +8,10 @@ build-vue:
- docker build --pull -t vue .
rules:
- if: $CI_MERGE_REQUEST_ID
changes:
- frontend/**/*
when: always
- when: never
build-backend:
image: docker:latest
@ -19,6 +23,11 @@ build-backend:
- docker build --pull -t backend .
rules:
- if: $CI_MERGE_REQUEST_ID
changes:
- backend/**/*
when: always
- when: never
build-android:
image: docker:latest
@ -30,3 +39,25 @@ build-android:
- docker build --pull -t android .
rules:
- if: $CI_MERGE_REQUEST_ID
changes:
- android/**/*
when: always
- when: never
build-documentation:
image: icaotix/latex:full-incremental
stage: build
script:
- export CI_JOB_TIMESTAMP=$(date --utc -I)
- cd documentation
- latexmk -pdf documentation
- mv "documentation.pdf" "documentation-${CI_JOB_TIMESTAMP}.pdf"
artifacts:
paths:
- "**/documentation*.pdf"
rules:
- if: $CI_MERGE_REQUEST_ID
changes:
- documentation/**/*
when: always
- when: never

View File

@ -1 +1,2 @@
# UBC - Timetracking with Geofences
# UBC SS2020 - Geo Timetracking - Team TacocaT
![Geo Timetracking](other-artifacts/Product-Flyer.png)

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -26,6 +26,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
@ -33,10 +37,12 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.2.0-alpha06'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation "com.google.android.gms:play-services-location:17.0.0"
implementation 'androidx.preference:preference:1.1.1'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
@ -45,4 +51,7 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation "android.arch.navigation:navigation-fragment-ktx:2.2.2"
implementation "android.arch.navigation:navigation-ui-ktx:2.2.2"
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
}

View File

@ -1,30 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.hft.geotracker">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- Required if your app targets Android 10 (API level 29) or higher -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
<activity android:name=".Register"></activity>
<activity android:name=".activities.Register" />
<activity
android:name=".Settings"
android:name=".activities.Settings"
android:label="@string/title_activity_settings" />
<activity android:name=".Login">
<activity android:name=".activities.Login">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity">
<activity android:name=".activities.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<receiver android:name=".GeofenceBroadcastReceiver"/>
</application>
</manifest>

View File

@ -0,0 +1,58 @@
package de.hft.geotracker
import android.content.BroadcastReceiver
import android.content.ContentValues.TAG
import android.content.Context
import android.content.Intent
import android.util.Log
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofenceStatusCodes
import com.google.android.gms.location.GeofencingEvent
class GeofenceBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
println("Event error")
Log.e(TAG, errorMessage)
return
}
// Test that the reported transition was of interest.
when (val geofenceTransition = geofencingEvent.geofenceTransition) {
Geofence.GEOFENCE_TRANSITION_ENTER -> {
context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
?.edit()
?.putBoolean("ENABLED", true)
?.apply()
// Get the geofences that were triggered. A single event can trigger multiple geofences.
val triggeringGeofences = geofencingEvent.triggeringGeofences
// Get the transition details as a String.
val geofenceTransitionDetails = "Transition: $geofenceTransition" +
"\nTriggering Geofences: $triggeringGeofences"
println("Success Transition: ")
Log.i(TAG, geofenceTransitionDetails)
}
Geofence.GEOFENCE_TRANSITION_EXIT -> {
context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
?.edit()
?.putBoolean("ENABLED", false)
?.apply()
val triggeringGeofences = geofencingEvent.triggeringGeofences
val geofenceTransitionDetails =
"Transition: $geofenceTransition\nTriggering Geofences: $triggeringGeofences"
println("Success Transition: ")
Log.i(TAG, geofenceTransitionDetails)
}
else -> {
println("Error Transition: ")
Log.e(TAG, geofenceTransition.toString())
}
}
}
}

View File

@ -1,37 +0,0 @@
package de.hft.geotracker
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
/**
* A simple [Fragment] subclass.
*/
class Login : AppCompatActivity() {
lateinit var login : TextView
lateinit var reg : TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
login = findViewById(R.id.button_create_account)
login.setOnClickListener {
login()
}
reg = findViewById(R.id.button_register)
reg.setOnClickListener {
register()
}
}
private fun register() {
val intent = Intent(this, Register::class.java)
startActivity(intent)
}
private fun login() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}

View File

@ -1,79 +0,0 @@
package de.hft.geotracker
import android.annotation.SuppressLint
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.Window
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Spinner
import androidx.navigation.findNavController
import androidx.databinding.DataBindingUtil
import androidx.navigation.ui.NavigationUI
import com.google.android.material.textfield.TextInputLayout
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(findViewById(R.id.my_toolbar))
setContentView(R.layout.activity_home)
// val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// val navController = this.findNavController(R.id.HostFragment)
// NavigationUI.setupActionBarWithNavController(this, navController)
// val dropdown : TextInputLayout = findViewById(R.id.filled_exposed_dropdown)
/*val editTextFilledExposedDropdown : AutoCompleteTextView = findViewById(R.id.filled_exposed_dropdown)
ArrayAdapter.createFromResource(this, R.array.accounts, R.layout.spinner_layout).also {
arrayAdapter -> arrayAdapter.setDropDownViewResource(R.layout.spinner_layout)
editTextFilledExposedDropdown.setAdapter(arrayAdapter)
}*/
/*val array = arrayOf("Test1", "Test2")
val a : ArrayAdapter<String> = ArrayAdapter(this, R.layout.spinner_layout, array)
val editTextFilledExposedDropdown : AutoCompleteTextView = findViewById(R.id.filled_exposed_dropdown)
editTextFilledExposedDropdown.setAdapter(a)*/
val spinner: Spinner = findViewById(R.id.account_spinner)
// Create an ArrayAdapter using the string array and a default spinner layout
ArrayAdapter.createFromResource(this, R.array.accounts, android.R.layout.simple_spinner_item).also { adapter ->
// Specify the layout to use when the list of choices appears
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
// Apply the adapter to the spinner
spinner.adapter = adapter
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.settings -> {
// User chose the "Settings" item, show the app settings UI...
var intent = Intent(this, Settings::class.java)
startActivity(intent)
println("test")
true
}
R.id.logout -> {
// User chose the "Settings" item, show the app settings UI...
var intent = Intent(this, Login::class.java)
startActivity(intent)
true
}
else -> {
// If we got here, the user's action was not recognized.
// Invoke the superclass to handle it.
println("test")
super.onOptionsItemSelected(item)
}
}
override fun onBackPressed() {
}
}

View File

@ -0,0 +1,44 @@
package de.hft.geotracker
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import de.hft.geotracker.activities.RecordEntry
class RecordsAdapter : RecyclerView.Adapter<TextItemViewHolder>() {
var data = listOf<RecordEntry>()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater
.inflate(R.layout.text_item_view, parent, false) as CardView
return TextItemViewHolder(view)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
val item = data[position]
holder.textFrom.setText("Start: " + item.from)
holder.textTo.setText("End: " + item.to)
if (item.duration != -1) {
holder.textTotal.setText("Duration: " + item.duration)
}
}
}
class TextItemViewHolder(textView: CardView): RecyclerView.ViewHolder(textView) {
val textFrom = itemView.findViewById<TextView>(R.id.recyclerText_from)
val textTo = itemView.findViewById<TextView>(R.id.recyclerText_to)
val textTotal = itemView.findViewById<TextView>(R.id.recyclerText_total)
}

View File

@ -1,24 +0,0 @@
package de.hft.geotracker
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
class Settings : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
}
}
}

View File

@ -0,0 +1,108 @@
package de.hft.geotracker.activities
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import de.hft.geotracker.R
import de.hft.geotracker.retrofit.GeofenceService
import de.hft.geotracker.retrofit.ValuesUserLogin
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import kotlinx.android.synthetic.main.activity_login.*
/**
* A simple [Fragment] subclass.
*/
class Login : AppCompatActivity() {
lateinit var login: TextView
lateinit var reg: TextView
lateinit var service: GeofenceService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
1000
)
} else {
// Background location runtime permission already granted.
// You can now call geofencingClient.addGeofences().
}
val retrofit = Retrofit.Builder()
.baseUrl("http://plesk.icaotix.de:5000")
.addConverterFactory(GsonConverterFactory.create())
.build()
service = retrofit.create(GeofenceService::class.java)
login = findViewById(R.id.button_login)
login.setOnClickListener {
intent = Intent(this, MainActivity::class.java)
login()
}
reg = findViewById(R.id.button_register)
reg.setOnClickListener {
register()
}
}
private fun register() {
val intent = Intent(this, Register::class.java)
startActivity(intent)
}
private fun login() {
val name = input_username.text.toString()
val pswd = input_password.text.toString()
val call = service.login(ValuesUserLogin(name, pswd))
call.enqueue(object : Callback<Void> {
override fun onResponse(call: Call<Void>?, response: Response<Void>?) {
if (response != null && response.isSuccessful) {
val headers = response.headers()
val authentication = headers.get("Authorization")
deleteFile("JWToken")
openFileOutput("JWToken", Context.MODE_PRIVATE).use {
it.write(authentication!!.toByteArray())
}
println(response.code())
startActivity(intent)
} else {
if (response != null) {
println(response.code())
Toast.makeText(this@Login, "Wrong Username or Password!", Toast.LENGTH_LONG)
.show()
} else {
println("Response is null")
}
}
}
override fun onFailure(call: Call<Void>?, t: Throwable?) {
println("Error: ${t.toString()}")
}
})
}
}

View File

@ -0,0 +1,405 @@
package de.hft.geotracker.activities
import android.Manifest
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.app.AlertDialog
import android.app.PendingIntent
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Looper
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityCompat.requestPermissions
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.gms.location.*
import de.hft.geotracker.GeofenceBroadcastReceiver
import de.hft.geotracker.R
import de.hft.geotracker.RecordsAdapter
import de.hft.geotracker.retrofit.*
import kotlinx.android.synthetic.main.activity_home.*
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.BufferedReader
import java.io.InputStreamReader
class MainActivity : AppCompatActivity() {
lateinit var geofencingClient: GeofencingClient
lateinit var geofence: Geofence
lateinit var actionButton: TextView
var running = false
var accName: String? = null
var workingSince: String? = null
lateinit var accounts: ValuesTimetrackAccounts
lateinit var service: GeofenceService
lateinit var locationRequest: LocationRequest
lateinit var fusedLocationClient: FusedLocationProviderClient
lateinit var locationCallback: LocationCallback
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
//Get location data and permissions
createLocationRequest()
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(this, arrayOf(ACCESS_FINE_LOCATION), 1000)
}
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
locationResult ?: return
}
}
//React on geofence state
this.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
?.edit()
?.putBoolean("ENABLED", false)
?.apply()
getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
val isInside = sharedPreferences.getBoolean("ENABLED", false)
println("Is inside? -> $isInside")
if (isInside) {
button_start_stop?.text = getString(R.string.start)
button_start_stop?.setBackgroundColor(resources.getColor(R.color.logo_blue))
} else {
button_start_stop?.setBackgroundColor(resources.getColor(R.color.colorPrimaryDark))
if (running) {
callStartStop()
}
button_start_stop?.text = getString(R.string.outside_place)
}
button_start_stop.isEnabled = isInside
}
//JWToken lesen
val fis = openFileInput("JWToken")
val isr = InputStreamReader(fis)
val bufferedReader = BufferedReader(isr)
val stringBuilder = StringBuilder()
var text: String? = null
while ({ text = bufferedReader.readLine(); text }() != null) {
stringBuilder.append(text)
}
val token = stringBuilder.toString()
println("Token Main: " + token)
//Retrofit declaration
val httpClient = OkHttpClient.Builder()
val interceptor = AuthenticationInterceptor(token)
httpClient.addInterceptor(interceptor)
val builder = Retrofit.Builder()
.baseUrl("http://plesk.icaotix.de:5000")
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
val retrofit = builder.build()
service = retrofit.create(GeofenceService::class.java)
showUsername()
updateRecyclerView()
actionButton = findViewById(R.id.button_start_stop)
actionButton.setBackgroundColor(resources.getColor(R.color.colorPrimaryDark))
actionButton.setOnClickListener {
if (running) {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle(R.string.app_name)
builder.setMessage("Do you want to stop?")
builder.setIcon(R.drawable.ic_logo)
builder.setPositiveButton("Yes", object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, id: Int) {
callStartStop()
dialog.dismiss()
}
})
builder.setNegativeButton("No", object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, id: Int) {
dialog.dismiss()
}
})
val alert: AlertDialog = builder.create()
alert.show()
} else {
callStartStop()
}
}
//Toolbar listener
my_toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.settings -> {
startActivity(Intent(this, Settings::class.java))
println("Settings pressed")
true
}
R.id.logout -> {
if (running) {
callStartStop()
}
deleteFile("JWToken")
startActivity(Intent(this, Login::class.java))
println("Logout pressed")
true
}
else -> false
}
}
}
private fun updateRecyclerView() {
//Recycler View
val recView: RecyclerView = records_list
recView.setHasFixedSize(true)
val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this)
val adapter = RecordsAdapter()
val recordList = ArrayList<RecordEntry>()
val call = service.getTodaysRecords()
call.enqueue(object: Callback<EmbeddedRecords> {
override fun onResponse(call: Call<EmbeddedRecords>, response: Response<EmbeddedRecords>) {
if (response.isSuccessful) {
val entries = response.body()!!.records.entries
if (!entries.isEmpty()) {
entries.forEach {
if (it.type.equals("PAID")) {
recordList.add(RecordEntry(it.startdate.substring(11, 16)
, it.enddate.substring(11, 16)
, it.duration))
}
}
} else {
println("No Records!")
}
if (running) {
recordList.add(RecordEntry(workingSince!!, "PENDING", -1))
}
adapter.data = recordList
recView.layoutManager = layoutManager
recView.adapter = adapter
} else {
println("Response for todays records was not successful")
}
}
override fun onFailure(call: Call<EmbeddedRecords>, t: Throwable) {
println("Getting todays records failed")
}
})
}
private fun callStartStop() {
running = !running
if (running) {
account_spinner.visibility = View.GONE
button_start_stop?.text = getString(R.string.stop)
} else {
account_spinner.visibility = View.VISIBLE
button_start_stop?.text = getString(R.string.start)
}
if (!accName.isNullOrEmpty()) {
val call = service.triggerTracking(accName!!)
call.enqueue(object : Callback<ValuesTracking> {
override fun onResponse(call: Call<ValuesTracking>, response: Response<ValuesTracking>) {
workingSince = response.body()?.startdate?.substring(11, 16)
updateRecyclerView()
println("Tracking event successful!")
}
override fun onFailure(call: Call<ValuesTracking>, t: Throwable) {
println("Problem at start tracking: " + t.message)
}
})
} else {
println("Accounts list is emty")
}
println("StartStop pressed: $running")
}
private fun showUsername() {
val call = service.getUser()
call.enqueue(object : Callback<ValuesUser> {
override fun onResponse(call: Call<ValuesUser>, response: Response<ValuesUser>) {
if (response.isSuccessful) {
val firstname = response.body()?.firstname
val location = response.body()?.location
val username = response.body()?.username
getTimetrackAccounts(username!!)
lbl_username.text = "Hello " + firstname
if (location?.latitude == null) {
button_start_stop?.text = "No geofence set for you"
button_start_stop?.setBackgroundColor(resources.getColor(R.color.colorPrimaryDark))
Toast.makeText(this@MainActivity, "No geofence set for you", Toast.LENGTH_LONG)
.show()
} else {
initializeGeofence(location.latitude, location.longitude, location.radius)
}
} else {
println("Response not successful: ${response.code()}")
}
}
override fun onFailure(call: Call<ValuesUser>, t: Throwable) {
println("Response 'whoami' failed. " + t.message)
}
})
}
private fun getTimetrackAccounts(user: String) {
val accountNames = mutableListOf<String>()
// accountNames.add("None")
val call = service.getAccounts(user)
call.enqueue(object: Callback<EmbeddedAccounts> {
override fun onResponse(
call: Call<EmbeddedAccounts>,
response: Response<EmbeddedAccounts>
) {
if (response.isSuccessful) {
accounts = response.body()!!.accounts
if (!accounts.entries.isEmpty()) {
accounts.entries.forEach {
accountNames.add(it.name + "")
}
} else {
accountNames.add("None")
initializeDropdown(accountNames)
display_description.setText("You dont have any Timetrack Accounts")
Toast.makeText(this@MainActivity, "You dont have any Timetrack Accounts", Toast.LENGTH_LONG)
.show()
}
initializeDropdown(accountNames)
println("Dropdown initialized")
}
}
override fun onFailure(call: Call<EmbeddedAccounts>, t: Throwable) {
println("Failed to get accounts")
}
})
}
private fun initializeDropdown(accountNames: MutableList<String>) {
val spinner: Spinner = findViewById(R.id.account_spinner)
// Create an ArrayAdapter using the string array and a default spinner layout
val arrayAdapter = ArrayAdapter<String>(this, R.layout.spinner_layout, accountNames)
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = arrayAdapter
spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
if (!accountNames.get(0).equals("None")) {
accName = accounts.entries.get(position).name
display_description.setText(accounts.entries.get(position).description)
display_revenue.setText(accounts.entries.get(position).revenue.toString())
} else {
display_revenue.visibility = View.GONE
display_revenue_layout.visibility = View.GONE
}
println("Selected: " + accountNames.get(position))
}
override fun onNothingSelected(parent: AdapterView<*>?) {
println("Nothing selected")
}
}
}
private fun initializeGeofence(lat: Double, long: Double, rad: Float) {
geofencingClient = LocationServices.getGeofencingClient(this)
geofence = Geofence.Builder().setRequestId("Test")
.setCircularRegion(lat, long, rad)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
if (ActivityCompat.checkSelfPermission(
this,
ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(this, arrayOf(ACCESS_FINE_LOCATION), 1000)
}
geofencingClient.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
addOnSuccessListener {
println("Geofence added with: latitude: $lat longitude: $long radius: $rad")
}
addOnFailureListener {
println("Error: " + it.stackTrace.forEach { println(it.toString()) })
}
}
}
private fun getGeofencingRequest(): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofence(geofence)
}.build()
}
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
// addGeofences() and removeGeofences().
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
override fun onResume() {
super.onResume()
startLocationUpdates()
}
private fun startLocationUpdates() {
if (ActivityCompat.checkSelfPermission(
this,
ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(this, arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), 1000)
}
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
private fun createLocationRequest() {
locationRequest = LocationRequest.create().apply {
interval = 10000
fastestInterval = 5000
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
}
override fun onBackPressed() {
}
}
class RecordEntry(from: String, to: String, duration: Int) {
val from = from
val to = to
val duration = duration
}

View File

@ -1,24 +1,26 @@
package de.hft.geotracker
package de.hft.geotracker.activities
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import de.hft.geotracker.R
class Register : AppCompatActivity() {
lateinit var reg: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register)
reg = findViewById(R.id.button_create_account)
reg = findViewById(R.id.button_login)
reg.setOnClickListener {
createAccount()
}
}
private fun createAccount() {
var intent = Intent(this, MainActivity::class.java)
startActivity(intent)
Toast.makeText(this@Register, "Not yet implemented!", Toast.LENGTH_LONG)
.show()
}

View File

@ -0,0 +1,24 @@
package de.hft.geotracker.activities
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import de.hft.geotracker.R
import kotlinx.android.synthetic.main.activity_home.*
class Settings : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
my_toolbar.setNavigationOnClickListener {
onBackPressed()
}
findViewById<TextView>(R.id.button_submit).setOnClickListener {
Toast.makeText(this@Settings, "Not yet implemented!", Toast.LENGTH_LONG)
.show()
}
}
}

View File

@ -0,0 +1,17 @@
package de.hft.geotracker.retrofit
import okhttp3.Interceptor
import okhttp3.Response
class AuthenticationInterceptor(pToken: String) : Interceptor {
private val token = pToken
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val builder = original.newBuilder()
.header("Authorization", token)
val request = builder.build()
return chain.proceed(request)
}
}

View File

@ -0,0 +1,8 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class EmbeddedAccounts(accounts: ValuesTimetrackAccounts) {
@SerializedName("_embedded")
var accounts = accounts
}

View File

@ -0,0 +1,8 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class EmbeddedRecords(records: ValuesRecordsArray) {
@SerializedName("_embedded")
var records = records
}

View File

@ -0,0 +1,21 @@
package de.hft.geotracker.retrofit
import retrofit2.Call
import retrofit2.http.*
interface GeofenceService {
@POST("/login")
fun login(@Body login_data: ValuesUserLogin): Call<Void>
@GET("whoami")
fun getUser(): Call<ValuesUser>
@GET("accounts/search/findByUsername")
fun getAccounts(@Query("username") username : String): Call<EmbeddedAccounts>
@GET("track")
fun triggerTracking(@Query("account") account: String): Call<ValuesTracking>
@GET("records/search/today")
fun getTodaysRecords(): Call<EmbeddedRecords>
}

View File

@ -0,0 +1,19 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesLocation(
latitude: Double,
longitude: Double,
radius: Int
) {
@SerializedName("latitude")
var latitude = latitude
@SerializedName("longitude")
var longitude = longitude
@SerializedName("radius")
var radius = radius.toFloat()
}

View File

@ -0,0 +1,22 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesRecordEntry(
start: String,
end: String,
type: String,
duration: Int
) {
@SerializedName("startdate")
var startdate = start
@SerializedName("enddate")
var enddate = end
@SerializedName("type")
var type = type
@SerializedName("duration")
var duration = duration
}

View File

@ -0,0 +1,8 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesRecordsArray(entries: Array<ValuesRecordEntry>) {
@SerializedName("records")
var entries = entries
}

View File

@ -0,0 +1,8 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesTimetrackAccounts(entries: Array<ValuesTimetrackAccountsEntries>) {
@SerializedName("accounts")
var entries = entries
}

View File

@ -0,0 +1,19 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesTimetrackAccountsEntries(
revenue: Double,
name: String,
description: String
) {
@SerializedName("revenue")
var revenue = revenue
@SerializedName("name")
var name = name
@SerializedName("description")
var description = description
}

View File

@ -0,0 +1,30 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesTracking(
duration: Int,
start: String,
end: String,
account: String,
user: String,
type: String
) {
@SerializedName("duration")
var duration = duration
@SerializedName("startdate")
var startdate = start
@SerializedName("enddate")
var enddate = end
@SerializedName("account")
var account = account
@SerializedName("username")
var username = user
@SerializedName("type")
var type = type
}

View File

@ -0,0 +1,32 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesUser(
role: String,
firstname: String,
lastname: String,
username: String,
location: ValuesLocation,
id: Int
) {
@SerializedName("role")
var role = role
@SerializedName("firstname")
var firstname = firstname
@SerializedName("lastname")
var lastname = lastname
@SerializedName("username")
var username = username
@SerializedName("location")
var location = location
@SerializedName("id")
var id = id
}

View File

@ -0,0 +1,13 @@
package de.hft.geotracker.retrofit
import com.google.gson.annotations.SerializedName
class ValuesUserLogin(name: String, pswd: String) {
@SerializedName("username")
var username = name
@SerializedName("password")
var password = pswd
}

View File

@ -0,0 +1,61 @@
<vector android:height="35dp" android:viewportHeight="12000"
android:viewportWidth="12000" android:width="35dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#2a2a2a"
android:pathData="M5640,11934c-297,-19 -571,-57 -905,-124 -566,-115 -1236,-379 -1770,-698 -441,-263 -792,-538 -1166,-911 -167,-168 -239,-244 -363,-391 -218,-257 -481,-643 -655,-960 -260,-475 -492,-1097 -591,-1585 -55,-274 -72,-386 -112,-730 -17,-150 -17,-920 0,-1070 40,-344 57,-456 112,-730 99,-488 331,-1110 591,-1585 173,-316 431,-693 655,-960 208,-247 507,-546 754,-754 267,-224 644,-482 960,-655 475,-260 1096,-492 1585,-591 222,-45 311,-60 500,-84 290,-38 355,-41 765,-41 410,0 475,3 765,41 189,24 278,39 500,84 566,115 1236,379 1770,698 441,263 792,538 1166,911 167,168 239,244 363,391 218,257 481,643 655,960 260,475 492,1097 591,1585 55,274 72,386 112,730 8,72 13,256 13,535 0,279 -5,463 -13,535 -40,344 -57,456 -112,730 -114,565 -379,1236 -698,1770 -458,768 -1080,1422 -1817,1912 -293,195 -529,326 -860,478 -323,149 -836,317 -1170,385 -216,43 -389,72 -540,90 -49,5 -130,15 -180,21 -86,10 -790,20 -905,13z" android:strokeColor="#00000000"/>
<path android:fillColor="#801010"
android:pathData="M5640,11934c-297,-19 -571,-57 -905,-124 -566,-115 -1236,-379 -1770,-698 -441,-263 -792,-538 -1166,-911 -167,-168 -239,-244 -363,-391 -218,-257 -481,-643 -655,-960 -260,-475 -492,-1097 -591,-1585 -55,-274 -72,-386 -112,-730 -17,-150 -17,-920 0,-1070 40,-344 57,-456 112,-730 99,-488 331,-1110 591,-1585 173,-316 431,-693 655,-960 208,-247 507,-546 754,-754 267,-224 644,-482 960,-655 475,-260 1096,-492 1585,-591 222,-45 311,-60 500,-84 290,-38 355,-41 765,-41 410,0 475,3 765,41 189,24 278,39 500,84 566,115 1236,379 1770,698 441,263 792,538 1166,911 167,168 239,244 363,391 218,257 481,643 655,960 260,475 492,1097 591,1585 55,274 72,386 112,730 8,72 13,256 13,535 0,279 -5,463 -13,535 -40,344 -57,456 -112,730 -114,565 -379,1236 -698,1770 -458,768 -1080,1422 -1817,1912 -293,195 -529,326 -860,478 -323,149 -836,317 -1170,385 -216,43 -389,72 -540,90 -49,5 -130,15 -180,21 -86,10 -790,20 -905,13zM6405,11039c1939,-165 3607,-1411 4321,-3228 158,-403 270,-859 319,-1311 22,-199 31,-663 16,-875 -72,-1051 -449,-2023 -1102,-2841 -924,-1158 -2312,-1859 -3781,-1911l-188,-6 0,145 0,145 39,6c67,9 157,56 207,109 70,75 89,125 89,238 0,82 -4,103 -26,151 -48,101 -154,183 -258,198l-46,6 -3,1749 -2,1749 57,-6c32,-4 185,-20 341,-37 155,-16 408,-44 562,-60 154,-16 405,-43 558,-60 152,-16 448,-48 657,-70 209,-22 505,-54 658,-70 304,-33 812,-88 867,-95 35,-5 35,-4 -6,9 -23,8 -216,91 -430,186 -214,95 -681,302 -1039,460 -357,158 -972,431 -1365,606l-715,317 -115,13c-63,8 -241,27 -395,44 -154,16 -407,43 -562,60 -156,16 -409,44 -563,60 -154,17 -406,44 -560,60 -154,17 -407,44 -562,60 -156,17 -449,48 -653,70 -203,22 -395,47 -425,55 -30,8 -56,15 -58,15 -15,0 32,-31 54,-36 29,-7 342,-144 1669,-732 1202,-532 1100,-485 1073,-495 -12,-5 -48,-15 -78,-23 -30,-7 -107,-27 -170,-44 -63,-17 -140,-38 -170,-46 -30,-8 -77,-21 -105,-28 -27,-8 -102,-28 -165,-45 -63,-17 -167,-44 -230,-62 -63,-17 -137,-37 -165,-44 -27,-7 -84,-22 -125,-33 -41,-11 -118,-32 -170,-45 -52,-14 -131,-35 -175,-47 -44,-12 -93,-26 -110,-30 -16,-4 -73,-19 -125,-33 -204,-55 -297,-80 -345,-92 -27,-7 -77,-21 -110,-30 -60,-17 -130,-35 -300,-80 -49,-13 -117,-31 -150,-40 -33,-10 -82,-23 -110,-30 -27,-7 -77,-21 -110,-30 -33,-10 -85,-23 -115,-31 -30,-7 -107,-27 -170,-44 -186,-52 -441,-120 -500,-135 -30,-8 -80,-21 -110,-29 -30,-8 -81,-22 -113,-30l-58,-14 -10,26c-6,15 -26,95 -44,178 -235,1059 -125,2174 311,3162 624,1411 1864,2474 3344,2865 360,96 678,144 1115,171 96,6 499,-4 630,-15zM2215,3913c44,-22 65,-43 87,-87 40,-79 14,-199 -54,-247 -141,-101 -321,-11 -321,161 0,31 7,69 16,86 21,41 80,92 115,100 15,3 32,7 37,9 18,7 89,-7 120,-22zM3846,2283c116,-60 139,-232 42,-319 -45,-42 -87,-56 -150,-52 -167,12 -245,200 -135,325 64,73 158,90 243,46z" android:strokeColor="#00000000"/>
<path android:fillColor="#243a49"
android:pathData="M5640,11934c-297,-19 -571,-57 -905,-124 -566,-115 -1236,-379 -1770,-698 -441,-263 -792,-538 -1166,-911 -167,-168 -239,-244 -363,-391 -218,-257 -481,-643 -655,-960 -260,-475 -492,-1097 -591,-1585 -55,-274 -72,-386 -112,-730 -17,-150 -17,-920 0,-1070 40,-344 57,-456 112,-730 99,-488 331,-1110 591,-1585 173,-316 431,-693 655,-960 208,-247 507,-546 754,-754 267,-224 644,-482 960,-655 475,-260 1096,-492 1585,-591 222,-45 311,-60 500,-84 290,-38 355,-41 765,-41 410,0 475,3 765,41 189,24 278,39 500,84 566,115 1236,379 1770,698 441,263 792,538 1166,911 167,168 239,244 363,391 218,257 481,643 655,960 260,475 492,1097 591,1585 55,274 72,386 112,730 8,72 13,256 13,535 0,279 -5,463 -13,535 -40,344 -57,456 -112,730 -114,565 -379,1236 -698,1770 -458,768 -1080,1422 -1817,1912 -293,195 -529,326 -860,478 -323,149 -836,317 -1170,385 -216,43 -389,72 -540,90 -49,5 -130,15 -180,21 -86,10 -790,20 -905,13zM6405,11039c1939,-165 3607,-1411 4321,-3228 158,-403 270,-859 319,-1311 22,-199 31,-663 16,-875 -72,-1051 -449,-2023 -1102,-2841 -924,-1158 -2312,-1859 -3781,-1911l-188,-6 0,145 0,145 39,6c67,9 157,56 207,109 70,75 89,125 89,238 0,82 -4,103 -26,151 -48,101 -154,183 -258,198l-46,6 -5,1750 -5,1751 160,-17c88,-10 288,-31 445,-48 157,-17 412,-44 568,-61 155,-16 408,-44 562,-60 154,-17 407,-44 563,-60 155,-17 449,-48 652,-70 204,-22 453,-48 555,-59 101,-11 185,-19 187,-17 2,1 -68,22 -154,45 -87,23 -374,100 -638,171 -826,221 -1283,344 -1790,480 -269,72 -631,169 -804,215 -172,47 -316,86 -318,89 -3,3 130,527 144,562 3,7 -34,15 -104,22 -59,6 -235,25 -390,42 -156,16 -409,44 -563,60 -154,17 -406,44 -560,60 -154,17 -406,44 -560,60 -154,17 -407,44 -562,60 -156,17 -449,48 -653,70 -203,22 -395,47 -425,55 -30,8 -56,15 -58,15 -15,0 32,-31 54,-36 29,-7 342,-144 1669,-732 1202,-532 1100,-485 1073,-495 -12,-5 -48,-15 -78,-23 -30,-7 -107,-27 -170,-44 -63,-17 -140,-38 -170,-46 -30,-8 -77,-21 -105,-28 -27,-8 -102,-28 -165,-45 -63,-17 -167,-44 -230,-62 -63,-17 -137,-37 -165,-44 -27,-7 -84,-22 -125,-33 -41,-11 -118,-32 -170,-45 -52,-14 -131,-35 -175,-47 -44,-12 -93,-26 -110,-30 -16,-4 -73,-19 -125,-33 -204,-55 -297,-80 -345,-92 -27,-7 -77,-21 -110,-30 -60,-17 -130,-35 -300,-80 -49,-13 -117,-31 -150,-40 -33,-10 -82,-23 -110,-30 -27,-7 -77,-21 -110,-30 -33,-10 -85,-23 -115,-31 -30,-7 -107,-27 -170,-44 -186,-52 -441,-120 -500,-135 -30,-8 -80,-21 -110,-29 -30,-8 -81,-22 -113,-30l-58,-14 -10,26c-6,15 -26,95 -44,178 -235,1059 -125,2174 311,3162 624,1411 1864,2474 3344,2865 360,96 678,144 1115,171 96,6 499,-4 630,-15zM2215,3913c44,-22 65,-43 87,-87 40,-79 14,-199 -54,-247 -141,-101 -321,-11 -321,161 0,31 7,69 16,86 21,41 80,92 115,100 15,3 32,7 37,9 18,7 89,-7 120,-22zM3846,2283c116,-60 139,-232 42,-319 -45,-42 -87,-56 -150,-52 -167,12 -245,200 -135,325 64,73 158,90 243,46z" android:strokeColor="#00000000"/>
<path android:fillColor="#243a49"
android:pathData="M3670,10005c-78,-36 -120,-103 -120,-192 1,-187 228,-273 354,-133 83,92 66,236 -37,307 -49,34 -142,42 -197,18z" android:strokeColor="#00000000"/>
<path android:fillColor="#243a49"
android:pathData="M8115,10002c-80,-37 -121,-113 -113,-208 14,-171 225,-245 347,-123 91,90 77,240 -27,314 -55,38 -146,46 -207,17z" android:strokeColor="#00000000"/>
<path android:fillColor="#243a49"
android:pathData="M2035,8373c-81,-42 -121,-115 -113,-207 14,-166 213,-242 338,-130 105,95 87,262 -35,331 -49,28 -144,31 -190,6z" android:strokeColor="#00000000"/>
<path android:fillColor="#243a49"
android:pathData="M9745,8376c-38,-17 -90,-72 -104,-109 -6,-16 -11,-54 -11,-86 0,-79 37,-138 111,-175 87,-43 167,-30 235,38 67,67 80,145 39,233 -34,75 -96,113 -179,113 -34,-1 -74,-7 -91,-14z" android:strokeColor="#00000000"/>
<path android:fillColor="#243a49"
android:pathData="M9750,3925c-78,-36 -120,-103 -120,-192 1,-187 228,-273 354,-133 83,92 66,236 -37,307 -49,34 -142,42 -197,18z" android:strokeColor="#00000000"/>
<path android:fillColor="#243a49"
android:pathData="M8115,2293c-81,-42 -121,-115 -113,-207 14,-166 213,-242 338,-130 105,95 87,262 -35,331 -49,28 -144,31 -190,6z" android:strokeColor="#00000000"/>
<path android:fillColor="#e21d1f"
android:pathData="M5640,11934c-297,-19 -571,-57 -905,-124 -566,-115 -1236,-379 -1770,-698 -441,-263 -792,-538 -1166,-911 -167,-168 -239,-244 -363,-391 -218,-257 -481,-643 -655,-960 -260,-475 -492,-1097 -591,-1585 -55,-274 -72,-386 -112,-730 -17,-150 -17,-920 0,-1070 40,-344 57,-456 112,-730 99,-488 331,-1110 591,-1585 173,-316 431,-693 655,-960 208,-247 507,-546 754,-754 267,-224 644,-482 960,-655 475,-260 1096,-492 1585,-591 222,-45 311,-60 500,-84 290,-38 355,-41 765,-41 410,0 475,3 765,41 189,24 278,39 500,84 566,115 1236,379 1770,698 441,263 792,538 1166,911 167,168 239,244 363,391 218,257 481,643 655,960 260,475 492,1097 591,1585 55,274 72,386 112,730 8,72 13,256 13,535 0,279 -5,463 -13,535 -40,344 -57,456 -112,730 -114,565 -379,1236 -698,1770 -458,768 -1080,1422 -1817,1912 -293,195 -529,326 -860,478 -323,149 -836,317 -1170,385 -216,43 -389,72 -540,90 -49,5 -130,15 -180,21 -86,10 -790,20 -905,13zM6405,11039c1939,-165 3607,-1411 4321,-3228 158,-403 270,-859 319,-1311 22,-199 31,-663 16,-875 -72,-1051 -449,-2023 -1102,-2841 -925,-1159 -2315,-1861 -3786,-1911l-193,-6 0,145 0,145 43,5c70,8 162,55 213,110 70,75 89,125 89,238 0,82 -4,103 -26,151 -15,32 -46,76 -69,99 -53,53 -151,100 -207,100l-43,0 -2,1754 -3,1754 30,-4c17,-2 157,-18 313,-34 155,-17 408,-43 562,-60 154,-16 406,-43 560,-60 154,-16 406,-43 560,-60 154,-17 405,-43 558,-60 152,-16 383,-41 512,-55 129,-13 318,-33 420,-44 101,-11 185,-19 187,-17 2,1 -68,22 -154,45 -87,23 -374,100 -638,171 -826,221 -1283,344 -1790,480 -269,72 -631,169 -804,215 -172,47 -316,86 -318,89 -3,3 130,527 144,562 3,7 -34,15 -104,22 -59,6 -235,25 -390,42 -156,16 -409,44 -563,60 -154,17 -406,44 -560,60 -154,17 -406,44 -560,60 -154,17 -407,44 -562,60 -156,17 -449,48 -653,70 -203,22 -395,47 -425,55 -30,8 -56,15 -58,15 -15,0 32,-31 54,-36 16,-4 241,-100 499,-214 589,-260 1802,-797 2079,-921l209,-93 -124,-33c-68,-19 -146,-40 -174,-47 -27,-8 -180,-49 -340,-91 -159,-42 -310,-83 -335,-90 -25,-7 -175,-48 -335,-90 -159,-42 -312,-83 -340,-91 -27,-8 -153,-41 -280,-75 -126,-33 -252,-67 -280,-75 -27,-7 -160,-43 -295,-79 -135,-36 -261,-70 -280,-75 -19,-5 -145,-39 -280,-75 -135,-36 -267,-72 -295,-79 -209,-59 -615,-165 -639,-168 -30,-3 -30,-2 -52,82 -57,222 -112,556 -135,825 -16,179 -16,640 0,815 70,786 296,1505 677,2157 790,1348 2136,2256 3674,2477 153,22 317,37 525,50 96,6 499,-4 630,-15zM2205,3921c70,-31 125,-113 125,-186 0,-66 -51,-150 -110,-180 -102,-53 -233,-6 -281,100 -27,58 -24,113 9,176 49,94 162,133 257,90zM3835,2291c56,-25 100,-79 114,-139 35,-145 -96,-276 -241,-241 -186,44 -215,301 -43,380 51,24 119,24 170,0z" android:strokeColor="#00000000"/>
<path android:fillColor="#0096ff"
android:pathData="M5640,11934c-297,-19 -571,-57 -905,-124 -566,-115 -1236,-379 -1770,-698 -441,-263 -792,-538 -1166,-911 -167,-168 -239,-244 -363,-391 -218,-257 -481,-643 -655,-960 -260,-475 -492,-1097 -591,-1585 -55,-274 -72,-386 -112,-730 -17,-150 -17,-920 0,-1070 40,-344 57,-456 112,-730 99,-488 331,-1110 591,-1585 173,-316 431,-693 655,-960 208,-247 507,-546 754,-754 267,-224 644,-482 960,-655 475,-260 1096,-492 1585,-591 222,-45 311,-60 500,-84 290,-38 355,-41 765,-41 410,0 475,3 765,41 189,24 278,39 500,84 566,115 1236,379 1770,698 441,263 792,538 1166,911 167,168 239,244 363,391 218,257 481,643 655,960 260,475 492,1097 591,1585 55,274 72,386 112,730 8,72 13,256 13,535 0,279 -5,463 -13,535 -40,344 -57,456 -112,730 -114,565 -379,1236 -698,1770 -458,768 -1080,1422 -1817,1912 -293,195 -529,326 -860,478 -323,149 -836,317 -1170,385 -216,43 -389,72 -540,90 -49,5 -130,15 -180,21 -86,10 -790,20 -905,13zM6405,11039c1939,-165 3607,-1411 4321,-3228 158,-403 270,-859 319,-1311 22,-199 31,-663 16,-875 -72,-1051 -449,-2023 -1102,-2841 -925,-1159 -2315,-1861 -3786,-1911l-193,-6 0,145 0,145 43,5c70,8 162,55 213,110 70,75 89,125 89,238 0,82 -4,103 -26,151 -15,32 -46,76 -69,99 -53,53 -151,100 -207,100l-43,0 -2,1752 -3,1753 -80,9c-44,5 -81,10 -83,11 -1,1 29,116 67,256 38,140 106,396 151,569 45,173 84,319 87,325 3,8 -31,16 -104,23 -59,6 -235,25 -390,42 -156,16 -409,44 -563,60 -154,17 -406,44 -560,60 -154,17 -406,44 -560,60 -154,17 -407,44 -562,60 -156,17 -449,48 -653,70 -203,22 -395,47 -425,55 -30,8 -56,15 -58,15 -15,0 32,-31 54,-36 16,-4 241,-100 499,-214 589,-260 1802,-797 2079,-921l209,-93 -124,-33c-68,-19 -146,-40 -174,-47 -27,-8 -180,-49 -340,-91 -159,-42 -310,-83 -335,-90 -25,-7 -175,-48 -335,-90 -159,-42 -312,-83 -340,-91 -27,-8 -153,-41 -280,-75 -126,-33 -252,-67 -280,-75 -27,-7 -160,-43 -295,-79 -135,-36 -261,-70 -280,-75 -19,-5 -145,-39 -280,-75 -135,-36 -267,-72 -295,-79 -209,-59 -615,-165 -639,-168 -30,-3 -30,-2 -52,82 -57,222 -112,556 -135,825 -16,179 -16,640 0,815 70,786 296,1505 677,2157 790,1348 2136,2256 3674,2477 153,22 317,37 525,50 96,6 499,-4 630,-15zM2205,3921c70,-31 125,-113 125,-186 0,-66 -51,-150 -110,-180 -102,-53 -233,-6 -281,100 -27,58 -24,113 9,176 49,94 162,133 257,90zM3835,2291c56,-25 100,-79 114,-139 35,-145 -96,-276 -241,-241 -186,44 -215,301 -43,380 51,24 119,24 170,0z" android:strokeColor="#00000000"/>
<path android:fillColor="#0096ff"
android:pathData="M3685,10008c-158,-55 -183,-270 -42,-362 151,-97 344,38 306,214 -25,116 -152,187 -264,148z" android:strokeColor="#00000000"/>
<path android:fillColor="#0096ff"
android:pathData="M8125,10001c-105,-49 -152,-166 -106,-266 60,-133 235,-163 332,-58 34,36 45,59 54,109 25,153 -140,280 -280,215z" android:strokeColor="#00000000"/>
<path android:fillColor="#0096ff"
android:pathData="M2044,8376c-104,-47 -152,-173 -103,-273 84,-174 329,-148 380,40 21,78 -16,169 -89,216 -45,29 -142,38 -188,17z" android:strokeColor="#00000000"/>
<path android:fillColor="#0096ff"
android:pathData="M9739,8367c-163,-86 -133,-331 46,-376 143,-36 280,100 244,244 -32,126 -177,192 -290,132z" android:strokeColor="#00000000"/>
<path android:fillColor="#0096ff"
android:pathData="M9761,3927c-44,-14 -106,-78 -122,-124 -16,-50 -6,-133 23,-176 122,-188 414,-66 367,153 -25,118 -148,185 -268,147z" android:strokeColor="#00000000"/>
<path android:fillColor="#0096ff"
android:pathData="M8124,2296c-103,-46 -151,-171 -105,-272 63,-138 265,-158 348,-34 68,99 44,224 -55,289 -45,29 -142,38 -188,17z" android:strokeColor="#00000000"/>
<path android:fillColor="#949187"
android:pathData="M5640,11934c-297,-19 -571,-57 -905,-124 -566,-115 -1236,-379 -1770,-698 -441,-263 -792,-538 -1166,-911 -167,-168 -239,-244 -363,-391 -218,-257 -481,-643 -655,-960 -260,-475 -492,-1097 -591,-1585 -55,-274 -72,-386 -112,-730 -17,-150 -17,-920 0,-1070 40,-344 57,-456 112,-730 99,-488 331,-1110 591,-1585 173,-316 431,-693 655,-960 208,-247 507,-546 754,-754 267,-224 644,-482 960,-655 475,-260 1096,-492 1585,-591 222,-45 311,-60 500,-84 290,-38 355,-41 765,-41 410,0 475,3 765,41 189,24 278,39 500,84 566,115 1236,379 1770,698 441,263 792,538 1166,911 167,168 239,244 363,391 218,257 481,643 655,960 260,475 492,1097 591,1585 55,274 72,386 112,730 8,72 13,256 13,535 0,279 -5,463 -13,535 -40,344 -57,456 -112,730 -114,565 -379,1236 -698,1770 -458,768 -1080,1422 -1817,1912 -293,195 -529,326 -860,478 -323,149 -836,317 -1170,385 -216,43 -389,72 -540,90 -49,5 -130,15 -180,21 -86,10 -790,20 -905,13zM6405,11039c1939,-165 3607,-1411 4321,-3228 158,-403 270,-859 319,-1311 22,-199 31,-663 16,-875 -72,-1051 -449,-2023 -1102,-2841 -779,-977 -1903,-1640 -3124,-1843 -337,-57 -454,-65 -870,-65 -414,-1 -514,7 -855,65 -1047,177 -2017,687 -2770,1456 -631,643 -1074,1425 -1293,2283 -112,436 -160,826 -160,1285 0,293 10,442 49,720 241,1720 1354,3199 2954,3925 576,261 1166,400 1885,444 96,6 499,-4 630,-15z" android:strokeColor="#00000000"/>
<path android:fillColor="#949187"
android:pathData="M5833,10731c-77,-37 -137,-96 -172,-170 -22,-48 -26,-69 -26,-151 0,-76 5,-104 22,-142 32,-70 103,-141 176,-174 53,-25 74,-29 147,-29 73,0 94,4 147,29 73,33 144,104 176,174 17,38 22,66 22,142 0,82 -4,103 -26,151 -35,74 -95,133 -172,170 -53,25 -74,29 -147,29 -73,0 -94,-4 -147,-29z" android:strokeColor="#00000000"/>
<path android:fillColor="#949187"
android:pathData="M2253,6965c8,-7 28,-17 43,-21 36,-9 101,-37 974,-424 388,-172 959,-425 1270,-562 311,-138 722,-320 914,-405 192,-85 352,-151 356,-146 9,9 103,355 226,823 42,162 78,300 81,306 3,7 -34,15 -104,22 -59,6 -235,25 -390,42 -156,16 -409,44 -563,60 -154,17 -406,44 -560,60 -154,17 -406,44 -560,60 -154,17 -407,44 -562,60 -156,17 -449,48 -653,70 -203,22 -395,47 -425,55 -30,8 -56,15 -58,15 -2,0 3,-7 11,-15z" android:strokeColor="#00000000"/>
<path android:fillColor="#949187"
android:pathData="M1444,6306c-161,-40 -264,-174 -264,-341 0,-143 69,-254 198,-317 60,-30 75,-33 152,-32 105,0 176,29 245,99 181,180 116,485 -123,579 -59,23 -147,28 -208,12z" android:strokeColor="#00000000"/>
<path android:fillColor="#949187"
android:pathData="M10344,6306c-161,-40 -264,-174 -264,-341 0,-143 69,-254 198,-317 60,-30 75,-33 152,-32 105,0 176,29 245,99 181,180 116,485 -123,579 -59,23 -147,28 -208,12z" android:strokeColor="#00000000"/>
<path android:fillColor="#949187"
android:pathData="M5853,1839c-71,-27 -155,-106 -190,-177 -25,-50 -28,-68 -28,-152 0,-79 4,-103 24,-142 37,-75 95,-134 169,-170 59,-29 76,-33 152,-33 73,0 94,4 147,29 73,33 144,104 176,174 17,38 22,66 22,142 0,82 -4,103 -26,151 -35,74 -95,133 -172,170 -53,25 -74,29 -142,28 -55,0 -96,-6 -132,-20z" android:strokeColor="#00000000"/>
<path android:fillColor="#eae6d8"
android:pathData="M5640,11934c-296,-19 -576,-57 -904,-124 -494,-101 -1110,-330 -1586,-591 -316,-173 -693,-431 -960,-655 -194,-164 -521,-483 -660,-644 -104,-121 -109,-127 -176,-210 -535,-666 -942,-1498 -1134,-2320 -17,-74 -35,-153 -40,-175 -12,-51 -45,-238 -59,-330 -10,-71 -20,-153 -43,-350 -17,-150 -17,-920 0,-1070 7,-60 17,-148 23,-195 96,-804 370,-1607 787,-2305 154,-259 368,-561 548,-775 208,-247 507,-546 754,-754 267,-224 644,-482 960,-655 476,-261 1092,-490 1586,-591 215,-44 310,-60 499,-84 290,-38 355,-41 765,-41 410,0 475,3 765,41 189,24 284,40 499,84 495,101 1115,332 1586,591 322,177 693,431 960,655 194,164 521,483 660,644 104,121 109,127 176,210 535,666 942,1498 1134,2320 17,74 35,153 40,175 12,51 45,238 59,330 10,71 20,153 43,350 8,72 13,256 13,535 0,279 -5,463 -13,535 -23,197 -33,279 -43,350 -14,92 -47,279 -59,330 -5,22 -23,101 -40,175 -131,561 -364,1135 -668,1645 -457,767 -1080,1422 -1817,1912 -293,195 -529,326 -860,478 -324,149 -832,316 -1171,385 -210,43 -388,72 -539,90 -49,5 -130,15 -180,21 -86,10 -790,20 -905,13zM6361,11050c815,-63 1568,-304 2259,-723 1040,-630 1836,-1632 2210,-2782 181,-559 260,-1100 247,-1700 -12,-579 -112,-1105 -311,-1645 -71,-195 -80,-215 -174,-415 -687,-1462 -2039,-2516 -3629,-2829 -1318,-260 -2693,13 -3808,757 -396,264 -731,561 -1048,927 -423,490 -760,1081 -967,1700 -181,541 -260,1033 -260,1626 0,582 79,1070 260,1614 203,608 502,1143 927,1660 112,136 498,521 635,634 827,682 1790,1081 2823,1170 186,17 656,20 836,6z" android:strokeColor="#00000000"/>
<path android:fillColor="#eae6d8"
android:pathData="M5873,10746c-89,-29 -163,-95 -210,-186 -24,-48 -27,-67 -28,-145 0,-82 3,-96 33,-157 38,-77 91,-128 172,-167 46,-21 69,-26 140,-26 71,0 94,5 140,26 81,39 134,90 172,167 30,61 33,75 33,157 -1,79 -4,97 -30,148 -34,68 -107,140 -173,169 -59,27 -188,34 -249,14z" android:strokeColor="#00000000"/>
<path android:fillColor="#eae6d8"
android:pathData="M2280,6964c0,-3 21,-14 48,-25 75,-33 1690,-747 2621,-1161 470,-208 857,-376 861,-371 6,7 127,443 150,540 9,38 23,31 -205,92 -82,22 -438,117 -790,211 -1199,321 -1760,471 -2190,586 -236,63 -445,119 -462,124 -18,4 -33,6 -33,4z" android:strokeColor="#00000000"/>
<path android:fillColor="#eae6d8"
android:pathData="M1430,6301c-93,-29 -177,-101 -219,-190 -22,-48 -26,-69 -26,-151 0,-85 3,-101 29,-150 63,-123 177,-192 316,-193 103,-1 180,34 255,115 206,222 46,581 -258,577 -40,0 -83,-4 -97,-8z" android:strokeColor="#00000000"/>
<path android:fillColor="#eae6d8"
android:pathData="M10330,6301c-93,-29 -177,-101 -219,-190 -22,-48 -26,-69 -26,-151 0,-85 3,-101 29,-150 63,-123 177,-192 316,-193 103,-1 180,34 255,115 206,222 46,581 -258,577 -40,0 -83,-4 -97,-8z" android:strokeColor="#00000000"/>
<path android:fillColor="#eae6d8"
android:pathData="M5853,1839c-71,-28 -155,-106 -190,-177 -25,-50 -28,-68 -28,-152 0,-79 4,-103 24,-142 37,-75 95,-134 169,-170 60,-30 75,-33 152,-33 70,1 94,5 140,27 81,38 134,89 172,166 30,61 33,75 33,157 -1,79 -4,97 -30,148 -34,68 -107,140 -173,170 -69,31 -200,34 -269,6z" android:strokeColor="#00000000"/>
</vector>

View File

@ -1,72 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey"
tools:context=".MainActivity">
tools:context=".activities.MainActivity">
<!-- TODO: Update blank fragment layout -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/my_toolbar"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:contextClickable="false"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
android:visibility="visible"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/my_toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/menu"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:titleTextAppearance="@style/text_style" />
app:navigationIcon="@drawable/ic_logo"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/lbl_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin16"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/margin16"
android:textAlignment="center"
android:textAppearance="@style/text_style"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="@color/colorAccent"
app:layout_constraintTop_toBottomOf="@+id/lbl_username" />
<TextView
android:id="@+id/selected_acc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginTop="@dimen/margin16"
android:layout_marginEnd="16dp"
android:fontFamily="@font/montserrat"
android:text="@string/timetrack_account"
android:textAppearance="@style/text_style"
android:textColor="@color/logo_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toEndOf="@id/account_spinner"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/my_toolbar"
app:layout_constraintTop_toBottomOf="@+id/divider2"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/display_acc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="140dp"
android:layout_marginEnd="28dp"
android:text="@string/no_account"
android:textAppearance="@style/text_style"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ToggleButton
<Button
android:id="@+id/button_start_stop"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="55dp"
android:layout_marginStart="@dimen/margin16"
android:layout_marginEnd="@dimen/margin16"
android:layout_marginBottom="@dimen/margin16"
android:background="@drawable/outlined_button_filled"
android:checked="true"
android:background="@color/colorPrimary"
android:enabled="false"
android:text="@string/outside_place"
android:textAppearance="@style/text_style"
android:textOff="@string/stop"
android:textOn="@string/btn_start_text"
android:textColor="@color/logo_white"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -76,16 +90,106 @@
android:id="@+id/account_spinner"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin16"
android:background="@color/logo_white"
android:layout_marginEnd="16dp"
android:background="@color/colorPrimary"
android:foreground="@android:drawable/arrow_down_float"
android:foregroundGravity="right|center_horizontal"
android:textAlignment="textEnd"
app:layout_constraintBottom_toTopOf="@+id/display_acc"
app:layout_constraintBottom_toBottomOf="@+id/selected_acc"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/display_description_layout"
style="@style/LoginTextInputLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin16"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/margin16"
android:colorControlNormal="@color/logo_blue"
android:hint="Description"
android:textColorHint="@color/logo_white"
app:boxBackgroundColor="@color/common_google_signin_btn_text_dark_disabled"
app:boxBackgroundMode="outline"
app:boxCornerRadiusBottomEnd="0dp"
app:boxCornerRadiusBottomStart="0dp"
app:layout_constraintBottom_toTopOf="@+id/display_revenue_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/my_toolbar" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/selected_acc"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/display_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:inputType="textPersonName"
android:lineSpacingExtra="8sp"
android:textAppearance="@style/text_style"
android:textColor="@color/logo_white"
android:textColorHint="@color/logo_white"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/display_revenue_layout"
style="@style/LoginTextInputLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin16"
android:layout_marginEnd="@dimen/margin16"
android:colorControlNormal="@color/logo_blue"
android:hint="Revenue"
android:textColorHint="@color/logo_white"
app:boxBackgroundColor="@color/common_google_signin_btn_text_dark_disabled"
app:boxBackgroundMode="outline"
app:boxCornerRadiusTopEnd="0dp"
app:boxCornerRadiusTopStart="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/display_description_layout"
app:layout_constraintVertical_chainStyle="packed">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/display_revenue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:inputType="textPersonName"
android:lineSpacingExtra="8sp"
android:textAppearance="@style/text_style"
android:textColor="@color/logo_white"
android:textColorHint="@color/logo_white"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/records_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/button_start_stop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_today" />
<TextView
android:id="@+id/text_today"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/today_records"
android:textAppearance="@style/text_style"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/display_revenue_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,77 +5,114 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey"
tools:context=".Login">
tools:context=".activities.Login">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/my_toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_logo"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<EditText
android:id="@+id/setting_input_username"
style="@style/input_field"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_username_layout"
style="@style/LoginTextInputLayoutStyle"
android:layout_width="240dp"
android:layout_height="42dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:colorControlNormal="@color/logo_blue"
android:ems="10"
android:hint="@string/username"
android:inputType="textPersonName"
android:textAlignment="center"
android:textColor="@color/logo_white"
app:layout_constraintBottom_toTopOf="@+id/input_password"
android:textColorHint="@color/logo_white"
app:boxBackgroundColor="@color/common_google_signin_btn_text_dark_disabled"
app:boxBackgroundMode="outline"
app:layout_constraintBottom_toTopOf="@+id/input_password_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.49"
app:layout_constraintVertical_chainStyle="packed" />
app:layout_constraintVertical_chainStyle="packed">
<EditText
android:id="@+id/input_password"
style="@style/input_field"
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:textAppearance="@style/text_style"
android:textColor="@color/logo_white"
android:textColorHint="@color/logo_white" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_password_layout"
style="@style/LoginTextInputLayoutStyle"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="32dp"
android:ems="10"
android:hint="@string/password"
android:inputType="textPassword"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@+id/button_create_account"
android:textColorHint="@color/logo_white"
app:boxBackgroundColor="@color/common_google_signin_btn_text_dark_disabled"
app:boxBackgroundMode="outline"
app:layout_constraintBottom_toTopOf="@+id/button_login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setting_input_username"
app:layout_constraintVertical_bias="0.13" />
app:layout_constraintTop_toBottomOf="@+id/input_username_layout"
app:layout_constraintVertical_bias="0.13">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:textAppearance="@style/text_style"
android:textColor="@color/logo_white"
android:textColorHint="@color/logo_white" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_create_account"
style="@style/Widget.AppCompat.Button"
android:id="@+id/button_login"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_height="55dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
android:background="@drawable/outlined_button_filled"
android:backgroundTint="@color/colorAccent"
android:text="@string/login"
android:textAppearance="@style/text_style"
android:textStyle="bold"
app:cornerRadius="8dp"
app:layout_constraintBottom_toTopOf="@+id/button_register"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.502"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_password" />
app:layout_constraintTop_toBottomOf="@+id/input_password_layout" />
<Button
android:id="@+id/button_register"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:background="@drawable/outlined_button"
android:layout_height="55dp"
android:backgroundTint="@color/colorPrimaryDark"
android:text="@string/register"
android:textAppearance="@style/text_style"
android:textStyle="bold"
app:cornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_create_account"
app:layout_constraintTop_toBottomOf="@+id/button_login"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,7 +6,7 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey"
tools:context=".MainActivity" >
tools:context=".activities.MainActivity" >
<fragment
android:id="@+id/HostFragment"

View File

@ -5,7 +5,23 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey"
tools:context=".Register">
tools:context=".activities.Register">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/my_toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_logo"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<EditText
android:id="@+id/input_password2"
@ -17,12 +33,12 @@
android:hint="@string/confirm_password"
android:inputType="textPassword"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@+id/button_create_account"
app:layout_constraintBottom_toTopOf="@+id/button_login"
app:layout_constraintEnd_toEndOf="@+id/input_email_register"
app:layout_constraintTop_toBottomOf="@+id/input_password" />
app:layout_constraintTop_toBottomOf="@+id/input_password_layout" />
<EditText
android:id="@+id/input_password"
android:id="@+id/input_password_layout"
style="@style/input_field"
android:layout_width="240dp"
android:layout_height="wrap_content"
@ -62,24 +78,26 @@
android:colorControlNormal="@color/logo_blue"
android:ems="10"
android:foregroundTint="@color/colorAccent"
android:hint="@string/username"
android:hint="@string/choose_username"
android:inputType="textPersonName"
android:textAlignment="center"
android:textColor="@color/logo_white"
app:layout_constraintBottom_toTopOf="@+id/input_password"
app:layout_constraintBottom_toTopOf="@+id/input_password_layout"
app:layout_constraintEnd_toEndOf="@+id/input_email_register"
app:layout_constraintTop_toBottomOf="@+id/input_email_register" />
<Button
android:id="@+id/button_create_account"
android:id="@+id/button_login"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin16"
android:layout_marginBottom="175dp"
android:background="@drawable/outlined_button_filled"
android:backgroundTint="@color/logo_blue"
android:text="@string/create_account"
android:textAppearance="@style/text_style"
android:textStyle="bold"
app:cornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/input_email_register"
app:layout_constraintTop_toBottomOf="@+id/input_password2" />

View File

@ -3,17 +3,48 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey">
android:background="@color/background_grey"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/my_toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/abc_vector_test"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button_submit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="@dimen/margin16"
android:layout_marginTop="@dimen/margin16"
android:layout_marginEnd="@dimen/margin16"
android:layout_marginBottom="@dimen/margin16"
android:background="@drawable/outlined_button_filled"
android:text="@string/submit" />
<ScrollView
android:id="@+id/settings_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="638dp">
<LinearLayout
android:layout_width="match_parent"
@ -37,7 +68,7 @@
android:background="@color/colorAccent" />
<EditText
android:id="@+id/setting_input_username"
android:id="@+id/input_username_layout"
style="@style/input_field"
android:layout_width="match_parent"
android:layout_height="42dp"
@ -101,17 +132,8 @@
android:textAlignment="center"
android:textColor="@color/logo_white" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin16"
android:layout_marginTop="@dimen/margin16"
android:layout_marginEnd="@dimen/margin16"
android:layout_marginBottom="@dimen/margin16"
android:background="@drawable/outlined_button_filled"
android:text="@string/submit" />
</LinearLayout>
</ScrollView>
</FrameLayout>
</LinearLayout>

View File

@ -1,13 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Test"
xmlns:android="http://schemas.android.com/apk/res/android">
android:layout_height="42dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="100dp"
android:hint="@string/no_account"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<AutoCompleteTextView
android:id="@+id/filled_exposed_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="59dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
</com.google.android.material.textfield.TextInputLayout>

View File

@ -0,0 +1,55 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/colorPrimary"
app:cardCornerRadius="4dp"
android:layout_marginBottom="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/recyclerText_from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textAlignment="textStart"
android:textAppearance="@style/text_style"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/recyclerText_to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textAlignment="viewStart"
android:textAppearance="@style/text_style"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/recyclerText_total"
app:layout_constraintStart_toEndOf="@+id/recyclerText_from"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/recyclerText_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textAppearance="@style/text_style"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -1,18 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_favorite"
android:icon="@drawable/ic_logo_gt"
android:title="test"
app:showAsAction="ifRoom" />
<item
android:id="@+id/setting"
android:icon="@android:drawable/btn_star"
android:id="@+id/settings"
android:contentDescription="@string/title_activity_settings"
android:title="@string/title_activity_settings"
app:showAsAction="collapseActionView" />
app:showAsAction="never" />
<item
android:id="@+id/logout"
android:title="@string/logout" />
android:contentDescription="@string/logout"
android:title="@string/logout"
app:showAsAction="never" />
</menu>

View File

@ -7,12 +7,12 @@
<fragment
android:id="@+id/login"
android:name="de.hft.geotracker.Login"
android:name="de.hft.geotracker.activities.Login"
android:label="fragment_login"
tools:layout="@layout/activity_login" />
<activity
android:id="@+id/mainActivity"
android:name="de.hft.geotracker.MainActivity"
android:name="de.hft.geotracker.activities.MainActivity"
android:label="activity_home"
tools:layout="@layout/activity_home" />
</navigation>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<color name="colorPrimary">#272727</color>
<color name="colorPrimaryDark">#272727</color>
<color name="colorAccent">#0096ff</color>
<color name="background_grey">#131313</color>
<color name="logo_blue">#0096ff</color>
<color name="logo_white">#EBE7D9</color>
<color name="mtrl_textinput_default_box_stroke_color" tools:override="true">@color/logo_white</color>
</resources>

View File

@ -8,8 +8,10 @@
<string name="btn_start_text">START</string>
<string name="pause">PAUSE</string>
<string name="stop">STOP</string>
<string name="start">START</string>
<string name="username_email">Your Username or E-Mail</string>
<string name="username">Choose a Username</string>
<string name="username">Username</string>
<string name="choose_username">Choose a Username</string>
<string name="email">E-Mail</string>
<string name="password">Password</string>
<string name="login">Login</string>
@ -35,4 +37,7 @@
<string name="create_account">Create Account</string>
<string name="submit">Submit</string>
<string name="logout">Logout</string>
<string name="hello">Hello</string>
<string name="outside_place">Outside your working place</string>
<string name="today_records">Todays Records:</string>
</resources>

View File

@ -1,7 +1,7 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
@ -20,5 +20,10 @@
<item name="android:fontFamily">@font/montserrat</item>
<item name="android:textSize">18sp</item>
</style>
<style name="LoginTextInputLayoutStyle" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="boxStrokeColor">@color/logo_blue</item>
<item name="boxStrokeWidth">2dp</item>
<item name="hintTextColor">@color/logo_white</item>
</style>
</resources>

View File

@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -1,6 +1,6 @@
#Sun Apr 05 19:25:41 CEST 2020
#Tue Jun 02 21:02:45 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

View File

@ -5,7 +5,7 @@ RUN ["gradle", "bootJar"]
FROM openjdk:11-jre-slim
WORKDIR /root
ENV TZ=Europe/Berlin
COPY --from=build /root/build/libs/*.jar app.jar
EXPOSE 5000
ENV PROFILE=prod
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@ -0,0 +1,68 @@
package de.hft.geotime.controllers;
import de.hft.geotime.entities.RecordType;
import de.hft.geotime.entities.TimeRecord;
import de.hft.geotime.entities.projections.RecordOverviewProjection;
import de.hft.geotime.repositories.RecordRepository;
import de.hft.geotime.repositories.TimetrackAccountRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
@RestController
public class RecordController {
private final RecordRepository recordRepository;
private final TimetrackAccountRepository accountRepository;
private final ProjectionFactory projectionFactory;
public RecordController(RecordRepository recordRepository, TimetrackAccountRepository accountRepository, ProjectionFactory projectionFactory) {
this.recordRepository = recordRepository;
this.accountRepository = accountRepository;
this.projectionFactory = projectionFactory;
}
@GetMapping("/track")
public ResponseEntity<RecordOverviewProjection> track(@RequestParam String account, Authentication authentication) {
if (account == null || account.isEmpty()) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
var selectedAccount = accountRepository.findByUser_UsernameAndName(authentication.getName(), account);
if (selectedAccount == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
var entires = recordRepository.findAllByEnddateIsNull(null);
var collect = entires.get()
.filter(Objects::nonNull)
.filter(timeRecord -> selectedAccount.equals(timeRecord.getAccount()))
.findFirst();
var now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
if (collect.isPresent()) {
collect.get().setEnddate(LocalDateTime.parse(now));
recordRepository.save(collect.get());
var projection = projectionFactory.createProjection(RecordOverviewProjection.class, collect.get());
return new ResponseEntity<>(projection, HttpStatus.OK);
} else {
var newRecord = new TimeRecord();
newRecord.setType(RecordType.PAID);
newRecord.setStartdate(LocalDateTime.parse(now));
newRecord.setAccount(accountRepository.findByUser_UsernameAndName(authentication.getName(), account));
recordRepository.save(newRecord);
var projection = projectionFactory.createProjection(RecordOverviewProjection.class, newRecord);
return new ResponseEntity<>(projection, HttpStatus.CREATED);
}
}
}

View File

@ -4,6 +4,7 @@ import de.hft.geotime.entities.TimetrackUser;
import de.hft.geotime.entities.projections.UserAllEmbeddedProjection;
import de.hft.geotime.repositories.TimetrackUserRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
@ -11,11 +12,12 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@RestController
@BasePathAwareController
@ResponseBody
public class UserController {
private final TimetrackUserRepository userRepository;

View File

@ -3,10 +3,12 @@ package de.hft.geotime.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
import java.time.Duration;
import java.util.Date;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
@Data
@NoArgsConstructor
@ -17,11 +19,25 @@ public class TimeRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@OneToOne(fetch = FetchType.LAZY)
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
private TimetrackAccount account;
private Date startdate;
private Date enddate;
private Duration time;
@Column(columnDefinition = "TIMESTAMP")
private LocalDateTime startdate;
@Column(columnDefinition = "TIMESTAMP")
private LocalDateTime enddate;
private RecordType type;
public long getDuration() {
if (enddate == null) {
return 0;
} else {
return startdate.until(enddate, ChronoUnit.MINUTES);
}
}
}

View File

@ -3,6 +3,8 @@ package de.hft.geotime.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
@ -18,7 +20,9 @@ public class TimetrackAccount {
private double revenue;
private String name;
private String description;
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
private TimetrackUser user;
}

View File

@ -6,7 +6,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.List;
@Data
@NoArgsConstructor
@ -28,12 +27,9 @@ public class TimetrackUser {
private String lastname;
@OneToOne(fetch = FetchType.EAGER)
@ManyToOne(fetch = FetchType.EAGER)
private Role role;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<TimetrackAccount> accounts;
@ManyToOne
private Location location;

View File

@ -0,0 +1,27 @@
package de.hft.geotime.entities.projections;
import de.hft.geotime.entities.TimeRecord;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.core.config.Projection;
import java.time.LocalDateTime;
@Projection(name = "overview", types = TimeRecord.class)
public interface RecordOverviewProjection {
LocalDateTime getStartdate();
LocalDateTime getEnddate();
long getDuration();
@Value("#{target.type.name()}")
String getType();
@Value("#{target.account.name}")
String getAccount();
@Value("#{target.account.user.username}")
String getUsername();
}

View File

@ -1,8 +1,58 @@
package de.hft.geotime.repositories;
import de.hft.geotime.entities.TimeRecord;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.format.annotation.DateTimeFormat;
public interface RecordRepository extends CrudRepository<TimeRecord, Long> {
import java.time.LocalDateTime;
@RepositoryRestResource(
path = "records",
itemResourceRel = "records",
collectionResourceRel = "records"
)
public interface RecordRepository extends PagingAndSortingRepository<TimeRecord, Long> {
@RestResource(rel = "allBetween", path = "allBetween")
Page<TimeRecord> findAllByStartdateBetween(
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime start,
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime end,
Pageable pageable
);
@RestResource(rel = "allBetweenAndUser", path = "allBetweenAndUser")
Page<TimeRecord> findAllByStartdateBetweenAndAccount_User_Username(
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime start,
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime end,
String username,
Pageable pageable
);
@RestResource(rel = "allForUser", path = "allForUser")
Page<TimeRecord> findAllByAccount_User_Username(String username, Pageable pageable);
@RestResource(rel = "allForUserAndAccount", path = "allForUserAndAccount")
Page<TimeRecord> findAllByAccount_User_UsernameAndAccount_Name(String username, String account, Pageable pageable);
@RestResource(rel = "allFrom", path = "allFrom")
Page<TimeRecord> findAllByStartdateGreaterThanEqual(
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime date,
Pageable pageable
);
@Query("SELECT record from TimeRecord record " +
"where record.account.user.username = :#{principal} " +
"AND record.enddate > current_date " +
"AND record.enddate < current_date+1"
)
Page<TimeRecord> today(Pageable pageable);
@RestResource(rel = "openEntries", path = "openEntries")
Page<TimeRecord> findAllByEnddateIsNull(Pageable pageable);
}

View File

@ -1,8 +1,11 @@
package de.hft.geotime.repositories;
import de.hft.geotime.entities.TimetrackAccount;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
@RepositoryRestResource(
path = "accounts",
@ -11,4 +14,10 @@ import org.springframework.data.rest.core.annotation.RepositoryRestResource;
)
public interface TimetrackAccountRepository extends PagingAndSortingRepository<TimetrackAccount, Long> {
@RestResource(rel = "findByUsernameAndName", path = "findByUsernameAndName")
TimetrackAccount findByUser_UsernameAndName(String username, String account);
@RestResource(rel = "findByUsername", path = "findByUsername")
Page<TimetrackAccount> findAllByUser_Username(String username, Pageable pageable);
}

View File

@ -53,6 +53,7 @@ public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilte
HttpServletResponse res,
FilterChain chain,
Authentication auth) {
res.setHeader("Access-Control-Expose-Headers", "Authorization");
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))

View File

@ -0,0 +1,14 @@
package de.hft.geotime.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.spel.spi.EvaluationContextExtension;
@Configuration
class SecurityConfiguration {
@Bean
EvaluationContextExtension securityExtension() {
return new SecurityEvaluationContextExtension();
}
}

View File

@ -0,0 +1,21 @@
package de.hft.geotime.security;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class SecurityEvaluationContextExtension implements EvaluationContextExtension {
@Override
public String getExtensionId() {
return "security";
}
@Override
public SecurityExpressionRoot getRootObject() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return new SecurityExpressionRoot(authentication) {
};
}
}

View File

@ -43,8 +43,10 @@ public class WebSecurity extends WebSecurityConfigurerAdapter {
@Bean
CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
configuration.addAllowedMethod("*");
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@ -2,4 +2,4 @@ server.port=5000
spring.datasource.hikari.initialization-fail-timeout=0
spring.datasource.hikari.max-lifetime=300000
spring.jpa.show-sql=true
spring.profiles.active=${PROFILE:dev}
spring.profiles.active=prod

View File

@ -20,8 +20,22 @@ INSERT INTO timetrack_user (id, firstname, lastname, password, username, role_id
(3, 'Tim', 'Zieger' ,'$2y$10$pYGHZhoaelceImO7aIN4nOkWJBp.oqNGFYaRAonHkYF4u9ljqPelC', 'ziti', 1, 1),
(4, 'Simon', 'Kellner' ,'$2y$10$Puzm/Nr/Dyq3nQxlkXGIfubS5JPtXJSOf2e6mrQ6HhVYQN9YiQQsC', 'kesi', 1, 1);
INSERT INTO timetrack_account (description, `name`, revenue, user_id) VALUES
('Gleitzeit Marcel', 'Primary Marcel', 16.0, 1),
('Festgeld Marcel', 'Secondary Marcel', 25.0, 1);
INSERT INTO timetrack_account (id, description, `name`, revenue, user_id) VALUES
(1, 'Gleitzeit Marcel', 'Primary', 16.0, 1),
(2, 'Festgeld Marcel', 'Secondary', 25.0, 1),
(3, 'Festgeld Tim', 'Primary', 25.0, 3);
INSERT INTO time_record (id, enddate, startdate, `type`, account_id) VALUES
(1, '2020-05-10 16:00:00', '2020-05-10 12:00:00', 0, 1),
(2, '2020-05-09 16:00:00', '2020-05-09 12:00:00', 1, 1),
(3, '2020-05-20 16:00:00', '2020-05-20 00:00:00', 1, 2),
(4, '2020-05-11 16:00:00', '2020-05-11 12:00:00', 1, 1),
(5, '2020-05-30 16:00:00', '2020-05-30 12:00:00', 1, 1),
(6, '2020-05-30 23:00:00', '2020-05-30 22:00:00', 1, 1),
(7, '2020-05-31 01:00:00', '2020-05-31 00:00:00', 1, 1),
(8, '2020-05-31 04:00:00', '2020-05-31 02:00:00', 1, 1),
(9, '2020-05-31 16:00:00', '2020-05-31 12:00:00', 1, 1),
(10, '2020-06-14 16:00:00', '2020-06-14 12:00:00', 1, 1),
(11, '2020-06-15 16:00:00', '2020-06-15 12:00:00', 1, 1);
SET FOREIGN_KEY_CHECKS=1;

View File

@ -17,8 +17,6 @@ services:
context: ./backend
ports:
- "5000:5000"
environment:
PROFILE: prod
depends_on:
- db

View File

@ -24,6 +24,121 @@
\usepackage{wrapfig}
\newcommand*{\source}[1]{\par\raggedleft\footnotesize Quelle:~#1} %source command fuer bildunterschriften
% Code
\usepackage{listings}
\usepackage{xcolor}
\definecolor{dkgreen}{rgb}{0,0.6,0}
\definecolor{gray}{rgb}{0.5,0.5,0.5}
\definecolor{mauve}{rgb}{0.58,0,0.82}
\lstset{literate=%
{Ö}{{\"O}}1
{Ä}{{\"A}}1
{Ü}{{\"U}}1
{ß}{{\ss}}1
{ü}{{\"u}}1
{ä}{{\"a}}1
{ö}{{\"o}}1
}
\lstset{
frame=tblr,
frameround=tttt,
aboveskip=3mm,
belowskip=3mm,
showstringspaces=false,
columns=flexible,
basicstyle={\small\ttfamily},
numbers=none,
numberstyle=\tiny\color{gray},
keywordstyle=\color{blue},
commentstyle=\color{dkgreen},
stringstyle=\color{mauve},
breaklines=true,
breakatwhitespace=true,
tabsize=3,
xleftmargin=1.0ex,
xrightmargin=1.0ex
}
\lstdefinelanguage{docker}{
keywords={FROM, RUN, COPY, ADD, ENTRYPOINT, CMD, ENV, ARG, WORKDIR, EXPOSE, LABEL, USER, VOLUME, STOPSIGNAL, ONBUILD, MAINTAINER},
keywordstyle=\color{blue}\bfseries,
identifierstyle=\color{black},
sensitive=false,
comment=[l]{\#},
commentstyle=\color{purple}\ttfamily,
stringstyle=\color{red}\ttfamily,
morestring=[b]',
morestring=[b]",
}
\lstdefinelanguage{docker-compose}{
keywords={VERSION, SERVICES, CONTAINER\_NAME, BUILD, CONTEXT, PORTS, DEPENDS\_ON, RESTART, VOLUMES, ENVIRONMENT},
keywordstyle=\color{blue}\bfseries,
identifierstyle=\color{black},
sensitive=false,
comment=[l]{\#},
commentstyle=\color{purple}\ttfamily,
stringstyle=\color{red}\ttfamily,
morestring=[b]',
morestring=[b]",
}
\lstdefinelanguage{Kotlin}{
comment=[l]{//},
commentstyle={\color{gray}\ttfamily},
emph={delegate, filter, first, firstOrNull, forEach, lazy, map, mapNotNull, println, return@},
emphstyle={\color{mauve}},
identifierstyle=\color{black},
keywords={abstract, actual, as, as?, break, by, class, companion, continue, data, do, dynamic, else, enum, expect, false, final, for, fun, get, if, import, in, interface, internal, is, null, object, override, package, private, public, return, set, super, suspend, this, throw, true, try, typealias, val, var, vararg, when, where, while},
keywordstyle={\color{blue}\bfseries},
morecomment=[s]{/*}{*/},
morestring=[b]",
morestring=[s]{"""*}{*"""},
ndkeywords={@Deprecated, @JvmField, @JvmName, @JvmOverloads, @JvmStatic, @JvmSynthetic, Array, Byte, Double, Float, Int, Integer, Iterable, Long, Runnable, Short, String},
ndkeywordstyle={\color{orange}\bfseries},
sensitive=true,
stringstyle={\color{dkgreen}\ttfamily},
}
\colorlet{punct}{red!60!black}
\definecolor{delim}{RGB}{20,105,176}
\colorlet{numb}{magenta!60!black}
\lstdefinelanguage{json}{
basicstyle=\normalfont\ttfamily,
stepnumber=1,
numbersep=8pt,
showstringspaces=false,
breaklines=true,
literate=
*{0}{{{\color{numb}0}}}{1}
{1}{{{\color{numb}1}}}{1}
{2}{{{\color{numb}2}}}{1}
{3}{{{\color{numb}3}}}{1}
{4}{{{\color{numb}4}}}{1}
{5}{{{\color{numb}5}}}{1}
{6}{{{\color{numb}6}}}{1}
{7}{{{\color{numb}7}}}{1}
{8}{{{\color{numb}8}}}{1}
{9}{{{\color{numb}9}}}{1}
{:}{{{\color{punct}{:}}}}{1}
{,}{{{\color{punct}{,}}}}{1}
{\{}{{{\color{delim}{\{}}}}{1}
{\}}{{{\color{delim}{\}}}}}{1}
{[}{{{\color{delim}{[}}}}{1}
{]}{{{\color{delim}{]}}}}{1},
}
\lstdefinelanguage{JavaScript}{
keywords={typeof, new, true, false, catch, function, return, null, catch, switch, var, if, in, while, do, else, case, break},
keywordstyle=\color{blue}\bfseries,
ndkeywords={class, export, boolean, throw, implements, import, this},
ndkeywordstyle=\color{darkgray}\bfseries,
identifierstyle=\color{black},
sensitive=false,
comment=[l]{//},
morecomment=[s]{/*}{*/},
commentstyle=\color{purple}\ttfamily,
stringstyle=\color{red}\ttfamily,
morestring=[b]',
morestring=[b]"
}
\usepackage{setspace}
\setstretch{1.2} %Zeilenabstand
\setlength\parindent{0pt} %keine Paragrapheneinrueckung
@ -51,20 +166,46 @@
\include{parts/titlepage}
\include{parts/abstract}
\tableofcontents
\listoffigures
\lstlistoflistings
\include{parts/einleitung}
\include{parts/projektidee}
\include{parts/projektplanung}
\include{parts/design}
\include{parts/entwicklungsumgebung}
\include{parts/dev-setup}
\include{parts/backend}
\include{parts/projektbericht}
\include{parts/frontend}
\include{parts/android}
\chapter{Vollständiger Application Stack}
\begin{figure}[H]
\centering
\includegraphics[width=0.9\linewidth]{img/ApplicationStack}
\caption{Application Stack}
\end{figure}
Das Deployment der Geo Timetracking Application ist in drei große Schichten aufgeteilt. Zunächst wäre hier die Backend Schicht, die Schicht der Datenhaltung und der API. Dieser Teil der Anwendung braucht am meisten Schutz, da er der wichtigste ist und dort alle Daten gespeichert werden. Der Zugriff auf die Datenbank ist nur auf das Backend beschränkt. Um nun die Applikation zu Nutzen gibt es zwei Möglichkeiten: Eine Android App oder ein Webbrowser.\\
Die Android App implementiert die View Schicht selbst und fragt nur für Daten den Backend-Dienst an. Diese Anfragen gehen zunächst an den Server, der die App hostet und werden dann von dem darauf laufenden Docker Deamon an den entsprechenden Container weitergeleitet.\\
Beim Zugriff über den Webbrowser funktioniert die Kommunikation geringfügig anders. Zunächst wird vom Client der nginx Container nach dem statischen Teil der Website gefragt, dieser lädt dann über ähnliche Anfragen wie in der Android App die Daten vom Backend. Das global gesprochene Protokoll ist hierbei immer HTTP.
\chapter{Projektjournal}
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/Clockify_Summary-Overview.pdf}
\end{figure}
Dieser Report zeigt eine Übersicht der geleisteten Arbeit jedes Gruppenmitglieds. Der vollständige Report ist separat angehängt. Dort kann jede Aktivität auf Issue Ebene genau nachvollzogen werden. Da lediglich die Issuenummern angegeben wurden, können die eigentlich dahinter liegenden Aufgaben auf GitLab\footnote{\url{https://gitlab.com/marcel.schwarz/2020ss-qbc-geofence-timetracking/-/issues?scope=all\&utf8=\%E2\%9C\%93\&state=all}} eingesehen werden.
\chapter{Projektfazit und Ausblick}
Bei dem Projekt im Rahmen von Ubiquitous/Pervasive Computing konnten wir Bekanntes anwenden und Neues lernen. Wir alle konnten uns gut einbringen und zusammen auf unser gemeinsames Ziel hinarbeiten. Im Rückblick auf die vergangenen fünf Sprints lässt sich sagen, dass diese erfolgreich verlaufen sind. Die Verteilung der Aufgaben war gleichmäßig und funktionierte reibungslos. Die Idee des Projekts konnte vollständig umgesetzt werden, zudem konnten anfangs nicht geplante Features umgesetzt werden. Hierzu zählen z.B. tagesübergreifende Time Records. Wir alle sind mit dem Ergebnis unserer Arbeit zufrieden und können das Projekt als erfolgreich bezeichnen.\\
Ebenso sehen wir ein großes Potential in der Weiterentwicklung unseres Endprodukts. Hier haben wir Ideen wie: Zuordnung der Benutzer in Gruppen, Benutzerprofile mit Daten über den Benutzer und dessen Tätigkeit oder auch Zuweisung von Kernarbeitszeit und Zeitrahmen, um Timetracking nur in einem festgelegten Zeitfenster zu erlauben. Mit ein paar Verbesserungen könnte unser Produkt von kleinen Unternehmen verwendet werden, die ein auf Vertrauen basiertes Zeitmeldesystem suchen.
\end{document}

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2020-06-09T17:16:25.356Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36" etag="hh3mw3zn5PIgEuO83UB7" version="13.2.0" type="device"><diagram id="5YaEyfn9u8j8nLohUPBC" name="Page-1">7Vpbc6M2FP41fqwHkAH7MbazbWeS6e54urvpS0cGBbQLiBFyYu+vrwTiIoED6xg7TZvMeNBBCOk753znYk/AKt7/SmEa3hMfRRPL8PcTsJ5YluO6/FMIDoUAOKAQBBT7hcisBRv8A0mhIaU77KNMmcgIiRhOVaFHkgR5TJFBSsmzOu2RROpbUxiglmDjwagt/YJ9FhbSueXW8t8QDsLyzaazKO7EsJwsT5KF0CfPDRG4nYAVJYQVV/F+hSKBXYlL8dyHI3erjVGUsCEPfDLvAXn8/QtdeQ6z/1qyb5//+MUpVnmC0U4eWG6WHUoEKNklPhKLGBOwfA4xQ5sUeuLuM1c5l4UsjvjI5JePOIpWJCKUjxOS8ElL+QZEGdof3bpZAcINCZEYMXrgU+QDMwmhtKGFHD7XCpnNpSxsKAPYUgilEQTVyjVO/EJC9ROwlTCNiVvGKPmOSuHEAu5c/J8HUdNWITVBG9MKPgVTYyxMzfm/whYrACRylX83kDO7rNEaDznQAgr5nMXkkFAWkoAkMLqtpUsVynrOHSGpBPAbYuwgKRnuGFHh5XjRw1f5fD54EIOpa5fj9b55d32Qo2KzYocvq4AfiOyoh144uTw4gzRArNdf2zqlKIIMP6kbObt+QL9hwywtotcj3gulLFOCE4bo7RPHJJOIVxFEoOjDLKzU11CLShvS7iO4RdFHkmGGScLFHhKL8xvCFTCPdnfahC1hjMSNCTcRDsQNJqxjCeWoWqfpaxPhI+Z8vi62nIojxvtApAfTOPMgmuZPpRRnaAoTnxLs/52GYqevd09X9U7QxWsdzjlapHBbuud7ZjjmZ3VgLLBMtlmae4Vxk6YRxzpXgW4gHBE2QNEdnNfSla7SGPt+TgldfKrSxBkUVPHlQVVYkz6tTvocK5a7PWHmFG7cY5ZT49SWo4fGnZoXxaCkxWN82ken/VR/RsJdDCTcIphfi3AXLaf7zC8s4wMl3AcS/y3mFabGXObMHeYYzmhpxey6aUVt+Q+K4Y+eVZS1Z7+VH1HpZay83GbDzDc8qCYBly2h9/2NGjrQSg9e2Fzb0M0WkPeQYrgWQXnz6Y5/riGDW5ihNwmonvN0MEdXQTJeddzGs80kiX8j2jMiMYlglmFvWHXRGw3LyNuIu3UUPhJ5f5LVzkk186FUY3TbQEPHXeV6KRvMSPINH0X50QhOjmpiMz0bKw4qn2o2obSFqr7eQbPdcqECiNZC3FbgoTEtL4+y4xuu8v7yPXpzrGe+5biaGxQ7qJ2i0sEr/KRdGK4J523KZX/GAdruBJe/20LAtbhfKimP2VELGHbbqserBWYtjVSR1Njstgli71chQGuzVg0rRR0XLc0GtAR7A0lnQHB7IsKJ0UfVSX9cQdE233mp+PMGlxKtk6u1CwWXhRZc9Ebp4OCiGXCr/3+m4GLpwcW2X97Xy8FonOBSvvNI9ZZb3kX6wRdsB1tD2xPWVfvBVrs/0dLVf6UhDH/sKJp6JE534sHXRzEtU7Vst8wyethsZo+kbtARtN67J4Lyu/q37YlgQKX6vyeexxNNx7qyHw74Hu60bHJoMjm1lJam2eOP/flj6zcCRv43ZsdicFK5uGpOqffEzFNzSktvDIyUU+o/K5i/Kkfkw/qXRcX0+udZ4PYf</diagram></mxfile>

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@ -1,20 +0,0 @@
\begin{abstract}
{\huge Checkliste Lieferobjekte}
\begin{itemize}
\item Ein Journal (z.B. Excel in Dropbox, Office 365, Google), in dem jedes Projektmitglied Tages- und Themenspezifisch seine geleisteten Arbeiten mit Aufwandsangabe listet. Z.b. "Hans 12.4 3 Std Backend Coding"- Das Journal muss zeitnah geführt werden und verfügbar sein. Ein unvollständiges Zeitjournal führt zur Abwertung in der Benotung.
\item Projektdokumentation (*: ca. 15-25 Seiten, je nach Bilderdichte)
\begin{itemize}
\item Source Code und alle erstellten Dokumente
\item Erläuterung des Designs (*)
\item Einrichtung der Entwicklungsumgebung (*) \\
(falls anwendbar, Standarddinge wie Eclipse, Android SDK müssen nicht detailliert erwähnt werden):
\begin{itemize}
\item Welche Frameworks wurden für die Entwicklung benutzt und was gab es bei der Konfiguration / Installation dieser Tools zu beachten?
\item Wie wurden die Zielanwendungen erzeugt und z.B. auf ein Zielgerät oder Server übertragen und dort aktiviert?
\end{itemize}
\item Projektbericht: Wie sind Sie vorgegangen, was wollten Sie erreichen, was haben Sie gelernt, was gab es für Probleme, was haben Sie erreicht, wer hat was gemacht?
\end{itemize}
\item Datei mit der Präsentation
\end{itemize}
\end{abstract}

View File

@ -0,0 +1,251 @@
\chapter{Android-App}
\section{Technologiebeschreibung}
\subsection{Android SDK}
Die Android-Entwicklung wurde, aufgrund der Ausgereiftheit und den Emulatoren, mit Android-Studio realisiert. Android-Studio verwaltet auch das SDK und unterstützt beim aktuell Halten der Bibliotheken.\\
Das minimale API-Level, welches das Endgerät haben darf, wurde auf 23 "Marshmallow" festgelegt. Dadurch werden ca. 85\% der Geräte unterstützt und ist aktuell genug um gewisse Features, wie das neue Berechtigungssystem, zu unterstützen. Die Zielversion ist das aktuelle Android 10 mit API-Level 29. In dieser Version wurden erneut Berechtigungen geändert, wodurch im Code einige Anpassungen gemacht werden mussten (siehe: \ref{sec:Probleme}).
\subsection{Kotlin}
Die Entscheidung fiel auf Kotlin als Programmiersprache, da die Sprache von Google für die Entwicklung von Android-Apps bevorzugt wird. Außerdem bietet dies die Gelegenheit, eine neue Programmiersprache zu erlernen. Dadurch musste jedoch viel Zeit investiert werden um zum einen die Sprache und zum anderen die Entwicklungsumgebung, sowie den Aufbau einer Android-App zu lernen. Dafür wurden zwei von insgesamt fünf Sprints eingeplant, weswegen die App nur die Grundfunktionen besitzt.
Die aktuellste Kotlin-Version zur Zeit der Fertigstellung ist 1.3.72.
\subsection{Retrofit}
Für die Kommunikation mit dem Backend wurde die Bibliothek Retrofit in der Version 2.8.1 verwendet. Retrofit ist ein HTTP-Client für Android, mit dem man REST-Endpunkte simpel ansprechen kann. Zusammen mit der Gson-Bibliothek lassen sich JSON-Nachrichten senden und empfangen.
Das angefragte API wird mit Klassen und Methoden in der Anwendung modelliert. Dadurch ist es möglich, nur die Felder abzufragen, welche auch benötigt werden. Genaueres in Kapitel \ref{subsec:AnzeigeDaten}.
\subsection{Material Design}
Material ist eine Bibliothek, die Komponenten und Richtlinien bereitstellt. Nach einmaligem Einbinden der Bibliothek können die Komponenten verwendet werden, indem der Komponente der Style zugewiesen wird.
\section{Farbschema und Designsprache}
In einem gemeinsamen Meeting mit dem Web-Frontend einigten wir uns auf Farbcodes, die auf beiden Oberflächen verwendet werden. So haben wir uns auf ein dunkles Schema festgelegt, mit den Farben aus dem Logo für Schrift und Akzente. Als Schriftart wird Montserrat verwendet (siehe: Abbildungen \ref{Abb:login} - \ref{Abb:settings}).
\section{Umsetzung}
\subsection{Design der Activities}
Insgesamt besitzt die App die vier Activities: Login, MainActivity, Register und Settings. Wobei die Register- und die Settings-Activity aus zeitlichen Gründen ohne Funktion sind. Sie haben auch noch die alten unschönen Eingabefelder, sind aber für die Funktion der gesamten Anwendung nicht sonderlich relevant, weshalb entschieden wurde, diese zu vernachlässigen und den Fokus auf die Funktionalität zu legen.\\
Jeder Bildschirm hat eine Top-Bar auf der, je nachdem auf welchem Bildschirm man sich befindet, unterschiedliche Inhalte angezeigt werden. Beim Einloggen und Account erstellen wird außer dem Logo und dem Namen der App nichts angezeigt. In den Einstellungen erscheint anstatt des Logos ein Zurück-Button und auf dem Hauptbildschirm gibt es ein Menü zum Ausloggen und um zu den Einstellungen zu gelangen.
\begin{figure}[H]
\centering
\begin{minipage}[b]{0.4\linewidth}
\includegraphics[width=\linewidth]{img/android/login}
\caption{Login Activity}
\label{Abb:login}
\end{minipage}
\hspace{.1\linewidth}
\begin{minipage}[b]{0.4\linewidth}
\includegraphics[width=\linewidth]{img/android/register}
\caption{Register Activity}
\end{minipage}
\end{figure}
Links die Eingabefelder mit Material Design und rechts die alten, selbst erstellten.
\begin{figure}[H]
\centering
\begin{minipage}[b]{0.4\linewidth}
\includegraphics[width=\linewidth]{img/android/main}
\caption{Main Activity}
\label{Abb:main}
\end{minipage}
\hspace{.1\linewidth}
\begin{minipage}[b]{0.4\linewidth}
\includegraphics[width=\linewidth]{img/android/settings}
\caption{Settings Activity}
\label{Abb:settings}
\end{minipage}
\end{figure}
Wie zu erkennen ist lag, der Fokus der Implementierung deutlich auf der Main Activtiy, da sie auch das Wichtigste der App beinhaltet. Prominent ist dabei der "START"-Knopf an der Unterseite, mit dem die Aufzeichnung gestartet werden kann (genaueres im Kapitel \ref{subsec:main}).
\subsection{Authentifizierung}
Zur Authentifizierung benutzen wir JWT, welches bei jeder Anfrage ans Backend mit geschickt werden muss. Das Token erhält man beim Einloggen mit den richtigen Daten und muss persistiert werden, bis sich der Benutzer ausloggt. Dazu wird das Token im privaten Speicher der App gespeichert. In allen weiteren Activities kann dann auf den Speicher zugegriffen werden und das Token beim Erstellen des "AuthenticationInterceptor"s mitgegeben werden. Beim Ausloggen wird einfach die Datei mit dem Token aus dem Speicher gelöscht.
Der "AuthenticationInterceptor" ist Kind von der "Interceptor"-Klasse aus der "okhttp3"-Bibliothek, welche in Retrofit eingebunden ist. Mithilfe des Interceptors können REST-Aufrufen Header-Daten mitgegeben werden. In unserem Fall ist das das "Authorization"-Feld mit dem Token.
\begin{lstlisting}[language=Kotlin,caption=AuthenticationInterceptor]
class AuthenticationInterceptor(pToken: String) : Interceptor {
private val token = pToken
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val builder = original.newBuilder()
.header("Authorization", token)
val request = builder.build()
return chain.proceed(request)
}
}
\end{lstlisting}
Der Interceptor wird dem HTTP-Client hinzugefügt, welcher später bei der Erzeugung des Retrofit-Builders notwendig ist.
\begin{lstlisting}[language=Kotlin,caption=HTTP Client]
val httpClient = OkHttpClient.Builder()
val interceptor = AuthenticationInterceptor(token)
httpClient.addInterceptor(interceptor)
\end{lstlisting}
\subsection{Anzeige der Daten in der Main Activity}\label{subsec:AnzeigeDaten}
Die Daten werden per REST-Aufruf mithilfe vom Retrofit-Framework vom Backend geholt. Um Anfragen zu senden, benötigt man einen Retrofit-Builder. Diesem wird die anzufragende URL, ein JSON-Konverter und ein HTTP-Client mitgegeben. Aus diesem Builder und einer Service-Klasse, in der die Methoden definiert sind, wird ein Objekt erzeugt, mit dem die Methoden aufrufbar sind.
\begin{lstlisting}[language=Kotlin,caption=Retrofit Builder]
val builder = Retrofit.Builder()
.baseUrl("http://plesk.icaotix.de:5000")
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
val retrofit = builder.build()
service = retrofit.create(GeofenceService::class.java)
\end{lstlisting}
Die Klasse "GeofenceService" dient, wie oben beschrieben, zur Definition der Endpunkte in Form von Methodenaufrufen. Dort wird definiert, ob es ein "POST"- oder "GET"-Entpunkt ist, wie der Pfad lautet und was für Parameter mitgegeben werden.
\begin{lstlisting}[language=Kotlin,caption=GeofenceService]
@POST("/login")
fun login(@Body login_data: ValuesUserLogin): Call<Void>
@GET("whoami")
fun getUser(): Call<ValuesUser>
@GET("accounts/search/findByUsername")
fun getAccounts(@Query("username") username : String): Call<EmbeddedAccounts>
\end{lstlisting}
Der Rückgabewert der Methoden ist immer vom Typ "Call". Wenn aus dem Body Werte gelesen werden sollen, muss eine Art Skelett-Klasse angelegt werden mit den, für die Anwendung relevanten, Feldern. Die Klasse "ValuesUser" stellt Werte der Antwort bereit, wie z. B. den Vornamen.
\begin{lstlisting}[language=Kotlin,caption=ValuesUser]
class ValuesUser(firstname: String) {
@SerializedName("firstname")
var firstname = firstname
}
\end{lstlisting}
Der Aufruf der Methode erfolgt asynchron. Deshalb darf sich nicht auf das Ergebnis des Aufrufs direkt danach verlassen werden, sonst bekommt man eine Null-Pointer-Excetion. Die Methode "enqueue" besitzt ein Callback-Objekt als Parameter, welches "onResponse" und "onFailure" überschreibt. Dort wird entsprechend definiert, was in den jeweiligen Fällen ausgeführt werden soll.
\begin{lstlisting}[language=Kotlin,caption=Callback der "getUser" Funktion]
val call = service.getUser()
call.enqueue(object : Callback<ValuesUser> {
override fun onResponse(call: Call<ValuesUser>, response: Response<ValuesUser>) {
if (response.isSuccessful) {
val firstname = response.body()?.firstname
lbl_username.text = "Hello " + firstname
} else {
println("Response not successful: ${response.code()}")
}
}
override fun onFailure(call: Call<ValuesUser>, t: Throwable) {
println("Response 'whoami' failed. " + t.message)
}
})
\end{lstlisting}
\bigskip
In dieser Art und Weise werden alle Anfragen ans Backend gehandhabt. Dazu zählen:
\begin{itemize}
\item Abfragen der Location-Daten zu dem Benutzer für den Geofence
\item Befüllen des Dropdown-Menüs mit den Timetrack-Accounts des Benutzers
\item Anzeigen der Beschreibung und der Vergütung
\item Befüllen des RecyclerViews mit den heutigen Einträgen
\item Auslösen des Start-/Stopp-Events
\item Einloggen
\end{itemize}
\subsection{Geofencing}\label{subsec:geofence}
Die Geofencing-Funktion ist die zentrale Funktion für die App und auch für das gesamte Projekt. Deshalb war es wichtig, dass sie frühzeitig funktioniert.\\
Um die Position eines Gerätes zu bestimmen, bedarf es einer Berechtigung, die vom Benutzer bestätigt werden muss. Für Geräte mit API-Level 28 und niedriger muss dafür die \linebreak "ACCESS\_FINE\_LOCATION"-Berechtigung gesetzt werden und für API-Level 29 und höher "ACCESS\_BACKGROUND\_LOCATION".\\
Der Geofence wird initialisiert, wenn für den Benutzer Geo-Daten gespeichert sind. Ist dies der Fall, so wird ein "GeofencingClient" angelegt, dem dann der Geofence hinzugefügt wird. Der Geofence wird erzeugt mit den Parametern: Breitengrad, Längengrad, Radius, der Lebenszeit des Fence und den Übergangstypen. Die Typen sind in unserem Fall "GEOFENCE\_TRANSITION\_ENTER" und "GEOFENCE\_TRANSITION\_EXIT", da wir immer reagieren wollen, wenn der Nutzer den Bereich verlässt oder betritt.
\begin{lstlisting}[language=Kotlin,caption=Anlegen des Geofencing Clients]
geofencingClient = LocationServices.getGeofencingClient(this)
geofence = Geofence.Builder().setRequestId("Geofence")
.setCircularRegion(lat, long, rad)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
\end{lstlisting}
Um den Geofence-Client zu starten wird auf das Objekt die "addGeofences"-Methode ausgeführt mit einem "GeofencingRequest"-Objekt und einem "PendingIntent"-Objekt als Parameter.
\begin{lstlisting}[language=Kotlin,caption="addGeofences" Methode]
geofencingClient.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
addOnSuccessListener { ... }
addOnFailureListener { ... }
}
\end{lstlisting}
In der "getGeofencingRequest"-Methode wird festgelegt, auf welches initiale Event reagiert werden soll und der oben erstellte Geofence wird hinzugefügt. Als initiales Event haben wir "INITIAL\_TRIGGER\_ENTER" gewählt, da es ausgelöst wird, wenn man sich bereits im Bereich befindet und die App startet. Denn erst mit dem Eintrittsevent wird der Button zum Starten der Aufzeichnung freigeschaltet. Das "geofencePendingIntent" definiert die BroadcastReceiver-Klasse, welche bei jedem Event aufgerufen wird.
\begin{lstlisting}[language=Kotlin,caption=Setzen der Geofence Trigger]
private fun getGeofencingRequest(): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofence(geofence)
}.build()
}
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
\end{lstlisting}
Die "GeofenceBroadcastReceiver"-Klasse definiert, was bei den jeweiligen Events ausgeführt werden soll. In unserem Fall ist dies das Verändern einer boolean Shared-Prefrences-Variable, je nachdem ob der Bereich betreten oder verlassen wurde. Warum diese Art und Weise gewählt wurde lesen Sie in Kapitel \ref{sec:Probleme}. Das Code-Beispiel zeigt die Aktion beim Betreten des Bereichs.
\begin{lstlisting}[language=Kotlin,caption=Ändern der Shared-Preferences]
context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
?.edit()
?.putBoolean("ENABLED", true)
?.apply()
\end{lstlisting}
In der "MainActivity" wird ein Listener für diese Shared-Prefrences-Variable definiert. Je nachdem, zu welchem Wert sich die Variable ändert, wird der Start/Stopp-Button freigeschaltet oder gesperrt. Wenn der Benutzer den Bereich verlässt, aber noch aufzeichnet, wird dadurch die Aufzeichnung automatisch gestoppt und gespeichert.
\section{Funktionen der App}
Wie oben beschrieben, besteht die Android-App aus vier Activities. Die Register- und Settings-Activity sind aus zeitlichen Gründen ohne Funktion und layouttechnisch nicht überarbeitet. Der Fokus lag stark auf der Main-Activity, die das Kernstück der App darstellt. Im Folgenden die Funktionalitäten der Activities Login und Main.
\subsection{Login Screen}
In der Abbildung \ref{Abb:login} ist der Login Screen zu sehen. Er besteht aus der Top-Action-Bar mit Logo und App-Name, den Eingabefeldern und zwei Buttons. Alle Komponenten sind aus der Material-Design-Bibliothek.
Zum Einloggen werden die Daten in die jeweiligen Felder eingegeben. Wenn ein Feld markiert ist, wird das ausgewählte Feld blau umrandet und der Hinweis wird auf die obere Linie verschoben. Das Passwortfeld zeigt nur kurz den eingegebenen Buchstaben an und wird dann zu einem "*", sodass das Passwort nicht offen lesbar ist.
Der Login-Button sendet die Daten an das Backend und prüft, ob die Daten korrekt sind. Wenn dies der Fall ist, enthält die Antwort das Token, welches in den privaten Speicher abgelegt wird, und die App wechselt zum Hauptbildschirm. War der Login nicht erfolgreich, wird dem Benutzer eine Pop-Up-Meldung angezeigt und nichts weiter unternommen. Mit dem Betätigen des Registrieren-Knopfes wird man auf die Register-Activity weitergeleitet.
\subsection{Main Activity}\label{subsec:main}
Auf dem Hauptbildschirm erscheint in der Top-Action-Bar ein drei Punkte Menü (Kebab-Menü), von dem aus man zu den Einstellungen gelangen oder sich ausloggen kann. Beim Ausloggen wird die Datei mit dem Benutzer-Token gelöscht und die Login-Activity aufgerufen.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/main_menu}
\caption{Menü auf dem Hauptbildschirm}
\label{Abb:menu}
\end{figure}
Direkt unter der Top-Action-Bar wird der Benutzer mit dem Vornamen begrüßt (Abb.: \ref{Abb:main}).
In der Bedienfläche kann der Benutzer den Timetrack-Account auswählen und dessen Details ansehen, seine heutigen Arbeitszeiten ansehen und die Aufzeichnung starten oder stoppen.\\
Die Auswahl des Accounts erfolgt über ein Dropdown-Menü. Bei Auswahl wird sofort die zugehörige Beschreibung und die Vergütung angezeigt. Wenn die Aufzeichnung am Laufen ist, wird das Dropdown-Menü ausgeblendet. Das verhindert, dass der Benutzer eine Aktivität für einen anderen Account stoppen kann, als für den, auf dem er sie gestartet hat. Ist für den Benutzer noch kein Account vorhanden, wird "None" im Menü angezeigt und die beiden Felder für Beschreibung und Vergütung werden ausgeblendet.\\
Für die Anzeige der heutigen Arbeitszeiten haben wir eine RecyclerView verwendet. Das Layout dazu wird in einer extra XML-Datei definiert und mit Daten in einer Adapter-Klasse befüllt. Durch eine Backendabfrage bekommen wir die nötigen Daten dafür. Bei aktiver Aufzeichnung wird ein Element angezeigt mit der Startzeit und der Info, dass das Ende offen ist.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/main_recording}
\caption{Laufende Aufzeichnung}
\end{figure}
Der Start-Stop-Button schaltet die Aufzeichnung um, in dem ein Backend-Endpunkt angesprochen wird. In der App haben wir eine boolean-Variable "running" definiert, welche speichert, ob die Aufzeichnung aktiv ist. Anhand ihr wird entschieden, wie der Start-Stop-Button aussieht und ob beim Verlassen des Geofence noch gestoppt werden muss. Der Button ist nicht auswählbar, wenn sich der Nutzer außerhalb seines Arbeitsplatzes befindet und zeigt dies auch an (Abb.: \ref{Abb:outside}). Ist der Nutzer dann im Bereich, wird "Start" angezeigt und der Button ist freigeschaltet. Während der Aufzeichnung trägt der Button die Schrift "Stop". Hat der Nutzer noch keine Geo-Daten für seinen Arbeitsplatz definiert, wird auch das auf dem Button angezeigt.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_outside}
\caption{Nutzer außerhalb seines Geofence}
\label{Abb:outside}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_start}
\caption{Aufzeichnung kann gestartet werden}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_stop}
\caption{Laufende Aufzeichnung}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_no_fence}
\caption{Nutzer hat noch keine Geo-Daten}
\end{figure}
Um versehentliches Stoppen der Aufzeichnung zu verhindern, muss der Nutzer in einem Pop-Up-Dialog seine Aktion bestätigen.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/confirm_stop}
\caption{Bestätigungsdialog zum stoppen}
\end{figure}
\section{Probleme und Lösungen}\label{sec:Probleme}
Damit die App auch die aktuellste Android Version unterstützt, mussten einige zusätzliche Punkte berücksichtigt werden. Neben der Berechtigung aus Kapitel \ref{subsec:geofence} mussten in der "build.gradle"-Datei Kompilierungsoptionen gesetzt werden.\\
Zu Beginn wollten wir alle Activities mit Fragments realisieren, sodass es nur eine Activity gibt und alles weitere Fragments sind. Allerdings war es schwieriger, zwischen den Fragments zu wechseln, als in den Tutorials beschrieben. Deshalb sind wir auf reine Activities umgestiegen. Zwischen diesen ist das Hin- und Herschalten deutlich einfacher, es besteht jedoch kein Zugriff auf Elemente der anderen Activities.\\
Das wurde aber erst dann problematisch, als wir aus der Klasse "GeofenceBroadcastReceiver" eine Methode der "MainActivity" zur Änderung der Oberfläche aufrufen wollten. Das hat den Grund, dass Android nicht sicher sagen kann, dass diese Activity gerade auch aktiv ist. Deshalb haben wir den Weg über die Shared-Preferences gewählt mit einem Listener in der "MainActivity".\\
Initial wollten wir das Token in einer Datenklasse abspeichern, welche beim Einloggen befüllt wird. Dazu müsste allerdings das Objekt oder die Referenz zu jeder anderen Activity übergeben werden. Eine andere Möglichkeit stellen erneut die Shared Preferences dar. Das wäre auch eine gute Lösung gewesen, welche wir aber zu spät entdeckt haben. Deshalb haben wir das Problem mit dem privaten Speicher gelöst. Er ist durch andere Apps und den Benutzer nicht einsehbar, bildet deshalb also kein Sicherheitsrisiko.\\
Unerwartet war, dass die Geofence-Funktion die normale Android Positionsbestimmung zusätzlich benötigt. Denn zuerst hatten wir die Positionsbestimmung implementiert und dann die Geofence-Funktion, was funktioniert hat. Da in der Geofence-Funktion kein Code der normalen Positionsbestimmung referenziert wurde, dachten wir, man könne diesen weglassen, was ein Trugschluss war. Auch der Versuch, Teile der Positionsbestimmung wegzulassen, war ohne Erfolg. Deshalb beinhaltet die App auch Code für die normale Positionsbestimmung.
\section{Deployment}
Das Deployment spielte im Entwicklungsprozess der App keine große Rolle, da es Android-Studio benötigt um die App zu starten. Zum Abschluss haben wir allerdings den aktuellen Stand des Projekts in einer APK-Datei persistiert. Damit lässt sich die App auf andere Geräte installieren und in den App-Store laden. Zur Erstellung einer solchen APK muss ein Key zur Signatur angegeben werden.

View File

@ -0,0 +1,316 @@
\chapter{Backend}
Das Backend ist das Herzstück einer jeden Anwendung. Es muss hochverfügbar und enorm fehlertolerant sein. Aus diesem Grund haben wir uns für Technologien entschieden, die Open-Source-Software sind und eine entsprechend große Verbreitung haben. Weiter war es von Anfang an wichtig, trotz der großen Abhängigkeit zum Backend die Entwicklung der anderen Teile nicht zu blockieren. Es wurden daher frühzeitig Modelle und Protokolle erstellt, die bereits vor der Fertigstellung gemockt werden konnten.
\section{Technologiebeschreibung}
\subsection{Spring Boot}
Für die Implementierung des REST-Backends haben wir auf das Spring Framework gesetzt. Genauer gesagt, das Spring \emph{Boot} Framework. Das Wort "Boot" steht hierbei sinngemäß für "bootstrap", was uns viel Konfigurationsarbeit abgenommen hat. Alle Standard Beans und Factories waren bereits initialisiert und konnten ohne weitere Konfiguration genutzt werden.
Es wurden im Projektverlauf auch noch diverse Erweiterungen des Frameworks genutzt.
\begin{itemize}
\item \textbf{org.springframework.boot:spring-boot-starter-web} bringt einen integrierten Tomcat Application Server mit und ermöglicht das Verarbeiten von Webanfragen.
\item \textbf{org.springframework.boot:spring-boot-starter-actuator} wird genutzt, um Endpoints für Diagnose freizuschalten.
\item \textbf{org.springframework.boot:spring-boot-starter-data-jpa} bringt alle nötigen Abhängigkeiten, um mit der Java Persistence API Daten in einer Datenbank abzulegen.
\item \textbf{org.springframework.boot:spring-boot-starter-data-rest} bietet Möglichkeiten, sehr leicht Datenbank Entitäten als HTTP REST Ressourcen bereitzustellen.
\item \textbf{org.springframework.boot:spring-boot-starter-security} wird später zusammen mit der Authentifizierung über JWT genutzt.
\item \textbf{org.springframework.boot:spring-boot-starter-test} bringt Möglichkeiten, leichtgewichtig Unit Tests für Webanwendungen zu schreiben.
\end{itemize}
Zur weiteren Reduktion des "Boilerplate Codes" wurde zusätzlich noch das Lombok Framework\footnote{\url{https://projectlombok.org/}} genutzt. Es bietet die Möglichkeit, Getter und Setter sowie diverse Konstruktoren für Datenklassen zu generieren. Dadurch konnten die Datenklassen um etwa 80\% in der Große reduziert werden, dies fördert die Lesbarkeit und vermeidet auch Leichtsinnsfehler.
\subsection{MariaDB}
Als Datenbank wurde MariaDB eingesetzt. MariaDB ist die quelloffene Entwicklung der MySQL Datenbank und nimmt deshalb alle Befehle an, die auch MySQL annimmt. Als Alternative stand noch Postgres zur Auswahl, da wir aber keine der erweiterten Funktionen von Postgres brauchten, fiel die Auswahl auf MariaDB. MariaDB musste auf keinem Entwicklungsrechner installiert werden, da immer das offizielle Dockerimage\footnote{\url{https://hub.docker.com/_/mariadb}} genutzt wurde.
\subsection{Weitere Open Source Software}
Eine weitere Bibliothek, die für die Authentifizierung benutzt wird, ist die Java-JWT Implementation von Auth0. Sowie die H2 In-Memory Datenbank. Diese zweite Datenbank wird während der Entwicklung genutzt, um schnell homogene Beispieldaten zu laden und Tests auf diesen durchzuführen.
\subsection{Spezielles Setup}
Um produktiv zu arbeiten, mussten noch weitere Tools genutzt werden. Dazu zählt primär die IntelliJ IDEA Ultimate Entwicklungsumgebung\footnote{\url{https://www.jetbrains.com/de-de/idea/}}. Diese IDE hat sehr viele Integrationen für das Spring Framework, als auch mit unseren Docker-Setup. Es wird dadurch möglich, ausschließlich in der IDE zu arbeiten, ohne weitere Kommandozeilenfenster.
Das zweite wichtige Programm war der REST-Client Insomnia REST\footnote{\url{https://insomnia.rest/}}, welcher alle Möglichkeiten bietet, um REST APIs zu testen und Testabfragen auszuführen.
\section{Umsetzung}
\subsection{Spring Entities}
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/backend/er-modell.png}
\caption{ER-Modell}
\label{fig:er-modell}
\end{figure}
Das ER-Modell in Abbildung \ref{fig:er-modell} zeigt die komplette Hierarchie, wie sie unserem Konzept entspricht. Wir legen diese Definition aber nicht selbst in SQL an, sondern lassen Java Hibernate dies für uns tun. Die Grundstruktur der gespeicherten Daten ist wie folgt zu verstehen:
\begin{itemize}
\item Der \textbf{TimetrackUser} ist die Grundstruktur, die alle anderen Daten des Users zusammenhält. Sie speichert allgemeine Nutzerdaten und hält Referenzen auf die \textbf{Role} des Nutzers, seine \textbf{Location} und alle ihm gehörenden \textbf{TimetrackAccounts}.
\item Die \textbf{Role} sollte ursprünglich erlauben, zwischen einem Admins und einem normalen Nutzers zu unterscheiden, aus Zeitgründen wurde dies aber weggelassen. Die Grundstruktur ist dennoch implementiert, allerdings so, dass jeder Nutzer automatisch Administrator ist.
\item Die \textbf{Location} Entität speichert den Geofence des Nutzers. Diese Daten werden ausschließlich von der Android App genutzt, um beim Einloggen den Geofence zu setzen.
\item Der \textbf{TimetrackAccount} ist die zweite große Struktur, die alle \textbf{TimeRecords} des Nutzers verwaltet. Jeder Nutzer kann mehrere \textbf{TimetrackAccounts} besitzen, aber jeder Account kann nur einem Nutzer gehören.
\item Jede getrackte Zeitspanne wird in einem \textbf{TimeRecord} abgespeichert. Dieser Record speichert einen Typ sowie das Start- und Enddatum. Der Typ kann entweder "PAID" oder "BREAK" sein. Jeder Record gehört zu genau einem TimetrackAccount.
\end{itemize}
Die Umsetzung in Java wird nun am Beispiel des TimetrackUsers und des TimetrackAccounts gezeigt.
\lstinputlisting[language=Java,caption=TimetrackUser,firstline=10]{../backend/src/main/java/de/hft/geotime/entities/TimetrackUser.java}
Die komplette Klasse ist durch die Lombok Integration sehr klein gehalten. Alles weitere wird durch Annotationen geregelt, einige Beispiele sind hier:
\begin{itemize}
\item[] \textbf{@Entity} markiert die Klasse als speicherbar in der Datenbank.
\item[] \textbf{@ManyToOne} markiert das Attribut als Fremdschlüssenrelation aus einer anderen Tabelle.
\item[] \textbf{@Id} zeichnet den Primärschlüssel der Tabelle aus.
\item[] \textbf{@Column} setzt spezielle Attribute für die Spalte in der Datenbank.
\end{itemize}
Die TimetrackAccounts haben zusätzlich noch die Eigenschaft, dass sie sich selbst rekursiv löschen, wenn der zugehörige User gelöscht wird. Selbiges gilt auch für die Records, wenn der zugehörige Account gelöscht wird.
\lstinputlisting[language=Java,caption=TimetrackAccount,firstline=11]{../backend/src/main/java/de/hft/geotime/entities/TimetrackAccount.java}
\subsection{Sicherheit durch JWT}
Da wird die Web App im Laufe des Projekts auch öffentlich in Internet stellen mussten, war eine Art Authentifizierung so gut wie unumgänglich. Damit wir keine Probleme mit Session-Affinity haben, entschieden wir uns für eine Token-Based Authentifizierung. Bei der genauen Implementation handelt es sich hier um das JSON Web Token, kurz JWT.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/backend/jwt.io.png}
\caption{Aufbau eines JWT}
\label{fig:aufbau-jwt}
\end{figure}
In Abbildung \ref{fig:aufbau-jwt} ist ein exemplarischer Aufbau eines JWT dargestellt. Das JWT besteht grundsätzlich aus drei Teilbereichen:
\begin{enumerate}
\item \textbf{Rot hinterlegt:} Bei diesem Teil handelt es sich um den Header, dieser beinhaltet den Typ des Tokens, als auch den Algorithmus, mit dem es verschlüsselt wurde.
\item \textbf{Lila hinterlegt:} In diesem Teil werden die eigentlichen Nutzdaten des Tokens abgelegt, dort können z.B. Nutzernamen oder Nutzer-Id sowie eine Rolle hinterlegt werden.
\item \textbf{Blau hinterlegt:} Der letzte Part ist dann noch die Signatur des Tokens.
\end{enumerate}
Jeder dieser Teile ist durch einen Punkt im Token abgetrennt. Es ist daher nicht verwunderlich, dass alle Token das selbe Präfix haben werden und nur der Mittelteil, sowie die Signatur sich ändern.
Die Implementation in Spring Boot gelang in drei, vergleichsweise einfachen, Schritten. Zunächst mussten einige Konstanten definiert werden, zur einfacheren Handhabung wurde auch das Secret in den Code platziert. Dieses könnte aber sehr leicht über eine Umgebungsvariable überschrieben werden.
\lstinputlisting[language=Java,caption=JWT Security Constants]{../backend/src/main/java/de/hft/geotime/security/SecurityConstants.java}
Die Lebensdauer eines Tokens wurde mit 10 Tagen ebenfalls sehr hoch gewählt, um die Entwicklung zu vereinfachen. Auch muss dem Token zur erfolgreichen Nutzung in anderen Systemen das Prefix "Bearer " vorangestellt werden.
Um nun die Tokens in Java zu erzeugen und Abzugleichen, musste die Filterkette von Spring Boot, welche bei jedem Request durchlaufen wird, bearbeitet werden. Jeder Endpunkt außer "/login" und "/sign-up" benötigte ab diesem Zeitpunkt eine autorisierte Anfrage.
\lstinputlisting[language=Java,linerange={30-48},caption=JWT Authentication Filter,label=code:jwt-authentication-parse]{../backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java}
In Listing \ref{code:jwt-authentication-parse} ist der Schritt zu sehen, der die ankommende Anfrage versucht, in eine Loginanfrage zu parsen. Diese Anfrage wird dann in der Filterkette weitergereicht. Bis sie zum UserDetailsService kommt, welcher den User in der Datenbank abfragt und auch das Password abgleicht. Sollte die interne Autorisation erfolgreich sein, wird dieses Objekt mit den Nutzerdaten wieder an die Filterkette zurückgegeben und landet schließlich bei Listing \ref{code:jwt-authentication-create}.
\lstinputlisting[language=Java,linerange={50-62},caption=JWT Authentication Filter,label=code:jwt-authentication-create]{../backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java}
Der letzte Schritt ist dann nur noch, das Token mit den erhaltenen Daten zu befüllen und dann den "Authorization" Header der Antwort auf das soeben erstelle Token zu setzen.
Ab jetzt kann sich der Client, der das Token angefragt hat, für die nächsten 10 Tage damit authentifizieren. Dies läuft sehr ähnlich ab, deshalb hier nur sehr kurz dargestellt.
\lstinputlisting[language=Java,linerange={40-55},caption=JWT Authorization Filter,label=code:jwt-authorization]{../backend/src/main/java/de/hft/geotime/security/JWTAuthorizationFilter.java}
Der eingehende Request geht wieder durch die Filterkette und wenn er an dem Filter in Listing \ref{code:jwt-authorization} ankommt, wird der User extrahiert und später im Security Manager als Autorisation für diesen Request gesetzt. Wichtig ist hier, dass keine weitere Prüfung auf die Existenz des Users durchgeführt wird, auch das Password wird nicht nochmal abgefragt. Der Grund hierfür ist, wenn es den User nicht geben würde, wie käme er dann an das Token?
\subsection{Repositories}
Nachdem der Nutzer authentifiziert ist, bekommt er Zugriff auf alle REST-Repositories. Für jede Ressource, die oben im ER-Modell definiert ist gibt es ein entsprechendes Repository. Dieses wird größtenteils automatisch vom Classpath Scan von Spring automatisch implementiert. Die normalen CRUD Operationen werden für jedes angelegte Repository komplett ohne zutun implementiert. Ein solches Repository ist beispielsweise das der Location.
\lstinputlisting[language=Java,firstline=8,caption=LocationRepository,label=code:location-repository]{../backend/src/main/java/de/hft/geotime/repositories/LocationRepository.java}
Das einzige, was dort getan werden muss, ist die Angabe des Typs, der hier behandelt wird, hier Location, und der Datentyp des Primärschlüssels, hier ein Long. Die Bedeutung der Klasse "PagingAndSortingRepository" wird in einem späteren Kapitel genauer erläutert. Um den Link der Ressource anzupassen, werden die Parameter in der "RepositoryRestRessource" Annotation genutzt. Der Pfad geht immer vom Rootpfad der Applikation aus.
Werden nun noch weitere Funktionalitäten in den Repositories benötigt, können diese entweder selbst implementiert werden, oder durch gut ausgewählte Funktionsdefinitionen im Interface der Ressource deklariert werden. Spring kann die Implementierung dann aus dem Namen und den Parametern der Funktion ableiten. Als unser Maximalbeispiel dient hier das RecordRepository.
\lstinputlisting[language=Java,linerange={14-27,38-41,47-58},caption=RecordRepository,label=code:record-repository]{../backend/src/main/java/de/hft/geotime/repositories/RecordRepository.java}
In diesem Repository befinden sich diverse verschiedene Methoden, wie Datenoperationen, die definiert werden können, ohne das sie aktiv implementiert werden müssen. Es beginnt mit der Funktion "findAllByStartdateBetween". Dieser Name kann direkt als Java Hibernate Statement interpretiert werden und nimmt als Parameter zwei Datumsangaben entgegen und eine Page. Die zwei Datumsangaben werden aus dem Schlüsselwort "Between" abgeleitet. Damit es sich aber um echt vergleichbare Daten handelt, müssen diese nach einem bestimmten Schema geparsed werden. Dieses Schema ist in der "DateTimeFormat" Annotation angegeben. Als Rückgabe liefert diese Funktion dann eine Menge aller Einträge zwischen diesen Daten.\\
Die nächste Funktion funktioniert nun ähnlich, nur dass dort über Eigenschaften mehrerer verlinkter Objekte gegangen werden kann. "findAllBy" ist wieder das selbe wie oben und zeigt an, dass eine Liste von Ergebnissen zurückgeliefert wird, aber "Account\_User\_Username" bedeutet nun folgendes: "Gehe zum Account des Records, dann zum User dieses Accounts und von diesem User dann den Username". Der gefundene Username wird dann mit dem Parameter der Funktion verglichen und die Ergebnisse entsprechend gefiltert. Weiter zeigt das "And" eine Verkettung eines weiteren Ausdrucks an. So können auch relativ komplexe Abfragen automatisch implementiert werden.\\
Reicht allerdings die obige Syntax nicht mehr aus, kann auch direkt eine Hibernate Abfrage über die "@Query" Annotation angegeben werden. Der Name der Funktion ist dann nicht mehr relevant für die Implementation, sondern nur noch für den Pfad, unter dem die Funktion später zu erreichen ist. Die Query an der "today" Funktion bietet nun die Möglichkeit, alle Einträge in der Records Tabelle für den aktuell anfragenden User zu bestimmen. Zusätzlich wird der Zeitraum noch auf den aktuellen Tag eingeschränkt, daher ergab sich auch der passende Name "today" für die Funktion. Der Nutzer wird automatisch über die "principal" Variable in der Abfrage eingefügt. Der Pricipal wird gesetzt, sobald der Authentication Filter den User erfolgreich eingeloggt hat. Weiter wird der aktuelle Tag über die Datenbankvariable "current\_date" abgefragt.\\
Zuletzt kann auch nach Standard SQL Werten wie "null" oder "not null" gefragt werden. Zu sehen ist dies in der zuletzt dargestellten Funktion.\\
Die Datei ist nicht vollständig abgedruckt, sondern nur ausschnittsweise, um die Grundkonzepte zu erläutern.
\subsection{Projections}
Projections bieten nun noch weitere Möglichkeiten, Daten vor der Rückgabe noch zu transformieren und gegebenenfalls mit Zusatzdaten anzureichern. Eine Projektion ist ebenfalls durch ein Interface definiert und bringt vor allem dann Vorteile, wenn mehrere Ressourcen gebündelt angefragt werden müssen, um beispielsweise eine Übersicht zu erstellen.
\lstinputlisting[language=Java,firstline=9,caption=RecordOverviewProjection,label=code:record-overview-projection]{../backend/src/main/java/de/hft/geotime/entities/projections/RecordOverviewProjection.java}
Die "RecordOverviewProjection" (Listing \ref{code:record-overview-projection}) reichert eine normale "Record" Ressource noch zusätzlich mit dem Username und den Accountnamen an. Dadurch muss nicht für jeden Record erneut einzeln der Accountname nachgeschlagen werden. Zudem wird noch ein, bei jeder Anfrage neu berechnetes, zusätzliches Feld angefügt. Nämlich die Dauer des Records in Minuten.
Eine Projektion kann am Beispiel des Records gut verdeutlicht werden.
\begin{lstlisting}[language=json,caption=Einzelner Record ohne Projektion,label=code:records-without-proj]
{
"startdate": "2020-05-30T18:00:00",
"enddate": "2020-05-30T19:00:00",
"type": "PAID",
"duration": 60,
"_links": {
"self": {
"href": "http://localhost:5000/records/27"
},
"records": {
"href": "http://localhost:5000/records/27{?projection}",
"templated": true
},
"account": {
"href": "http://localhost:5000/records/27/account{?projection}",
"templated": true
}
}
}
\end{lstlisting}
\begin{lstlisting}[language=json,caption=Einzelner Record mit Projektion "overview",label=code:records-with-proj]
{
"duration": 60,
"username": "scma",
"account": "TestAccount",
"startdate": "2020-05-30T18:00:00",
"enddate": "2020-05-30T19:00:00",
"type": "PAID",
"_links": {
"self": {
"href": "http://localhost:5000/records/27"
},
"records": {
"href": "http://localhost:5000/records/27{?projection}",
"templated": true
},
"account": {
"href": "http://localhost:5000/records/27/account{?projection}",
"templated": true
}
}
}
\end{lstlisting}
Es ist zu sehen, dass in Listing \ref{code:records-without-proj} die beiden Felder "account" und "username" fehlen, diese tauchen erst bei der Abfrage mit angewendeter, serverseitiger, Projektion auf (siehe Listing \ref{code:records-with-proj}). Die Anfrage für Listing \ref{code:records-without-proj} lautet \verb|http://localhost/records/27| und um nun die Projektion anzuhängen, wird die URL wie folgt erweitert: \verb|http://localhost/records/27?projection=overview|. Projektionen können hierbei nicht nur auf einzelne Objekte einer Ressource angewendet werden, sondern auch auf eine Menge dieser.
\section{Endpoints}
Die vier Hauptendpoints sind sicherlich die unserer Hautptressourcen: locations, accounts, records und users. Unten gibt es noch den nicht implementierten Endpoint für die Rollen ("roles"), dieser liefert aber kaum Informationen. Der "profile" Endpoint wird erst im nächsten Kapitel erläutert. Um diesen Output zu bekommen, muss der Nutzer authentifiziert sein. Dies geschieht, wie oben schon erwähnt, über den "/login" Endpoint. Da dieser aber keine Ausgaben außer dem Header mit dem Token liefert, wird er hier nicht weiter erläutert. Selbiges gilt auch für den "/sign-up" Endpoint. Alle Anfragen, die ab jetzt ausgeführt werden, geschehen immer mit vorheriger Authentifizierung.
\begin{lstlisting}[language=json,caption=Zugriff auf die Route "/" der API,label=code:main-route-api]
{
"_links": {
"locations": {
"href": "http://localhost:5000/locations{?page,size,sort}",
"templated": true
},
"accounts": {
"href": "http://localhost:5000/accounts{?page,size,sort,projection}",
"templated": true
},
"records": {
"href": "http://localhost:5000/records{?page,size,sort,projection}",
"templated": true
},
"users": {
"href": "http://localhost:5000/users{?page,size,sort,projection}",
"templated": true
},
"roles": {
"href": "http://localhost:5000/roles"
},
"profile": {
"href": "http://localhost:5000/profile"
}
}
}
\end{lstlisting}
\begin{lstlisting}[language=json,caption=Zugriff auf die Route "/locations" der API,label=code:locations-route-api]
{
"_embedded": {
"locations": [
{
"latitude": 1.0,
"longitude": 2.0,
"radius": 3,
"_links": {
"self": {
"href": "http://plesk.icaotix.de:5000/locations/1"
},
"locations": {
"href": "http://plesk.icaotix.de:5000/locations/1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://plesk.icaotix.de:5000/locations{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://plesk.icaotix.de:5000/profile/locations"
}
},
"page": {
"size": 20,
"totalElements": 6,
"totalPages": 1,
"number": 0
}
}
\end{lstlisting}
Aufgrund der massiven Größe der Ausgaben der API werden die weiteren Endpoints nur noch mit ihrem Link angegeben. Alle Ressourcen unterstützen zudem die CRUD Operationen auf einzelnen Ressourcen, als auch auf der Hauptressource, deshalb werden sie aus Platzgründen ebenfalls übergangen.
\subsection*{Endpoints für Ressourcen}
\begin{itemize}
\item \verb|/locations{?page,size,sort,projection}|
\item \verb|/locations/<nr>{?projection}|
\item \verb|/roles{?page,size,sort,projection}|
\item \verb|/accounts{?page,size,sort,projection}|
\item \verb|/accounts/<nr>{?projection}|
\item \verb|/accounts/<nr>/user{?projection}|
\item \verb|/accounts/search/findByUsernameAndName{?username,account,projection}|
\item \verb|/accounts/search/findByUsername{?username,page,size,sort,projection}|
\item \verb|/users{?page,size,sort,projection}|
\item \verb|/users/<nr>{?projection}|
\item \verb|/users/<nr>/location{?projection}|
\item \verb|/users/<nr>/role{?projection}|
\item \verb|/users/search/byUsername{?username,projection}|
\item \verb|/records{?page,size,sort,projection}|
\item \verb|/records/<nr>{?projection}|
\item \verb|/records/<nr>/account{?projection}|
\item \verb|/records/search/allBetweenAndUser{?start,end,username,page,size,sort,projection}|
\item \verb|/records/search/openEntries{?page,size,sort,projection}|
\item \verb|/records/search/today{?page,size,sort,projection}|
\item \verb|/records/search/allForUser{?username,page,size,sort,projection}|
\item \verb|/records/search/allBetween{?start,end,page,size,sort,projection}|
\item \verb|/records/search/allFrom{?date,page,size,sort,projection}|
\item \verb|/records/search/allForUserAndAccount{?username,account,page,size,sort,projection}|
\end{itemize}
Wenn Ressourcen aktualisiert werden müssen, müssen die Daten immer im JSON Format vorliegen. Die Felder des JSON Objekts müssen mit denen der Ressource übereinstimmen. Es müssen aber nicht alle Felder Werte beinhalten. Soll eine neue Ressource erstellt werden, werden die Daten als POST abgesendet, bei einer Aktualisierung als PATCH. Links zwischen Ressourcen können über den Self Link der Ressource hergestellt werden. Weiter gibt es noch zwei komplett selbst gebaute Endpoints.
\subsection*{Der "/whoami" Endpoint}
Dieser Endpoint dient dazu, um nach dem Login schnell die Startseite der App oder der Webseite mit den Nutzerdaten zu befüllen. Es sind Daten wie der Vor- und Nachname, sowie der Username enthalten. Zusätzlich wird noch die gesetzte Location des Nutzers mitgegeben.
\subsection*{Der "/track" Endpoint}
Beim "/track" Endpoint handelt es sich um einen der wichtigsten Endpoints. Er erlaubt es, ein Recording zu erstellen, ohne Angabe des Nutzers. Lediglich der Name des Timetrack Accounts, auf den gebucht werden soll, muss angegeben werden. Der Endpoint entscheidet auf Serverseite, von welchem Nutzer die Anfrage ankam. Dazu wird der Nutzer aus dem JWT extrahiert und abhängig davon im Account des Nutzers geschaut, ob schon ein Tracking läuft oder nicht. Sollte noch kein Tracking laufen, wird ein neuer Eintrag mit der aktuellen Zeit erstellt und zurückgeliefert. Das Enddatum ist zu diesem Zeitpunkt noch leer und auch die Duration zeigt "0" an. Sollte schon ein Tracking laufen, wird dieses mit der aktuellen Zeit als Endzeit beendet und ebenfalls zurückgeliefert. Sollte der Account nicht gefunden werden, oder ein anderer Fehler auftreten, wird ein entsprechender HTTP Statuscode zurückgeliefert.
\begin{lstlisting}[language=json,caption=Aufruf von "/track" ohne laufendes Tracking,label=code:start-tracking-endpoint]
{
"duration": 0,
"username": "scma",
"account": "Demo",
"startdate": "2020-06-11T00:59:22",
"enddate": null,
"type": "PAID"
}
\end{lstlisting}
\begin{lstlisting}[language=json,caption=Aufruf von "/track" mit laufendem Tracking,label=code:stop-tracking-endpoint]
{
"duration": 129,
"username": "scma",
"account": "Demo",
"startdate": "2020-06-10T22:47:55",
"enddate": "2020-06-11T00:57:41",
"type": "PAID"
}
\end{lstlisting}
\subsection{HAL, Paging und Sorting}
Die Hypertext Application Language, kurz HAL, ist eine Spezifikation, mit der APIs automatisch erkundbar gemacht werden können. Sie bietet META-Elemente an, einige davon werden auch bei uns benutzt.
\begin{itemize}
\item \textbf{"\_links"} zeigt weiterführende Links zu Ressourcen oder Informationen zum Paging an.
\item \textbf{"\_embedded"} enthält die Nutzdaten zur entsprechenden Ressource, aber auch weitere Einbettungen zu Sub-Ressourcen.
\end{itemize}
Zusätzlich dazu nutzt Spring bei der Generierung der Repositories auch Teile der "Hypermedia as the Engine of Application State", kurz HATEOAS, Spezifikation. Das Listing \ref{code:main-route-api} zeigt hierfür den zusätzlichen Endpoint "profile". Unter diesem sind viele Spezifikationen zu finden, wie alle anderen Routen auf bestimmte Daten reagieren und auch antworten.
Der "profile" Endpoint zeigt zusätzlich noch alle Projektionen an, die auf eine bestimmte Ressource angewendet werden können. Der Name der Projektion wird dann durch den URL-Parameter "projection=" angehängt.
Zuletzt gilt es noch zu erwähnen, dass alle Ressourcen Paging und Sorting unterstützen. Paging ist besonders bei Web APIs wichtig, da die Geschwindigkeit sehr stark von der Menge der übertragenen Daten abhängt. Wenn eine Ressource immer alle Daten zurückliefern würde, würde dies bei mehreren hundert Einträgen sicher noch funktionieren. Aber sobald die Zahl der Einträge deutlich höher wird, muss abgeschnitten und aufgeteilt werden. Unsere Standard Seitengröße ist auf 20 Einträge gesetzt. Weiter enthält die Antwort des Servers durch die HAL Integration immer Links zur aktuellen, nächsten und vorherigen Seite als Link. Sorting wird ebenfalls unterstützt. Es kann nach jedem Feld im zurückgegebenen JSON sortiert werden, auch die Richtung ist spezifizierbar.
\section{Probleme und Lösungen}
\subsection{Einlesen in Spring}
Spring ist ein sehr komplexes Framework, weshalb es manchmal wirklich sehr schwierig war, Fehler zu verstehen, und die Gründe dahinter zu verstehen. Solange man sich aber an viele der Best-Practices von Spring hält, ist es überhaupt nicht schwer, in relativ kurzer Zeit auch sehr komplexe APIs zu bauen. Durch die enorme Menge an Dokumentation und auch Hilfe aus der Community sowie Techtalks können viele Probleme leicht gelöst werden.
\subsection{Änderungen an den Endpoints}
Es mussten anfangs viele Endpoints immer wieder umdefiniert werden, da sie nicht Best-Practices entsprochen haben oder nicht performant funktioniert haben. Dies wurde später aber immer einfacher, wenn man sich an die Denkweise einer REST-API gewöhnt hat. Auch zwei Wege Links zwischen Ressourcen waren bei uns nicht möglich, da sie zu Endlosrekursionen führten. Später wurde aber auch klar, das dies überhaupt nicht gewünscht ist.
\subsection{Probleme mit MariaDB}
Zu Beginn haben wir für das Docker-Image der Datenbank den "latest"-Tag benutzt. Dies war möglich, da wir keinerlei eigene Konfiguration der Datenbank und deren Tabellen vorgenommen haben. Allerdings wurde Mitte April die neue LTS-Version von Ubuntu veröffentlicht und damit auch das Basisimage von MariaDB angepasst. Durch Änderungen in Ubuntu 20.04 funktionierten nun gewisse Datenbankfunktionen nicht mehr ordnungsgemäß. Als Lösung kam dann nur ein Downgrade auf eine ältere Version in Frage.
\section{Deployment}
Das Deployment des Backends findet immer gleichzeitig mit der Datenbank und dem Frontend statt. Die Daten bleiben dabei erhalten und werden, so fern nötig, von Spring automatisch migriert. Auch beim Hinzufügen oder Entfernen von Feldern aus Entitäten aktualisiert Spring die Datenbanktabellen entsprechend den neuen Feldern. Sollten Felder wegfallen, werden diese einfach gelöscht. Kommen neue hinzu, wird entweder der Defaultwert gesetzt oder, wenn erlaubt, "Null".

View File

@ -1 +0,0 @@
\chapter{Erläuterung des Designs}

View File

@ -1,5 +0,0 @@
\chapter{Einrichtung der Entwicklungsumgebungen}
\section{Frameworks}
\section{Erzeugung der Binaries}

View File

@ -1 +1,8 @@
\chapter{Einleitung}
Im Rahmen der Vorlesung Ubiquitous Computing ist eine Projektarbeit mit dem Thema der Allgegenwärtigkeit von Computern vorgesehen.\\
Dazu fanden wir uns als Gruppe zusammen und sammelten unsere Ideen für Projektthemen. Letztendlich setzte sich der Timetracker mit Geofence gegen eine Wetterstation und eine Augmented Reality App durch.\\
Die Idee der Timetrack-Anwendung ist, dass ein Arbeitnehmer erst dann mit der Aufzeichnung seiner Arbeitszeit beginnen kann, wenn er sich am Arbeitsort befindet. Wenn er diesen verlässt, wird seine Aufzeichnung beendet. Dabei hat er mehrere Zeitkonten zur Auswahl, auf die er seine Arbeitszeit verbuchen kann.\\
Das Projekt ist in drei Teile unterteilt: Backend, Web-Frontend und Android-App. Entsprechend dem Aufwand arbeiteten am Web-Frontend zwei Studenten, am Backend und der Android-App jeweils ein Student. Die Android-App soll lediglich die Basisfunktionalitäten bieten, wie Starten und Stoppen der Aufzeichnung für einen gewählten Account, wenn man sich am Arbeitsort befindet, sowie log in und log out. Im Web-Frontend sollen verschiedenste Statistiken angezeigt werden, sowie Adminfunktionaltäten, um Accounts zu editieren, neue Einträge hinzufügen, Geodaten für den Arbeitnehmer setzen und Benutzer löschen. Das Backend kommuniziert mit der Datenbank, sichert die Authentifizierung der Benutzer und stellt den Oberflächen Endpoints zur Verfügung.

View File

@ -0,0 +1,47 @@
\chapter{Entwicklungsumgebung}
Da wir uns für eine Full-Stack Application entschieden haben, war es wichtig, gleich zu Beginn die Entwicklungsumgebung so robust wie möglich zu gestalten. Weiter sollte das ganze Setup einfach unter Versionskontrolle gestellt werden können, um überall reproduzierbar zu sein.
\section{Versionsverwaltung}
Für die Versionsverwaltung haben wir das aktuell am weitesten verbreitete Tool Git benutzt. Dies war notwendig, damit wir unabhängig voneinander arbeiten können. Das Repository ist öffentlich auf der Plattform GitLab einsehbar. Die Adresse lautet \url{https://gitlab.com/marcel.schwarz/2020ss-qbc-geofence-timetracking}.
\subsection{GitLab}
Die Entscheidung für GitLab fiel aber nicht ohne Grund. GitLab bietet auch sehr ausgeprägte Projektplanungsmöglichkeiten, die die Kollaboration sehr vereinfachen. Dazu zählen:
\begin{itemize}
\item Issues. In den Issues werden alle Aufgaben für das Projekt abgelegt, die noch erledigt werden müssen oder eine weitere Betrachtung benötigen. Auch Bugfixes werden dort angelegt.
\item Issues werden in Merge Requests bearbeitet. Diese Merge Requests können genutzt werden, um über Code zu diskutieren und gegebenenfalls zu verbessern.
\item Code-Ownership. Da wir die Teile der Anwendung nach Personen aufgeteilt haben, gibt es für jeden Teil der Anwendung mindestens ein Teammitglied, welches sich besonders gut mit diesen Themen auskennt. Diese Teammitglieder haben dadurch auch die Code-Ownership für diesen Teil des Codes.
\item Merge Request Approval. Wenn ein Issue mehrere Teile der Anwendung ändert, muss der jeweilige Codeowner dieses Teils dem Merge Request ebenfalls zustimmen. Ein Beispiel wäre hier die Implementation: "ein Datum im Frontend ändern", was aber zusätzlich die Anpassung des Datumsformates im Backend erfordert. Bei diesem Merge Request muss dann sowohl ein Codeowner des Frontends als auch des Backends zustimmen. Diese zusätzliche Sicherheitsschicht dient der Stabilität des Master-Branches und der allgemeinen Codequalität.
\end{itemize}
\subsection{Umgang mit Issues}
Die Issues sind die komplette Dokumentation der erledigten Aufgaben während des Projekts. In ihnen können alle Informationen abgelegt werden, die relevant sind. Beispiele sind hier Bugfixes, Features aber auch Definitionen von Design und Farbschema, die die Zustimmung mehrerer Gruppenmitglieder benötigen. Auch API-Definitionen und Alignment-Meetings gehören bei uns dazu.
Der Lebenszyklus eines Issues sieht wie folgt aus:
\begin{enumerate}
\item Issue wird angelegt. Entweder während eines anderen Sprints aus einer Idee heraus, oder bei der Sprintplanung.
\item Zuweisung und Schätzung. Das Issue wird spätestens beim nächsten Planning einem Sprint zugewiesen und erhält Labels die zeigen, welche Teile der Anwendung von diesem Issue betroffen sind. Zuletzt wird dem Issue noch ein Gewicht zugewiesen, welches die geschätzte Dauer in Stunden darstellt.
\item Assignment. Das Issue wird der Person zugewiesen, die es erledigen wird.
\item Merge Request. Es wird ein Merge Request zum Issue angelegt, welcher, wenn er akzeptiert wird, das Issue automatisch schließt.
\item Coding.
\item Review und Ausführung der Continuous Integration Pipeline.
\item Merge des Merge Requests und Schließen des Issues.
\end{enumerate}
\subsection{GitLab Wiki}
Das GitLab Wiki ist eine, auf der Markdown Sprache basierende, Möglichkeit, genauere Dokumentationen zu erstellen. Sollten beispielsweise Entscheidungen getroffen werden, die das ganze Team betreffen, werden diese hier abgelegt.
\subsection{Continuous Integration}
GitLab bietet die Möglichkeit Pipelines für Continuous Integration anzulegen. Diese Pipelines werden dann auf geteilten Maschinen (sog. Runners) der GitLab eigenen Infrastruktur ausgeführt. Die Pipeline baut im Kontext eines Merge Requests die Codeteile, in denen Änderungen erfolgt sind, und validiert damit die Funktionalität auf einem sauberen System. Auch Unit- oder Integrationtests sowie Deployments könnten an dieser Stelle ausgeführt werden.
\section{Docker}
Um die Umgebung unserer Anwendung so homogen wie möglich zu halten, wurde schon von Anfang an auf Docker als Runntime gesetzt. Jeder Teil der Anwendung besitzt eine Dockerfile, die den Code zu einem Executable zusammenbaut. Alle Teile außer Android besitzen zusätzlich noch eine Runtime-Umgebung in Docker. Die Dockerfiles sehen wie folgt aus:
\lstinputlisting[language=docker, caption=Dockerfile Backend]{../backend/Dockerfile}
\lstinputlisting[language=docker, caption=Dockerfile Frontend]{../frontend/Dockerfile}
\lstinputlisting[language=docker, caption=Dockerfile Android, basicstyle={\footnotesize\ttfamily}]{../android/Dockerfile}
Zu erwähnen sind noch einige Besonderheiten. Da wir ein Programm entwickeln, was sich mit Zeiterfassung beschäftigt, ist es wichtig im Backend die Zeitzone des Images zu setzen, da sonst immer die UTC Zeitzone als Standard angenommen wird. Selbiges gilt auch für die SQL Datenbank, welche mit MariaDB umgesetzt ist.
\section{Docker-Compose}
Um nun die ganze Applikation auszuführen, kommt ein Docker-Compose Setup zum Einsatz.
\lstinputlisting[language=docker-compose,caption=Docker-Compose]{../docker-compose.yml}
Es werden drei Services definiert: frontend, backend und db. Diese Dienste arbeiten zusammen und warten auf ihre jeweiligen Abhängigkeiten. Zudem wird das Frontend auf dem Port 8080 an die lokale Maschine gebunden und das Backend an Port 5000. Android ist hier nicht vertreten, da dies eine alleinstehende Anwendung ist. Dieses Setup funktioniert sowohl zu Entwicklungszwecken, als auch in unserem kleinen production deployment. Um den Stack bereitzustellen, muss nur der Konsolenbefehl
\begin{lstlisting}[language=bash]
docker-compose up --build -d
\end{lstlisting}
im Root-Verzeichnis der Anwendung ausgeführt werden. Es sei noch zu erwähnen, dass beim allerersten Start die Datenbankinitialisierung etwas länger brauchen kann und deshalb das backend mehrere Versuche braucht, bis die Verbindung aufgebaut werden kann.
\section{Infrastruktur}
Die Infrastruktur, auf dem die Anwendung zur Zeit bereitgestellt ist, ist ein kleiner Linux Server bei Strato. Dieser Server hat ebenfalls eine Docker-Compose Installation und läuft auf Ubuntu 20.04 LTS. Gestartet wird es dann exakt gleich, wie im obigen Abschnitt erklärt. Natürlich ist Docker-Compose kein Deployment, welches in Produktion verwendet werden sollte, aber es reicht aktuell für unsere Zwecke aus. Nichts desto trotz ist die Anwendung aber auf eine sehr viel größere Skalierung bestens vorbereitet. Durch die Containerisierung ist unsere Anwendung komplett entkoppelt und könnte somit unabhängig skalieren. Einzig die SQL Datenbank müsste als Container entfernt werden und in ein eigenes Deployment verschoben werden.

View File

@ -0,0 +1,256 @@
\chapter{Web-Frontend}
\section{Technologiebeschreibung}
\subsection{Vue.js}
Vue.js\footnote{\url{https://vuejs.org/}} ist ein Javascript Framework, welches den Aufbau von Frontend-Anwendungen erleichtert. Ein Hauptmerkmal hierbei ist die Kapselung der einzelnen Elemente in Komponenten, welche ihren eigenen HTML, Javascript und CSS Code enthalten. Eine Komponente kann mehrere andere Komponenten einbinden, sowie diesen Daten mitgeben. Eingebundene Komponenten können an die übergeordnete Komponente Daten senden.
\subsection{Vuetify}
Vuetify\footnote{\url{https://vuetifyjs.com/de-DE/}} ist ein Designframework für Vue.js, das viele Elemente wie Menüleisten, Buttons und Dialogfenster bereitstellt. Ein bekanntes äquivalentes Framework ist Bootstrap. Das Designschema von Vuetify ist an Googles Material Design angelehnt. Nach Installation können die Elemente sehr einfach eingebunden und verwendet werden.
\section{Farbschema und Designsprache}
Wir haben uns für die, von Google entwickelte, Designsprache "Material Design" entschieden. Diese zeichnet sich durch ihre kartenartigen Flächen und dem Gestaltungsstil Flat Design aus. Verwendet werden auch viele Schatten, um die materialistisch-physikalische Darstellung zu erzeugen.
In unserem eigens entwickelten Farbschema haben wir uns für ein dunkles Thema mit Blau als Hauptfarbe entschieden.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth/2]{img/frontend/color_sceme.png}
\caption{Farbschema}
\end{figure}
Unser Logo wurde im abgestimmten Farbschema umgesetzt und stellt die Kombination zwischen einer Stoppuhr und einem Kompass dar. So verbindet das Logo Zeit und Ort, welche bei der Verwendung unseres Produkts eine wichtige Rolle spielen.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth/3]{img/frontend/logo_dark_gt.png}
\caption{Logo unserer Anwendung}
\end{figure}
\section{Umsetzung}
\subsection{Einarbeitung}
Zur Einarbeitung haben wir den Vue JS Crash Course\footnote{\url{https://www.youtube.com/watch?v=Wy9q22isx3U}} von Traversy Media genutzt. Dieser ist kostenlos auf YouTube zu finden.
\subsection{Arbeit mit Dummy-Daten}
Zur Erstellung der Listen und Diagramme haben wir häufig Dummy-Daten verwendet, um die Funktionalität im Frontend unabhängig vom Backend zu entwickeln. Die Dummy-Daten haben wir im jeweiligen Vue Component wie folgt angelegt:
\begin{lstlisting}[language=JavaScript,caption=Dummy-Daten]
<script>
...
export default {
...
data() {
return {
timeRecords: [
{
id: 1,
start: "25.04.2020 / 8:00",
end: "25.04.2020 / 13:00",
time: "5:00",
type: "Paid"
},
{
id: 2,
start: "25.04.2020 / 13:00",
end: "25.04.2020 / 14:00",
time: "1:00",
type: "Lunch"
},
{
id: 3,
start: "25.04.2020 / 14:00 ",
end: "25.04.2020 / 16:30",
time: "2:30",
type: "Paid"
}
]
}
},
...
}
</script>
\end{lstlisting}
Durch Verwendung der Dummy-Daten war es ebenso möglich, Funktionsaufrufe zum Löschen oder Bearbeiten von Daten zu testen, ohne persistente Veränderungen an den Daten auszulösen. Durch neu laden der Seite sind die Dummy-Daten wiederhergestellt. Bei der Erstellung der Diagramme waren die Dummy-Daten ebenfalls wichtig, so konnten Formatierungsfunktionen für die Zeitanzeige getestet werden. Ebenfalls konnte so die optimale Größe und Anordnung der Diagramme bestimmt werden.\\
Durch die Verwendung von Dummy-Daten war der Umstieg auf die Livedaten nicht allzu schwer. Die Dummy-Daten konnten bei Anbindung an die Datenbank reibungslos durch Live-Daten aus der Datenbank ersetzt werden.
\subsection{Authentifizierung}
Wie schon im Backend beschrieben wurde, haben wir zur Authenfizierung JSON Web Token benutzt. Beim Login wurde das Token abgeholt und in den Sessionstorage geschrieben. Wir haben uns für den Sessionstorage entschieden, weil dieser beim Schließen des Browsertabs automatisch gelöscht wird. Der Logout Button entfernt ebenso das Token aus dem Storage.
\subsection{Abrufen der Daten in Listen}
Zum Abrufen der Daten nutzen wir "XMLHttpRequests". Diese geben vom Backend ein JSON Objekt zurück. Dies ermöglicht es uns, die JSON Funktionen von Java Script zu nutzen.
\begin{lstlisting}[language=JavaScript,caption=Get Request]
var xhttp = new XMLHttpRequest();
var today;
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
today = JSON.parse(xhttp.responseText);
today = today._embedded.records;
}
};
xhttp.open("GET", baseUri + "/records/search/today", false);
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
xhttp.send(null);
\end{lstlisting}
\subsection{Ändern und Hinzufügen von Daten}
Zum Ändern und Hinzufügen von Daten haben wir ebenfalls "XMLHttpRequests" genutzt. Zum Hinzufügen wurden Post Requests gesendet, zum Ändern Patch Requests.
\begin{lstlisting}[language=JavaScript, caption=Post Request]
xhttp.open("Post", baseUri + path, false);
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
xhttp.send(JSONData);
\end{lstlisting}
\begin{lstlisting}[language=JavaScript, caption=Patch Request]
xhttp.open("PATCH", baseUri + path, false);
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
xhttp.send(JSONData);
\end{lstlisting}
\subsection{Auswertung in Diagrammen}
Jeder Benutzer kann seine Daten in einer Übersicht zusammengefasst betrachten, hier verwenden wir folgende Diagramme:
\begin{itemize}
\item Kreisdiagramme
\begin{itemize}
\item Verhältnis von Arbeitszeit zu Pausenzeit von allen Accounts des Benutzers.
\item Verhältnis der Arbeitszeit je Timetrack Account des Benutzers mit Angabe des Gesamtverdienstes.
\item Verhältnis des Verdienstes je Timetrack Account des Benutzers.
\end{itemize}
\item Säulendiagramme
\begin{itemize}
\item Übersicht über die Letzten 7 Tage mit Arbeits- und Pausenzeit.
\item Übersicht über die Letzten 30 Tage mit Arbeits- und Pausenzeit.
\end{itemize}
\end{itemize}
Um Diagramme verwenden zu können, haben wir das Framework Apexcharts\footnote{\url{https://apexcharts.com/}} eingebunden, welches es ermöglicht, konfigurierbare Diagramme einzufügen.
Die Konfiguration des Säulendiagramms für die Ansicht der letzten 30 Tage ist nachfolgend dargestellt.
\begin{lstlisting}[language=JavaScript, caption=Konfiguration Säulendiagramm]
<script>
...
export default {
...
data() {
return {
series: [
{
name: "Working Hours",
data: []
},
{
name: "Pause Hours",
data: []
}
],
chartOptions: {
chart: {
type: "bar",
stacked: true,
background: "#202020",
toolbar: {
...
}
},
colors: ["#0096ff", "#e21d1f", "#546E7A", "#E91E63", "#FF9800"],
...
plotOptions: {
bar: {
horizontal: false,
columnWidth: "50%"
}
},
xaxis: {
type: "datetime",
categories: []
},
yaxis: {
labels: formatter: function(value) {
...
return hours + ":" + minutes + ":" + seconds;
}
},
...
}
};
},
\end{lstlisting}
Für die Kreisdiagramme war es notwendig, alle Zeiteinträge abzuholen, die Zeiten zu addieren und in die Datenfelder des Diagramms zu schreiben. Bei dem Kreisdiagramm, das die Accounts darstellt, war es notwendig, die Timetrack-Accounts des Benutzers abzufragen und für jeden ein Feld der addierten Zeit anzulegen und den Lohn in einer Variablen abzulegen.
\begin{lstlisting}[language=JavaScript, caption=Zuordnung der Zeit zu den Timetrack Accounts]
for (let index = 0; index < records.length; index++){
var record = records[index];
for (let indexAccs = 0; indexAccs < this.chartOptions.labels.length; indexAccs++) {
if (record.account == this.chartOptions.labels[indexAccs] && record.type == "PAID") {
this.series[indexAccs] += record.duration;
}
}
}
\end{lstlisting}
Ebenfalls war es notwendig, eine Funktion zu erstellen, die den gesamten Lohn des jeweiligen Accounts nach den ermittelten Stunden berechnet.\\
Bei den Säulendiagrammen müssen lediglich die nötigen Zeiteinträge beim zuständigen Endpoint angefragt werden. Dieser Endpoint liefert alle Einträge zwischen einem Startdatum und einem Enddatum. Hier wird immer das aktuelle Datum verwendet und die Zeitspanne entsprechend zurückgerechnet.\\
Um riesige Anfragen zu verhindern wird Paging verwendet, das heißt, es werden so oft 50 Einträge angefragt, bis die letzte Seite erreicht ist.
\subsection{Administrator Funktionalitäten}\label{subsection:frontend:admin}
Ein Administrator hat die Möglichkeit zur vollen Nutzerverwaltung. Er kann Nutzer löschen, und bearbeiten. Als Bearbeitungsmöglichkeiten hat er die Accountverwaltung von Nutzern, das Setzen der Arbeitslocation für einen Nutzer und das Ändern des Namens.
\section{Funktionen der Website}
\subsection{Home}
Die Home Seite hat zwei Ansichten. Wenn kein User angemeldet ist, sieht man lediglich einen Wilkommensgruß und hat die Möglichkeit, sich anzumelden. Wenn man angemeldet ist, sieht man seine persönlichen Informationen, die einem zugeordnete Abeitslocation, die Tagesarbeitszeit und die eigenen Accounts.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/frontend/home.PNG}
\caption{Home eingeloggt}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/frontend/HomeLoggedOut.PNG}
\caption{Home ausgeloggt}
\end{figure}
\subsection{Time Records}
Auf der Time Records Seite kann man die eigenen Arbeitszeiten einsehen. Außerdem hat man die Möglichkeit, fehlerhafte Einträge zu verbessern oder zu löschen, indem man über den Stift hovert. Neue Einträge können erstellt werden, indem man den "+"-Button am Ende der Seite anklickt.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/frontend/timerecords.PNG}
\caption{Time Records}
\end{figure}
\subsection{Statistics}
Auf der Statistics Seite sind die Daten der Time Records übersichtlich ausgewertet. Hier werden zwei verschiedene Diagrammtypen eingesetzt, um dem Benutzer eine bestmögliche Auswertung seiner Zeiteinträge zu bieten.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/frontend/statistics_pie.PNG}
\caption{Kreisdiagramme}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/frontend/statistics_column.PNG}
\caption{Säulendiagramme}
\end{figure}
\subsection{Accounts}
Die Accouts Seite bietet Möglichkeiten, um eigene Accounts einzusehen und zu verwalten. Es ist möglich, neue Accounts hinzuzufügen und bestehende Accounts zu löschen oder anzupassen.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/frontend/accounts.PNG}
\caption{Accounts}
\end{figure}
\subsection{Admin}
Die Admin Seite bietet einem die Möglichkeiten, welche in \ref{subsection:frontend:admin} beschrieben werden. Um die einzelnen Verwaltungsmöglichkeiten zu sehen, reicht es über den Stift zu hovern. Das linke Zeichen (rote Mülltonne) löscht den jeweiligen Nutzer, das mittlere (grünes Papier mit Stift) ist zum Ändern der Nutzerinformationen und der Position der Arbeitsstelle. Das rechte Zeichen (blaue Person mit drei Strichen) führt zur Accountverwaltung für den jeweiligen Nutzer.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{img/frontend/admin.PNG}
\caption{Admin}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth/2]{img/frontend/verwaltung.PNG}
\caption{Nutzerverwaltung}
\end{figure}
\section{Probleme und Lösungen}
\subsection{Diagramme}
Beim Erstellen der Säulendiagramme sind wir auf den Fehler gestoßen, dass der erste Eintrag von links nicht richtig angezeigt wird. Dieser Fehler ist den Entwicklern von Apexcharts bekannt, aber noch nicht behoben. Wir haben das Problem behoben, indem wir die Daten an der ersten Stelle entfernen. Dies führt zu einem kleinen Abstand, jedoch wird das Diagramm so optimal ohne fehlende Beschriftungen dargestellt.
\subsection{Custom Headers Chrome}
Ein weiteres unserer Probleme war, dass Chrome sich geweigert hat, auf den selbst erstellten Header zuzugreifen. Dieses Problem konnten wir im Backend lösen, indem wir den Header zu den "Access-Control-Expose-Headers" hinzugefügt haben.
\begin{lstlisting}[language=Java]
res.setHeader("Access-Control-Expose-Headers", "Authorization");
\end{lstlisting}
\subsection{Kein Patch möglich}
Beim Erstellen eines Patch Requests hatten wir das Problem, dass dieser verweigert wurde. Dies lag daran, dass in den Standard Cors-Konfigurationen nur Get, Head und Post erlaubt sind. Da wir noch Delete und Patch brauchen, haben wir alle Methoden erlaubt.
\begin{lstlisting}[language=Java]
final CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
configuration.addAllowedMethod("*");
\end{lstlisting}

View File

@ -1 +0,0 @@
\chapter{Projektbericht}

View File

@ -1 +0,0 @@
\chapter{Projektidee}

View File

@ -0,0 +1,22 @@
\chapter{Projektplanung}
\section{Ziel des Projekts}
Es sollte ein Projekt gebaut werden, das es ermöglicht, die Arbeitszeit über eine App zu tracken. Dies sollte aber nur möglich sein, wenn man sich in einem definierten Umkreis zu seinem Arbeitsplatz befindet. Des Weiteren sollte es eine Website zur Verwaltung geben.
\section{Definition des Workflows}
\subsection{Kommunikation}
Zur Kommunikation haben wir für kurze Fragen, sowie das Vereinbaren von Treffen, WhatsApp genutzt. Für Besprechungen haben wir TeamSpeak verwendet.
\subsection{Sprints}
Wir haben die Projektzeit in fünf zweiwöchige Arbeitssprints und einen einwöchigen Vorbereitungssprint aufgeteilt. Die Enddaten der Sprints waren dabei an die Treffen mit Professor Knauth angepasst.
\subsection{Code-Owners}
In unserem Git-Repository haben wir mit Code-Ownership gearbeitet. Dazu haben wir drei Ownerships eingeführt. Marcel Schwarz war Code-Owner für das Backend, Tobias Wieck für die Android App und Simon Kellner, sowie Tim Zieger für das Frontend. Wenn eine Änderung im jeweiligen Gebiet gemacht wurde, musste immer mindestens ein Code-Owner diese genehmigen.
\section{Sprintziele}
\subsection{Iteration 1}
Das Ziel des ersten Sprints war die Erlernung der notwendigen Technologien und die Schnittstellendefinition.
\subsection{Iteration 2}
Im zweiten Sprint sollten die Designgrundlagen und Feature Scopes besprochen werden. Des Weiteren sollte im Backend die Verbindung zwischen dem Backend und der Datenbank hergestellt werden. Im Frontend war geplant, weiter am Grundgerüst der Seite zu arbeiten und bei der App wurde die Einarbeitung weitergeführt.
\subsection{Iteration 3}
Im Frontend sollte im dritten Sprint ein neues Designframework eingeführt werden und die
Kommunikation mit dem Backend getestet werden. Der Plan für das Backend war die Erstellung der REST-Controller. Bei der Android App sollte die Login-Funktionalität, sowie das Einlesen der Geo-Informationen realisiert werden.
\subsection{Iteration 4}
Für den vierten Sprint war im Backend geplant, die letzten Controller und Endpoints zu entwickeln. Im Frontend sollten die restlichen Seiten mitsamt Datenabholung aus dem Backend entwickelt werden. Für die App sollte der Geofence entwickelt und die Kommunikation mit dem Backend aufgebaut werden.
\subsection{Iteration 5}
Das Ziel des letzten Sprint war die Fertigstellung des Projekts, die Erstellung der Dokumentation und die Vorbereitung der Präsentation.

View File

@ -1,11 +1,13 @@
\begin{titlepage}
\centering
\leftline{\includegraphics[width=0.35\linewidth]{img/hft_logo}} \par
\includegraphics[width=0.35\linewidth]{img/hft_logo}
\hfill
\includegraphics[width=0.22\linewidth]{img/gt_logo} \par
\textcolor{lightgray}{\rule{\linewidth}{1pt}}
\vspace{1cm}\\
{\scshape\LARGE Studiengang Informatik \\ Ubiquitous Computing \\ Sommersemester 2020 \par}
\vspace{2cm}
{\Huge\bfseries GeoTime \par}
{\Huge\bfseries Geo Timetracking \par}
{\LARGE Timetracking auf Standortbasis \par}
\vfill
{\Large \textsc{von}}

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