activityInjector() {
43 | return dispatchingAndroidInjector;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/NetworkBoundResource.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data;
2 |
3 | import androidx.annotation.MainThread;
4 | import androidx.annotation.NonNull;
5 | import androidx.annotation.Nullable;
6 | import androidx.annotation.WorkerThread;
7 | import androidx.lifecycle.LiveData;
8 | import androidx.lifecycle.MediatorLiveData;
9 | import ezike.tobenna.myweather.data.remote.api.ApiResponse;
10 | import ezike.tobenna.myweather.utils.AppExecutors;
11 | import ezike.tobenna.myweather.utils.Resource;
12 | import timber.log.Timber;
13 |
14 | /**
15 | * A generic class that can provide a resource backed by both the sqlite database and the network.
16 | *
17 | *
18 | * You can read more about it in the [Architecture Guide]
19 | * (https://developer.android.com/jetpack/docs/guide#addendum).
20 | *
21 | * @param
22 | * @param
23 | */
24 | public abstract class NetworkBoundResource {
25 |
26 | private final AppExecutors mExecutors;
27 |
28 | private final MediatorLiveData> result = new MediatorLiveData<>();
29 |
30 | @MainThread
31 | public NetworkBoundResource(AppExecutors appExecutors) {
32 | mExecutors = appExecutors;
33 | result.setValue(Resource.loading(null));
34 | final LiveData dbSource = loadFromDb();
35 | result.addSource(dbSource, data -> {
36 | result.removeSource(dbSource);
37 | if (shouldFetch(data)) {
38 | fetchFromNetwork(dbSource);
39 | } else {
40 | result.addSource(dbSource, newData -> setValue(Resource.success(newData)));
41 | }
42 | });
43 | }
44 |
45 | /**
46 | * Fetch the data from network and persist into DB and then
47 | * send it back to UI.
48 | */
49 | private void fetchFromNetwork(final LiveData dbSource) {
50 | final LiveData> apiResponse = createCall();
51 | // we re-attach dbSource as a new source, it will dispatch its latest value quickly
52 | result.addSource(dbSource, newData -> setValue(Resource.loading(newData)));
53 | result.addSource(apiResponse, response -> {
54 | result.removeSource(apiResponse);
55 | result.removeSource(dbSource);
56 | if (response.isSuccessful()) {
57 | mExecutors.diskIO().execute(() -> {
58 | if (response.body != null) {
59 | saveCallResult(response.body);
60 | }
61 | mExecutors.mainThread().execute(() -> {
62 | // we specially request a new live data,
63 | // otherwise we will get immediately last cached value,
64 | // which may not be updated with latest results received from network.
65 | result.addSource(loadFromDb(), newData -> setValue(Resource.success(newData)));
66 | });
67 | });
68 | } else {
69 | onFetchFailed();
70 | Timber.d("error %s", response.getError().getMessage());
71 | result.addSource(dbSource, newData -> setValue(Resource.error(response.getError().getMessage(), newData)));
72 | }
73 | });
74 | }
75 |
76 | @MainThread
77 | private void setValue(Resource newValue) {
78 | if (result.getValue() != newValue) {
79 | result.setValue(newValue);
80 | }
81 | }
82 |
83 | // Called to save the result of the API response into the database.
84 | @WorkerThread
85 | protected abstract void saveCallResult(@NonNull RequestType item);
86 |
87 | // Called with the data in the database to decide whether to fetch
88 | // potentially updated data from the network.
89 | @MainThread
90 | protected abstract boolean shouldFetch(@Nullable ResultType data);
91 |
92 | // Called to get the cached data from the database.
93 | @NonNull
94 | @MainThread
95 | protected abstract LiveData loadFromDb();
96 |
97 | // Called to create the API call.
98 | @NonNull
99 | @MainThread
100 | protected abstract LiveData> createCall();
101 |
102 | // Called when the fetch fails. The child class may want to reset components
103 | // like rate limiter.
104 | @MainThread
105 | protected abstract void onFetchFailed();
106 |
107 | // Returns a LiveData object that represents the resource that's implemented
108 | // in the base class.
109 | public final LiveData> asLiveData() {
110 | return result;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/local/LocalDataSource.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.local;
2 |
3 | import ezike.tobenna.myweather.data.source.BaseSource;
4 |
5 | public interface LocalDataSource extends BaseSource {
6 |
7 | void save(K k);
8 |
9 | boolean hasLocationChanged(K k);
10 |
11 | boolean shouldFetch(K k);
12 |
13 | @Override
14 | V get();
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/local/LocalDataSourceImpl.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.local;
2 |
3 | import org.threeten.bp.ZonedDateTime;
4 |
5 | import javax.inject.Inject;
6 |
7 | import androidx.lifecycle.LiveData;
8 | import ezike.tobenna.myweather.data.local.dao.WeatherDao;
9 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
10 | import ezike.tobenna.myweather.provider.LocationProvider;
11 |
12 | public class LocalDataSourceImpl implements LocalDataSource> {
13 |
14 | private final WeatherDao mWeatherDao;
15 |
16 | private LocationProvider mLocationProvider;
17 |
18 | @Inject
19 | LocalDataSourceImpl(LocationProvider locationProvider,
20 | WeatherDao weatherDao) {
21 | mLocationProvider = locationProvider;
22 | mWeatherDao = weatherDao;
23 | }
24 |
25 | @Override
26 | public void save(WeatherResponse response) {
27 | mWeatherDao.insertWeatherResponse(response);
28 | }
29 |
30 | @Override
31 | public boolean hasLocationChanged(WeatherResponse response) {
32 | return mLocationProvider.isLocationChanged(response.getLocation());
33 | }
34 |
35 | @Override
36 | public boolean shouldFetch(WeatherResponse response) {
37 | ZonedDateTime locationTime = response.getLocation().getZonedDateTime();
38 | ZonedDateTime timeElapsed = ZonedDateTime.now().minusMinutes(30);
39 | return locationTime.isBefore(timeElapsed);
40 | }
41 |
42 | @Override
43 | public LiveData get() {
44 | return mWeatherDao.getWeatherResponse();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/local/WeatherDatabase.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.local;
2 |
3 | import androidx.room.Database;
4 | import androidx.room.RoomDatabase;
5 | import androidx.room.TypeConverters;
6 | import ezike.tobenna.myweather.data.local.dao.WeatherDao;
7 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
8 |
9 | /**
10 | * @author tobennaezike
11 | */
12 |
13 | @Database(entities = {WeatherResponse.class}, version = 1, exportSchema = false)
14 | @TypeConverters({WeatherTypeConverter.class})
15 | public abstract class WeatherDatabase extends RoomDatabase {
16 |
17 | public abstract WeatherDao weatherDao();
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/local/WeatherTypeConverter.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.local;
2 |
3 | import com.squareup.moshi.Moshi;
4 |
5 | import java.io.IOException;
6 |
7 | import androidx.room.TypeConverter;
8 | import ezike.tobenna.myweather.data.model.CurrentWeather;
9 | import ezike.tobenna.myweather.data.model.Forecast;
10 | import ezike.tobenna.myweather.data.model.WeatherLocation;
11 |
12 | /**
13 | * @author tobennaezike
14 | */
15 | public class WeatherTypeConverter {
16 |
17 | private Moshi moshi = new Moshi.Builder().build();
18 |
19 | @TypeConverter
20 | public String weatherToJson(CurrentWeather weather) {
21 | return weather == null ? null : moshi.adapter(CurrentWeather.class).toJson(weather);
22 | }
23 |
24 | @TypeConverter
25 | public CurrentWeather weatherFromJson(String string) {
26 | CurrentWeather weather;
27 | try {
28 | weather = moshi.adapter(CurrentWeather.class).fromJson(string);
29 | } catch (IOException e) {
30 | e.printStackTrace();
31 | return null;
32 | }
33 | return weather;
34 | }
35 |
36 |
37 | @TypeConverter
38 | public String locationToJson(WeatherLocation weatherLocation) {
39 | return weatherLocation == null ? null : moshi.adapter(WeatherLocation.class).toJson(weatherLocation);
40 | }
41 |
42 | @TypeConverter
43 | public WeatherLocation locationFromJson(String string) {
44 | WeatherLocation location;
45 | try {
46 | location = moshi.adapter(WeatherLocation.class).fromJson(string);
47 | } catch (IOException e) {
48 | e.printStackTrace();
49 | return null;
50 | }
51 | return location;
52 | }
53 |
54 |
55 | @TypeConverter
56 | public String forecastToJson(Forecast forecast) {
57 | return forecast == null ? null : moshi.adapter(Forecast.class).toJson(forecast);
58 | }
59 |
60 | @TypeConverter
61 | public Forecast forecastFromJson(String string) {
62 | Forecast weatherForecast;
63 | try {
64 | weatherForecast = moshi.adapter(Forecast.class).fromJson(string);
65 | } catch (IOException e) {
66 | e.printStackTrace();
67 | return null;
68 | }
69 | return weatherForecast;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/local/dao/WeatherDao.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.local.dao;
2 |
3 | import androidx.lifecycle.LiveData;
4 | import androidx.room.Dao;
5 | import androidx.room.Insert;
6 | import androidx.room.OnConflictStrategy;
7 | import androidx.room.Query;
8 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
9 |
10 | /**
11 | * @author tobennaezike
12 | */
13 |
14 | @Dao
15 | public interface WeatherDao {
16 |
17 | @Query("select * from weather_response")
18 | LiveData getWeatherResponse();
19 |
20 | @Insert(onConflict = OnConflictStrategy.REPLACE)
21 | void insertWeatherResponse(WeatherResponse weather);
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/local/entity/WeatherResponse.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.local.entity;
2 |
3 | import com.squareup.moshi.Json;
4 |
5 | import androidx.room.Entity;
6 | import androidx.room.PrimaryKey;
7 | import ezike.tobenna.myweather.data.model.CurrentWeather;
8 | import ezike.tobenna.myweather.data.model.Forecast;
9 | import ezike.tobenna.myweather.data.model.WeatherLocation;
10 |
11 | /**
12 | * @author tobennaezike
13 | */
14 |
15 | @Entity(tableName = "weather_response")
16 | public class WeatherResponse {
17 |
18 | @PrimaryKey
19 | private int id;
20 |
21 | @Json(name = "current")
22 | private CurrentWeather current;
23 |
24 | @Json(name = "location")
25 | private WeatherLocation location;
26 |
27 | @Json(name = "forecast")
28 | private Forecast forecast;
29 |
30 | public WeatherResponse() {
31 | }
32 |
33 | public CurrentWeather getCurrent() {
34 | return current;
35 | }
36 |
37 | public void setCurrent(CurrentWeather current) {
38 | this.current = current;
39 | }
40 |
41 | public WeatherLocation getLocation() {
42 | return location;
43 | }
44 |
45 | public void setLocation(WeatherLocation location) {
46 | this.location = location;
47 | }
48 |
49 | public Forecast getForecast() {
50 | return forecast;
51 | }
52 |
53 | public void setForecast(Forecast forecast) {
54 | this.forecast = forecast;
55 | }
56 |
57 | public int getId() {
58 | return id;
59 | }
60 |
61 | public void setId(int id) {
62 | this.id = id;
63 | }
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/model/Astro.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.model;
2 |
3 | import com.squareup.moshi.Json;
4 |
5 | public class Astro {
6 |
7 | @Json(name = "sunrise")
8 | private String sunrise;
9 |
10 | @Json(name = "sunset")
11 | private String sunset;
12 |
13 | public String getSunrise() {
14 | return sunrise;
15 | }
16 |
17 | public void setSunrise(String sunrise) {
18 | this.sunrise = sunrise;
19 | }
20 |
21 | public String getSunset() {
22 | return sunset;
23 | }
24 |
25 | public void setSunset(String sunset) {
26 | this.sunset = sunset;
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/model/Condition.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.model;
2 |
3 | import com.squareup.moshi.Json;
4 |
5 | /**
6 | * @author tobennaezike
7 | */
8 | public class Condition {
9 |
10 | @Json(name = "code")
11 | private int code;
12 |
13 | @Json(name = "icon")
14 | private String icon;
15 |
16 | @Json(name = "text")
17 | private String text;
18 |
19 | public int getCode() {
20 | return code;
21 | }
22 |
23 | public void setCode(int code) {
24 | this.code = code;
25 | }
26 |
27 | public String getIcon() {
28 | return icon;
29 | }
30 |
31 | public void setIcon(String icon) {
32 | this.icon = icon;
33 | }
34 |
35 | public String getText() {
36 | return text;
37 | }
38 |
39 | public void setText(String text) {
40 | this.text = text;
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/model/CurrentWeather.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.model;
2 |
3 | import com.squareup.moshi.Json;
4 |
5 | import androidx.room.Embedded;
6 |
7 | /**
8 | * @author tobennaezike
9 | */
10 | public class CurrentWeather {
11 |
12 | @Json(name = "feelslike_c")
13 | private double feelslikeC;
14 |
15 | @Json(name = "feelslike_f")
16 | private double feelslikeF;
17 |
18 | @Json(name = "uv")
19 | private double uv;
20 |
21 | @Json(name = "last_updated")
22 | private String lastUpdated;
23 |
24 | @Json(name = "is_day")
25 | private int isDay;
26 |
27 | @Json(name = "temp_c")
28 | private double tempC;
29 |
30 | @Json(name = "temp_f")
31 | private double tempF;
32 |
33 | @Json(name = "precip_mm")
34 | private double precipMm;
35 |
36 | @Json(name = "precip_in")
37 | private double precipIn;
38 |
39 | @Json(name = "wind_kph")
40 | private double windKph;
41 |
42 | @Embedded(prefix = "condition_")
43 | private Condition condition;
44 |
45 | @Json(name = "wind_mph")
46 | private double windMph;
47 |
48 | @Json(name = "vis_km")
49 | private double visKm;
50 |
51 | @Json(name = "vis_miles")
52 | private double visMiles;
53 |
54 | public CurrentWeather(double feelslikeC, double feelslikeF, double uv, String lastUpdated, int isDay,
55 | double tempC, double tempF, double precipMm, double precipIn, double windKph, Condition condition, double windMph, double visKm, double visMiles) {
56 | this.feelslikeC = feelslikeC;
57 | this.feelslikeF = feelslikeF;
58 | this.uv = uv;
59 | this.lastUpdated = lastUpdated;
60 | this.isDay = isDay;
61 | this.tempC = tempC;
62 | this.tempF = tempF;
63 | this.precipMm = precipMm;
64 | this.precipIn = precipIn;
65 | this.windKph = windKph;
66 | this.condition = condition;
67 | this.windMph = windMph;
68 | this.visKm = visKm;
69 | this.visMiles = visMiles;
70 | }
71 |
72 | public double getFeelslikeC() {
73 | return feelslikeC;
74 | }
75 |
76 | public void setFeelslikeC(double feelslikeC) {
77 | this.feelslikeC = feelslikeC;
78 | }
79 |
80 | public double getUv() {
81 | return uv;
82 | }
83 |
84 | public void setUv(double uv) {
85 | this.uv = uv;
86 | }
87 |
88 | public String getLastUpdated() {
89 | return lastUpdated;
90 | }
91 |
92 | public void setLastUpdated(String lastUpdated) {
93 | this.lastUpdated = lastUpdated;
94 | }
95 |
96 | public double getFeelslikeF() {
97 | return feelslikeF;
98 | }
99 |
100 | public void setFeelslikeF(double feelslikeF) {
101 | this.feelslikeF = feelslikeF;
102 | }
103 |
104 | public int getIsDay() {
105 | return isDay;
106 | }
107 |
108 | public void setIsDay(int isDay) {
109 | this.isDay = isDay;
110 | }
111 |
112 | public double getPrecipIn() {
113 | return precipIn;
114 | }
115 |
116 | public void setPrecipIn(double precipIn) {
117 | this.precipIn = precipIn;
118 | }
119 |
120 | public double getTempC() {
121 | return tempC;
122 | }
123 |
124 | public void setTempC(double tempC) {
125 | this.tempC = tempC;
126 | }
127 |
128 | public double getTempF() {
129 | return tempF;
130 | }
131 |
132 | public void setTempF(double tempF) {
133 | this.tempF = tempF;
134 | }
135 |
136 | public double getPrecipMm() {
137 | return precipMm;
138 | }
139 |
140 | public void setPrecipMm(double precipMm) {
141 | this.precipMm = precipMm;
142 | }
143 |
144 | public double getWindKph() {
145 | return windKph;
146 | }
147 |
148 | public void setWindKph(double windKph) {
149 | this.windKph = windKph;
150 | }
151 |
152 | public Condition getCondition() {
153 | return condition;
154 | }
155 |
156 | public void setCondition(Condition condition) {
157 | this.condition = condition;
158 | }
159 |
160 | public double getWindMph() {
161 | return windMph;
162 | }
163 |
164 | public void setWindMph(double windMph) {
165 | this.windMph = windMph;
166 | }
167 |
168 | public double getVisKm() {
169 | return visKm;
170 | }
171 |
172 | public void setVisKm(double visKm) {
173 | this.visKm = visKm;
174 | }
175 |
176 | public double getVisMiles() {
177 | return visMiles;
178 | }
179 |
180 | public void setVisMiles(double visMiles) {
181 | this.visMiles = visMiles;
182 | }
183 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/model/Day.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.model;
2 |
3 | import com.squareup.moshi.Json;
4 |
5 | /**
6 | * @author tobennaezike
7 | */
8 | public class Day {
9 |
10 | @Json(name = "avgvis_km")
11 | private double avgvisKm;
12 |
13 | @Json(name = "uv")
14 | private double uv;
15 |
16 | @Json(name = "avgtemp_f")
17 | private double avgtempF;
18 |
19 | @Json(name = "avgtemp_c")
20 | private double avgtempC;
21 |
22 | @Json(name = "maxtemp_c")
23 | private double maxtempC;
24 |
25 | @Json(name = "maxtemp_f")
26 | private double maxtempF;
27 |
28 | @Json(name = "mintemp_c")
29 | private double mintempC;
30 |
31 | @Json(name = "avgvis_miles")
32 | private double avgvisMiles;
33 |
34 | @Json(name = "mintemp_f")
35 | private double mintempF;
36 |
37 | @Json(name = "totalprecip_in")
38 | private double totalprecipIn;
39 |
40 | @Json(name = "condition")
41 | private Condition condition;
42 |
43 | @Json(name = "maxwind_kph")
44 | private double maxwindKph;
45 |
46 | @Json(name = "maxwind_mph")
47 | private double maxwindMph;
48 |
49 | @Json(name = "totalprecip_mm")
50 | private double totalprecipMm;
51 |
52 | public double getAvgvisKm() {
53 | return avgvisKm;
54 | }
55 |
56 | public void setAvgvisKm(double avgvisKm) {
57 | this.avgvisKm = avgvisKm;
58 | }
59 |
60 | public double getUv() {
61 | return uv;
62 | }
63 |
64 | public void setUv(double uv) {
65 | this.uv = uv;
66 | }
67 |
68 | public double getAvgtempF() {
69 | return avgtempF;
70 | }
71 |
72 | public void setAvgtempF(double avgtempF) {
73 | this.avgtempF = avgtempF;
74 | }
75 |
76 | public double getAvgtempC() {
77 | return avgtempC;
78 | }
79 |
80 | public void setAvgtempC(double avgtempC) {
81 | this.avgtempC = avgtempC;
82 | }
83 |
84 | public double getMaxtempC() {
85 | return maxtempC;
86 | }
87 |
88 | public void setMaxtempC(double maxtempC) {
89 | this.maxtempC = maxtempC;
90 | }
91 |
92 | public double getMaxtempF() {
93 | return maxtempF;
94 | }
95 |
96 | public void setMaxtempF(double maxtempF) {
97 | this.maxtempF = maxtempF;
98 | }
99 |
100 | public double getMintempC() {
101 | return mintempC;
102 | }
103 |
104 | public void setMintempC(double mintempC) {
105 | this.mintempC = mintempC;
106 | }
107 |
108 | public double getAvgvisMiles() {
109 | return avgvisMiles;
110 | }
111 |
112 | public void setAvgvisMiles(double avgvisMiles) {
113 | this.avgvisMiles = avgvisMiles;
114 | }
115 |
116 | public double getMintempF() {
117 | return mintempF;
118 | }
119 |
120 | public void setMintempF(double mintempF) {
121 | this.mintempF = mintempF;
122 | }
123 |
124 | public double getTotalprecipIn() {
125 | return totalprecipIn;
126 | }
127 |
128 | public void setTotalprecipIn(double totalprecipIn) {
129 | this.totalprecipIn = totalprecipIn;
130 | }
131 |
132 | public Condition getCondition() {
133 | return condition;
134 | }
135 |
136 | public void setCondition(Condition condition) {
137 | this.condition = condition;
138 | }
139 |
140 | public double getMaxwindKph() {
141 | return maxwindKph;
142 | }
143 |
144 | public void setMaxwindKph(double maxwindKph) {
145 | this.maxwindKph = maxwindKph;
146 | }
147 |
148 | public double getMaxwindMph() {
149 | return maxwindMph;
150 | }
151 |
152 | public void setMaxwindMph(double maxwindMph) {
153 | this.maxwindMph = maxwindMph;
154 | }
155 |
156 | public double getTotalprecipMm() {
157 | return totalprecipMm;
158 | }
159 |
160 | public void setTotalprecipMm(double totalprecipMm) {
161 | this.totalprecipMm = totalprecipMm;
162 | }
163 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/model/Forecast.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.model;
2 |
3 | import com.squareup.moshi.Json;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * @author tobennaezike
9 | */
10 | public class Forecast {
11 |
12 | @Json(name = "forecastday")
13 | private List forecastday;
14 |
15 | public List getForecastday() {
16 | return forecastday;
17 | }
18 |
19 | public void setForecastday(List forecastday) {
20 | this.forecastday = forecastday;
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/model/ForecastdayItem.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.model;
2 |
3 | import com.squareup.moshi.Json;
4 |
5 | /**
6 | * @author tobennaezike
7 | */
8 | public class ForecastdayItem {
9 |
10 | @Json(name = "date")
11 | private String date;
12 |
13 | @Json(name = "astro")
14 | private Astro astro;
15 |
16 | @Json(name = "day")
17 | private Day day;
18 |
19 | public String getDate() {
20 | return date;
21 | }
22 |
23 | public void setDate(String date) {
24 | this.date = date;
25 | }
26 |
27 | public Astro getAstro() {
28 | return astro;
29 | }
30 |
31 | public void setAstro(Astro astro) {
32 | this.astro = astro;
33 | }
34 |
35 | public Day getDay() {
36 | return day;
37 | }
38 |
39 | public void setDay(Day day) {
40 | this.day = day;
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/model/WeatherLocation.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.model;
2 |
3 | import android.annotation.TargetApi;
4 | import android.os.Build;
5 |
6 | import com.squareup.moshi.Json;
7 |
8 | import org.threeten.bp.DayOfWeek;
9 | import org.threeten.bp.Instant;
10 | import org.threeten.bp.LocalDate;
11 | import org.threeten.bp.ZoneId;
12 | import org.threeten.bp.ZonedDateTime;
13 | import org.threeten.bp.format.TextStyle;
14 |
15 | import java.util.Locale;
16 |
17 | /**
18 | * @author tobennaezike
19 | */
20 |
21 | public class WeatherLocation {
22 |
23 | @Json(name = "localtime")
24 | private String localtime;
25 |
26 | @Json(name = "country")
27 | private String country;
28 |
29 | @Json(name = "localtime_epoch")
30 | private int localtimeEpoch;
31 |
32 | @Json(name = "name")
33 | private String name;
34 |
35 | @Json(name = "lon")
36 | private double longitude;
37 |
38 | @Json(name = "region")
39 | private String region;
40 |
41 | @Json(name = "lat")
42 | private double latitude;
43 |
44 | @Json(name = "tz_id")
45 | private String tzId;
46 |
47 | public WeatherLocation(String localtime, String country, int localtimeEpoch, String name,
48 | double longitude, String region, double latitude, String tzId) {
49 | this.localtime = localtime;
50 | this.country = country;
51 | this.localtimeEpoch = localtimeEpoch;
52 | this.name = name;
53 | this.longitude = longitude;
54 | this.region = region;
55 | this.latitude = latitude;
56 | this.tzId = tzId;
57 | }
58 |
59 | public String getLocaltime() {
60 | return localtime;
61 | }
62 |
63 | public void setLocaltime(String localtime) {
64 | this.localtime = localtime;
65 | }
66 |
67 | public String getCountry() {
68 | return country;
69 | }
70 |
71 | public void setCountry(String country) {
72 | this.country = country;
73 | }
74 |
75 | public int getLocaltimeEpoch() {
76 | return localtimeEpoch;
77 | }
78 |
79 | public void setLocaltimeEpoch(int localtimeEpoch) {
80 | this.localtimeEpoch = localtimeEpoch;
81 | }
82 |
83 | public String getName() {
84 | return name;
85 | }
86 |
87 | public void setName(String name) {
88 | this.name = name;
89 | }
90 |
91 | public double getLongitude() {
92 | return longitude;
93 | }
94 |
95 | public void setLongitude(double longitude) {
96 | this.longitude = longitude;
97 | }
98 |
99 | public String getRegion() {
100 | return region;
101 | }
102 |
103 | public void setRegion(String region) {
104 | this.region = region;
105 | }
106 |
107 | public double getLatitude() {
108 | return latitude;
109 | }
110 |
111 | public void setLatitude(double latitude) {
112 | this.latitude = latitude;
113 | }
114 |
115 | public String getTzId() {
116 | return tzId;
117 | }
118 |
119 | public void setTzId(String tzId) {
120 | this.tzId = tzId;
121 | }
122 |
123 | @TargetApi(Build.VERSION_CODES.O)
124 | public ZonedDateTime getZonedDateTime() {
125 | Instant instant = Instant.ofEpochSecond(localtimeEpoch);
126 | ZoneId zoneId = ZoneId.of(tzId);
127 | return ZonedDateTime.ofInstant(instant, zoneId);
128 | }
129 |
130 | public String getDay() {
131 | DayOfWeek dow = LocalDate.now(ZoneId.of(tzId)).getDayOfWeek();
132 | return dow.getDisplayName(TextStyle.FULL, Locale.ENGLISH);
133 | }
134 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/LiveDataCallAdapter.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote;
2 |
3 |
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | import java.lang.reflect.Type;
7 | import java.util.concurrent.atomic.AtomicBoolean;
8 |
9 | import androidx.annotation.NonNull;
10 | import androidx.lifecycle.LiveData;
11 | import ezike.tobenna.myweather.data.remote.api.ApiResponse;
12 | import retrofit2.Call;
13 | import retrofit2.CallAdapter;
14 | import retrofit2.Callback;
15 | import retrofit2.Response;
16 |
17 | /**
18 | * A Retrofit adapter that converts the Call into a LiveData of ApiResponse.
19 | *
20 | * @param
21 | *
22 | */
23 | public class LiveDataCallAdapter implements CallAdapter>> {
24 |
25 | private final Type responseType;
26 |
27 | LiveDataCallAdapter(Type responseType) {
28 | this.responseType = responseType;
29 | }
30 |
31 | @NotNull
32 | @Override
33 | public Type responseType() {
34 | return responseType;
35 | }
36 |
37 | @NotNull
38 | @Override
39 | public LiveData> adapt(@NotNull final Call call) {
40 | return new LiveData>() {
41 | AtomicBoolean started = new AtomicBoolean(false);
42 |
43 | @Override
44 | protected void onActive() {
45 | super.onActive();
46 | if (started.compareAndSet(false, true)) {
47 | call.enqueue(new Callback() {
48 | @Override
49 | public void onResponse(@NonNull Call call, @NonNull Response response) {
50 | postValue(new ApiResponse<>(response));
51 | }
52 |
53 | @Override
54 | public void onFailure(@NonNull Call call, @NotNull Throwable throwable) {
55 | postValue(new ApiResponse<>(throwable));
56 | }
57 | });
58 | }
59 | }
60 | };
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/LiveDataCallAdapterFactory.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.lang.annotation.Annotation;
6 | import java.lang.reflect.ParameterizedType;
7 | import java.lang.reflect.Type;
8 |
9 | import androidx.lifecycle.LiveData;
10 | import ezike.tobenna.myweather.data.remote.api.ApiResponse;
11 | import retrofit2.CallAdapter;
12 | import retrofit2.Retrofit;
13 |
14 | /**
15 | *
16 | */
17 | public class LiveDataCallAdapterFactory extends CallAdapter.Factory {
18 |
19 | @Override
20 | public CallAdapter get(@NotNull Type returnType, @NotNull Annotation[] annotations, @NotNull Retrofit retrofit) {
21 | if (getRawType(returnType) != LiveData.class) {
22 | return null;
23 | }
24 | Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
25 | Class> rawObservableType = getRawType(observableType);
26 | if (rawObservableType != ApiResponse.class) {
27 | throw new IllegalArgumentException("type must be a resource");
28 | }
29 | if (!(observableType instanceof ParameterizedType)) {
30 | throw new IllegalArgumentException("resource must be parameterized");
31 | }
32 | Type bodyType = getParameterUpperBound(0, (ParameterizedType) observableType);
33 | return new LiveDataCallAdapter<>(bodyType);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/RemoteSourceImpl.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote;
2 |
3 | import java.util.Locale;
4 |
5 | import javax.inject.Inject;
6 |
7 | import androidx.lifecycle.LiveData;
8 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
9 | import ezike.tobenna.myweather.data.remote.api.ApiResponse;
10 | import ezike.tobenna.myweather.data.remote.api.ApiService;
11 | import ezike.tobenna.myweather.data.source.BaseSource;
12 | import ezike.tobenna.myweather.provider.LocationProvider;
13 |
14 | public class RemoteSourceImpl implements BaseSource>> {
15 |
16 | private static final int DAYS = 1;
17 |
18 | private final ApiService mApiService;
19 |
20 | private final LocationProvider mLocationProvider;
21 |
22 | @Inject
23 | RemoteSourceImpl(ApiService service,
24 | LocationProvider locationProvider) {
25 | mApiService = service;
26 | mLocationProvider = locationProvider;
27 | }
28 |
29 | @Override
30 | public LiveData> get() {
31 | return mApiService.getWeather(mLocationProvider.getPreferredLocationString(), DAYS, Locale.getDefault().getLanguage());
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/api/ApiResponse.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote.api;
2 |
3 | import java.io.IOException;
4 |
5 | import androidx.annotation.Nullable;
6 | import retrofit2.Response;
7 | import timber.log.Timber;
8 |
9 | public class ApiResponse {
10 |
11 | private final int code;
12 | @Nullable
13 | public final T body;
14 | @Nullable
15 | private final Throwable error;
16 |
17 |
18 | public ApiResponse(@Nullable Throwable error) {
19 | code = 500;
20 | body = null;
21 | this.error = error;
22 | }
23 |
24 | public ApiResponse(Response response) {
25 | code = response.code();
26 | if (response.isSuccessful()) {
27 | body = response.body();
28 | error = null;
29 | } else {
30 | String message = null;
31 | if (response.errorBody() != null) {
32 | try {
33 | message = response.errorBody().string();
34 | } catch (IOException ignored) {
35 | Timber.e(ignored, "error while parsing response");
36 | }
37 | }
38 | if (message == null || message.trim().length() == 0) {
39 | message = response.message();
40 | }
41 | error = new IOException(message);
42 | body = null;
43 | }
44 | }
45 |
46 | public boolean isSuccessful() {
47 | return code >= 200 && code < 300;
48 | }
49 |
50 |
51 | public int getCode() {
52 | return code;
53 | }
54 |
55 | @Nullable
56 | public T getBody() {
57 | return body;
58 | }
59 |
60 | @Nullable
61 | public Throwable getError() {
62 | return error;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/api/ApiService.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote.api;
2 |
3 | import androidx.lifecycle.LiveData;
4 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
5 | import retrofit2.http.GET;
6 | import retrofit2.http.Query;
7 |
8 | /**
9 | * @author tobennaezike
10 | */
11 |
12 | // https://api.apixu.com/v1/forecast.json?key=ENTER_KEY&q=Los%20Angeles&days=1
13 | public interface ApiService {
14 |
15 | @GET("forecast.json")
16 | LiveData> getWeather(@Query("q") String location,
17 | @Query("days") Integer days,
18 | @Query("lang") String languageCode);
19 |
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/interceptors/ApiInterceptor.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote.interceptors;
2 |
3 | import okhttp3.Interceptor;
4 |
5 | /**
6 | * @author tobennaezike
7 | */
8 | public interface ApiInterceptor extends Interceptor {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/interceptors/ConnectivityInterceptorImpl.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote.interceptors;
2 |
3 | import android.content.Context;
4 |
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.io.IOException;
8 |
9 | import javax.inject.Inject;
10 | import javax.inject.Singleton;
11 |
12 | import ezike.tobenna.myweather.utils.Utilities;
13 | import okhttp3.Response;
14 |
15 | /**
16 | * @author tobennaezike
17 | */
18 |
19 | @Singleton
20 | public class ConnectivityInterceptorImpl implements ApiInterceptor {
21 |
22 | private Context mContext;
23 |
24 | @Inject
25 | public ConnectivityInterceptorImpl(Context context) {
26 | mContext = context.getApplicationContext();
27 | }
28 |
29 | @NotNull
30 | @Override
31 | public Response intercept(@NotNull Chain chain) throws IOException {
32 | if (!Utilities.isOnline(mContext)) {
33 | throw new IOException();
34 | }
35 | return chain.proceed(chain.request());
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/remote/interceptors/RequestInterceptorImpl.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.remote.interceptors;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.io.IOException;
6 |
7 | import javax.inject.Inject;
8 | import javax.inject.Singleton;
9 |
10 | import ezike.tobenna.myweather.BuildConfig;
11 | import okhttp3.HttpUrl;
12 | import okhttp3.Request;
13 | import okhttp3.Response;
14 |
15 | /**
16 | * @author tobennaezike
17 | */
18 |
19 | @Singleton
20 | public class RequestInterceptorImpl implements ApiInterceptor {
21 |
22 | @Inject
23 | public RequestInterceptorImpl() {
24 | }
25 |
26 | @NotNull
27 | @Override
28 | public Response intercept(@NotNull Chain chain) throws IOException {
29 | Request request = chain.request();
30 |
31 | HttpUrl url = request.url().newBuilder()
32 | .addQueryParameter("key", BuildConfig.ApiKey)
33 | .build();
34 |
35 | request = request.newBuilder().url(url).build();
36 | return chain.proceed(request);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/data/source/BaseSource.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.data.source;
2 |
3 | public interface BaseSource {
4 | T get();
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/AppComponent.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di;
2 |
3 | import android.app.Application;
4 |
5 | import javax.inject.Singleton;
6 |
7 | import dagger.BindsInstance;
8 | import dagger.Component;
9 | import dagger.android.support.AndroidSupportInjectionModule;
10 | import ezike.tobenna.myweather.WeatherApplication;
11 | import ezike.tobenna.myweather.di.module.ActivityModule;
12 | import ezike.tobenna.myweather.di.module.ApiModule;
13 | import ezike.tobenna.myweather.di.module.AppModule;
14 | import ezike.tobenna.myweather.di.module.DataSourceModule;
15 | import ezike.tobenna.myweather.di.module.DatabaseModule;
16 | import ezike.tobenna.myweather.di.module.FragmentBuildersModule;
17 | import ezike.tobenna.myweather.di.module.LocationModule;
18 | import ezike.tobenna.myweather.di.module.UnitModule;
19 |
20 |
21 | /**
22 | * @author tobennaezike
23 | * @since 20/03/19
24 | */
25 | @Singleton
26 | @Component(modules = {
27 | AndroidSupportInjectionModule.class,
28 | ActivityModule.class,
29 | FragmentBuildersModule.class,
30 | ApiModule.class,
31 | DatabaseModule.class,
32 | AppModule.class,
33 | UnitModule.class,
34 | LocationModule.class,
35 | DataSourceModule.class,
36 | ViewModelModule.class})
37 | public interface AppComponent {
38 |
39 | void inject(WeatherApplication application);
40 |
41 | @Component.Builder
42 | interface Builder {
43 |
44 | @BindsInstance
45 | Builder application(Application application);
46 |
47 | AppComponent build();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/AppInjector.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ezike.tobenna.myweather.di;
18 |
19 | import android.app.Activity;
20 | import android.app.Application;
21 | import android.os.Bundle;
22 |
23 | import org.jetbrains.annotations.NotNull;
24 |
25 | import androidx.fragment.app.Fragment;
26 | import androidx.fragment.app.FragmentActivity;
27 | import androidx.fragment.app.FragmentManager;
28 | import dagger.android.AndroidInjection;
29 | import dagger.android.support.AndroidSupportInjection;
30 | import dagger.android.support.HasSupportFragmentInjector;
31 | import ezike.tobenna.myweather.WeatherApplication;
32 |
33 | /**
34 | * Helper class to automatically inject fragments if they implement {@link Injectable}.
35 | */
36 | public class AppInjector {
37 |
38 | private AppInjector() {
39 |
40 | }
41 |
42 | public static void init(WeatherApplication weatherApplication) {
43 |
44 | DaggerAppComponent
45 | .builder()
46 | .application(weatherApplication)
47 | .build()
48 | .inject(weatherApplication);
49 |
50 | weatherApplication
51 | .registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
52 | @Override
53 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
54 | handleActivity(activity);
55 | }
56 |
57 | @Override
58 | public void onActivityStarted(Activity activity) {
59 |
60 | }
61 |
62 | @Override
63 | public void onActivityResumed(Activity activity) {
64 |
65 | }
66 |
67 | @Override
68 | public void onActivityPaused(Activity activity) {
69 |
70 | }
71 |
72 | @Override
73 | public void onActivityStopped(Activity activity) {
74 |
75 | }
76 |
77 | @Override
78 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
79 |
80 | }
81 |
82 | @Override
83 | public void onActivityDestroyed(Activity activity) {
84 |
85 | }
86 | });
87 | }
88 |
89 | private static void handleActivity(Activity activity) {
90 | if (activity instanceof HasSupportFragmentInjector) {
91 | AndroidInjection.inject(activity);
92 | }
93 | if (activity instanceof FragmentActivity) {
94 | ((FragmentActivity) activity).getSupportFragmentManager()
95 | .registerFragmentLifecycleCallbacks(
96 | new FragmentManager.FragmentLifecycleCallbacks() {
97 | @Override
98 | public void onFragmentCreated(@NotNull FragmentManager fm, @NotNull Fragment f,
99 | Bundle savedInstanceState) {
100 | if (f instanceof Injectable) {
101 | AndroidSupportInjection.inject(f);
102 | }
103 | }
104 | }, true);
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/Injectable.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di;
2 |
3 | /**
4 | * @author tobennaezike
5 | */
6 | public interface Injectable {
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/ViewModelKey.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di;
2 |
3 | import java.lang.annotation.Documented;
4 | import java.lang.annotation.ElementType;
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.RetentionPolicy;
7 | import java.lang.annotation.Target;
8 |
9 | import androidx.lifecycle.ViewModel;
10 | import dagger.MapKey;
11 |
12 | /**
13 | * @author tobennaezike
14 | */
15 |
16 | @Documented
17 | @Target({ElementType.METHOD})
18 | @Retention(RetentionPolicy.RUNTIME)
19 | @MapKey
20 | @interface ViewModelKey {
21 | Class extends ViewModel> value();
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/ViewModelModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di;
2 |
3 | import androidx.lifecycle.ViewModel;
4 | import androidx.lifecycle.ViewModelProvider;
5 | import dagger.Binds;
6 | import dagger.Module;
7 | import dagger.multibindings.IntoMap;
8 | import ezike.tobenna.myweather.ui.WeatherViewModel;
9 | import ezike.tobenna.myweather.viewmodel.WeatherViewModelFactory;
10 |
11 | /**
12 | * @author tobennaezike
13 | */
14 | @Module
15 | abstract class ViewModelModule {
16 |
17 | /*
18 | * inject this object into a Map using the @IntoMap annotation,
19 | * with the WeatherViewModel.class as key,
20 | * and a Provider that will build a WeatherViewModel
21 | * object.
22 | *
23 | * */
24 |
25 | @Binds
26 | @IntoMap
27 | @ViewModelKey(WeatherViewModel.class)
28 | abstract ViewModel currentWeatherViewModel(WeatherViewModel weatherViewModel);
29 |
30 | @Binds
31 | abstract ViewModelProvider.Factory bindViewModelFactory(WeatherViewModelFactory factory);
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/ActivityModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import dagger.Module;
4 | import dagger.android.ContributesAndroidInjector;
5 | import ezike.tobenna.myweather.ui.activity.MainActivity;
6 |
7 | /**
8 | * @author tobennaezike
9 | */
10 |
11 | @Module
12 | public abstract class ActivityModule {
13 |
14 | @ContributesAndroidInjector(modules = FragmentBuildersModule.class)
15 | abstract MainActivity contributeMainActivity();
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/ApiModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import android.app.Application;
4 |
5 | import com.squareup.moshi.Moshi;
6 |
7 | import java.io.File;
8 |
9 | import javax.inject.Named;
10 | import javax.inject.Singleton;
11 |
12 | import dagger.Module;
13 | import dagger.Provides;
14 | import ezike.tobenna.myweather.data.remote.LiveDataCallAdapterFactory;
15 | import ezike.tobenna.myweather.data.remote.api.ApiService;
16 | import ezike.tobenna.myweather.data.remote.interceptors.ApiInterceptor;
17 | import okhttp3.Cache;
18 | import okhttp3.OkHttpClient;
19 | import okhttp3.logging.HttpLoggingInterceptor;
20 | import retrofit2.Retrofit;
21 | import retrofit2.converter.moshi.MoshiConverterFactory;
22 |
23 | /**
24 | * @author tobennaezike
25 | */
26 | @Module(includes = ClientModule.class)
27 | public class ApiModule {
28 |
29 | @Provides
30 | @Singleton
31 | static Cache provideCache(Application application) {
32 | long cacheSize = 10 * 1024 * 1024; // 10 MB
33 | File httpCacheDirectory = new File(application.getCacheDir(), "http-cache");
34 | return new Cache(httpCacheDirectory, cacheSize);
35 | }
36 |
37 | @Provides
38 | @Singleton
39 | static OkHttpClient provideOkHttpClient(Cache cache, @Named("connect") ApiInterceptor connectivityInterceptor,
40 | @Named("request") ApiInterceptor requestInterceptor) {
41 |
42 | HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
43 | logging.setLevel(HttpLoggingInterceptor.Level.BODY);
44 | OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
45 | httpClient.cache(cache);
46 | httpClient.addInterceptor(logging);
47 | httpClient.addNetworkInterceptor(requestInterceptor);
48 | httpClient.addInterceptor(connectivityInterceptor);
49 | return httpClient.build();
50 | }
51 |
52 | @Provides
53 | @Singleton
54 | static Moshi provideMoshi() {
55 | return new Moshi.Builder().build();
56 | }
57 |
58 | @Singleton
59 | @Provides
60 | static ApiService provideApiService(Moshi moshi, OkHttpClient okHttpClient) {
61 | return new Retrofit.Builder()
62 | .baseUrl("http://api.apixu.com/v1/")
63 | .addConverterFactory(MoshiConverterFactory.create(moshi))
64 | .addCallAdapterFactory(new LiveDataCallAdapterFactory())
65 | .client(okHttpClient)
66 | .build()
67 | .create(ApiService.class);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/AppModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import android.location.LocationManager;
6 |
7 | import com.google.android.gms.location.FusedLocationProviderClient;
8 | import com.google.android.gms.location.LocationServices;
9 |
10 | import javax.inject.Singleton;
11 |
12 | import dagger.Module;
13 | import dagger.Provides;
14 |
15 | import static android.content.Context.LOCATION_SERVICE;
16 |
17 | /**
18 | * @author tobennaezike
19 | * @since 22/03/19
20 | */
21 | @Module
22 | public class AppModule {
23 |
24 | @Provides
25 | @Singleton
26 | static Context provideContext(Application application) {
27 | return application.getApplicationContext();
28 | }
29 |
30 | @Singleton
31 | @Provides
32 | static FusedLocationProviderClient provideFusedLocationProviderClient(Context context) {
33 | return LocationServices.getFusedLocationProviderClient(context);
34 | }
35 |
36 | @Singleton
37 | @Provides
38 | static LocationManager provideLocatioManager(Context context) {
39 | return (LocationManager) context.getSystemService(LOCATION_SERVICE);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/ClientModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import javax.inject.Named;
4 |
5 | import dagger.Binds;
6 | import dagger.Module;
7 | import ezike.tobenna.myweather.data.remote.interceptors.ApiInterceptor;
8 | import ezike.tobenna.myweather.data.remote.interceptors.ConnectivityInterceptorImpl;
9 | import ezike.tobenna.myweather.data.remote.interceptors.RequestInterceptorImpl;
10 |
11 | /**
12 | * @author tobennaezike
13 | * @since 20/03/19
14 | */
15 | @Module(includes = AppModule.class)
16 | public abstract class ClientModule {
17 |
18 | @Binds
19 | @Named("connect")
20 | abstract ApiInterceptor provideConnectivityInterceptor(ConnectivityInterceptorImpl interceptor);
21 |
22 | @Binds
23 | @Named("request")
24 | abstract ApiInterceptor provideRequestInterceptor(RequestInterceptorImpl interceptor);
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/DataSourceModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import androidx.lifecycle.LiveData;
4 | import dagger.Binds;
5 | import dagger.Module;
6 | import ezike.tobenna.myweather.data.local.LocalDataSource;
7 | import ezike.tobenna.myweather.data.local.LocalDataSourceImpl;
8 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
9 | import ezike.tobenna.myweather.data.remote.RemoteSourceImpl;
10 | import ezike.tobenna.myweather.data.source.BaseSource;
11 | import ezike.tobenna.myweather.repository.WeatherRepository;
12 | import ezike.tobenna.myweather.repository.WeatherRepositoryImpl;
13 | import ezike.tobenna.myweather.utils.Resource;
14 |
15 | @Module
16 | public abstract class DataSourceModule {
17 |
18 | @Binds
19 | abstract LocalDataSource provideDataSource(LocalDataSourceImpl localDataSource);
20 |
21 | @Binds
22 | abstract BaseSource provideBaseSource(RemoteSourceImpl remoteSource);
23 |
24 | @Binds
25 | abstract WeatherRepository>> provideRepoImpl(WeatherRepositoryImpl repo);
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/DatabaseModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import android.content.Context;
4 |
5 | import javax.inject.Singleton;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.room.Room;
9 | import dagger.Module;
10 | import dagger.Provides;
11 | import ezike.tobenna.myweather.data.local.WeatherDatabase;
12 | import ezike.tobenna.myweather.data.local.dao.WeatherDao;
13 |
14 |
15 | /**
16 | * @author tobennaezike
17 | * @since 20/03/19
18 | */
19 | @Module
20 | public class DatabaseModule {
21 |
22 | @Provides
23 | @Singleton
24 | static WeatherDatabase provideDatabase(@NonNull Context context) {
25 | return Room.databaseBuilder(context,
26 | WeatherDatabase.class, "weather_db")
27 | .build();
28 | }
29 |
30 | @Provides
31 | @Singleton
32 | static WeatherDao provideWeatherResponseDao(@NonNull WeatherDatabase appDatabase) {
33 | return appDatabase.weatherDao();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/FragmentBuildersModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import dagger.Module;
4 | import dagger.android.ContributesAndroidInjector;
5 | import ezike.tobenna.myweather.ui.fragment.WeatherFragment;
6 |
7 | /**
8 | * @author tobennaezike
9 | */
10 |
11 | @Module
12 | public abstract class FragmentBuildersModule {
13 |
14 | @ContributesAndroidInjector
15 | abstract WeatherFragment contributeWeatherFragmet();
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/LocationModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import dagger.Binds;
4 | import dagger.Module;
5 | import ezike.tobenna.myweather.provider.LocationProvider;
6 | import ezike.tobenna.myweather.provider.LocationProviderImpl;
7 |
8 | /**
9 | * @author tobennaezike
10 | */
11 |
12 | @Module
13 | public abstract class LocationModule {
14 |
15 | @Binds
16 | abstract LocationProvider provideLocationProvider(LocationProviderImpl locationProvider);
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/di/module/UnitModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.di.module;
2 |
3 | import dagger.Binds;
4 | import dagger.Module;
5 | import ezike.tobenna.myweather.provider.UnitProvider;
6 | import ezike.tobenna.myweather.provider.UnitProviderImpl;
7 |
8 | /**
9 | * @author tobennaezike
10 | * @since 23/03/19
11 | */
12 | @Module
13 | public abstract class UnitModule {
14 |
15 | @Binds
16 | abstract UnitProvider provideUnitProvider(UnitProviderImpl unitProvider);
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/provider/LocationProvider.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.provider;
2 |
3 | import ezike.tobenna.myweather.data.model.WeatherLocation;
4 |
5 | public interface LocationProvider {
6 |
7 | boolean isLocationChanged(WeatherLocation location);
8 |
9 | String getPreferredLocationString();
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/provider/LocationProviderImpl.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.provider;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.location.Location;
6 | import android.widget.Toast;
7 |
8 | import com.google.android.gms.location.FusedLocationProviderClient;
9 | import com.google.android.gms.location.LocationCallback;
10 | import com.google.android.gms.location.LocationResult;
11 | import com.google.android.gms.tasks.OnSuccessListener;
12 |
13 | import javax.inject.Inject;
14 |
15 | import ezike.tobenna.myweather.R;
16 | import ezike.tobenna.myweather.data.model.WeatherLocation;
17 | import ezike.tobenna.myweather.utils.Utilities;
18 | import timber.log.Timber;
19 |
20 | public class LocationProviderImpl extends PreferenceProvider implements LocationProvider, OnSuccessListener {
21 |
22 | private static final String USE_DEVICE_LOCATION = "USE_DEVICE_LOCATION";
23 |
24 | private static final String CUSTOM_LOCATION = "CUSTOM_LOCATION";
25 |
26 | private Context mContext;
27 |
28 | private FusedLocationProviderClient mFusedLocationProviderClient;
29 |
30 | private Location deviceLocation;
31 |
32 | @Inject
33 | LocationProviderImpl(Context context, FusedLocationProviderClient client) {
34 | super(context);
35 | mFusedLocationProviderClient = client;
36 | mContext = context;
37 | }
38 |
39 | @Override
40 | public boolean isLocationChanged(WeatherLocation location) {
41 | Timber.d("Device location change %b", hasDeviceLocationChanged(location));
42 | return hasDeviceLocationChanged(location) || hasCustomLocationChanged(location);
43 | }
44 |
45 | @Override
46 | public String getPreferredLocationString() {
47 | if (isUsingDeviceLocation()) {
48 |
49 | if (getLastDeviceLocation() == null) {
50 | Utilities.showToast(mContext, mContext.getString(R.string.location_not_available), Toast.LENGTH_LONG);
51 | return getCustomLocationName();
52 | } else {
53 | String latitude = String.valueOf(getLastDeviceLocation().getLatitude());
54 | String longitude = String.valueOf(getLastDeviceLocation().getLongitude());
55 | Timber.d("Coordinates %s,%s", latitude, longitude);
56 | return (latitude + "," + longitude);
57 | }
58 | } else {
59 | return getCustomLocationName();
60 | }
61 | }
62 |
63 | private boolean hasDeviceLocationChanged(WeatherLocation location) {
64 | if (!isUsingDeviceLocation()) {
65 | return false;
66 | }
67 |
68 | double comparisonThreshold = 0.03;
69 |
70 | if (getLastDeviceLocation() != null && location != null) {
71 | return Math.abs(getLastDeviceLocation().getLatitude() - location.getLatitude()) > comparisonThreshold
72 | && Math.abs(getLastDeviceLocation().getLongitude() - location.getLongitude()) > comparisonThreshold;
73 |
74 | }
75 | return false;
76 | }
77 |
78 | private boolean hasCustomLocationChanged(WeatherLocation location) {
79 | if (!isUsingDeviceLocation()) {
80 | String customLocationName = getCustomLocationName();
81 | return !customLocationName.equals(location.getName());
82 | }
83 | return false;
84 | }
85 |
86 | private boolean isUsingDeviceLocation() {
87 | startLocationUpdates();
88 | return getSharedPreferences().getBoolean(USE_DEVICE_LOCATION, true);
89 | }
90 |
91 | private String getCustomLocationName() {
92 | return getSharedPreferences().getString(CUSTOM_LOCATION, null);
93 | }
94 |
95 | private Location getLastDeviceLocation() {
96 |
97 | startLocationUpdates();
98 |
99 | return deviceLocation;
100 | }
101 |
102 | @SuppressLint("MissingPermission")
103 | private void startLocationUpdates() {
104 | mFusedLocationProviderClient.getLastLocation().addOnSuccessListener(this);
105 |
106 | new LocationCallback() {
107 | @Override
108 | public void onLocationResult(LocationResult locationResult) {
109 | super.onLocationResult(locationResult);
110 | for (Location location : locationResult.getLocations()) {
111 | if (location != null) {
112 | deviceLocation = location;
113 | }
114 | }
115 |
116 | }
117 | };
118 | }
119 |
120 | @Override
121 | public void onSuccess(Location location) {
122 | if (location != null) {
123 | deviceLocation = location;
124 | } else {
125 | Timber.d("Device Location not yet available. Please try again");
126 | }
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/provider/PreferenceProvider.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.provider;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.preference.PreferenceManager;
6 |
7 |
8 | /**
9 | * @author tobennaezike
10 | * @since 23/03/19
11 | */
12 | abstract class PreferenceProvider {
13 |
14 | private Context mContext;
15 |
16 | public PreferenceProvider(Context context) {
17 | mContext = context;
18 | }
19 |
20 | public SharedPreferences getSharedPreferences() {
21 | return PreferenceManager.getDefaultSharedPreferences(mContext);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/provider/UnitProvider.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.provider;
2 |
3 | import ezike.tobenna.myweather.utils.UnitSystem;
4 |
5 | /**
6 | * @author tobennaezike
7 | * @since 23/03/19
8 | */
9 | public interface UnitProvider {
10 |
11 | UnitSystem getUnitSystem();
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/provider/UnitProviderImpl.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.provider;
2 |
3 | import android.content.Context;
4 |
5 | import javax.inject.Inject;
6 | import javax.inject.Singleton;
7 |
8 | import ezike.tobenna.myweather.utils.UnitSystem;
9 |
10 | /**
11 | * @author tobennaezike
12 | * @since 23/03/19
13 | */
14 | @Singleton
15 | public class UnitProviderImpl extends PreferenceProvider implements UnitProvider {
16 |
17 | private static final String UNIT_SYSTEM = "UNIT_SYSTEM";
18 |
19 | @Inject
20 | public UnitProviderImpl(Context context) {
21 | super(context);
22 | }
23 |
24 | @Override
25 | public UnitSystem getUnitSystem() {
26 | String selectedName = getSharedPreferences().getString(UNIT_SYSTEM, UnitSystem.METRIC.name());
27 | return UnitSystem.valueOf(selectedName);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/repository/WeatherRepository.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.repository;
2 |
3 | public interface WeatherRepository {
4 |
5 | V loadData(String s);
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/repository/WeatherRepositoryImpl.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.repository;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | import javax.inject.Inject;
6 | import javax.inject.Singleton;
7 |
8 | import androidx.annotation.NonNull;
9 | import androidx.annotation.Nullable;
10 | import androidx.lifecycle.LiveData;
11 | import ezike.tobenna.myweather.data.NetworkBoundResource;
12 | import ezike.tobenna.myweather.data.local.LocalDataSource;
13 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
14 | import ezike.tobenna.myweather.data.remote.api.ApiResponse;
15 | import ezike.tobenna.myweather.data.source.BaseSource;
16 | import ezike.tobenna.myweather.utils.AppExecutors;
17 | import ezike.tobenna.myweather.utils.RateLimiter;
18 | import ezike.tobenna.myweather.utils.Resource;
19 | import timber.log.Timber;
20 |
21 | @Singleton
22 | public class WeatherRepositoryImpl implements WeatherRepository>> {
23 |
24 | private final LocalDataSource> mLocalDataSource;
25 |
26 | private final BaseSource>> mBaseSource;
27 |
28 | private final AppExecutors mExecutors;
29 |
30 | private RateLimiter rateLimit = new RateLimiter<>(30, TimeUnit.MINUTES);
31 |
32 | @Inject
33 | WeatherRepositoryImpl(AppExecutors executors, LocalDataSource dataSource, BaseSource baseSource) {
34 | mExecutors = executors;
35 | mLocalDataSource = dataSource;
36 | mBaseSource = baseSource;
37 | }
38 |
39 | @Override
40 | public LiveData> loadData(String input) {
41 | return new NetworkBoundResource(mExecutors) {
42 |
43 | @Override
44 | protected void saveCallResult(@NonNull WeatherResponse item) {
45 | mLocalDataSource.save(item);
46 | Timber.d("Weather response saved");
47 | }
48 |
49 | @Override
50 | protected boolean shouldFetch(@Nullable WeatherResponse data) {
51 | if (data != null) {
52 | Timber.d("LOCATION %s", data.getLocation().getCountry());
53 | return mLocalDataSource.shouldFetch(data) || mLocalDataSource.hasLocationChanged(data);
54 | }
55 | return data == null || rateLimit.shouldFetch(input);
56 | }
57 |
58 | @NonNull
59 | @Override
60 | protected LiveData loadFromDb() {
61 | Timber.d("loading Weather data from database");
62 | return mLocalDataSource.get();
63 | }
64 |
65 | @NonNull
66 | @Override
67 | protected LiveData> createCall() {
68 | Timber.d("Weather data fetch started");
69 | return mBaseSource.get();
70 | }
71 |
72 | @Override
73 | protected void onFetchFailed() {
74 | Timber.d("Fetch failed!!");
75 | rateLimit.reset(input);
76 | }
77 |
78 | }.asLiveData();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/ui/BindingAdapters.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.ui;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.widget.ImageView;
6 |
7 | import com.github.pwittchen.weathericonview.WeatherIconView;
8 |
9 | import androidx.databinding.BindingAdapter;
10 | import ezike.tobenna.myweather.R;
11 | import ezike.tobenna.myweather.utils.GlideApp;
12 | import ezike.tobenna.myweather.utils.WeatherIconUtils;
13 |
14 | /**
15 | * @author tobennaezike
16 | */
17 | public class BindingAdapters {
18 |
19 | @BindingAdapter({"imageUrl"})
20 | public static void bindImage(ImageView imageView, String imagePath) {
21 | GlideApp.with(imageView.getContext())
22 | .load("http:" + imagePath)
23 | .placeholder(R.drawable.day)
24 | .into(imageView);
25 | }
26 |
27 | @BindingAdapter({"showIcon"})
28 | public static void showIcon(WeatherIconView iconView, String condition) {
29 | Context context = iconView.getContext();
30 | WeatherIconUtils.getIconResource(context, iconView, condition);
31 | }
32 |
33 | @BindingAdapter("visibleGone")
34 | public static void showHide(View view, Boolean show) {
35 | if (show) view.setVisibility(View.VISIBLE);
36 | else view.setVisibility(View.GONE);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/ui/WeatherViewModel.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.ui;
2 |
3 | import androidx.lifecycle.LiveData;
4 | import androidx.lifecycle.MutableLiveData;
5 | import androidx.lifecycle.Transformations;
6 | import androidx.lifecycle.ViewModel;
7 |
8 | import javax.inject.Inject;
9 |
10 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
11 | import ezike.tobenna.myweather.repository.WeatherRepository;
12 | import ezike.tobenna.myweather.utils.Resource;
13 |
14 | public class WeatherViewModel extends ViewModel {
15 |
16 | private MutableLiveData value = new MutableLiveData<>();
17 |
18 | private LiveData> mWeather;
19 |
20 | @Inject
21 | WeatherViewModel(WeatherRepository>> repository) {
22 | mWeather = Transformations.switchMap(value, repository::loadData);
23 | setRefreshId(value.getValue());
24 | }
25 |
26 | public LiveData> getCurrentWeather() {
27 | return mWeather;
28 | }
29 |
30 | public void retry(String input) {
31 | setRefreshId(input);
32 | }
33 |
34 | private void setRefreshId(String input) {
35 | value.setValue(input);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/ui/activity/MainActivity.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.ui.activity;
2 |
3 | import android.Manifest;
4 | import android.content.pm.PackageManager;
5 | import android.location.LocationManager;
6 | import android.os.Bundle;
7 | import android.view.Menu;
8 | import android.view.MenuItem;
9 | import android.widget.Toast;
10 |
11 | import com.google.android.gms.ads.MobileAds;
12 | import com.google.android.gms.location.LocationCallback;
13 | import com.google.android.gms.location.LocationResult;
14 |
15 | import javax.inject.Inject;
16 |
17 | import androidx.annotation.NonNull;
18 | import androidx.appcompat.app.AppCompatActivity;
19 | import androidx.core.app.ActivityCompat;
20 | import androidx.databinding.DataBindingUtil;
21 | import androidx.drawerlayout.widget.DrawerLayout;
22 | import androidx.fragment.app.Fragment;
23 | import androidx.navigation.NavController;
24 | import androidx.navigation.Navigation;
25 | import androidx.navigation.ui.NavigationUI;
26 | import dagger.android.AndroidInjector;
27 | import dagger.android.DispatchingAndroidInjector;
28 | import dagger.android.support.HasSupportFragmentInjector;
29 | import ezike.tobenna.myweather.R;
30 | import ezike.tobenna.myweather.databinding.ActivityMainBinding;
31 | import ezike.tobenna.myweather.utils.LocationHandler;
32 | import ezike.tobenna.myweather.utils.Utilities;
33 | import timber.log.Timber;
34 |
35 | /**
36 | * @author tobennaezike
37 | * @since 16/03/19
38 | */
39 | public class MainActivity extends AppCompatActivity implements HasSupportFragmentInjector {
40 |
41 | private static final int PERMISSION_ACCESS_COARSE_LOCATION = 98;
42 |
43 | @Inject
44 | LocationManager mLocationManager;
45 |
46 | @Inject
47 | DispatchingAndroidInjector dispatchingAndroidInjector;
48 |
49 | private String[] permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION,
50 | Manifest.permission.ACCESS_FINE_LOCATION};
51 |
52 | private LocationCallback mLocationCallback = new LocationCallback() {
53 | @Override
54 | public void onLocationResult(LocationResult locationResult) {
55 | super.onLocationResult(locationResult);
56 | }
57 | };
58 |
59 | @Override
60 | public AndroidInjector supportFragmentInjector() {
61 | return dispatchingAndroidInjector;
62 | }
63 |
64 | private NavController mNavController;
65 |
66 | @Override
67 | protected void onCreate(Bundle savedInstanceState) {
68 | super.onCreate(savedInstanceState);
69 |
70 | ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
71 |
72 | setSupportActionBar(binding.toolbar);
73 |
74 | binding.toolbar.setTitle("");
75 |
76 | mNavController = Navigation.findNavController(this, R.id.nav_host_fragment);
77 |
78 | NavigationUI.setupActionBarWithNavController(this, mNavController);
79 |
80 | NavigationUI.setupWithNavController(binding.bottomNav, mNavController);
81 |
82 | checkLocationPermission();
83 |
84 | checkGpsEnabled();
85 |
86 | MobileAds.initialize(this, getString(R.string.ad_id));
87 | }
88 |
89 | @Override
90 | public boolean onSupportNavigateUp() {
91 | return NavigationUI.navigateUp(mNavController, (DrawerLayout) null);
92 | }
93 |
94 | @Override
95 | public boolean onCreateOptionsMenu(Menu menu) {
96 | getMenuInflater().inflate(R.menu.menu_main, menu);
97 | return true;
98 | }
99 |
100 | @Override
101 | public boolean onOptionsItemSelected(MenuItem item) {
102 | boolean navigated = NavigationUI.onNavDestinationSelected(item, mNavController);
103 | return navigated || super.onOptionsItemSelected(item);
104 | }
105 |
106 | private void checkGpsEnabled() {
107 | if (Utilities.isLocationProviderEnabled(mLocationManager)) {
108 | Timber.d("gps enabled");
109 | startLocationUpdates();
110 | } else {
111 | Timber.d("gps disabled");
112 | Utilities.enableLocationProvider(this, getString(R.string.enable_gps),
113 | getString(R.string.gps_enable_prompt));
114 | }
115 | }
116 |
117 | private void startLocationUpdates() {
118 | LocationHandler.getLocationHandler(this, mLocationCallback);
119 | }
120 |
121 | public void checkLocationPermission() {
122 | if (!isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION) ||
123 | !isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
124 | if (ActivityCompat.shouldShowRequestPermissionRationale(this,
125 | Manifest.permission.ACCESS_COARSE_LOCATION)) {
126 | Utilities.showDialog(this, getString(R.string.location_permission_dialog_title),
127 | getString(R.string.location_permission_prompt),
128 | (dialog, i) -> requestPermission(permissions),
129 | (dialog, i) -> Utilities.showToast(this,
130 | getString(R.string.set_custom_location),
131 | Toast.LENGTH_LONG));
132 | } else {
133 | requestPermission(permissions);
134 | }
135 | } else {
136 | Timber.d("Permission granted");
137 | startLocationUpdates();
138 | }
139 | }
140 |
141 | private void requestPermission(String[] permissions) {
142 | ActivityCompat.requestPermissions(this, permissions, PERMISSION_ACCESS_COARSE_LOCATION);
143 | }
144 |
145 | private boolean isPermissionGranted(String permission) {
146 | return ActivityCompat.checkSelfPermission(this,
147 | permission) == PackageManager.PERMISSION_GRANTED;
148 | }
149 |
150 | @Override
151 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
152 | @NonNull int[] grantResults) {
153 | switch (requestCode) {
154 | case PERMISSION_ACCESS_COARSE_LOCATION: {
155 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] ==
156 | PackageManager.PERMISSION_GRANTED) {
157 | startLocationUpdates();
158 | Timber.d("permission granted");
159 | } else {
160 | Timber.d("permission not granted");
161 | }
162 | }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/ui/activity/SplashActivity.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.ui.activity;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.ActivityOptions;
5 | import android.content.Intent;
6 | import android.os.Build;
7 | import android.os.Bundle;
8 | import android.os.Handler;
9 |
10 | import androidx.appcompat.app.AppCompatActivity;
11 | import ezike.tobenna.myweather.R;
12 |
13 | public class SplashActivity extends AppCompatActivity {
14 |
15 | @Override
16 | protected void onCreate(Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | setContentView(R.layout.activity_splash);
19 |
20 | scheduleSplashScreen();
21 | }
22 |
23 | private void scheduleSplashScreen() {
24 | new Handler().postDelayed(() -> {
25 | routeToActivity();
26 | finish();
27 | }, 1000L);
28 | }
29 |
30 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
31 | private void routeToActivity() {
32 | ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this);
33 | Intent intent = new Intent(SplashActivity.this, MainActivity.class);
34 | startActivity(intent, options.toBundle());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/ui/fragment/AboutFragment.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.ui.fragment;
2 |
3 |
4 | import android.os.Bundle;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 |
9 | import androidx.annotation.NonNull;
10 | import androidx.fragment.app.Fragment;
11 | import ezike.tobenna.myweather.R;
12 |
13 |
14 | /**
15 | * A simple {@link Fragment} subclass.
16 | */
17 | public class AboutFragment extends Fragment {
18 |
19 | @Override
20 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
21 | Bundle savedInstanceState) {
22 |
23 | return inflater.inflate(R.layout.fragment_about, container, false);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/ui/fragment/SettingsFragment.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.ui.fragment;
2 |
3 |
4 | import android.os.Bundle;
5 |
6 | import androidx.annotation.Nullable;
7 | import androidx.fragment.app.Fragment;
8 | import androidx.preference.PreferenceFragmentCompat;
9 | import ezike.tobenna.myweather.R;
10 |
11 | /**
12 | * A simple {@link Fragment} subclass.
13 | */
14 | public class SettingsFragment extends PreferenceFragmentCompat {
15 |
16 | @Override
17 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
18 | addPreferencesFromResource(R.xml.preferences);
19 | }
20 |
21 | @Override
22 | public void onActivityCreated(@Nullable Bundle savedInstanceState) {
23 | super.onActivityCreated(savedInstanceState);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/ui/fragment/WeatherFragment.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.ui.fragment;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.os.Bundle;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 |
10 | import com.google.android.gms.ads.AdRequest;
11 | import com.google.android.material.snackbar.Snackbar;
12 |
13 | import org.jetbrains.annotations.NotNull;
14 |
15 | import java.util.Objects;
16 |
17 | import javax.inject.Inject;
18 |
19 | import androidx.annotation.NonNull;
20 | import androidx.annotation.Nullable;
21 | import androidx.appcompat.app.AppCompatActivity;
22 | import androidx.core.content.ContextCompat;
23 | import androidx.databinding.DataBindingUtil;
24 | import androidx.fragment.app.Fragment;
25 | import androidx.lifecycle.ViewModelProvider;
26 | import androidx.lifecycle.ViewModelProviders;
27 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
28 | import ezike.tobenna.myweather.R;
29 | import ezike.tobenna.myweather.data.local.entity.WeatherResponse;
30 | import ezike.tobenna.myweather.databinding.FragmentWeatherBinding;
31 | import ezike.tobenna.myweather.di.Injectable;
32 | import ezike.tobenna.myweather.ui.WeatherViewModel;
33 | import ezike.tobenna.myweather.utils.Resource;
34 | import ezike.tobenna.myweather.utils.Status;
35 | import ezike.tobenna.myweather.utils.Utilities;
36 | import ezike.tobenna.myweather.widget.WeatherWidgetProvider;
37 |
38 | /**
39 | * A simple {@link Fragment} subclass.
40 | */
41 | public class WeatherFragment extends Fragment implements Injectable, SwipeRefreshLayout.OnRefreshListener {
42 |
43 | public static final String WIDGET_PREF = "ezike.tobenna.myweather.ui.widget.pref";
44 | public static final String WIDGET_TEXT = "ezike.tobenna.myweather.ui.widget.text";
45 | public static final String WIDGET_LOCATION = "ezike.tobenna.myweather.ui.widget.location";
46 | public static final String WIDGET_ICON = "ezike.tobenna.myweather.ui.widget.icon";
47 |
48 | @Inject
49 | ViewModelProvider.Factory viewModelFactory;
50 |
51 | private WeatherViewModel mWeatherViewModel;
52 |
53 | private FragmentWeatherBinding mBinding;
54 |
55 | private boolean isLoading = true;
56 |
57 | @Override
58 | public void onActivityCreated(@Nullable Bundle savedInstanceState) {
59 | super.onActivityCreated(savedInstanceState);
60 | initViewModel();
61 | isConnected();
62 | ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar().setTitle("");
63 | }
64 |
65 | @Override
66 | public void onCreate(@Nullable Bundle savedInstanceState) {
67 | super.onCreate(savedInstanceState);
68 | }
69 |
70 | @Override
71 | public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
72 | mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_weather, container, false);
73 | mBinding.setLifecycleOwner(this);
74 |
75 | AdRequest adRequest = new AdRequest.Builder().build();
76 | mBinding.adView.loadAd(adRequest);
77 |
78 | mBinding.swipeRefresh.setOnRefreshListener(this);
79 | mBinding.swipeRefresh.setColorSchemeColors(
80 | ContextCompat.getColor(Objects.requireNonNull(getActivity()), R.color.colorPrimary),
81 | ContextCompat.getColor(getActivity(), R.color.colorAccent),
82 | ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark)
83 | );
84 |
85 | return mBinding.getRoot();
86 | }
87 |
88 | private void initViewModel() {
89 | mWeatherViewModel = ViewModelProviders.of(this, viewModelFactory).get(WeatherViewModel.class);
90 | observeWeather();
91 | }
92 |
93 | private void observeWeather() {
94 | mWeatherViewModel.getCurrentWeather().observe(this, currentWeatherResource -> {
95 | if (currentWeatherResource.data != null) {
96 | bindData(currentWeatherResource);
97 | showError(currentWeatherResource);
98 | showSuccess(currentWeatherResource);
99 | updateWidgetData(currentWeatherResource.data);
100 | isLoading = false;
101 | } else {
102 | ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar().setTitle("");
103 | }
104 | mBinding.setResource(currentWeatherResource);
105 | });
106 | }
107 |
108 | private void bindData(@NonNull Resource currentWeatherResource) {
109 | assert currentWeatherResource.data != null;
110 | mBinding.setCondition(currentWeatherResource.data.getCurrent().getCondition());
111 | mBinding.setWeather(currentWeatherResource.data.getCurrent());
112 | String location = (currentWeatherResource.data.getLocation().getName() + ", " +
113 | currentWeatherResource.data.getLocation().getRegion());
114 | ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar().setTitle(location);
115 | mBinding.setLocation(currentWeatherResource.data.getLocation());
116 | }
117 |
118 | private void showError(Resource currentWeatherResource) {
119 | if (currentWeatherResource.status == Status.ERROR) {
120 | if (currentWeatherResource.message != null) {
121 | if (!currentWeatherResource.message.isEmpty()) {
122 | showSnackBar(currentWeatherResource.message, v -> snackRetryAction());
123 | }
124 | }
125 | }
126 | }
127 |
128 | private void showSuccess(Resource currentWeatherResource) {
129 | if (currentWeatherResource.status == Status.SUCCESS) {
130 | isLoading = false;
131 | }
132 | }
133 |
134 | private void saveToPreferences(WeatherResponse weather) {
135 | SharedPreferences sharedpreferences = Objects.requireNonNull(getActivity()).getSharedPreferences(WIDGET_PREF, Context.MODE_PRIVATE);
136 | SharedPreferences.Editor editor = sharedpreferences.edit();
137 | editor.putString(WIDGET_TEXT, weather.getCurrent().getCondition().getText());
138 | editor.putString(WIDGET_LOCATION, weather.getLocation().getRegion());
139 | editor.putString(WIDGET_ICON, weather.getCurrent().getCondition().getIcon());
140 | editor.apply();
141 | }
142 |
143 | private void updateWidgetData(WeatherResponse weather) {
144 | saveToPreferences(weather);
145 | WeatherWidgetProvider.updateWidget(getActivity());
146 | }
147 |
148 | private void showSnackBar(String message, View.OnClickListener listener) {
149 | Snackbar.make(mBinding.getRoot(), message, Snackbar.LENGTH_LONG)
150 | .setAction(R.string.retry, listener)
151 | .show();
152 | }
153 |
154 | private void retryFetch() {
155 | mWeatherViewModel.retry(String.valueOf(isLoading));
156 | }
157 |
158 | private boolean isConnected() {
159 | if (!Utilities.isOnline(Objects.requireNonNull(getActivity()))) {
160 | showSnackBar(getString(R.string.no_internet), v -> snackRetryAction());
161 | }
162 | return true;
163 | }
164 |
165 | private void snackRetryAction() {
166 | if (isConnected()) {
167 | retryFetch();
168 | }
169 | isConnected();
170 | }
171 |
172 | @Override
173 | public void onPause() {
174 | if (mBinding.adView != null) {
175 | mBinding.adView.pause();
176 | }
177 | super.onPause();
178 | }
179 |
180 | @Override
181 | public void onResume() {
182 | if (mBinding.adView != null) {
183 | mBinding.adView.resume();
184 | }
185 | super.onResume();
186 | }
187 |
188 | @Override
189 | public void onDestroy() {
190 | if (mBinding.adView != null) {
191 | mBinding.adView.destroy();
192 | }
193 | super.onDestroy();
194 | }
195 |
196 | @Override
197 | public void onRefresh() {
198 | if (isConnected()) {
199 | retryFetch();
200 | mBinding.swipeRefresh.setRefreshing(isLoading);
201 | }
202 | mBinding.swipeRefresh.setRefreshing(false);
203 | }
204 | }
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/AppExecutors.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ezike.tobenna.myweather.utils;
18 |
19 | import android.os.Handler;
20 | import android.os.Looper;
21 |
22 | import java.util.concurrent.Executor;
23 | import java.util.concurrent.Executors;
24 |
25 | import javax.inject.Inject;
26 | import javax.inject.Singleton;
27 |
28 | /**
29 | * Global executor pools for the whole application.
30 | *
31 | * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind
32 | * webservice requests).
33 | */
34 | @Singleton
35 | public class AppExecutors {
36 |
37 | private final Executor diskIO;
38 |
39 | private final Executor networkIO;
40 |
41 | private final Executor mainThread;
42 |
43 | public AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) {
44 | this.diskIO = diskIO;
45 | this.networkIO = networkIO;
46 | this.mainThread = mainThread;
47 | }
48 |
49 | @Inject
50 | public AppExecutors() {
51 | this(Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(3),
52 | new MainThreadExecutor());
53 | }
54 |
55 | public Executor diskIO() {
56 | return diskIO;
57 | }
58 |
59 | public Executor networkIO() {
60 | return networkIO;
61 | }
62 |
63 | public Executor mainThread() {
64 | return mainThread;
65 | }
66 |
67 | private static class MainThreadExecutor implements Executor {
68 | private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
69 |
70 | @Override
71 | public void execute(Runnable command) {
72 | mainThreadHandler.post(command);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/LocationHandler.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.utils;
2 |
3 | import android.annotation.SuppressLint;
4 |
5 | import androidx.lifecycle.Lifecycle;
6 | import androidx.lifecycle.LifecycleObserver;
7 | import androidx.lifecycle.LifecycleOwner;
8 | import androidx.lifecycle.OnLifecycleEvent;
9 |
10 | import com.google.android.gms.location.FusedLocationProviderClient;
11 | import com.google.android.gms.location.LocationCallback;
12 | import com.google.android.gms.location.LocationRequest;
13 |
14 | import javax.inject.Inject;
15 |
16 | import timber.log.Timber;
17 |
18 | public class LocationHandler implements LifecycleObserver {
19 |
20 | private static LocationHandler sInstance;
21 |
22 | @Inject
23 | FusedLocationProviderClient mFusedClient;
24 |
25 | private LocationRequest locationRequest;
26 |
27 | private LocationCallback mLocationCallback;
28 |
29 | private LocationHandler(LifecycleOwner lifecycleOwner,
30 | LocationCallback callback) {
31 | lifecycleOwner.getLifecycle().addObserver(this);
32 | mLocationCallback = callback;
33 | }
34 |
35 | public static LocationHandler getLocationHandler(LifecycleOwner lifecycleOwner,
36 | LocationCallback locationCallback) {
37 | if (sInstance == null) {
38 | sInstance = new LocationHandler(lifecycleOwner, locationCallback);
39 | }
40 | return sInstance;
41 | }
42 |
43 | private LocationRequest getLocationRequest() {
44 | try {
45 | locationRequest = new LocationRequest();
46 | locationRequest.setNumUpdates(1);
47 | locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
48 | locationRequest.setSmallestDisplacement(2);
49 | } catch (Exception e) {
50 | e.printStackTrace();
51 | return null;
52 | }
53 | return locationRequest;
54 | }
55 |
56 | @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
57 | @SuppressLint("MissingPermission")
58 | void requestLocation() {
59 | try {
60 | if (mFusedClient != null) {
61 | getLocationRequest();
62 | Timber.d("requesting location");
63 | mFusedClient.requestLocationUpdates(locationRequest, mLocationCallback, null);
64 | }
65 | } catch (Exception e) {
66 | e.printStackTrace();
67 | }
68 | }
69 |
70 | @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
71 | void stopLocationUpdates() {
72 | try {
73 | if (mFusedClient != null) {
74 | Timber.d("stop location requests");
75 | mFusedClient.removeLocationUpdates(mLocationCallback);
76 | }
77 | } catch (Exception e) {
78 | e.printStackTrace();
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/RateLimiter.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.utils;
2 |
3 | import android.util.ArrayMap;
4 |
5 | import java.util.concurrent.TimeUnit;
6 |
7 | /**
8 | * Utility class that decides whether we should fetch some data or not.
9 | */
10 | public class RateLimiter {
11 |
12 | private final long timeOut;
13 | private ArrayMap timeStamps = new ArrayMap<>();
14 |
15 | public RateLimiter(int timeOut, TimeUnit timeUnit) {
16 | this.timeOut = timeUnit.toMillis(timeOut);
17 | }
18 |
19 | public synchronized boolean shouldFetch(KEY key) {
20 | Long lastFetched = timeStamps.get(key);
21 | long now = now();
22 | if (lastFetched == null) {
23 | timeStamps.put(key, now);
24 | return true;
25 | }
26 |
27 | if (now - lastFetched > timeOut) {
28 | timeStamps.put(key, now);
29 | return true;
30 | }
31 | return false;
32 | }
33 |
34 | private long now() {
35 | return System.currentTimeMillis();
36 | }
37 |
38 | public synchronized void reset(KEY key) {
39 | timeStamps.remove(key);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/Resource.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.utils;
2 |
3 | import java.util.Objects;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.Nullable;
7 |
8 | /**
9 | * @param
10 | * @author Google samples
11 | */
12 | public class Resource {
13 |
14 | @NonNull
15 | public final Status status;
16 | @Nullable
17 | public final T data;
18 | @Nullable
19 | public final String message;
20 |
21 | private Resource(@NonNull Status status, @Nullable T data,
22 | @Nullable String message) {
23 | this.status = status;
24 | this.data = data;
25 | this.message = message;
26 | }
27 |
28 | /**
29 | * Creates [Resource] object with `SUCCESS` status and [data].
30 | */
31 | public static Resource success(@NonNull T data) {
32 | return new Resource<>(Status.SUCCESS, data, null);
33 | }
34 |
35 | /**
36 | * Creates [Resource] object with `ERROR` status and [message].
37 | */
38 | public static Resource error(String msg, @Nullable T data) {
39 | return new Resource<>(Status.ERROR, data, msg);
40 | }
41 |
42 | /**
43 | * Creates [Resource] object with `LOADING` status to notify
44 | * the UI to show loading.
45 | */
46 | public static Resource loading(@Nullable T data) {
47 | return new Resource<>(Status.LOADING, data, null);
48 | }
49 |
50 | @Override
51 | public boolean equals(Object o) {
52 | if (this == o) return true;
53 | if (!(o instanceof Resource)) return false;
54 | Resource> resource = (Resource>) o;
55 | return status == resource.status &&
56 | Objects.equals(data, resource.data) &&
57 | Objects.equals(message, resource.message);
58 | }
59 |
60 | @Override
61 | public int hashCode() {
62 | return Objects.hash(status, data, message);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/Status.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package ezike.tobenna.myweather.utils;
18 |
19 | /**
20 | * Status of a resource that is provided to the UI.
21 | *
22 | * These are usually created by the Repository classes where they return
23 | * {@code LiveData>} to pass back the latest data to the UI with its fetch status.
24 | */
25 | public enum Status {
26 | SUCCESS,
27 | ERROR,
28 | LOADING
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/UnitSystem.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.utils;
2 |
3 |
4 | /**
5 | * @author tobennaezike
6 | * @since 23/03/19
7 | */
8 | public enum UnitSystem {
9 | METRIC, IMPERIAL
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/Utilities.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.utils;
2 |
3 | import android.content.Context;
4 | import android.content.DialogInterface;
5 | import android.content.Intent;
6 | import android.location.LocationManager;
7 | import android.net.ConnectivityManager;
8 | import android.net.NetworkInfo;
9 | import android.provider.Settings;
10 | import android.widget.Toast;
11 |
12 | import androidx.appcompat.app.AlertDialog;
13 |
14 | public class Utilities {
15 |
16 | /**
17 | * checks if device has internet connection
18 | */
19 | public static boolean isOnline(Context context) {
20 | ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
21 | NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
22 | return networkInfo != null && networkInfo.isConnected();
23 | }
24 |
25 |
26 | /**
27 | * shows a dialog
28 | */
29 | public static void showDialog(Context context, String title, String message, DialogInterface.OnClickListener positive,
30 | DialogInterface.OnClickListener negative) {
31 | new AlertDialog.Builder(context)
32 | .setTitle(title)
33 | .setMessage(message)
34 | .setPositiveButton("Ok", positive)
35 | .setNegativeButton("Cancel", negative)
36 | .create()
37 | .show();
38 | }
39 |
40 | /**
41 | * checks if gps is enabled
42 | */
43 | public static boolean isLocationProviderEnabled(LocationManager locationManager) {
44 | return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && locationManager.isProviderEnabled(
45 | LocationManager.NETWORK_PROVIDER);
46 | }
47 |
48 |
49 | /**
50 | * enables GPS by opening settings
51 | */
52 | public static void enableLocationProvider(Context context, String title, String message) {
53 | showDialog(context, title, message, (dialog, which) -> openSettingsActivity(context), null);
54 | }
55 |
56 |
57 | /**
58 | * open settings for enabling gps
59 | */
60 | public static void openSettingsActivity(Context context) {
61 | context.startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
62 | }
63 |
64 |
65 | /**
66 | * show toast
67 | */
68 | public static void showToast(Context context, String message, int length) {
69 | Toast.makeText(context, message, length).show();
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/WeatherGlideModule.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.utils;
2 |
3 | import com.bumptech.glide.annotation.GlideModule;
4 | import com.bumptech.glide.module.AppGlideModule;
5 |
6 | /**
7 | * @author tobennaezike
8 | */
9 | @GlideModule
10 | public class WeatherGlideModule extends AppGlideModule {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/utils/WeatherIconUtils.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.utils;
2 |
3 | import android.content.Context;
4 |
5 | import com.github.pwittchen.weathericonview.WeatherIconView;
6 |
7 | import ezike.tobenna.myweather.R;
8 |
9 | public class WeatherIconUtils {
10 |
11 | public static void getIconResource(Context context, WeatherIconView iconView, String condition) {
12 | if (condition != null) {
13 | if (condition.contains("rain")) {
14 | iconView.setIconResource(context.getString(R.string.wi_rain));
15 | } else if (condition.contains("snow")) {
16 | iconView.setIconResource(context.getString(R.string.wi_snow));
17 | } else if (condition.contains("sun")) {
18 | iconView.setIconResource(context.getString(R.string.wi_day_sunny));
19 | } else if (condition.contains("cloud")) {
20 | iconView.setIconResource(context.getString(R.string.wi_forecast_io_cloudy));
21 | } else if (condition.contains("Clear")) {
22 | iconView.setIconResource(context.getString(R.string.wi_wu_clear));
23 | } else if (condition.contains("Overcast")) {
24 | iconView.setIconResource(context.getString(R.string.wi_day_sunny_overcast));
25 | } else if (condition.contains("sleet")) {
26 | iconView.setIconResource(context.getString(R.string.wi_day_sleet_storm));
27 | } else if (condition.contains("Mist")) {
28 | iconView.setIconResource(context.getString(R.string.wi_fog));
29 | } else if (condition.contains("drizzle")) {
30 | iconView.setIconResource(context.getString(R.string.wi_raindrops));
31 | } else if (condition.contains("thunderstorm")) {
32 | iconView.setIconResource(context.getString(R.string.wi_wu_tstorms));
33 | } else if (condition.contains("Thunder")) {
34 | iconView.setIconResource(context.getString(R.string.wi_thunderstorm));
35 | } else if (condition.contains("Cloudy")) {
36 | iconView.setIconResource(context.getString(R.string.wi_forecast_io_cloudy));
37 | } else if (condition.contains("Fog")) {
38 | iconView.setIconResource(context.getString(R.string.wi_forecast_io_fog));
39 | } else if (condition.contains("Sunny")) {
40 | iconView.setIconResource(context.getString(R.string.wi_wu_mostlysunny));
41 | } else if (condition.contains("Blizzard")) {
42 | iconView.setIconResource(context.getString(R.string.wi_snow_wind));
43 | } else if (condition.contains("Ice")) {
44 | iconView.setIconResource(context.getString(R.string.wi_wu_chancesnow));
45 | } else if (condition.contains("ice")) {
46 | iconView.setIconResource(context.getString(R.string.wi_forecast_io_snow));
47 | } else if (condition.contains("Rain")) {
48 | iconView.setIconResource(context.getString(R.string.wi_rain_wind));
49 | } else if (condition.contains("wind")) {
50 | iconView.setIconResource(context.getString(R.string.wi_windy));
51 | } else if (condition.contains("Wind")) {
52 | iconView.setIconResource(context.getString(R.string.wi_strong_wind));
53 | } else if (condition.contains("storm")) {
54 | iconView.setIconResource(context.getString(R.string.wi_storm_warning));
55 | } else if (condition.contains("Storm")) {
56 | iconView.setIconResource(context.getString(R.string.wi_forecast_io_thunderstorm));
57 | } else if (condition.contains("thunder")) {
58 | iconView.setIconResource(context.getString(R.string.wi_day_snow_thunderstorm));
59 | } else {
60 | iconView.setIconResource(context.getString(R.string.wi_forecast_io_partly_cloudy_day));
61 | }
62 | }
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/viewmodel/WeatherViewModelFactory.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.viewmodel;
2 |
3 | import java.util.Map;
4 |
5 | import javax.inject.Inject;
6 | import javax.inject.Provider;
7 | import javax.inject.Singleton;
8 |
9 | import androidx.annotation.NonNull;
10 | import androidx.lifecycle.ViewModel;
11 | import androidx.lifecycle.ViewModelProvider;
12 |
13 | /**
14 | * @author tobennaezike
15 | */
16 | @Singleton
17 | public class WeatherViewModelFactory implements ViewModelProvider.Factory {
18 |
19 | private final Map, Provider> creators;
20 |
21 | @Inject
22 | public WeatherViewModelFactory(Map, Provider> creators) {
23 | this.creators = creators;
24 | }
25 |
26 | @SuppressWarnings("unchecked")
27 | @NonNull
28 | @Override
29 | public T create(@NonNull Class modelClass) {
30 |
31 | Provider extends ViewModel> creator = creators.get(modelClass);
32 | if (creator == null) {
33 | for (Map.Entry, Provider> entry : creators.entrySet()) {
34 | if (modelClass.isAssignableFrom(entry.getKey())) {
35 | creator = entry.getValue();
36 | break;
37 | }
38 | }
39 | }
40 |
41 | if (creator == null) {
42 | throw new IllegalArgumentException("unknown model class " + modelClass);
43 | }
44 |
45 | try {
46 | return (T) creator.get();
47 | } catch (Exception e) {
48 | throw new RuntimeException(e);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/widget/WeatherWidgetProvider.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.widget;
2 |
3 | import android.app.AlarmManager;
4 | import android.app.PendingIntent;
5 | import android.appwidget.AppWidgetManager;
6 | import android.appwidget.AppWidgetProvider;
7 | import android.content.ComponentName;
8 | import android.content.Context;
9 | import android.content.Intent;
10 | import android.content.SharedPreferences;
11 | import android.graphics.Bitmap;
12 | import android.os.SystemClock;
13 | import android.widget.RemoteViews;
14 |
15 | import com.bumptech.glide.request.RequestOptions;
16 | import com.bumptech.glide.request.target.AppWidgetTarget;
17 | import com.bumptech.glide.request.transition.Transition;
18 |
19 | import androidx.annotation.NonNull;
20 | import ezike.tobenna.myweather.R;
21 | import ezike.tobenna.myweather.ui.activity.MainActivity;
22 | import ezike.tobenna.myweather.ui.fragment.WeatherFragment;
23 | import ezike.tobenna.myweather.utils.GlideApp;
24 |
25 | /**
26 | * Implementation of App Widget functionality.
27 | */
28 | public class WeatherWidgetProvider extends AppWidgetProvider {
29 |
30 | private PendingIntent pendingIntent;
31 |
32 | private AlarmManager manager;
33 |
34 | static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
35 | int appWidgetId) {
36 |
37 | SharedPreferences sharedPreferences = context.getSharedPreferences(WeatherFragment.WIDGET_PREF, Context.MODE_PRIVATE);
38 | String defaultValue = context.getString(R.string.no_data);
39 | String conditionText = sharedPreferences.getString(WeatherFragment.WIDGET_TEXT, defaultValue);
40 | String location = sharedPreferences.getString(WeatherFragment.WIDGET_LOCATION, defaultValue);
41 | String iconUrl = sharedPreferences.getString(WeatherFragment.WIDGET_ICON, defaultValue);
42 |
43 | RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.weather_widget);
44 | views.setTextViewText(R.id.appwidget_location, location);
45 | views.setTextViewText(R.id.appwidget_condition, context.getString(R.string.widget_forecast, conditionText));
46 |
47 | Intent clickIntent = new Intent(context, MainActivity.class);
48 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent, 0);
49 | views.setOnClickPendingIntent(R.id.appwidget_root, pendingIntent);
50 |
51 | // Display weather condition icon using Glide
52 | showWeatherIcon(context, appWidgetId, iconUrl, views);
53 |
54 | appWidgetManager.updateAppWidget(appWidgetId, views);
55 | }
56 |
57 | public static void updateWidget(Context context) {
58 | AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
59 | int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, WeatherWidgetProvider.class));
60 | for (int appWidgetId : appWidgetIds) {
61 | WeatherWidgetProvider.updateAppWidget(context, appWidgetManager, appWidgetId);
62 | }
63 | }
64 |
65 | private static void showWeatherIcon(Context context, int appWidgetId, String iconUrl, RemoteViews views) {
66 | AppWidgetTarget widgetTarget = new AppWidgetTarget(context, R.id.appwidget_icon, views, appWidgetId) {
67 | @Override
68 | public void onResourceReady(@NonNull Bitmap resource, Transition super Bitmap> transition) {
69 | super.onResourceReady(resource, transition);
70 | }
71 | };
72 |
73 | RequestOptions options = new RequestOptions().
74 | override(300, 300).placeholder(R.drawable.day).error(R.drawable.day);
75 |
76 | GlideApp.with(context.getApplicationContext())
77 | .asBitmap()
78 | .load("http:" + iconUrl)
79 | .apply(options)
80 | .into(widgetTarget);
81 | }
82 |
83 | @Override
84 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
85 | for (int appWidgetId : appWidgetIds) {
86 | updateAppWidget(context, appWidgetManager, appWidgetId);
87 | startWidgetUpdateService(context);
88 | }
89 | super.onUpdate(context, appWidgetManager, appWidgetIds);
90 | }
91 |
92 | private void startWidgetUpdateService(Context context) {
93 | manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
94 | final Intent updateIntent = new Intent(context, WidgetUpdateService.class);
95 |
96 | if (pendingIntent == null) {
97 | pendingIntent = PendingIntent.getService(context, 0, updateIntent, PendingIntent.FLAG_CANCEL_CURRENT);
98 | }
99 | manager.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(), 60000, pendingIntent);
100 | }
101 |
102 | @Override
103 | public void onEnabled(Context context) {
104 | }
105 |
106 | @Override
107 | public void onDisabled(Context context) {
108 | if (manager != null) {
109 | manager.cancel(pendingIntent);
110 | }
111 | }
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/java/ezike/tobenna/myweather/widget/WidgetUpdateService.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather.widget;
2 |
3 | import android.app.Service;
4 | import android.content.Intent;
5 | import android.os.IBinder;
6 |
7 | import timber.log.Timber;
8 |
9 | public class WidgetUpdateService extends Service {
10 |
11 | public WidgetUpdateService() {
12 | }
13 |
14 | @Override
15 | public IBinder onBind(Intent intent) {
16 | return null;
17 | }
18 |
19 | @Override
20 | public int onStartCommand(Intent intent, int flags, int startId) {
21 | WeatherWidgetProvider.updateWidget(this);
22 | Timber.d("widget update service started");
23 | return super.onStartCommand(intent, flags, startId);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/example_appwidget_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/drawable-nodpi/example_appwidget_preview.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/day.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/drawable-v24/day.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
13 |
18 |
23 |
28 |
33 |
38 |
43 |
48 |
53 |
58 |
63 |
68 |
73 |
78 |
83 |
88 |
93 |
98 |
103 |
108 |
113 |
118 |
123 |
128 |
133 |
138 |
143 |
148 |
153 |
158 |
163 |
168 |
173 |
174 |
175 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
22 |
29 |
36 |
43 |
50 |
57 |
64 |
71 |
78 |
81 |
84 |
91 |
98 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_today.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/widget_bg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/drawable/widget_bg.jpeg
--------------------------------------------------------------------------------
/app/src/main/res/font/googlesans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/font/googlesans.ttf
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/fragment_about.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
24 |
25 |
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/fragment_weather.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
22 |
23 |
26 |
27 |
28 |
29 |
33 |
34 |
40 |
41 |
52 |
53 |
63 |
64 |
73 |
74 |
82 |
83 |
94 |
95 |
106 |
107 |
108 |
119 |
120 |
131 |
132 |
133 |
134 |
140 |
141 |
151 |
152 |
163 |
164 |
175 |
176 |
189 |
190 |
200 |
201 |
211 |
212 |
222 |
223 |
232 |
233 |
244 |
245 |
246 |
247 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-v17/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
21 |
22 |
30 |
31 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
22 |
23 |
31 |
32 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_about.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
22 |
23 |
33 |
34 |
43 |
44 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_weather.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
22 |
23 |
26 |
27 |
28 |
29 |
33 |
34 |
40 |
41 |
51 |
52 |
62 |
63 |
72 |
73 |
82 |
83 |
94 |
95 |
106 |
107 |
118 |
119 |
130 |
131 |
132 |
133 |
139 |
140 |
150 |
151 |
162 |
163 |
174 |
175 |
188 |
189 |
199 |
200 |
210 |
211 |
221 |
222 |
231 |
232 |
244 |
245 |
246 |
247 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/weather_widget.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
14 |
15 |
30 |
31 |
32 |
44 |
45 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/mobile_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
16 |
17 |
21 |
22 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/transition/explode.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/transition/slide_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v14/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | 0dp
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - METRIC
5 | - IMPERIAL
6 |
7 |
8 |
9 | - Metric
10 | - Imperial
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #8157AF
4 | #00574B
5 | #D81B60
6 | #51CEEEFC
7 | #7986CB
8 | #5A67AC
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | 8dp
9 | 8dp
10 | 16sp
11 | 60sp
12 | 25sp
13 | 16dp
14 | 35sp
15 | 530dp
16 | 18sp
17 | 32dp
18 | 40sp
19 | 24dp
20 | 44sp
21 | 34sp
22 | 150dp
23 | 1dp
24 | 20sp
25 | 64dp
26 | 40dp
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MyWeather
3 | Settings
4 | Today
5 | Location Permssion
6 | You need to grant location permission in order to get weather updates
7 | weather_icon
8 | Retry
9 | Loading weather
10 | Precipitation
11 | Visibility
12 | Wind
13 | Unknown Error
14 | Check your internet connection
15 | You can set custom location in App Settings
16 | Device location not available
17 | You need to enable GPS in order to get weather updates
18 | ca-app-pub-8292346947931553~5764095591
19 | ca-app-pub-3940256099942544/6300978111
20 | weather condition
21 | Add widget
22 | condition_text
23 | No data
24 | widget background
25 | Forecast:
26 | Content loading
27 | Forecast: %1$s
28 | Data fetch failed.\nPlease check your internet connection and try again.\n\nSwipe down to retry 🙂
29 | About
30 | Version 1.0
31 | MyWeather 1.0
32 | \u2103
33 | Enable GPS
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/weather_widget_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/test/java/ezike/tobenna/myweather/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package ezike.tobenna.myweather;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.30'
5 | ext.nav_version = '2.0.0'
6 | repositories {
7 | google()
8 | jcenter()
9 | maven {
10 | url 'https://maven.fabric.io/public'
11 | }
12 | }
13 |
14 | dependencies {
15 | classpath 'com.android.tools.build:gradle:3.4.0'
16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
18 | classpath 'com.google.gms:google-services:4.2.0'
19 | classpath 'io.fabric.tools:gradle:1.28.1'
20 |
21 | // NOTE: Do not place your application dependencies here; they belong
22 | // in the individual module build.gradle files
23 | }
24 | }
25 |
26 | allprojects {
27 | repositories {
28 | google()
29 | jcenter()
30 | }
31 | }
32 |
33 | ext{
34 | // Google
35 | room_version = "2.0.0"
36 | lifecycle_version = "2.0.0"
37 | constraint_layout_version = "1.1.3"
38 | annotation_version = "1.0.2"
39 | appcompat_version = "1.0.2"
40 | material_version = "1.0.0"
41 | dagger_version = "2.20"
42 | preference_version = "1.0.0"
43 | location_version = "16.0.0"
44 | nav_version = "2.0.0"
45 | legacy_support_version = "1.0.0"
46 | ads_version = "17.2.0"
47 | multi_dex_version = "1.0.3"
48 | firebase_version = "16.0.8"
49 | crashlytics_version = "2.9.9"
50 |
51 | // Third party
52 | retrofit_version = "2.5.0"
53 | glide_version = "4.9.0"
54 | timber_version = "4.7.1"
55 | threetenabp_version = "1.2.0"
56 | moshi_version = "1.8.0"
57 | okhttp_version = "3.10.0"
58 | weather_icon_version = "1.1.0"
59 |
60 | // Testing
61 | junit_version = "4.12"
62 | espresso_version = "3.1.1"
63 | rules_version = "1.1.1"
64 | core_version = "1.1.0"
65 | hamcrest_version = "1.3"
66 |
67 | }
68 |
69 | task clean(type: Delete) {
70 | delete rootProject.buildDir
71 | }
72 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | android.databinding.enableV2=true
13 | # When configured, Gradle will run in incubating parallel mode.
14 | # This option should only be used with decoupled projects. More details, visit
15 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
16 | # org.gradle.parallel=true
17 |
18 |
19 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Apr 09 18:38:19 WAT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | weatherLocation of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | weatherLocation of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo weatherLocation of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo weatherLocation of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/weather.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ezike/MyWeather/eb338327ed00baf3155667ebfd8309fcb08cb280/weather.jks
--------------------------------------------------------------------------------