commit ee3c8501815c9c339b94b53884677d00fac66471 Author: bernd32 Date: Tue Feb 25 15:56:01 2020 +0500 First commit 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 0000000..0881507 Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ 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 @@ + + + + + + + +