├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── de │ │ └── codefor │ │ └── karlsruhe │ │ └── opensense │ │ ├── OpenSenseApplication.kt │ │ ├── data │ │ ├── DateTimeAdapter.kt │ │ ├── OpenSenseMapService.kt │ │ └── boxes │ │ │ ├── BoxesApi.kt │ │ │ └── model │ │ │ ├── Geometry.kt │ │ │ ├── LastMeasurement.kt │ │ │ ├── LocItem.kt │ │ │ ├── SenseBox.kt │ │ │ ├── Sensor.kt │ │ │ └── SensorHistory.kt │ │ └── widget │ │ ├── WidgetHelper.kt │ │ ├── base │ │ ├── BaseWidget.kt │ │ ├── BaseWidgetConfigurationActivity.kt │ │ └── SensorListAdapter.kt │ │ ├── onevalue │ │ ├── OneValueConfigurationActivity.kt │ │ └── OneValueWidget.kt │ │ └── plot │ │ ├── PlotWidget.kt │ │ └── PlotWidgetConfigurationActivity.kt │ └── res │ ├── drawable-hdpi │ ├── configuration_action_save.png │ ├── configuration_list_adapter_cloud.png │ ├── widget_configuration.png │ └── widget_refresh.png │ ├── drawable-mdpi │ ├── configuration_action_save.png │ ├── configuration_list_adapter_cloud.png │ ├── widget_configuration.png │ └── widget_refresh.png │ ├── drawable-nodpi │ ├── one_value_widget_example.png │ └── plot_widget_example.png │ ├── drawable-xhdpi │ ├── configuration_action_save.png │ ├── configuration_list_adapter_cloud.png │ ├── widget_configuration.png │ └── widget_refresh.png │ ├── drawable-xxhdpi │ ├── configuration_action_save.png │ ├── configuration_list_adapter_cloud.png │ ├── widget_configuration.png │ └── widget_refresh.png │ ├── drawable │ └── sensor_list_item_selector.xml │ ├── layout │ ├── activity_base_widget_configuration.xml │ ├── one_value_widget.xml │ ├── plot_widget.xml │ └── recycler_view_sensor_list_item.xml │ ├── menu │ └── base_widget_configuration.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 │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── one_value_widget_info.xml │ └── plot_widget_info.xml ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── de │ └── codefor │ └── karlsruhe │ └── opensense │ └── build │ └── Config.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /buildSrc/build 10 | /buildSrc/.gradle -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | An app with widgets for the great [openSenseMap](https://opensensemap.org) project. 3 | 4 | ## 0.6.0 (2011-06-13) 5 | Thanks to [Tobias Preuss](https://github.com/johnjohndoe) for contributing the below changes. 6 | - Update of several dependencies 7 | - Build: Use gradle-versions-plugin to detect version updates 8 | - Build: Debug build variant with different package name to enable parallel install 9 | 10 | ## 0.4.1 (2019-04-10) 11 | #### Fixed 12 | - Update mapbox dependency to fix a crash with the map view 13 | 14 | ## 0.4.0 (2019-04-10) 15 | Update build script, API calls and migrate to Gradle Kotlin DSL 16 | 17 | ## 0.3.0 (2018-01-29) 18 | Expand the features 19 | 20 | #### Added 21 | - The `Plot Widget`, which shows a plot of one sensor value 22 | 23 | 24 | ## 0.2.0 (2017-10-08) 25 | Improve widget design and configuration 26 | 27 | #### Added 28 | - Two additional buttons for the widget. They make it possible to refresh and change the configuration 29 | 30 | #### Changed 31 | - The senseBox is now selected in the configuration due a map and not by id 32 | 33 | 34 | ## 0.1.0 (2017-10-02) 35 | Initial release with an app widget. 36 | 37 | #### Added 38 | - The `One Value Widget`, which shows one sensor value of a senseBox 39 | - A configuration screen to select the senseBox and a sensor for the widget 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Open Knowledge Lab Karlsruhe 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Sense 2 | An app with widgets for the great [openSenseMap](https://opensensemap.org) project. 3 | It's still work in progress and in an early stage of development. 4 | 5 | The current version `0.6.0` is available in the [Play Store](https://play.google.com/store/apps/details?id=de.codefor.karlsruhe.opensense). 6 | 7 | 8 | ## Features 9 | - `One Value Widget`, which shows one sensor value of a senseBox 10 | - `Plot Widget`, which shows a plot of one sensor value 11 | 12 | 13 | ## Development 14 | The app is work in progress and in an early development stage. 15 | Any suggestions, feature requests, bug reports or pull requests are very much appreciated. 16 | 17 | 18 | Open Sense uses the [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows#gitflow-workflow) workflow: 19 | - All pull requests should be branched from develop 20 | - The pull request is merged into develop 21 | - The develop branch is merged into master for the next release 22 | 23 | To work correctly, Open Sense requires an API token from [mapbox](https://www.mapbox.com/). Setup steps: 24 | - Register for the free mapbox account 25 | - Go to [API access token](https://www.mapbox.com/studio/account/tokens/) and get your API access token 26 | - Define the `mapboxApiToken` variable with the acquired token in the gradle user home `gradle.properties`. 27 | For Linux and Mac this is usually `~/.gradle/gradle.properties` 28 | 29 | Example of the `gradle.properties`: 30 | ``` 31 | mapboxApiToken="API TOKEN" 32 | ``` 33 | 34 | 35 | ## License 36 | Copyright (c) Open Knowledge Lab Karlsruhe 37 | All rights reserved. 38 | 39 | Redistribution and use in source and binary forms, with or without 40 | modification, are permitted provided that the following conditions are met: 41 | 42 | 1. Redistributions of source code must retain the above copyright notice, this 43 | list of conditions and the following disclaimer. 44 | 45 | 2. Redistributions in binary form must reproduce the above copyright notice, 46 | this list of conditions and the following disclaimer in the documentation 47 | and/or other materials provided with the distribution. 48 | 49 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 50 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 51 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 52 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 53 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 54 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 55 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 56 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 57 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 58 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.codefor.karlsruhe.opensense.build.BuildConfig 2 | 3 | plugins { 4 | id("com.android.application") 5 | id("kotlin-android") 6 | id("kotlin-android-extensions") 7 | } 8 | 9 | val mapboxApiToken: String by project 10 | 11 | android { 12 | compileSdkVersion(BuildConfig.compileSdkVersion) 13 | defaultConfig { 14 | applicationId = "de.codefor.karlsruhe.opensense" 15 | minSdkVersion(BuildConfig.minSdkVersion) 16 | targetSdkVersion(BuildConfig.targetSdkVersion) 17 | versionCode = BuildConfig.versionCode 18 | versionName = BuildConfig.versionName 19 | 20 | multiDexEnabled = true 21 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 22 | 23 | buildConfigField("String", "MAPBOX_API_TOKEN", mapboxApiToken) 24 | } 25 | buildTypes { 26 | getByName("debug") { 27 | applicationIdSuffix = ".debug" 28 | } 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 32 | } 33 | } 34 | } 35 | 36 | dependencies { 37 | implementation("com.android.support:multidex:${BuildConfig.supportLibMultiDexVersion}") 38 | implementation("com.android.support:appcompat-v7:${BuildConfig.supportLibVersion}") 39 | implementation("com.android.support:design:${BuildConfig.supportLibVersion}") 40 | implementation("com.android.support:recyclerview-v7:${BuildConfig.supportLibVersion}") 41 | 42 | implementation("net.danlew:android.joda:${BuildConfig.jodaVersion}") 43 | 44 | implementation("io.reactivex.rxjava2:rxandroid:${BuildConfig.rxAndroidVersion}") 45 | implementation("io.reactivex.rxjava2:rxjava:${BuildConfig.rxJavaVersion}") 46 | 47 | implementation("com.squareup.retrofit2:retrofit:${BuildConfig.retrofitVersion}") 48 | implementation("com.squareup.retrofit2:converter-moshi:${BuildConfig.retrofitVersion}") 49 | implementation("com.squareup.moshi:moshi-kotlin:${BuildConfig.moshiVersion}") 50 | implementation("com.squareup.retrofit2:adapter-rxjava2:${BuildConfig.retrofitVersion}") 51 | 52 | implementation("com.mapbox.mapboxsdk:mapbox-android-sdk:${BuildConfig.mapboxVersion}") 53 | 54 | implementation("com.androidplot:androidplot-core:${BuildConfig.androidPlotVersion}") 55 | 56 | testImplementation("junit:junit:${BuildConfig.junitVersion}") 57 | 58 | androidTestImplementation("com.android.support.test.espresso:espresso-core:${BuildConfig.espressoVersion}") { 59 | exclude(group = "com.android.support", module = "support-annotations") 60 | } 61 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class com.androidplot.** { *; } -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Open Sense DEBUG 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/OpenSenseApplication.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense 2 | 3 | import android.support.multidex.MultiDexApplication 4 | import com.mapbox.mapboxsdk.Mapbox 5 | import net.danlew.android.joda.JodaTimeAndroid 6 | 7 | class OpenSenseApplication: MultiDexApplication() { 8 | override fun onCreate() { 9 | super.onCreate() 10 | 11 | JodaTimeAndroid.init(this) 12 | Mapbox.getInstance(applicationContext, BuildConfig.MAPBOX_API_TOKEN) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/DateTimeAdapter.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data 2 | 3 | import com.squareup.moshi.JsonAdapter 4 | import com.squareup.moshi.JsonReader 5 | import com.squareup.moshi.JsonWriter 6 | import org.joda.time.DateTime 7 | import org.joda.time.format.ISODateTimeFormat 8 | import java.io.IOException 9 | 10 | 11 | class DateTimeAdapter : JsonAdapter() { 12 | private val fmt = ISODateTimeFormat.dateTime() 13 | 14 | @Synchronized 15 | @Throws(IOException::class) 16 | override fun fromJson(reader: JsonReader): DateTime { 17 | return DateTime(reader.nextString()) 18 | } 19 | 20 | @Synchronized 21 | @Throws(IOException::class) 22 | override fun toJson(writer: JsonWriter, value: DateTime?) { 23 | writer.value(fmt.print(value)) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/OpenSenseMapService.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 5 | import de.codefor.karlsruhe.opensense.data.boxes.BoxesApi 6 | import de.codefor.karlsruhe.opensense.data.boxes.model.SenseBox 7 | import de.codefor.karlsruhe.opensense.data.boxes.model.Sensor 8 | import de.codefor.karlsruhe.opensense.data.boxes.model.SensorHistory 9 | import io.reactivex.Single 10 | import io.reactivex.functions.BiFunction 11 | import org.joda.time.DateTime 12 | import retrofit2.Retrofit 13 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 14 | import retrofit2.converter.moshi.MoshiConverterFactory 15 | 16 | typealias SensorData = Pair> 17 | 18 | object OpenSenseMapService { 19 | private val boxesApi: BoxesApi 20 | 21 | init { 22 | val moshi = Moshi.Builder() 23 | .add(KotlinJsonAdapterFactory()) 24 | .add(DateTime::class.java, DateTimeAdapter()).build() 25 | 26 | val retrofit = Retrofit.Builder() 27 | .baseUrl("https://api.opensensemap.org/") 28 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 29 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 30 | .build() 31 | 32 | boxesApi = retrofit.create(BoxesApi::class.java) 33 | } 34 | 35 | fun getBox(boxId: String): Single { 36 | return boxesApi.getBox(boxId) 37 | } 38 | 39 | fun getAllBoxes(): Single> { 40 | return boxesApi.getAllBoxes() 41 | } 42 | 43 | fun getSenseBoxAndSensorData(boxId: String, sensorId: String): Single> { 44 | return boxesApi.getBox(boxId).zipWith(boxesApi.getSensorHistory(boxId, sensorId), 45 | BiFunction, Pair> { senseBox, sensorHistory -> 46 | val sensor = senseBox.sensors?.first { it.id == sensorId } 47 | ?: throw IllegalStateException("The box $boxId doesn't contain the sensor id $sensorId") 48 | Pair(senseBox, SensorData(sensor, sensorHistory)) 49 | }) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/boxes/BoxesApi.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data.boxes 2 | 3 | import de.codefor.karlsruhe.opensense.data.boxes.model.SenseBox 4 | import de.codefor.karlsruhe.opensense.data.boxes.model.SensorHistory 5 | import io.reactivex.Single 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | import retrofit2.http.Query 9 | 10 | interface BoxesApi { 11 | @GET("boxes/{boxId}") 12 | fun getBox(@Path("boxId") boxId: String) : Single 13 | 14 | @GET("boxes") 15 | fun getAllBoxes(@Query("minimal") minimal: Boolean = true): Single> 16 | 17 | @GET("boxes/{boxId}/data/{sensorId}") 18 | fun getSensorHistory(@Path("boxId") boxId: String, @Path("sensorId") sensorId: String): Single> 19 | } -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/boxes/model/Geometry.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data.boxes.model 2 | 3 | data class Geometry( 4 | val coordinates: List?, 5 | val type: String? 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/boxes/model/LastMeasurement.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data.boxes.model 2 | 3 | import org.joda.time.DateTime 4 | 5 | data class LastMeasurement( 6 | val createdAt: DateTime?, 7 | val value: String? 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/boxes/model/LocItem.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data.boxes.model 2 | 3 | data class LocItem( 4 | val geometry: Geometry?, 5 | val type: String? 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/boxes/model/SenseBox.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data.boxes.model 2 | 3 | import com.squareup.moshi.Json 4 | import org.joda.time.DateTime 5 | 6 | data class SenseBox( 7 | @Json(name = "_id") val id: String?, 8 | val createdAt: DateTime?, 9 | val updatedAt: DateTime?, 10 | val name: String?, 11 | val boxType: String?, 12 | val model: String?, 13 | val grouptag: String?, 14 | val exposure: String?, 15 | val weblink: String?, 16 | val description: String?, 17 | val currentLocation: Geometry?, 18 | val loc: List?, 19 | val sensors: List? 20 | ) 21 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/boxes/model/Sensor.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data.boxes.model 2 | 3 | import com.squareup.moshi.Json 4 | 5 | data class Sensor( 6 | @Json(name = "_id") val id: String?, 7 | val lastMeasurement: LastMeasurement?, 8 | val sensorType: String?, 9 | val title: String?, 10 | val unit: String? 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/data/boxes/model/SensorHistory.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.data.boxes.model 2 | 3 | import org.joda.time.DateTime 4 | 5 | data class SensorHistory( 6 | val value: Double?, 7 | val location: List?, 8 | val createdAt: DateTime? 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/WidgetHelper.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget 2 | 3 | import android.app.PendingIntent 4 | import android.appwidget.AppWidgetManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import de.codefor.karlsruhe.opensense.data.OpenSenseMapService 8 | import de.codefor.karlsruhe.opensense.data.SensorData 9 | import de.codefor.karlsruhe.opensense.data.boxes.model.SenseBox 10 | import de.codefor.karlsruhe.opensense.data.boxes.model.Sensor 11 | import de.codefor.karlsruhe.opensense.widget.base.BaseWidget 12 | import de.codefor.karlsruhe.opensense.widget.base.BaseWidgetConfigurationActivity 13 | import io.reactivex.Single 14 | import io.reactivex.android.schedulers.AndroidSchedulers 15 | import io.reactivex.schedulers.Schedulers 16 | import kotlin.reflect.KClass 17 | 18 | object WidgetHelper { 19 | private val PREFS_NAME = "de.codefor.karlsruhe.opensense.widget" 20 | private val PREF_BOX_ID = "box_id_" 21 | private val PREF_SENSOR_IDS = "sensor_ids_" 22 | 23 | internal fun saveConfiguration(context: Context, appWidgetId: Int, boxId: String, sensors: List) { 24 | val sensorIds = mutableListOf() 25 | sensors.forEach { it.id?.let { it1 -> sensorIds.add(it1) } } 26 | 27 | context.getSharedPreferences(PREFS_NAME, 0) 28 | .edit() 29 | .putString(PREF_BOX_ID + appWidgetId, boxId) 30 | .putStringSet(PREF_SENSOR_IDS + appWidgetId, sensorIds.toSet()) 31 | .apply() 32 | } 33 | 34 | internal fun deleteConfiguration(context: Context, appWidgetId: Int) { 35 | context.getSharedPreferences(PREFS_NAME, 0) 36 | .edit() 37 | .remove(PREF_BOX_ID + appWidgetId) 38 | .remove(PREF_SENSOR_IDS + appWidgetId) 39 | .apply() 40 | } 41 | 42 | internal fun loadBoxId(context: Context, appWidgetId: Int): String { 43 | val prefs = context.getSharedPreferences(PREFS_NAME, 0) 44 | return prefs.getString(PREF_BOX_ID + appWidgetId, "")!! 45 | } 46 | 47 | internal fun loadSensorIds(context: Context, appWidgetId: Int): List { 48 | val prefs = context.getSharedPreferences(PREFS_NAME, 0) 49 | return prefs.getStringSet(PREF_SENSOR_IDS + appWidgetId, emptySet())!!.toList() 50 | } 51 | 52 | internal fun getSenseBox(context: Context, appWidgetId: Int): Single { 53 | val boxId = loadBoxId(context, appWidgetId) 54 | return getSenseBox(boxId) 55 | } 56 | 57 | internal fun getSenseBox(boxId: String): Single { 58 | if (boxId.isEmpty()) return Single.error { Exception() } 59 | 60 | return OpenSenseMapService.getBox(boxId) 61 | .subscribeOn(Schedulers.io()) 62 | .observeOn(AndroidSchedulers.mainThread()) 63 | } 64 | 65 | internal fun getAllBoxes(): Single> { 66 | return OpenSenseMapService.getAllBoxes() 67 | .subscribeOn(Schedulers.io()) 68 | .observeOn(AndroidSchedulers.mainThread()) 69 | } 70 | 71 | internal fun getSenseBoxAndSensorData(context: Context, appWidgetId: Int): Single> { 72 | val boxId = loadBoxId(context, appWidgetId) 73 | // We keep it really simple here and just use the first selected sensor of this widget 74 | val sensorId = loadSensorIds(context, appWidgetId).firstOrNull() ?: return Single.error(IllegalStateException("No sensor id stored")) 75 | 76 | return OpenSenseMapService.getSenseBoxAndSensorData(boxId, sensorId) 77 | .subscribeOn(Schedulers.io()) 78 | .observeOn(AndroidSchedulers.mainThread()) 79 | } 80 | 81 | internal fun createConfigurationPendingIntent(context: Context, 82 | appWidgetId: Int, 83 | configActivity: KClass): PendingIntent { 84 | val intent = Intent(context, configActivity.java) 85 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) 86 | return PendingIntent.getActivity(context, appWidgetId, intent, 0) 87 | } 88 | 89 | internal fun createRefreshPendingIntent(context: Context, 90 | appWidgetId: Int, 91 | appWidgetProvider: KClass): PendingIntent { 92 | val intent = Intent(context, appWidgetProvider.java) 93 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, IntArray(1, { appWidgetId })) 94 | intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE 95 | return PendingIntent.getBroadcast(context, appWidgetId, intent, 0) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/base/BaseWidget.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget.base 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.appwidget.AppWidgetProvider 5 | import android.content.Context 6 | import de.codefor.karlsruhe.opensense.widget.WidgetHelper 7 | 8 | /** 9 | * Implementation of default widget containing a maximum of five sensor data. 10 | * The configuration is implemented in [BaseWidgetConfigurationActivity] and its child classes. 11 | */ 12 | abstract class BaseWidget : AppWidgetProvider() { 13 | override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { 14 | for (appWidgetId in appWidgetIds) { 15 | onUpdateWidget(context, appWidgetId, appWidgetManager) 16 | } 17 | } 18 | 19 | override fun onDeleted(context: Context, appWidgetIds: IntArray) { 20 | for (appWidgetId in appWidgetIds) { 21 | WidgetHelper.deleteConfiguration(context, appWidgetId) 22 | } 23 | } 24 | 25 | abstract fun onUpdateWidget(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager) 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/base/BaseWidgetConfigurationActivity.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget.base 2 | 3 | import android.app.Activity 4 | import android.appwidget.AppWidgetManager 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.support.design.widget.Snackbar 8 | import android.support.v7.app.AppCompatActivity 9 | import android.support.v7.widget.LinearLayoutManager 10 | import android.view.Menu 11 | import android.view.MenuItem 12 | import android.view.View 13 | import com.mapbox.mapboxsdk.annotations.MarkerOptions 14 | import com.mapbox.mapboxsdk.camera.CameraUpdateFactory 15 | import com.mapbox.mapboxsdk.geometry.LatLng 16 | import com.mapbox.mapboxsdk.maps.MapboxMap 17 | import com.mapbox.mapboxsdk.maps.Style 18 | import de.codefor.karlsruhe.opensense.R 19 | import de.codefor.karlsruhe.opensense.data.boxes.model.SenseBox 20 | import de.codefor.karlsruhe.opensense.widget.WidgetHelper 21 | import kotlinx.android.synthetic.main.activity_base_widget_configuration.* 22 | 23 | 24 | /** 25 | * The configuration screen for the widgets. 26 | */ 27 | abstract class BaseWidgetConfigurationActivity : AppCompatActivity() { 28 | // The maximum items that can be selected. Set in the child class. 29 | protected var maxSensorItems = 0 30 | 31 | private var widgetId = AppWidgetManager.INVALID_APPWIDGET_ID 32 | private var boxId = "" 33 | 34 | public override fun onCreate(icicle: Bundle?) { 35 | super.onCreate(icicle) 36 | 37 | setResult(Activity.RESULT_CANCELED) 38 | 39 | setContentView(R.layout.activity_base_widget_configuration) 40 | default_widget_configure_box_sensors_recycler_view.layoutManager = LinearLayoutManager(this) 41 | 42 | val extras = intent.extras 43 | if (extras != null) { 44 | widgetId = extras.getInt( 45 | AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) 46 | } 47 | 48 | when (widgetId) { 49 | AppWidgetManager.INVALID_APPWIDGET_ID -> finish() 50 | } 51 | 52 | default_widget_configure_mapView.onCreate(icicle) 53 | default_widget_configure_mapView.getMapAsync { mapboxMap -> 54 | mapboxMap.setStyle(Style.LIGHT) { 55 | WidgetHelper.getAllBoxes().subscribe { boxes -> displayBoxesOnMap(mapboxMap, boxes) } 56 | 57 | // handle marker clicks 58 | mapboxMap.setOnMarkerClickListener { marker -> 59 | run { 60 | WidgetHelper.getSenseBox(marker.snippet) 61 | .subscribe(this::showBoxInformation) { 62 | Snackbar.make(coordinator_layout, R.string.widget_configuration_snackbar_error_loading, Snackbar.LENGTH_SHORT) 63 | .show() 64 | } 65 | return@setOnMarkerClickListener true 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 73 | menuInflater.inflate(R.menu.base_widget_configuration, menu) 74 | return true 75 | } 76 | 77 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 78 | if (item.itemId == R.id.action_save) { 79 | saveAndShowWidget() 80 | return true 81 | } 82 | return super.onOptionsItemSelected(item) 83 | } 84 | 85 | private fun displayBoxesOnMap(mapboxMap: MapboxMap, boxes: List) { 86 | if (boxes.isEmpty()) { 87 | Snackbar.make(coordinator_layout, R.string.widget_configuration_snackbar_error_loading, Snackbar.LENGTH_SHORT) 88 | .show() 89 | return 90 | } 91 | 92 | val currentBoxId = WidgetHelper.loadBoxId(this@BaseWidgetConfigurationActivity, widgetId) 93 | for (box in boxes) { 94 | val coordinates = box.currentLocation?.coordinates ?: continue 95 | val markerPosition = LatLng(coordinates[1], coordinates[0]) 96 | mapboxMap.addMarker(MarkerOptions() 97 | .position(markerPosition) 98 | .title(box.name) 99 | .snippet(box.id) 100 | ) 101 | 102 | if (box.id == currentBoxId) { 103 | mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(markerPosition, 11.0)) 104 | showBoxInformation(box) 105 | } 106 | } 107 | } 108 | 109 | private fun showBoxInformation(senseBox: SenseBox) { 110 | if (senseBox.id == null || senseBox.sensors == null) { 111 | Snackbar.make(coordinator_layout, R.string.widget_configuration_snackbar_error_invalid_data, Snackbar.LENGTH_SHORT) 112 | .show() 113 | return 114 | } 115 | 116 | boxId = senseBox.id 117 | default_widget_configure_box.visibility = View.VISIBLE 118 | default_widget_configure_box_name.text = senseBox.name 119 | default_widget_configure_box_description.text = senseBox.description 120 | default_widget_configure_box_sensors_recycler_view.adapter = SensorListAdapter(senseBox.sensors) 121 | } 122 | 123 | private fun saveAndShowWidget() { 124 | val adapter = default_widget_configure_box_sensors_recycler_view.adapter 125 | if (adapter == null || adapter !is SensorListAdapter) return 126 | 127 | when { 128 | adapter.getSelectedItems().isEmpty() -> { 129 | Snackbar.make(coordinator_layout, 130 | R.string.widget_configuration_snackbar_empty_list, Snackbar.LENGTH_SHORT).show() 131 | } 132 | 133 | adapter.getSelectedItems().size <= maxSensorItems -> { 134 | WidgetHelper.saveConfiguration(this, widgetId, boxId, adapter.getSelectedItems()) 135 | update(widgetId) 136 | closeConfigurationActivity() 137 | } 138 | 139 | else -> { 140 | val text = resources.getQuantityString(R.plurals.maximumNumberOfSensors, maxSensorItems, maxSensorItems) 141 | Snackbar.make(coordinator_layout, text, Snackbar.LENGTH_SHORT).show() 142 | } 143 | } 144 | } 145 | 146 | private fun closeConfigurationActivity() { 147 | val resultValue = Intent() 148 | resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) 149 | setResult(Activity.RESULT_OK, resultValue) 150 | finish() 151 | } 152 | 153 | protected abstract fun update(widgetId: Int) 154 | } 155 | 156 | 157 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/base/SensorListAdapter.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget.base 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import de.codefor.karlsruhe.opensense.R 8 | import de.codefor.karlsruhe.opensense.data.boxes.model.Sensor 9 | import kotlinx.android.synthetic.main.recycler_view_sensor_list_item.view.* 10 | 11 | class SensorListAdapter(private val sensors: List) : RecyclerView.Adapter() { 12 | private val selectedSensors: MutableList = mutableListOf() 13 | 14 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 15 | val view = LayoutInflater.from(parent.context) 16 | .inflate(R.layout.recycler_view_sensor_list_item, parent, false) 17 | return ViewHolder(view, { 18 | when { 19 | selectedSensors.contains(it) -> selectedSensors.remove(it) 20 | else -> selectedSensors.add(it) 21 | } 22 | }) 23 | } 24 | 25 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 26 | holder.bind(sensors[position]) 27 | } 28 | 29 | override fun getItemCount() = sensors.size 30 | 31 | fun getSelectedItems(): List { 32 | return selectedSensors 33 | } 34 | 35 | 36 | class ViewHolder(view: View, private val itemClick: (Sensor) -> Unit) : RecyclerView.ViewHolder(view) { 37 | fun bind(sensor: Sensor) { 38 | with(sensor) { 39 | itemView.sensor_item_title.text = sensor.title 40 | itemView.sensor_item_value.text = itemView.context.getString(R.string.widget_configuration_current_value, 41 | sensor.lastMeasurement?.value, sensor.unit) 42 | itemView.setOnClickListener({ 43 | itemView.isSelected = !itemView.isSelected 44 | itemClick(this) 45 | }) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/onevalue/OneValueConfigurationActivity.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget.onevalue 2 | 3 | import android.appwidget.AppWidgetManager 4 | import de.codefor.karlsruhe.opensense.widget.base.BaseWidgetConfigurationActivity 5 | 6 | class OneValueConfigurationActivity : BaseWidgetConfigurationActivity() { 7 | init { 8 | maxSensorItems = 1 9 | } 10 | 11 | override fun update(widgetId: Int) { 12 | OneValueWidget.update(this, widgetId, AppWidgetManager.getInstance(this)) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/onevalue/OneValueWidget.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget.onevalue 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.Context 5 | import android.view.View 6 | import android.widget.RemoteViews 7 | import de.codefor.karlsruhe.opensense.R 8 | import de.codefor.karlsruhe.opensense.widget.WidgetHelper 9 | import de.codefor.karlsruhe.opensense.widget.base.BaseWidget 10 | 11 | 12 | class OneValueWidget : BaseWidget() { 13 | override fun onUpdateWidget(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager) { 14 | update(context, appWidgetId, appWidgetManager) 15 | } 16 | 17 | companion object { 18 | fun update(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager) { 19 | val views = RemoteViews(context.packageName, R.layout.one_value_widget) 20 | //Show progress bar, hide refresh button 21 | views.apply { 22 | setViewVisibility(R.id.one_value_widget_refresh_button, View.INVISIBLE) 23 | setViewVisibility(R.id.one_value_widget_progress_bar, View.VISIBLE) 24 | setProgressBar(R.id.one_value_widget_progress_bar, 100, 0, true) 25 | } 26 | appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views) 27 | 28 | WidgetHelper.getSenseBox(context, appWidgetId).subscribe({ senseBox -> 29 | val sensorIds = WidgetHelper.loadSensorIds(context, appWidgetId) 30 | val sensor = senseBox.sensors?.first { (id) -> id == sensorIds.first() } 31 | views.apply { 32 | //Show refresh button, hide progress bar 33 | setViewVisibility(R.id.one_value_widget_refresh_button, View.VISIBLE) 34 | setViewVisibility(R.id.one_value_widget_progress_bar, View.GONE) 35 | //Update values 36 | setTextViewText(R.id.one_value_widget_box_name, senseBox.name) 37 | setTextViewText(R.id.one_value_widget_sensor_data, "${sensor?.lastMeasurement?.value} ${sensor?.unit}") 38 | setTextViewText(R.id.one_value_widget_sensor_title, sensor?.title) 39 | } 40 | 41 | setOnClickPendingIntents(context, appWidgetId, views) 42 | appWidgetManager.updateAppWidget(appWidgetId, views) 43 | }, { 44 | views.apply { 45 | //Show refresh button, hide progress bar 46 | setViewVisibility(R.id.one_value_widget_refresh_button, View.VISIBLE) 47 | setViewVisibility(R.id.one_value_widget_progress_bar, View.GONE) 48 | //Remove values, set error text 49 | setTextViewText(R.id.one_value_widget_box_name, "") 50 | setTextViewText(R.id.one_value_widget_sensor_data, context.getString(R.string.loading_error_text_generic)) 51 | setTextViewText(R.id.one_value_widget_sensor_title, "") 52 | } 53 | 54 | setOnClickPendingIntents(context, appWidgetId, views) 55 | appWidgetManager.updateAppWidget(appWidgetId, views) 56 | }) 57 | } 58 | 59 | private fun setOnClickPendingIntents(context: Context, appWidgetId: Int, views: RemoteViews) { 60 | views.setOnClickPendingIntent( 61 | R.id.one_value_widget_configuration_button, 62 | WidgetHelper.createConfigurationPendingIntent(context, appWidgetId, OneValueConfigurationActivity::class) 63 | ) 64 | 65 | views.setOnClickPendingIntent( 66 | R.id.one_value_widget_refresh_button, 67 | WidgetHelper.createRefreshPendingIntent(context, appWidgetId, OneValueWidget::class) 68 | ) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/plot/PlotWidget.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget.plot 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import android.graphics.Canvas 7 | import android.graphics.Color 8 | import android.graphics.Paint 9 | import android.os.Bundle 10 | import android.view.View 11 | import android.widget.RemoteViews 12 | import com.androidplot.ui.HorizontalPositioning 13 | import com.androidplot.ui.Size 14 | import com.androidplot.ui.VerticalPositioning 15 | import com.androidplot.util.PixelUtils 16 | import com.androidplot.xy.* 17 | import de.codefor.karlsruhe.opensense.R 18 | import de.codefor.karlsruhe.opensense.data.boxes.model.Sensor 19 | import de.codefor.karlsruhe.opensense.data.boxes.model.SensorHistory 20 | import de.codefor.karlsruhe.opensense.widget.WidgetHelper 21 | import de.codefor.karlsruhe.opensense.widget.base.BaseWidget 22 | import org.joda.time.DateTime 23 | import org.joda.time.format.DateTimeFormat 24 | import java.text.FieldPosition 25 | import java.text.Format 26 | import java.text.ParsePosition 27 | 28 | 29 | class PlotWidget : BaseWidget() { 30 | override fun onUpdateWidget(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager) { 31 | update(context, appWidgetId, appWidgetManager) 32 | } 33 | 34 | override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, 35 | appWidgetId: Int, newOptions: Bundle) { 36 | onUpdateWidget(context, appWidgetId, appWidgetManager) 37 | } 38 | 39 | companion object { 40 | private val dateTimeFormatterStartEnd = DateTimeFormat.forPattern("dd.MM.") 41 | private val dateTimeFormatter = DateTimeFormat.forPattern("dd.MM. HH:mm") 42 | 43 | 44 | fun update(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager) { 45 | val views = RemoteViews(context.packageName, R.layout.plot_widget) 46 | 47 | //Show progress bar, hide refresh button 48 | views.apply { 49 | setViewVisibility(R.id.plot_widget_refresh_button, View.INVISIBLE) 50 | setViewVisibility(R.id.plot_widget_progress_bar, View.VISIBLE) 51 | setProgressBar(R.id.plot_widget_progress_bar, 100, 0, true) 52 | } 53 | appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views) 54 | 55 | WidgetHelper.getSenseBoxAndSensorData(context, appWidgetId).subscribe( 56 | // onSuccess 57 | { (senseBox, sensorData) -> 58 | // Kotlin doesn't support nested destructuring, so we do it here 59 | val (sensor, sensorHist) = sensorData 60 | views.apply { 61 | //Show refresh button, hide progress bar 62 | setViewVisibility(R.id.plot_widget_refresh_button, View.VISIBLE) 63 | setViewVisibility(R.id.plot_widget_progress_bar, View.GONE) 64 | setTextViewText(R.id.plot_widget_box_name, senseBox.name) 65 | setViewVisibility(R.id.plot_widget_sensor_title, View.VISIBLE) 66 | setTextViewText(R.id.plot_widget_sensor_title, sensor.title) 67 | } 68 | appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views) 69 | drawPlot(context, appWidgetId, appWidgetManager, sensor, sensorHist) 70 | }, { 71 | showErrorScreen(context, appWidgetId, appWidgetManager, views) 72 | } 73 | ) 74 | } 75 | 76 | private fun showErrorScreen(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager, 77 | views: RemoteViews, errorId: Int = R.string.loading_error_text_generic) { 78 | views.apply { 79 | // Show refresh button, hide progress bar 80 | setViewVisibility(R.id.plot_widget_refresh_button, View.VISIBLE) 81 | setViewVisibility(R.id.plot_widget_progress_bar, View.GONE) 82 | setTextViewText(R.id.plot_widget_error_text, context.getString(errorId)) 83 | setViewVisibility(R.id.plot_widget_error_text, View.VISIBLE) 84 | // Hide widget image and sensor title 85 | setViewVisibility(R.id.plot_widget_img, View.GONE) 86 | setViewVisibility(R.id.plot_widget_sensor_title, View.GONE) 87 | } 88 | setOnClickPendingIntents(context, appWidgetId, views) 89 | appWidgetManager.updateAppWidget(appWidgetId, views) 90 | } 91 | 92 | private fun drawPlot(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager, 93 | sensor: Sensor, sensorHist: List) { 94 | val views = RemoteViews(context.packageName, R.layout.plot_widget) 95 | 96 | if (sensorHist.isEmpty()) { 97 | showErrorScreen(context, appWidgetId, appWidgetManager, views, R.string.loading_error_text_no_data) 98 | return 99 | } 100 | 101 | val dates = mutableListOf() 102 | val values = mutableListOf() 103 | 104 | // The time in a plot usually increases from left to right. 105 | // The provided data starts with the newest first and we have to reverse it. 106 | sensorHist.reversed().forEach { (value, _, createdAt) -> 107 | dates.add(createdAt ?: DateTime.now()) 108 | values.add(value ?: Double.NaN) 109 | } 110 | 111 | if (dates.isEmpty() || values.isEmpty()) { 112 | showErrorScreen(context, appWidgetId, appWidgetManager, views, R.string.loading_error_text_no_data) 113 | return 114 | } 115 | 116 | val plot = XYPlot(context, "") // no title for the plot, it should be self-evident 117 | plot.apply { 118 | setRangeLabel(sensor.unit) 119 | setRangeStep(StepMode.SUBDIVIDE, 4.0) 120 | setDomainLabel(context.getString(R.string.plot_graph_time)) 121 | setDomainStep(StepMode.SUBDIVIDE, 3.0) 122 | setBackgroundColor(Color.TRANSPARENT) 123 | } 124 | 125 | 126 | val textSize = context.resources.getDimension(R.dimen.widget_text_size_small) 127 | 128 | // Configure the graph 129 | plot.graph.apply { 130 | // show the tick labels 131 | setLineLabelEdges(XYGraphWidget.Edge.RIGHT, XYGraphWidget.Edge.BOTTOM) 132 | 133 | // add space for the labels 134 | size = Size.FILL 135 | marginLeft = PixelUtils.dpToPix(8f) 136 | marginTop = PixelUtils.dpToPix(8f) 137 | marginRight = PixelUtils.dpToPix(40f) 138 | marginBottom = PixelUtils.dpToPix(24f) 139 | 140 | lineLabelInsets.right = PixelUtils.dpToPix(-15f) 141 | lineLabelInsets.bottom = PixelUtils.dpToPix(-10f) 142 | 143 | // format the labels 144 | getLineLabelStyle(XYGraphWidget.Edge.RIGHT).paint.color = Color.WHITE 145 | getLineLabelStyle(XYGraphWidget.Edge.RIGHT).paint.textSize = textSize 146 | getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).paint.color = Color.WHITE 147 | getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).paint.textSize = textSize 148 | getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).paint.textAlign = Paint.Align.LEFT 149 | 150 | // format the DateTime 151 | getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).format = object : Format() { 152 | override fun format(obj: Any, toAppendTo: StringBuffer, pos: FieldPosition): StringBuffer { 153 | val index = (obj as Number).toInt() 154 | if (index < 0 || index >= dates.size) return toAppendTo 155 | 156 | val date = dates[index] 157 | 158 | return when (index) { 159 | 0, dates.lastIndex -> toAppendTo.append(date.toString(dateTimeFormatterStartEnd)) 160 | else -> toAppendTo.append(date.toString(dateTimeFormatter)) 161 | } 162 | } 163 | 164 | override fun parseObject(source: String, pos: ParsePosition): Any? { 165 | return null 166 | } 167 | } 168 | } 169 | 170 | // Format the title (time and unit) 171 | plot.rangeTitle.labelPaint.textSize = textSize 172 | plot.rangeTitle.position( 173 | 25f, HorizontalPositioning.ABSOLUTE_FROM_RIGHT, 174 | 0f, VerticalPositioning.ABSOLUTE_FROM_CENTER) 175 | plot.domainTitle.labelPaint.textSize = textSize 176 | plot.domainTitle.position( 177 | 0f, HorizontalPositioning.ABSOLUTE_FROM_CENTER, 178 | 20f, VerticalPositioning.ABSOLUTE_FROM_BOTTOM) 179 | plot.legend.isVisible = false 180 | 181 | val widgetWidth = appWidgetManager.getAppWidgetOptions(appWidgetId).getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) 182 | val widgetHeight = appWidgetManager.getAppWidgetOptions(appWidgetId).getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) 183 | plot.measure(widgetWidth, widgetHeight) 184 | plot.layout(0, 0, widgetWidth, widgetHeight) 185 | 186 | val series = SimpleXYSeries(values, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "") 187 | val seriesFormat = LineAndPointFormatter(Color.TRANSPARENT, Color.BLACK, Color.TRANSPARENT, null) 188 | 189 | // add the series to the xyplot: 190 | plot.addSeries(series, seriesFormat) 191 | 192 | val bitmap = Bitmap.createBitmap(widgetWidth, widgetHeight, Bitmap.Config.ARGB_8888) 193 | plot.draw(Canvas(bitmap)) 194 | views.setImageViewBitmap(R.id.plot_widget_img, bitmap) 195 | 196 | views.apply { 197 | setViewVisibility(R.id.plot_widget_refresh_button, View.VISIBLE) 198 | setViewVisibility(R.id.plot_widget_progress_bar, View.GONE) 199 | setViewVisibility(R.id.plot_widget_error_text, View.GONE) 200 | setViewVisibility(R.id.plot_widget_img, View.VISIBLE) 201 | } 202 | 203 | setOnClickPendingIntents(context, appWidgetId, views) 204 | appWidgetManager.updateAppWidget(appWidgetId, views) 205 | } 206 | 207 | private fun setOnClickPendingIntents(context: Context, appWidgetId: Int, views: RemoteViews) { 208 | views.setOnClickPendingIntent( 209 | R.id.plot_widget_configuration_button, 210 | WidgetHelper.createConfigurationPendingIntent(context, appWidgetId, PlotWidgetConfigurationActivity::class) 211 | ) 212 | 213 | views.setOnClickPendingIntent( 214 | R.id.plot_widget_refresh_button, 215 | WidgetHelper.createRefreshPendingIntent(context, appWidgetId, PlotWidget::class) 216 | ) 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /app/src/main/java/de/codefor/karlsruhe/opensense/widget/plot/PlotWidgetConfigurationActivity.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.widget.plot 2 | 3 | import android.appwidget.AppWidgetManager 4 | import de.codefor.karlsruhe.opensense.widget.base.BaseWidgetConfigurationActivity 5 | 6 | class PlotWidgetConfigurationActivity : BaseWidgetConfigurationActivity() { 7 | init { 8 | maxSensorItems = 1 9 | } 10 | 11 | override fun update(widgetId: Int) { 12 | PlotWidget.update(this, widgetId, AppWidgetManager.getInstance(this)) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/configuration_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-hdpi/configuration_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/configuration_list_adapter_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-hdpi/configuration_list_adapter_cloud.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/widget_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-hdpi/widget_configuration.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/widget_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-hdpi/widget_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/configuration_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-mdpi/configuration_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/configuration_list_adapter_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-mdpi/configuration_list_adapter_cloud.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/widget_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-mdpi/widget_configuration.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/widget_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-mdpi/widget_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/one_value_widget_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-nodpi/one_value_widget_example.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/plot_widget_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-nodpi/plot_widget_example.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/configuration_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xhdpi/configuration_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/configuration_list_adapter_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xhdpi/configuration_list_adapter_cloud.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/widget_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xhdpi/widget_configuration.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/widget_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xhdpi/widget_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/configuration_action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xxhdpi/configuration_action_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/configuration_list_adapter_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xxhdpi/configuration_list_adapter_cloud.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/widget_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xxhdpi/widget_configuration.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/widget_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/drawable-xxhdpi/widget_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/sensor_list_item_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_base_widget_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 27 | 28 | 36 | 37 | 43 | 44 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/one_value_widget.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 32 | 33 | 40 | 41 | 49 | 50 | 63 | 64 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/layout/plot_widget.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 18 | 19 | 33 | 34 | 41 | 42 | 50 | 51 | 60 | 61 | 76 | 77 | 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/res/layout/recycler_view_sensor_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 22 | 23 | 28 | 29 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/menu/base_widget_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %1$s %2$s 5 | Speichern 6 | Bitte mindestens einen Sensor auswählen 7 | Fehler beim Laden der senseBox-Daten 8 | Ungültige senseBox-Daten 9 | 10 | 11 | Ein Fehler ist aufgetreten 12 | Keine Daten verfügbar 13 | 14 | 15 | Bitte wähle maximal %1$d Sensor 16 | Bitte wähle maximal %1$d Sensoren 17 | 18 | 19 | Aktualisieren 20 | Konfiguration 21 | 22 | 23 | Plot der Sensorhistorie 24 | Zeit 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4EAF47 4 | #388E3C 5 | #5F7E9D 6 | #A7B8C9 7 | 8 | #30000000 9 | #FFFFFF 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 18dp 5 | 10sp 6 | 18sp 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Open Sense 3 | 4 | 5 | 6 | %1$s %2$s 7 | Save 8 | Please select at least one sensor 9 | Error fetching the senseBox data 10 | Invalid senseBox data 11 | 12 | 13 | An error occurred 14 | No data available 15 | 16 | 17 | Please select a maximum of %1$d sensor 18 | Please select a maximum of %1$d sensors 19 | 20 | 21 | Refresh 22 | Configuration 23 | 24 | 25 | Sensor history plot 26 | Time 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/one_value_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/xml/plot_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | apply(plugin = "com.github.ben-manes.versions") 4 | 5 | buildscript { 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | dependencies { 11 | classpath("com.android.tools.build:gradle:4.1.2") 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${de.codefor.karlsruhe.opensense.build.BuildConfig.kotlinVersion}") 13 | classpath("com.github.ben-manes:gradle-versions-plugin:0.36.0") 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | } 25 | } 26 | 27 | task("clean") { 28 | delete(rootProject.buildDir) 29 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | } -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/buildSrc/settings.gradle.kts -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/de/codefor/karlsruhe/opensense/build/Config.kt: -------------------------------------------------------------------------------- 1 | package de.codefor.karlsruhe.opensense.build 2 | 3 | object BuildConfig { 4 | val minSdkVersion = 19 5 | val targetSdkVersion = 30 6 | val compileSdkVersion = targetSdkVersion 7 | val versionCode = 6 8 | val versionName = "0.6.0" 9 | 10 | val kotlinVersion = "1.4.31" 11 | 12 | val supportLibVersion = "28.0.0" 13 | val supportLibMultiDexVersion = "1.0.3" 14 | val jodaVersion = "2.10.1.1" 15 | val rxAndroidVersion = "2.1.1" 16 | val rxJavaVersion = "2.2.21" 17 | val retrofitVersion = "2.6.4" 18 | val moshiVersion = "1.11.0" 19 | val mapboxVersion = "7.3.0" 20 | val androidPlotVersion = "1.5.7" 21 | val junitVersion = "4.13.2" 22 | val espressoVersion = "3.0.2" 23 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeforKarlsruhe/opensense/a464a33c732cd6bc43a7659cce4db482f001d8b1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":app") --------------------------------------------------------------------------------