From ee3c8501815c9c339b94b53884677d00fac66471 Mon Sep 17 00:00:00 2001 From: bernd32 Date: Tue, 25 Feb 2020 15:56:01 +0500 Subject: [PATCH] First commit --- .gitignore | 14 + .idea/codeStyles/Project.xml | 164 +++++++ .idea/dbnavigator.xml | 454 ++++++++++++++++++ .idea/gradle.xml | 16 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 12 + app/.gitignore | 1 + app/build.gradle | 61 +++ app/proguard-rules.pro | 21 + .../weatherdemo/ExampleInstrumentedTest.java | 27 ++ app/src/main/AndroidManifest.xml | 31 ++ app/src/main/ic_launcher-web.png | Bin 0 -> 15684 bytes .../com/bernd32/weatherdemo/Constants.java | 10 + .../weatherdemo/EndpointInterface.java | 32 ++ .../com/bernd32/weatherdemo/LocationsDao.java | 41 ++ .../weatherdemo/LocationsRoomDatabase.java | 56 +++ .../bernd32/weatherdemo/RetrofitAdapter.java | 46 ++ .../weatherdemo/models/UserLocation.java | 44 ++ .../weatherdemo/models/WeatherItem.java | 41 ++ .../weatherdemo/models/forecastdata/City.java | 54 +++ .../models/forecastdata/Clouds.java | 21 + .../models/forecastdata/Coord.java | 32 ++ .../models/forecastdata/ForecastData.java | 65 +++ .../weatherdemo/models/forecastdata/List.java | 98 ++++ .../weatherdemo/models/forecastdata/Main.java | 95 ++++ .../weatherdemo/models/forecastdata/Snow.java | 21 + .../weatherdemo/models/forecastdata/Sys.java | 21 + .../models/forecastdata/Weather.java | 54 +++ .../weatherdemo/models/forecastdata/Wind.java | 32 ++ .../models/weatherdata/Clouds.java | 21 + .../weatherdemo/models/weatherdata/Coord.java | 32 ++ .../weatherdemo/models/weatherdata/Main.java | 76 +++ .../weatherdemo/models/weatherdata/Sys.java | 76 +++ .../models/weatherdata/Weather.java | 54 +++ .../models/weatherdata/WeatherData.java | 144 ++++++ .../weatherdemo/models/weatherdata/Wind.java | 32 ++ .../presenter/CurrentConditionsPresenter.java | 116 +++++ .../presenter/ForecastPresenter.java | 173 +++++++ .../weatherdemo/ui/AddNewLocation.java | 223 +++++++++ .../ui/CurrentWeatherFragment.java | 168 +++++++ .../weatherdemo/ui/ForecastFragment.java | 103 ++++ .../bernd32/weatherdemo/ui/MainActivity.java | 166 +++++++ .../ui/TurnedOffLocationDialog.java | 55 +++ .../ui/adapters/LocationsRecyclerAdapter.java | 100 ++++ .../ui/adapters/RecyclerAdapter.java | 71 +++ .../ui/adapters/TabsPagerAdapter.java | 53 ++ .../weatherdemo/utils/LocationProvider.java | 119 +++++ .../weatherdemo/utils/PreferencesManager.java | 100 ++++ app/src/main/res/drawable-v24/ic_call.xml | 5 + .../drawable-v24/ic_launcher_foreground.xml | 34 ++ .../main/res/drawable/ic_add_black_24dp.xml | 9 + .../main/res/drawable/ic_call_black_24dp.xml | 4 + .../drawable/ic_info_outline_black_24dp.xml | 9 + .../res/drawable/ic_launcher_background.xml | 170 +++++++ .../drawable/ic_location_on_black_24dp.xml | 9 + .../res/drawable/ic_refresh_black_24dp.xml | 9 + .../res/drawable/ic_settings_black_24dp.xml | 9 + app/src/main/res/drawable/ic_wi_barometer.xml | 9 + app/src/main/res/drawable/ic_wi_day_windy.xml | 9 + app/src/main/res/drawable/ic_wi_humidity.xml | 9 + .../drawable/location_cardview_item_bg.xml | 8 + app/src/main/res/font/raleway_semibold.xml | 7 + .../res/layout/activity_add_new_location.xml | 68 +++ app/src/main/res/layout/activity_main.xml | 34 ++ app/src/main/res/layout/forecast_item.xml | 64 +++ .../res/layout/fragment_current_weather.xml | 106 ++++ app/src/main/res/layout/fragment_forecast.xml | 27 ++ app/src/main/res/layout/location_item.xml | 36 ++ app/src/main/res/layout/settings_activity.xml | 9 + .../main/res/menu/add_new_location_menu.xml | 7 + app/src/main/res/menu/main_menu.xml | 17 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1471 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1561 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3306 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1099 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1124 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2164 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2038 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2416 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4767 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3397 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 3924 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 7794 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4581 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 5721 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 10967 bytes app/src/main/res/values-ru/strings.xml | 33 ++ app/src/main/res/values/arrays.xml | 12 + app/src/main/res/values/colors.xml | 7 + app/src/main/res/values/dimens.xml | 8 + app/src/main/res/values/font_certs.xml | 17 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/preloaded_fonts.xml | 6 + app/src/main/res/values/strings.xml | 43 ++ app/src/main/res/values/styles.xml | 20 + .../main/res/xml/network_security_config.xml | 6 + .../bernd32/weatherdemo/ExampleUnitTest.java | 17 + .../utils/LocationProviderTest.java | 12 + build.gradle | 32 ++ gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++ gradlew.bat | 84 ++++ settings.gradle | 2 + 107 files changed, 4634 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/dbnavigator.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/bernd32/weatherdemo/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/java/com/bernd32/weatherdemo/Constants.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/EndpointInterface.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/LocationsDao.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/LocationsRoomDatabase.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/RetrofitAdapter.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/UserLocation.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/WeatherItem.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/City.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Clouds.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Coord.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/ForecastData.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/List.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Main.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Snow.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Sys.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Weather.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Wind.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Clouds.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Coord.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Main.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Sys.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Weather.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/WeatherData.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Wind.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/presenter/CurrentConditionsPresenter.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/presenter/ForecastPresenter.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/AddNewLocation.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/CurrentWeatherFragment.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/ForecastFragment.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/MainActivity.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/TurnedOffLocationDialog.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/adapters/LocationsRecyclerAdapter.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/adapters/RecyclerAdapter.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/ui/adapters/TabsPagerAdapter.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/utils/LocationProvider.java create mode 100644 app/src/main/java/com/bernd32/weatherdemo/utils/PreferencesManager.java create mode 100644 app/src/main/res/drawable-v24/ic_call.xml create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_add_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_call_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_info_outline_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_location_on_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_refresh_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_wi_barometer.xml create mode 100644 app/src/main/res/drawable/ic_wi_day_windy.xml create mode 100644 app/src/main/res/drawable/ic_wi_humidity.xml create mode 100644 app/src/main/res/drawable/location_cardview_item_bg.xml create mode 100644 app/src/main/res/font/raleway_semibold.xml create mode 100644 app/src/main/res/layout/activity_add_new_location.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/forecast_item.xml create mode 100644 app/src/main/res/layout/fragment_current_weather.xml create mode 100644 app/src/main/res/layout/fragment_forecast.xml create mode 100644 app/src/main/res/layout/location_item.xml create mode 100644 app/src/main/res/layout/settings_activity.xml create mode 100644 app/src/main/res/menu/add_new_location_menu.xml create mode 100644 app/src/main/res/menu/main_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/font_certs.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/preloaded_fonts.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 app/src/test/java/com/bernd32/weatherdemo/ExampleUnitTest.java create mode 100644 app/src/test/java/com/bernd32/weatherdemo/utils/LocationProviderTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..fb22c1d --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..1962184 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..d291b3d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b6ea2b1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a11b8b1 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,61 @@ +apply plugin: 'com.android.application' + +android { + testOptions { + unitTests.returnDefaultValues = true + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + compileSdkVersion 29 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "com.bernd32.weatherdemo" + minSdkVersion 24 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.preference:preference:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + implementation 'com.google.android.material:material:1.2.0-alpha03' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'com.squareup.retrofit2:retrofit:2.7.0' + implementation 'com.squareup.retrofit2:converter-gson:2.7.0' + implementation "com.squareup.retrofit2:adapter-rxjava2:2.4.0" + implementation "io.reactivex.rxjava2:rxandroid:2.1.0" + implementation "androidx.room:room-rxjava2:2.2.3" + // GSON body parser + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' + // RecyclerView + implementation 'androidx.recyclerview:recyclerview:1.1.0' + // Glide + implementation 'com.github.bumptech.glide:glide:4.10.0' + annotationProcessor 'com.github.bumptech.glide:glide:4.10.0' + implementation 'com.google.android.gms:play-services-location:17.0.0' + implementation 'com.facebook.stetho:stetho:1.5.1' + // Room components + implementation "androidx.room:room-runtime:$rootProject.roomVersion" + annotationProcessor "androidx.room:room-compiler:$rootProject.roomVersion" + androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion" + // debugImplementation because LeakCanary should only run in debug builds. + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/bernd32/weatherdemo/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/bernd32/weatherdemo/ExampleInstrumentedTest.java new file mode 100644 index 0000000..dc2f281 --- /dev/null +++ b/app/src/androidTest/java/com/bernd32/weatherdemo/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.bernd32.weatherdemo; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.bernd32.weatherdemo", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..304b0b9 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..08815072c25c08edd74da511122fd7e9cacb99c0 GIT binary patch literal 15684 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/bernd32/weatherdemo/Constants.java b/app/src/main/java/com/bernd32/weatherdemo/Constants.java new file mode 100644 index 0000000..70145e8 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/Constants.java @@ -0,0 +1,10 @@ +package com.bernd32.weatherdemo; + +/** + * Constants for our API + */ +public class Constants { + public static final String BASE_URL = "http://api.openweathermap.org"; + public static final String API_KEY = "8296eaaa0ef012da8f776aa0ec890817"; + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/EndpointInterface.java b/app/src/main/java/com/bernd32/weatherdemo/EndpointInterface.java new file mode 100644 index 0000000..696f5d4 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/EndpointInterface.java @@ -0,0 +1,32 @@ +package com.bernd32.weatherdemo; + +import com.bernd32.weatherdemo.models.forecastdata.ForecastData; +import com.bernd32.weatherdemo.models.weatherdata.WeatherData; + +import io.reactivex.Observable; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * Here we define the Retrofit endpoints with Observable return types to use it later + * in RxJava. + */ + +public interface EndpointInterface { + + @GET("data/2.5/weather") + Observable getCurrentConditions(@Query("units") String units, + @Query("APPID") String apiKey, + @Query("q") String city, + @Query("lang") String lang, + @Query("lat") String lat, + @Query("lon") String lon); + + @GET("data/2.5/forecast") + Observable getForecast(@Query("units") String units, + @Query("APPID") String apiKey, + @Query("q") String city, + @Query("lang") String lang, + @Query("lat") String lat, + @Query("lon") String lon); +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/LocationsDao.java b/app/src/main/java/com/bernd32/weatherdemo/LocationsDao.java new file mode 100644 index 0000000..443427a --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/LocationsDao.java @@ -0,0 +1,41 @@ +package com.bernd32.weatherdemo; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.bernd32.weatherdemo.models.UserLocation; + +import java.util.List; + +import io.reactivex.Flowable; + +/** + * This class is used for accessing the database by defining DAO methods + * https://developer.android.com/training/data-storage/room/accessing-data + */ + +@Dao +public interface LocationsDao { + + // Notifies its active observers when the data has changed. + @Query("SELECT * FROM locations") + Flowable> getAllLocations(); + + @Query("SELECT * from locations ORDER BY id DESC") + Flowable> getLocationsById(); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insert(UserLocation location); + + @Query("DELETE FROM locations") + void deleteAll(); + + @Delete + void deleteLocation(UserLocation userLocation); + + @Query("DELETE FROM locations WHERE id = :id") + void deleteLocationById(int id); +} \ No newline at end of file diff --git a/app/src/main/java/com/bernd32/weatherdemo/LocationsRoomDatabase.java b/app/src/main/java/com/bernd32/weatherdemo/LocationsRoomDatabase.java new file mode 100644 index 0000000..5bc26ef --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/LocationsRoomDatabase.java @@ -0,0 +1,56 @@ +package com.bernd32.weatherdemo; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import com.bernd32.weatherdemo.models.UserLocation; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Database(entities = {UserLocation.class}, version = 2, exportSchema = false) +public abstract class LocationsRoomDatabase extends RoomDatabase { + + public abstract LocationsDao locationsDao(); + + // marking the instance as volatile to ensure atomic access to the variable + private static volatile LocationsRoomDatabase INSTANCE; + private static final int NUMBER_OF_THREADS = 4; + public static final ExecutorService databaseWriteExecutor = + Executors.newFixedThreadPool(NUMBER_OF_THREADS); + + public static LocationsRoomDatabase getDatabase(final Context context) { + if (INSTANCE == null) { + synchronized (LocationsRoomDatabase.class) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context.getApplicationContext(), + LocationsRoomDatabase.class, "locations_database") + .fallbackToDestructiveMigration() + .addCallback(sRoomDatabaseCallback) + .build(); + } + } + } + return INSTANCE; + } + + /** + * Override the onOpen method to populate the database. + * For this sample, we clear the database every time it is created or opened. + * + * If you want to populate the database only when the database is created for the 1st time, + * override RoomDatabase.Callback()#onCreate + */ + private static Callback sRoomDatabaseCallback = new Callback() { + @Override + public void onOpen(@NonNull SupportSQLiteDatabase db) { + super.onOpen(db); + } + }; +} + diff --git a/app/src/main/java/com/bernd32/weatherdemo/RetrofitAdapter.java b/app/src/main/java/com/bernd32/weatherdemo/RetrofitAdapter.java new file mode 100644 index 0000000..01de40a --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/RetrofitAdapter.java @@ -0,0 +1,46 @@ +package com.bernd32.weatherdemo; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +import static com.bernd32.weatherdemo.Constants.BASE_URL; + +/** + * This class supplies an instance of Retrofit adapter + */ + +public class RetrofitAdapter { + + private static Retrofit retrofit; + private static Gson gson; + + public static Retrofit getInstance() { + + if (retrofit == null) { + if (gson == null) { + gson = new GsonBuilder().setLenient().create(); + } + + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); + logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .addInterceptor(logging) + .build(); + + retrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(okHttpClient) + .build(); + } + return retrofit; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/UserLocation.java b/app/src/main/java/com/bernd32/weatherdemo/models/UserLocation.java new file mode 100644 index 0000000..807dc09 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/UserLocation.java @@ -0,0 +1,44 @@ +package com.bernd32.weatherdemo.models; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +/** + * Model for Room + */ + +@Entity(tableName = "locations") +public class UserLocation { + + @PrimaryKey(autoGenerate = true) + private Integer id; + + @NonNull + @ColumnInfo(name = "city") + private String city; + + + + public UserLocation(@NonNull String city) { + this.city = city; + } + + @NonNull + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/WeatherItem.java b/app/src/main/java/com/bernd32/weatherdemo/models/WeatherItem.java new file mode 100644 index 0000000..e27f208 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/WeatherItem.java @@ -0,0 +1,41 @@ +package com.bernd32.weatherdemo.models; + +/** + * Model for RecyclerView + */ + +public class WeatherItem { + + private int mImgResource; + private String mTitle; + private String mValue; + private String imgUrl; + + public WeatherItem(int mImgResource, String mTitle, String mValue) { + this.mImgResource = mImgResource; + this.mTitle = mTitle; + this.mValue = mValue; + } + + public WeatherItem(String imgUrl, String mTitle, String mValue) { + this.imgUrl = imgUrl; + this.mTitle = mTitle; + this.mValue = mValue; + } + + public String getImgUrl() { + return imgUrl; + } + + public int getImgResource() { + return mImgResource; + } + + public String getTitle() { + return mTitle; + } + + public String getValue() { + return mValue; + } +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/City.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/City.java new file mode 100644 index 0000000..2317c9f --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/City.java @@ -0,0 +1,54 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class City { + + @SerializedName("id") + @Expose + private Integer id; + @SerializedName("name") + @Expose + private String name; + @SerializedName("coord") + @Expose + private Coord coord; + @SerializedName("country") + @Expose + private String country; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Coord getCoord() { + return coord; + } + + public void setCoord(Coord coord) { + this.coord = coord; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Clouds.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Clouds.java new file mode 100644 index 0000000..f1e0390 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Clouds.java @@ -0,0 +1,21 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Clouds { + + @SerializedName("all") + @Expose + private Integer all; + + public Integer getAll() { + return all; + } + + public void setAll(Integer all) { + this.all = all; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Coord.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Coord.java new file mode 100644 index 0000000..acadf2a --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Coord.java @@ -0,0 +1,32 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Coord { + + @SerializedName("lat") + @Expose + private Double lat; + @SerializedName("lon") + @Expose + private Double lon; + + public Double getLat() { + return lat; + } + + public void setLat(Double lat) { + this.lat = lat; + } + + public Double getLon() { + return lon; + } + + public void setLon(Double lon) { + this.lon = lon; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/ForecastData.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/ForecastData.java new file mode 100644 index 0000000..cfebd64 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/ForecastData.java @@ -0,0 +1,65 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class ForecastData { + + @SerializedName("cod") + @Expose + private String cod; + @SerializedName("message") + @Expose + private Double message; + @SerializedName("cnt") + @Expose + private Integer cnt; + @SerializedName("list") + @Expose + private java.util.List list = null; + @SerializedName("city") + @Expose + private City city; + + public String getCod() { + return cod; + } + + public void setCod(String cod) { + this.cod = cod; + } + + public Double getMessage() { + return message; + } + + public void setMessage(Double message) { + this.message = message; + } + + public Integer getCnt() { + return cnt; + } + + public void setCnt(Integer cnt) { + this.cnt = cnt; + } + + public java.util.List getList() { + return list; + } + + public void setList(java.util.List list) { + this.list = list; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/List.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/List.java new file mode 100644 index 0000000..641c82c --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/List.java @@ -0,0 +1,98 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class List { + + @SerializedName("dt") + @Expose + private Integer dt; + @SerializedName("main") + @Expose + private Main main; + @SerializedName("weather") + @Expose + private java.util.List weather = null; + @SerializedName("clouds") + @Expose + private Clouds clouds; + @SerializedName("wind") + @Expose + private Wind wind; + @SerializedName("snow") + @Expose + private Snow snow; + @SerializedName("sys") + @Expose + private Sys sys; + @SerializedName("dt_txt") + @Expose + private String dtTxt; + + public Integer getDt() { + return dt; + } + + public void setDt(Integer dt) { + this.dt = dt; + } + + public Main getMain() { + return main; + } + + public void setMain(Main main) { + this.main = main; + } + + public java.util.List getWeather() { + return weather; + } + + public void setWeather(java.util.List weather) { + this.weather = weather; + } + + public Clouds getClouds() { + return clouds; + } + + public void setClouds(Clouds clouds) { + this.clouds = clouds; + } + + public Wind getWind() { + return wind; + } + + public void setWind(Wind wind) { + this.wind = wind; + } + + public Snow getSnow() { + return snow; + } + + public void setSnow(Snow snow) { + this.snow = snow; + } + + public Sys getSys() { + return sys; + } + + public void setSys(Sys sys) { + this.sys = sys; + } + + public String getDtTxt() { + return dtTxt; + } + + public void setDtTxt(String dtTxt) { + this.dtTxt = dtTxt; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Main.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Main.java new file mode 100644 index 0000000..1f43f56 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Main.java @@ -0,0 +1,95 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Main { + + @SerializedName("temp") + @Expose + private Double temp; + @SerializedName("temp_min") + @Expose + private Double tempMin; + @SerializedName("temp_max") + @Expose + private Double tempMax; + @SerializedName("pressure") + @Expose + private Integer pressure; + @SerializedName("sea_level") + @Expose + private Double seaLevel; + @SerializedName("grnd_level") + @Expose + private Integer grndLevel; + @SerializedName("humidity") + @Expose + private Integer humidity; + @SerializedName("temp_kf") + @Expose + private Double tempKf; + + public Double getTemp() { + return temp; + } + + public void setTemp(Double temp) { + this.temp = temp; + } + + public Double getTempMin() { + return tempMin; + } + + public void setTempMin(Double tempMin) { + this.tempMin = tempMin; + } + + public Double getTempMax() { + return tempMax; + } + + public void setTempMax(Double tempMax) { + this.tempMax = tempMax; + } + + public Integer getPressure() { + return pressure; + } + + public void setPressure(Integer pressure) { + this.pressure = pressure; + } + + public Double getSeaLevel() { + return seaLevel; + } + + public void setSeaLevel(Double seaLevel) { + this.seaLevel = seaLevel; + } + + public Integer getGrndLevel() { + return grndLevel; + } + + public void setGrndLevel(Integer grndLevel) { + this.grndLevel = grndLevel; + } + + public Integer getHumidity() { + return humidity; + } + + public void setHumidity(Integer humidity) { + this.humidity = humidity; + } + + public Double getTempKf() { + return tempKf; + } + + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Snow.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Snow.java new file mode 100644 index 0000000..6aa9143 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Snow.java @@ -0,0 +1,21 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Snow { + + @SerializedName("3h") + @Expose + private Double _3h; + + public Double get3h() { + return _3h; + } + + public void set3h(Double _3h) { + this._3h = _3h; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Sys.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Sys.java new file mode 100644 index 0000000..659a24f --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Sys.java @@ -0,0 +1,21 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Sys { + + @SerializedName("pod") + @Expose + private String pod; + + public String getPod() { + return pod; + } + + public void setPod(String pod) { + this.pod = pod; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Weather.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Weather.java new file mode 100644 index 0000000..a2d8e51 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Weather.java @@ -0,0 +1,54 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Weather { + + @SerializedName("id") + @Expose + private Integer id; + @SerializedName("main") + @Expose + private String main; + @SerializedName("description") + @Expose + private String description; + @SerializedName("icon") + @Expose + private String icon; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getMain() { + return main; + } + + public void setMain(String main) { + this.main = main; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Wind.java b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Wind.java new file mode 100644 index 0000000..5b0f457 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/forecastdata/Wind.java @@ -0,0 +1,32 @@ + +package com.bernd32.weatherdemo.models.forecastdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Wind { + + @SerializedName("speed") + @Expose + private Double speed; + @SerializedName("deg") + @Expose + private Double deg; + + public Double getSpeed() { + return speed; + } + + public void setSpeed(Double speed) { + this.speed = speed; + } + + public Double getDeg() { + return deg; + } + + public void setDeg(Double deg) { + this.deg = deg; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Clouds.java b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Clouds.java new file mode 100644 index 0000000..2348e3c --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Clouds.java @@ -0,0 +1,21 @@ + +package com.bernd32.weatherdemo.models.weatherdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Clouds { + + @SerializedName("all") + @Expose + private Integer all; + + public Integer getAll() { + return all; + } + + public void setAll(Integer all) { + this.all = all; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Coord.java b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Coord.java new file mode 100644 index 0000000..a85d70e --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Coord.java @@ -0,0 +1,32 @@ + +package com.bernd32.weatherdemo.models.weatherdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Coord { + + @SerializedName("lon") + @Expose + private Double lon; + @SerializedName("lat") + @Expose + private Double lat; + + public Double getLon() { + return lon; + } + + public void setLon(Double lon) { + this.lon = lon; + } + + public Double getLat() { + return lat; + } + + public void setLat(Double lat) { + this.lat = lat; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Main.java b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Main.java new file mode 100644 index 0000000..60851ca --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Main.java @@ -0,0 +1,76 @@ + +package com.bernd32.weatherdemo.models.weatherdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Main { + + @SerializedName("temp") + @Expose + private Double temp; + @SerializedName("feels_like") + @Expose + private Double feelsLike; + @SerializedName("temp_min") + @Expose + private Double tempMin; + @SerializedName("temp_max") + @Expose + private Double tempMax; + @SerializedName("pressure") + @Expose + private Integer pressure; + @SerializedName("humidity") + @Expose + private Integer humidity; + + public Double getTemp() { + return temp; + } + + public void setTemp(Double temp) { + this.temp = temp; + } + + public Double getFeelsLike() { + return feelsLike; + } + + public void setFeelsLike(Double feelsLike) { + this.feelsLike = feelsLike; + } + + public Double getTempMin() { + return tempMin; + } + + public void setTempMin(Double tempMin) { + this.tempMin = tempMin; + } + + public Double getTempMax() { + return tempMax; + } + + public void setTempMax(Double tempMax) { + this.tempMax = tempMax; + } + + public Integer getPressure() { + return pressure; + } + + public void setPressure(Integer pressure) { + this.pressure = pressure; + } + + public Integer getHumidity() { + return humidity; + } + + public void setHumidity(Integer humidity) { + this.humidity = humidity; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Sys.java b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Sys.java new file mode 100644 index 0000000..b4c341f --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Sys.java @@ -0,0 +1,76 @@ + +package com.bernd32.weatherdemo.models.weatherdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Sys { + + @SerializedName("type") + @Expose + private Integer type; + @SerializedName("id") + @Expose + private Integer id; + @SerializedName("message") + @Expose + private Double message; + @SerializedName("country") + @Expose + private String country; + @SerializedName("sunrise") + @Expose + private Integer sunrise; + @SerializedName("sunset") + @Expose + private Integer sunset; + + public Integer getType() { + return type; + } + + public void setType(Integer type) { + this.type = type; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Double getMessage() { + return message; + } + + public void setMessage(Double message) { + this.message = message; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public Integer getSunrise() { + return sunrise; + } + + public void setSunrise(Integer sunrise) { + this.sunrise = sunrise; + } + + public Integer getSunset() { + return sunset; + } + + public void setSunset(Integer sunset) { + this.sunset = sunset; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Weather.java b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Weather.java new file mode 100644 index 0000000..6fa1381 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Weather.java @@ -0,0 +1,54 @@ + +package com.bernd32.weatherdemo.models.weatherdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Weather { + + @SerializedName("id") + @Expose + private Integer id; + @SerializedName("main") + @Expose + private String main; + @SerializedName("description") + @Expose + private String description; + @SerializedName("icon") + @Expose + private String icon; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getMain() { + return main; + } + + public void setMain(String main) { + this.main = main; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/WeatherData.java b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/WeatherData.java new file mode 100644 index 0000000..837f6c2 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/WeatherData.java @@ -0,0 +1,144 @@ + +package com.bernd32.weatherdemo.models.weatherdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class WeatherData { + + @SerializedName("coord") + @Expose + private Coord coord; + @SerializedName("weather") + @Expose + private List weather = null; + @SerializedName("base") + @Expose + private String base; + @SerializedName("main") + @Expose + private Main main; + @SerializedName("wind") + @Expose + private Wind wind; + @SerializedName("clouds") + @Expose + private Clouds clouds; + @SerializedName("dt") + @Expose + private Integer dt; + @SerializedName("sys") + @Expose + private Sys sys; + @SerializedName("timezone") + @Expose + private Integer timezone; + @SerializedName("id") + @Expose + private Integer id; + @SerializedName("name") + @Expose + private String name; + @SerializedName("cod") + @Expose + private Integer cod; + + public Coord getCoord() { + return coord; + } + + public void setCoord(Coord coord) { + this.coord = coord; + } + + public List getWeather() { + return weather; + } + + public void setWeather(List weather) { + this.weather = weather; + } + + public String getBase() { + return base; + } + + public void setBase(String base) { + this.base = base; + } + + public Main getMain() { + return main; + } + + public void setMain(Main main) { + this.main = main; + } + + public Wind getWind() { + return wind; + } + + public void setWind(Wind wind) { + this.wind = wind; + } + + public Clouds getClouds() { + return clouds; + } + + public void setClouds(Clouds clouds) { + this.clouds = clouds; + } + + public Integer getDt() { + return dt; + } + + public void setDt(Integer dt) { + this.dt = dt; + } + + public Sys getSys() { + return sys; + } + + public void setSys(Sys sys) { + this.sys = sys; + } + + public Integer getTimezone() { + return timezone; + } + + public void setTimezone(Integer timezone) { + this.timezone = timezone; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getCod() { + return cod; + } + + public void setCod(Integer cod) { + this.cod = cod; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Wind.java b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Wind.java new file mode 100644 index 0000000..2cfc7f5 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/models/weatherdata/Wind.java @@ -0,0 +1,32 @@ + +package com.bernd32.weatherdemo.models.weatherdata; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Wind { + + @SerializedName("speed") + @Expose + private Double speed; + @SerializedName("deg") + @Expose + private Double deg; + + public Double getSpeed() { + return speed; + } + + public void setSpeed(Double speed) { + this.speed = speed; + } + + public Double getDeg() { + return deg; + } + + public void setDeg(Double deg) { + this.deg = deg; + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/presenter/CurrentConditionsPresenter.java b/app/src/main/java/com/bernd32/weatherdemo/presenter/CurrentConditionsPresenter.java new file mode 100644 index 0000000..786ea99 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/presenter/CurrentConditionsPresenter.java @@ -0,0 +1,116 @@ +package com.bernd32.weatherdemo.presenter; + +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +import com.bernd32.weatherdemo.EndpointInterface; +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.RetrofitAdapter; +import com.bernd32.weatherdemo.models.weatherdata.WeatherData; + +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import retrofit2.Retrofit; + +import static com.bernd32.weatherdemo.Constants.API_KEY; + +/** + * Here we get current conditions information by calling our weather API, + * and then update our UI via the callback interface + */ + +public class CurrentConditionsPresenter { + + private View mView; + private Context mContext; + private static final String TAG = "CurrentConditionsPresenter"; + + public CurrentConditionsPresenter(View view, Context context) { + mView = view; + mContext = context; + } + + public void getCurrentConditions(String latitude, String longitude, String city) { + Log.d(TAG, "getCurrentConditions: started"); + Log.d(TAG, "getCurrentConditions: lat/long=" + latitude + "/"+ longitude); + Log.d(TAG, "getCurrentConditions: city=" + city); + Retrofit retrofit = RetrofitAdapter.getInstance(); + EndpointInterface apiService = retrofit.create(EndpointInterface.class); + // Format latitude and longitude values +/* String formattedLat = new DecimalFormat("###.###").format(latitude); + String formattedLon = new DecimalFormat("###.###").format(longitude);*/ + + // Get the observable Weather object + apiService.getCurrentConditions("metric", API_KEY, city, + Locale.getDefault().getLanguage(), latitude, longitude) + .throttleFirst(10, TimeUnit.MINUTES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe(__ -> mView.showProgressBar(true)) + .doOnTerminate(() -> mView.showProgressBar(false)) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onNext(WeatherData weatherData) { + String location = String.format("%s, %s", + weatherData.getName(), weatherData.getSys().getCountry()); + + String url = String.format("http://openweathermap.org/img/wn/%s@2x.png", + weatherData.getWeather().get(0).getIcon()); + + String temperature = String.format(Locale.getDefault(), "%d%s", + Math.round(weatherData.getMain().getTemp()), "°C"); + + String pressure = String.format(Locale.getDefault(), "%d %s", + weatherData.getMain().getPressure(), + mContext.getString(R.string.pressure_unit)); + + String humidity = String.format(Locale.getDefault(), "%d %%", + weatherData.getMain().getHumidity()); + + String wind = String.format(Locale.getDefault(), "%.2f %s", + weatherData.getWind().getSpeed(), + mContext.getString(R.string.meters_per_second)); + + mView.updateTemperature(temperature); + mView.updateLocation(location); + mView.updateWeatherText(weatherData.getWeather().get(0).getDescription()); + mView.updateIcon(url); + mView.updateDetails(pressure, wind, humidity); + } + + @Override + public void onError(Throwable t) { + Log.e(TAG, "onError: ", t.fillInStackTrace()); + Log.e(TAG, "onError: ", t.getCause()); + mView.showError(t); + } + + @Override + public void onComplete() { + Log.d(TAG, "onComplete: "); + } + }); + } + + public interface View { + void updateTemperature(String temp); + void updateLocation(String location); + void updateWeatherText(String weatherText); + void updateIcon(String url); + void updateDetails(String pressure, String wind, String humidity); + void showProgressBar(boolean show); + void showError(Throwable t); + } +} + + diff --git a/app/src/main/java/com/bernd32/weatherdemo/presenter/ForecastPresenter.java b/app/src/main/java/com/bernd32/weatherdemo/presenter/ForecastPresenter.java new file mode 100644 index 0000000..6f0c517 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/presenter/ForecastPresenter.java @@ -0,0 +1,173 @@ +package com.bernd32.weatherdemo.presenter; + +import android.util.Log; + +import com.bernd32.weatherdemo.EndpointInterface; +import com.bernd32.weatherdemo.RetrofitAdapter; +import com.bernd32.weatherdemo.models.forecastdata.ForecastData; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import retrofit2.Retrofit; + +import static com.bernd32.weatherdemo.Constants.API_KEY; + +/** + * Here we get current conditions information by calling our weather API, + * and then update our UI via the callback interface. We also performing + * a lot of formatting to make everything look clean :) + */ + +public class ForecastPresenter { + + private static final String TAG = "ForecastPresenter"; + private View mView; + private List mDates = new ArrayList<>(); + private List mMaxTempAll = new ArrayList<>(); + private List mMinTempAll = new ArrayList<>(); + private List mDescriptions = new ArrayList<>(); + private List mImgUrls = new ArrayList<>(); + + + public ForecastPresenter(View view) { + this.mView = view; + } + + public void getForecast(String latitude, String longitude, String city) { + Log.d(TAG, "getForecast: started"); + Retrofit retrofit = RetrofitAdapter.getInstance(); + EndpointInterface apiService = retrofit.create(EndpointInterface.class); + apiService.getForecast("metric", API_KEY, city, + Locale.getDefault().getLanguage(), latitude, longitude) + .throttleFirst(10, TimeUnit.MINUTES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onNext(ForecastData forecastData) { + forecastDataHandle(forecastData); + } + + @Override + public void onError(Throwable e) { + Log.d(TAG, "onError: started"); + Log.e(TAG, "onError: ", e.fillInStackTrace()); + Log.e(TAG, "onError: ", e.getCause()); + } + + @Override + public void onComplete() { + + } + }); + + } + + private void forecastDataHandle(ForecastData forecastData) { + // Populate array list with values emitted from rxjava + int size= forecastData.getList().size(); + for (int i = 0; i < size; i++) { + mMaxTempAll.add(forecastData.getList().get(i).getMain().getTempMax()); + mMinTempAll.add(forecastData.getList().get(i).getMain().getTempMin()); + mDates.add(formatUnixTime(forecastData.getList().get(i).getDt())); + mDescriptions.add(forecastData.getList().get(i).getWeather().get(0).getDescription()); + mImgUrls.add(formatImgUrl(forecastData.getList().get(i).getWeather().get(0).getIcon())); + } + Log.d(TAG, "forecastDataHandle: mImgUrls = " + mImgUrls); + // We get 5 day / 3 hour forecast data, 40 items in total + // But we don't need such precise forecast, we just need + // to get 5 day / 1 day forecast, with min and max temps. + // But first we should calculate an arithmetic mean for the + // min/max temperature and add it to result array + final List maxTempAvg = getMaxAverage(mMaxTempAll, 8); + final List minTempAvg = getMinAverage(mMinTempAll, 8); + final List avgMinMax = formatMinMaxValues(maxTempAvg, minTempAvg); + // Now we need to take every 8th element from mTemperatures, mDates, etc. + final List dates = takeEveryNthElement(mDates, 8); + final List descriptions = takeEveryNthElement(mDescriptions, 8); + final List imgUrls = takeEveryNthElement(mImgUrls, 8); + // Send all of this to the UI + mView.updateForecast(avgMinMax, dates, descriptions, imgUrls); + } + + private List formatMinMaxValues(List maxTempAvg, List minTempAvg) { + List result = new ArrayList<>(); + for (int i = 0; i getMinAverage(List list, int offset) { + List temp = new ArrayList<>(); + for (int i = 0; i < list.size(); i += offset) { + int toIndex = i + offset; + if (toIndex > list.size()) break; + temp.add(findMin(list.subList(i, toIndex))); + } + return temp; + } + + private List getMaxAverage(List list, int offset) { + List temp = new ArrayList<>(); + for (int i = 0; i < list.size(); i += offset) { + int toIndex = i + offset; + if (toIndex > list.size()) break; + temp.add(findMax(list.subList(i, toIndex))); + } + return temp; + } + + private List takeEveryNthElement(List list, int nth) { + return IntStream.range(0, list.size()) + .filter(n -> n % nth == 0) + .mapToObj(list::get) + .collect(Collectors.toList()); + } + + private double findMin(List subList) { + return subList.stream().mapToDouble(val -> val).min().orElse(0.0); + } + + private double findMax(List subList) { + return subList.stream().mapToDouble(val -> val).max().orElse(0.0); + } + + private String formatImgUrl(String iconCode) { + return String.format("http://openweathermap.org/img/wn/%s@2x.png", iconCode); + } + + private String formatUnixTime(Integer unixSeconds) { + String[] locale = {Locale.getDefault().getLanguage(), Locale.getDefault().getCountry()}; + Date date = new java.util.Date(unixSeconds*1000L); + SimpleDateFormat sdf = new java.text.SimpleDateFormat( + "EEEE dd MMMM", + new Locale(locale[0], locale[1])); + sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT+5")); + return sdf.format(date); + } + + + public interface View { + void updateForecast(List avgMinMax, List mDates, + List mDescriptions, List mImgUrls); + } +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/AddNewLocation.java b/app/src/main/java/com/bernd32/weatherdemo/ui/AddNewLocation.java new file mode 100644 index 0000000..5042f3c --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/AddNewLocation.java @@ -0,0 +1,223 @@ +package com.bernd32.weatherdemo.ui; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.weatherdemo.LocationsDao; +import com.bernd32.weatherdemo.LocationsRoomDatabase; +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.models.UserLocation; +import com.bernd32.weatherdemo.ui.adapters.LocationsRecyclerAdapter; +import com.facebook.stetho.Stetho; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.functions.Consumer; +import io.reactivex.observers.DisposableCompletableObserver; +import io.reactivex.schedulers.Schedulers; + +/** + * In this activity user can add a new location manually or choose existing locations. + * Also here we load/save this data in a local database using Room + */ + +public class AddNewLocation extends AppCompatActivity { + + private static final String TAG = "AddNewLocation"; + private RecyclerView mRecyclerView; + private LocationsDao mLocationsDao; + private EditText cityEdit; + private CompositeDisposable mDisposable; + private LocationsRecyclerAdapter adapter; + private LocationsRoomDatabase db; + private Button addButton; + private TextView emptyMsg; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_add_new_location); + Stetho.initializeWithDefaults(this); + AddNewLocation.this.setTitle(getString(R.string.add_new_location)); + cityEdit = findViewById(R.id.city_edit_text); + addButton = findViewById(R.id.button_add); + emptyMsg = findViewById(R.id.empty_message); + mRecyclerView = findViewById(R.id.recycler_view_locations); + mRecyclerView.setHasFixedSize(true); + adapter = new LocationsRecyclerAdapter(this, new ArrayList<>()); + mRecyclerView.setAdapter(adapter); + RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(this); + mRecyclerView.setLayoutManager(mLayoutManager); + + db = LocationsRoomDatabase.getDatabase(this); + mLocationsDao = db.locationsDao(); + mDisposable = new CompositeDisposable(); + addButton.setEnabled(false); + cityEdit.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if(s.toString().trim().length()==0){ + addButton.setEnabled(false); + } else { + addButton.setEnabled(true); + } + } + + @Override + public void afterTextChanged(Editable s) { } + }); + + mDisposable.add( + mLocationsDao.getAllLocations() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer>() { + @Override + public void accept(List userLocations) throws Exception { + adapter.addItems(userLocations); + // Show a message if the list is empty + emptyMsg.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + })); + + deleteItemBySwipe(); + } + + private void deleteItemBySwipe() { + ItemTouchHelper helper = new ItemTouchHelper( + new ItemTouchHelper.SimpleCallback(0, + ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + return false; + } + + @Override + // Delete an item from the database. + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + UserLocation location = adapter.getItem(position); + Toast.makeText(AddNewLocation.this, + getString(R.string.delete_item_msg), + Toast.LENGTH_SHORT).show(); + deleteLocation(location); + } + }); + // Attach the item touch helper to the recycler view + helper.attachToRecyclerView(mRecyclerView); + } + + private void deleteLocation(UserLocation location) { + Completable.fromAction(() -> { + mLocationsDao.deleteLocation(location); + }) + .subscribeOn(Schedulers.io()) + .subscribe(new DisposableCompletableObserver() { + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + Log.e(TAG, "onError: ", e.fillInStackTrace()); + showInformationDialog(getString(R.string.error), e.getLocalizedMessage()); + } + }); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mDisposable.dispose(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.add_new_location_menu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.delete_all) { + deleteAllItems(); + Toast.makeText(this, getString(R.string.all_items_deleted_msg), Toast.LENGTH_SHORT).show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void deleteAllItems() { + Completable.fromAction(() -> { + mLocationsDao.deleteAll(); + }) + .subscribeOn(Schedulers.io()) + .subscribe(new DisposableCompletableObserver() { + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + Log.e(TAG, "onError: ", e.fillInStackTrace()); + showInformationDialog(getString(R.string.error), e.getLocalizedMessage()); + } + }); + } + + public void onAddButton(View view) { + String city = cityEdit.getText().toString(); + + Completable.fromAction(() -> { + UserLocation userLocation = new UserLocation(city); + LocationsRoomDatabase.databaseWriteExecutor.execute(() -> { + mLocationsDao.insert(userLocation); + }); + }).subscribe(new DisposableCompletableObserver() { + @Override + public void onComplete() { + Log.d(TAG, "onComplete: done!"); + } + + @Override + public void onError(Throwable e) { + Log.e(TAG, "onError: ", e.fillInStackTrace()); + } + }); + } + + public void showInformationDialog(String title, String message) { + AlertDialog.Builder builder = new AlertDialog.Builder(AddNewLocation.this); + builder.setTitle(title) + .setMessage(message) + .setIcon(R.drawable.ic_info_outline_black_24dp) + .setCancelable(true) + .setNegativeButton("OK", (dialog, id) -> dialog.cancel()); + AlertDialog alert = builder.create(); + alert.show(); + } +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/CurrentWeatherFragment.java b/app/src/main/java/com/bernd32/weatherdemo/ui/CurrentWeatherFragment.java new file mode 100644 index 0000000..e413540 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/CurrentWeatherFragment.java @@ -0,0 +1,168 @@ +package com.bernd32.weatherdemo.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.models.WeatherItem; +import com.bernd32.weatherdemo.presenter.CurrentConditionsPresenter; +import com.bernd32.weatherdemo.ui.adapters.RecyclerAdapter; +import com.bernd32.weatherdemo.utils.PreferencesManager; +import com.bumptech.glide.Glide; + +import java.util.ArrayList; + +/** + * Here we display current weather data by overriding callback interface methods + * sent by presenter.CurrentConditionsPresenter + */ + +public class CurrentWeatherFragment extends Fragment implements CurrentConditionsPresenter.View { + + private static final String TAG = "CurrentWeatherFragment"; + + private TextView mCity, mTemp, mWeatherText; + private ImageView mIcon; + private RecyclerView mRecyclerView; + private ProgressBar mProgressBar; + private ArrayList mItems = new ArrayList<>(); + private String mImgUrl; + + public CurrentWeatherFragment() { + // Required empty public constructor + } + + /** + * @return A new instance of fragment CurrentWeatherFragment. + */ + public static CurrentWeatherFragment newInstance() { + return new CurrentWeatherFragment(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + Log.d(TAG, "onSaveInstanceState: start"); + outState.putString("image_url", mImgUrl); + super.onSaveInstanceState(outState); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate: started"); + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (savedInstanceState == null) { + getData(); + } else { + updateIcon(mImgUrl); + } + } + + private void getData() { + // Read longitude and latitude values from PrefManager + PreferencesManager.initializeInstance(getContext()); + PreferencesManager prefManager = PreferencesManager.getInstance(); + String longitude = prefManager.getLongitude(); + String latitude = prefManager.getLatitude(); + String city = prefManager.getCity(); + // Invoke the Presenter + CurrentConditionsPresenter conditionsPresenter = new CurrentConditionsPresenter(this, getContext()); + conditionsPresenter.getCurrentConditions(latitude, longitude, city); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Log.d(TAG, "onCreateView: started"); + // Inflate the layout for this fragment + View root = inflater.inflate(R.layout.fragment_current_weather, container, false); + mCity = root.findViewById(R.id.city_tv); + mTemp = root.findViewById(R.id.temperature_tv); + mWeatherText = root.findViewById(R.id.weatherText); + mIcon = root.findViewById(R.id.weatherIcon); + mProgressBar = root.findViewById(R.id.progressBar); + mRecyclerView = root.findViewById(R.id.recyclerView); + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setAdapter(new RecyclerAdapter(getContext(), mItems)); + RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getContext()); + mRecyclerView.setLayoutManager(mLayoutManager); + + return root; + } + + @Override + public void updateTemperature(String temp) { + mTemp.setText(temp); + } + + @Override + public void updateLocation(String location) { + mCity.setText(location); + } + + @Override + public void updateWeatherText(String weatherText) { + mWeatherText.setText(weatherText); + } + + @Override + public void updateIcon(String url) { + mImgUrl = url; + Glide.with(this).load(mImgUrl).into(mIcon); + } + + @Override + public void updateDetails(String pressure, String wind, String humidity) { + // Clear old information + mItems.clear(); + + mItems.add(new WeatherItem(R.drawable.ic_wi_barometer, getString(R.string.pressure), pressure)); + mItems.add(new WeatherItem(R.drawable.ic_wi_day_windy, getString(R.string.wind), wind)); + mItems.add(new WeatherItem(R.drawable.ic_wi_humidity, getString(R.string.humidity), humidity)); + + saveItems(mItems); + } + + @Override + public void showProgressBar(boolean show) { + if (show) { + mProgressBar.setVisibility(View.VISIBLE); + } else { + mProgressBar.setVisibility(View.INVISIBLE); + } + } + + @Override + public void showError(Throwable t) { + String title = getString(R.string.error); + String message = t.getLocalizedMessage(); + Activity act = getActivity(); + if (act instanceof MainActivity) { + ((MainActivity) act).showInformationDialog(title, message); + } + } + + private void saveItems(ArrayList mItems) { + RecyclerView.Adapter mAdapter = new RecyclerAdapter(getContext(), mItems); + mRecyclerView.setAdapter(mAdapter); + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/ForecastFragment.java b/app/src/main/java/com/bernd32/weatherdemo/ui/ForecastFragment.java new file mode 100644 index 0000000..e029f89 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/ForecastFragment.java @@ -0,0 +1,103 @@ +package com.bernd32.weatherdemo.ui; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.models.WeatherItem; +import com.bernd32.weatherdemo.presenter.ForecastPresenter; +import com.bernd32.weatherdemo.ui.adapters.RecyclerAdapter; +import com.bernd32.weatherdemo.utils.PreferencesManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Here we display forecast data by overriding callback interface methods + * sent by presenter.ForecastPresenter + */ + +public class ForecastFragment extends Fragment implements ForecastPresenter.View{ + + private static final String TAG = "ForecastFragment"; + private ArrayList mItems = new ArrayList<>(); + private RecyclerView mRecyclerView; + + public ForecastFragment() { + // Required empty public constructor + } + + /** + * @return A new instance of fragment ForecastFragment. + */ + public static ForecastFragment newInstance() { + return new ForecastFragment(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "onCreate: started"); + setRetainInstance(true); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (savedInstanceState == null) { + getData(); + } + } + + private void getData() { + // Read longitude and latitude values from PrefManager + PreferencesManager.initializeInstance(getContext()); + PreferencesManager prefManager = PreferencesManager.getInstance(); + String longitude = prefManager.getLongitude(); + String latitude = prefManager.getLatitude(); + String city = prefManager.getCity(); + // Invoke the Presenter + ForecastPresenter forecastPresenter = new ForecastPresenter(this); + forecastPresenter.getForecast(latitude, longitude, city); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View root = inflater.inflate(R.layout.fragment_forecast, container, false); + mRecyclerView = root.findViewById(R.id.recyclerView2); + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setAdapter(new RecyclerAdapter(getContext(), mItems)); + RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getContext()); + mRecyclerView.setLayoutManager(mLayoutManager); + + + + return root; + } + + private void saveItems(ArrayList mItems) { + RecyclerView.Adapter mAdapter = new RecyclerAdapter(getContext(), mItems); + mRecyclerView.setAdapter(mAdapter); + } + + @Override + public void updateForecast(List avgMinMax, List mDates, + List mDescriptions, List mImgUrls) { + mItems.clear(); + for (int i = 0; mDates.size() > i; i++) { + mItems.add(new WeatherItem(mImgUrls.get(i), mDates.get(i), avgMinMax.get(i))); + } + Log.d(TAG, "updateForecast: mImgUrls" + mImgUrls); + saveItems(mItems); + } +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/MainActivity.java b/app/src/main/java/com/bernd32/weatherdemo/ui/MainActivity.java new file mode 100644 index 0000000..3a6db0b --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/MainActivity.java @@ -0,0 +1,166 @@ +package com.bernd32.weatherdemo.ui; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.location.Location; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.viewpager.widget.ViewPager; + +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.ui.adapters.TabsPagerAdapter; +import com.bernd32.weatherdemo.utils.LocationProvider; +import com.bernd32.weatherdemo.utils.PreferencesManager; +import com.google.android.material.tabs.TabLayout; + +import static com.bernd32.weatherdemo.utils.LocationProvider.PERMISSION_ID; + +/** + * Choose what parameters (city name or location coordinates) we're using to call + * the weather API. Save params to PrefManager to use it across the app. Use + * utils.LocationProvider to get user's latitude and longitude if we're using + * location coordinates. Setup the UI (tabs, viewpager and fragments). + */ + +public class MainActivity extends AppCompatActivity implements LocationProvider.Callback{ + + private static final String TAG = "MainActivity"; + private PreferencesManager prefManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate: started"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + PreferencesManager.initializeInstance(this); + prefManager = PreferencesManager.getInstance(); + if (savedInstanceState == null) { + prefManager.clear(); + } + + startApplication(); + } + + private void startApplication() { + /* + If we have a city value in Intent then it means that user selected + a location from AddNewLocation activity. In this case we just initialize + the UI and use location name as a parameter + */ + if (getIntent().hasExtra("city")) { + prefManager.saveCity(getIntent().getStringExtra("city")); + initUI(); + } else { + /* + Otherwise, we request an user location data (latitude and longitude) and + use them as parameters. To achieve that we get user coordinates from the + LocationProvider and then return results via Callback interface by overriding + setResult() method. + */ + LocationProvider locationProvider = new LocationProvider(this, this); + locationProvider.getLastLocation(); + } + } + + // Start the app after we got permission result + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PERMISSION_ID) {// If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "onRequestPermissionsResult: granted"); + // Permission was granted, yay! + startApplication(); + } else { + Log.d(TAG, "onRequestPermissionsResult: denied"); + // permission denied :( Open AddNewLocation activity where user + // can add a location for weather manually + startActivity(new Intent(this, AddNewLocation.class)); + } + return; + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()){ + case R.id.open_saved_locations: + startActivity(new Intent(this, AddNewLocation.class)); + return true; + case R.id.update: + startApplication(); + Toast.makeText(this, getString(R.string.updated_msg), Toast.LENGTH_SHORT).show(); + return true; + case R.id.about_dialog: + showInformationDialog( + getString(R.string.about), + getString(R.string.about_message) + ); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void setResult(Location location) { + // Get location from LocationProvider.Callback interface + double lat = location.getLatitude(); + double lon = location.getLongitude(); + Log.d(TAG, "setResult: started"); + Log.d(TAG, "onNext: lat = " + lat); + Log.d(TAG, "onNext: long = " + lon); + + // Save longitude and latitude values via PrefManager + prefManager.saveLatitude(String.valueOf(lat)); + prefManager.saveLongitude(String.valueOf(lon)); + + // Show tab fragments + initUI(); + } + + @Override + public void locationTurnedOff() { + // Show an alert dialog if location is turned off + FragmentManager fm = getSupportFragmentManager(); + TurnedOffLocationDialog alertDialog = TurnedOffLocationDialog.newInstance(); + alertDialog.show(fm, "fragment_alert"); + } + + private void initUI() { + // Show tab fragments + TabsPagerAdapter tabsPagerAdapter = new TabsPagerAdapter(this, getSupportFragmentManager()); + ViewPager viewPager = findViewById(R.id.view_pager); + viewPager.setAdapter(tabsPagerAdapter); + TabLayout tabs = findViewById(R.id.tabs); + tabs.setupWithViewPager(viewPager); + } + + public void showInformationDialog(String title, String message) { + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); + builder.setTitle(title) + .setMessage(message) + .setIcon(R.drawable.ic_info_outline_black_24dp) + .setCancelable(true) + .setNegativeButton("OK", (dialog, id) -> dialog.cancel()); + AlertDialog alert = builder.create(); + alert.show(); + } +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/TurnedOffLocationDialog.java b/app/src/main/java/com/bernd32/weatherdemo/ui/TurnedOffLocationDialog.java new file mode 100644 index 0000000..1f35368 --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/TurnedOffLocationDialog.java @@ -0,0 +1,55 @@ +package com.bernd32.weatherdemo.ui; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +import com.bernd32.weatherdemo.R; + +/** + * This dialog fragment is showed if user's location if turned off + */ + +public class TurnedOffLocationDialog extends DialogFragment { + + private static final String TAG = "TurnedOffLocationDialog"; + + public TurnedOffLocationDialog() { + } + + public static TurnedOffLocationDialog newInstance() { + TurnedOffLocationDialog frag = new TurnedOffLocationDialog(); + return frag; + } + + @Override + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + Log.d(TAG, "onCreateDialog: started"); + // Use the Builder class for convenient dialog construction + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(R.string.location_turned_off) + .setPositiveButton(R.string.turn_on_location, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + startActivity(intent); + } + }) + .setNegativeButton(R.string.enter_a_city, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + startActivity(new Intent(getContext(), AddNewLocation.class)); + } + }); + // Create the AlertDialog object and return it + return builder.create(); + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/LocationsRecyclerAdapter.java b/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/LocationsRecyclerAdapter.java new file mode 100644 index 0000000..fe140ec --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/LocationsRecyclerAdapter.java @@ -0,0 +1,100 @@ +package com.bernd32.weatherdemo.ui.adapters; + +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.models.UserLocation; +import com.bernd32.weatherdemo.ui.MainActivity; +import com.google.android.material.card.MaterialCardView; + +import java.util.List; + +/** + * RecyclerView adapter used in ui.AddNewLocation activity + */ + +public class LocationsRecyclerAdapter extends RecyclerView.Adapter { + + private List mItems; + private Context mContext; + private static final String TAG = "LocationsRecyclerAdapter"; + + public LocationsRecyclerAdapter(Context context, List items) { + this.mItems = items; + this.mContext = context; + } + + public void addItems(List postItems) { + mItems.addAll(postItems); + mItems = postItems; + notifyDataSetChanged(); + } + + public void clear() { + mItems.clear(); + notifyDataSetChanged(); + } + + public UserLocation getItem(int position) { + return mItems.get(position); + } + + @NonNull + @Override + public LocationsRecyclerAdapter.BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.location_item, parent, false); + return new BaseViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) { + UserLocation currentItem = mItems.get(position); + + // Since we have location names in getCity(), we should capitalize the first letter + String cityName = currentItem.getCity(); + String capitalizedCityName = cityName.substring(0, 1).toUpperCase() + cityName.substring(1); + + holder.itemValue.setText(capitalizedCityName); + + holder.parentLayout.setOnClickListener(view -> { + Intent intent = new Intent(mContext, MainActivity.class); + intent.putExtra("city", holder.itemValue.getText().toString()); + mContext.startActivity(intent); + }); + + // Show a tooltip on a long click + holder.parentLayout.setOnLongClickListener(view -> { + Toast.makeText(mContext, mContext.getString(R.string.tooltip_msg), Toast.LENGTH_SHORT).show(); + return true; + }); + } + + @Override + public int getItemCount() { + return mItems == null ? 0 : mItems.size(); + } + + + public static class BaseViewHolder extends RecyclerView.ViewHolder { + + private TextView itemValue; + MaterialCardView parentLayout; + + BaseViewHolder(@NonNull View itemView) { + super(itemView); + itemValue = itemView.findViewById(R.id.item_value); + parentLayout = itemView.findViewById(R.id.parent_layout); + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/RecyclerAdapter.java b/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/RecyclerAdapter.java new file mode 100644 index 0000000..3d126cf --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/RecyclerAdapter.java @@ -0,0 +1,71 @@ +package com.bernd32.weatherdemo.ui.adapters; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.models.WeatherItem; +import com.bumptech.glide.Glide; + +import java.util.ArrayList; + +/** + * RecyclerView adapter used in our fragments (ui.CurrentWeatherFragment and ui.ForecastFragment) + */ + +public class RecyclerAdapter extends RecyclerView.Adapter { + + private ArrayList mWeatherItems; + private Context mContext; + private static final String TAG = "RecyclerAdapter"; + + public RecyclerAdapter(Context context, ArrayList items) { + this.mWeatherItems = items; + this.mContext = context; + } + + public static class BaseViewHolder extends RecyclerView.ViewHolder { + + private ImageView imageView; + private TextView titleText, valueText; + + BaseViewHolder(@NonNull View itemView) { + super(itemView); + imageView = itemView.findViewById(R.id.image); + titleText = itemView.findViewById(R.id.card_title); + valueText = itemView.findViewById(R.id.card_value); + } + } + + @NonNull + @Override + public RecyclerAdapter.BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_item, parent, false); + return new BaseViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) { + WeatherItem currentItem = mWeatherItems.get(position); + holder.imageView.setImageResource(currentItem.getImgResource()); + if (currentItem.getImgResource() != 0) { + holder.imageView.setImageResource(currentItem.getImgResource()); + } else if (currentItem.getImgUrl() != null) { + Glide.with(mContext).load(currentItem.getImgUrl()).into(holder.imageView); + } + holder.titleText.setText(currentItem.getTitle()); + holder.valueText.setText(currentItem.getValue()); + } + + @Override + public int getItemCount() { + return mWeatherItems.size(); + } +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/TabsPagerAdapter.java b/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/TabsPagerAdapter.java new file mode 100644 index 0000000..936d3ff --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/ui/adapters/TabsPagerAdapter.java @@ -0,0 +1,53 @@ +package com.bernd32.weatherdemo.ui.adapters; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + +import com.bernd32.weatherdemo.R; +import com.bernd32.weatherdemo.ui.CurrentWeatherFragment; +import com.bernd32.weatherdemo.ui.ForecastFragment; + +/** + * A [FragmentPagerAdapter] that returns a fragment corresponding to + * one of the sections/tabs/pages. + */ +public class TabsPagerAdapter extends FragmentPagerAdapter { + + @StringRes + private static final int[] TAB_TITLES = + new int[]{R.string.tab_text_1, R.string.tab_text_2}; + private final Context mContext; + + public TabsPagerAdapter(Context context, FragmentManager fm) { + super(fm); + mContext = context; + } + + @NonNull + @Override + public Fragment getItem(int position) { + // getItem is called to instantiate the fragment for the given page. + if (position == 0) { + return CurrentWeatherFragment.newInstance(); + } else { + return ForecastFragment.newInstance(); + } + } + + @Nullable + @Override + public CharSequence getPageTitle(int position) { + return mContext.getResources().getString(TAB_TITLES[position]); + } + + @Override + public int getCount() { + return 2; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bernd32/weatherdemo/utils/LocationProvider.java b/app/src/main/java/com/bernd32/weatherdemo/utils/LocationProvider.java new file mode 100644 index 0000000..fec09fe --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/utils/LocationProvider.java @@ -0,0 +1,119 @@ +package com.bernd32.weatherdemo.utils; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationManager; +import android.os.Looper; +import android.util.Log; + +import androidx.core.app.ActivityCompat; + +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; + +/** + * This class provides user's location coordinates, it is also checking permissions + * and requesting location data. Part of the code were taken from here: + * https://www.androdocs.com/java/getting-current-location-latitude-longitude-in-android-using-java.html + */ + +public class LocationProvider { + + public static final int PERMISSION_ID = 44; + private FusedLocationProviderClient mFusedLocationClient; + private Context mContext; + private Callback mCallback; + private static final String TAG = "LocationProvider"; + + public LocationProvider(Context context, Callback callback) { + this.mCallback = callback; + this.mContext = context; + mFusedLocationClient = LocationServices.getFusedLocationProviderClient(mContext); + } + + public void getLastLocation(){ + Log.d(TAG, "getLastLocation: started"); + if (checkPermissions()) { + if (isLocationEnabled()) { + mFusedLocationClient.getLastLocation().addOnCompleteListener( + task -> { + Location location = task.getResult(); + if (location == null) { + LocationProvider.this.requestNewLocationData(); + } else { + mCallback.setResult(location); + } + } + ); + } else { + mCallback.locationTurnedOff(); + } + } else { + requestPermissions(); + } + } + + + @SuppressLint("MissingPermission") + private void requestNewLocationData(){ + Log.d(TAG, "requestNewLocationData: "); + LocationRequest mLocationRequest = new LocationRequest(); + mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + mLocationRequest.setInterval(0); + mLocationRequest.setFastestInterval(0); + mLocationRequest.setNumUpdates(1); + + mFusedLocationClient = LocationServices.getFusedLocationProviderClient(mContext); + mFusedLocationClient.requestLocationUpdates( + mLocationRequest, mLocationCallback, + Looper.myLooper() + ); + + } + + private LocationCallback mLocationCallback = new LocationCallback() { + @Override + public void onLocationResult(LocationResult locationResult) { + Log.d(TAG, "onLocationResult: started"); + Location mLocation = locationResult.getLastLocation(); + mCallback.setResult(mLocation); + } + }; + + private boolean checkPermissions() { + return ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + } + + private void requestPermissions() { + Log.d(TAG, "requestPermissions: "); + ActivityCompat.requestPermissions( + (Activity) mContext, + new String[]{ + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION + }, + PERMISSION_ID + ); + } + + private boolean isLocationEnabled() { + LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled( + LocationManager.NETWORK_PROVIDER + ); + } + + public interface Callback { + void setResult(Location location); + void locationTurnedOff(); + } + +} diff --git a/app/src/main/java/com/bernd32/weatherdemo/utils/PreferencesManager.java b/app/src/main/java/com/bernd32/weatherdemo/utils/PreferencesManager.java new file mode 100644 index 0000000..d20ea8c --- /dev/null +++ b/app/src/main/java/com/bernd32/weatherdemo/utils/PreferencesManager.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.weatherdemo.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * PrefManager utility to save/load/clear data + */ + +public class PreferencesManager { + + /* a thread-safe singleton class to make the global access method + synchronized, so that only one thread can execute this method at a time */ + + private static final String APP_PREFS = "com.bernd32.jlyrics.prefs"; + private static final String LAT = "com.example.app.lat"; + private static final String LON = "com.example.app.lon"; + private static final String CITY = "com.example.app.city"; + + + private static PreferencesManager sInstance; + private final SharedPreferences mPref; + + private PreferencesManager(Context context) { + mPref = context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE); + } + + public static synchronized void initializeInstance(Context context) { + if (sInstance == null) { + sInstance = new PreferencesManager(context); + } + } + + public static synchronized PreferencesManager getInstance() { + if (sInstance == null) { + throw new IllegalStateException(PreferencesManager.class.getSimpleName() + + " is not initialized, call initializeInstance(..) method first."); + } + return sInstance; + } + + public void saveCity(String value) { + mPref.edit() + .putString(CITY, value) + .apply(); + } + + public String getCity() { + return mPref.getString(CITY, ""); + } + + public void saveLatitude(String value) { + mPref.edit() + .putString(LAT, value) + .apply(); + } + + public void saveLongitude(String value) { + mPref.edit() + .putString(LON, value) + .apply(); + } + + + public String getLatitude() { + return mPref.getString(LAT, ""); + } + + public String getLongitude() { + return mPref.getString(LON, ""); + } + + public void remove(String key) { + mPref.edit() + .remove(key) + .apply(); + } + + public boolean clear() { + return mPref.edit() + .clear() + .commit(); + } +} diff --git a/app/src/main/res/drawable-v24/ic_call.xml b/app/src/main/res/drawable-v24/ic_call.xml new file mode 100644 index 0000000..60d03c8 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_call.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_add_black_24dp.xml b/app/src/main/res/drawable/ic_add_black_24dp.xml new file mode 100644 index 0000000..0258249 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_call_black_24dp.xml b/app/src/main/res/drawable/ic_call_black_24dp.xml new file mode 100644 index 0000000..fbe7fcc --- /dev/null +++ b/app/src/main/res/drawable/ic_call_black_24dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_outline_black_24dp.xml b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml new file mode 100644 index 0000000..cf53e14 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_location_on_black_24dp.xml b/app/src/main/res/drawable/ic_location_on_black_24dp.xml new file mode 100644 index 0000000..e3291a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_on_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100644 index 0000000..8229a9a --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_black_24dp.xml b/app/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 0000000..24a5623 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wi_barometer.xml b/app/src/main/res/drawable/ic_wi_barometer.xml new file mode 100644 index 0000000..a098132 --- /dev/null +++ b/app/src/main/res/drawable/ic_wi_barometer.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wi_day_windy.xml b/app/src/main/res/drawable/ic_wi_day_windy.xml new file mode 100644 index 0000000..7fe1779 --- /dev/null +++ b/app/src/main/res/drawable/ic_wi_day_windy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wi_humidity.xml b/app/src/main/res/drawable/ic_wi_humidity.xml new file mode 100644 index 0000000..0cdc9dc --- /dev/null +++ b/app/src/main/res/drawable/ic_wi_humidity.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/location_cardview_item_bg.xml b/app/src/main/res/drawable/location_cardview_item_bg.xml new file mode 100644 index 0000000..e110a2c --- /dev/null +++ b/app/src/main/res/drawable/location_cardview_item_bg.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/font/raleway_semibold.xml b/app/src/main/res/font/raleway_semibold.xml new file mode 100644 index 0000000..39cde5a --- /dev/null +++ b/app/src/main/res/font/raleway_semibold.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_add_new_location.xml b/app/src/main/res/layout/activity_add_new_location.xml new file mode 100644 index 0000000..c536a60 --- /dev/null +++ b/app/src/main/res/layout/activity_add_new_location.xml @@ -0,0 +1,68 @@ + + + + + + + +