├── .circleci └── config.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── MAINTENANCE.md ├── README.md ├── RELEASING.md ├── app ├── build.gradle ├── proguard-rules.txt └── src │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ ├── com │ │ │ └── woozzu │ │ │ │ └── android │ │ │ │ ├── util │ │ │ │ └── StringMatcher.java │ │ │ │ └── widget │ │ │ │ ├── IndexScroller.java │ │ │ │ └── IndexableListView.java │ │ └── io │ │ │ └── spaceapi │ │ │ └── community │ │ │ └── myhackerspace │ │ │ ├── AboutLayout.java │ │ │ ├── Main.java │ │ │ ├── Net.java │ │ │ ├── Network.java │ │ │ ├── Prefs.java │ │ │ ├── Utils.java │ │ │ ├── Widget.java │ │ │ └── Widget_config.java │ └── res │ │ ├── drawable │ │ ├── ic_refresh_black_24dp.xml │ │ ├── ic_settings_black_24dp.xml │ │ └── ic_view_list_black_24dp.xml │ │ ├── layout │ │ ├── about.xml │ │ ├── base.xml │ │ ├── entry.xml │ │ ├── entry_sensor.xml │ │ ├── hs_choose.xml │ │ ├── hs_entry.xml │ │ ├── main.xml │ │ ├── separator.xml │ │ ├── subtitle.xml │ │ ├── title.xml │ │ ├── widget.xml │ │ └── widget_config.xml │ │ ├── menu │ │ └── main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values-da │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-nl │ │ └── strings.xml │ │ ├── values-v11 │ │ └── styles.xml │ │ ├── values │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── locales_config.xml │ │ ├── network_security_config.xml │ │ ├── preferences.xml │ │ └── widget.xml │ └── test │ └── java │ └── io │ └── spaceapi │ └── community │ └── myhackerspace │ └── UtilsTest.java ├── build.gradle ├── gpl-3.0.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── graphics ├── feature-graphic.png ├── feature-graphic.svg ├── get-it-on-accrescent.png ├── myhackerspace.png ├── myhackerspace.svg ├── screenshot-phone.png ├── screenshot-tablet-10in.png └── screenshot-tablet-7in.png ├── local.properties.example ├── metadata └── en-US │ ├── changelogs │ ├── 100.txt │ ├── 101.txt │ ├── 102.txt │ ├── 103.txt │ ├── 104.txt │ ├── 105.txt │ ├── 106.txt │ └── 107.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ ├── phoneScreenshots │ │ ├── 1.png │ │ └── 2.png │ ├── sevenInchScreenshots │ │ └── 1.png │ └── tenInchScreenshots │ │ └── 1.png │ ├── short_description.txt │ └── title.txt └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | orbs: 3 | android: circleci/android@2.5.0 4 | jobs: 5 | build: 6 | executor: 7 | name: android/android-machine 8 | resource-class: medium 9 | tag: 2024.04.1 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 14 | - run: 15 | name: Download Dependencies 16 | command: ./gradlew androidDependencies 17 | - save_cache: 18 | paths: 19 | - ~/.gradle 20 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 21 | - run: 22 | name: Run Tests 23 | command: ./gradlew lint test 24 | - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ 25 | path: app/build/reports 26 | destination: reports 27 | - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ 28 | path: app/build/test-results 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | .project 4 | .classpath 5 | project.properties 6 | local.properties 7 | build.properties 8 | .settings 9 | *.swp 10 | *.iml 11 | .idea 12 | out 13 | .gradle 14 | /local.properties 15 | .DS_Store 16 | build 17 | /captures 18 | apks/ 19 | app/release/ 20 | 21 | # Release config 22 | /release.properties 23 | /release.keystore 24 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spaceapi-community/my-hackerspace/07f765319be6c9f104ba22f2df1bb2653e8d7939/.gitmodules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | Possible tags: 4 | 5 | - [info] An information not directly related to code changes 6 | - [feature] A new feature or an improvement 7 | - [bug] A bugfix 8 | - [change] A change that's neither a feature nor a bugfix 9 | - [i18n] Internationalization / translation 10 | 11 | 12 | # Unreleased 13 | 14 | 15 | # v2.2.0 (2025-01-20) 16 | 17 | - [feature] Add support for SpaceAPI v15 ([#54]) 18 | 19 | Contributors to this version: 20 | 21 | - @s3lph 22 | 23 | Thanks! 24 | 25 | [#54]: https://github.com/spaceapi-community/my-hackerspace/pull/54 26 | 27 | 28 | # v2.1.3 (2024-07-21) 29 | 30 | - [bug] Fix a parsing error for certain endpoints ([#50]) 31 | - [change] Upgrade dependencies, switch to Java 17, switch to Gradle 8, change 32 | TargetSDK to 34 ([#53]) 33 | 34 | Contributors to this version: 35 | 36 | - Danilo Bargen (@dbrgn) 37 | 38 | Thanks! 39 | 40 | [#50]: https://github.com/spaceapi-community/my-hackerspace/pull/50 41 | [#53]: https://github.com/spaceapi-community/my-hackerspace/pull/53 42 | 43 | 44 | # v2.1.2 (2023-10-02) 45 | 46 | - [feature] Show state "last change" timestamp as localized datetime ([#47]) 47 | - [bug] Use correct mastodon property ([#49]) 48 | - [bug] Fix network error message 49 | - [change] Drop support for Android 5–7, require at least Android 8 50 | - [change] Remove old autolinking-workaround for HTC devices ([#48]) 51 | 52 | Contributors to this version: 53 | 54 | - cyroxx (@cyroxx) 55 | - Danilo Bargen (@dbrgn) 56 | 57 | Thanks! 58 | 59 | [#47]: https://github.com/spaceapi-community/my-hackerspace/pull/47 60 | [#48]: https://github.com/spaceapi-community/my-hackerspace/pull/48 61 | [#49]: https://github.com/spaceapi-community/my-hackerspace/pull/49 62 | 63 | 64 | ## v2.1.1 (2023-04-27) 65 | 66 | - [bug] Fix a bug when parsing v14 endpoints that contain a SpaceFED key 67 | without a "spacephone" field ([#44]) 68 | - [bug] Temporarily use directory from GitHub directly to speed up loading 69 | ([#45]) 70 | 71 | Contributors to this version: 72 | 73 | - Danilo Bargen (@dbrgn) 74 | 75 | Thanks! 76 | 77 | [#44]: https://github.com/spaceapi-community/my-hackerspace/pull/44 78 | [#45]: https://github.com/spaceapi-community/my-hackerspace/pull/45 79 | 80 | 81 | ## v2.1.0 (2023-04-16) 82 | 83 | - [feature] Add information about the app to settings ([#18]) 84 | - [bug] Fix widget crashing on Android 12 ([#24]) 85 | - [bug] Export widget config activity to fix crashing OpenLauncher ([#33]) 86 | - [change] Reverse latitude and longitude displayed on the screen ([#37]) 87 | - [change] Improvements for widget ([#34]) 88 | - [change] Upgrade dependencies, change TargetSDK to 33 ([#42]) 89 | 90 | Contributors to this version: 91 | 92 | - Danilo Bargen (@dbrgn) 93 | - Nicco Kunzmann (@niccokunzmann) 94 | - Adrian Pascu (@adipascu) 95 | - Frieder Hannenheim (@FriederHannenheim) 96 | 97 | Thanks! 98 | 99 | [#18]: https://github.com/spaceapi-community/my-hackerspace/pull/18 100 | [#24]: https://github.com/spaceapi-community/my-hackerspace/pull/24 101 | [#33]: https://github.com/spaceapi-community/my-hackerspace/pull/33 102 | [#34]: https://github.com/spaceapi-community/my-hackerspace/pull/34 103 | [#37]: https://github.com/spaceapi-community/my-hackerspace/pull/37 104 | [#42]: https://github.com/spaceapi-community/my-hackerspace/pull/42 105 | [#44]: https://github.com/spaceapi-community/my-hackerspace/pull/44 106 | 107 | 108 | ## v2.0.2 (2022-08-07) 109 | 110 | Unfortunately the app was pulled down from Google Play by Google due to 111 | "developer inactivity". Due to the lack of e-mail forwarding, we never noticed 112 | the warnings, so the app is now gone. 113 | 114 | To fix this, we had to change the app ID. Additionally, there will be an F-Droid release. 115 | 116 | Changes: 117 | 118 | - [change] Update dependencies 119 | - [change] Rename package to `io.spaceapi.community.myhackerspace` 120 | - [info] Add F-Droid metadata 121 | 122 | 123 | ## v2.0.1 (2021-05-14) 124 | 125 | - [bug] Fix refresh button ([#5][i5]) 126 | 127 | [i5]: https://github.com/spaceapi-community/my-hackerspace/pull/5 128 | 129 | 130 | ## v2.0.0 (2021-02-20) 131 | 132 | - [info] App was re-released by the SpaceAPI project under a new package name ([#1][i1]) 133 | - [info] GitHub is now at https://github.com/spaceapi-community/my-hackerspace/ 134 | - [info] The app now requires at least Android 5 (API 21) ([#75][i75]) 135 | - [feature] Support for SpaceAPI v14 ([#85][i85]) 136 | - [feature] New app launcher icon ([#3][i3]) 137 | - [feature] More modern icons in app UI ([#74][i74]) 138 | - [bug] Don't save empty data in application state ([#64][i64]) 139 | - [change] Update all domains to spaceapi.io ([#65][i65], [#71][i71]) 140 | - [change] Switch to Java 8 ([#73][i73]) 141 | - [change] Remove MemorizingTrustManager ([#65][i65]) 142 | - [change] Upgrade dependencies ([#69][i69]) 143 | - [change] Switch to CircleCI ([#69][i69]) 144 | - [change] Add support for annotations ([#77][i77]) 145 | - [i18n] Improved translations 146 | 147 | [i1]: https://github.com/spaceapi-community/my-hackerspace/pull/1 148 | [i3]: https://github.com/spaceapi-community/my-hackerspace/pull/3 149 | [i64]: https://github.com/fixme-lausanne/MyHackerspace/pull/64 150 | [i65]: https://github.com/fixme-lausanne/MyHackerspace/pull/65 151 | [i69]: https://github.com/fixme-lausanne/MyHackerspace/pull/69 152 | [i71]: https://github.com/fixme-lausanne/MyHackerspace/pull/71 153 | [i73]: https://github.com/fixme-lausanne/MyHackerspace/pull/73 154 | [i74]: https://github.com/fixme-lausanne/MyHackerspace/pull/74 155 | [i75]: https://github.com/fixme-lausanne/MyHackerspace/pull/75 156 | [i77]: https://github.com/fixme-lausanne/MyHackerspace/pull/77 157 | [i85]: https://github.com/fixme-lausanne/MyHackerspace/pull/85 158 | 159 | 160 | ## v1.8.3 (2017-01-XX) 161 | 162 | - Change links from SpaceAPI.net to SpaceDirectory.org 163 | - Display all webcams 164 | 165 | 166 | ## v1.8.2 (2016-07-02) 167 | 168 | - Fix camera and stream url being displayed 169 | - Fix twitter link to the new url format 170 | 171 | 172 | ## v1.8.1 (2016-05-06) 173 | 174 | - Uses custom API directory end point (https://spaceapi.fixme.ch/directory.json) 175 | - Allow editing of the API directory end point and the current hackerspace API 176 | - Add Danish translation (thanks Mikkel) 177 | 178 | 179 | ## v1.8 (2016-04-14) 180 | 181 | - Supports invalid SSL certificates 182 | - Allow widget to be resized 183 | - Add Dutch translation 184 | - Fix http to https redirection 185 | - General fixes 186 | 187 | 188 | ## v1.7.4.1 (2014-08-26) 189 | 190 | - Fix crash when there's no error message 191 | 192 | 193 | ## v1.7.4 (2014-08-07) 194 | 195 | - German translation (thanks to Lokke and Phervieux) 196 | - Better hs list with alphabetical index 197 | - Better errors messages 198 | - Caching for http requests (images, hs directory) 199 | - Add status message to the widget (thanks Fpletz) 200 | - Fix bugs: widget updates, ignore ext fields, click from widget 201 | 202 | 203 | ## v1.7.3 (2013-10-25) 204 | 205 | - Fix regression with widget custom open/close logo 206 | - Fix order of hackerspaces with different cases 207 | 208 | 209 | ## v1.7.2 (2013-09-09) 210 | 211 | - Better layout for sensors 212 | - Support more fields for sensors (machines, names, properties) 213 | 214 | 215 | ## v1.7.1 (2013-09-06) 216 | 217 | - Faster http requests (Use DefaultHttpClient instead of HttpURLConnection) 218 | 219 | 220 | ## v1.7 (2013-09-05) 221 | 222 | - Full support of SpaceAPI 0.13, drops mixed api definition: hackerspaces must comply to the level they declare! 223 | - Widget transparency preference added (by default transparency is deactivated) 224 | 225 | 226 | ## v1.6.1 (2013-06-04) 227 | 228 | - French translation 229 | - Fix the widget's image not updating 230 | - Change to the new spaceapi url 231 | 232 | 233 | ## v1.6 (2013-01-02) 234 | 235 | - Better layout in general 236 | - Use Holo light theme for Android >=3 237 | - Refresh the current hackerspace 238 | - Default to 15mn for the Widget 239 | - Settings button to change the widget interval 240 | - Fix lat/lon link 241 | - Fix crash when maps/email app not found 242 | 243 | 244 | ## v1.5.1 (2012-10-29) 245 | 246 | - Bug fixes 247 | - Add a spinner when loading image 248 | - Faster download 249 | 250 | 251 | ## v1.5 (2012-05-19) 252 | 253 | - Only download image if there is a change of state (better battery live and reduce network usage) 254 | 255 | 256 | ## v1.4 (2012-05-15) 257 | 258 | - Add Cam and Stream links if present 259 | - Link for adresses opening GMaps 260 | - Sort Hackerspaces by name 261 | - Accept untrusted SSL certificates 262 | - Better error reporting 263 | - BUGFIX: Theme shoud be correct on all devices/versions 264 | - BUGFIX: Should work after reboot correctly 265 | 266 | 267 | ## v1.3 (2012-05-08) 268 | 269 | - White theme by default (may break on samsung devices) 270 | - Check if network is enabled 271 | - Handle rotation correctly 272 | 273 | 274 | ## v1.2 (2012-05-06) 275 | 276 | 277 | ## v1.1 (2012-05-04) 278 | 279 | 280 | ## v1.0 (2012-04-29) 281 | 282 | - Initial release 283 | -------------------------------------------------------------------------------- /MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # Maintenance 2 | 3 | This project is maintained by the SpaceAPI community. 4 | 5 | ## Current Maintainers 6 | 7 | Maintainers (may review and merge): 8 | 9 | - @dbrgn 10 | - @niccokunzmann 11 | 12 | People with publication rights on Google Play: 13 | 14 | - @dbrgn 15 | 16 | People with publication rights on Accrescent: 17 | 18 | - @dbrgn 19 | 20 | ## Review and Merge Policy 21 | 22 | In case there are multiple maintainers, non-trivial changes should be reviewed 23 | by another team member. A review is requested through the GitHub UI. 24 | 25 | If a change is not reviewed within 1-2 weeks, the merge request may be merged 26 | without a review. 27 | 28 | For urgent bugfixes, the review may be skipped. 29 | 30 | ## Non-Responsive Maintainers 31 | 32 | If maintainers are not responding, send a reminder and/or try to contact them 33 | through other communication channels. If this situation persists for a longer 34 | time period, other members of the SpaceAPI community can and should take over 35 | the maintenance of the project. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

