diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43573ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Android Studio +.idea +.gradle +/*/.gitignore +/*/local.properties +local.properties +/*/out +/out +/*/*/build +/*/build +build +*.iml +*.iws +*.ipr +*~ +*.swp +.DS_* diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..e4552ce --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,58 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.chjaeggi.volkiweather" + minSdkVersion 24 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + lintOptions { + disable('AllowBackup', 'GoogleAppIndexingWarning', 'MissingApplicationIcon') + } + dataBinding { + enabled = true + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + implementation 'androidx.appcompat:appcompat:1.1.0-rc01' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha02' + + implementation 'org.koin:koin-android:1.0.2' + implementation 'org.koin:koin-android-viewmodel:1.0.2' + + implementation 'io.reactivex.rxjava2:rxjava:2.2.9' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' + + implementation 'com.jakewharton.timber:timber:4.7.1' + + implementation 'com.squareup.retrofit2:retrofit:2.6.0' + implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.6.0' + + testImplementation 'junit:junit:4.12' + + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + compileOnly 'com.google.android.things:androidthings:1.0' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/chjaeggi/volkiweather/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/chjaeggi/volkiweather/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..287ebd7 --- /dev/null +++ b/app/src/androidTest/java/com/chjaeggi/volkiweather/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.chjaeggi.volkiweather + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("com.chjaeggi.volkiweather", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9b08be6 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/MainActivity.kt b/app/src/main/java/com/chjaeggi/volkiweather/MainActivity.kt new file mode 100644 index 0000000..c5ad369 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/MainActivity.kt @@ -0,0 +1,48 @@ +package com.chjaeggi.volkiweather + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.NetworkInfo +import android.net.wifi.WifiManager +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import com.chjaeggi.volkiweather.databinding.ActivityMainBinding +import org.koin.android.viewmodel.ext.android.viewModel + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + private val viewModel by viewModel() + private val intentFilter = IntentFilter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + binding.model = viewModel + + binding.lifecycleOwner = this + + if (wifiAvailable()) { + viewModel.requestWeatherData() + } else { + intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION) + registerReceiver(broadCastReceiver, intentFilter) + } + } + + private fun wifiAvailable(): Boolean { + return (getSystemService(Context.WIFI_SERVICE) as WifiManager).connectionInfo.networkId != -1 + } + + private val broadCastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO) + if (info != null && info.isConnected) { + viewModel.requestWeatherData() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/MainViewModel.kt b/app/src/main/java/com/chjaeggi/volkiweather/MainViewModel.kt new file mode 100644 index 0000000..16b5823 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/MainViewModel.kt @@ -0,0 +1,37 @@ +package com.chjaeggi.volkiweather + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.chjaeggi.volkiweather.util.AppRxSchedulers +import com.chjaeggi.volkiweather.util.RxAwareViewModel +import com.chjaeggi.volkiweather.util.plusAssign +import com.chjaeggi.volkiweather.weather.WeatherDataSource +import io.reactivex.rxkotlin.subscribeBy +import timber.log.Timber +import java.text.DateFormat +import java.util.* + +class MainViewModel( + private val schedulers: AppRxSchedulers, + private val data: WeatherDataSource +) : + RxAwareViewModel() { + + private val _dateTime = MutableLiveData() + val dateTime = _dateTime + + fun requestWeatherData() { + disposables += data + .getWeatherData() + .subscribeOn(schedulers.io) + .observeOn(schedulers.main) + .subscribeBy( + onNext = { + dateTime.value = "${DateFormat.getDateTimeInstance().format(Date())}\n\n$it" + }, + onError = { + Timber.d(it) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/VolkiWeatherApp.kt b/app/src/main/java/com/chjaeggi/volkiweather/VolkiWeatherApp.kt new file mode 100644 index 0000000..bd1349d --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/VolkiWeatherApp.kt @@ -0,0 +1,17 @@ +package com.chjaeggi.volkiweather +import android.app.Application +import com.chjaeggi.volkiweather.di.appModule +import org.koin.android.ext.android.startKoin +import timber.log.Timber + +class VolkiWeatherApp : Application() { + + override fun onCreate() { + super.onCreate() + startKoin(this, listOf(appModule)) + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/di/modules.kt b/app/src/main/java/com/chjaeggi/volkiweather/di/modules.kt new file mode 100644 index 0000000..3fd7345 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/di/modules.kt @@ -0,0 +1,20 @@ +package com.chjaeggi.volkiweather.di +import com.chjaeggi.volkiweather.MainViewModel +import com.chjaeggi.volkiweather.weather.WeatherCloudApi +import com.chjaeggi.volkiweather.weather.WeatherDataSource +import com.chjaeggi.volkiweather.weather.WeatherRepository +import com.chjaeggi.volkiweather.util.AppRxSchedulers +import org.koin.android.viewmodel.ext.koin.viewModel +import org.koin.dsl.module.module + +val appModule = module(override = true) { + + single { AppRxSchedulers() } + single { + WeatherRepository( + WeatherCloudApi.newInstance() + ) + } + + viewModel { MainViewModel(get(), get())} +} diff --git a/app/src/main/java/com/chjaeggi/volkiweather/domain/WeatherData.kt b/app/src/main/java/com/chjaeggi/volkiweather/domain/WeatherData.kt new file mode 100644 index 0000000..fa8bcff --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/domain/WeatherData.kt @@ -0,0 +1,19 @@ +package com.chjaeggi.volkiweather.domain + +/** + * Immutable model class for weather data + */ +data class WeatherData( + val id: Int, + val description: String, + val wind: Float, + val temperatureHigh: Float, + val temperatureLow: Float, + val humidity: Float, + val icon: String, + val sunrise: Int, + val sunset: Int, + val idTomorrow: Int, + val temperatureTomorrow: Float, + val humidityTomorrow: Float +) \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/util/AppRxSchedulers.kt b/app/src/main/java/com/chjaeggi/volkiweather/util/AppRxSchedulers.kt new file mode 100644 index 0000000..b7e98c2 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/util/AppRxSchedulers.kt @@ -0,0 +1,11 @@ +package com.chjaeggi.volkiweather.util + +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +data class AppRxSchedulers( + val data: Scheduler = Schedulers.single(), + val io: Scheduler = Schedulers.io(), + val main: Scheduler = AndroidSchedulers.mainThread() +) \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/util/RxAwareViewModel.kt b/app/src/main/java/com/chjaeggi/volkiweather/util/RxAwareViewModel.kt new file mode 100644 index 0000000..509b5bf --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/util/RxAwareViewModel.kt @@ -0,0 +1,18 @@ +package com.chjaeggi.volkiweather.util + +import androidx.lifecycle.ViewModel +import io.reactivex.disposables.CompositeDisposable + +/** + * Simple ViewModel which exposes a CompositeDisposable which is automatically cleared when + * the ViewModel is cleared. + */ +open class RxAwareViewModel : ViewModel() { + + val disposables = CompositeDisposable() + + override fun onCleared() { + super.onCleared() + disposables.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/util/RxExtensions.kt b/app/src/main/java/com/chjaeggi/volkiweather/util/RxExtensions.kt new file mode 100644 index 0000000..284d9e9 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/util/RxExtensions.kt @@ -0,0 +1,8 @@ +package com.chjaeggi.volkiweather.util + +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable + +operator fun CompositeDisposable.plusAssign(disposable: Disposable) { + add(disposable) +} diff --git a/app/src/main/java/com/chjaeggi/volkiweather/util/SingleLiveEvent.kt b/app/src/main/java/com/chjaeggi/volkiweather/util/SingleLiveEvent.kt new file mode 100644 index 0000000..64a6843 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/util/SingleLiveEvent.kt @@ -0,0 +1,40 @@ +package com.chjaeggi.volkiweather.util + +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + + +class SingleLiveEvent : MutableLiveData() { + + companion object { + private val TAG = "SingleLiveEvent" + } + + private val mPending = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + check(!hasActiveObservers()) { "Multiple observers registered!" } + + // Observe the internal MutableLiveData + super.observe(owner, Observer { t -> + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + }) + } + + @MainThread + override fun setValue(t: T?) { + mPending.set(true) + super.setValue(t) + } + + @MainThread + fun call() { + value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherCloudApi.kt b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherCloudApi.kt new file mode 100644 index 0000000..030062f --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherCloudApi.kt @@ -0,0 +1,34 @@ +package com.chjaeggi.volkiweather.weather + + +import io.reactivex.Observable +import io.reactivex.Single +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET + +interface WeatherCloudApi { + + // https://api.openweathermap.org/data/2.5/weather?q=volketswil&appid=886705b4c1182eb1c69f28eb8c520e20 + + companion object { + private const val BASE_URL = "https://api.openweathermap.org/" + private const val DATA_PATH = "data/2.5/weather" + private const val APP_KEY_PARAM = "appid=886705b4c1182eb1c69f28eb8c520e20" + private const val WEATHER_QUERY = "q=volketswil" + private const val UNITS = "units=metric " + + fun newInstance(): WeatherCloudApi { + return Retrofit.Builder() + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl(BASE_URL) + .build() + .create(WeatherCloudApi::class.java) + } + } + + @GET("$DATA_PATH?$WEATHER_QUERY&$APP_KEY_PARAM&$UNITS") + fun currentWeather(): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherDataSource.kt b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherDataSource.kt new file mode 100644 index 0000000..af74739 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherDataSource.kt @@ -0,0 +1,8 @@ +package com.chjaeggi.volkiweather.weather + +import io.reactivex.Observable + + +interface WeatherDataSource { + fun getWeatherData() : Observable +} \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherQueries.kt b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherQueries.kt new file mode 100644 index 0000000..4f46eac --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherQueries.kt @@ -0,0 +1,63 @@ +package com.chjaeggi.volkiweather.weather + +import androidx.annotation.Keep + +@Keep +data class WeatherProperties( + val coord: Coord, + val weather: List, + val base: String, + val main: Main, + val wind: Wind, + val clouds: Clouds, + val dt: Int, + val sys: Sys, + val timezone: Int, + val id: Int, + val name: String, + val cod: Int +) + +@Keep +data class Clouds( + val all: Int +) + +@Keep +data class Coord( + val lon: Double, + val lat: Double +) + +@Keep +data class Main( + val temp: Double, + val pressure: Double, + val humidity: Double, + val temp_min: Double, + val temp_max: Double +) + +@Keep +data class Sys( + val type: Int, + val id: Int, + val message: Double, + val country: String, + val sunrise: Int, + val sunset: Int +) + +@Keep +data class Weather( + val id: Int, + val main: String, + val description: String, + val icon: String +) + +@Keep +data class Wind( + val speed: Double, + val deg: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherRepository.kt b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherRepository.kt new file mode 100644 index 0000000..28f0dd9 --- /dev/null +++ b/app/src/main/java/com/chjaeggi/volkiweather/weather/WeatherRepository.kt @@ -0,0 +1,21 @@ +package com.chjaeggi.volkiweather.weather + +import io.reactivex.Observable +import java.util.concurrent.TimeUnit + +class WeatherRepository( + private val weatherCloudApi: WeatherCloudApi +) : WeatherDataSource { + + override fun getWeatherData(): Observable { + return Observable.interval(1, TimeUnit.SECONDS) + .flatMapSingle { + weatherCloudApi + .currentWeather() + .map { + it.weather[0].main + "\n" + it.main.temp + "°C / " + it.main.humidity + "%" + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..6737d79 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..69b2233 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..541c40c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + VolkiWeather + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..b6b9204 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,6 @@ + + + +