├── .gitignore
├── .travis.yml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── lib
│ ├── espresso-1.1.jar
│ ├── testrunner-1.1.jar
│ └── testrunner-runtime-1.1.jar
├── my-release-key.keystore
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── io
│ │ │ └── dp
│ │ │ └── weather
│ │ │ └── app
│ │ │ ├── AppComponent.kt
│ │ │ ├── AppModule.kt
│ │ │ ├── BusModule.kt
│ │ │ ├── BusSubcomponent.kt
│ │ │ ├── Const.kt
│ │ │ ├── SchedulersManager.kt
│ │ │ ├── WeatherApp.kt
│ │ │ ├── activity
│ │ │ ├── ActivityComponent.kt
│ │ │ ├── ActivityModule.kt
│ │ │ ├── BaseActivity.kt
│ │ │ ├── BaseActivityComponent.kt
│ │ │ ├── HasComponent.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ └── debug
│ │ │ │ ├── DebugActivity.kt
│ │ │ │ ├── DebugBusModule.kt
│ │ │ │ └── DebugBusSubcomponent.kt
│ │ │ ├── adapter
│ │ │ ├── CursorRecyclerViewAdapter.kt
│ │ │ ├── OrmliteCursorRecyclerViewAdapter.kt
│ │ │ ├── PlacesAdapter.kt
│ │ │ └── PlacesAutoCompleteAdapter.kt
│ │ │ ├── annotation
│ │ │ ├── CachePrefs.kt
│ │ │ ├── ConfigPrefs.kt
│ │ │ ├── IOSched.kt
│ │ │ ├── PerActivity.kt
│ │ │ ├── PerFragment.kt
│ │ │ └── UISched.kt
│ │ │ ├── db
│ │ │ ├── DatabaseHelper.kt
│ │ │ ├── NoIdCursorWrapper.kt
│ │ │ ├── OrmliteCursorAdapter.kt
│ │ │ ├── OrmliteCursorLoader.kt
│ │ │ ├── OrmliteListLoader.kt
│ │ │ ├── Queries.kt
│ │ │ ├── Tables.kt
│ │ │ └── table
│ │ │ │ └── Place.kt
│ │ │ ├── event
│ │ │ ├── AddPlaceEvent.kt
│ │ │ ├── DeletePlaceEvent.kt
│ │ │ └── UpdateListEvent.kt
│ │ │ ├── fragment
│ │ │ ├── BaseFragment.kt
│ │ │ └── WeatherFragment.kt
│ │ │ ├── net
│ │ │ ├── PlacesApi.kt
│ │ │ ├── WeatherApi.kt
│ │ │ └── dto
│ │ │ │ ├── CurrentCondition.kt
│ │ │ │ ├── Data.kt
│ │ │ │ ├── Forecast.kt
│ │ │ │ ├── Request.kt
│ │ │ │ ├── Weather.kt
│ │ │ │ ├── WeatherDesc.kt
│ │ │ │ └── WeatherIconUrl.kt
│ │ │ ├── utils
│ │ │ ├── AsyncBus.kt
│ │ │ ├── MetricsController.kt
│ │ │ ├── MyPreferences.kt
│ │ │ ├── Observables.kt
│ │ │ └── WhiteBorderCircleTransformation.kt
│ │ │ └── widget
│ │ │ ├── ArrayAdapterSearchView.kt
│ │ │ └── WeatherFor5DaysView.kt
│ └── res
│ │ ├── drawable-hdpi
│ │ ├── ic_launcher.png
│ │ └── ico_menu_small.png
│ │ ├── drawable-mdpi
│ │ ├── ic_launcher.png
│ │ └── ico_menu_small.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ico_menu_small.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ico_menu_small.png
│ │ ├── drawable-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── drawable
│ │ └── card.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_test.xml
│ │ ├── fragment_main.xml
│ │ ├── fragment_weather.xml
│ │ ├── item_city_weather.xml
│ │ ├── item_place_content.xml
│ │ ├── item_search_list.xml
│ │ └── view_weather_for_week.xml
│ │ ├── menu
│ │ ├── item_place.xml
│ │ ├── main.xml
│ │ └── menu_debug.xml
│ │ ├── raw
│ │ └── ormlite_config.txt
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ ├── strings_activity_settings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── pref_general.xml
│ └── test
│ ├── TestAndroidManifest.xml
│ └── java
│ └── io
│ └── dp
│ └── weather
│ └── app
│ ├── MockAppComponent.kt
│ ├── MockAppModule.kt
│ ├── TestApp.kt
│ ├── TestUtils.kt
│ ├── activity
│ └── MockActivity.kt
│ └── fragment
│ └── WeatherFragmentTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | .DS_Store
5 | /build
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 |
3 | jdk:
4 | - oraclejdk8
5 |
6 | android:
7 | components:
8 | - build-tools-23.0.1
9 | - android-23
10 | - extra-android-m2repository
11 |
12 | env:
13 | global:
14 | - ADB_INSTALL_TIMEOUT=8
15 |
16 | script:
17 | - ./gradlew clean build -x test
18 |
19 | branches:
20 | except:
21 | - gh-pages
22 |
23 | notifications:
24 | email: false
25 |
26 | sudo: false
27 |
28 | cache:
29 | directories:
30 | - $HOME/.gradle
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### What is this repository for? ###
2 |
3 | * This is weather android app which uses worldweatheronline.com as data provider
4 | * v1.0.0
5 |
6 | ### How do I get set up? ###
7 |
8 | * ./gradlew clean build
9 | * and don't forget to setup your sdk.dir (via local.properties or direct specification)
10 |
11 | ### Design concepts ###
12 |
13 | The whole design of my app is based on Dependency Injections and EventBus concepts.
14 | I wanted to split all classes out from each other onto independent components.
15 |
16 | Interaction with weather web-service is based on fetching forecast by latitude and longitude.
17 | I use shared preferences as a cache for forecast responses and update this cache in two cases:
18 |
19 | 1. user refreshed the list manually by pull-to-refresh action on the list of data
20 | 2. time of response is expired (it's 24 hours) and in the next scrolling action - application will try to fetch new data for this location
21 |
22 | ### Dependencies ###
23 | I used a lot of 3rd libraries and the main reason why I did it, just to speed up development process.
24 |
25 | * rxjava - don't like to hassle with AsyncTask
26 | * retrofit - quick bootstrap of any http webservice stub
27 | * ormlite - quick bootstrap for database
28 | * otto - eventbus
29 | * dagger2 - DI
30 | * com.etsy.android.grid - Staggered GridView for weather dashboard
31 | * timber - very customizable logger for android
32 |
33 | ### Which design patterns I used ###
34 | DI, EventBus, Observer, Builder
35 |
36 | Applications consists of two screens:
37 |
38 | 1. main screen with weather dashboard
39 | 2. settings activity where the user can specify Temperature Units (Celcius or Fahrenheit)
40 | 3. and Debug activity to show how to override eventbus
41 |
42 | Small clarification: To refresh weathers list - just pull down (I used SwipeRefreshLayout from support library)
43 |
44 | ### Unit-testing ###
45 | To run & debug unit-tests in IDE you need to use Android Studio 1.4
46 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.0.0-beta-2423'
3 | repositories {
4 | jcenter()
5 |
6 | maven {
7 | url 'http://oss.sonatype.org/content/repositories/snapshots'
8 | }
9 | }
10 |
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:1.3.1'
13 |
14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
15 | classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
16 | }
17 | }
18 |
19 | apply plugin: 'com.android.application'
20 | apply plugin: 'kotlin-android'
21 |
22 | android {
23 | compileSdkVersion 23
24 | buildToolsVersion "23.0.1"
25 |
26 | defaultConfig {
27 | applicationId "io.dp.weather.app"
28 | minSdkVersion 14
29 | targetSdkVersion 23
30 | versionCode 1
31 | versionName "1.0"
32 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
33 |
34 | buildConfigField "int", "DATABASE_VERSION", "1";
35 | buildConfigField "String", "FORECAST_API_URL",
36 | "\"https://api.worldweatheronline.com/free/v1/\""
37 | buildConfigField "String", "FORECAST_API_KEY",
38 | "\"499cb12ae4fc7740adee2fa7b2007982f75338c0\""
39 | buildConfigField "String", "PLACES_API_URL", "\"https://maps.googleapis.com/maps/api/\""
40 | buildConfigField "String", "PLACES_API_KEY", "\"AIzaSyA0o8WdYc40tWfRrKimdAWHvaFwW8h-N8w\""
41 | }
42 |
43 | signingConfigs {
44 | release {
45 | storeFile file("my-release-key.keystore")
46 | storePassword "12345678"
47 | keyAlias "alias_name"
48 | keyPassword "12345678"
49 | }
50 | }
51 |
52 | buildTypes {
53 | release {
54 | signingConfig signingConfigs.release
55 | }
56 | }
57 |
58 | packagingOptions {
59 | exclude 'META-INF/services/javax.annotation.processing.Processor'
60 | }
61 |
62 | lintOptions {
63 | abortOnError false
64 | }
65 |
66 | testOptions {
67 | unitTests.returnDefaultValues = true
68 | }
69 |
70 | sourceSets {
71 | main.java.srcDirs += 'src/main/kotlin'
72 | test.java.srcDirs += 'src/test/java'
73 | test.java.srcDirs += 'src/test/kotlin'
74 | }
75 | }
76 |
77 | dependencies {
78 | compile fileTree(dir: 'libs', include: ['*.jar'])
79 |
80 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
81 |
82 | compile 'com.android.support:appcompat-v7:23.1.0'
83 | compile 'com.android.support:recyclerview-v7:23.1.0'
84 |
85 | compile 'com.squareup.retrofit:retrofit:1.9.0'
86 | compile 'com.google.dagger:dagger:2.0.2'
87 | kapt 'com.google.dagger:dagger-compiler:2.0.2'
88 | provided 'org.glassfish:javax.annotation:10.0-b28'
89 |
90 | compile 'com.squareup.picasso:picasso:2.5.2'
91 |
92 | compile 'com.squareup:otto:1.3.8'
93 | compile 'com.jakewharton.timber:timber:2.4.1'
94 | compile 'com.j256.ormlite:ormlite-android:4.48'
95 |
96 | compile 'io.reactivex:rxkotlin:0.24.100'
97 | compile 'io.reactivex:rxandroid:1.0.1'
98 | compile 'com.trello:rxlifecycle:0.3.0'
99 | compile 'com.trello:rxlifecycle-components:0.3.0'
100 |
101 | compile 'org.jetbrains.anko:anko-sdk15:0.7.3'
102 | compile 'org.jetbrains.anko:anko-support-v4:0.7.3'
103 |
104 | kaptTest 'com.google.dagger:dagger-compiler:2.0.2'
105 | testCompile 'junit:junit:4.11'
106 | testCompile 'org.glassfish:javax.annotation:10.0-b28'
107 | testCompile 'org.hamcrest:hamcrest-integration:1.1'
108 | testCompile 'org.hamcrest:hamcrest-core:1.1'
109 | testCompile 'org.hamcrest:hamcrest-library:1.1'
110 | testCompile 'com.squareup.retrofit:retrofit-mock:1.9.0'
111 | testCompile 'org.mockito:mockito-all:1.9.5'
112 | testCompile 'org.robolectric:robolectric:3.0'
113 | testCompile 'org.robolectric:shadows-support-v4:3.0'
114 | }
115 |
116 | kapt {
117 | generateStubs = true
118 | }
119 |
--------------------------------------------------------------------------------
/app/lib/espresso-1.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/lib/espresso-1.1.jar
--------------------------------------------------------------------------------
/app/lib/testrunner-1.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/lib/testrunner-1.1.jar
--------------------------------------------------------------------------------
/app/lib/testrunner-runtime-1.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/lib/testrunner-runtime-1.1.jar
--------------------------------------------------------------------------------
/app/my-release-key.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/my-release-key.keystore
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import android.location.Geocoder
4 | import com.google.gson.Gson
5 | import dagger.Component
6 | import io.dp.weather.app.annotation.IOSched
7 | import io.dp.weather.app.annotation.UISched
8 | import io.dp.weather.app.net.PlacesApi
9 | import io.dp.weather.app.net.WeatherApi
10 | import rx.Scheduler
11 | import javax.inject.Singleton
12 |
13 | /**
14 | * Created by deepol on 19/08/15.
15 | */
16 | @Singleton
17 | @Component(modules = arrayOf(AppModule::class))
18 | public interface AppComponent {
19 |
20 | public fun provideGson(): Gson
21 |
22 | public fun proviceGeocoder(): Geocoder
23 |
24 | public fun provideForecastApi(): WeatherApi
25 |
26 | public fun providePlacesApi(): PlacesApi
27 |
28 | @IOSched public fun provideIoScheduler(): Scheduler
29 |
30 | @UISched public fun provideUiScheduler(): Scheduler
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/AppModule.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import android.app.Application
4 | import android.location.Geocoder
5 | import com.google.gson.Gson
6 | import com.google.gson.GsonBuilder
7 | import dagger.Module
8 | import dagger.Provides
9 | import io.dp.weather.app.annotation.IOSched
10 | import io.dp.weather.app.annotation.UISched
11 | import io.dp.weather.app.net.PlacesApi
12 | import io.dp.weather.app.net.WeatherApi
13 | import retrofit.RestAdapter
14 | import rx.Scheduler
15 | import rx.android.schedulers.AndroidSchedulers
16 | import rx.schedulers.Schedulers
17 | import timber.log.Timber
18 | import javax.inject.Singleton
19 |
20 | /**
21 | * Created by dp on 07/10/14.
22 | */
23 | @Module public class AppModule(application: WeatherApp) {
24 |
25 | protected val application: Application
26 |
27 | init {
28 | this.application = application
29 |
30 | Timber.plant(Timber.DebugTree())
31 | }
32 |
33 | @Provides @Singleton public fun proviceGeocoder(): Geocoder {
34 | return Geocoder(application)
35 | }
36 |
37 | @Provides @Singleton public fun provideApplication(): Application {
38 | return application
39 | }
40 |
41 | @Provides @Singleton public fun provideForecastApi(): WeatherApi {
42 | val b = RestAdapter.Builder()
43 |
44 | if (BuildConfig.DEBUG) {
45 | b.setLogLevel(RestAdapter.LogLevel.FULL)
46 | }
47 |
48 | b.setRequestInterceptor({ request ->
49 | request.addQueryParam("key", BuildConfig.FORECAST_API_KEY)
50 | request.addQueryParam("format", "json")
51 | })
52 |
53 | val restAdapter = b.setEndpoint(BuildConfig.FORECAST_API_URL).build()
54 | return restAdapter.create(WeatherApi::class.java)
55 | }
56 |
57 | @Provides @Singleton public fun providePlacesApi(): PlacesApi {
58 | val b = RestAdapter.Builder()
59 |
60 | if (BuildConfig.DEBUG) {
61 | b.setLogLevel(RestAdapter.LogLevel.FULL)
62 | }
63 |
64 | b.setRequestInterceptor({ request ->
65 | request.addQueryParam("key", BuildConfig.PLACES_API_KEY)
66 | request.addQueryParam("sensor", "false")
67 | })
68 |
69 | val restAdapter = b.setEndpoint(BuildConfig.PLACES_API_URL).build()
70 | return restAdapter.create(PlacesApi::class.java)
71 | }
72 |
73 | @Provides @Singleton public fun provideGson(): Gson {
74 | return GsonBuilder().create()
75 | }
76 |
77 | @Provides @Singleton @IOSched public fun provideIoScheduler(): Scheduler {
78 | return Schedulers.io()
79 | }
80 |
81 | @Provides @Singleton @UISched public fun provideUiScheduler(): Scheduler {
82 | return AndroidSchedulers.mainThread()
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/BusModule.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import com.squareup.otto.Bus
4 | import com.squareup.otto.ThreadEnforcer
5 | import dagger.Module
6 | import dagger.Provides
7 | import io.dp.weather.app.annotation.PerActivity
8 | import io.dp.weather.app.utils.AsyncBus
9 |
10 | /**
11 | * Created by deepol on 10/09/15.
12 | */
13 | @Module
14 | public class BusModule {
15 |
16 | @Provides
17 | @PerActivity
18 | public fun provideBus(): Bus {
19 | return AsyncBus(ThreadEnforcer.ANY, "default")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/BusSubcomponent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import dagger.Subcomponent
4 | import io.dp.weather.app.activity.BaseActivityComponent
5 | import io.dp.weather.app.annotation.PerActivity
6 | import io.dp.weather.app.fragment.WeatherFragment
7 |
8 | /**
9 | * Created by deepol on 16/09/15.
10 | */
11 | @PerActivity
12 | @Subcomponent(modules = arrayOf(BusModule::class))
13 | public interface BusSubcomponent : BaseActivityComponent {
14 |
15 | public fun inject(fragment: WeatherFragment)
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/Const.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | /**
4 | * Created by dp on 08/10/14.
5 | */
6 | public object Const {
7 |
8 | public val USE_CELCIUS: String = "temperature_switch"
9 | public val USE_KMPH: String = "use_kmph"
10 | public val USE_MMHG: String = "use_mmhg"
11 |
12 | public val FORECAST_FOR_DAYS: Int = 5
13 | public var CONVERT_MMHG: Float = 0.75f
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/SchedulersManager.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import android.support.v4.app.FragmentActivity
4 | import com.trello.rxlifecycle.components.ActivityLifecycleProvider
5 | import io.dp.weather.app.annotation.IOSched
6 | import io.dp.weather.app.annotation.PerActivity
7 | import io.dp.weather.app.annotation.UISched
8 | import rx.Observable
9 | import rx.Scheduler
10 | import javax.inject.Inject
11 |
12 | /**
13 | * Created by deepol on 24/08/15.
14 | */
15 | @PerActivity
16 | public class SchedulersManager
17 | @Inject
18 | constructor(private val activity: FragmentActivity,
19 | @IOSched private val ioScheduler: Scheduler,
20 | @UISched private val uiScheduler: Scheduler) {
21 |
22 | public fun applySchedulers(): Observable.Transformer
23 | = Observable.Transformer { observable ->
24 | (observable as Observable)
25 | .subscribeOn(ioScheduler)
26 | .observeOn(uiScheduler)
27 | .compose((activity as ActivityLifecycleProvider).bindToLifecycle())
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/WeatherApp.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import android.app.Application
4 |
5 | /**
6 | * Created by dp on 07/10/14.
7 | */
8 | public open class WeatherApp : Application() {
9 |
10 | public var component: AppComponent? = null
11 | private set
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 |
16 | this.component = createComponent()
17 | }
18 |
19 | public open fun createComponent(): AppComponent {
20 | return DaggerAppComponent.builder().appModule(AppModule(this)).build()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/ActivityComponent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | import dagger.Component
4 | import io.dp.weather.app.AppComponent
5 | import io.dp.weather.app.BusModule
6 | import io.dp.weather.app.BusSubcomponent
7 | import io.dp.weather.app.activity.debug.DebugBusModule
8 | import io.dp.weather.app.activity.debug.DebugBusSubcomponent
9 | import io.dp.weather.app.annotation.PerActivity
10 |
11 | @PerActivity
12 | @Component(modules = arrayOf(ActivityModule::class), dependencies = arrayOf(AppComponent::class))
13 | interface ActivityComponent : BaseActivityComponent {
14 |
15 | fun plusSubComponent(module: BusModule): BusSubcomponent
16 |
17 | fun plusSubComponent(module: DebugBusModule): DebugBusSubcomponent
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/ActivityModule.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.preference.PreferenceManager
6 | import android.support.v4.app.FragmentActivity
7 | import com.j256.ormlite.android.apptools.OpenHelperManager
8 | import dagger.Module
9 | import dagger.Provides
10 | import io.dp.weather.app.annotation.CachePrefs
11 | import io.dp.weather.app.annotation.ConfigPrefs
12 | import io.dp.weather.app.annotation.PerActivity
13 | import io.dp.weather.app.db.DatabaseHelper
14 |
15 | @Module
16 | class ActivityModule(private val activity: FragmentActivity) {
17 |
18 | @Provides
19 | @PerActivity
20 | fun provideActivity(): FragmentActivity {
21 | return activity
22 | }
23 |
24 | @Provides
25 | @PerActivity
26 | fun provideDatabaseHelper(): DatabaseHelper {
27 | return OpenHelperManager.getHelper(activity, DatabaseHelper::class.java)
28 | }
29 |
30 | @Provides
31 | @ConfigPrefs
32 | @PerActivity
33 | fun provideConfigPrefs(): SharedPreferences {
34 | return PreferenceManager.getDefaultSharedPreferences(activity)
35 | }
36 |
37 | @Provides
38 | @CachePrefs
39 | @PerActivity
40 | fun provideCachePrefs(): SharedPreferences {
41 | return activity.getSharedPreferences("cachePrefs", Context.MODE_PRIVATE)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | import android.os.Bundle
4 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity
5 | import io.dp.weather.app.BusModule
6 | import io.dp.weather.app.WeatherApp
7 |
8 | open class BaseActivity : RxAppCompatActivity(), HasComponent {
9 | private lateinit var component: BaseActivityComponent
10 |
11 | override protected fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | this.component = createComponent()
14 | }
15 |
16 | override fun getComponent(): BaseActivityComponent {
17 | return component
18 | }
19 |
20 | override fun createComponent(): BaseActivityComponent {
21 | val app = getApplication() as WeatherApp
22 | val component = DaggerActivityComponent.builder().appComponent(app.component).activityModule(ActivityModule(this)).build()
23 |
24 | return component.plusSubComponent(BusModule())
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/BaseActivityComponent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | interface BaseActivityComponent
4 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/HasComponent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | interface HasComponent {
4 |
5 | fun createComponent(): T
6 |
7 | fun getComponent(): T
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | import android.os.Bundle
4 | import io.dp.weather.app.R
5 |
6 | class MainActivity : BaseActivity() {
7 |
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | setContentView(R.layout.activity_main)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | import android.annotation.TargetApi
4 | import android.content.Context
5 | import android.content.res.Configuration
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.preference.PreferenceActivity
9 | import android.preference.PreferenceFragment
10 |
11 | import io.dp.weather.app.R
12 |
13 | /**
14 | * A [PreferenceActivity] that presents a set of application settings. On handset devices,
15 | * settings are presented as a single list. On tablets, settings are split by category, with
16 | * category headers shown to the left of the list of settings.
17 | *
18 | * See [ Android Design: Settings](http://developer.android.com/design/patterns/settings.html)
19 | * for design guidelines and the [Settings
20 | * API Guide](http://developer.android.com/guide/topics/ui/settings.html) for more information on developing a Settings UI.
21 | */
22 | class SettingsActivity : PreferenceActivity() {
23 |
24 | override fun onPostCreate(savedInstanceState: Bundle?) {
25 | super.onPostCreate(savedInstanceState)
26 |
27 | setupSimplePreferencesScreen()
28 | }
29 |
30 | /**
31 | * Shows the simplified settings UI if the device configuration if the device configuration
32 | * dictates that a simplified, single-pane UI should be shown.
33 | */
34 | @SuppressWarnings("deprecation")
35 | private fun setupSimplePreferencesScreen() {
36 | if (!isSimplePreferences(this)) {
37 | return
38 | }
39 |
40 | // In the simplified UI, fragments are not used at all and we instead
41 | // use the older PreferenceActivity APIs.
42 |
43 | // Add 'general' preferences.
44 | addPreferencesFromResource(R.xml.pref_general)
45 | }
46 |
47 | /**
48 | * {@inheritDoc}
49 | */
50 | override fun onIsMultiPane(): Boolean {
51 | return isXLargeTablet(this) && !isSimplePreferences(this)
52 | }
53 |
54 | /**
55 | * This fragment shows general preferences only. It is used when the activity is showing a
56 | * two-pane settings UI.
57 | */
58 | @TargetApi(Build.VERSION_CODES.HONEYCOMB)
59 | public class GeneralPreferenceFragment : PreferenceFragment() {
60 |
61 | override fun onCreate(savedInstanceState: Bundle?) {
62 | super.onCreate(savedInstanceState)
63 | addPreferencesFromResource(R.xml.pref_general)
64 | }
65 | }
66 |
67 | companion object {
68 |
69 | /**
70 | * Determines whether to always show the simplified settings UI, where settings are presented in a
71 | * single list. When false, settings are shown as a master/detail two-pane view on tablets. When
72 | * true, a single pane is shown on tablets.
73 | */
74 | private val ALWAYS_SIMPLE_PREFS = false
75 |
76 | /**
77 | * Helper method to determine if the device has an extra-large screen. For example, 10" tablets
78 | * are extra-large.
79 | */
80 | private fun isXLargeTablet(context: Context): Boolean {
81 | return (context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE
82 | }
83 |
84 | /**
85 | * Determines whether the simplified settings UI should be shown. This is true if this is forced
86 | * via [.ALWAYS_SIMPLE_PREFS], or the device doesn't have newer APIs like [ ], or the device doesn't have an extra-large screen. In these cases, a
87 | * single-pane "simplified" settings UI should be shown.
88 | */
89 | private fun isSimplePreferences(context: Context): Boolean {
90 | return ALWAYS_SIMPLE_PREFS || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || !isXLargeTablet(context)
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/debug/DebugActivity.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity.debug
2 |
3 | import android.os.Bundle
4 | import com.squareup.otto.Bus
5 | import io.dp.weather.app.WeatherApp
6 | import io.dp.weather.app.activity.ActivityModule
7 | import io.dp.weather.app.activity.BaseActivity
8 | import io.dp.weather.app.activity.BaseActivityComponent
9 | import io.dp.weather.app.activity.DaggerActivityComponent
10 | import org.jetbrains.anko.relativeLayout
11 | import org.jetbrains.anko.textView
12 | import javax.inject.Inject
13 |
14 | public class DebugActivity : BaseActivity() {
15 | @Inject lateinit var bus: Bus
16 |
17 | override fun createComponent(): BaseActivityComponent {
18 | val app = getApplication() as WeatherApp
19 | val activityComponent = DaggerActivityComponent.builder()
20 | .appComponent(app.component)
21 | .activityModule(ActivityModule(this))
22 | .build()
23 |
24 | return activityComponent.plusSubComponent(DebugBusModule())
25 | }
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 |
30 | relativeLayout {
31 | textView("Hello, world!") {
32 | }
33 | }
34 |
35 | (getComponent() as DebugBusSubcomponent).inject(this)
36 | }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/debug/DebugBusModule.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity.debug
2 |
3 | import com.squareup.otto.Bus
4 | import com.squareup.otto.ThreadEnforcer
5 | import dagger.Module
6 | import dagger.Provides
7 | import io.dp.weather.app.annotation.PerActivity
8 | import io.dp.weather.app.utils.AsyncBus
9 |
10 | /**
11 | * Created by deepol on 10/09/15.
12 | */
13 | @Module
14 | public class DebugBusModule {
15 |
16 | @Provides
17 | @PerActivity
18 | // in Kotlin we need to have different method name for overriden dependency
19 | // the reason - dagger2 + kapt won't generate BusModule_Factory code
20 | public fun provideDebugBus(): Bus {
21 | return AsyncBus(ThreadEnforcer.ANY, "debug")
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/activity/debug/DebugBusSubcomponent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity.debug
2 |
3 | import dagger.Subcomponent
4 | import io.dp.weather.app.BusSubcomponent
5 | import io.dp.weather.app.annotation.PerActivity
6 |
7 | /**
8 | * Created by deepol on 16/09/15.
9 | */
10 | @PerActivity
11 | @Subcomponent(modules = arrayOf(DebugBusModule::class))
12 | public interface DebugBusSubcomponent : BusSubcomponent {
13 |
14 | public fun inject(debugActivity: DebugActivity)
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/adapter/CursorRecyclerViewAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.adapter
2 |
3 |
4 | import android.database.Cursor
5 | import android.database.DataSetObserver
6 | import android.support.v7.widget.RecyclerView
7 |
8 | abstract class CursorRecyclerViewAdapter(cursor: Cursor?) : RecyclerView.Adapter() {
9 |
10 | private var cursor: Cursor?
11 | private var dataValid: Boolean
12 | private var rowIdColumn: Int
13 | private val dataSetObserver: DataSetObserver?
14 |
15 | init {
16 | this.cursor = cursor
17 | dataValid = cursor != null
18 | rowIdColumn = if (dataValid) this.cursor!!.getColumnIndex("_id") else -1
19 | dataSetObserver = NotifyingDataSetObserver()
20 | this.cursor?.registerDataSetObserver(dataSetObserver)
21 | }
22 |
23 | override fun getItemCount(): Int {
24 | return cursor?.count ?: 0
25 | }
26 |
27 | override fun getItemId(position: Int): Long {
28 | if (dataValid && cursor?.moveToPosition(position) ?: false) {
29 | return cursor?.getLong(rowIdColumn) ?: 0
30 | }
31 | return 0
32 | }
33 |
34 | fun getItem(position: Int): Any? {
35 | when {
36 | dataValid -> {
37 | cursor?.moveToPosition(position)
38 | return cursor
39 | }
40 | else -> return null
41 | }
42 | }
43 |
44 | override fun setHasStableIds(hasStableIds: Boolean) {
45 | super.setHasStableIds(true)
46 | }
47 |
48 | abstract fun onBindViewHolder(viewHolder: VH, cursor: Cursor?)
49 |
50 | override fun onBindViewHolder(viewHolder: VH, position: Int) {
51 | if (!dataValid) {
52 | throw IllegalStateException("this should only be called when the cursor is valid")
53 | }
54 | if (!cursor!!.moveToPosition(position)) {
55 | throw IllegalStateException("couldn't move cursor to position " + position)
56 | }
57 | onBindViewHolder(viewHolder, cursor)
58 | }
59 |
60 | /**
61 | * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
62 | * closed.
63 | */
64 | open fun changeCursor(cursor: Cursor?) {
65 | if (cursor != null) {
66 | val old = swapCursor(cursor)
67 | old?.close()
68 | }
69 | }
70 |
71 | /**
72 | * Swap in a new Cursor, returning the old Cursor. Unlike
73 | * [.changeCursor], the returned old Cursor is *not*
74 | * closed.
75 | */
76 | fun swapCursor(newCursor: Cursor): Cursor? {
77 | if (newCursor === cursor) {
78 | return null
79 | }
80 | val oldCursor = cursor
81 | if (oldCursor != null && dataSetObserver != null) {
82 | oldCursor.unregisterDataSetObserver(dataSetObserver)
83 | }
84 | cursor = newCursor
85 | if (cursor != null) {
86 | if (dataSetObserver != null) {
87 | cursor!!.registerDataSetObserver(dataSetObserver)
88 | }
89 | rowIdColumn = newCursor.getColumnIndexOrThrow("_id")
90 | dataValid = true
91 | notifyDataSetChanged()
92 | } else {
93 | rowIdColumn = -1
94 | dataValid = false
95 | notifyDataSetChanged()
96 | //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
97 | }
98 | return oldCursor
99 | }
100 |
101 | private inner class NotifyingDataSetObserver : DataSetObserver() {
102 | override fun onChanged() {
103 | super.onChanged()
104 | dataValid = true
105 | notifyDataSetChanged()
106 | }
107 |
108 | override fun onInvalidated() {
109 | super.onInvalidated()
110 | dataValid = false
111 | notifyDataSetChanged()
112 | //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/adapter/OrmliteCursorRecyclerViewAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.adapter
2 |
3 | import android.database.Cursor
4 | import android.support.v7.widget.RecyclerView
5 | import com.j256.ormlite.android.AndroidDatabaseResults
6 | import com.j256.ormlite.stmt.PreparedQuery
7 | import java.sql.SQLException
8 |
9 | abstract class OrmliteCursorRecyclerViewAdapter() : CursorRecyclerViewAdapter(null) {
10 | var preparedQuery: PreparedQuery? = null
11 |
12 | abstract fun onBindViewHolder(holder: VH, t: T)
13 |
14 | override fun onBindViewHolder(viewHolder: VH, cursor: Cursor?) {
15 | try {
16 | onBindViewHolder(viewHolder, this.cursorToObject(cursor!!))
17 | } catch (e: SQLException) {
18 | e.printStackTrace()
19 | }
20 | }
21 |
22 | override fun changeCursor(cursor: Cursor?) {
23 | this.preparedQuery = preparedQuery
24 | super.changeCursor(cursor)
25 | }
26 |
27 | fun changeCursor(cursor: Cursor, preparedQuery: PreparedQuery) {
28 | this.preparedQuery = preparedQuery
29 | super.changeCursor(cursor)
30 | }
31 |
32 | fun getTypedItem(position: Int): T {
33 | try {
34 | return this.cursorToObject(getItem(position) as Cursor)
35 | } catch (var3: SQLException) {
36 | throw RuntimeException(var3)
37 | }
38 | }
39 |
40 | @Throws(SQLException::class)
41 | protected fun cursorToObject(cursor: Cursor): T {
42 | return this.preparedQuery!!.mapRow(AndroidDatabaseResults(cursor, null))
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/adapter/PlacesAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.adapter
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.support.v4.app.FragmentActivity
6 | import android.support.v7.widget.RecyclerView
7 | import android.text.format.DateUtils
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.widget.PopupMenu
11 | import com.google.gson.Gson
12 | import com.squareup.otto.Bus
13 | import com.squareup.picasso.Picasso
14 | import io.dp.weather.app.Const
15 | import io.dp.weather.app.R
16 | import io.dp.weather.app.SchedulersManager
17 | import io.dp.weather.app.annotation.CachePrefs
18 | import io.dp.weather.app.annotation.PerActivity
19 | import io.dp.weather.app.db.table.Place
20 | import io.dp.weather.app.event.DeletePlaceEvent
21 | import io.dp.weather.app.net.WeatherApi
22 | import io.dp.weather.app.net.dto.Forecast
23 | import io.dp.weather.app.net.dto.Weather
24 | import io.dp.weather.app.utils.MetricsController
25 | import io.dp.weather.app.utils.WhiteBorderCircleTransformation
26 | import io.dp.weather.app.utils.myEdit
27 | import kotlinx.android.synthetic.item_city_weather.view.*
28 | import kotlinx.android.synthetic.item_place_content.view.*
29 | import rx.lang.kotlin.subscribeWith
30 | import timber.log.Timber
31 | import javax.inject.Inject
32 |
33 | @PerActivity
34 | class PlacesAdapter
35 | @Inject constructor(val activity: FragmentActivity,
36 | val gson: Gson,
37 | val api: WeatherApi,
38 | val bus: Bus) : OrmliteCursorRecyclerViewAdapter() {
39 |
40 | private val cache = android.support.v4.util.LruCache(16)
41 |
42 | private lateinit var schedulersManager: SchedulersManager
43 | private lateinit var prefs: SharedPreferences
44 | private lateinit var metrics: MetricsController
45 |
46 | private val transformation = WhiteBorderCircleTransformation()
47 |
48 | @Inject
49 | fun setMetricsController(metrics: MetricsController) {
50 | this.metrics = metrics
51 | }
52 |
53 | @Inject
54 | fun setSharedPreferences(@CachePrefs prefs: SharedPreferences) {
55 | this.prefs = prefs
56 | }
57 |
58 | @Inject
59 | fun setSchedulersManager(schedulersManager: SchedulersManager) {
60 | this.schedulersManager = schedulersManager
61 | }
62 |
63 | public fun clear() {
64 | this.cache.evictAll()
65 | this.prefs.edit().clear().apply()
66 | }
67 |
68 | fun SharedPreferences.isNeedToUpdateWeather(hash: String): Boolean {
69 | val lastRequestTime = prefs.getLong(hash + "_time", -1)
70 | return lastRequestTime == -1L
71 | || (lastRequestTime > 0
72 | && (System.currentTimeMillis() - lastRequestTime) > DateUtils.DAY_IN_MILLIS)
73 | }
74 |
75 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder? {
76 | return Holder(activity.layoutInflater.inflate(R.layout.item_city_weather, parent, false))
77 | }
78 |
79 | fun getForecast(place: Place): Forecast? {
80 | val hash = "${place.hashCode()}"
81 | var forecast = cache.get(place.id)
82 | if (forecast == null && place.id != null) {
83 | // forecast exists - load it from cache
84 | val rawForecast = prefs.getString(hash, null)
85 | forecast = gson.fromJson(rawForecast, Forecast::class.java)
86 | forecast?.let {
87 | cache.put(place.id, forecast)
88 | }
89 | }
90 |
91 | return forecast
92 | }
93 |
94 | override fun onBindViewHolder(holder: Holder, place: Place) {
95 | val hash = "${place.hashCode()}"
96 |
97 | val isNeedToUpdate = prefs.isNeedToUpdateWeather(hash)
98 |
99 | with (holder) {
100 | with (itemView) {
101 | city_name.text = place.name
102 | temperature.text = ""
103 |
104 | menu.tag = place.id
105 | menu.setOnClickListener(popupOnClickListener)
106 |
107 | when {
108 | metrics.useCelsius -> degrees_type.setText(R.string.celcius)
109 | else -> degrees_type.setText(R.string.fahrenheit)
110 | }
111 |
112 | progress.visibility = if (isNeedToUpdate) View.VISIBLE else View.GONE
113 | content.visibility = if (isNeedToUpdate) View.GONE else View.VISIBLE
114 | }
115 | }
116 |
117 | if (isNeedToUpdate) {
118 | api.getForecast("${place.lat},${place.lon}", Const.FORECAST_FOR_DAYS)
119 | .compose(schedulersManager.applySchedulers())
120 | .subscribeWith {
121 | onNext {
122 | prefs.myEdit {
123 | arrayOf(hash + "_time" to System.currentTimeMillis(),
124 | hash to gson.toJson(it))
125 | }
126 | notifyDataSetChanged()
127 | }
128 |
129 | onError {
130 | Timber.e(it, "Got throwable")
131 | }
132 | }
133 | } else {
134 | val forecast = getForecast(place) ?: Forecast()
135 | holder.fillViewWithForecast(activity, forecast, metrics, transformation)
136 | }
137 | }
138 |
139 | val popupOnClickListener: View.OnClickListener = object : View.OnClickListener {
140 | override fun onClick(v: View) {
141 | val id = v.tag as Long
142 |
143 | val popupMenu = PopupMenu(activity, v)
144 |
145 | with (popupMenu) {
146 | inflate(R.menu.item_place)
147 |
148 | setOnMenuItemClickListener {
149 | bus.post(DeletePlaceEvent(id))
150 | true
151 | }
152 |
153 | show()
154 | }
155 |
156 | }
157 | }
158 |
159 | class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
160 |
161 | fun fillViewWithForecast(context: Context,
162 | forecast: Forecast,
163 | metrics: MetricsController,
164 | transformation: WhiteBorderCircleTransformation) {
165 | with (itemView) {
166 | val conditions = forecast.data?.currentCondition
167 |
168 | conditions?.isNotEmpty().let {
169 | val condition = conditions.get(0)
170 |
171 | humidity.text = "${condition.humidity} %"
172 |
173 | try {
174 | val pressure_val = Integer.valueOf(condition.pressure)!!
175 |
176 | pressure.text = when {
177 | metrics.useMmhg -> context.getString(R.string.fmt_pressure_mmhg, (pressure_val * Const.CONVERT_MMHG).toInt())
178 | else -> context.getString(R.string.fmt_pressure_kpa, pressure_val)
179 | }
180 | } catch (e: NumberFormatException) {
181 | pressure.setText(R.string.undef)
182 | }
183 |
184 | val metric = when {
185 | metrics.useKmph -> context.getString(R.string.fmt_windspeed_kmph, condition.windspeedKmph ?: "")
186 | else -> context.getString(R.string.fmt_windspeed_mph, condition.windspeedMiles ?: "")
187 | }
188 |
189 | wind.text = "${condition.winddir16Point}, $metric"
190 |
191 | val descList = condition.weatherDesc
192 | if (descList?.isNotEmpty() ?: false) {
193 | val description = descList?.get(0)?.value ?: ""
194 | weather_description.text = description
195 | }
196 |
197 | when {
198 | metrics.useCelsius -> temperature.text = condition.tempC ?: ""
199 | else -> temperature.text = condition.tempF ?: ""
200 | }
201 |
202 | val urls = conditions.get(0).weatherIconUrl
203 |
204 | if (urls?.isNotEmpty() ?: false) {
205 | val url = urls?.get(0)?.value ?: ""
206 | Picasso.with(context)
207 | .load(url)
208 | .transform(transformation)
209 | .into(weather_state)
210 | }
211 | }
212 |
213 | val weather5days = forecast.data?.weather ?: listOf()
214 | weather_for_week.setWeatherForWeek(weather5days, metrics.useCelsius, transformation)
215 | }
216 | }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/adapter/PlacesAutoCompleteAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.adapter
2 |
3 | import android.support.v4.app.FragmentActivity
4 | import android.widget.ArrayAdapter
5 | import android.widget.Filter
6 | import android.widget.Filterable
7 | import com.google.gson.JsonObject
8 | import io.dp.weather.app.R
9 | import io.dp.weather.app.net.PlacesApi
10 | import java.util.*
11 | import javax.inject.Inject
12 |
13 | class PlacesAutoCompleteAdapter
14 | @Inject
15 | constructor(activity: FragmentActivity, private val placesApi: PlacesApi) :
16 | ArrayAdapter(activity, R.layout.item_search_list), Filterable {
17 |
18 | private var resultList: ArrayList? = null
19 |
20 | override fun getCount(): Int = resultList?.size ?: 0
21 |
22 | override fun getItem(index: Int): String = resultList?.get(index) ?: ""
23 |
24 | override fun getFilter(): Filter {
25 | return object : Filter() {
26 | override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
27 | val filterResults = Filter.FilterResults()
28 | if (constraint != null) {
29 | val jsonResults = placesApi.getAutocomplete(constraint.toString())
30 |
31 | // Create a JSON object hierarchy from the results
32 | val predsJsonArray = jsonResults.getAsJsonArray("predictions")
33 |
34 | // Extract the Place descriptions from the results
35 | resultList = ArrayList(predsJsonArray.size())
36 | for (i in 0..predsJsonArray.size() - 1) {
37 | val o = predsJsonArray[i] as JsonObject
38 | resultList?.add(o.get("description").asString)
39 | }
40 |
41 | // Assign the data to the FilterResults
42 | filterResults.values = resultList
43 | filterResults.count = resultList?.size ?: 0
44 | }
45 | return filterResults
46 | }
47 |
48 | override fun publishResults(constraint: CharSequence?,
49 | results: Filter.FilterResults?) = when {
50 | results?.count ?: -1 > 0 -> notifyDataSetChanged()
51 | else -> notifyDataSetInvalidated()
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/annotation/CachePrefs.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.annotation
2 |
3 | import javax.inject.Qualifier
4 |
5 | import kotlin.annotation.AnnotationRetention.RUNTIME
6 |
7 | /**
8 | * Created by dp on 11/10/14.
9 | */
10 | @Qualifier
11 | @MustBeDocumented
12 | @Retention(RUNTIME)
13 | annotation public class CachePrefs
14 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/annotation/ConfigPrefs.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.annotation
2 |
3 | import javax.inject.Qualifier
4 |
5 | import kotlin.annotation.AnnotationRetention.RUNTIME
6 |
7 | /**
8 | * Created by dp on 11/10/14.
9 | */
10 | @Qualifier
11 | @MustBeDocumented
12 | @Retention(RUNTIME)
13 | annotation public class ConfigPrefs
14 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/annotation/IOSched.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.annotation
2 |
3 | import javax.inject.Qualifier
4 |
5 | import kotlin.annotation.AnnotationRetention.RUNTIME
6 |
7 | /**
8 | * Created by deepol on 11/09/15.
9 | */
10 | @Qualifier
11 | @MustBeDocumented
12 | @Retention(RUNTIME)
13 | annotation public class IOSched
14 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/annotation/PerActivity.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.annotation
2 |
3 | import javax.inject.Scope
4 |
5 | import kotlin.annotation.AnnotationRetention.RUNTIME
6 |
7 | /**
8 | * A scoping annotation to permit objects whose lifetime should
9 | * conform to the life of the activity to be memorized in the
10 | * correct component.
11 | */
12 | @Scope @Retention(RUNTIME) annotation public class PerActivity
13 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/annotation/PerFragment.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.annotation
2 |
3 | import javax.inject.Scope
4 |
5 | import kotlin.annotation.AnnotationRetention.RUNTIME
6 |
7 | /**
8 | * A scoping annotation to permit objects whose lifetime should
9 | * conform to the life of the activity to be memorized in the
10 | * correct component.
11 | */
12 | @Scope @Retention(RUNTIME) annotation public class PerFragment
13 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/annotation/UISched.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.annotation
2 |
3 | import javax.inject.Qualifier
4 |
5 | import kotlin.annotation.AnnotationRetention.RUNTIME
6 |
7 | /**
8 | * Created by deepol on 11/09/15.
9 | */
10 | @Qualifier
11 | @MustBeDocumented
12 | @Retention(RUNTIME)
13 | annotation public class UISched
14 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/DatabaseHelper.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db
2 |
3 | import android.content.Context
4 | import android.database.sqlite.SQLiteDatabase
5 | import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
6 | import com.j256.ormlite.dao.Dao
7 | import com.j256.ormlite.support.ConnectionSource
8 | import com.j256.ormlite.table.TableUtils
9 | import io.dp.weather.app.BuildConfig
10 | import io.dp.weather.app.R
11 | import io.dp.weather.app.db.table.Place
12 | import timber.log.Timber
13 | import java.sql.SQLException
14 |
15 | /**
16 | * Created by dp on 09/10/14.
17 | */
18 | public class DatabaseHelper : OrmLiteSqliteOpenHelper {
19 |
20 | var context: Context
21 |
22 | @Volatile private var placeDao: Dao? = null
23 |
24 | var predefinedCities = arrayOf(
25 | Place("Dublin", 53.34410, -6.2674),
26 | Place("London", 51.51121, -0.1198),
27 | Place("New York", 40.71278, -74.00594),
28 | Place("Barcelona", 41.3850, 2.1734))
29 |
30 | public constructor(context: Context, databaseName: String, factory: SQLiteDatabase.CursorFactory,
31 | databaseVersion: Int) : super(context, databaseName, factory, databaseVersion, R.raw.ormlite_config) {
32 | this.context = context
33 | }
34 |
35 | public constructor(context: Context) : super(context, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config) {
36 | this.context = context
37 | }
38 |
39 | override fun onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
40 | Timber.v("! onCreateDatabase")
41 |
42 | try {
43 | TableUtils.createTableIfNotExists(connectionSource, Place::class.java)
44 | val cityDao = getPlaceDao()
45 | for (place in predefinedCities) {
46 | cityDao?.createIfNotExists(place)
47 | }
48 | } catch (e: SQLException) {
49 | e.printStackTrace()
50 | }
51 |
52 | }
53 |
54 | override fun onUpgrade(database: SQLiteDatabase, connectionSource: ConnectionSource, oldVersion: Int,
55 | newVersion: Int) {
56 | Timber.v("! onUpgradeDatabase")
57 | }
58 |
59 | @Throws(SQLException::class)
60 | public fun getPlaceDao(): Dao? {
61 | var resultDao = placeDao
62 |
63 | if (resultDao == null) {
64 | synchronized (this) {
65 | resultDao = placeDao
66 |
67 | if (resultDao == null) {
68 | resultDao = getDao(Place::class.java)
69 | placeDao = resultDao
70 | }
71 | }
72 | }
73 | return resultDao
74 | }
75 |
76 | override fun close() {
77 | super.close()
78 |
79 | placeDao = null
80 | }
81 |
82 | companion object {
83 |
84 | private val DATABASE_NAME = "weather.db"
85 | private val DATABASE_VERSION = BuildConfig.DATABASE_VERSION
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/NoIdCursorWrapper.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db
2 |
3 | import android.database.Cursor
4 | import android.database.CursorWrapper
5 | import android.provider.BaseColumns
6 |
7 | /**
8 | * Created by dp on 09/10/14.
9 | */
10 | public class NoIdCursorWrapper : CursorWrapper {
11 |
12 | private var idColumnIndex: Int = 0
13 |
14 | /**
15 | * Create a NoIdCursorWrapper using the alias column index.
16 |
17 | * @param c the cursor to wrap
18 | * *
19 | * @param idColumnIndex the column index to use as the _id column alias
20 | */
21 | public constructor(c: Cursor, idColumnIndex: Int) : super(c) {
22 | this.idColumnIndex = idColumnIndex
23 | }
24 |
25 | /**
26 | * Create a NoIdCursorWrapper using the alias column name.
27 |
28 | * @param c the cursor to wrap
29 | * *
30 | * @param idColumnName the column name to use as the _id column alias
31 | */
32 | public constructor(c: Cursor, idColumnName: String) : super(c) {
33 | idColumnIndex = c.getColumnIndex(idColumnName)
34 | }
35 |
36 | override fun getColumnIndex(columnName: String): Int {
37 | var index = super.getColumnIndex(columnName)
38 | if (index < 0 && BaseColumns._ID == columnName) {
39 | index = idColumnIndex
40 | }
41 | return index
42 | }
43 |
44 | @Throws(IllegalArgumentException::class)
45 | override fun getColumnIndexOrThrow(columnName: String): Int {
46 | val index = getColumnIndex(columnName)
47 | if (index >= 0) {
48 | return index
49 | }
50 | // let the AbstractCursor generate the exception
51 | return super.getColumnIndexOrThrow(columnName)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/OrmliteCursorAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db
2 |
3 | import android.content.Context
4 | import android.database.Cursor
5 | import android.support.v4.widget.CursorAdapter
6 | import android.view.View
7 |
8 | import com.j256.ormlite.android.AndroidDatabaseResults
9 | import com.j256.ormlite.stmt.PreparedQuery
10 |
11 | import java.sql.SQLException
12 |
13 | /**
14 | * Created by dp on 09/10/14.
15 | */
16 | public abstract class OrmliteCursorAdapter(context: Context, c: Cursor?, public var query: PreparedQuery?) : CursorAdapter(context, c, false) {
17 |
18 | override public fun bindView(itemView: View, context: Context, cursor: Cursor) {
19 | try {
20 | var item = query?.mapRow(AndroidDatabaseResults(cursor, null))
21 | bindView(itemView, context, item)
22 | } catch (e: SQLException) {
23 | e.printStackTrace()
24 | }
25 |
26 | }
27 |
28 | override public fun getItem(position: Int): T? {
29 | val c = super.getItem(position) as Cursor
30 | try {
31 | return query?.mapRow(AndroidDatabaseResults(c, null))
32 | } catch (e: SQLException) {
33 | e.printStackTrace()
34 | }
35 |
36 | return null
37 | }
38 |
39 | public abstract fun bindView(itemView: View, context: Context, item: T?)
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/OrmliteCursorLoader.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db
2 |
3 | import android.content.Context
4 | import android.database.Cursor
5 | import android.support.v4.content.AsyncTaskLoader
6 | import android.support.v4.content.Loader
7 | import com.j256.ormlite.android.AndroidDatabaseResults
8 | import com.j256.ormlite.dao.BaseDaoImpl
9 | import com.j256.ormlite.dao.Dao
10 | import com.j256.ormlite.stmt.PreparedQuery
11 | import java.io.FileDescriptor
12 | import java.io.PrintWriter
13 | import java.sql.SQLException
14 |
15 | /**
16 | * Created by dp on 09/10/14.
17 | */
18 | public class OrmliteCursorLoader(context: Context, public val dao: Dao?,
19 | public val query: PreparedQuery?) : AsyncTaskLoader(context) {
20 |
21 | val observer: Loader.ForceLoadContentObserver = Loader(context).ForceLoadContentObserver()
22 |
23 | private var cursor: Cursor? = null
24 |
25 | /* Runs on a worker thread */
26 |
27 | override fun loadInBackground(): Cursor? {
28 | var cursor: Cursor? = null
29 | try {
30 | cursor = getCursor(query)
31 | } catch (e: SQLException) {
32 | e.printStackTrace()
33 | }
34 |
35 | if (cursor != null) {
36 | // Ensure the cursor window is filled
37 | cursor.count
38 | registerContentObserver(cursor)
39 | }
40 | return cursor
41 | }
42 |
43 | @Throws(SQLException::class)
44 | public fun getCursor(query: PreparedQuery?): Cursor {
45 | val baseDao = dao as BaseDaoImpl
46 | val iterator = dao.iterator(query)
47 | val results = iterator.getRawResults() as AndroidDatabaseResults
48 | val idColumnName = baseDao.getTableInfo().getIdField().getColumnName()
49 | return NoIdCursorWrapper(results.getRawCursor(), idColumnName)
50 | }
51 |
52 | /**
53 | * Registers an observer to get notifications from the content provider when the cursor needs to
54 | * be refreshed.
55 | */
56 | fun registerContentObserver(cursor: Cursor) {
57 | cursor.registerContentObserver(this.observer)
58 | }
59 |
60 | /* Runs on the UI thread */
61 | override public fun deliverResult(cursor: Cursor?) {
62 | if (isReset()) {
63 | // An async query came in while the loader is stopped
64 | cursor?.close()
65 | return
66 | }
67 | val oldCursor = this.cursor
68 | this.cursor = cursor
69 |
70 | if (isStarted()) {
71 | super.deliverResult(cursor)
72 | }
73 |
74 | if (oldCursor != null && oldCursor !== cursor && !oldCursor.isClosed) {
75 | oldCursor.close()
76 | }
77 | }
78 |
79 | override fun onStartLoading() {
80 | if (cursor != null) {
81 | deliverResult(cursor)
82 | }
83 | if (takeContentChanged() || cursor == null) {
84 | forceLoad()
85 | }
86 | }
87 |
88 | /**
89 | * Must be called from the UI thread
90 | */
91 | override fun onStopLoading() {
92 | // Attempt to cancel the current load task if possible.
93 | cancelLoad()
94 | }
95 |
96 | override fun onCanceled(cursor: Cursor?) {
97 | if (cursor != null && !cursor.isClosed) {
98 | cursor.close()
99 | }
100 | }
101 |
102 | override fun onReset() {
103 | super.onReset()
104 |
105 | // Ensure the loader is stopped
106 | onStopLoading()
107 |
108 | if (cursor != null && !cursor!!.isClosed) {
109 | cursor!!.close()
110 | }
111 | cursor = null
112 | }
113 |
114 | override fun dump(prefix: String, fd: FileDescriptor, writer: PrintWriter, args: Array) {
115 | super.dump(prefix, fd, writer, args)
116 | writer.print(prefix)
117 | writer.print("cursor=")
118 | writer.println(cursor)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/OrmliteListLoader.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db
2 |
3 | import android.content.Context
4 | import android.support.v4.content.AsyncTaskLoader
5 |
6 | import com.j256.ormlite.dao.Dao
7 | import com.j256.ormlite.stmt.PreparedQuery
8 |
9 | import java.sql.SQLException
10 | import java.util.*
11 |
12 | /**
13 | * Created by dp on 09/10/14.
14 | */
15 | public class OrmliteListLoader(context: Context, dao: Dao, query: PreparedQuery) : AsyncTaskLoader>(context) {
16 |
17 | private lateinit var dao: Dao
18 | private lateinit var query: PreparedQuery
19 | private var data: List? = null
20 |
21 | init {
22 | this.dao = dao
23 | this.query = query
24 | }
25 |
26 | override fun loadInBackground(): List {
27 | val result: ArrayList = ArrayList()
28 |
29 | try {
30 | result.addAll(dao.query(query))
31 |
32 | } catch (e: SQLException) {
33 | }
34 |
35 | return result
36 | }
37 |
38 | override fun deliverResult(datas: List?) {
39 | if (isReset()) {
40 | // An async query came in while the loader is stopped. We
41 | // don't need the result.
42 | if (datas != null) {
43 | onReleaseResources(datas)
44 | }
45 | }
46 |
47 | val oldDatas = data
48 | data = datas
49 |
50 | if (isStarted()) {
51 | // If the Loader is currently started, we can immediately
52 | // deliver its results.
53 | super.deliverResult(datas)
54 | }
55 |
56 | if (oldDatas != null && !oldDatas.isEmpty()) {
57 | onReleaseResources(oldDatas)
58 | }
59 | }
60 |
61 | /**
62 | * Handles a request to start the Loader.
63 | */
64 | override fun onStartLoading() {
65 | if (data != null) {
66 | // If we currently have a result available, deliver it
67 | // immediately.
68 | deliverResult(data)
69 | } else {
70 | // If the data has changed since the last time it was loaded
71 | // or is not currently available, start a load.
72 | forceLoad()
73 | }
74 | }
75 |
76 | /**
77 | * Handles a request to stop the Loader.
78 | */
79 | override fun onStopLoading() {
80 | // Attempt to cancel the current load task if possible.
81 | cancelLoad()
82 | }
83 |
84 | /**
85 | * Handles a request to cancel a load.
86 | */
87 | override fun onCanceled(datas: List) {
88 | super.onCanceled(datas)
89 |
90 | // At this point we can release the resources associated with 'apps'
91 | // if needed.
92 | onReleaseResources(datas)
93 | }
94 |
95 | /**
96 | * Handles a request to completely reset the Loader.
97 | */
98 | override fun onReset() {
99 | super.onReset()
100 |
101 | // Ensure the loader is stopped
102 | onStopLoading()
103 |
104 | // At this point we can release the resources associated with 'apps'
105 | // if needed.
106 | if (data != null) {
107 | onReleaseResources(data)
108 | data = null
109 | }
110 | }
111 |
112 | /**
113 | * Helper function to take care of releasing resources associated with an actively loaded data
114 | * set.
115 | */
116 | protected fun onReleaseResources(datas: List?) {
117 | // For a simple List<> there is nothing to do. For something
118 | // like a Cursor, we would close it here.
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/Queries.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db
2 |
3 | import com.j256.ormlite.stmt.PreparedQuery
4 | import io.dp.weather.app.db.table.Place
5 | import java.sql.SQLException
6 |
7 | /**
8 | * Created by dp on 09/10/14.
9 | */
10 | public object Queries {
11 |
12 | public fun prepareCityQuery(dbHelper: DatabaseHelper?): PreparedQuery? {
13 | try {
14 | val qb = dbHelper!!.getPlaceDao()!!.queryBuilder()
15 | return qb.prepare()
16 | } catch (e: SQLException) {
17 | e.printStackTrace()
18 | }
19 |
20 | return null
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/Tables.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db
2 |
3 | /**
4 | * Created by deepol on 10/11/15.
5 | */
6 | object PlaceTable {
7 | val NAME = "Place"
8 | val ID = "_id"
9 | val CITY = "city"
10 | val LAT = "lat"
11 | val LON = "LON"
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/db/table/Place.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.db.table
2 |
3 | import com.j256.ormlite.field.DatabaseField
4 | import com.j256.ormlite.table.DatabaseTable
5 |
6 | @DatabaseTable(tableName = "places")
7 | data class Place(@DatabaseField(generatedId = true, dataType = com.j256.ormlite.field.DataType.LONG, columnName = Place.ID)
8 | public var id: Long? = 0,
9 |
10 | @DatabaseField(dataType = com.j256.ormlite.field.DataType.STRING, columnName = Place.NAME)
11 | public var name: String? = null,
12 |
13 | @DatabaseField(dataType = com.j256.ormlite.field.DataType.DOUBLE_OBJ, columnName = Place.LAT)
14 | public var lat: Double? = null,
15 |
16 | @DatabaseField(dataType = com.j256.ormlite.field.DataType.DOUBLE_OBJ, columnName = Place.LON)
17 | public var lon: Double? = null) {
18 |
19 | public constructor(name: String, lat: Double, lon: Double) : this() {
20 | this.name = name
21 | this.lat = lat
22 | this.lon = lon
23 | }
24 |
25 | companion object {
26 | const public val ID = "id"
27 | const public val NAME = "name"
28 | const public val LAT = "lat"
29 | const public val LON = "lon"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/event/AddPlaceEvent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.event
2 |
3 | /**
4 | * Created by dp on 10/10/14.
5 | */
6 | public class AddPlaceEvent(public val lookupPlace: String)
7 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/event/DeletePlaceEvent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.event
2 |
3 | /**
4 | * Created by dp on 09/10/14.
5 | */
6 | public class DeletePlaceEvent(public val id: Long?)
7 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/event/UpdateListEvent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.event
2 |
3 | /**
4 | * Created by dp on 10/10/14.
5 | */
6 | public class UpdateListEvent
7 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/fragment/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.fragment
2 |
3 | import android.os.Bundle
4 | import com.trello.rxlifecycle.components.support.RxFragment
5 | import io.dp.weather.app.activity.BaseActivityComponent
6 | import io.dp.weather.app.activity.HasComponent
7 |
8 | /**
9 | * Created by dp on 08/10/14.
10 | */
11 | public abstract class BaseFragment : RxFragment() {
12 |
13 | public lateinit var component: BaseActivityComponent
14 |
15 | override fun onActivityCreated(savedInstanceState: Bundle?) {
16 | super.onActivityCreated(savedInstanceState)
17 |
18 | val activity = getActivity() as HasComponent
19 | this.component = activity.getComponent()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/fragment/WeatherFragment.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.fragment
2 |
3 | import android.content.Intent
4 | import android.database.Cursor
5 | import android.location.Geocoder
6 | import android.os.Bundle
7 | import android.support.v4.app.LoaderManager
8 | import android.support.v4.content.Loader
9 | import android.support.v4.view.MenuItemCompat
10 | import android.support.v4.widget.SwipeRefreshLayout
11 | import android.support.v7.widget.StaggeredGridLayoutManager
12 | import android.view.*
13 | import android.widget.AdapterView
14 | import com.squareup.otto.Bus
15 | import com.squareup.otto.Subscribe
16 | import io.dp.weather.app.BusSubcomponent
17 | import io.dp.weather.app.R
18 | import io.dp.weather.app.SchedulersManager
19 | import io.dp.weather.app.activity.SettingsActivity
20 | import io.dp.weather.app.activity.debug.DebugActivity
21 | import io.dp.weather.app.adapter.PlacesAdapter
22 | import io.dp.weather.app.adapter.PlacesAutoCompleteAdapter
23 | import io.dp.weather.app.db.DatabaseHelper
24 | import io.dp.weather.app.db.OrmliteCursorLoader
25 | import io.dp.weather.app.db.Queries
26 | import io.dp.weather.app.db.table.Place
27 | import io.dp.weather.app.event.AddPlaceEvent
28 | import io.dp.weather.app.event.DeletePlaceEvent
29 | import io.dp.weather.app.event.UpdateListEvent
30 | import io.dp.weather.app.utils.Observables
31 | import io.dp.weather.app.widget.ArrayAdapterSearchView
32 | import org.jetbrains.anko.support.v4.longToast
33 | import rx.lang.kotlin.subscribeWith
34 | import java.sql.SQLException
35 | import javax.inject.Inject
36 |
37 | class WeatherFragment : BaseFragment(), LoaderManager.LoaderCallbacks, SwipeRefreshLayout.OnRefreshListener {
38 |
39 | @Inject lateinit var geoCoder: Geocoder
40 | @Inject lateinit var adapter: PlacesAdapter
41 | @Inject lateinit var dbHelper: DatabaseHelper
42 | @Inject lateinit var bus: Bus
43 | @Inject lateinit var placesAutoCompleteAdapter: PlacesAutoCompleteAdapter
44 | @Inject lateinit var schedulersManager: SchedulersManager
45 |
46 | override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
47 | savedInstanceState: Bundle?): View? {
48 | return inflater!!.inflate(R.layout.fragment_weather, container, false)
49 | }
50 |
51 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
52 | super.onViewCreated(view, savedInstanceState)
53 |
54 | swipe_layout.setOnRefreshListener(this)
55 | recycler.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
56 | }
57 |
58 | override fun onActivityCreated(savedInstanceState: Bundle?) {
59 | super.onActivityCreated(savedInstanceState)
60 |
61 | (component as BusSubcomponent).inject(this)
62 |
63 | adapter.preparedQuery = Queries.prepareCityQuery(dbHelper)
64 | recycler.adapter = adapter
65 |
66 | loaderManager.restartLoader(0, null, this)
67 |
68 | setHasOptionsMenu(true)
69 | }
70 |
71 | override fun onResume() {
72 | super.onResume()
73 | bus.register(this)
74 | adapter.notifyDataSetChanged()
75 | }
76 |
77 | override fun onPause() {
78 | super.onPause()
79 | bus.unregister(this)
80 | }
81 |
82 | override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
83 | super.onCreateOptionsMenu(menu, inflater)
84 |
85 | inflater!!.inflate(R.menu.main, menu)
86 |
87 | if (Geocoder.isPresent()) {
88 | val addItem = menu!!.findItem(R.id.action_add)
89 |
90 | val searchView = MenuItemCompat.getActionView(addItem) as ArrayAdapterSearchView;
91 | searchView.setOnItemClickListener(AdapterView.OnItemClickListener { adapterView, view, pos, id ->
92 | addItem.collapseActionView();
93 | searchView.setText("");
94 | bus.post(AddPlaceEvent(adapterView.getItemAtPosition(pos) as String));
95 | });
96 |
97 | val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
98 | searchView.layoutParams = params;
99 | searchView.setAdapter(placesAutoCompleteAdapter);
100 | } else {
101 | longToast("Geocoder is not present")
102 | }
103 | }
104 |
105 | override fun onOptionsItemSelected(item: MenuItem?): Boolean {
106 | when (item!!.itemId) {
107 | R.id.action_add -> {
108 | Geocoder.isPresent()
109 | }
110 |
111 | R.id.action_settings -> {
112 | startActivity(Intent(activity, SettingsActivity::class.java))
113 | return true
114 | }
115 |
116 | R.id.action_debug -> {
117 | startActivity(Intent(activity, DebugActivity::class.java))
118 | return true
119 | }
120 | }
121 | return super.onOptionsItemSelected(item)
122 | }
123 |
124 | override fun onCreateLoader(i: Int, bundle: Bundle?): Loader? {
125 | try {
126 | return OrmliteCursorLoader(activity, dbHelper.getPlaceDao(), adapter.preparedQuery)
127 | } catch (e: SQLException) {
128 | e.printStackTrace()
129 | }
130 |
131 | return null
132 | }
133 |
134 | override fun onLoadFinished(loader: Loader, cursor: Cursor) = adapter.changeCursor(cursor)
135 |
136 | override fun onLoaderReset(loader: Loader) = adapter.changeCursor(null)
137 |
138 | override fun onRefresh() {
139 | swipe_layout.isRefreshing = true
140 | adapter.clear()
141 | adapter.notifyDataSetChanged()
142 | swipe_layout.isRefreshing = false
143 | }
144 |
145 | @Subscribe fun onUpdateList(event: UpdateListEvent) {
146 | loaderManager.restartLoader(0, null, this)
147 | }
148 |
149 | @Subscribe fun onDeletePlace(event: DeletePlaceEvent) {
150 | if (event.id != null) {
151 | try {
152 | dbHelper.getPlaceDao()!!.deleteById(event.id)
153 | loaderManager.restartLoader(0, null, this)
154 | } catch (e: SQLException) {
155 | e.printStackTrace()
156 | }
157 | }
158 | }
159 |
160 | @Subscribe fun onAddPlace(event: AddPlaceEvent) {
161 | Observables.getGeoForPlace(activity, dbHelper, geoCoder, event.lookupPlace)
162 | .compose(schedulersManager.applySchedulers())
163 | .subscribeWith {
164 | onNext { bus.post(UpdateListEvent()) }
165 | onError { }
166 | }
167 | }
168 |
169 | companion object {
170 |
171 | public fun newInstance(): WeatherFragment {
172 | return WeatherFragment()
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/PlacesApi.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net
2 |
3 | import com.google.gson.JsonObject
4 | import retrofit.http.GET
5 | import retrofit.http.Query
6 |
7 | /**
8 | * Created by dp on 10/10/14.
9 | */
10 | interface PlacesApi {
11 |
12 | @GET("/place/autocomplete/json")
13 | fun getAutocomplete(@Query("input") input: String): JsonObject
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/WeatherApi.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net
2 |
3 | import io.dp.weather.app.net.dto.Forecast
4 | import retrofit.http.GET
5 | import retrofit.http.Query
6 | import rx.Observable
7 |
8 | /**
9 | * Created by dp on 08/10/14.
10 | */
11 | interface WeatherApi {
12 |
13 | @GET("/weather.ashx")
14 | fun getForecast(@Query("q") params: String, @Query("num_of_days") days: Int): Observable
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/dto/CurrentCondition.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import io.dp.weather.app.WeatherIconUrl
5 |
6 | public data class CurrentCondition(@SerializedName("cloudcover") var cloudcover: String?,
7 | @SerializedName("humidity") var humidity: String?,
8 | @SerializedName("observation_time") var observationTime: String?,
9 | @SerializedName("precipMM") var precipMM: String?,
10 | @SerializedName("temp_C") var tempC: String?,
11 | @SerializedName("temp_F") var tempF: String?,
12 | @SerializedName("pressure") var pressure: String?,
13 | @SerializedName("visibility") var visibility: String?,
14 | @SerializedName("weatherCode") var weatherCode: String?,
15 | @SerializedName("weatherDesc") var weatherDesc: List?,
16 | @SerializedName("weatherIconUrl") var weatherIconUrl: List?,
17 | @SerializedName("winddir16Point") var winddir16Point: String?,
18 | @SerializedName("winddirDegree") var winddirDegree: String?,
19 | @SerializedName("windspeedKmph") var windspeedKmph: String?,
20 | @SerializedName("windspeedMiles") var windspeedMiles: String?)
21 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/dto/Data.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Data(@SerializedName("current_condition") var currentCondition: List?,
6 | @SerializedName("request") var request: List?,
7 | @SerializedName("weather") var weather: List?)
8 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/dto/Forecast.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Forecast(@SerializedName("data") var data: Data? = null)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/dto/Request.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Request(@SerializedName("query") var query: String?,
6 | @SerializedName("type") var type: String?)
7 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/dto/Weather.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 | import io.dp.weather.app.WeatherIconUrl
5 |
6 | data class Weather(@SerializedName("date") var date: String?,
7 | @SerializedName("precipMM") var precipMM: String?,
8 | @SerializedName("tempMaxC") var tempMaxC: String?,
9 | @SerializedName("tempMaxF") var tempMaxF: String?,
10 | @SerializedName("tempMinC") var tempMinC: String?,
11 | @SerializedName("tempMinF") var tempMinF: String?,
12 | @SerializedName("weatherCode") var weatherCode: String?,
13 | @SerializedName("weatherDesc") var weatherDesc: List?,
14 | @SerializedName("weatherIconUrl") var weatherIconUrl: List?,
15 | @SerializedName("winddir16Point") var winddir16Point: String?,
16 | @SerializedName("winddirDegree") var winddirDegree: String?,
17 | @SerializedName("winddirection") var winddirection: String?,
18 | @SerializedName("windspeedKmph") var windspeedKmph: String?,
19 | @SerializedName("windspeedMiles") var windspeedMiles: String?)
20 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/dto/WeatherDesc.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.net.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class WeatherDesc(@SerializedName("value") var value: String?)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/net/dto/WeatherIconUrl.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class WeatherIconUrl(@SerializedName("value") var value: String?)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/utils/AsyncBus.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.utils
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import com.squareup.otto.Bus
6 | import com.squareup.otto.ThreadEnforcer
7 |
8 | /**
9 | * Created by dp on 09/10/14.
10 | */
11 | public class AsyncBus(enforcer: ThreadEnforcer, name: String) : Bus(enforcer, name) {
12 |
13 | private val mainThread = Handler(Looper.getMainLooper())
14 |
15 | override fun post(event: Any) {
16 | mainThread.post(object : Runnable {
17 | override fun run() {
18 | super@AsyncBus.post(event)
19 | }
20 | })
21 | }
22 |
23 | public fun postDelayed(event: Any, delayMs: Long) {
24 | mainThread.postDelayed(object : Runnable {
25 | override fun run() {
26 | super@AsyncBus.post(event)
27 | }
28 | }, delayMs)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/utils/MetricsController.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.utils
2 |
3 | import android.content.SharedPreferences
4 | import io.dp.weather.app.Const
5 | import io.dp.weather.app.annotation.ConfigPrefs
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Created by dp on 11/10/14.
10 | */
11 | public class MetricsController
12 | @Inject
13 | constructor(@ConfigPrefs private val prefs: SharedPreferences) {
14 | val useCelsius: Boolean
15 | get() = prefs.getBoolean(Const.USE_CELCIUS, false)
16 |
17 | val useKmph: Boolean
18 | get() = prefs.getBoolean(Const.USE_KMPH, false)
19 |
20 | val useMmhg: Boolean
21 | get() = prefs.getBoolean(Const.USE_MMHG, false)
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/utils/MyPreferences.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.utils
2 |
3 | import android.content.SharedPreferences
4 |
5 | /**
6 | * Created by dp on 15/11/15.
7 | */
8 |
9 | fun SharedPreferences.myEdit(func: SharedPreferences.Editor.() -> Array>) : SharedPreferences.Editor {
10 | val editor = edit()
11 |
12 | val pairs = editor.func()
13 |
14 | for ((key, value) in pairs) {
15 | when (value) {
16 | is String -> editor.putString(key, value)
17 | is Set<*> -> {
18 | if (!value.all { it is String }) {
19 | throw IllegalArgumentException("Only Set is supported")
20 | }
21 | editor.putStringSet(key, value as Set)
22 | }
23 | is Int -> editor.putInt(key, value)
24 | is Long -> editor.putLong(key, value)
25 | is Float -> editor.putFloat(key, value)
26 | is Boolean -> editor.putBoolean(key, value)
27 | else -> throw IllegalArgumentException("Unsupported value type: ${value.javaClass}")
28 | }
29 | }
30 |
31 | if (pairs.size > 0) {
32 | editor.apply()
33 | }
34 |
35 | return editor
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/utils/Observables.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.utils
2 |
3 | import android.content.Context
4 | import android.location.Address
5 | import android.location.Geocoder
6 | import android.widget.Toast
7 | import io.dp.weather.app.R
8 | import io.dp.weather.app.db.DatabaseHelper
9 | import io.dp.weather.app.db.table.Place
10 | import org.jetbrains.anko.toast
11 | import rx.Observable
12 | import rx.lang.kotlin.observable
13 | import timber.log.Timber
14 | import java.io.IOException
15 | import java.sql.SQLException
16 |
17 | /**
18 | * Created by dp on 10/10/14.
19 | */
20 | public object Observables {
21 |
22 | public fun getGeoForPlace(context: Context,
23 | dbHelper: DatabaseHelper,
24 | geocoder: Geocoder,
25 | lookupPlace: String): Observable {
26 |
27 | return observable>() { subscriber ->
28 | try {
29 | val addresses = geocoder.getFromLocationName(lookupPlace, 1)
30 | Timber.v("! got addresses: $addresses")
31 | subscriber.onNext(addresses)
32 | } catch (e: IOException) {
33 | Toast.makeText(context, R.string.cannot_find_geo_for_specified_location,
34 | Toast.LENGTH_SHORT).show()
35 | Timber.e(e, "Cannot find geo for location name")
36 | subscriber.onError(e)
37 | } finally {
38 | subscriber.onCompleted()
39 | }
40 | }.flatMap { addresses ->
41 | observable() { subscriber ->
42 | if (addresses?.size ?: -1 > 0) {
43 | val address = addresses.first()
44 | try {
45 | Timber.v("! Add place to database: $lookupPlace")
46 | val place = Place(lookupPlace, address.latitude, address.longitude)
47 | dbHelper.getPlaceDao()!!.createIfNotExists(place)
48 |
49 | subscriber.onNext(place)
50 | } catch (e: SQLException) {
51 | context.toast(R.string.something_went_wrong_with_adding_new_location)
52 | Timber.e(e, "Cannot add city $address lookupName: $lookupPlace lat ${address.latitude} lon ${address.longitude}")
53 | subscriber.onError(e)
54 | } finally {
55 | subscriber.onCompleted()
56 | }
57 | } else {
58 | Timber.v("! empty addresses: $addresses")
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/utils/WhiteBorderCircleTransformation.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.utils
2 |
3 | import android.graphics.*
4 |
5 | import com.squareup.picasso.Transformation
6 |
7 | /**
8 | * Created by dp on 10/10/14.
9 | */
10 | public class WhiteBorderCircleTransformation : Transformation {
11 |
12 | private val whitePaint = Paint()
13 |
14 | init {
15 | whitePaint.color = Color.WHITE
16 | whitePaint.isAntiAlias = true
17 | }
18 |
19 | override fun transform(source: Bitmap): Bitmap {
20 | val size = Math.min(source.width, source.height)
21 |
22 | val x = (source.width - size) / 2
23 | val y = (source.height - size) / 2
24 |
25 | val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size)
26 | if (squaredBitmap != source) {
27 | source.recycle()
28 | }
29 |
30 | val bitmap = Bitmap.createBitmap(size, size, source.config)
31 |
32 | val canvas = Canvas(bitmap)
33 | val paint = Paint()
34 |
35 | val shader = BitmapShader(squaredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
36 | paint.setShader(shader)
37 | paint.isAntiAlias = true
38 |
39 | val r = size / 2f
40 | val margin = r - 2.0f
41 |
42 | canvas.drawCircle(r, r, r, whitePaint)
43 | canvas.drawCircle(r, r, margin, paint)
44 |
45 | squaredBitmap.recycle()
46 | return bitmap
47 | }
48 |
49 | override fun key(): String {
50 | return "white_border_circle"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/widget/ArrayAdapterSearchView.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.widget
2 |
3 | import android.content.Context
4 | import android.support.v4.widget.CursorAdapter
5 | import android.support.v7.widget.SearchView
6 | import android.util.AttributeSet
7 | import android.widget.AdapterView
8 | import android.widget.ArrayAdapter
9 |
10 | class ArrayAdapterSearchView : SearchView {
11 |
12 | private lateinit var searchAutoComplete: SearchView.SearchAutoComplete
13 |
14 | constructor(context: Context) : super(context) {
15 | initialize()
16 | }
17 |
18 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
19 | initialize()
20 | }
21 |
22 | fun initialize() {
23 | searchAutoComplete = findViewById(
24 | android.support.v7.appcompat.R.id.search_src_text) as SearchView.SearchAutoComplete
25 | this.setAdapter(null)
26 | this.setOnItemClickListener(null)
27 | }
28 |
29 | override fun setSuggestionsAdapter(adapter: CursorAdapter) {
30 | // don't let anyone touch this
31 | }
32 |
33 | fun setOnItemClickListener(listener: AdapterView.OnItemClickListener?) {
34 | searchAutoComplete.onItemClickListener = listener
35 | }
36 |
37 | fun setAdapter(adapter: ArrayAdapter<*>?) = searchAutoComplete.setAdapter>(adapter)
38 |
39 | fun setText(text: String) = searchAutoComplete.setText(text)
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/dp/weather/app/widget/WeatherFor5DaysView.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.widget
2 |
3 | import android.content.Context
4 | import android.text.TextUtils
5 | import android.text.format.DateUtils
6 | import android.util.AttributeSet
7 | import android.view.LayoutInflater
8 | import android.widget.ImageView
9 | import android.widget.LinearLayout
10 | import android.widget.TextView
11 | import com.squareup.picasso.Picasso
12 | import com.squareup.picasso.Transformation
13 | import io.dp.weather.app.R
14 | import io.dp.weather.app.net.dto.Weather
15 | import kotlinx.android.synthetic.view_weather_for_week.view.*
16 | import java.sql.Date
17 |
18 | public class WeatherFor5DaysView : LinearLayout {
19 |
20 | lateinit var dayNameViews: List
21 | lateinit var dayViews: List
22 | lateinit var tempViews: List
23 |
24 | lateinit var transformation: Transformation
25 |
26 | var celsius: String? = null
27 | var fahrenheit: String? = null
28 |
29 | public constructor(context: Context) : super(context) {
30 | init(context)
31 | }
32 |
33 | public constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
34 | init(context)
35 | }
36 |
37 | private fun init(context: Context) {
38 | LayoutInflater.from(context).inflate(R.layout.view_weather_for_week, this, true)
39 |
40 | celsius = context.getString(R.string.celcius)
41 | fahrenheit = context.getString(R.string.fahrenheit)
42 | }
43 |
44 | override fun onFinishInflate() {
45 | super.onFinishInflate()
46 |
47 | dayNameViews = arrayListOf(day_name_1, day_name_2, day_name_3, day_name_4, day_name_5)
48 | dayViews = arrayListOf(day_1, day_2, day_3, day_4, day_5)
49 | tempViews = arrayListOf(temp_1, temp_2, temp_3, temp_4, temp_5)
50 | }
51 |
52 | public fun setWeatherForWeek(weatherList: List, useCelsius: Boolean, transformation: Transformation) {
53 | this.transformation = transformation
54 |
55 | for (i in weatherList.indices) {
56 | val v = dayViews[i]
57 | val weather = weatherList[i]
58 |
59 | try {
60 | val date = Date.valueOf(weather.date)
61 | val weekDay = DateUtils.formatDateTime(context, date.time, DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY)
62 | dayNameViews[i].text = weekDay
63 | } catch (e: IllegalArgumentException) {
64 | dayNameViews[i].text = ""
65 | }
66 |
67 | if (useCelsius) {
68 | tempViews[i].text = "${weather.tempMinC}-${weather.tempMaxC}${context!!.getString(R.string.celcius)}"
69 | } else {
70 | tempViews[i].text = "${weather.tempMinF}-${weather.tempMaxF}${context!!.getString(R.string.fahrenheit)}"
71 | }
72 |
73 | val urls = weather.weatherIconUrl
74 | if (urls?.isNotEmpty() ?: false) {
75 | val url = urls?.get(0)
76 | if (!TextUtils.isEmpty(url?.value)) {
77 | Picasso.with(context).load(url?.value).transform(transformation).into(v)
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ico_menu_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-hdpi/ico_menu_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ico_menu_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-mdpi/ico_menu_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ico_menu_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xhdpi/ico_menu_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ico_menu_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xxhdpi/ico_menu_small.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/card.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 |
17 |
18 | -
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | -
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_main.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_weather.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
12 |
13 |
18 |
19 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_city_weather.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
12 |
13 |
17 |
18 |
22 |
23 |
25 |
26 |
35 |
36 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_place_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
15 |
16 |
21 |
22 |
32 |
33 |
39 |
40 |
41 |
42 |
47 |
48 |
58 |
59 |
65 |
66 |
67 |
68 |
73 |
74 |
84 |
85 |
91 |
92 |
93 |
94 |
101 |
102 |
108 |
109 |
117 |
118 |
124 |
125 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_search_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_weather_for_week.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
12 |
13 |
17 |
18 |
21 |
22 |
26 |
27 |
28 |
29 |
33 |
34 |
38 |
39 |
42 |
43 |
47 |
48 |
49 |
50 |
54 |
55 |
59 |
60 |
63 |
64 |
68 |
69 |
70 |
71 |
75 |
76 |
80 |
81 |
84 |
85 |
89 |
90 |
91 |
92 |
96 |
97 |
101 |
102 |
105 |
106 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/item_place.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_debug.xml:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/ormlite_config.txt:
--------------------------------------------------------------------------------
1 | # --table-start--
2 | dataClass=io.dp.weather.app.db.table.Place
3 | tableName=cities
4 | # --table-fields-start--
5 | # --field-start--
6 | fieldName=id
7 | generatedId=true
8 | # --field-end--
9 | # --field-start--
10 | fieldName=name
11 | # --field-end--
12 | # --field-start--
13 | fieldName=lat
14 | # --field-end--
15 | # --field-start--
16 | fieldName=lon
17 | # --field-end--
18 | # --table-fields-end--
19 | # --table-end--
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #00000000
4 |
5 | #ffffff
6 | #d4d4d4
7 | #dddddd
8 | #eee
9 |
10 | #ff9e129c
11 | #ff5e9e20
12 | #ff126fc6
13 | #fff98879
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 | 9sp
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Weather
5 | Hello world!
6 | Settings
7 | Add
8 | Remove
9 | Cannot find geo for specified location
10 |
11 | Something went wrong with adding
12 | new location
13 |
14 | wind
15 | pressure
16 | humidity
17 | undef
18 | %1$s kmph
19 | %1$s mph
20 | %1$d mmHg
21 | %1$d kPa
22 | C˚
23 | F˚
24 | DebugActivity
25 | Debug
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_activity_settings.xml:
--------------------------------------------------------------------------------
1 |
2 | Settings
3 |
4 | General
5 |
6 | Temperature units
7 | Celsius - ON, Fahrenheit - OFF
8 |
9 | Pressure units
10 | kPa - ON, mmHg - OFF
11 |
12 | Wind units
13 | mph - ON, kmph - OFF
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/pref_general.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
14 |
15 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/test/TestAndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/test/java/io/dp/weather/app/MockAppComponent.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import dagger.Component
4 | import io.dp.weather.app.fragment.WeatherFragmentTest
5 | import javax.inject.Singleton
6 |
7 | /**
8 | * Created by deepol on 11/09/15.
9 | */
10 | @Singleton
11 | @Component(modules = arrayOf(MockAppModule::class))
12 | interface MockAppComponent : AppComponent {
13 | fun inject(weatherFragmentTest: WeatherFragmentTest)
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/test/java/io/dp/weather/app/MockAppModule.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import android.location.Geocoder
7 | import android.preference.PreferenceManager
8 | import com.google.gson.Gson
9 | import com.google.gson.GsonBuilder
10 | import com.google.gson.reflect.TypeToken
11 | import com.squareup.otto.Bus
12 | import dagger.Module
13 | import dagger.Provides
14 | import io.dp.weather.app.annotation.CachePrefs
15 | import io.dp.weather.app.annotation.ConfigPrefs
16 | import io.dp.weather.app.annotation.IOSched
17 | import io.dp.weather.app.annotation.UISched
18 | import io.dp.weather.app.db.DatabaseHelper
19 | import io.dp.weather.app.net.PlacesApi
20 | import io.dp.weather.app.net.WeatherApi
21 | import io.dp.weather.app.net.dto.Forecast
22 | import org.mockito.Mockito.mock
23 | import retrofit.MockRestAdapter
24 | import retrofit.RequestInterceptor
25 | import retrofit.RestAdapter
26 | import retrofit.http.Query
27 | import rx.Observable
28 | import rx.Scheduler
29 | import rx.Subscriber
30 | import rx.schedulers.Schedulers
31 | import javax.inject.Singleton
32 |
33 | /**
34 | * Created by dp on 08/10/14.
35 | */
36 | @Module class MockAppModule(internal val app: WeatherApp) {
37 |
38 | inline fun Gson.fromJson(json: String) = this.fromJson(json, object: TypeToken() {}.type)
39 |
40 | @Provides @Singleton fun provideApplication(): Application {
41 | return app
42 | }
43 |
44 | @Provides @Singleton fun provideForecastApi(gson: Gson): WeatherApi {
45 | val b = RestAdapter.Builder()
46 |
47 | if (BuildConfig.DEBUG) {
48 | b.setLogLevel(RestAdapter.LogLevel.FULL)
49 | }
50 |
51 | b.setRequestInterceptor(object : RequestInterceptor {
52 | override fun intercept(request: RequestInterceptor.RequestFacade) {
53 | request.addQueryParam("key", BuildConfig.FORECAST_API_KEY)
54 | request.addQueryParam("format", "json")
55 | }
56 | })
57 |
58 | val restAdapter = b.setEndpoint(BuildConfig.FORECAST_API_URL).build()
59 |
60 | val mock = MockRestAdapter.from(restAdapter)
61 |
62 | val f = gson.fromJson(TestUtils.WEATHER_JSON)
63 | val mockWeatherApi = MockWeatherApi(f)
64 | return mock.create(WeatherApi::class.java, mockWeatherApi)
65 | }
66 |
67 | @Provides @Singleton fun providePlacesApi(): PlacesApi {
68 | val b = RestAdapter.Builder()
69 |
70 | if (BuildConfig.DEBUG) {
71 | b.setLogLevel(RestAdapter.LogLevel.FULL)
72 | }
73 |
74 | b.setRequestInterceptor(object : RequestInterceptor {
75 | override fun intercept(request: RequestInterceptor.RequestFacade) {
76 | request.addQueryParam("key", BuildConfig.PLACES_API_KEY)
77 | request.addQueryParam("sensor", "false")
78 | }
79 | })
80 |
81 | val restAdapter = b.setEndpoint(BuildConfig.PLACES_API_URL).build()
82 | return restAdapter.create(PlacesApi::class.java)
83 | }
84 |
85 | @Provides @Singleton fun provideGson(): Gson {
86 | return GsonBuilder().create()
87 | }
88 |
89 | @Provides @Singleton fun provideBus(): Bus {
90 | return Bus()
91 | }
92 |
93 | @Provides @Singleton fun provideGeocoder(): Geocoder {
94 | return mock(Geocoder::class.java)
95 | }
96 |
97 | @Provides @Singleton @ConfigPrefs fun provideConfigPrefs(): SharedPreferences {
98 | return PreferenceManager.getDefaultSharedPreferences(app)
99 | }
100 |
101 | @Provides @Singleton @CachePrefs fun provideCachePrefs(): SharedPreferences {
102 | return app.getSharedPreferences("cachePrefs", Context.MODE_PRIVATE)
103 | }
104 |
105 | @Provides @Singleton fun provideDatabaseHelper(app: Application): DatabaseHelper {
106 | return DatabaseHelper(app)
107 | }
108 |
109 | @Provides @Singleton @IOSched fun provideIoScheduler(): Scheduler {
110 | return Schedulers.immediate()
111 | }
112 |
113 | @Provides @Singleton @UISched fun provideUiScheduler(): Scheduler {
114 | return Schedulers.immediate()
115 | }
116 |
117 | class MockWeatherApi(val forecast: Forecast) : WeatherApi {
118 |
119 | override fun getForecast(@Query("q") params: String,
120 | @Query("num_of_days") days: Int): Observable {
121 | return Observable.create(object : Observable.OnSubscribe {
122 | override fun call(subscriber: Subscriber) {
123 | subscriber.onNext(forecast)
124 | subscriber.onCompleted()
125 | }
126 | })
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/app/src/test/java/io/dp/weather/app/TestApp.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | import org.robolectric.TestLifecycleApplication
4 | import java.lang.reflect.Method
5 |
6 | /**
7 | * Created by deepol on 10/09/15.
8 | */
9 | class TestApp : WeatherApp(), TestLifecycleApplication {
10 | override fun createComponent(): AppComponent {
11 | return DaggerMockAppComponent.builder().mockAppModule(MockAppModule(this)).build()
12 | }
13 |
14 | override fun beforeTest(method: Method) {
15 |
16 | }
17 |
18 | override fun prepareTest(test: Any) {
19 |
20 | }
21 |
22 | override fun afterTest(method: Method) {
23 |
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/test/java/io/dp/weather/app/TestUtils.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app
2 |
3 | /**
4 | * Created by dp on 10/10/14.
5 | */
6 | object TestUtils {
7 |
8 | val WEATHER_JSON = "{ \"data\": { \"current_condition\": [ {\"cloudcover\": \"50\", \"humidity\": \"72\", \"observation_time\": \"12:29 PM\", \"precipMM\": \"0.4\", \"pressure\": \"1004\", \"temp_C\": \"13\", \"temp_F\": \"55\", \"visibility\": \"10\", \"weatherCode\": \"116\", \"weatherDesc\": [ {\"value\": \"Partly Cloudy\" } ], \"weatherIconUrl\": [ {\"value\": \"http:\\/\\/cdn.worldweatheronline.net\\/images\\/wsymbols01_png_64\\/wsymbol_0002_sunny_intervals.png\" } ], \"winddir16Point\": \"WSW\", \"winddirDegree\": \"250\", \"windspeedKmph\": \"17\", \"windspeedMiles\": \"11\" } ], \"request\": [ {\"query\": \"Lat 53.34 and Lon -6.27\", \"type\": \"LatLon\" } ], \"weather\": [ {\"date\": \"2014-10-10\", \"precipMM\": \"1.9\", \"tempMaxC\": \"14\", \"tempMaxF\": \"56\", \"tempMinC\": \"8\", \"tempMinF\": \"46\", \"weatherCode\": \"113\", \"weatherDesc\": [ {\"value\": \"Sunny\" } ], \"weatherIconUrl\": [ {\"value\": \"http:\\/\\/cdn.worldweatheronline.net\\/images\\/wsymbols01_png_64\\/wsymbol_0001_sunny.png\" } ], \"winddir16Point\": \"SW\", \"winddirDegree\": \"227\", \"winddirection\": \"SW\", \"windspeedKmph\": \"19\", \"windspeedMiles\": \"12\" }, {\"date\": \"2014-10-11\", \"precipMM\": \"3.9\", \"tempMaxC\": \"15\", \"tempMaxF\": \"59\", \"tempMinC\": \"6\", \"tempMinF\": \"43\", \"weatherCode\": \"176\", \"weatherDesc\": [ {\"value\": \"Patchy rain nearby\" } ], \"weatherIconUrl\": [ {\"value\": \"http:\\/\\/cdn.worldweatheronline.net\\/images\\/wsymbols01_png_64\\/wsymbol_0009_light_rain_showers.png\" } ], \"winddir16Point\": \"W\", \"winddirDegree\": \"261\", \"winddirection\": \"W\", \"windspeedKmph\": \"19\", \"windspeedMiles\": \"12\" }, {\"date\": \"2014-10-12\", \"precipMM\": \"0.4\", \"tempMaxC\": \"16\", \"tempMaxF\": \"61\", \"tempMinC\": \"6\", \"tempMinF\": \"42\", \"weatherCode\": \"113\", \"weatherDesc\": [ {\"value\": \"Sunny\" } ], \"weatherIconUrl\": [ {\"value\": \"http:\\/\\/cdn.worldweatheronline.net\\/images\\/wsymbols01_png_64\\/wsymbol_0001_sunny.png\" } ], \"winddir16Point\": \"WSW\", \"winddirDegree\": \"241\", \"winddirection\": \"WSW\", \"windspeedKmph\": \"12\", \"windspeedMiles\": \"8\" }, {\"date\": \"2014-10-13\", \"precipMM\": \"0.0\", \"tempMaxC\": \"15\", \"tempMaxF\": \"60\", \"tempMinC\": \"5\", \"tempMinF\": \"41\", \"weatherCode\": \"113\", \"weatherDesc\": [ {\"value\": \"Sunny\" } ], \"weatherIconUrl\": [ {\"value\": \"http:\\/\\/cdn.worldweatheronline.net\\/images\\/wsymbols01_png_64\\/wsymbol_0001_sunny.png\" } ], \"winddir16Point\": \"W\", \"winddirDegree\": \"262\", \"winddirection\": \"W\", \"windspeedKmph\": \"11\", \"windspeedMiles\": \"7\" }, {\"date\": \"2014-10-14\", \"precipMM\": \"0.0\", \"tempMaxC\": \"14\", \"tempMaxF\": \"58\", \"tempMinC\": \"10\", \"tempMinF\": \"49\", \"weatherCode\": \"113\", \"weatherDesc\": [ {\"value\": \"Sunny\" } ], \"weatherIconUrl\": [ {\"value\": \"http:\\/\\/cdn.worldweatheronline.net\\/images\\/wsymbols01_png_64\\/wsymbol_0001_sunny.png\" } ], \"winddir16Point\": \"SE\", \"winddirDegree\": \"133\", \"winddirection\": \"SE\", \"windspeedKmph\": \"12\", \"windspeedMiles\": \"7\" } ] }}"
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/test/java/io/dp/weather/app/activity/MockActivity.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.activity
2 |
3 | import android.os.Bundle
4 | import com.trello.rxlifecycle.components.support.RxFragmentActivity
5 | import io.dp.weather.app.WeatherApp
6 | import io.dp.weather.app.activity.debug.DebugBusModule
7 |
8 | /**
9 | * Created by deepol on 11/09/15.
10 | */
11 | class MockActivity : RxFragmentActivity(), HasComponent {
12 | lateinit var myComponent: BaseActivityComponent
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | this.myComponent = createComponent()
17 | }
18 |
19 | override fun createComponent(): BaseActivityComponent {
20 | val app = application as WeatherApp
21 | val component = DaggerActivityComponent.builder().appComponent(app.component).activityModule(ActivityModule(this)).build()
22 |
23 | return component.plusSubComponent(DebugBusModule())
24 | }
25 |
26 | override fun getComponent(): BaseActivityComponent = myComponent
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/test/java/io/dp/weather/app/fragment/WeatherFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package io.dp.weather.app.fragment
2 |
3 | import android.location.Address
4 | import android.location.Geocoder
5 | import io.dp.weather.app.BuildConfig
6 | import io.dp.weather.app.MockAppComponent
7 | import io.dp.weather.app.TestApp
8 | import io.dp.weather.app.activity.MockActivity
9 | import io.dp.weather.app.db.DatabaseHelper
10 | import io.dp.weather.app.db.table.Place
11 | import io.dp.weather.app.event.AddPlaceEvent
12 | import io.dp.weather.app.event.DeletePlaceEvent
13 | import org.junit.After
14 | import org.junit.Assert.assertEquals
15 | import org.junit.Assert.assertNotNull
16 | import org.junit.Before
17 | import org.junit.Test
18 | import org.junit.runner.RunWith
19 | import org.mockito.Mockito.`when`
20 | import org.mockito.Mockito.mock
21 | import org.robolectric.RobolectricGradleTestRunner
22 | import org.robolectric.RuntimeEnvironment
23 | import org.robolectric.annotation.Config
24 | import org.robolectric.shadows.ShadowSQLiteConnection
25 | import org.robolectric.shadows.support.v4.SupportFragmentTestUtil
26 | import java.io.IOException
27 | import java.util.*
28 |
29 | /**
30 | * Created by dp on 10/10/14.
31 | */
32 | @RunWith(RobolectricGradleTestRunner::class)
33 | @Config(constants = BuildConfig::class,
34 | application = TestApp::class,
35 | manifest = "app/src/test/TestAndroidManifest.xml",
36 | resourceDir = "../main/res", sdk = intArrayOf(23))
37 | class WeatherFragmentTest {
38 | lateinit var geocoder: Geocoder
39 |
40 | lateinit var databaseHelper: DatabaseHelper
41 |
42 | @Before @Throws(Exception::class)
43 | fun setUp() {
44 | val app = (RuntimeEnvironment.application as TestApp)
45 |
46 | (app.component as MockAppComponent?)!!.inject(this)
47 | }
48 |
49 | @After fun tearDown() {
50 | ShadowSQLiteConnection.reset()
51 | }
52 |
53 | @Test @Throws(Exception::class)
54 | fun testAddRemovePlaceFragment() {
55 | val f = WeatherFragment.newInstance()
56 |
57 | SupportFragmentTestUtil.startFragment(f, MockActivity::class.java)
58 | assertNotNull(f.adapter)
59 |
60 | val placeName = "Shanghai"
61 |
62 | val address = mock(Address::class.java)
63 | `when`(address.latitude).thenReturn(-1.0)
64 | `when`(address.longitude).thenReturn(-1.0)
65 |
66 | val addresses = ArrayList()
67 | addresses.add(address)
68 |
69 | try {
70 | `when`(geocoder.getFromLocationName(placeName, 1)).thenReturn(addresses)
71 | } catch (e: IOException) {
72 | e.printStackTrace()
73 | }
74 |
75 | f.onAddPlace(AddPlaceEvent(placeName))
76 |
77 | val places = databaseHelper.getPlaceDao()!!.queryForEq(Place.NAME, placeName)
78 |
79 | assertEquals(1, places.size.toLong())
80 | assertEquals(placeName, places[0].name)
81 |
82 | assertEquals(5, f.adapter.itemCount.toLong())
83 |
84 | f.onDeletePlace(DeletePlaceEvent(1L))
85 | f.adapter.notifyDataSetChanged()
86 |
87 | val placeList = databaseHelper.getPlaceDao()!!.queryForAll()
88 | assertEquals(4, placeList.size.toLong())
89 | assertEquals(4, f.adapter.itemCount.toLong())
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | allprojects {
3 | repositories {
4 | jcenter()
5 | maven {
6 | url 'http://oss.sonatype.org/content/repositories/snapshots'
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Settings specified in this file will override any Gradle settings
5 | # configured through the IDE.
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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 10 15:27:10 PDT 2013
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------