├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── de │ │ │ └── perflyst │ │ │ └── untis │ │ │ ├── activity │ │ │ ├── ActivityLogin.java │ │ │ ├── ActivityLoginDataInput.java │ │ │ ├── ActivityMain.java │ │ │ ├── ActivityPreferences.java │ │ │ ├── ActivityRoomFinder.java │ │ │ └── appcompat │ │ │ │ └── ActivityPreferences.java │ │ │ ├── adapter │ │ │ ├── AdapterCheckBoxGridView.java │ │ │ ├── AdapterFeatures.java │ │ │ ├── AdapterGridView.java │ │ │ ├── AdapterItemDetails.java │ │ │ ├── AdapterItemFeatures.java │ │ │ ├── AdapterItemRoomFinder.java │ │ │ ├── AdapterRoomFinder.java │ │ │ ├── AdapterTimetable.java │ │ │ └── AdapterTimetableHeader.java │ │ │ ├── dialog │ │ │ └── DialogItemDetailsFragment.java │ │ │ ├── fragment │ │ │ ├── FragmentDatePicker.java │ │ │ ├── FragmentTimetable.java │ │ │ ├── FragmentTimetableHeader.java │ │ │ └── FragmentTimetableItemDetails.java │ │ │ ├── notification │ │ │ ├── NotificationReceiver.java │ │ │ ├── NotificationSetup.java │ │ │ └── StartupReceiver.java │ │ │ ├── preference │ │ │ └── UnitPreference.java │ │ │ ├── utils │ │ │ ├── BetterToast.java │ │ │ ├── ColorPreferenceList.java │ │ │ ├── Constants.java │ │ │ ├── Conversions.java │ │ │ ├── DateOperations.java │ │ │ ├── DragSortController.java │ │ │ ├── ElementName.java │ │ │ ├── ListManager.java │ │ │ ├── PreferenceUtils.java │ │ │ ├── SessionInfo.java │ │ │ ├── StreamUtils.java │ │ │ ├── ThemeUtils.java │ │ │ ├── connectivity │ │ │ │ ├── DownloadTask.java │ │ │ │ ├── UntisAuthentication.java │ │ │ │ └── UntisRequest.java │ │ │ ├── lazyload │ │ │ │ ├── FileCache.java │ │ │ │ ├── ImageLoader.java │ │ │ │ ├── MemoryCache.java │ │ │ │ └── SimpleFloatViewManager.java │ │ │ └── timetable │ │ │ │ ├── TimegridUnitManager.java │ │ │ │ ├── Timetable.java │ │ │ │ ├── TimetableItemData.java │ │ │ │ └── TimetableSetup.java │ │ │ └── view │ │ │ ├── SortableListView.java │ │ │ ├── SortableListViewItem.java │ │ │ ├── TimetableItemBackground.java │ │ │ ├── VerticalTextView.java │ │ │ ├── VerticalViewPager.java │ │ │ └── behavior │ │ │ └── ScrollAwareFABBehavior.java │ └── res │ │ ├── drawable-hdpi │ │ └── ic_stat_timetable.png │ │ ├── drawable-mdpi │ │ └── ic_stat_timetable.png │ │ ├── drawable-v21 │ │ ├── ic_add_circle.xml │ │ ├── ic_arrow_left.xml │ │ ├── ic_arrow_right.xml │ │ ├── ic_calendar.xml │ │ ├── ic_delete.xml │ │ ├── ic_error.xml │ │ ├── ic_prefs_info.xml │ │ ├── ic_prefs_notifications.xml │ │ ├── ic_prefs_personal.xml │ │ ├── ic_prefs_roomfinder.xml │ │ ├── ic_prefs_styling.xml │ │ ├── ic_prefs_timetable.xml │ │ ├── non_selected_item_indicator_dot.xml │ │ ├── selected_item_indicator_dot.xml │ │ └── side_nav_bar.xml │ │ ├── drawable-xhdpi │ │ └── ic_stat_timetable.png │ │ ├── drawable-xxhdpi │ │ └── ic_stat_timetable.png │ │ ├── drawable-xxxhdpi │ │ └── ic_stat_timetable.png │ │ ├── drawable │ │ ├── ic_add.xml │ │ ├── ic_add_circle.xml │ │ ├── ic_arrow_left.xml │ │ ├── ic_arrow_right.xml │ │ ├── ic_calendar.xml │ │ ├── ic_check.xml │ │ ├── ic_classes.xml │ │ ├── ic_delete.xml │ │ ├── ic_drag_handle.xml │ │ ├── ic_error.xml │ │ ├── ic_failed.xml │ │ ├── ic_gift.xml │ │ ├── ic_info.xml │ │ ├── ic_prefs_info.xml │ │ ├── ic_prefs_notifications.xml │ │ ├── ic_prefs_personal.xml │ │ ├── ic_prefs_roomfinder.xml │ │ ├── ic_prefs_styling.xml │ │ ├── ic_prefs_timetable.xml │ │ ├── ic_room_available.xml │ │ ├── ic_room_occupied.xml │ │ ├── ic_rooms.xml │ │ ├── ic_search_rooms.xml │ │ ├── ic_settings.xml │ │ ├── ic_share.xml │ │ ├── ic_suggested_features.xml │ │ ├── ic_teacher.xml │ │ ├── non_selected_item_indicator_dot.xml │ │ ├── selected_item_indicator_dot.xml │ │ └── side_nav_bar.xml │ │ ├── layout │ │ ├── activity_login.xml │ │ ├── activity_login_data_input.xml │ │ ├── activity_main.xml │ │ ├── activity_room_finder.xml │ │ ├── app_bar_login.xml │ │ ├── app_bar_login_data_input.xml │ │ ├── app_bar_main.xml │ │ ├── borderless_button.xml │ │ ├── content_header.xml │ │ ├── content_login.xml │ │ ├── content_login_data_input.xml │ │ ├── content_main.xml │ │ ├── content_timetable.xml │ │ ├── dialog_timetable_item_detail_page.xml │ │ ├── dialog_timetable_item_detail_page_info.xml │ │ ├── dialog_timetable_item_details.xml │ │ ├── grid_view_item_checkboxes.xml │ │ ├── item_day.xml │ │ ├── item_hour.xml │ │ ├── list_item_features.xml │ │ ├── list_item_features_text.xml │ │ ├── list_item_room_finder.xml │ │ ├── nav_header_main.xml │ │ └── table_item.xml │ │ ├── menu-v21 │ │ └── activity_main_drawer.xml │ │ ├── menu │ │ ├── activity_main_drawer.xml │ │ └── main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── .strings.xml.swp │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── defaults.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── static.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ ├── xml-de │ │ └── remote_config_defaults.xml │ │ └── xml │ │ ├── backup_descriptor.xml │ │ ├── prefs_about.xml │ │ ├── prefs_account.xml │ │ ├── prefs_headers.xml │ │ ├── prefs_notifications.xml │ │ ├── prefs_roomfinder.xml │ │ ├── prefs_styling.xml │ │ ├── prefs_timetable.xml │ │ ├── provider_paths.xml │ │ └── remote_config_defaults.xml │ └── test │ └── java │ └── de │ └── perflyst │ └── untis │ └── test │ ├── ConversionsTest.java │ ├── DateOperationsTest.java │ ├── ElementNameTest.java │ └── SessionInfoTest.java ├── build.gradle ├── color-picker-view ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── danielnilsson9 │ │ └── colorpickerview │ │ ├── ColorUtils.java │ │ ├── dialog │ │ └── ColorPickerDialogFragment.java │ │ ├── drawable │ │ └── AlphaPatternDrawable.java │ │ ├── preference │ │ └── ColorPreference.java │ │ └── view │ │ ├── ColorPanelView.java │ │ ├── ColorPickerView.java │ │ └── DrawingUtils.java │ └── res │ ├── drawable │ ├── colorpickerview__btn_background.xml │ └── colorpickerview__btn_background_pressed.xml │ ├── layout │ ├── colorpickerview__dialog_color_picker.xml │ └── colorpickerview__preference_preview_layout.xml │ ├── values-de │ └── strings.xml │ ├── values-sw360dp │ ├── dimen.xml │ └── styles.xml │ ├── values-sw400dp │ ├── dimen.xml │ └── styles.xml │ ├── values-sw600dp │ └── styles.xml │ ├── values-v21 │ └── styles.xml │ └── values │ ├── attrs.xml │ ├── dimen.xml │ ├── ids.xml │ ├── strings.xml │ └── styles.xml ├── docs └── setup-proxy.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/android,osx,windows,linux,intellij,java 2 | 3 | ### Android ### 4 | # Generated files 5 | bin/ 6 | gen/ 7 | out/ 8 | 9 | # Gradle files 10 | .gradle/ 11 | build/ 12 | app/release 13 | 14 | # Local configuration file (sdk path, etc) 15 | local.properties 16 | 17 | # Log Files 18 | *.log 19 | 20 | # Android Studio Navigation editor temp files 21 | .navigation/ 22 | 23 | # Android Studio captures folder 24 | captures/ 25 | 26 | # Intellij 27 | *.iml 28 | .idea/ 29 | 30 | # Keystore files 31 | *.jks 32 | 33 | # External native build folder generated in Android Studio 2.2 and later 34 | .externalNativeBuild 35 | 36 | # Freeline 37 | freeline.py 38 | freeline/ 39 | freeline_project_description.json 40 | 41 | ### Android Patch ### 42 | gen-external-apklibs 43 | 44 | ### IntelliJ ### 45 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 46 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 47 | 48 | # User-specific stuff: 49 | .idea/**/tasks.xml 50 | 51 | # Sensitive or high-churn files: 52 | .idea/**/dataSources/ 53 | .idea/**/dataSources.ids 54 | .idea/**/dataSources.xml 55 | .idea/**/dataSources.local.xml 56 | .idea/**/sqlDataSources.xml 57 | .idea/**/dynamic.xml 58 | .idea/**/uiDesigner.xml 59 | 60 | # Mongo Explorer plugin: 61 | .idea/**/mongoSettings.xml 62 | 63 | ## File-based project format: 64 | *.iws 65 | 66 | ## Plugin-specific files: 67 | 68 | # IntelliJ 69 | /out/ 70 | 71 | # mpeltonen/sbt-idea plugin 72 | .idea_modules/ 73 | 74 | # JIRA plugin 75 | atlassian-ide-plugin.xml 76 | 77 | # Cursive Clojure plugin 78 | .idea/replstate.xml 79 | 80 | ### Intellij Patch ### 81 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 82 | 83 | # *.iml 84 | # modules.xml 85 | # .idea/misc.xml 86 | # *.ipr 87 | 88 | # Sonarlint plugin 89 | .idea/sonarlint 90 | 91 | ### Java ### 92 | # Compiled class file 93 | 94 | # Log file 95 | 96 | # BlueJ files 97 | *.ctxt 98 | 99 | # Mobile Tools for Java (J2ME) 100 | .mtj.tmp/ 101 | 102 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 103 | hs_err_pid* 104 | 105 | ### Linux ### 106 | *~ 107 | 108 | # temporary files which can be created if a process still has a handle open of a deleted file 109 | .fuse_hidden* 110 | 111 | # KDE directory preferences 112 | .directory 113 | 114 | # Linux trash folder which might appear on any partition or disk 115 | .Trash-* 116 | 117 | # .nfs files are created when an open file is removed but is still being accessed 118 | .nfs* 119 | 120 | ### OSX ### 121 | *.DS_Store 122 | .AppleDouble 123 | .LSOverride 124 | 125 | # Icon must end with two \r 126 | Icon 127 | 128 | 129 | # Thumbnails 130 | ._* 131 | 132 | # Files that might appear in the root of a volume 133 | .DocumentRevisions-V100 134 | .fseventsd 135 | .Spotlight-V100 136 | .TemporaryItems 137 | .Trashes 138 | .VolumeIcon.icns 139 | .com.apple.timemachine.donotpresent 140 | 141 | # Directories potentially created on remote AFP share 142 | .AppleDB 143 | .AppleDesktop 144 | Network Trash Folder 145 | Temporary Items 146 | .apdisk 147 | 148 | ### Windows ### 149 | # Windows thumbnail cache files 150 | Thumbs.db 151 | ehthumbs.db 152 | ehthumbs_vista.db 153 | 154 | # Folder config file 155 | Desktop.ini 156 | 157 | # Recycle Bin used on file shares 158 | $RECYCLE.BIN/ 159 | 160 | # Windows Installer files 161 | *.cab 162 | *.msi 163 | *.msm 164 | *.msp 165 | 166 | # Windows shortcuts 167 | *.lnk 168 | 169 | # End of https://www.gitignore.io/api/android,osx,windows,linux,intellij,java 170 | /.idea/caches 171 | /keys.tar 172 | /app/fabric.properties 173 | /app/google-services.json 174 | /.idea/codeStyles/ 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenUntis 2 | 3 | Fork of [SapuSeven BetterUntis-Legacy](https://github.com/SapuSeven/BetterUntis-Legacy). 4 | An alternative FLOSS Android client for the Untis timetable system. 5 | 6 | OpenUntis will be replaced with the [new BetterUntis](https://github.com/SapuSeven/BetterUntis/). If you want to contribute or add new features please do it there. 7 | 8 | [Get it on F-Droid](https://f-droid.org/packages/de.perflyst.untis/) 11 | 12 | 13 | ## Proxied WebUntis 14 | 15 | For more privacy (no transmission of IP address or useragent) you can use following domain instead of the normal webuntis.com domains. 16 | Please replace `server` with your webuntis server: `server.openuntis.perflyst.de` 17 | 18 | Example: 19 | `demo.webuntis.com` -> `demo.openuntis.perflyst.de` 20 | 21 | Please note: I have full control over the connection and therefore I could inject malicious code, read your password or analyze the data you transmit. 22 | I do not process nor look into any of your data which is transmitted. **Access logs are disabled!** Only **error logs are enabled**, no user or password is ever logged, in order to debug issues which may occur. They are **deleted** at least **after 7 days**. 23 | 24 | To setup this yourself, check the [proxied WebUntis documentation](https://github.com/Perflyst/OpenUntis/blob/master/docs/setup-proxy.md). 25 | 26 | ## Anti Features 27 | webuntis.com, non-free network service 28 | 29 | 30 | ## Installation 31 | If there are release builds available, you can download them from the [release page](https://github.com/Perflyst/OpenUntis/releases). 32 | Alternatively, you can build the app yourself. 33 | 34 | 35 | ### Others 36 | Feel free to submit issues or pull requests! 37 | 38 | Original Author: SapuSeven 39 | FLOSS maintainer: Perflyst 40 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | buildToolsVersion '27.0.3' 6 | defaultConfig { 7 | applicationId "de.perflyst.untis" 8 | minSdkVersion 16 9 | targetSdkVersion 27 10 | versionCode 16 11 | versionName "2.2.0" 12 | vectorDrawables.useSupportLibrary = true 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | debug { 20 | minifyEnabled false 21 | applicationIdSuffix '.debug' 22 | versionNameSuffix '-DEBUG' 23 | } 24 | } 25 | useLibrary 'org.apache.http.legacy' 26 | compileOptions { 27 | targetCompatibility 1.8 28 | sourceCompatibility 1.8 29 | } 30 | testOptions { 31 | unitTests.returnDefaultValues = true 32 | } 33 | return void 34 | } 35 | 36 | dependencies { 37 | implementation project(':color-picker-view') 38 | implementation 'com.android.support:appcompat-v7:27.1.1' 39 | implementation 'com.android.support:design:27.1.1' 40 | implementation 'com.android.support:recyclerview-v7:27.1.1' 41 | implementation 'com.android.support:support-v4:27.1.1' 42 | implementation 'com.android.support:support-vector-drawable:27.1.1' 43 | implementation 'commons-codec:commons-codec:1.6' 44 | implementation 'org.jetbrains:annotations-java5:15.0' 45 | testImplementation 'junit:junit:4.12' 46 | testImplementation 'org.mockito:mockito-core:1.10.19' 47 | testImplementation 'org.hamcrest:hamcrest-library:1.3' 48 | testImplementation 'org.json:json:20180130' 49 | } 50 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/paul/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | #-renamesourcefileattribute SourceFile 20 | #-keepattributes SourceFile,LineNumberTable 21 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | BetaUntis 4 | -------------------------------------------------------------------------------- /app/src/debug/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | BetaUntis 4 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BetaUntis 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 44 | 45 | 49 | 50 | 51 | 56 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 76 | 79 | 80 | 81 | 86 | 89 | 90 | 91 | 96 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/activity/ActivityLogin.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.activity; 2 | 3 | import android.content.ActivityNotFoundException; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.widget.Button; 10 | 11 | import de.perflyst.untis.R; 12 | 13 | public class ActivityLogin extends AppCompatActivity { 14 | 15 | private static final int REQUEST_SCAN_CODE = 0x00000001; 16 | private static final int REQUEST_LOGIN = 0x00000002; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_login); 22 | Toolbar toolbar = findViewById(R.id.toolbar); 23 | setSupportActionBar(toolbar); 24 | 25 | Button btnScanCode = findViewById(R.id.btnScanCode); 26 | Button btnManualDataInput = findViewById(R.id.btnManualDataInput); 27 | 28 | btnManualDataInput.setOnClickListener(v -> { 29 | Intent i = new Intent(ActivityLogin.this, ActivityLoginDataInput.class); 30 | startActivityForResult(i, REQUEST_LOGIN); 31 | }); 32 | 33 | btnScanCode.setOnClickListener(v -> { 34 | try { 35 | Intent intent = new Intent("com.google.zxing.client.android.SCAN"); 36 | intent.putExtra("SCAN_MODE", "QR_CODE_MODE"); 37 | startActivityForResult(intent, REQUEST_SCAN_CODE); 38 | } catch (ActivityNotFoundException | SecurityException e) { 39 | Intent i = new Intent(Intent.ACTION_VIEW); 40 | i.setData(Uri.parse("https://f-droid.org/en/packages/com.google.zxing.client.android/")); 41 | startActivity(i); 42 | } 43 | }); 44 | } 45 | 46 | public void onActivityResult(int requestCode, int resultCode, Intent intent) { 47 | switch (requestCode) { 48 | case REQUEST_SCAN_CODE: 49 | if (resultCode == RESULT_OK) { 50 | Intent i = new Intent(ActivityLogin.this, ActivityLoginDataInput.class); 51 | i.setData(Uri.parse(intent.getStringExtra("SCAN_RESULT"))); 52 | startActivityForResult(i, REQUEST_LOGIN); 53 | } 54 | break; 55 | case REQUEST_LOGIN: 56 | Intent i = new Intent(ActivityLogin.this, ActivityMain.class); 57 | startActivity(i); 58 | finish(); 59 | break; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/activity/appcompat/ActivityPreferences.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.activity.appcompat; 2 | 3 | import android.content.res.Configuration; 4 | import android.os.Bundle; 5 | import android.preference.PreferenceActivity; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.annotation.NonNull; 8 | import android.support.v7.app.ActionBar; 9 | import android.support.v7.app.AppCompatDelegate; 10 | import android.view.MenuInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | 14 | /** 15 | * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls 16 | * to be used with AppCompat. 17 | */ 18 | public abstract class ActivityPreferences extends PreferenceActivity { 19 | 20 | private AppCompatDelegate mDelegate; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | getDelegate().installViewFactory(); 25 | getDelegate().onCreate(savedInstanceState); 26 | super.onCreate(savedInstanceState); 27 | } 28 | 29 | @Override 30 | protected void onPostCreate(Bundle savedInstanceState) { 31 | super.onPostCreate(savedInstanceState); 32 | getDelegate().onPostCreate(savedInstanceState); 33 | } 34 | 35 | protected ActionBar getSupportActionBar() { 36 | return getDelegate().getSupportActionBar(); 37 | } 38 | 39 | @NonNull 40 | @Override 41 | public MenuInflater getMenuInflater() { 42 | return getDelegate().getMenuInflater(); 43 | } 44 | 45 | @Override 46 | public void setContentView(@LayoutRes int layoutResId) { 47 | getDelegate().setContentView(layoutResId); 48 | } 49 | 50 | @Override 51 | public void setContentView(View view) { 52 | getDelegate().setContentView(view); 53 | } 54 | 55 | @Override 56 | public void setContentView(View view, ViewGroup.LayoutParams params) { 57 | getDelegate().setContentView(view, params); 58 | } 59 | 60 | @Override 61 | public void addContentView(View view, ViewGroup.LayoutParams params) { 62 | getDelegate().addContentView(view, params); 63 | } 64 | 65 | @Override 66 | protected void onPostResume() { 67 | super.onPostResume(); 68 | getDelegate().onPostResume(); 69 | } 70 | 71 | @Override 72 | protected void onTitleChanged(CharSequence title, int color) { 73 | super.onTitleChanged(title, color); 74 | getDelegate().setTitle(title); 75 | } 76 | 77 | @Override 78 | public void onConfigurationChanged(Configuration newConfig) { 79 | super.onConfigurationChanged(newConfig); 80 | getDelegate().onConfigurationChanged(newConfig); 81 | } 82 | 83 | @Override 84 | protected void onStop() { 85 | super.onStop(); 86 | getDelegate().onStop(); 87 | } 88 | 89 | @Override 90 | protected void onDestroy() { 91 | super.onDestroy(); 92 | getDelegate().onDestroy(); 93 | } 94 | 95 | public void invalidateOptionsMenu() { 96 | getDelegate().invalidateOptionsMenu(); 97 | } 98 | 99 | private AppCompatDelegate getDelegate() { 100 | if (mDelegate == null) { 101 | mDelegate = AppCompatDelegate.create(this, null); 102 | } 103 | return mDelegate; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterCheckBoxGridView.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | import android.widget.CheckBox; 10 | import android.widget.Filter; 11 | import android.widget.Filterable; 12 | 13 | import de.perflyst.untis.R; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public class AdapterCheckBoxGridView extends BaseAdapter implements Filterable { 19 | private final List originalItems; 20 | private final ItemFilter filter = new ItemFilter(); 21 | private final LayoutInflater inflater; 22 | private final List selectedItems; 23 | private List filteredItems; 24 | 25 | public AdapterCheckBoxGridView(@NonNull Context context, @NonNull List objects) { 26 | filteredItems = objects; 27 | originalItems = new ArrayList<>(filteredItems); 28 | selectedItems = new ArrayList<>(); 29 | inflater = LayoutInflater.from(context); 30 | } 31 | 32 | @NonNull 33 | public Filter getFilter() { 34 | return filter; 35 | } 36 | 37 | @Override 38 | public int getCount() { 39 | return filteredItems.size(); 40 | } 41 | 42 | @Override 43 | public Object getItem(int position) { 44 | return position; 45 | } 46 | 47 | @Override 48 | public long getItemId(int position) { 49 | return position; 50 | } 51 | 52 | @Override 53 | public View getView(final int position, View convertView, ViewGroup parent) { 54 | Holder holder = new Holder(); 55 | if (convertView == null) 56 | convertView = inflater.inflate(R.layout.grid_view_item_checkboxes, parent, false); 57 | holder.checkBox = convertView.findViewById(R.id.checkbox); 58 | holder.checkBox.setText(filteredItems.get(position)); 59 | holder.checkBox.setOnCheckedChangeListener(null); 60 | holder.checkBox.setChecked(selectedItems.contains(filteredItems.get(position))); 61 | holder.checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { 62 | if (isChecked && !selectedItems.contains(filteredItems.get(position))) 63 | selectedItems.add(filteredItems.get(position)); 64 | else if (selectedItems.contains(filteredItems.get(position))) 65 | selectedItems.remove(filteredItems.get(position)); 66 | }); 67 | return convertView; 68 | } 69 | 70 | public List getSelectedItems() { 71 | return selectedItems; 72 | } 73 | 74 | private class Holder { 75 | CheckBox checkBox; 76 | } 77 | 78 | private class ItemFilter extends Filter { 79 | @Override 80 | protected void publishResults(CharSequence constraint, FilterResults results) { 81 | filteredItems.clear(); 82 | try { 83 | //noinspection unchecked 84 | filteredItems.addAll((List) results.values); 85 | } catch (ClassCastException e) { 86 | filteredItems.addAll(originalItems); 87 | } 88 | notifyDataSetChanged(); 89 | } 90 | 91 | @Override 92 | protected FilterResults performFiltering(CharSequence constraint) { 93 | FilterResults results = new FilterResults(); 94 | List filteredList = new ArrayList<>(); 95 | 96 | if (filteredItems == null) { 97 | filteredItems = new ArrayList<>(filteredList); 98 | } 99 | 100 | if (constraint == null || constraint.length() == 0) { 101 | results.count = originalItems.size(); 102 | results.values = originalItems; 103 | } else { 104 | constraint = constraint.toString().toLowerCase(); 105 | for (int i = 0; i < originalItems.size(); i++) { 106 | String data = originalItems.get(i); 107 | if (data.toLowerCase().contains(constraint.toString().toLowerCase())) 108 | filteredList.add(data); 109 | 110 | } 111 | results.count = filteredList.size(); 112 | results.values = filteredList; 113 | } 114 | return results; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterFeatures.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | import android.widget.TextView; 9 | 10 | import de.perflyst.untis.R; 11 | 12 | import java.util.List; 13 | 14 | public class AdapterFeatures extends BaseAdapter { 15 | private static LayoutInflater inflater = null; 16 | private final List data; 17 | 18 | public AdapterFeatures(Context context, List data) { 19 | this.data = data; 20 | inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 21 | } 22 | 23 | @Override 24 | public int getCount() { 25 | return data.size(); 26 | } 27 | 28 | @Override 29 | public Object getItem(int position) { 30 | return data.get(position); 31 | } 32 | 33 | @Override 34 | public long getItemId(int position) { 35 | return position; 36 | } 37 | 38 | @Override 39 | public View getView(final int position, final View convertView, ViewGroup parent) { 40 | View v = convertView; 41 | 42 | if (data.get(position).getLabel() != null) { 43 | if (v == null || !(v instanceof TextView)) 44 | v = inflater.inflate(R.layout.list_item_features_text, parent, false); 45 | 46 | ((TextView) v.findViewById(R.id.tvText)).setText(data.get(position).getLabel()); 47 | } else { 48 | v = inflater.inflate(R.layout.list_item_features, parent, false); 49 | 50 | ((TextView) v.findViewById(R.id.tvTitle)).setText(data.get(position).getTitle()); 51 | ((TextView) v.findViewById(R.id.tvDesc)).setText(data.get(position).getDesc()); 52 | } 53 | 54 | return v; 55 | } 56 | 57 | public void remove(int index) { 58 | data.remove(index); 59 | } 60 | 61 | public void insert(AdapterItemFeatures item, int index) { 62 | data.add(index, item); 63 | } 64 | 65 | public List getData() { 66 | return data; 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterGridView.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.widget.ArrayAdapter; 6 | import android.widget.Filter; 7 | import android.widget.Filterable; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class AdapterGridView extends ArrayAdapter implements Filterable { 13 | private final List originalItems; 14 | private final ItemFilter filter = new ItemFilter(); 15 | private List filteredItems; 16 | 17 | public AdapterGridView(@NonNull Context context, @NonNull List objects) { 18 | super(context, android.R.layout.simple_list_item_1, objects); 19 | filteredItems = objects; 20 | originalItems = new ArrayList<>(filteredItems); 21 | } 22 | 23 | @NonNull 24 | public Filter getFilter() { 25 | return filter; 26 | } 27 | 28 | private class ItemFilter extends Filter { 29 | @Override 30 | protected void publishResults(CharSequence constraint, FilterResults results) { 31 | filteredItems.clear(); 32 | try { 33 | //noinspection unchecked 34 | filteredItems.addAll((List) results.values); 35 | } catch (ClassCastException e) { 36 | filteredItems.addAll(originalItems); 37 | } 38 | notifyDataSetChanged(); 39 | } 40 | 41 | @Override 42 | protected FilterResults performFiltering(CharSequence constraint) { 43 | FilterResults results = new FilterResults(); 44 | List filteredList = new ArrayList<>(); 45 | 46 | if (filteredItems == null) { 47 | filteredItems = new ArrayList<>(filteredList); 48 | } 49 | 50 | if (constraint == null || constraint.length() == 0) { 51 | results.count = originalItems.size(); 52 | results.values = originalItems; 53 | } else { 54 | constraint = constraint.toString().toLowerCase(); 55 | for (int i = 0; i < originalItems.size(); i++) { 56 | String data = originalItems.get(i); 57 | if (data.toLowerCase().contains(constraint.toString().toLowerCase())) 58 | filteredList.add(data); 59 | 60 | } 61 | results.count = filteredList.size(); 62 | results.values = filteredList; 63 | } 64 | return results; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterItemDetails.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | 4 | import android.support.v4.app.Fragment; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v4.app.FragmentPagerAdapter; 7 | 8 | import de.perflyst.untis.fragment.FragmentTimetableItemDetails; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class AdapterItemDetails extends FragmentPagerAdapter { 14 | private final List mFragmentCollection = new ArrayList<>(); 15 | 16 | public AdapterItemDetails(FragmentManager fm) { 17 | super(fm); 18 | } 19 | 20 | public void addFragment(FragmentTimetableItemDetails fragment) { 21 | mFragmentCollection.add(fragment); 22 | } 23 | 24 | @Override 25 | public Fragment getItem(int position) { 26 | return mFragmentCollection.get(position); 27 | } 28 | 29 | @Override 30 | public int getCount() { 31 | return mFragmentCollection.size(); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterItemFeatures.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.content.Context; 4 | 5 | import de.perflyst.untis.view.SortableListViewItem; 6 | 7 | public class AdapterItemFeatures extends SortableListViewItem { 8 | private String title; 9 | private String desc; 10 | private int id; 11 | private String label; 12 | 13 | public AdapterItemFeatures(Context context) { 14 | super(context); 15 | } 16 | 17 | public int getId() { 18 | return id; 19 | } 20 | 21 | public void setId(int id) { 22 | this.id = id; 23 | } 24 | 25 | public String getTitle() { 26 | return title; 27 | } 28 | 29 | public void setTitle(String title) { 30 | this.title = title; 31 | } 32 | 33 | public String getDesc() { 34 | return desc; 35 | } 36 | 37 | public void setDesc(String desc) { 38 | this.desc = desc; 39 | } 40 | 41 | public String getLabel() { 42 | return label; 43 | } 44 | 45 | public void setLabel(String label) { 46 | this.label = label; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterItemRoomFinder.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import de.perflyst.untis.activity.ActivityRoomFinder; 6 | 7 | import java.util.Calendar; 8 | 9 | import static de.perflyst.untis.utils.DateOperations.getStartDateFromWeek; 10 | 11 | public class AdapterItemRoomFinder implements Comparable { 12 | static final int STATE_OCCUPIED = 0; 13 | static final int STATE_FREE = 1; 14 | private static final int STATE_LOADING = -1; 15 | private final String name; 16 | private final ActivityRoomFinder roomFinderActivity; 17 | private boolean[] states; 18 | private boolean loading; 19 | private long date; 20 | 21 | public AdapterItemRoomFinder(ActivityRoomFinder roomFinderActivity, String name, boolean loading) { 22 | this.roomFinderActivity = roomFinderActivity; 23 | this.name = name; 24 | this.loading = loading; 25 | } 26 | 27 | private long getDate() { 28 | return date; 29 | } 30 | 31 | public void setDate(long date) { 32 | this.date = date; 33 | } 34 | 35 | private int getIndex() { 36 | return roomFinderActivity.getCurrentHourIndex(); 37 | } 38 | 39 | public void setStates(boolean[] states) { 40 | this.states = states; 41 | } 42 | 43 | public String getName() { 44 | return name; 45 | } 46 | 47 | int getState(int index) { 48 | if (isLoading()) 49 | return STATE_LOADING; 50 | int i = 0, hours = 0; 51 | while (index + i < states.length && !states[index + i]) { 52 | hours++; 53 | i++; 54 | } 55 | return hours; 56 | } 57 | 58 | @Override 59 | public int compareTo(@NonNull AdapterItemRoomFinder o) { 60 | int state1 = getState(getIndex()); 61 | int state2 = o.getState(o.getIndex()); 62 | 63 | if (state1 < state2) 64 | return 1; 65 | else if (state1 > state2) 66 | return -1; 67 | else 68 | return getName().compareTo(o.getName()); 69 | } 70 | 71 | @Override 72 | public int hashCode() { 73 | return name.hashCode(); 74 | } 75 | 76 | @Override 77 | public boolean equals(Object o) { 78 | return o instanceof AdapterItemRoomFinder && ((AdapterItemRoomFinder) o).getName().equals(getName()); 79 | } 80 | 81 | boolean isLoading() { 82 | return loading; 83 | } 84 | 85 | public void setLoading() { 86 | this.loading = true; 87 | } 88 | 89 | public boolean isOutdated() { 90 | return getDate() != getStartDateFromWeek(Calendar.getInstance(), 0, true).getTimeInMillis(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterRoomFinder.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.AppCompatImageView; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageButton; 10 | import android.widget.ProgressBar; 11 | import android.widget.TextView; 12 | 13 | import de.perflyst.untis.R; 14 | import de.perflyst.untis.activity.ActivityRoomFinder; 15 | 16 | import java.util.ArrayList; 17 | 18 | public class AdapterRoomFinder extends RecyclerView.Adapter { 19 | private final ActivityRoomFinder activity; 20 | private final ArrayList roomList; 21 | 22 | public AdapterRoomFinder(ActivityRoomFinder activity, ArrayList roomList) { 23 | this.activity = activity; 24 | this.roomList = roomList; 25 | 26 | setHasStableIds(true); 27 | } 28 | 29 | @NonNull 30 | @Override 31 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 32 | View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_room_finder, parent, false); 33 | v.setOnClickListener(activity); 34 | return new ViewHolder(v); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { 39 | int currentHourIndex = activity.getCurrentHourIndex(); 40 | AdapterItemRoomFinder room = roomList.get(position); 41 | 42 | holder.tvName.setText(room.getName()); 43 | 44 | if (room.getState(currentHourIndex) == AdapterItemRoomFinder.STATE_OCCUPIED) 45 | holder.tvDetails.setText(activity.getResources().getString(R.string.room_desc_occupied)); 46 | else if (room.getState(currentHourIndex) >= AdapterItemRoomFinder.STATE_FREE) 47 | holder.tvDetails.setText(activity.getResources().getQuantityString(R.plurals.room_desc, room.getState(currentHourIndex), room.getState(currentHourIndex))); 48 | else 49 | holder.tvDetails.setText(activity.getResources().getString(R.string.loading_data)); 50 | 51 | if (room.getState(currentHourIndex) >= AdapterItemRoomFinder.STATE_FREE && !room.isLoading()) { 52 | holder.ivState.setImageResource(R.drawable.ic_room_available); 53 | holder.ivState.setVisibility(View.VISIBLE); 54 | holder.pbState.setVisibility(View.GONE); 55 | holder.btnRoomExpired.setVisibility(room.isOutdated() ? View.VISIBLE : View.GONE); 56 | } else if (room.getState(currentHourIndex) == AdapterItemRoomFinder.STATE_OCCUPIED && !room.isLoading()) { 57 | holder.ivState.setImageResource(R.drawable.ic_room_occupied); 58 | holder.ivState.setVisibility(View.VISIBLE); 59 | holder.pbState.setVisibility(View.GONE); 60 | holder.btnRoomExpired.setVisibility(room.isOutdated() ? View.VISIBLE : View.GONE); 61 | } else { 62 | holder.ivState.setVisibility(View.GONE); 63 | holder.pbState.setVisibility(View.VISIBLE); 64 | holder.btnRoomExpired.setVisibility(View.GONE); 65 | } 66 | 67 | holder.btnDelete.setOnClickListener(v -> activity.showDeleteItemDialog(holder.getAdapterPosition())); 68 | 69 | holder.btnRoomExpired.setOnClickListener(v -> activity.refreshItem(holder.getAdapterPosition())); 70 | } 71 | 72 | @Override 73 | public int getItemCount() { 74 | return roomList.size(); 75 | } 76 | 77 | @Override 78 | public long getItemId(int position) { 79 | return roomList.get(position).hashCode(); 80 | } 81 | 82 | static class ViewHolder extends RecyclerView.ViewHolder { 83 | final TextView tvName; 84 | final TextView tvDetails; 85 | final AppCompatImageView ivState; 86 | final ProgressBar pbState; 87 | final ImageButton btnDelete; 88 | final ImageButton btnRoomExpired; 89 | 90 | ViewHolder(View rootView) { 91 | super(rootView); 92 | 93 | tvName = rootView.findViewById(R.id.tvName); 94 | tvDetails = rootView.findViewById(R.id.tvDetails); 95 | ivState = rootView.findViewById(R.id.ivState); 96 | pbState = rootView.findViewById(R.id.pbState); 97 | btnDelete = rootView.findViewById(R.id.btnDelete); 98 | btnRoomExpired = rootView.findViewById(R.id.btnRoomExpired); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterTimetable.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.app.FragmentManager; 7 | import android.support.v4.app.FragmentStatePagerAdapter; 8 | 9 | import de.perflyst.untis.fragment.FragmentTimetable; 10 | 11 | public class AdapterTimetable extends FragmentStatePagerAdapter { 12 | public AdapterTimetable(FragmentManager fm) { 13 | super(fm); 14 | } 15 | 16 | @Override 17 | public int getItemPosition(@NonNull Object object) { 18 | return POSITION_NONE; 19 | } 20 | 21 | @Override 22 | public Fragment getItem(int position) { 23 | FragmentTimetable fragment = new FragmentTimetable(); 24 | Bundle args = new Bundle(); 25 | args.putInt("position", position); 26 | fragment.setArguments(args); 27 | return fragment; 28 | } 29 | 30 | @Override 31 | public int getCount() { 32 | return 100; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/adapter/AdapterTimetableHeader.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.adapter; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v4.app.FragmentStatePagerAdapter; 7 | 8 | import de.perflyst.untis.fragment.FragmentTimetableHeader; 9 | 10 | public class AdapterTimetableHeader extends FragmentStatePagerAdapter { 11 | public AdapterTimetableHeader(FragmentManager fm) { 12 | super(fm); 13 | } 14 | 15 | @Override 16 | public Fragment getItem(int position) { 17 | FragmentTimetableHeader fragment = new FragmentTimetableHeader(); 18 | Bundle args = new Bundle(); 19 | args.putInt("position", position); 20 | fragment.setArguments(args); 21 | return fragment; 22 | } 23 | 24 | @Override 25 | public int getCount() { 26 | return 100; 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/dialog/DialogItemDetailsFragment.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.dialog; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.DialogFragment; 7 | import android.support.v4.view.ViewPager; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ImageView; 12 | import android.widget.LinearLayout; 13 | 14 | import de.perflyst.untis.R; 15 | import de.perflyst.untis.activity.ActivityMain; 16 | import de.perflyst.untis.adapter.AdapterItemDetails; 17 | import de.perflyst.untis.fragment.FragmentTimetable; 18 | import de.perflyst.untis.fragment.FragmentTimetableItemDetails; 19 | import de.perflyst.untis.utils.timetable.TimetableItemData; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | public class DialogItemDetailsFragment extends DialogFragment implements ViewPager.OnPageChangeListener { 25 | private ViewPager mPager; 26 | private LinearLayout mPagerIndicator; 27 | private ImageView[] dots; 28 | private List mItems = new ArrayList<>(); 29 | private FragmentTimetable mFragment; 30 | private ActivityMain mMainActivity; 31 | 32 | public void setItems(ArrayList timetableItemDataArray) { 33 | mItems = timetableItemDataArray; 34 | } 35 | 36 | public void setFragment(FragmentTimetable fragment) { 37 | mFragment = fragment; 38 | } 39 | 40 | 41 | public void setMainActivity(ActivityMain main) { 42 | mMainActivity = main; 43 | } 44 | 45 | @Nullable 46 | @Override 47 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 48 | View rootView = inflater.inflate(R.layout.dialog_timetable_item_details, container, false); 49 | mPager = rootView.findViewById(R.id.viewpager); 50 | mPagerIndicator = rootView.findViewById(R.id.viewpagerIndicator); 51 | AdapterItemDetails adapter = new AdapterItemDetails(getChildFragmentManager()); 52 | for (TimetableItemData item : mItems) 53 | adapter.addFragment(FragmentTimetableItemDetails.createInstance(mFragment, mMainActivity, item)); 54 | mPager.setAdapter(adapter); 55 | if (mItems.size() > 1) 56 | setupPagerIndicator(); 57 | else 58 | mPagerIndicator.setVisibility(View.GONE); 59 | return rootView; 60 | } 61 | 62 | private void setupPagerIndicator() { 63 | mPager.addOnPageChangeListener(this); 64 | dots = new ImageView[mItems.size()]; 65 | 66 | for (int i = 0; i < mItems.size(); i++) { 67 | dots[i] = new ImageView(this.getContext()); 68 | dots[i].setImageResource(R.drawable.non_selected_item_indicator_dot); 69 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( 70 | LinearLayout.LayoutParams.WRAP_CONTENT, 71 | LinearLayout.LayoutParams.WRAP_CONTENT 72 | ); 73 | 74 | params.setMargins(6, 6, 6, 6); 75 | mPagerIndicator.addView(dots[i], params); 76 | } 77 | 78 | dots[0].setImageResource(R.drawable.selected_item_indicator_dot); 79 | } 80 | 81 | @Override 82 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 83 | 84 | } 85 | 86 | @Override 87 | public void onPageSelected(int position) { 88 | for (int i = 0; i < mItems.size(); i++) 89 | dots[i].setImageResource(R.drawable.non_selected_item_indicator_dot); 90 | 91 | dots[position].setImageResource(R.drawable.selected_item_indicator_dot); 92 | } 93 | 94 | @Override 95 | public void onPageScrollStateChanged(int state) { 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/fragment/FragmentDatePicker.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.fragment; 2 | 3 | import android.app.DatePickerDialog; 4 | import android.app.Dialog; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.v4.app.DialogFragment; 8 | import android.widget.DatePicker; 9 | 10 | import de.perflyst.untis.R; 11 | import de.perflyst.untis.activity.ActivityMain; 12 | 13 | import java.util.Calendar; 14 | import java.util.Objects; 15 | 16 | public class FragmentDatePicker extends DialogFragment implements DatePickerDialog.OnDateSetListener { 17 | 18 | @NonNull 19 | @Override 20 | public Dialog onCreateDialog(Bundle savedInstanceState) { 21 | DatePickerDialog dialog; 22 | if (getArguments() != null) { 23 | int year = getArguments().getInt("year"); 24 | int month = getArguments().getInt("month"); 25 | int day = getArguments().getInt("day"); 26 | dialog = new DatePickerDialog(Objects.requireNonNull(getActivity()), this, year, month, day); 27 | } else { 28 | Calendar c = Calendar.getInstance(); 29 | int year = c.get(Calendar.YEAR); 30 | int month = c.get(Calendar.MONTH); 31 | int day = c.get(Calendar.DAY_OF_MONTH); 32 | dialog = new DatePickerDialog(Objects.requireNonNull(getActivity()), this, year, month, day); 33 | } 34 | dialog.setButton(DatePickerDialog.BUTTON_NEUTRAL, getString(R.string.today), (dialogInterface, i) -> { 35 | Calendar calendar = Calendar.getInstance(); 36 | onDateSet(new DatePicker(getContext()), calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); 37 | }); 38 | return dialog; 39 | } 40 | 41 | public void onDateSet(DatePicker view, int year, int month, int day) { 42 | Calendar calendar = Calendar.getInstance(); 43 | calendar.set(Calendar.YEAR, year); 44 | calendar.set(Calendar.MONTH, month); 45 | calendar.set(Calendar.DAY_OF_MONTH, day); 46 | ((ActivityMain) Objects.requireNonNull(getActivity())).goTo(calendar); 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/notification/StartupReceiver.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.notification; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.AlarmManager; 5 | import android.app.PendingIntent; 6 | import android.content.BroadcastReceiver; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.SharedPreferences; 10 | import android.preference.PreferenceManager; 11 | import android.util.Log; 12 | 13 | import java.util.Calendar; 14 | 15 | public class StartupReceiver extends BroadcastReceiver { 16 | 17 | @SuppressLint("UnsafeProtectedBroadcastReceiver") 18 | @Override 19 | public void onReceive(Context context, Intent intent) { 20 | Log.d("StartupReceiver", "StartupReceiver received"); 21 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 22 | if (!prefs.getBoolean("preference_notifications_enable", true)) 23 | return; 24 | Calendar calendar = Calendar.getInstance(); 25 | calendar.set(Calendar.HOUR_OF_DAY, 1); 26 | calendar.set(Calendar.MINUTE, 0); 27 | calendar.set(Calendar.SECOND, 0); 28 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(context, NotificationSetup.class), PendingIntent.FLAG_UPDATE_CURRENT); 29 | AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 30 | if (am != null) 31 | am.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent); 32 | 33 | Intent i = new Intent(context, NotificationSetup.class); 34 | context.sendBroadcast(i); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/preference/UnitPreference.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.preference; 2 | 3 | import android.content.Context; 4 | import android.preference.EditTextPreference; 5 | import android.util.AttributeSet; 6 | 7 | public class UnitPreference extends EditTextPreference { 8 | private static final String ATTRIBUTE_KEY_UNIT = "unit"; 9 | 10 | private String unit = ""; 11 | 12 | public UnitPreference(Context context, AttributeSet attrs, int defStyle) { 13 | super(context, attrs, defStyle); 14 | setupUnit(attrs); 15 | } 16 | 17 | public UnitPreference(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | setupUnit(attrs); 20 | } 21 | 22 | public UnitPreference(Context context) { 23 | super(context); 24 | } 25 | 26 | private void setupUnit(AttributeSet attrs) { 27 | for (int i = 0; i < attrs.getAttributeCount(); i++) { 28 | String attr = attrs.getAttributeName(i); 29 | if (attr.equalsIgnoreCase(ATTRIBUTE_KEY_UNIT)) 30 | unit = attrs.getAttributeValue(i); 31 | } 32 | } 33 | 34 | @Override 35 | public CharSequence getSummary() { 36 | CharSequence summary = super.getSummary(); 37 | if (summary == null) 38 | return getText() + unit; 39 | else 40 | return String.format(summary.toString(), getText()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/BetterToast.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.StringRes; 5 | import android.widget.Toast; 6 | 7 | public class BetterToast { 8 | private final Context context; 9 | private Toast toast; 10 | 11 | public BetterToast(Context context) { 12 | this.context = context; 13 | } 14 | 15 | private void showToast(String text, int duration) { 16 | try { 17 | toast.getView().isShown(); 18 | toast.setText(text); 19 | toast.show(); 20 | } catch (Exception e) { 21 | toast = Toast.makeText(context, text, duration); 22 | toast.show(); 23 | } 24 | } 25 | 26 | public void showToast(@StringRes int resId, int duration) { 27 | showToast(context.getString(resId), duration); 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/ColorPreferenceList.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class ColorPreferenceList { 6 | private final ArrayList keys = new ArrayList<>(); 7 | 8 | public void add(String key) { 9 | keys.add(key); 10 | } 11 | 12 | public int size() { 13 | return keys.size(); 14 | } 15 | 16 | public String getKey(int index) { 17 | return keys.get(index); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/Constants.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | @SuppressWarnings("unused") 4 | public class Constants { 5 | public class UntisAPI { 6 | public static final String DEFAULT_PROTOCOL = "https://"; 7 | public static final String DEFAULT_WEBUNTIS_PATH = "/WebUntis/jsonrpc_intern.do"; 8 | 9 | public static final int ERROR_CODE_INVALID_SCHOOLNAME = -8500; 10 | public static final int ERROR_CODE_INVALID_CREDENTIALS = -8504; 11 | public static final int ERROR_CODE_INVALID_CLIENT_TIME = -8524; 12 | public static final int ERROR_CODE_NO_SERVER_FOUND = 100; 13 | public static final int ERROR_CODE_WEBUNTIS_NOT_INSTALLED = 101; 14 | 15 | public static final int ERROR_CODE_UNKNOWN = 0; 16 | 17 | public static final String METHOD_GET_USER_DATA = "getUserData2017"; 18 | public static final String METHOD_GET_TIMETABLE = "getTimetable2017"; 19 | } 20 | 21 | public class TimetableItem { 22 | public static final String CODE_REGULAR = "REGULAR"; 23 | public static final String CODE_CANCELLED = "CANCELLED"; 24 | public static final String CODE_IRREGULAR = "IRREGULAR"; 25 | public static final String CODE_EXAM = "EXAM"; 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/Conversions.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import android.content.Context; 4 | 5 | public class Conversions { 6 | private static float scale = -1; 7 | 8 | public Conversions() { 9 | throw new RuntimeException("Instantiation not allowed"); 10 | } 11 | 12 | public static void setScale(Context context) { 13 | scale = context.getResources().getDisplayMetrics().density; 14 | } 15 | 16 | public static int dp2px(int dp) { 17 | if (scale < 0) 18 | throw new IllegalStateException("You have to call setScale first!"); 19 | return (int) (dp * scale + 0.5f); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/DateOperations.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import android.util.Log; 4 | 5 | import java.text.ParseException; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Calendar; 8 | import java.util.Date; 9 | import java.util.Locale; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | public class DateOperations { 14 | private static final String TAG = "DateOperations"; 15 | 16 | private static final SimpleDateFormat FROM_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.ENGLISH); 17 | 18 | private DateOperations() { 19 | throw new RuntimeException("Instantiation not allowed"); 20 | } 21 | 22 | public static Calendar getStartDateFromWeek(Calendar week, int offset, boolean resetTime) { 23 | week.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); 24 | week.add(Calendar.DAY_OF_MONTH, offset); 25 | if (resetTime) { 26 | week.set(Calendar.HOUR_OF_DAY, 0); 27 | week.set(Calendar.MINUTE, 0); 28 | week.set(Calendar.SECOND, 0); 29 | week.set(Calendar.MILLISECOND, 0); 30 | } 31 | return week; 32 | } 33 | 34 | public static Calendar getStartDateFromWeek(Calendar week, int offset) { 35 | return getStartDateFromWeek(week, offset, false); 36 | } 37 | 38 | public static int addDaysToInt(int startDate, int days) { 39 | String result = addDaysToInt(startDate, days, new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH)); 40 | return result == null ? startDate : Integer.parseInt(result); 41 | } 42 | 43 | public static String addDaysToInt(int startDate, int days, SimpleDateFormat targetFormat) { 44 | try { 45 | Calendar c = Calendar.getInstance(); 46 | SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); 47 | c.setTime(sdf.parse(Integer.toString(startDate))); 48 | c.add(Calendar.DATE, days); 49 | return targetFormat.format(c.getTime()); 50 | } catch (ParseException e) { 51 | Log.w(TAG, e.getMessage()); 52 | return null; 53 | } 54 | } 55 | 56 | public static String getStringDateFromInt(int date, Locale locale) { 57 | try { 58 | Calendar c = Calendar.getInstance(); 59 | SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); 60 | c.setTime(sdf.parse(Integer.toString(date))); 61 | return new SimpleDateFormat("d. MMM", locale).format(c.getTime()); 62 | } catch (ParseException e) { 63 | Log.w(TAG, e.getMessage()); 64 | return null; 65 | } 66 | } 67 | 68 | public static String getDayNameFromInt(int date, Locale locale) { 69 | try { 70 | Calendar c = Calendar.getInstance(); 71 | SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); 72 | c.setTime(sdf.parse(Integer.toString(date))); 73 | return new SimpleDateFormat("EEE", locale).format(c.getTime()); 74 | } catch (ParseException e) { 75 | Log.w(TAG, e.getMessage()); 76 | return null; 77 | } 78 | } 79 | 80 | public static Date parseFromISO(String dateTime) throws ParseException { 81 | return FROM_ISO_8601.parse(dateTime); 82 | } 83 | 84 | public static String convertToISO(Date date) { 85 | return FROM_ISO_8601.format(date); 86 | } 87 | 88 | public static int getComparableTime(String dateTime) { 89 | Pattern pattern = Pattern.compile("[0-9]{2}:[0-9]{2}"); 90 | Matcher matcher = pattern.matcher(dateTime); 91 | if (matcher.find()) 92 | return Integer.parseInt((matcher.group(0).replace(":", ""))); 93 | else 94 | throw new IllegalArgumentException("No time found"); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/ElementName.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.NoSuchElementException; 9 | 10 | public class ElementName { 11 | public static final boolean FULL = true; 12 | public static final boolean SHORT = false; 13 | private final List names = new ArrayList<>(); 14 | private final List longNames = new ArrayList<>(); 15 | private ElementType type; 16 | private JSONObject userData; 17 | 18 | public ElementName() { 19 | } 20 | 21 | public ElementName(JSONObject userDataList) { 22 | this.userData = userDataList; 23 | } 24 | 25 | public ElementName(ElementType type, JSONObject userDataList) { 26 | this.type = type; 27 | this.userData = userDataList; 28 | } 29 | 30 | public static String getTypeName(ElementType type) { 31 | switch (type) { 32 | case CLASS: 33 | return "klassen"; 34 | case TEACHER: 35 | return "teachers"; 36 | case SUBJECT: 37 | return "subjects"; 38 | case ROOM: 39 | return "rooms"; 40 | case STUDENT: 41 | return "students"; 42 | case HOLIDAY: 43 | return "holidays"; 44 | default: 45 | return null; 46 | } 47 | } 48 | 49 | public ElementName fromIdList(List list, ElementType elemType) { 50 | type = elemType; 51 | for (int i : list) 52 | try { 53 | names.add((String) findFieldByValue("id", i, "name")); 54 | } catch (NoSuchElementException | JSONException ignored) { 55 | } 56 | for (int i : list) 57 | try { 58 | longNames.add((String) findFieldByValue("id", i, "longName")); 59 | } catch (NoSuchElementException | JSONException ignored) { 60 | } 61 | return this; 62 | } 63 | 64 | public Object findFieldByValue(String srcField, Object srcValue, String dstFieldName) throws JSONException { 65 | if (srcField == null || srcValue == null || dstFieldName == null) 66 | return null; 67 | for (int i = 0; i < userData.getJSONObject("masterData").getJSONArray(getTypeName(type)).length(); i++) 68 | if (userData.getJSONObject("masterData").getJSONArray(getTypeName(type)).getJSONObject(i).get(srcField).equals(srcValue)) 69 | return userData.getJSONObject("masterData").getJSONArray(getTypeName(type)).getJSONObject(i).get(dstFieldName); 70 | throw new JSONException("Data array empty"); 71 | } 72 | 73 | public boolean isEmpty() { 74 | return names.isEmpty() || longNames.isEmpty(); 75 | } 76 | 77 | public String getName(boolean full) { 78 | if (full) { 79 | StringBuilder sb = new StringBuilder(); 80 | if (names.size() > 1) { 81 | sb.append(names.get(0)); 82 | for (int i = 1; i < names.size(); i++) 83 | sb.append(", ").append(names.get(i)); 84 | return sb.toString(); 85 | } else if (names.size() == 1) 86 | return names.get(0); 87 | else 88 | return ""; 89 | } else { 90 | if (names.size() > 1) 91 | return names.get(0) + ", …"; 92 | else if (names.size() == 1) 93 | return names.get(0); 94 | else 95 | return ""; 96 | } 97 | } 98 | 99 | public String getLongName(boolean full) { 100 | if (full) { 101 | StringBuilder sb = new StringBuilder(); 102 | if (longNames.size() > 1) { 103 | sb.append(longNames.get(0)); 104 | for (int i = 1; i < longNames.size(); i++) 105 | sb.append(", ").append(longNames.get(i)); 106 | return sb.toString(); 107 | } else if (longNames.size() == 1) 108 | return longNames.get(0); 109 | else 110 | return ""; 111 | } else { 112 | if (longNames.size() > 1) 113 | return longNames.get(0) + ", …"; 114 | else if (longNames.size() == 1) 115 | return longNames.get(0); 116 | else 117 | return ""; 118 | } 119 | } 120 | 121 | public List getNames() { 122 | return names; 123 | } 124 | 125 | public List getLongNames() { 126 | return longNames; 127 | } 128 | 129 | public enum ElementType { 130 | UNKNOWN(0), 131 | CLASS(1), 132 | TEACHER(2), 133 | SUBJECT(3), 134 | ROOM(4), 135 | STUDENT(5), 136 | HOLIDAY(6); 137 | 138 | public final int value; 139 | 140 | ElementType(int value) { 141 | this.value = value; 142 | } 143 | 144 | public static ElementType fromValue(int value) { 145 | for (ElementType result : values()) 146 | if (result.value == value) return result; 147 | 148 | return UNKNOWN; 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/ListManager.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.io.File; 10 | import java.io.FileInputStream; 11 | import java.io.FileOutputStream; 12 | 13 | public class ListManager { 14 | private final Context context; 15 | 16 | private static JSONObject userDataCache; 17 | 18 | public ListManager(Context context) { 19 | this.context = context; 20 | } 21 | 22 | public void saveList(String name, String content, boolean isCacheData) { 23 | Log.d("ListManager", "Writing list " + name + (isCacheData ? " (using cache)" : "")); 24 | try { 25 | FileOutputStream outputStream; 26 | if (isCacheData) 27 | outputStream = new FileOutputStream(new File(getCacheDir(), name + ".json")); 28 | else 29 | outputStream = new FileOutputStream(new File(context.getFilesDir(), name + ".json")); 30 | outputStream.write(content.getBytes()); 31 | outputStream.close(); 32 | } catch (Exception e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | 37 | public static JSONObject getUserData(ListManager listManager) { 38 | if (userDataCache == null) { 39 | try { 40 | userDataCache = new JSONObject(listManager.readList("userData", false)); 41 | } catch (JSONException e) { 42 | e.printStackTrace(); 43 | } 44 | } else { 45 | Log.d("ListManager", "Returning userData from cache"); 46 | } 47 | 48 | return userDataCache; 49 | } 50 | 51 | public boolean exists(String name, @SuppressWarnings("SameParameterValue") boolean useCaching) { 52 | File file; 53 | if (useCaching) 54 | file = new File(getCacheDir(), name + ".json"); 55 | else 56 | file = new File(context.getFilesDir(), name + ".json"); 57 | return file.exists(); 58 | } 59 | 60 | public void delete(String name, boolean isCacheData) { 61 | Log.d("ListManager", "Deleting list " + name + (isCacheData ? " (using cache)" : "")); 62 | if (isCacheData) { 63 | if (!new File(getCacheDir(), name + ".json").delete()) { 64 | Log.e(this.getClass().getSimpleName(), "Failed to delete " + name); 65 | } 66 | } else { 67 | if (!new File(context.getFilesDir(), name + ".json").delete()) { 68 | Log.e(this.getClass().getSimpleName(), "Failed to delete " + name); 69 | } 70 | } 71 | } 72 | 73 | private File getCacheDir() { 74 | return context.getCacheDir(); 75 | } 76 | 77 | public static JSONObject getUserData(Context context) { 78 | return getUserData(new ListManager(context)); 79 | } 80 | 81 | public String readList(String name, boolean isCacheData) { 82 | Log.d("ListManager", "Reading list " + name + (isCacheData ? " (using cache)" : "") + ", origin: " + new Exception().getStackTrace()[1].getClassName()); 83 | long timer = System.nanoTime(); 84 | StringBuilder content = new StringBuilder(); 85 | try { 86 | FileInputStream inputStream; 87 | if (isCacheData) 88 | inputStream = new FileInputStream(new File(getCacheDir(), name + ".json")); 89 | else 90 | inputStream = new FileInputStream(new File(context.getFilesDir(), name + ".json")); 91 | byte[] input = new byte[inputStream.available()]; 92 | //noinspection StatementWithEmptyBody 93 | while (inputStream.read(input) != -1) { 94 | // TODO: Potential ANR 95 | } 96 | content.append(new String(input)); 97 | } catch (Exception e) { 98 | e.printStackTrace(); 99 | } 100 | Log.d("ListManager", "Took " + (System.nanoTime() - timer) / 1000000.0 + "ms"); 101 | return content.toString(); 102 | } 103 | 104 | public void invalidateCaches() { 105 | userDataCache = null; 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/PreferenceUtils.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.res.Resources; 6 | 7 | public class PreferenceUtils { 8 | public static int getPrefInt(Context context, SharedPreferences prefs, String key, boolean convertString) { 9 | Resources res = context.getResources(); 10 | if (convertString) 11 | return Integer.parseInt(prefs.getString(key, String.valueOf(res.getInteger(res.getIdentifier(key + "_default", "integer", context.getPackageName()))))); 12 | else 13 | return prefs.getInt(key, res.getInteger(res.getIdentifier(key + "_default", "integer", context.getPackageName()))); 14 | } 15 | 16 | public static int getPrefInt(Context context, SharedPreferences prefs, String key) { 17 | return getPrefInt(context, prefs, key, false); 18 | } 19 | 20 | public static boolean getPrefBool(Context context, SharedPreferences prefs, String key) { 21 | if (context == null) 22 | return false; 23 | Resources res = context.getResources(); 24 | return prefs.getBoolean(key, res.getBoolean(res.getIdentifier(key + "_default", "bool", context.getPackageName()))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/SessionInfo.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import org.json.JSONObject; 4 | 5 | import static de.perflyst.untis.utils.ElementName.ElementType; 6 | import static de.perflyst.untis.utils.ElementName.ElementType.CLASS; 7 | import static de.perflyst.untis.utils.ElementName.ElementType.ROOM; 8 | import static de.perflyst.untis.utils.ElementName.ElementType.STUDENT; 9 | import static de.perflyst.untis.utils.ElementName.ElementType.TEACHER; 10 | import static de.perflyst.untis.utils.ElementName.ElementType.UNKNOWN; 11 | 12 | public class SessionInfo { 13 | private int elemId; 14 | private String elemType; 15 | private String displayName; 16 | 17 | public SessionInfo() { 18 | elemId = -1; 19 | elemType = ""; 20 | displayName = ""; 21 | } 22 | 23 | public static String getElemTypeName(ElementType type) { 24 | switch (type) { 25 | case STUDENT: 26 | return "STUDENT"; 27 | case CLASS: 28 | return "CLASS"; 29 | case TEACHER: 30 | return "TEACHER"; 31 | case ROOM: 32 | return "ROOM"; 33 | default: 34 | return ""; 35 | } 36 | } 37 | 38 | public static ElementType getElemTypeId(String type) { 39 | switch (type) { 40 | case "STUDENT": 41 | return STUDENT; 42 | case "CLASS": 43 | return CLASS; 44 | case "TEACHER": 45 | return TEACHER; 46 | case "ROOM": 47 | return ROOM; 48 | default: 49 | return UNKNOWN; 50 | } 51 | } 52 | 53 | public void setDataFromJsonObject(JSONObject data) { 54 | elemId = data.optInt("elemId", -1); 55 | elemType = data.optString("elemType", ""); 56 | displayName = data.optString("displayName", "OpenUntis"); 57 | } 58 | 59 | public int getElemId() { 60 | return elemId; 61 | } 62 | 63 | public void setElemId(int elemId) { 64 | this.elemId = elemId; 65 | } 66 | 67 | public String getElemType() { 68 | return elemType; 69 | } 70 | 71 | public void setElemType(String elemType) { 72 | this.elemType = elemType; 73 | } 74 | 75 | public String getDisplayName() { 76 | return displayName; 77 | } 78 | 79 | public void setDisplayName(String displayName) { 80 | this.displayName = displayName; 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/StreamUtils.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | class StreamUtils { 8 | public static String readStream(InputStream is) { 9 | try { 10 | ByteArrayOutputStream bo = new ByteArrayOutputStream(); 11 | int i = is.read(); 12 | while (i != -1) { 13 | bo.write(i); 14 | i = is.read(); 15 | } 16 | return bo.toString(); 17 | } catch (IOException e) { 18 | e.printStackTrace(); 19 | return ""; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/connectivity/DownloadTask.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils.connectivity; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.net.HttpURLConnection; 11 | import java.net.URL; 12 | 13 | public class DownloadTask extends AsyncTask { 14 | private ResponseHandler handler; 15 | 16 | public DownloadTask() { 17 | 18 | } 19 | 20 | @Override 21 | protected String doInBackground(String... parameters) { 22 | InputStream input = null; 23 | OutputStream output = null; 24 | HttpURLConnection connection = null; 25 | try { 26 | URL tmpUrl = new URL(parameters[0]); 27 | HttpURLConnection urlConnection = (HttpURLConnection) tmpUrl.openConnection(); 28 | urlConnection.setInstanceFollowRedirects(false); 29 | String s = urlConnection.getHeaderField("Location"); 30 | URL url = new URL(s); 31 | 32 | connection = (HttpURLConnection) url.openConnection(); 33 | connection.connect(); 34 | 35 | if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) 36 | return "Server returned HTTP " + connection.getResponseCode() 37 | + " " + connection.getResponseMessage(); 38 | 39 | int fileLength = connection.getContentLength(); 40 | 41 | File outputFile = new File(parameters[1]); 42 | input = connection.getInputStream(); 43 | output = new FileOutputStream(outputFile); 44 | 45 | byte data[] = new byte[4096]; 46 | long total = 0; 47 | int count; 48 | while ((count = input.read(data)) != -1) { 49 | if (isCancelled()) { 50 | input.close(); 51 | return null; 52 | } 53 | total += count; 54 | if (fileLength > 0) 55 | publishProgress((int) (total * 100 / fileLength), (int) total, fileLength); 56 | output.write(data, 0, count); 57 | } 58 | } catch (Exception e) { 59 | return e.toString(); 60 | } finally { 61 | try { 62 | if (output != null) 63 | output.close(); 64 | if (input != null) 65 | input.close(); 66 | } catch (IOException ignored) { 67 | } 68 | 69 | if (connection != null) 70 | connection.disconnect(); 71 | } 72 | return "OK"; 73 | } 74 | 75 | @Override 76 | protected void onProgressUpdate(Integer... progress) { 77 | handler.onProgressUpdate(progress); 78 | } 79 | 80 | @Override 81 | protected void onPostExecute(String response) { 82 | handler.onFileReceived(response); 83 | } 84 | 85 | public void submit(String downloadUrl, String downloadPath) { 86 | this.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, downloadUrl, downloadPath); 87 | } 88 | 89 | public DownloadTask setResponseHandler(DownloadTask.ResponseHandler handler) { 90 | this.handler = handler; 91 | return this; 92 | } 93 | 94 | public interface ResponseHandler { 95 | void onProgressUpdate(Integer... progress); 96 | 97 | void onFileReceived(String response); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/connectivity/UntisAuthentication.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils.connectivity; 2 | 3 | import org.apache.commons.codec.binary.Base32; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | import java.security.GeneralSecurityException; 8 | import java.security.InvalidKeyException; 9 | import java.security.NoSuchAlgorithmException; 10 | 11 | import javax.crypto.Mac; 12 | import javax.crypto.spec.SecretKeySpec; 13 | 14 | public class UntisAuthentication { 15 | private static long createTimeBasedCode(long timestamp, String secret) { 16 | GeneralSecurityException e; 17 | if (secret == null || secret.isEmpty()) { 18 | return 0; 19 | } 20 | try { 21 | return (long) verify_code(new Base32().decode(secret.toUpperCase().getBytes()), timestamp / 30000); 22 | } catch (InvalidKeyException | NoSuchAlgorithmException e2) { 23 | e = e2; 24 | throw new RuntimeException(e); 25 | } 26 | } 27 | 28 | private static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException { 29 | if (key == null || key.length == 0) { 30 | return 0; 31 | } 32 | byte[] data = new byte[8]; 33 | long value = t; 34 | int i = 8; 35 | while (true) { 36 | int i2 = i - 1; 37 | if (i <= 0) 38 | break; 39 | data[i2] = (byte) ((int) value); 40 | value >>>= 8; 41 | i = i2; 42 | } 43 | //noinspection SpellCheckingInspection 44 | SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1"); 45 | //noinspection SpellCheckingInspection 46 | Mac mac = Mac.getInstance("HmacSHA1"); 47 | mac.init(signKey); 48 | byte[] hash = mac.doFinal(data); 49 | int offset = hash[19] & 15; 50 | long truncatedHash = 0; 51 | for (int i2 = 0; i2 < 4; i2++) 52 | truncatedHash = (truncatedHash << 8) | ((long) (hash[offset + i2] & 255)); 53 | return (int) ((truncatedHash & 2147483647L) % 1000000); 54 | } 55 | 56 | public static JSONObject getAuthObject(String user, String key) throws JSONException { 57 | return new JSONObject() 58 | .put("user", user) 59 | .put("otp", createTimeBasedCode(System.currentTimeMillis(), key)) 60 | .put("clientTime", System.currentTimeMillis()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/lazyload/FileCache.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils.lazyload; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.File; 6 | import java.io.UnsupportedEncodingException; 7 | import java.net.URLEncoder; 8 | 9 | class FileCache { 10 | 11 | private final File cacheDir; 12 | 13 | FileCache(Context context) { 14 | cacheDir = new File(context.getCacheDir(), "images"); 15 | cacheDir.mkdirs(); 16 | } 17 | 18 | File getFile(String url) throws UnsupportedEncodingException { 19 | String filename = URLEncoder.encode(url, "UTF-8"); 20 | return new File(cacheDir, filename); 21 | } 22 | 23 | public void clear() { // TODO: Implement some setting to clear cached images 24 | File[] files = cacheDir.listFiles(); 25 | if (files == null) 26 | return; 27 | for (File f : files) 28 | f.delete(); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/lazyload/MemoryCache.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils.lazyload; 2 | 3 | import android.graphics.Bitmap; 4 | import android.util.Log; 5 | 6 | import java.util.Collections; 7 | import java.util.Iterator; 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | import java.util.Map.Entry; 11 | 12 | class MemoryCache { 13 | private static final String TAG = "MemoryCache"; 14 | private final Map cache = Collections.synchronizedMap( 15 | new LinkedHashMap(10, 1.5f, true)); 16 | private long size = 0; 17 | private long limit = 1000000; 18 | 19 | MemoryCache() { 20 | setLimit(Runtime.getRuntime().maxMemory() / 4); 21 | } 22 | 23 | private void setLimit(long new_limit) { 24 | limit = new_limit; 25 | Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB"); 26 | } 27 | 28 | public Bitmap get(String id) { 29 | try { 30 | if (!cache.containsKey(id)) 31 | return null; 32 | return cache.get(id); 33 | } catch (NullPointerException ex) { 34 | ex.printStackTrace(); 35 | return null; 36 | } 37 | } 38 | 39 | public void put(String id, Bitmap bitmap) { 40 | try { 41 | if (cache.containsKey(id)) 42 | size -= getSizeInBytes(cache.get(id)); 43 | cache.put(id, bitmap); 44 | size += getSizeInBytes(bitmap); 45 | checkSize(); 46 | } catch (Throwable th) { 47 | th.printStackTrace(); 48 | } 49 | } 50 | 51 | private void checkSize() { 52 | Log.i(TAG, "cache size=" + size + " length=" + cache.size()); 53 | if (size > limit) { 54 | Iterator> iter = cache.entrySet().iterator(); 55 | while (iter.hasNext()) { 56 | Entry entry = iter.next(); 57 | size -= getSizeInBytes(entry.getValue()); 58 | iter.remove(); 59 | if (size <= limit) 60 | break; 61 | } 62 | Log.i(TAG, "Clean cache. New size " + cache.size()); 63 | } 64 | } 65 | 66 | public void clear() { 67 | try { 68 | cache.clear(); 69 | size = 0; 70 | } catch (NullPointerException ex) { 71 | ex.printStackTrace(); 72 | } 73 | } 74 | 75 | private long getSizeInBytes(Bitmap bitmap) { 76 | if (bitmap == null) 77 | return 0; 78 | return bitmap.getRowBytes() * bitmap.getHeight(); 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/lazyload/SimpleFloatViewManager.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils.lazyload; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Color; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.ListView; 9 | 10 | import de.perflyst.untis.view.SortableListView; 11 | 12 | public class SimpleFloatViewManager implements SortableListView.FloatViewManager { 13 | private Bitmap floatBitmap; 14 | private ImageView imageView; 15 | private int floatBGColor = Color.BLACK; 16 | private final ListView listView; 17 | 18 | protected SimpleFloatViewManager(ListView lv) { 19 | listView = lv; 20 | } 21 | 22 | public void setBackgroundColor(int color) { 23 | floatBGColor = color; 24 | } 25 | 26 | @Override 27 | public View onCreateFloatView(int position) { 28 | if (listView == null) 29 | return null; 30 | 31 | View v = listView.getChildAt(position + listView.getHeaderViewsCount() - listView.getFirstVisiblePosition()); 32 | 33 | if (v == null) 34 | return null; 35 | 36 | v.setPressed(false); 37 | 38 | v.setDrawingCacheEnabled(true); 39 | floatBitmap = Bitmap.createBitmap(v.getDrawingCache()); 40 | v.setDrawingCacheEnabled(false); 41 | 42 | if (imageView == null) { 43 | imageView = new ImageView(listView.getContext()); 44 | } 45 | imageView.setBackgroundColor(floatBGColor); 46 | imageView.setPadding(0, 0, 0, 0); 47 | imageView.setImageBitmap(floatBitmap); 48 | imageView.setLayoutParams(new ViewGroup.LayoutParams(v.getWidth(), v.getHeight())); 49 | 50 | return imageView; 51 | } 52 | 53 | @Override 54 | public void onDestroyFloatView(View floatView) { 55 | ((ImageView) floatView).setImageDrawable(null); 56 | 57 | floatBitmap.recycle(); 58 | floatBitmap = null; 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/timetable/TimegridUnitManager.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils.timetable; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONException; 5 | 6 | import java.text.SimpleDateFormat; 7 | import java.util.ArrayList; 8 | import java.util.Calendar; 9 | import java.util.Locale; 10 | 11 | public class TimegridUnitManager { 12 | private ArrayList unitList; 13 | private JSONArray days; 14 | private int numberOfDays = -1; 15 | private int longestDay = 0; 16 | private int maxHoursPerDay = -1; 17 | 18 | public TimegridUnitManager(JSONArray days) { 19 | if (days == null) 20 | this.days = new JSONArray(); 21 | this.days = days; 22 | } 23 | 24 | public int getNumberOfDays() { 25 | if (numberOfDays < 0) 26 | calculateCounts(); 27 | return numberOfDays; 28 | } 29 | 30 | private void calculateCounts() { 31 | numberOfDays = days.length(); 32 | 33 | for (int i = 0; i < days.length(); i++) 34 | try { 35 | final int unitsLength = days.getJSONObject(i).getJSONArray("units").length(); 36 | if (maxHoursPerDay < unitsLength) { 37 | maxHoursPerDay = unitsLength; 38 | // remember the day with the most units 39 | longestDay = i; 40 | } 41 | } catch (JSONException e) { 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | public int getMaxHoursPerDay() { 47 | if (maxHoursPerDay < 0) 48 | calculateCounts(); 49 | return maxHoursPerDay; 50 | } 51 | 52 | public ArrayList getUnits() { 53 | if (unitList == null) { 54 | // determine the number of days and longest day in the week before building list of units 55 | // (important if week has days with different length) 56 | calculateCounts(); 57 | unitList = new ArrayList<>(); 58 | try { 59 | JSONArray units = days.getJSONObject(longestDay).getJSONArray("units"); 60 | for (int i = 0; i < units.length(); i++) { 61 | UnitData unitData = new UnitData( 62 | units.getJSONObject(i).getString("startTime").substring(1), 63 | units.getJSONObject(i).getString("endTime").substring(1) 64 | ); 65 | 66 | unitList.add(unitData); 67 | } 68 | } catch (JSONException e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | 73 | return unitList; 74 | } 75 | 76 | public int getDayIndex(Calendar c) throws IndexOutOfBoundsException { 77 | try { 78 | SimpleDateFormat dayFormat = new SimpleDateFormat("E", Locale.ENGLISH); 79 | for (int i = 0; i < days.length(); i++) { 80 | String day = days.getJSONObject(i).getString("day"); 81 | if (day.equalsIgnoreCase(dayFormat.format(c.getTime()))) 82 | return i; 83 | } 84 | throw new IndexOutOfBoundsException("Day not in timetable"); 85 | } catch (JSONException e) { 86 | throw new IndexOutOfBoundsException("Invalid userData!"); 87 | } 88 | } 89 | 90 | public class UnitData { 91 | private final String startTime; 92 | private final String endTime; 93 | 94 | private UnitData(String startTime, String endTime) { 95 | this.startTime = startTime; 96 | this.endTime = endTime; 97 | } 98 | 99 | String getStartTime() { 100 | return startTime; 101 | } 102 | 103 | String getEndTime() { 104 | return endTime; 105 | } 106 | 107 | public String getDisplayStartTime() { 108 | String displayTime = startTime; 109 | while (displayTime.charAt(0) == '0') 110 | displayTime = displayTime.substring(1); 111 | return displayTime; 112 | } 113 | 114 | public String getDisplayEndTime() { 115 | String displayTime = endTime; 116 | while (displayTime.charAt(0) == '0') 117 | displayTime = displayTime.substring(1); 118 | return displayTime; 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/utils/timetable/Timetable.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.utils.timetable; 2 | 3 | import android.content.SharedPreferences; 4 | import android.util.Log; 5 | 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | import java.text.ParseException; 11 | import java.text.SimpleDateFormat; 12 | import java.util.ArrayList; 13 | import java.util.Calendar; 14 | import java.util.Date; 15 | import java.util.List; 16 | import java.util.Locale; 17 | 18 | public class Timetable { 19 | private UnitList[][] units; 20 | private int numberOfDays; 21 | private int hoursPerDay; 22 | private TimegridUnitManager unitManager; 23 | private int[][] offsets; 24 | 25 | public Timetable(JSONObject timetable, SharedPreferences prefs) throws IllegalArgumentException { 26 | long start = System.currentTimeMillis(); 27 | try { 28 | unitManager = new TimegridUnitManager(timetable.getJSONObject("masterData") 29 | .getJSONObject("timeGrid").getJSONArray("days")); 30 | numberOfDays = unitManager.getNumberOfDays(); 31 | hoursPerDay = unitManager.getMaxHoursPerDay(); 32 | 33 | units = new UnitList[numberOfDays][hoursPerDay]; 34 | 35 | offsets = new int[numberOfDays][hoursPerDay]; 36 | 37 | boolean includeCancelled = !prefs.getBoolean("preference_timetable_hide_cancelled", true); 38 | 39 | addPeriods(timetable.getJSONObject("timetable").getJSONArray("periods"), includeCancelled); 40 | } catch (JSONException e) { 41 | e.printStackTrace(); 42 | throw new IllegalArgumentException("Invalid timetable object (" + e.getMessage() + ")"); 43 | } 44 | long end = System.currentTimeMillis(); 45 | Log.d("DebugTimer", "Setup duration: " + (end - start) + "ms"); 46 | } 47 | 48 | private void addPeriods(JSONArray periods, boolean includeCancelled) { 49 | ArrayList units = unitManager.getUnits(); 50 | 51 | for (int x = 0; x < this.units.length; x++) 52 | for (int y = 0; y < this.units[0].length; y++) 53 | this.units[x][y] = new UnitList(); 54 | 55 | int hourIndex; 56 | 57 | for (int i = 0; i < periods.length(); i++) { 58 | try { 59 | JSONObject item = periods.getJSONObject(i); 60 | 61 | Calendar c = Calendar.getInstance(); 62 | Date date = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(item.getString("startDateTime").substring(0, 10)); 63 | c.setTime(date); 64 | 65 | hourIndex = -1; 66 | while (hourIndex + 1 < hoursPerDay && Integer.parseInt(item.getString("startDateTime") 67 | .substring(11, 16).replace(":", "")) >= 68 | Integer.parseInt(units.get(hourIndex + 1).getStartTime() 69 | .replace(":", ""))) { 70 | hourIndex++; 71 | } 72 | 73 | TimetableItemData itemData = new TimetableItemData(item); 74 | if ((includeCancelled || !itemData.isCancelled()) && hourIndex > -1) 75 | this.units[c.get(Calendar.DAY_OF_WEEK) - Calendar.MONDAY][hourIndex].units.add(itemData); 76 | } catch (JSONException | ParseException e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | } 81 | 82 | public List getItems(int day, int hour) { 83 | return units[day][hour].units; 84 | } 85 | 86 | public int getNumberOfDays() { 87 | return numberOfDays; 88 | } 89 | 90 | public int getHoursPerDay() { 91 | return hoursPerDay; 92 | } 93 | 94 | public int getOffset(int day, int hour) { 95 | return offsets[day][hour]; 96 | } 97 | 98 | public void addOffset(int day, int hour) { 99 | offsets[day][hour] += 1; 100 | } 101 | 102 | public String getStartDateTime(int day, int hour) { 103 | return getItems(day, hour).get(0).getStartDateTime(); 104 | } 105 | 106 | public String getEndDateTime(int day, int hour) { 107 | return getItems(day, hour).get(0).getEndDateTime(); 108 | } 109 | 110 | public boolean has(int day, int hour) { 111 | return getItems(day, hour).size() > 0; 112 | } 113 | 114 | public void addDummyItem(int day, int hour, TimetableItemData item) { 115 | item.setDummy(true); 116 | units[day][hour].units.add(item); 117 | } 118 | 119 | private class UnitList { 120 | final ArrayList units = new ArrayList<>(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/view/SortableListViewItem.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.view; 2 | 3 | import android.content.Context; 4 | import android.view.Gravity; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.AbsListView; 8 | 9 | /** 10 | * Lightweight ViewGroup that wraps list items obtained from user's 11 | * ListAdapter. ItemView expects a single child that has a definite 12 | * height (i.e. the child's layout height is not MATCH_PARENT). 13 | * The width of 14 | * ItemView will always match the width of its child (that is, 15 | * the width MeasureSpec given to ItemView is passed directly 16 | * to the child, and the ItemView measured width is set to the 17 | * child's measured width). 18 | *