5 |
6 | 7 | # My Hackerspace 8 | 9 | [![Build status](https://circleci.com/gh/spaceapi-community/my-hackerspace.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/spaceapi-community/my-hackerspace) 10 | 11 | This is an Android app with the following features: 12 | 13 | - Show the opening status of hacker- and makerspaces using the [SpaceAPI](https://spaceapi.io/) 14 | - Show information about the space (contact, location, sensors, ...) 15 | - Status widget, multiple widgets supported 16 | 17 | The app was originally developed in 2012 by [@rorist] from [FIXME Lausanne]. In 18 | 2021, the app was transferred to the [SpaceAPI community repositories] and is 19 | now mainly being developed by members of [Coredump]. 20 | You can join our [Matrix](https://matrix.org/) chat at `#spaceapi:matrix.coredump.ch`. 21 | 22 | [@rorist]: https://github.com/rorist 23 | [FIXME Lausanne]: https://fixme.ch/ 24 | [SpaceAPI community repositories]: https://github.com/spaceapi-community/ 25 | [Coredump]: https://www.coredump.ch/ 26 | 27 | Get it on F-Droid 28 | Get it on Accrescent 29 | Get it on Google Play 30 | 31 | ## How it works 32 | 33 | The app will get the list of hackspaces from [https://directory.spaceapi.io](https://directory.spaceapi.io). 34 | You can then choose the space by its name from a list. 35 | When the space is chosen, the associated data is retrieved from the space's 36 | SpaceAPI endpoint (which is registered in the SpaceAPI directory). 37 | If you would like to add your space to the directory, have a look at 38 | [the SpaceAPI website](https://spaceapi.io/provide-an-endpoint/). 39 | 40 | ### The Widget 41 | 42 | The image for the widget is specified in the SpaceAPI endpoint JSON. 43 | Have a look at the [schema documentation](https://spaceapi.io/docs/) to make your 44 | widget more pretty! 45 | 46 | 1. `open.icon` - if present, the widget chooses the specific open/closed images 47 | 2. `logo` - the widget chooses the logo of the hackspace to display 48 | 49 | ## How to Compile 50 | 51 | First, get the sources. 52 | 53 | git clone --recursive https://github.com/spaceapi-community/my-hackerspace.git 54 | cd my-hackerspace 55 | 56 | ### Android Studio 57 | 58 | With Android Studio, simply open the project directory and you should be set. 59 | 60 | ### Command Line 61 | 62 | You can build the project using Gradle. 63 | 64 | The following examples use the gradle wrapper script which will automatically 65 | download gradle to your local directory. If you want to use your own 66 | system-wide installation instead, simply replace `./gradlew` commands with 67 | `gradle`. 68 | 69 | First, copy `local.properties.example` to `local.properties` and adjust the 70 | path to your Android SDK installation. 71 | 72 | To build a debug APK: 73 | 74 | ./gradlew assembleDebug 75 | 76 | You will find your APK file in the ` app/build/outputs/apk/` directory. 77 | 78 | You can also build and directly install the file to your connected smartphone: 79 | 80 | ./gradlew assembleDebug installDebug 81 | 82 | To build a release APK, create file `release.properties` in the project root 83 | looking like this: 84 | 85 | keystoreFile=release.keystore 86 | keystorePassword=*** 87 | keyAlias=the-key-alias 88 | keyPassword=*** 89 | 90 | Make sure that the `keystoreFile` path exists. Then run: 91 | 92 | ./gradlew clean assembleRelease 93 | 94 | 95 | To see other tasks that gradle offers, run 96 | 97 | ./gradlew tasks 98 | 99 | 100 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Set variables: 4 | 5 | $ export VERSION=X.Y.Z 6 | $ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1 # Danilo 7 | 8 | Update version numbers: 9 | 10 | $ vim app/build.gradle 11 | 12 | Update changelog: 13 | 14 | $ vim CHANGELOG.md 15 | 16 | Add the changelog to `metadata/en-US/changelogs/.txt` as well. 17 | 18 | Commit & tag: 19 | 20 | $ git commit -S${GPG_KEY} -m "Release v${VERSION}" 21 | $ git tag -s -u ${GPG_KEY} v${VERSION} -m "Version ${VERSION}" 22 | 23 | Generate signed release artifacts: 24 | 25 | ./gradlew clean assembleRelease buildApksRelease 26 | 27 | For the releases: 28 | 29 | - GitHub: Upload the signed release APK file to GitHub releases 30 | - Accrescent: Upload the signed release APKS file 31 | - F-Droid: Will be updated automatically (might take a few days) 32 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "app.accrescent.tools.bundletool" version "0.2.4" 4 | } 5 | 6 | def getReleaseProperties() { 7 | final Properties props = new Properties() 8 | final File propertiesFile = file("${projectDir.getParent()}/release.properties") 9 | if (propertiesFile.exists()) { 10 | props.load(new FileInputStream(propertiesFile)) 11 | } 12 | return props 13 | } 14 | 15 | final Properties properties = getReleaseProperties() 16 | 17 | android { 18 | namespace 'io.spaceapi.community.myhackerspace' 19 | 20 | defaultConfig { 21 | applicationId "io.spaceapi.community.myhackerspace" 22 | minSdkVersion 26 23 | compileSdk 34 24 | targetSdkVersion 34 25 | versionCode 107 26 | versionName "2.2.0" 27 | } 28 | 29 | buildFeatures { 30 | buildConfig = true 31 | } 32 | 33 | signingConfigs { 34 | if (releaseProperties['keystoreFile'] != null) { 35 | release { 36 | storeFile file(releaseProperties['keystoreFile']) 37 | storePassword releaseProperties['keystorePassword'] 38 | keyAlias releaseProperties['keyAlias'] 39 | keyPassword releaseProperties['keyPassword'] 40 | } 41 | } else { 42 | logger.warn('No release config found. In case you want to create a release build, please ensure that `release.properties` exists. See README.md for more information.') 43 | } 44 | } 45 | 46 | buildTypes { 47 | release { 48 | minifyEnabled false 49 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 50 | if (releaseProperties['keystoreFile'] != null) { 51 | signingConfig signingConfigs.release 52 | } 53 | } 54 | } 55 | 56 | compileOptions { 57 | sourceCompatibility = JavaVersion.VERSION_17 58 | targetCompatibility = JavaVersion.VERSION_17 59 | } 60 | 61 | lint { 62 | warning 'MissingTranslation' 63 | } 64 | } 65 | 66 | dependencies { 67 | implementation "androidx.annotation:annotation:1.9.1" 68 | 69 | // SpaceAPI Library 70 | implementation "io.github.spaceapi-community:spaceapi-kt:0.7.0" 71 | 72 | // JUnit for testing 73 | testImplementation 'junit:junit:4.13.2' 74 | } 75 | 76 | bundletool { 77 | if (releaseProperties['keystoreFile'] != null) { 78 | signingConfig { 79 | storeFile = file(releaseProperties['keystoreFile']) 80 | storePassword = releaseProperties['keystorePassword'] 81 | keyAlias = releaseProperties['keyAlias'] 82 | keyPassword = releaseProperties['keyPassword'] 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /app/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spaceapi-community/my-hackerspace/07f765319be6c9f104ba22f2df1bb2653e8d7939/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/woozzu/android/util/StringMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 woozzu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.woozzu.android.util; 18 | 19 | public class StringMatcher { 20 | 21 | private final static char KOREAN_UNICODE_START = '가'; 22 | private final static char KOREAN_UNICODE_END = '힣'; 23 | private final static char KOREAN_UNIT = '까' - '가'; 24 | private final static char[] KOREAN_INITIAL = {'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ' 25 | , 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'}; 26 | 27 | public static boolean match(String value, String keyword) { 28 | if (value == null || keyword == null) 29 | return false; 30 | if (keyword.length() > value.length()) 31 | return false; 32 | 33 | int i = 0, j = 0; 34 | do { 35 | if (isKorean(value.charAt(i)) && isInitialSound(keyword.charAt(j))) { 36 | if (keyword.charAt(j) == getInitialSound(value.charAt(i))) { 37 | i++; 38 | j++; 39 | } else if (j > 0) 40 | break; 41 | else 42 | i++; 43 | } else { 44 | if (keyword.charAt(j) == value.charAt(i)) { 45 | i++; 46 | j++; 47 | } else if (j > 0) 48 | break; 49 | else 50 | i++; 51 | } 52 | } while (i < value.length() && j < keyword.length()); 53 | 54 | return (j == keyword.length())? true : false; 55 | } 56 | 57 | private static boolean isKorean(char c) { 58 | if (c >= KOREAN_UNICODE_START && c <= KOREAN_UNICODE_END) 59 | return true; 60 | return false; 61 | } 62 | 63 | private static boolean isInitialSound(char c) { 64 | for (char i : KOREAN_INITIAL) { 65 | if (c == i) 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | private static char getInitialSound(char c) { 72 | 73 | if(!isKorean(c)){ 74 | return c; 75 | } 76 | 77 | return KOREAN_INITIAL[(c - KOREAN_UNICODE_START) / KOREAN_UNIT]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/woozzu/android/widget/IndexScroller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 woozzu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.woozzu.android.widget; 18 | 19 | import android.content.Context; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Paint; 23 | import android.graphics.RectF; 24 | import android.os.Handler; 25 | import android.os.Message; 26 | import android.os.SystemClock; 27 | import android.view.MotionEvent; 28 | import android.widget.Adapter; 29 | import android.widget.ListView; 30 | import android.widget.SectionIndexer; 31 | 32 | public class IndexScroller { 33 | 34 | private float mIndexbarWidth; 35 | private float mIndexbarMargin; 36 | private float mPreviewPadding; 37 | private float mDensity; 38 | private float mScaledDensity; 39 | private float mAlphaRate; 40 | private int mState = STATE_HIDDEN; 41 | private int mListViewWidth; 42 | private int mListViewHeight; 43 | private int mCurrentSection = -1; 44 | private boolean mIsIndexing = false; 45 | private ListView mListView = null; 46 | private SectionIndexer mIndexer = null; 47 | private String[] mSections = null; 48 | private RectF mIndexbarRect; 49 | 50 | private static final int STATE_HIDDEN = 0; 51 | private static final int STATE_SHOWING = 1; 52 | private static final int STATE_SHOWN = 2; 53 | private static final int STATE_HIDING = 3; 54 | 55 | public IndexScroller(Context context, ListView lv) { 56 | mDensity = context.getResources().getDisplayMetrics().density; 57 | mScaledDensity = context.getResources().getDisplayMetrics().scaledDensity; 58 | mListView = lv; 59 | setAdapter(mListView.getAdapter()); 60 | 61 | mIndexbarWidth = 20 * mDensity; 62 | mIndexbarMargin = 10 * mDensity; 63 | mPreviewPadding = 5 * mDensity; 64 | } 65 | 66 | public void draw(Canvas canvas) { 67 | if (mState == STATE_HIDDEN) 68 | return; 69 | 70 | // mAlphaRate determines the rate of opacity 71 | Paint indexbarPaint = new Paint(); 72 | indexbarPaint.setColor(Color.BLACK); 73 | indexbarPaint.setAlpha((int) (64 * mAlphaRate)); 74 | indexbarPaint.setAntiAlias(true); 75 | canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity, indexbarPaint); 76 | 77 | if (mSections != null && mSections.length > 0) { 78 | // Preview is shown when mCurrentSection is set 79 | if (mCurrentSection >= 0) { 80 | Paint previewPaint = new Paint(); 81 | previewPaint.setColor(Color.BLACK); 82 | previewPaint.setAlpha(96); 83 | previewPaint.setAntiAlias(true); 84 | previewPaint.setShadowLayer(3, 0, 0, Color.argb(64, 0, 0, 0)); 85 | 86 | Paint previewTextPaint = new Paint(); 87 | previewTextPaint.setColor(Color.WHITE); 88 | previewTextPaint.setAntiAlias(true); 89 | previewTextPaint.setTextSize(50 * mScaledDensity); 90 | 91 | float previewTextWidth = previewTextPaint.measureText(mSections[mCurrentSection]); 92 | float previewSize = 2 * mPreviewPadding + previewTextPaint.descent() - previewTextPaint.ascent(); 93 | RectF previewRect = new RectF((mListViewWidth - previewSize) / 2 94 | , (mListViewHeight - previewSize) / 2 95 | , (mListViewWidth - previewSize) / 2 + previewSize 96 | , (mListViewHeight - previewSize) / 2 + previewSize); 97 | 98 | canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity, previewPaint); 99 | canvas.drawText(mSections[mCurrentSection], previewRect.left + (previewSize - previewTextWidth) / 2 - 1 100 | , previewRect.top + mPreviewPadding - previewTextPaint.ascent() + 1, previewTextPaint); 101 | } 102 | 103 | Paint indexPaint = new Paint(); 104 | indexPaint.setColor(Color.WHITE); 105 | indexPaint.setAlpha((int) (255 * mAlphaRate)); 106 | indexPaint.setAntiAlias(true); 107 | indexPaint.setTextSize(12 * mScaledDensity); 108 | 109 | float sectionHeight = (mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length; 110 | float paddingTop = (sectionHeight - (indexPaint.descent() - indexPaint.ascent())) / 2; 111 | for (int i = 0; i < mSections.length; i++) { 112 | float paddingLeft = (mIndexbarWidth - indexPaint.measureText(mSections[i])) / 2; 113 | canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft 114 | , mIndexbarRect.top + mIndexbarMargin + sectionHeight * i + paddingTop - indexPaint.ascent(), indexPaint); 115 | } 116 | } 117 | } 118 | 119 | public boolean onTouchEvent(MotionEvent ev) { 120 | switch (ev.getAction()) { 121 | case MotionEvent.ACTION_DOWN: 122 | // If down event occurs inside index bar region, start indexing 123 | if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) { 124 | setState(STATE_SHOWN); 125 | 126 | // It demonstrates that the motion event started from index bar 127 | mIsIndexing = true; 128 | // Determine which section the point is in, and move the list to that section 129 | mCurrentSection = getSectionByPoint(ev.getY()); 130 | mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection)); 131 | return true; 132 | } 133 | break; 134 | case MotionEvent.ACTION_MOVE: 135 | if (mIsIndexing) { 136 | // If this event moves inside index bar 137 | if (contains(ev.getX(), ev.getY())) { 138 | // Determine which section the point is in, and move the list to that section 139 | mCurrentSection = getSectionByPoint(ev.getY()); 140 | mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection)); 141 | } 142 | return true; 143 | } 144 | break; 145 | case MotionEvent.ACTION_UP: 146 | if (mIsIndexing) { 147 | mIsIndexing = false; 148 | mCurrentSection = -1; 149 | } 150 | if (mState == STATE_SHOWN) 151 | setState(STATE_HIDING); 152 | break; 153 | } 154 | return false; 155 | } 156 | 157 | public void onSizeChanged(int w, int h, int oldw, int oldh) { 158 | mListViewWidth = w; 159 | mListViewHeight = h; 160 | mIndexbarRect = new RectF(w - mIndexbarMargin - mIndexbarWidth 161 | , mIndexbarMargin 162 | , w - mIndexbarMargin 163 | , h - mIndexbarMargin); 164 | } 165 | 166 | public void show() { 167 | if (mState == STATE_HIDDEN) 168 | setState(STATE_SHOWING); 169 | else if (mState == STATE_HIDING) 170 | setState(STATE_HIDING); 171 | } 172 | 173 | public void hide() { 174 | if (mState == STATE_SHOWN) 175 | setState(STATE_HIDING); 176 | } 177 | 178 | public void setAdapter(Adapter adapter) { 179 | if (adapter instanceof SectionIndexer) { 180 | mIndexer = (SectionIndexer) adapter; 181 | mSections = (String[]) mIndexer.getSections(); 182 | } 183 | } 184 | 185 | private void setState(int state) { 186 | if (state < STATE_HIDDEN || state > STATE_HIDING) 187 | return; 188 | 189 | mState = state; 190 | switch (mState) { 191 | case STATE_HIDDEN: 192 | // Cancel any fade effect 193 | mHandler.removeMessages(0); 194 | break; 195 | case STATE_SHOWING: 196 | // Start to fade in 197 | mAlphaRate = 0; 198 | fade(0); 199 | break; 200 | case STATE_SHOWN: 201 | // Cancel any fade effect 202 | mHandler.removeMessages(0); 203 | break; 204 | case STATE_HIDING: 205 | // Start to fade out after three seconds 206 | mAlphaRate = 1; 207 | fade(3000); 208 | break; 209 | } 210 | } 211 | 212 | public boolean contains(float x, float y) { 213 | // Determine if the point is in index bar region, which includes the right margin of the bar 214 | return (x >= mIndexbarRect.left && y >= mIndexbarRect.top && y <= mIndexbarRect.top + mIndexbarRect.height()); 215 | } 216 | 217 | private int getSectionByPoint(float y) { 218 | if (mSections == null || mSections.length == 0) 219 | return 0; 220 | if (y < mIndexbarRect.top + mIndexbarMargin) 221 | return 0; 222 | if (y >= mIndexbarRect.top + mIndexbarRect.height() - mIndexbarMargin) 223 | return mSections.length - 1; 224 | return (int) ((y - mIndexbarRect.top - mIndexbarMargin) / ((mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length)); 225 | } 226 | 227 | private void fade(long delay) { 228 | mHandler.removeMessages(0); 229 | mHandler.sendEmptyMessageAtTime(0, SystemClock.uptimeMillis() + delay); 230 | } 231 | 232 | private Handler mHandler = new Handler() { 233 | 234 | @Override 235 | public void handleMessage(Message msg) { 236 | super.handleMessage(msg); 237 | 238 | switch (mState) { 239 | case STATE_SHOWING: 240 | // Fade in effect 241 | mAlphaRate += (1 - mAlphaRate) * 0.2; 242 | if (mAlphaRate > 0.9) { 243 | mAlphaRate = 1; 244 | setState(STATE_SHOWN); 245 | } 246 | 247 | mListView.invalidate(); 248 | fade(10); 249 | break; 250 | case STATE_SHOWN: 251 | // If no action, hide automatically 252 | setState(STATE_HIDING); 253 | break; 254 | case STATE_HIDING: 255 | // Fade out effect 256 | mAlphaRate -= mAlphaRate * 0.2; 257 | if (mAlphaRate < 0.1) { 258 | mAlphaRate = 0; 259 | setState(STATE_HIDDEN); 260 | } 261 | 262 | mListView.invalidate(); 263 | fade(10); 264 | break; 265 | } 266 | } 267 | 268 | }; 269 | } 270 | -------------------------------------------------------------------------------- /app/src/main/java/com/woozzu/android/widget/IndexableListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 woozzu 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.woozzu.android.widget; 18 | 19 | import android.content.Context; 20 | import android.graphics.Canvas; 21 | import android.util.AttributeSet; 22 | import android.view.GestureDetector; 23 | import android.view.MotionEvent; 24 | import android.widget.ListAdapter; 25 | import android.widget.ListView; 26 | 27 | public class IndexableListView extends ListView { 28 | 29 | private boolean mIsFastScrollEnabled = false; 30 | private IndexScroller mScroller = null; 31 | private GestureDetector mGestureDetector = null; 32 | 33 | public IndexableListView(Context context) { 34 | super(context); 35 | } 36 | 37 | public IndexableListView(Context context, AttributeSet attrs) { 38 | super(context, attrs); 39 | } 40 | 41 | public IndexableListView(Context context, AttributeSet attrs, int defStyle) { 42 | super(context, attrs, defStyle); 43 | } 44 | 45 | @Override 46 | public boolean isFastScrollEnabled() { 47 | return mIsFastScrollEnabled; 48 | } 49 | 50 | @Override 51 | public void setFastScrollEnabled(boolean enabled) { 52 | mIsFastScrollEnabled = enabled; 53 | if (mIsFastScrollEnabled) { 54 | if (mScroller == null) 55 | mScroller = new IndexScroller(getContext(), this); 56 | } else { 57 | if (mScroller != null) { 58 | mScroller.hide(); 59 | mScroller = null; 60 | } 61 | } 62 | } 63 | 64 | @Override 65 | public void draw(Canvas canvas) { 66 | super.draw(canvas); 67 | 68 | // Overlay index bar 69 | if (mScroller != null) 70 | mScroller.draw(canvas); 71 | } 72 | 73 | @Override 74 | public boolean onTouchEvent(MotionEvent ev) { 75 | // Intercept ListView's touch event 76 | if (mScroller != null && mScroller.onTouchEvent(ev)) 77 | return true; 78 | 79 | if (mGestureDetector == null) { 80 | mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { 81 | 82 | @Override 83 | public boolean onFling(MotionEvent e1, MotionEvent e2, 84 | float velocityX, float velocityY) { 85 | // If fling happens, index bar shows 86 | if (mScroller != null) 87 | mScroller.show(); 88 | return super.onFling(e1, e2, velocityX, velocityY); 89 | } 90 | 91 | }); 92 | } 93 | mGestureDetector.onTouchEvent(ev); 94 | 95 | return super.onTouchEvent(ev); 96 | } 97 | 98 | @Override 99 | public boolean onInterceptTouchEvent(MotionEvent ev) { 100 | if(mScroller.contains(ev.getX(), ev.getY())) 101 | return true; 102 | 103 | return super.onInterceptTouchEvent(ev); 104 | } 105 | 106 | @Override 107 | public void setAdapter(ListAdapter adapter) { 108 | super.setAdapter(adapter); 109 | if (mScroller != null) 110 | mScroller.setAdapter(adapter); 111 | } 112 | 113 | @Override 114 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 115 | super.onSizeChanged(w, h, oldw, oldh); 116 | if (mScroller != null) 117 | mScroller.onSizeChanged(w, h, oldw, oldh); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/io/spaceapi/community/myhackerspace/AboutLayout.java: -------------------------------------------------------------------------------- 1 | package io.spaceapi.community.myhackerspace; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.LayoutInflater; 6 | import android.widget.LinearLayout; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.Nullable; 10 | 11 | public class AboutLayout extends LinearLayout { 12 | 13 | public AboutLayout(Context context) { 14 | super(context); 15 | } 16 | 17 | public AboutLayout(Context context, @Nullable AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public AboutLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 22 | super(context, attrs, defStyleAttr); 23 | } 24 | 25 | public AboutLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 26 | super(context, attrs, defStyleAttr, defStyleRes); 27 | } 28 | 29 | public void init() { 30 | TextView version = findViewById(R.id.about_version_text); 31 | version.setText(BuildConfig.VERSION_NAME + " (" + Integer.toString(BuildConfig.VERSION_CODE) + ")"); 32 | } 33 | 34 | public static AboutLayout create(Context context) { 35 | LayoutInflater layoutInflater = LayoutInflater.from(context); 36 | AboutLayout about = (AboutLayout) layoutInflater.inflate(R.layout.about, null, false); 37 | about.init(); 38 | return about; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/io/spaceapi/community/myhackerspace/Net.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2017 Aubort Jean-Baptiste (Rorist) 3 | * Copyright (C) 2020-2025 Danilo Bargen (dbrgn) 4 | * Licensed under GNU's GPL 3, see README 5 | */ 6 | package io.spaceapi.community.myhackerspace; 7 | 8 | import android.graphics.Bitmap; 9 | import android.graphics.BitmapFactory; 10 | import android.os.Build; 11 | import android.util.Log; 12 | 13 | import java.io.BufferedReader; 14 | import java.io.FilterInputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.InputStreamReader; 18 | import java.net.HttpURLConnection; 19 | import java.net.URL; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.annotation.WorkerThread; 23 | 24 | // From CommonsWare and Android Blog 25 | // https://github.com/commonsguy/cw-android/tree/master/Internet 26 | // http://android-developers.blogspot.ch/2010/07/multithreading-for-performance.html 27 | public class Net { 28 | 29 | private final String USERAGENT = "Android/" + Build.VERSION.RELEASE + " (" 30 | + Build.MODEL + ") MyHackerspace/" + BuildConfig.VERSION_NAME; 31 | 32 | private static final String TAG = "MyHackerspace_Net"; 33 | 34 | private HttpURLConnection mUrlConnection; 35 | private InputStream mInputStream; 36 | 37 | @WorkerThread 38 | public Net(@NonNull String urlStr) throws Throwable { 39 | this(urlStr, true); 40 | } 41 | 42 | @WorkerThread 43 | public Net(@NonNull String urlStr, boolean useCache) throws Throwable { 44 | // Connect to URL 45 | URL url; 46 | int responseCode; 47 | int redirect_limt = 10; 48 | do { 49 | Log.v(TAG, "fetching " + urlStr); 50 | url = new URL(urlStr); 51 | mUrlConnection = (HttpURLConnection) url.openConnection(); 52 | mUrlConnection.setRequestProperty("User-Agent", USERAGENT); 53 | mUrlConnection.setUseCaches(useCache); 54 | 55 | mUrlConnection.connect(); 56 | responseCode = mUrlConnection.getResponseCode(); 57 | 58 | // HttpsURLConnection does not support redirect with protocol switch, 59 | // so we take care of that here: 60 | if(responseCode == HttpURLConnection.HTTP_MOVED_TEMP 61 | || responseCode == HttpURLConnection.HTTP_MOVED_PERM) { 62 | urlStr = mUrlConnection.getHeaderField("Location"); 63 | redirect_limt -= 1; 64 | } else { 65 | break; 66 | } 67 | } while(redirect_limt > 0); 68 | 69 | if (responseCode != HttpURLConnection.HTTP_OK) { 70 | String msg = mUrlConnection.getResponseMessage(); 71 | mUrlConnection.disconnect(); 72 | throw new Throwable(msg); 73 | } 74 | 75 | mInputStream = mUrlConnection.getInputStream(); 76 | } 77 | 78 | @WorkerThread 79 | @NonNull 80 | public String getString() throws Throwable { 81 | try { 82 | BufferedReader r = new BufferedReader(new InputStreamReader(mInputStream)); 83 | StringBuilder str = new StringBuilder(); 84 | String line; 85 | while ((line = r.readLine()) != null) { 86 | str.append(line); 87 | } 88 | return str.toString(); 89 | } finally { 90 | if (mInputStream != null) { 91 | mInputStream.close(); 92 | } 93 | mUrlConnection.disconnect(); 94 | } 95 | } 96 | 97 | @WorkerThread 98 | @NonNull 99 | public Bitmap getBitmap() throws Throwable { 100 | try { 101 | return BitmapFactory.decodeStream(new FlushedInputStream(mInputStream)); 102 | } finally { 103 | if (mInputStream != null) { 104 | mInputStream.close(); 105 | } 106 | mUrlConnection.disconnect(); 107 | } 108 | } 109 | 110 | static class FlushedInputStream extends FilterInputStream { 111 | public FlushedInputStream(InputStream inputStream) { 112 | super(inputStream); 113 | } 114 | 115 | @Override 116 | public long skip(long n) throws IOException { 117 | long totalBytesSkipped = 0L; 118 | while (totalBytesSkipped < n) { 119 | long bytesSkipped = in.skip(n - totalBytesSkipped); 120 | if (bytesSkipped == 0L) { 121 | int b = read(); 122 | if (b < 0) { 123 | break; // we reached EOF 124 | } else { 125 | bytesSkipped = 1; // we read one byte 126 | } 127 | } 128 | totalBytesSkipped += bytesSkipped; 129 | } 130 | return totalBytesSkipped; 131 | } 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/io/spaceapi/community/myhackerspace/Network.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2017 Aubort Jean-Baptiste (Rorist) 3 | * Copyright (C) 2020-2025 Danilo Bargen (dbrgn) 4 | * Licensed under GNU's GPL 3, see README 5 | */ 6 | package io.spaceapi.community.myhackerspace; 7 | 8 | import android.content.BroadcastReceiver; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.util.Log; 12 | 13 | public class Network extends BroadcastReceiver { 14 | 15 | @Override 16 | public void onReceive(Context ctxt, Intent intent) { 17 | if (Main.hasNetwork(ctxt)) { 18 | Log.i(Main.TAG, "Update widget on " + intent.getAction()); 19 | Widget.UpdateAllWidgets(ctxt, true); 20 | } else { 21 | Log.e(Main.TAG, "Network not ready on " + intent.getAction()); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/io/spaceapi/community/myhackerspace/Prefs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2017 Aubort Jean-Baptiste (Rorist) 3 | * Copyright (C) 2020-2025 Danilo Bargen (dbrgn) 4 | * Licensed under GNU's GPL 3, see README 5 | */ 6 | package io.spaceapi.community.myhackerspace; 7 | 8 | import static android.view.ViewGroup.LayoutParams.*; 9 | 10 | import android.content.SharedPreferences; 11 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 12 | import android.os.Bundle; 13 | import android.preference.PreferenceActivity; 14 | import android.preference.PreferenceScreen; 15 | import android.view.LayoutInflater; 16 | import android.view.ViewGroup; 17 | import android.widget.LinearLayout; 18 | 19 | public class Prefs extends PreferenceActivity implements 20 | OnSharedPreferenceChangeListener { 21 | 22 | public static final String KEY_API_ENDPOINT = "api_endpoint"; 23 | public static final String DEFAULT_API_ENDPOINT = "https://raw.githubusercontent.com/SpaceApi/directory/master/directory.json"; 24 | 25 | public static final String KEY_API_URL = "apiurl"; 26 | 27 | public static final String KEY_CHECK_INTERVAL = "check_interval"; 28 | public static final String DEFAULT_CHECK_INTERVAL = "30"; // minutes 29 | 30 | public static final String KEY_WIDGET_TRANSPARENCY = "widget_transparency"; 31 | public static final boolean DEFAULT_WIDGET_TRANSPARENCY = false; 32 | 33 | public static final String KEY_WIDGET_TEXT = "widget_text"; 34 | public static final boolean DEFAULT_WIDGET_TEXT = false; 35 | 36 | public void onCreate(Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | addPreferencesFromResource(R.xml.preferences); 39 | this.getListView().addFooterView(AboutLayout.create(this)); // tried addContentView 40 | PreferenceScreen ps = getPreferenceScreen(); 41 | ps.getSharedPreferences() 42 | .registerOnSharedPreferenceChangeListener(this); 43 | } 44 | 45 | public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 46 | if (key.equals(KEY_WIDGET_TRANSPARENCY) || key.equals(KEY_WIDGET_TEXT) 47 | || key.equals(KEY_CHECK_INTERVAL)) { 48 | Widget.UpdateAllWidgets(getApplicationContext(), true); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/io/spaceapi/community/myhackerspace/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2025 Danilo Bargen (dbrgn) 3 | * Licensed under GNU's GPL 3, see README 4 | */ 5 | package io.spaceapi.community.myhackerspace; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | 10 | public class Utils { 11 | /** 12 | * Join the strings using the specified separator. 13 | */ 14 | public static @Nullable String joinStrings(@NonNull String separator, String... strings) { 15 | final StringBuilder builder = new StringBuilder(); 16 | boolean empty = true; 17 | for (String string : strings) { 18 | if (string != null) { 19 | if (empty) { 20 | builder.append(string); 21 | empty = false; 22 | } else { 23 | builder.append(separator).append(string); 24 | } 25 | } 26 | } 27 | return empty ? null : builder.toString(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/spaceapi/community/myhackerspace/Widget.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2017 Aubort Jean-Baptiste (Rorist) 3 | * Copyright (C) 2020-2025 Danilo Bargen (dbrgn) 4 | * Licensed under GNU's GPL 3, see README 5 | */ 6 | package io.spaceapi.community.myhackerspace; 7 | 8 | import android.app.AlarmManager; 9 | import android.app.IntentService; 10 | import android.app.PendingIntent; 11 | import android.appwidget.AppWidgetManager; 12 | import android.appwidget.AppWidgetProvider; 13 | import android.content.ComponentName; 14 | import android.content.Context; 15 | import android.content.Intent; 16 | import android.content.SharedPreferences; 17 | import android.content.SharedPreferences.Editor; 18 | import android.graphics.Bitmap; 19 | import android.os.AsyncTask; 20 | import android.os.Build; 21 | import android.os.Handler; 22 | import android.os.Looper; 23 | import android.preference.PreferenceManager; 24 | import android.util.Log; 25 | import android.view.View; 26 | import android.widget.RemoteViews; 27 | import android.widget.Toast; 28 | 29 | import java.lang.ref.WeakReference; 30 | import java.net.MalformedURLException; 31 | import java.net.URL; 32 | 33 | import io.spaceapi.ParseError; 34 | import io.spaceapi.SpaceApiParser; 35 | 36 | public class Widget extends AppWidgetProvider { 37 | 38 | static final String TAG ="MyHackerspace_Widget"; 39 | static final String WIDGET_IDS = "widget_ids"; 40 | static final String WIDGET_FORCE = "widget_force"; 41 | 42 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 43 | // see https://intrepidgeeks.com/tutorial/android-app-widgets 44 | final int N = appWidgetIds.length; 45 | 46 | // Perform this loop procedure for each App Widget that belongs to this provider 47 | for (int i=0; i { 134 | 135 | private final int mId; 136 | private WeakReference mCtxt; 137 | private final String mText; 138 | private String mError = null; 139 | 140 | public GetImage(Context ctxt, int id, String text) { 141 | mCtxt = new WeakReference<>(ctxt); 142 | mId = id; 143 | mText = text; 144 | } 145 | 146 | @Override 147 | protected Bitmap doInBackground(URL... url) { 148 | try { 149 | return new Net(url[0].toString()).getBitmap(); 150 | } catch (Throwable e) { 151 | e.printStackTrace(); 152 | mError = e.getMessage(); 153 | cancel(true); 154 | } 155 | return null; 156 | } 157 | 158 | @Override 159 | protected void onPostExecute(Bitmap result) { 160 | final Context ctxt = mCtxt.get(); 161 | if(ctxt == null) { Log.e(TAG, "Context error (postExecute)"); return; } 162 | AppWidgetManager manager = AppWidgetManager.getInstance(ctxt); 163 | updateWidget(ctxt, mId, manager, result, mText); 164 | } 165 | 166 | @Override 167 | protected void onCancelled () { 168 | final Context ctxt = mCtxt.get(); 169 | if (mError != null && ctxt != null) { 170 | printMessage(ctxt, mError); 171 | } 172 | } 173 | 174 | } 175 | 176 | protected static void updateWidget(final Context ctxt, int widgetId, 177 | AppWidgetManager manager, Bitmap bitmap, String text) { 178 | RemoteViews views = new RemoteViews(ctxt.getPackageName(), 179 | R.layout.widget); 180 | SharedPreferences prefs = PreferenceManager 181 | .getDefaultSharedPreferences(ctxt); 182 | Editor edit = prefs.edit(); 183 | if (prefs.getBoolean(Prefs.KEY_WIDGET_TRANSPARENCY, 184 | Prefs.DEFAULT_WIDGET_TRANSPARENCY)) { 185 | views.setInt(R.id.widget_image, "setBackgroundResource", 0); 186 | } else { 187 | views.setInt(R.id.widget_image, "setBackgroundResource", 188 | android.R.drawable.btn_default_small); 189 | } 190 | if (bitmap != null) { 191 | views.setImageViewBitmap(R.id.widget_image, bitmap); 192 | edit.putBoolean(Main.PREF_FORCE_WIDGET + widgetId, false); 193 | } else { 194 | // Something went wrong 195 | views.setImageViewResource(R.id.widget_image, 196 | android.R.drawable.ic_popup_sync); 197 | edit.putBoolean(Main.PREF_FORCE_WIDGET + widgetId, true); 198 | } 199 | if (text != null) { 200 | views.setTextViewText(R.id.widget_status, text); 201 | views.setViewVisibility(R.id.widget_status, View.VISIBLE); 202 | } else { 203 | views.setViewVisibility(R.id.widget_status, View.GONE); 204 | } 205 | edit.commit(); 206 | Intent clickIntent = new Intent(ctxt, Main.class); 207 | clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); 208 | PendingIntent pendingIntent = PendingIntent.getActivity(ctxt, widgetId, 209 | clickIntent, PendingIntent.FLAG_CANCEL_CURRENT + getPendingIntentMutableFlag()); 210 | views.setOnClickPendingIntent(R.id.widget_image, pendingIntent); 211 | manager.updateAppWidget(widgetId, views); 212 | } 213 | 214 | private static class GetApiTask extends AsyncTask { 215 | 216 | private final int mId; 217 | private WeakReference mCtxt; 218 | private String mError = null; 219 | 220 | public GetApiTask(Context ctxt, int id) { 221 | mCtxt = new WeakReference<>(ctxt); 222 | mId = id; 223 | } 224 | 225 | @Override 226 | protected String doInBackground(String... url) { 227 | try { 228 | return new Net(url[0], false).getString(); 229 | } catch (Throwable e) { 230 | e.printStackTrace(); 231 | mError = e.getMessage(); 232 | cancel(true); 233 | } 234 | return ""; 235 | } 236 | 237 | @Override 238 | protected void onCancelled() { 239 | Log.i(TAG, "Reset alarm after cancel"); 240 | final Context ctxt = mCtxt.get(); 241 | if(ctxt != null) { 242 | Intent intent = getIntent(ctxt, mId); 243 | setAlarm(ctxt, intent, mId, 500); 244 | if (mError != null) { 245 | printMessage(ctxt, mError); 246 | } 247 | } 248 | } 249 | 250 | @Override 251 | protected void onPostExecute(String endpointJson) { 252 | final Context ctxt = mCtxt.get(); 253 | if(ctxt == null) { Log.e(TAG, "Context error (postExecute)"); return; } 254 | 255 | try { 256 | final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctxt); 257 | 258 | final io.spaceapi.types.Status data = SpaceApiParser.parseString(endpointJson); 259 | 260 | boolean statusBool = data.state != null && data.state.open; 261 | 262 | // Update only if different than last status or not forced 263 | if (prefs.contains(Main.PREF_LAST_WIDGET + mId) 264 | && prefs.getBoolean(Main.PREF_LAST_WIDGET + mId, false) == statusBool 265 | && !prefs.getBoolean(Main.PREF_FORCE_WIDGET + mId, false)) { 266 | Log.d(TAG, "Nothing to update"); 267 | return; 268 | } 269 | 270 | // Mandatory fields 271 | final Editor edit = prefs.edit(); 272 | edit.putBoolean(Main.PREF_LAST_WIDGET + mId, statusBool); 273 | edit.apply(); 274 | 275 | String status_text = null; 276 | if (prefs.getBoolean(Prefs.KEY_WIDGET_TEXT, Prefs.DEFAULT_WIDGET_TEXT)) { 277 | if (data.state != null && data.state.message != null) { 278 | status_text = data.state.message; 279 | } else { 280 | status_text = statusBool 281 | ? ctxt.getString(R.string.status_open) 282 | : ctxt.getString(R.string.status_closed); 283 | } 284 | } 285 | 286 | // Status icon or space icon 287 | if (data.state != null && data.state.icon != null) { 288 | new GetImage(ctxt, mId, status_text).execute( 289 | statusBool? new URL(data.state.icon.open) : new URL(data.state.icon.closed) 290 | ); 291 | } else { 292 | new GetImage(ctxt, mId, status_text).execute(new URL(data.logo)); 293 | } 294 | } catch (ParseError | MalformedURLException e) { 295 | e.printStackTrace(); 296 | String msg = e.getMessage(); 297 | printMessage(ctxt, msg); 298 | } 299 | } 300 | } 301 | 302 | public static class UpdateService extends IntentService { 303 | 304 | public UpdateService() { 305 | super("MyHackerspaceWidgetService"); 306 | } 307 | 308 | @Override 309 | protected void onHandleIntent(Intent intent) { 310 | final Context context = UpdateService.this; 311 | final int widgetId = intent.getIntExtra( 312 | AppWidgetManager.EXTRA_APPWIDGET_ID, 313 | AppWidgetManager.INVALID_APPWIDGET_ID); 314 | Widget.startWidgetUpdateTask(context, widgetId); 315 | stopSelf(); 316 | } 317 | } 318 | 319 | private static void startWidgetUpdateTask(Context context, int widgetId) { 320 | SharedPreferences prefs = PreferenceManager 321 | .getDefaultSharedPreferences(context); 322 | if (Main.hasNetwork(context) && prefs.contains(Main.PREF_API_URL_WIDGET + widgetId)) { 323 | final String url = prefs.getString(Main.PREF_API_URL_WIDGET + widgetId, Main.API_DEFAULT); 324 | Log.i(TAG, "Update widgetId " + widgetId + " with url " + url); 325 | new Handler(Looper.getMainLooper()) 326 | .post(() -> new GetApiTask(context, widgetId).execute(url)); 327 | } 328 | } 329 | 330 | public static void UpdateAllWidgets(final Context ctxt, boolean force) { 331 | AppWidgetManager man = AppWidgetManager.getInstance(ctxt); 332 | int[] ids = man.getAppWidgetIds(new ComponentName(ctxt, Widget.class)); 333 | Intent ui = new Intent(); 334 | ui.setClass(ctxt, Widget.class); 335 | ui.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); 336 | ui.putExtra(Widget.WIDGET_IDS, ids); 337 | ui.putExtra(Widget.WIDGET_FORCE, force); 338 | ctxt.sendBroadcast(ui); 339 | Log.i(TAG, "UpdateAllWidgets force=" + force); 340 | } 341 | 342 | private static void printMessage(final Context ctxt, String msg){ 343 | if(msg == null){ 344 | return; 345 | } 346 | Log.e(TAG, msg); 347 | Toast.makeText(ctxt, msg, Toast.LENGTH_SHORT).show(); 348 | } 349 | 350 | } 351 | -------------------------------------------------------------------------------- /app/src/main/java/io/spaceapi/community/myhackerspace/Widget_config.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2017 Aubort Jean-Baptiste (Rorist) 3 | * Copyright (C) 2020-2025 Danilo Bargen (dbrgn) 4 | * Licensed under GNU's GPL 3, see README 5 | */ 6 | package io.spaceapi.community.myhackerspace; 7 | 8 | import android.app.Activity; 9 | import android.app.AlertDialog; 10 | import android.app.Dialog; 11 | import android.app.ProgressDialog; 12 | import android.appwidget.AppWidgetManager; 13 | import android.content.Context; 14 | import android.content.Intent; 15 | import android.content.SharedPreferences; 16 | import android.content.SharedPreferences.Editor; 17 | import android.os.AsyncTask; 18 | import android.os.Bundle; 19 | import android.preference.PreferenceManager; 20 | import android.text.Editable; 21 | import android.text.TextWatcher; 22 | import android.util.Log; 23 | import android.view.View; 24 | import android.widget.AdapterView; 25 | import android.widget.AdapterView.OnItemSelectedListener; 26 | import android.widget.ArrayAdapter; 27 | import android.widget.CheckBox; 28 | import android.widget.EditText; 29 | import android.widget.Spinner; 30 | 31 | import org.json.JSONArray; 32 | import org.json.JSONException; 33 | import org.json.JSONObject; 34 | 35 | import java.util.ArrayList; 36 | import java.util.Arrays; 37 | 38 | public class Widget_config extends Activity { 39 | 40 | private static final int DIALOG_LOADING = 0; 41 | private SharedPreferences mPrefs; 42 | private GetDirTask mGetDirTask; 43 | private int mAppWidgetId; 44 | private String mApiEndpoint; 45 | 46 | @Override 47 | public void onCreate(Bundle savedInstanceState) { 48 | super.onCreate(savedInstanceState); 49 | setContentView(R.layout.widget_config); 50 | mPrefs = PreferenceManager 51 | .getDefaultSharedPreferences(Widget_config.this); 52 | mApiEndpoint = mPrefs.getString(Prefs.KEY_API_ENDPOINT, Prefs.DEFAULT_API_ENDPOINT); 53 | mGetDirTask = new GetDirTask(); 54 | mGetDirTask.execute(mApiEndpoint); 55 | Intent intent = getIntent(); 56 | Bundle extras = intent.getExtras(); 57 | mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, 58 | AppWidgetManager.INVALID_APPWIDGET_ID); 59 | findViewById(R.id.choose_ok).setOnClickListener(v -> { 60 | Editor edit = mPrefs.edit(); 61 | edit.putBoolean( 62 | Prefs.KEY_WIDGET_TRANSPARENCY, 63 | ((CheckBox) findViewById(R.id.choose_transparency)) 64 | .isChecked()); 65 | edit.putBoolean(Prefs.KEY_WIDGET_TEXT, 66 | ((CheckBox) findViewById(R.id.choose_text)) 67 | .isChecked()); 68 | edit.commit(); 69 | setWidgetAlarm(); 70 | finish(); 71 | }); 72 | ((CheckBox) findViewById(R.id.choose_transparency)).setChecked(mPrefs 73 | .getBoolean(Prefs.KEY_WIDGET_TRANSPARENCY, 74 | Prefs.DEFAULT_WIDGET_TRANSPARENCY)); 75 | ((CheckBox) findViewById(R.id.choose_text)).setChecked(mPrefs 76 | .getBoolean(Prefs.KEY_WIDGET_TEXT, Prefs.DEFAULT_WIDGET_TEXT)); 77 | ((EditText) findViewById(R.id.choose_update)).setText(mPrefs.getString( 78 | Prefs.KEY_CHECK_INTERVAL, Prefs.DEFAULT_CHECK_INTERVAL)); 79 | ((EditText) findViewById(R.id.choose_update)) 80 | .addTextChangedListener(new TextWatcher() { 81 | @Override 82 | public void onTextChanged(CharSequence s, int start, 83 | int before, int count) { 84 | String inter = s.toString(); 85 | if (!"".equals(inter) && !"0".equals(inter)) { 86 | Editor edit = mPrefs.edit(); 87 | edit.putString(Prefs.KEY_CHECK_INTERVAL, inter); 88 | edit.commit(); 89 | } 90 | } 91 | 92 | @Override 93 | public void beforeTextChanged(CharSequence s, int start, 94 | int count, int after) { 95 | } 96 | 97 | @Override 98 | public void afterTextChanged(Editable s) { 99 | } 100 | }); 101 | } 102 | 103 | @Override 104 | protected Dialog onCreateDialog(int id) { 105 | AlertDialog dialog = null; 106 | switch (id) { 107 | case DIALOG_LOADING: 108 | dialog = new ProgressDialog(this); 109 | dialog.setMessage(getString(R.string.msg_loading)); 110 | dialog.setCancelable(true); 111 | ((ProgressDialog) dialog).setIndeterminate(true); 112 | break; 113 | } 114 | return dialog; 115 | } 116 | 117 | private void setWidgetAlarm() { 118 | Context ctxt = getApplicationContext(); 119 | Intent i = Widget.getIntent(ctxt, mAppWidgetId); 120 | setResult(RESULT_OK, i); 121 | Widget.setAlarm(ctxt, i, mAppWidgetId); 122 | } 123 | 124 | public class GetDirTask extends AsyncTask { 125 | @Override 126 | protected void onPreExecute() { 127 | showDialog(DIALOG_LOADING); 128 | } 129 | 130 | @Override 131 | protected String doInBackground(String... url) { 132 | try { 133 | return new Net(url[0], false).getString(); 134 | } catch (Throwable e) { 135 | e.printStackTrace(); 136 | } 137 | return ""; 138 | } 139 | 140 | @Override 141 | protected void onPostExecute(String result) { 142 | // Construct hackerspaces list 143 | Spinner s = findViewById(R.id.choose_hs); 144 | try { 145 | JSONObject obj = new JSONObject(result); 146 | JSONArray arr = obj.names(); 147 | int len = obj.length(); 148 | String[] names = new String[len]; 149 | final ArrayList url = new ArrayList<>(len); 150 | for (int i = 0; i < len; i++) { 151 | names[i] = arr.getString(i); 152 | } 153 | Arrays.sort(names); 154 | for (int i = 0; i < len; i++) { 155 | url.add(i, obj.getString(names[i])); 156 | } 157 | ArrayAdapter adapter = new ArrayAdapter<>( 158 | Widget_config.this, 159 | android.R.layout.simple_spinner_item, names); 160 | adapter.setDropDownViewResource(android.R.layout.simple_list_item_1); 161 | s.setAdapter(adapter); 162 | s.setOnItemSelectedListener(new OnItemSelectedListener() { 163 | public void onItemSelected(AdapterView adapter, View v, 164 | int position, long id) { 165 | Editor edit = mPrefs.edit(); 166 | edit.putString(Main.PREF_API_URL_WIDGET + mAppWidgetId, 167 | url.get(position)); 168 | edit.commit(); 169 | } 170 | 171 | public void onNothingSelected(AdapterView arg0) { 172 | } 173 | }); 174 | } catch (JSONException e) { 175 | e.printStackTrace(); 176 | Log.e(Main.TAG, mApiEndpoint); 177 | Log.e(Main.TAG, result); 178 | } 179 | 180 | removeDialog(DIALOG_LOADING); 181 | } 182 | 183 | @Override 184 | protected void onCancelled() { 185 | removeDialog(DIALOG_LOADING); 186 | } 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_view_list_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/about.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | 20 | 27 | 28 | 29 | 33 | 34 | 39 | 40 | 47 | 48 | 49 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/base.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 12 | 13 | 14 | 15 | 23 | 24 | 29 | 30 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/entry_sensor.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 23 | 34 | 45 | 56 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/hs_choose.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/hs_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 16 | 21 | 22 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 13 | 14 | 21 | 22 | 29 | 30 | 39 | 40 | 48 | 49 | 58 | 59 | 68 | 69 | 70 | 71 | 72 | 73 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/res/layout/separator.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/subtitle.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/title.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 21 | 22 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 18 | 19 | 26 | 27 | 34 | 35 | 42 | 43 | 49 | 50 | 56 | 57 |