19 | * The purpose of this class is to optimize slide 20 | * shuffle animations. 21 | */ 22 | public class SortableListViewItem extends ViewGroup { 23 | 24 | private int mGravity = Gravity.TOP; 25 | 26 | public SortableListViewItem(Context context) { 27 | super(context); 28 | 29 | setLayoutParams(new AbsListView.LayoutParams( 30 | ViewGroup.LayoutParams.MATCH_PARENT, 31 | ViewGroup.LayoutParams.WRAP_CONTENT)); 32 | } 33 | 34 | public void setGravity(int gravity) { 35 | mGravity = gravity; 36 | } 37 | 38 | @Override 39 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 40 | final View child = getChildAt(0); 41 | 42 | if (child == null) { 43 | return; 44 | } 45 | 46 | if (mGravity == Gravity.TOP) { 47 | child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight()); 48 | } else { 49 | child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight()); 50 | } 51 | } 52 | 53 | @Override 54 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 55 | 56 | int height = MeasureSpec.getSize(heightMeasureSpec); 57 | int width = MeasureSpec.getSize(widthMeasureSpec); 58 | 59 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 60 | 61 | final View child = getChildAt(0); 62 | if (child == null) { 63 | setMeasuredDimension(0, width); 64 | return; 65 | } 66 | 67 | if (child.isLayoutRequested()) { 68 | measureChild(child, widthMeasureSpec, 69 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 70 | } 71 | 72 | if (heightMode == MeasureSpec.UNSPECIFIED) { 73 | ViewGroup.LayoutParams lp = getLayoutParams(); 74 | 75 | if (lp.height > 0) { 76 | height = lp.height; 77 | } else { 78 | height = child.getMeasuredHeight(); 79 | } 80 | } 81 | 82 | setMeasuredDimension(width, height); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/view/TimetableItemBackground.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Path; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.graphics.RectF; 12 | import android.util.AttributeSet; 13 | import android.view.View; 14 | 15 | import static de.perflyst.untis.utils.Conversions.dp2px; 16 | 17 | public class TimetableItemBackground extends View { 18 | private final Paint topPaint = new Paint(); 19 | private final Paint bottomPaint = new Paint(); 20 | private final Paint dividerPaint = new Paint(); 21 | private final Paint indicatorPaint = new Paint(); 22 | private final Path indicatorPath = new Path(); 23 | private int dividerPosition; 24 | private boolean drawIndicator = false; 25 | private Paint maskPaint; 26 | private Paint paint; 27 | private Bitmap maskBitmap; 28 | private float cornerRadius = 0; 29 | 30 | public TimetableItemBackground(Context context, AttributeSet attrs) { 31 | super(context, attrs); 32 | init(); 33 | } 34 | 35 | private void init() { 36 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 37 | maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 38 | maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 39 | 40 | setWillNotDraw(false); 41 | } 42 | 43 | @Override 44 | public void draw(Canvas canvas) { 45 | int width = canvas.getWidth(); 46 | int height = canvas.getHeight(); 47 | 48 | Bitmap offscreenBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 49 | Canvas offscreenCanvas = new Canvas(offscreenBitmap); 50 | 51 | super.draw(canvas); 52 | 53 | if (dividerPosition == height) { 54 | offscreenCanvas.drawRect(0, 0, width, height, bottomPaint); 55 | } else if (dividerPosition == 0) { 56 | offscreenCanvas.drawRect(0, 0, width, height, topPaint); 57 | } else { 58 | offscreenCanvas.drawRect(0, 0, width, height - dividerPosition, topPaint); 59 | offscreenCanvas.drawRect(0, height - dividerPosition, width, height, bottomPaint); 60 | offscreenCanvas.drawRect(0, height - dividerPosition - dp2px(1), width, height - dividerPosition + dp2px(1), dividerPaint); 61 | } 62 | 63 | if (drawIndicator) { 64 | if (indicatorPath.isEmpty()) { 65 | indicatorPath.moveTo(width - dp2px(8), height); 66 | indicatorPath.lineTo(width, height); 67 | indicatorPath.lineTo(width, height - dp2px(8)); 68 | indicatorPath.close(); 69 | } 70 | 71 | offscreenCanvas.drawPath(indicatorPath, indicatorPaint); 72 | } 73 | 74 | if (maskBitmap == null) 75 | maskBitmap = createMask(canvas.getWidth(), canvas.getHeight()); 76 | 77 | offscreenCanvas.drawBitmap(maskBitmap, 0, 0, maskPaint); 78 | canvas.drawBitmap(offscreenBitmap, 0, 0, paint); 79 | } 80 | 81 | public void setTopColor(int topColor) { 82 | topPaint.setColor(topColor); 83 | } 84 | 85 | public void setBottomColor(int bottomColor) { 86 | bottomPaint.setColor(bottomColor); 87 | } 88 | 89 | public void setDividerColor(int dividerColor) { 90 | dividerPaint.setColor(dividerColor); 91 | } 92 | 93 | public void setDividerPosition(int dividerPosition) { 94 | this.dividerPosition = dividerPosition; 95 | } 96 | 97 | public void setIndicatorColor(int color) { 98 | this.drawIndicator = true; 99 | this.indicatorPaint.setStyle(Paint.Style.FILL); 100 | this.indicatorPaint.setColor(color); 101 | } 102 | 103 | private Bitmap createMask(int width, int height) { 104 | Bitmap mask = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8); 105 | Canvas c = new Canvas(mask); 106 | 107 | Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 108 | paint.setColor(Color.WHITE); 109 | c.drawRect(0, 0, width, height, paint); 110 | 111 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 112 | c.drawRoundRect(new RectF(0, 0, width, height), cornerRadius, cornerRadius, paint); 113 | 114 | return mask; 115 | } 116 | 117 | public void setCornerRadius(int cornerRadius) { 118 | this.cornerRadius = cornerRadius; 119 | } 120 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/view/VerticalTextView.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.text.TextPaint; 6 | import android.view.Gravity; 7 | 8 | public class VerticalTextView extends android.support.v7.widget.AppCompatTextView { 9 | private final boolean topDown; 10 | 11 | public VerticalTextView(Context context) { 12 | super(context); 13 | final int gravity = getGravity(); 14 | if (Gravity.isVertical(gravity) && (gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { 15 | setGravity((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) | Gravity.TOP); 16 | topDown = false; 17 | } else 18 | topDown = true; 19 | } 20 | 21 | @Override 22 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 23 | //noinspection SuspiciousNameCombination 24 | super.onMeasure(heightMeasureSpec, widthMeasureSpec); 25 | setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); 26 | } 27 | 28 | @Override 29 | protected void onDraw(Canvas canvas) { 30 | TextPaint textPaint = getPaint(); 31 | textPaint.setColor(getCurrentTextColor()); 32 | textPaint.drawableState = getDrawableState(); 33 | 34 | canvas.save(); 35 | 36 | if (topDown) { 37 | canvas.translate(getWidth(), 0); 38 | canvas.rotate(90); 39 | } else { 40 | canvas.translate(0, getHeight()); 41 | canvas.rotate(-90); 42 | } 43 | 44 | 45 | canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop()); 46 | 47 | getLayout().draw(canvas); 48 | canvas.restore(); 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/view/VerticalViewPager.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.support.annotation.NonNull; 6 | import android.support.v4.view.ViewPager; 7 | import android.util.AttributeSet; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | 11 | /** 12 | * Uses a combination of a PageTransformer and swapping X & Y coordinates 13 | * of touch events to create the illusion of a vertically scrolling ViewPager. 14 | *

15 | * Source: StackOverflow 16 | */ 17 | public class VerticalViewPager extends ViewPager { 18 | 19 | public VerticalViewPager(Context context) { 20 | super(context); 21 | init(); 22 | } 23 | 24 | public VerticalViewPager(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | init(); 27 | } 28 | 29 | private void init() { 30 | // The majority of the magic happens here 31 | setPageTransformer(true, new VerticalPageTransformer()); 32 | // The easiest way to get rid of the overscroll drawing that happens on the left and right 33 | setOverScrollMode(OVER_SCROLL_NEVER); 34 | } 35 | 36 | /** 37 | * Swaps the X and Y coordinates of your touch event. 38 | */ 39 | private MotionEvent swapXY(MotionEvent ev) { 40 | float width = getWidth(); 41 | float height = getHeight(); 42 | 43 | float newX = (ev.getY() / height) * width; 44 | float newY = (ev.getX() / width) * height; 45 | 46 | ev.setLocation(newX, newY); 47 | 48 | return ev; 49 | } 50 | 51 | @Override 52 | public boolean onInterceptTouchEvent(MotionEvent ev) { 53 | boolean intercepted = super.onInterceptTouchEvent(swapXY(ev)); 54 | swapXY(ev); // return touch coordinates to original reference frame for any child views 55 | return intercepted; 56 | } 57 | 58 | @SuppressLint("ClickableViewAccessibility") 59 | @Override 60 | public boolean onTouchEvent(MotionEvent ev) { 61 | return super.onTouchEvent(swapXY(ev)); 62 | } 63 | 64 | @Override 65 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 66 | if (getChildCount() < 1) 67 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 68 | 69 | int height = 0; 70 | for (int i = 0; i < getChildCount(); i++) { 71 | View child = getChildAt(i); 72 | child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 73 | if (child.getMeasuredHeight() > height) 74 | height = child.getMeasuredHeight(); 75 | } 76 | 77 | super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 78 | } 79 | 80 | private class VerticalPageTransformer implements ViewPager.PageTransformer { 81 | 82 | @Override 83 | public void transformPage(@NonNull View view, float position) { 84 | 85 | if (position < -1) { // [-Infinity,-2) 86 | // This page is way off-screen to the left. 87 | view.setAlpha(0); 88 | 89 | } else if (position <= 1) { // [-1,1] 90 | view.setAlpha(1); 91 | 92 | // Counteract the default slide transition 93 | view.setTranslationX(view.getWidth() * -position); 94 | 95 | //set Y position to swipe in from top 96 | float yPosition = position * view.getHeight(); 97 | view.setTranslationY(yPosition); 98 | 99 | } else { // (2,+Infinity] 100 | // This page is way off-screen to the right. 101 | view.setAlpha(0); 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/de/perflyst/untis/view/behavior/ScrollAwareFABBehavior.java: -------------------------------------------------------------------------------- 1 | package de.perflyst.untis.view.behavior; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.design.widget.CoordinatorLayout; 6 | import android.support.design.widget.FloatingActionButton; 7 | import android.support.design.widget.FloatingActionButton.Behavior; 8 | import android.support.v4.view.ViewCompat; 9 | import android.util.AttributeSet; 10 | import android.view.View; 11 | 12 | @SuppressWarnings("unused") // This is used in layouts (activity_room_finder.xml) 13 | public class ScrollAwareFABBehavior extends Behavior { 14 | public ScrollAwareFABBehavior(Context context, AttributeSet attrs) { 15 | super(); 16 | } 17 | 18 | @Override 19 | public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, 20 | @NonNull FloatingActionButton child, @NonNull View directTargetChild, 21 | @NonNull View target, int nestedScrollAxes, int type) { 22 | return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL || 23 | super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, 24 | target, nestedScrollAxes, type); 25 | } 26 | 27 | @Override 28 | public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, 29 | @NonNull View target, int dxConsumed, int dyConsumed, 30 | int dxUnconsumed, int dyUnconsumed, int type) { 31 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, 32 | dxUnconsumed, dyUnconsumed, type); 33 | 34 | if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) 35 | child.hide(new FloatingActionButton.OnVisibilityChangedListener() { 36 | @Override 37 | public void onHidden(FloatingActionButton fab) { 38 | super.onShown(fab); 39 | fab.setVisibility(View.INVISIBLE); 40 | } 41 | }); 42 | else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) 43 | child.show(); 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_stat_timetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/main/res/drawable-hdpi/ic_stat_timetable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_stat_timetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/main/res/drawable-mdpi/ic_stat_timetable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_add_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_calendar.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_error.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_prefs_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_prefs_notifications.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_prefs_personal.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_prefs_roomfinder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_prefs_styling.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_prefs_timetable.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/non_selected_item_indicator_dot.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/selected_item_indicator_dot.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_stat_timetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/main/res/drawable-xhdpi/ic_stat_timetable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/main/res/drawable-xxhdpi/ic_stat_timetable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perflyst/OpenUntis/7e4db94ccd2cd1269d3ee695e792c066d0b495a7/app/src/main/res/drawable-xxxhdpi/ic_stat_timetable.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_calendar.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_classes.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_drag_handle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_failed.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gift.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prefs_info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prefs_notifications.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prefs_personal.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prefs_roomfinder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prefs_styling.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prefs_timetable.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_room_available.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_room_occupied.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_rooms.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_rooms.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_suggested_features.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_teacher.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/non_selected_item_indicator_dot.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selected_item_indicator_dot.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login_data_input.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 19 | 20 | 21 | 22 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_room_finder.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 27 | 28 | 36 | 37 | 43 | 44 | 55 | 56 | 65 | 66 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_bar_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_bar_login_data_input.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_bar_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/borderless_button.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |