├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ └── one
│ │ └── mann
│ │ └── weatherman
│ │ ├── WeatherManApp.kt
│ │ ├── api
│ │ ├── common
│ │ │ ├── ApiDataMappers.kt
│ │ │ ├── Keys.kt
│ │ │ └── QueryInterceptor.kt
│ │ ├── openweathermap
│ │ │ ├── OwmWeatherDataSource.kt
│ │ │ ├── OwmWeatherService.kt
│ │ │ ├── WeatherCodes.kt
│ │ │ └── dto
│ │ │ │ ├── CurrentWeather.kt
│ │ │ │ ├── DailyForecast.kt
│ │ │ │ └── HourlyForecast.kt
│ │ ├── teleport
│ │ │ ├── TeleportTimezoneDataSource.kt
│ │ │ ├── TeleportTimezoneService.kt
│ │ │ └── dto
│ │ │ │ └── Timezone.kt
│ │ └── tomtom
│ │ │ ├── TomTomSearchDataSource.kt
│ │ │ ├── TomTomSearchService.kt
│ │ │ └── dto
│ │ │ └── FuzzySearch.kt
│ │ ├── common
│ │ └── Constants.kt
│ │ ├── di
│ │ ├── annotations
│ │ │ ├── keys
│ │ │ │ ├── ViewModelKey.kt
│ │ │ │ └── WorkerKey.kt
│ │ │ ├── qualifiers
│ │ │ │ ├── OpenWeatherMapApi.kt
│ │ │ │ ├── OwmAppId.kt
│ │ │ │ ├── TeleportApi.kt
│ │ │ │ ├── TomTomApi.kt
│ │ │ │ ├── TomTomKey.kt
│ │ │ │ └── Units.kt
│ │ │ └── scope
│ │ │ │ └── ActivityScope.kt
│ │ ├── components
│ │ │ ├── WeatherManAppComponent.kt
│ │ │ └── WeatherSubComponent.kt
│ │ └── modules
│ │ │ ├── WeatherManAppModule.kt
│ │ │ ├── api
│ │ │ ├── ApiDataSourceModule.kt
│ │ │ └── ApiServiceModule.kt
│ │ │ ├── framework
│ │ │ ├── DbModule.kt
│ │ │ ├── FrameworkDataSourceModule.kt
│ │ │ ├── LocationModule.kt
│ │ │ └── WorkerModule.kt
│ │ │ └── ui
│ │ │ └── ViewModelModule.kt
│ │ ├── framework
│ │ ├── data
│ │ │ ├── database
│ │ │ │ ├── DbDataMappers.kt
│ │ │ │ ├── WeatherDao.kt
│ │ │ │ ├── WeatherDb.kt
│ │ │ │ ├── WeatherDbDataSource.kt
│ │ │ │ └── entities
│ │ │ │ │ ├── City.kt
│ │ │ │ │ ├── CurrentWeather.kt
│ │ │ │ │ ├── DailyForecast.kt
│ │ │ │ │ ├── HourlyForecast.kt
│ │ │ │ │ └── relations
│ │ │ │ │ ├── CityWithCurrentWeather.kt
│ │ │ │ │ ├── CityWithDailyForecasts.kt
│ │ │ │ │ ├── CityWithHourlyForecasts.kt
│ │ │ │ │ └── CurrentWeatherWithHourlyForecasts.kt
│ │ │ ├── location
│ │ │ │ └── FusedLocationDataSource.kt
│ │ │ └── preferences
│ │ │ │ └── SettingsPreferencesDataSource.kt
│ │ └── service
│ │ │ └── workers
│ │ │ ├── NotificationWorker.kt
│ │ │ └── factory
│ │ │ ├── ChildWorkerFactory.kt
│ │ │ └── ParentWorkerFactory.kt
│ │ └── ui
│ │ ├── common
│ │ ├── base
│ │ │ ├── BaseLocationActivity.kt
│ │ │ ├── BaseUiModelWithState.kt
│ │ │ ├── BaseViewModel.kt
│ │ │ └── ViewModelFactory.kt
│ │ ├── models
│ │ │ ├── City.kt
│ │ │ ├── CurrentWeather.kt
│ │ │ ├── DailyForecast.kt
│ │ │ ├── HourlyForecast.kt
│ │ │ ├── NotificationData.kt
│ │ │ └── Weather.kt
│ │ └── util
│ │ │ ├── Extensions.kt
│ │ │ ├── UiDataMappers.kt
│ │ │ └── UiHelpers.kt
│ │ ├── detail
│ │ ├── DetailActivity.kt
│ │ ├── DetailUiModel.kt
│ │ ├── DetailViewModel.kt
│ │ ├── adapters
│ │ │ ├── DetailRecyclerAdapter.kt
│ │ │ └── WeatherViewHolder.kt
│ │ └── views
│ │ │ ├── ForecastGraphView.kt
│ │ │ └── SunPositionView.kt
│ │ ├── main
│ │ ├── CityFragment.kt
│ │ ├── MainActivity.kt
│ │ ├── MainUiModel.kt
│ │ ├── MainViewModel.kt
│ │ └── adapters
│ │ │ ├── MainViewPagerAdapter.kt
│ │ │ └── SearchCityRecyclerAdapter.kt
│ │ ├── notification
│ │ └── WeatherNotification.kt
│ │ └── settings
│ │ └── SettingsActivity.kt
│ └── res
│ ├── anim
│ ├── anim_slide_left.xml
│ └── anim_slide_up.xml
│ ├── drawable
│ ├── background_gradient_day_clear.xml
│ ├── background_gradient_day_clouds.xml
│ ├── background_gradient_night_clear.xml
│ ├── background_gradient_night_clouds.xml
│ ├── background_gradient_sunrise_clear.xml
│ ├── background_gradient_sunrise_clouds.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_notification.xml
│ ├── ll_splash.xml
│ ├── location_current.xml
│ ├── menu_add.xml
│ ├── menu_more.xml
│ ├── menu_remove.xml
│ ├── menu_settings.xml
│ ├── weather_parameter_cloud_cover.xml
│ ├── weather_parameter_humidity.xml
│ ├── weather_parameter_pressure.xml
│ ├── weather_parameter_sun_position.png
│ ├── weather_parameter_time.xml
│ ├── weather_parameter_visibility.xml
│ ├── weather_parameter_wind_deg.xml
│ ├── weather_parameter_wind_speed.xml
│ ├── weather_type_clear_day.xml
│ ├── weather_type_clear_night.xml
│ ├── weather_type_cloud_unknown.xml
│ ├── weather_type_cloudy.xml
│ ├── weather_type_cloudy_day_1.xml
│ ├── weather_type_cloudy_day_2.xml
│ ├── weather_type_cloudy_day_3.xml
│ ├── weather_type_cloudy_night_1.xml
│ ├── weather_type_cloudy_night_2.xml
│ ├── weather_type_cloudy_night_3.xml
│ ├── weather_type_hazy.xml
│ ├── weather_type_rainy_1.xml
│ ├── weather_type_rainy_2.xml
│ ├── weather_type_rainy_3.xml
│ ├── weather_type_rainy_4.xml
│ ├── weather_type_rainy_5.xml
│ ├── weather_type_rainy_6.xml
│ ├── weather_type_rainy_7.xml
│ ├── weather_type_snowy_1.xml
│ ├── weather_type_snowy_2.xml
│ ├── weather_type_snowy_3.xml
│ ├── weather_type_snowy_4.xml
│ ├── weather_type_snowy_5.xml
│ ├── weather_type_snowy_6.xml
│ └── weather_type_thunder.xml
│ ├── layout-land
│ ├── activity_detail.xml
│ ├── activity_main.xml
│ ├── fragment_city.xml
│ ├── item_weather_current.xml
│ ├── item_weather_forecast_daily.xml
│ └── view_search_city.xml
│ ├── layout
│ ├── activity_detail.xml
│ ├── activity_main.xml
│ ├── activity_settings.xml
│ ├── fragment_city.xml
│ ├── item_city_search.xml
│ ├── item_weather_conditions.xml
│ ├── item_weather_current.xml
│ ├── item_weather_forecast_daily.xml
│ ├── item_weather_forecast_hourly.xml
│ ├── item_weather_sun_cycle.xml
│ ├── notification_collapsed.xml
│ ├── notification_expanded.xml
│ └── view_search_city.xml
│ ├── menu
│ └── menu_main.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values
│ ├── arrays.xml
│ ├── colors.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── root_preferences.xml
├── build.gradle
├── domain
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── one
│ └── mann
│ └── domain
│ ├── logic
│ ├── Constants.kt
│ └── DataConverters.kt
│ └── models
│ ├── CitySearchResult.kt
│ ├── Direction.kt
│ ├── ErrorType.kt
│ ├── NotificationData.kt
│ ├── UnitsType.kt
│ ├── ViewPagerUpdateType.kt
│ ├── location
│ ├── Location.kt
│ ├── LocationServicesResponse.kt
│ └── LocationType.kt
│ └── weather
│ ├── City.kt
│ ├── CurrentWeather.kt
│ ├── DailyForecast.kt
│ ├── HourlyForecast.kt
│ └── Weather.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── interactors
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── one
│ └── mann
│ └── interactors
│ ├── data
│ ├── DataMappers.kt
│ ├── repositories
│ │ ├── CitySearchRepository.kt
│ │ └── WeatherRepository.kt
│ └── sources
│ │ ├── api
│ │ ├── CitySearchDataSource.kt
│ │ ├── TimezoneDataSource.kt
│ │ └── WeatherDataSource.kt
│ │ └── framework
│ │ ├── DatabaseDataSource.kt
│ │ ├── DeviceLocationSource.kt
│ │ └── PreferencesDataSource.kt
│ └── usecases
│ ├── AddCity.kt
│ ├── ChangeUnits.kt
│ ├── GetAllWeather.kt
│ ├── GetCitySearch.kt
│ ├── GetNotificationData.kt
│ ├── RemoveCity.kt
│ └── UpdateWeather.kt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | /app/release/
10 | /gradle-build-cache/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeatherMan
2 |
3 | An Android weather app that shows current and forecast weather for user location and other cities.
4 | Uses OpenWeatherMap API for weather information and TomTom Search API to get cities.
5 |
6 | ## Screenshots
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Project Structure
16 |
17 | The project has a modular structure and uses an implementation of Clean Architecture.
18 | MVVM pattern with the help of Android Architecture Components is used in the presentation layer.
19 | Code is written in Kotlin and uses coroutines to handle all asynchronous work.
20 |
21 | ### Modules
22 |
23 | 1) domain (Kotlin): Contains all the domain level business logic such as data entities and algorithms.
24 | 2) interactors (Kotlin): Contains usecases and repository patterns.
25 | 3) app (Android): This is the presentation module. Contains all UI and framework code (including API services).
26 |
27 | ### Dependencies
28 |
29 | * [Dagger 2](https://dagger.dev/) - Dependency Injection
30 | * [Retrofit 2](https://square.github.io/retrofit/) - Network calls
31 | * [OkHttp 3](https://square.github.io/okhttp/) - Network calls
32 | * [Room Persistence Library](https://developer.android.com/topic/libraries/architecture/room) - SQLite Database
33 | * [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - Asynchronous programming
34 | * [Work Manager](https://developer.android.com/topic/libraries/architecture/workmanager) - Background tasks (data sync, notifications)
35 | * [Google Play Services](https://developers.google.com/android/reference/com/google/android/gms/location/package-summary) - Device GPS location
36 | * [TomTom Search API](https://developer.tomtom.com/search-api) - City names and locations
37 | * [OpenWeatherMap API](https://openweathermap.org/api) - Weather information
38 | * [Teleport Timezone API](https://developers.teleport.org/api/resources/Timezone/) - Time zones
39 |
40 | ## Getting Started
41 |
42 | 1) Clone the project repository.
43 | 2) Generate keys for OpenWeatherMap API and TomTom Search API.
44 | 3) Add your keys to [app/../api/common/Keys.kt](app/src/main/java/one/mann/weatherman/api/common/Keys.kt) and rebuild project.
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | android {
6 | compileSdkVersion 33
7 | namespace 'one.mann.weatherman'
8 | buildFeatures.viewBinding = true
9 |
10 | defaultConfig {
11 | applicationId "one.mann.weatherman"
12 | minSdkVersion 21
13 | targetSdkVersion 33
14 | versionCode 3
15 | versionName "1.0.3"
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled true
22 | shrinkResources true
23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 |
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_11
29 | targetCompatibility JavaVersion.VERSION_11
30 | }
31 | }
32 |
33 | dependencies {
34 | def lifecycle_version = '2.5.1'
35 | def room_version = '2.5.0'
36 |
37 | // Modules
38 | implementation project(":interactors")
39 |
40 | /** Android Framework */
41 | // AppCompat
42 | implementation 'androidx.appcompat:appcompat:1.6.1'
43 | // Material design
44 | implementation 'com.google.android.material:material:1.8.0'
45 | // Constraint Layout
46 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
47 | // SwipeRefresh Layout
48 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
49 | // Shared Preferences
50 | implementation 'androidx.preference:preference-ktx:1.2.0'
51 | // Kotlin framework extensions
52 | implementation 'androidx.core:core-ktx:1.9.0'
53 |
54 | /** Jetpack */
55 | // ViewModel
56 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
57 | // LiveData
58 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
59 | // Lifecycle
60 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
61 | // Room Persistence Library
62 | implementation "androidx.room:room-runtime:$room_version"
63 | kapt "androidx.room:room-compiler:$room_version"
64 | implementation "androidx.room:room-ktx:$room_version"
65 | // WorkManager
66 | implementation 'androidx.work:work-runtime-ktx:2.8.0'
67 |
68 | // Kotlin Coroutines
69 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
70 |
71 | // Google Play Services - Location
72 | implementation 'com.google.android.gms:play-services-location:21.0.1'
73 |
74 | // Dagger 2
75 | implementation 'com.google.dagger:dagger:2.45'
76 | kapt 'com.google.dagger:dagger-compiler:2.45'
77 |
78 | // Retrofit
79 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
80 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
81 | // OkHttp
82 | implementation 'com.squareup.okhttp3:okhttp:4.10.0'
83 | }
84 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the sunset of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # DTO classes
24 | -keep class one.mann.weatherman.api.openweathermap.dto** { *; }
25 | -keep class one.mann.weatherman.api.teleport.dto** { *; }
26 | -keep class one.mann.weatherman.api.tomtom.dto** { *; }
27 |
28 | # OkHttp
29 | -dontwarn org.conscrypt.**
30 | -dontwarn org.bouncycastle.**
31 | -dontwarn org.openjsse.**
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
45 |
46 |
51 |
52 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/WeatherManApp.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman
2 |
3 | import android.app.Application
4 | import androidx.work.Configuration
5 | import androidx.work.WorkManager
6 | import androidx.work.WorkerFactory
7 | import one.mann.weatherman.di.components.DaggerWeatherManAppComponent
8 | import one.mann.weatherman.di.components.WeatherManAppComponent
9 | import one.mann.weatherman.di.modules.WeatherManAppModule
10 | import javax.inject.Inject
11 |
12 | /* Created by Psmann. */
13 |
14 | internal class WeatherManApp : Application() {
15 |
16 | companion object {
17 | lateinit var appComponent: WeatherManAppComponent
18 | private set
19 | }
20 |
21 | @Inject
22 | lateinit var workerFactory: WorkerFactory
23 |
24 | override fun onCreate() {
25 | super.onCreate()
26 | // Dagger setup
27 | appComponent = DaggerWeatherManAppComponent.builder()
28 | .weatherManAppModule(WeatherManAppModule(this))
29 | .build()
30 | .apply { injectApplication(this@WeatherManApp) }
31 | // WorkManager setup
32 | WorkManager.initialize(this, Configuration.Builder().setWorkerFactory(workerFactory).build())
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/common/ApiDataMappers.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.common
2 |
3 | import one.mann.domain.logic.countryCodeToEmoji
4 | import one.mann.domain.models.CitySearchResult
5 | import one.mann.weatherman.api.teleport.dto.Timezone
6 | import one.mann.weatherman.api.tomtom.dto.FuzzySearch
7 | import one.mann.domain.models.weather.CurrentWeather as DomainCurrentWeather
8 | import one.mann.domain.models.weather.DailyForecast as DomainDailyForecast
9 | import one.mann.domain.models.weather.HourlyForecast as DomainHourlyForecast
10 | import one.mann.weatherman.api.openweathermap.dto.CurrentWeather as ApiCurrentWeather
11 | import one.mann.weatherman.api.openweathermap.dto.DailyForecast as ApiDailyForecast
12 | import one.mann.weatherman.api.openweathermap.dto.HourlyForecast as ApiHourlyForecast
13 |
14 | /* Created by Psmann. */
15 |
16 | /** Map API OWM CurrentWeather to Domain, all parameters are nullable and are given default values */
17 | internal fun ApiCurrentWeather.mapToDomain(): DomainCurrentWeather = DomainCurrentWeather(
18 | 0,
19 | name ?: "Earth",
20 | main?.temp ?: 0f,
21 | main?.pressure?.toInt() ?: 0,
22 | main?.humidity?.toInt() ?: 0,
23 | weather?.get(0)?.main ?: "",
24 | weather?.get(0)?.id ?: 0,
25 | sys?.sunrise?.times(1000) ?: 1000000000000,
26 | sys?.sunset?.times(1000) ?: 1000000050000,
27 | sys?.country ?: "AC",
28 | clouds?.all?.toInt() ?: 0,
29 | wind?.speed ?: 0f,
30 | wind?.deg?.toInt() ?: 0,
31 | dt?.times(1000) ?: 1000000000000,
32 | visibility ?: 0f
33 | )
34 |
35 | /** Map API OWM HourlyForecast to Domain, all parameters are nullable and are given default values */
36 | internal fun ApiHourlyForecast.ListObject?.mapToDomain(): DomainHourlyForecast = DomainHourlyForecast(
37 | 0,
38 | this?.dt?.times(1000) ?: 1000000000000,
39 | this?.main?.temp ?: 0f,
40 | this?.weather?.get(0)?.id ?: 0
41 | )
42 |
43 | /** Map API OWM DailyForecast to Domain, all parameters are nullable and are given default values */
44 | internal fun ApiDailyForecast.ListObject?.mapToDomain(): DomainDailyForecast = DomainDailyForecast(
45 | 0,
46 | this?.dt?.times(1000) ?: 1000000000000,
47 | this?.temp?.min ?: 0f,
48 | this?.temp?.max ?: 0f,
49 | this?.weather?.get(0)?.id ?: 0
50 | )
51 |
52 | /** Map API Teleport Timezone to String to be used in Domain logic, parameter is nullable and is given a default value */
53 | internal fun Timezone?.mapToString(): String {
54 | return this?.embedded1?.locationNearestCities?.get(0)?.embedded2?.locationNearestCity?.embedded3?.cityTimezone?.ianaName
55 | ?: ""
56 | }
57 |
58 | /** Map API TomTom Search to Domain, all parameters are nullable and are given default values */
59 | internal fun FuzzySearch.mapToDomain(): List {
60 | val citySearchResultList = mutableListOf()
61 |
62 | results?.forEach { result ->
63 | result.position?.let { pos ->
64 | // Only add address if it has valid lat and lon parameters
65 | if (pos.lat != null && pos.lon != null) {
66 | val countryEmoji: String = result.address?.countryCode?.let { countryCodeToEmoji(it) } ?: ""
67 | val splitName: List? = result.address?.freeformAddress?.split(",") // Split at ','
68 |
69 | splitName?.let { name ->
70 | // Added an If condition since some names do not contain a ','
71 | citySearchResultList.add(
72 | if (name.size > 1) CitySearchResult(name[0], name[1], pos.lat, pos.lon, countryEmoji)
73 | else CitySearchResult(name[0], "", pos.lat, pos.lon, countryEmoji)
74 | )
75 | }
76 | }
77 | }
78 | }
79 |
80 | return citySearchResultList
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/common/Keys.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.common
2 |
3 | /* Created by Psmann. */
4 |
5 | internal object Keys {
6 | /** API key for OpenWeatherMap */
7 | const val OWM_APPID = ""
8 |
9 | /** API key for TomTom Search */
10 | const val TOMTOM_KEY = ""
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/common/QueryInterceptor.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.common
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Response
5 |
6 | /* Created by Psmann. */
7 |
8 | internal class QueryInterceptor(
9 | private var key: String,
10 | private var value: String
11 | ) : Interceptor {
12 |
13 | override fun intercept(chain: Interceptor.Chain): Response {
14 | val oldRequest = chain.request() // Original request
15 | val queryUrl = oldRequest.url
16 | .run {
17 | // Add new query using key-val pair
18 | newBuilder().addQueryParameter(key, value)
19 | .build()
20 | }
21 | // Rebuild the request with query
22 | val newRequest = oldRequest.newBuilder()
23 | .url(queryUrl)
24 | .build()
25 |
26 | return chain.proceed(newRequest)
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/openweathermap/OwmWeatherDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.openweathermap
2 |
3 | import one.mann.domain.models.location.Location
4 | import one.mann.domain.models.weather.CurrentWeather
5 | import one.mann.domain.models.weather.DailyForecast
6 | import one.mann.domain.models.weather.HourlyForecast
7 | import one.mann.interactors.data.sources.api.WeatherDataSource
8 | import one.mann.weatherman.api.common.mapToDomain
9 | import javax.inject.Inject
10 |
11 | /* Created by Psmann. */
12 |
13 | internal class OwmWeatherDataSource @Inject constructor(private val owmWeatherService: OwmWeatherService) :
14 | WeatherDataSource {
15 |
16 | override suspend fun getCurrentWeather(location: Location): CurrentWeather {
17 | return owmWeatherService.getCurrentWeather(location.coordinates[0], location.coordinates[1])
18 | .mapToDomain()
19 | }
20 |
21 | override suspend fun getDailyForecasts(location: Location): List {
22 | return owmWeatherService.getDailyForecast(location.coordinates[0], location.coordinates[1]).list
23 | ?.map { it.mapToDomain() }!!
24 | }
25 |
26 | override suspend fun getHourlyForecasts(location: Location): List {
27 | return owmWeatherService.getHourlyForecast(location.coordinates[0], location.coordinates[1]).list
28 | ?.map { it.mapToDomain() }!!
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/openweathermap/OwmWeatherService.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.openweathermap
2 |
3 | import one.mann.weatherman.api.openweathermap.dto.CurrentWeather
4 | import one.mann.weatherman.api.openweathermap.dto.DailyForecast
5 | import one.mann.weatherman.api.openweathermap.dto.HourlyForecast
6 | import retrofit2.http.GET
7 | import retrofit2.http.Query
8 |
9 | /* Created by Psmann. */
10 |
11 | internal interface OwmWeatherService {
12 |
13 | @GET("weather")
14 | suspend fun getCurrentWeather(
15 | @Query("lat") latitude: Float,
16 | @Query("lon") longitude: Float
17 | ): CurrentWeather
18 |
19 | @GET("forecast/daily")
20 | suspend fun getDailyForecast(
21 | @Query("lat") latitude: Float,
22 | @Query("lon") longitude: Float
23 | ): DailyForecast
24 |
25 | @GET("forecast")
26 | suspend fun getHourlyForecast(
27 | @Query("lat") latitude: Float,
28 | @Query("lon") longitude: Float,
29 | @Query("cnt") count: Int = 7 // Restrict to only next 7 three-hourly forecasts (= 21 hours)
30 | ): HourlyForecast
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/openweathermap/WeatherCodes.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.openweathermap
2 |
3 | /* Created by Psmann. */
4 |
5 | /** Return file name of vector asset (day) corresponding to the code received from API call */
6 | internal fun dayIcons(code: Int): String = when (code) {
7 | in 200..232 -> "weather_type_thunder" // Thunderstorm
8 | in 300..310 -> "weather_type_rainy_2" // Light rain
9 | in 311..321 -> "weather_type_rainy_3" // Medium rain
10 | in 500..510, in 512..531 -> "weather_type_rainy_6" // Heavy rain
11 | 511, in 603..619 -> "weather_type_rainy_7" // Freezing rain, sleet
12 | 600, 620 -> "weather_type_snowy_2" // Light snow
13 | 601, 621 -> "weather_type_snowy_3" // Medium snow
14 | 602, 622 -> "weather_type_snowy_6" // Heavy snow
15 | in 700..781 -> "weather_type_hazy" // Mist, fog, dust etc
16 | 800 -> "weather_type_clear_day" // Clear sky
17 | 801 -> "weather_type_cloudy_day_1" // Cloud cover 11%-25%
18 | 802 -> "weather_type_cloudy_day_2" // Cloud cover 26%-50%
19 | 803 -> "weather_type_cloudy_day_3" // Cloud cover 51%-85%
20 | 804 -> "weather_type_cloudy" // Cloud cover 86%-100%
21 | else -> "weather_type_cloud_unknown" // Cloud with question mark for unknown code
22 | }
23 |
24 | /** Return file name of vector asset (night) corresponding to the code received from API call */
25 | internal fun nightIcons(code: Int): String = when (code) {
26 | in 200..232 -> "weather_type_thunder" // Thunderstorm
27 | in 300..310 -> "weather_type_rainy_4" // Light rain
28 | in 311..321 -> "weather_type_rainy_5" // Medium rain
29 | in 500..510, in 512..531 -> "weather_type_rainy_6" // Heavy rain
30 | 511, in 603..619 -> "weather_type_rainy_7" // Freezing rain, sleet
31 | 600, 620 -> "weather_type_snowy_4" // Light snow
32 | 601, 621 -> "weather_type_snowy_5" // Medium snow
33 | 602, 622 -> "weather_type_snowy_6" // Heavy snow
34 | in 700..781 -> "weather_type_hazy" // Mist, fog, dust etc
35 | 800 -> "weather_type_clear_night" // Clear sky
36 | 801 -> "weather_type_cloudy_night_1" // Cloud cover 11%-25%
37 | 802 -> "weather_type_cloudy_night_2" // Cloud cover 26%-50%
38 | 803 -> "weather_type_cloudy_night_3" // Cloud cover 51%-85%
39 | 804 -> "weather_type_cloudy" // Cloud cover 86%-100%
40 | else -> "weather_type_cloud_unknown" // Cloud with question mark for unknown code
41 | }
42 |
43 | /** Check if current weather is overcast */
44 | internal fun isOvercast(code: Int): Boolean = when (code) {
45 | in 200..232, in 500..510, in 512..531, 511, in 602..619, 622, in 700..781, 804 -> true
46 | else -> false
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/openweathermap/dto/CurrentWeather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.openweathermap.dto
2 |
3 | /* Created by Psmann. */
4 |
5 | /**
6 | * Data Transfer Object (model) for OpenWeatherMap Weather API
7 | * All parameters are nullable to maintain Kotlin null-safety
8 | */
9 | internal data class CurrentWeather(
10 | val main: Main?,
11 | val sys: Sys?,
12 | val wind: Wind?,
13 | val clouds: Clouds?,
14 | val weather: List?,
15 | val name: String?,
16 | val dt: Long?,
17 | val visibility: Float?
18 | ) {
19 |
20 | data class Clouds(val all: Float?)
21 |
22 | data class Main(
23 | val temp: Float?,
24 | val pressure: Float?,
25 | val humidity: Float?
26 | )
27 |
28 | data class Sys(
29 | val sunrise: Long?,
30 | val sunset: Long?,
31 | val country: String?
32 | )
33 |
34 | data class Weather(
35 | val main: String?,
36 | val id: Int?
37 | )
38 |
39 | data class Wind(
40 | val speed: Float?,
41 | val deg: Float?
42 | )
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/openweathermap/dto/DailyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.openweathermap.dto
2 |
3 | /* Created by Psmann. */
4 |
5 | /**
6 | * Data Transfer Object (model) for OpenWeatherMap DailyForecast API
7 | * All parameters are nullable to maintain Kotlin null-safety
8 | */
9 | internal data class DailyForecast(val list: List?) {
10 |
11 | data class ListObject(
12 | val dt: Long?,
13 | val temp: Temp?,
14 | val weather: List?
15 | )
16 |
17 | data class Temp(
18 | val min: Float?,
19 | val max: Float?
20 | )
21 |
22 | data class Weather(val id: Int?)
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/openweathermap/dto/HourlyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.openweathermap.dto
2 |
3 | /* Created by Psmann. */
4 |
5 | /**
6 | * Data Transfer Object (model) for OpenWeatherMap HourlyForecast API
7 | * All parameters are nullable to maintain Kotlin null-safety
8 | */
9 | internal data class HourlyForecast(val list: List?) {
10 |
11 | data class ListObject(
12 | val dt: Long?,
13 | val main: Main?,
14 | val weather: List?
15 | )
16 |
17 | data class Main(val temp: Float?)
18 |
19 | data class Weather(val id: Int?)
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/teleport/TeleportTimezoneDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.teleport
2 |
3 | import one.mann.domain.models.location.Location
4 | import one.mann.interactors.data.sources.api.TimezoneDataSource
5 | import one.mann.weatherman.api.common.mapToString
6 | import javax.inject.Inject
7 |
8 | /* Created by Psmann. */
9 |
10 | internal class TeleportTimezoneDataSource @Inject constructor(
11 | private val teleportTimezoneService: TeleportTimezoneService
12 | ) : TimezoneDataSource {
13 |
14 | override suspend fun getTimezone(location: Location): String {
15 | return teleportTimezoneService.getTimezone(
16 | location.coordinates[0].toString(),
17 | location.coordinates[1].toString()
18 | ).mapToString()
19 | }
20 |
21 | override suspend fun getAllTimezone(locations: List): List = locations.map {
22 | teleportTimezoneService.getTimezone(
23 | it.coordinates[0].toString(),
24 | it.coordinates[1].toString()
25 | ).mapToString()
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/teleport/TeleportTimezoneService.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.teleport
2 |
3 | import one.mann.weatherman.api.teleport.dto.Timezone
4 | import retrofit2.http.GET
5 | import retrofit2.http.Path
6 |
7 | /* Created by Psmann. */
8 |
9 | internal interface TeleportTimezoneService {
10 |
11 | @GET("{lat},{long}/?embed=location:nearest-cities/location:nearest-city/city:timezone")
12 | suspend fun getTimezone(
13 | @Path("lat") latitude: String,
14 | @Path("long") longitude: String
15 | ): Timezone?
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/teleport/dto/Timezone.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.teleport.dto
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | /* Created by Psmann. */
6 |
7 | /**
8 | * Data Transfer Object (model) for Teleport TimeZone API
9 | * All parameters are nullable to maintain Kotlin null-safety
10 | */
11 | internal data class Timezone(
12 | @SerializedName("_embedded")
13 | val embedded1: Embedded1?
14 | ) {
15 |
16 | data class CityTimezone(
17 | @SerializedName("iana_name")
18 | val ianaName: String?
19 | )
20 |
21 | data class Embedded1(
22 | @SerializedName("location:nearest-cities")
23 | val locationNearestCities: List?
24 | )
25 |
26 | data class Embedded2(
27 | @SerializedName("location:nearest-city")
28 | val locationNearestCity: LocationNearestCity?
29 | )
30 |
31 | data class Embedded3(
32 | @SerializedName("city:timezone")
33 | val cityTimezone: CityTimezone?
34 | )
35 |
36 | data class LocationNearestCities(
37 | @SerializedName("_embedded")
38 | val embedded2: Embedded2?
39 | )
40 |
41 | data class LocationNearestCity(
42 | @SerializedName("_embedded")
43 | val embedded3: Embedded3?
44 | )
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/tomtom/TomTomSearchDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.tomtom
2 |
3 | import one.mann.domain.models.CitySearchResult
4 | import one.mann.interactors.data.sources.api.CitySearchDataSource
5 | import one.mann.weatherman.api.common.mapToDomain
6 | import javax.inject.Inject
7 |
8 | /* Created by Psmann. */
9 |
10 | internal class TomTomSearchDataSource @Inject constructor(private val tomTomSearchService: TomTomSearchService) :
11 | CitySearchDataSource {
12 |
13 | override suspend fun getCitySearch(cityNameQuery: String): List {
14 | return tomTomSearchService.getSearch(cityNameQuery).mapToDomain()
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/tomtom/TomTomSearchService.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.tomtom
2 |
3 | import one.mann.weatherman.api.tomtom.dto.FuzzySearch
4 | import retrofit2.http.GET
5 | import retrofit2.http.Path
6 | import retrofit2.http.Query
7 |
8 | /* Created by Psmann. */
9 |
10 | internal interface TomTomSearchService {
11 |
12 | @GET("{query}.json")
13 | suspend fun getSearch(
14 | @Path("query") citySearchQuery: String,
15 | @Query("typeahead") typeahead: Boolean = true,
16 | @Query("limit") limit: Int = 5,
17 | @Query("idxSet") idxSet: String = "Geo"
18 | ): FuzzySearch
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/api/tomtom/dto/FuzzySearch.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.api.tomtom.dto
2 |
3 | /* Created by Psmann. */
4 |
5 | /**
6 | * Data Transfer Object (model) for TomTom Fuzzy Search API
7 | * All parameters are nullable to maintain Kotlin null-safety
8 | */
9 | internal data class FuzzySearch(val results: List?) {
10 |
11 | data class Results(
12 | val address: Address?,
13 | val position: Position?
14 | )
15 |
16 | data class Address(
17 | val freeformAddress: String?,
18 | val countryCode: String?
19 | )
20 |
21 | data class Position(
22 | val lat: Float?,
23 | val lon: Float?
24 | )
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/common/Constants.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.common
2 |
3 | /* Created by Psmann. */
4 |
5 | /** Preferences */
6 | internal const val SETTINGS_UNITS_KEY = "units"
7 | internal const val SETTINGS_UNITS_DEFAULT = "metric"
8 | internal const val SETTINGS_NOTIFICATIONS_KEY = "notification"
9 | internal const val SETTINGS_NOTIFICATIONS_DEFAULT = true
10 | internal const val SETTINGS_FREQUENCY_KEY = "notification_frequency"
11 | internal const val SETTINGS_FREQUENCY_DEFAULT = "24"
12 | internal const val NAVIGATION_GUIDE_KEY = "navigation_guide"
13 | internal const val NAVIGATION_GUIDE_DEFAULT = false
14 | internal const val LAST_UPDATED_KEY = "last_updated_time"
15 | internal const val LAST_UPDATED_DEFAULT = 0L
16 | internal const val LAST_CHECKED_KEY = "last_checked_time"
17 |
18 | /** WorkManager */
19 | internal const val NOTIFICATION_WORKER = "NOTIFICATION_WORKER"
20 | internal const val NOTIFICATION_WORKER_TAG = "WORK_COMPLETED"
21 |
22 | /** Notification Channel */
23 | internal const val NOTIFICATION_CHANNEL_NAME = "WeatherMan Notifications"
24 | internal const val NOTIFICATION_CHANNEL_ID = "WEATHERMAN_NOTIFICATION"
25 | internal const val NOTIFICATION_ID = 1
26 |
27 | /** Bundle */
28 | internal const val PAGER_POSITION = "pager_position"
29 | internal const val PAGER_COUNT = "pager_count"
30 | internal const val ACTIVITY_BACKGROUND = "activity_background"
31 | internal const val DETAIL_BUTTON_CLICKED = "detail_button_clicked"
32 |
33 | internal const val MAXIMUM_CITIES_ALLOWED = 10
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/keys/ViewModelKey.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.keys
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.MapKey
5 | import kotlin.annotation.AnnotationTarget.*
6 | import kotlin.reflect.KClass
7 |
8 | /* Created by Psmann. */
9 |
10 | @MapKey
11 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
12 | @Retention(AnnotationRetention.RUNTIME)
13 | internal annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/keys/WorkerKey.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.keys
2 |
3 | import androidx.work.ListenableWorker
4 | import dagger.MapKey
5 | import kotlin.annotation.AnnotationTarget.*
6 | import kotlin.reflect.KClass
7 |
8 | /* Created by Psmann. */
9 |
10 | @MapKey
11 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
12 | @Retention(AnnotationRetention.RUNTIME)
13 | internal annotation class WorkerKey(val value: KClass)
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/qualifiers/OpenWeatherMapApi.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | /* Created by Psmann. */
6 |
7 | @Qualifier
8 | @Retention(AnnotationRetention.RUNTIME)
9 | internal annotation class OpenWeatherMapApi
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/qualifiers/OwmAppId.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | /* Created by Psmann. */
6 |
7 | @Qualifier
8 | @Retention(AnnotationRetention.RUNTIME)
9 | internal annotation class OwmAppId
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/qualifiers/TeleportApi.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | /* Created by Psmann. */
6 |
7 | @Qualifier
8 | @Retention(AnnotationRetention.RUNTIME)
9 | internal annotation class TeleportApi
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/qualifiers/TomTomApi.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | /* Created by Psmann. */
6 |
7 | @Qualifier
8 | @Retention(AnnotationRetention.RUNTIME)
9 | internal annotation class TomTomApi
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/qualifiers/TomTomKey.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | /* Created by Psmann. */
6 |
7 | @Qualifier
8 | @Retention(AnnotationRetention.RUNTIME)
9 | internal annotation class TomTomKey
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/qualifiers/Units.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | /* Created by Psmann. */
6 |
7 | @Qualifier
8 | @Retention(AnnotationRetention.RUNTIME)
9 | internal annotation class Units
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/annotations/scope/ActivityScope.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.annotations.scope
2 |
3 | import javax.inject.Scope
4 |
5 | /* Created by Psmann. */
6 |
7 | @Scope
8 | @Retention(AnnotationRetention.RUNTIME)
9 | internal annotation class ActivityScope
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/components/WeatherManAppComponent.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.components
2 |
3 | import dagger.Component
4 | import one.mann.weatherman.WeatherManApp
5 | import one.mann.weatherman.di.modules.WeatherManAppModule
6 | import one.mann.weatherman.di.modules.api.ApiDataSourceModule
7 | import one.mann.weatherman.di.modules.api.ApiServiceModule
8 | import one.mann.weatherman.di.modules.framework.DbModule
9 | import one.mann.weatherman.di.modules.framework.FrameworkDataSourceModule
10 | import one.mann.weatherman.di.modules.framework.LocationModule
11 | import one.mann.weatherman.di.modules.framework.WorkerModule
12 | import one.mann.weatherman.di.modules.ui.ViewModelModule
13 | import javax.inject.Singleton
14 |
15 | /* Created by Psmann. */
16 |
17 | @Singleton
18 | @Component(
19 | modules = [
20 | WeatherManAppModule::class,
21 | ApiServiceModule::class,
22 | LocationModule::class,
23 | DbModule::class,
24 | WorkerModule::class,
25 | ViewModelModule::class,
26 | ApiDataSourceModule::class,
27 | FrameworkDataSourceModule::class
28 | ]
29 | )
30 | internal interface WeatherManAppComponent {
31 |
32 | fun getSubComponent(): WeatherSubComponent
33 |
34 | fun injectApplication(weatherManApp: WeatherManApp)
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/components/WeatherSubComponent.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.components
2 |
3 | import dagger.Subcomponent
4 | import one.mann.weatherman.di.annotations.scope.ActivityScope
5 | import one.mann.weatherman.ui.detail.DetailActivity
6 | import one.mann.weatherman.ui.main.CityFragment
7 | import one.mann.weatherman.ui.main.MainActivity
8 |
9 | /* Created by Psmann. */
10 |
11 | @ActivityScope
12 | @Subcomponent
13 | internal interface WeatherSubComponent {
14 |
15 | fun injectMainActivity(mainActivity: MainActivity)
16 |
17 | fun injectMainFragment(cityFragment: CityFragment)
18 |
19 | fun injectDetailActivity(detailActivity: DetailActivity)
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/modules/WeatherManAppModule.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.modules
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.core.content.edit
6 | import androidx.preference.PreferenceManager
7 | import androidx.work.WorkManager
8 | import dagger.Module
9 | import dagger.Provides
10 | import one.mann.weatherman.WeatherManApp
11 | import one.mann.weatherman.common.SETTINGS_UNITS_DEFAULT
12 | import one.mann.weatherman.common.SETTINGS_UNITS_KEY
13 | import javax.inject.Inject
14 | import javax.inject.Singleton
15 |
16 | /* Created by Psmann. */
17 |
18 | @Module
19 | internal class WeatherManAppModule @Inject constructor(private val application: WeatherManApp) {
20 |
21 | @Provides
22 | @Singleton
23 | fun provideApplicationContext(): Context = application
24 |
25 | @Provides
26 | @Singleton
27 | fun provideDefaultPreferences(context: Context): SharedPreferences {
28 | return PreferenceManager.getDefaultSharedPreferences(context).apply {
29 | if (getString(SETTINGS_UNITS_KEY, "")!! == "") edit {
30 | putString(SETTINGS_UNITS_KEY, SETTINGS_UNITS_DEFAULT)
31 | }
32 | }
33 | }
34 |
35 | @Provides
36 | @Singleton
37 | fun provideWorkManager(context: Context): WorkManager = WorkManager.getInstance(context)
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/modules/api/ApiDataSourceModule.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.modules.api
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import one.mann.interactors.data.sources.api.CitySearchDataSource
6 | import one.mann.interactors.data.sources.api.TimezoneDataSource
7 | import one.mann.interactors.data.sources.api.WeatherDataSource
8 | import one.mann.weatherman.api.openweathermap.OwmWeatherDataSource
9 | import one.mann.weatherman.api.teleport.TeleportTimezoneDataSource
10 | import one.mann.weatherman.api.tomtom.TomTomSearchDataSource
11 |
12 | /* Created by Psmann. */
13 |
14 | @Module
15 | internal abstract class ApiDataSourceModule {
16 |
17 | @Binds
18 | abstract fun bindWeatherDataSource(owmWeatherDataSource: OwmWeatherDataSource): WeatherDataSource
19 |
20 | @Binds
21 | abstract fun bindTimezoneDataSource(teleportTimezoneDataSource: TeleportTimezoneDataSource): TimezoneDataSource
22 |
23 | @Binds
24 | abstract fun bindCitySearchDataSource(tomTomSearchDataSource: TomTomSearchDataSource): CitySearchDataSource
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/modules/framework/DbModule.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.modules.framework
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import dagger.Module
6 | import dagger.Provides
7 | import one.mann.weatherman.framework.data.database.WeatherDb
8 | import javax.inject.Singleton
9 |
10 | /* Created by Psmann. */
11 |
12 | @Module
13 | internal class DbModule {
14 |
15 | companion object {
16 | private const val DB_NAME = "weather-db"
17 | }
18 |
19 | @Provides
20 | @Singleton
21 | fun provideDb(context: Context): WeatherDb = Room.databaseBuilder(context, WeatherDb::class.java, DB_NAME).build()
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/modules/framework/FrameworkDataSourceModule.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.modules.framework
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import one.mann.interactors.data.sources.framework.DatabaseDataSource
6 | import one.mann.interactors.data.sources.framework.DeviceLocationSource
7 | import one.mann.interactors.data.sources.framework.PreferencesDataSource
8 | import one.mann.weatherman.framework.data.database.WeatherDbDataSource
9 | import one.mann.weatherman.framework.data.location.FusedLocationDataSource
10 | import one.mann.weatherman.framework.data.preferences.SettingsPreferencesDataSource
11 |
12 | /* Created by Psmann. */
13 |
14 | @Module
15 | internal abstract class FrameworkDataSourceModule {
16 |
17 | @Binds
18 | abstract fun bindDatabaseDataSource(weatherDbDataSource: WeatherDbDataSource): DatabaseDataSource
19 |
20 | @Binds
21 | abstract fun bindDeviceLocationDataSource(fusedLocationDataSource: FusedLocationDataSource): DeviceLocationSource
22 |
23 | @Binds
24 | abstract fun bindPreferencesDataSource(settingsPreferencesDataSource: SettingsPreferencesDataSource): PreferencesDataSource
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/modules/framework/LocationModule.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.modules.framework
2 |
3 | import android.content.Context
4 | import com.google.android.gms.location.FusedLocationProviderClient
5 | import com.google.android.gms.location.LocationServices
6 | import dagger.Module
7 | import dagger.Provides
8 | import javax.inject.Singleton
9 |
10 | /* Created by Psmann. */
11 |
12 | @Module
13 | class LocationModule {
14 |
15 | @Provides
16 | @Singleton
17 | fun provideFusedLocationProvider(context: Context): FusedLocationProviderClient {
18 | return LocationServices.getFusedLocationProviderClient(context)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/modules/framework/WorkerModule.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.modules.framework
2 |
3 | import androidx.work.WorkerFactory
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.multibindings.IntoMap
7 | import one.mann.weatherman.di.annotations.keys.WorkerKey
8 | import one.mann.weatherman.framework.service.workers.NotificationWorker
9 | import one.mann.weatherman.framework.service.workers.factory.ChildWorkerFactory
10 | import one.mann.weatherman.framework.service.workers.factory.ParentWorkerFactory
11 |
12 | /* Created by Psmann. */
13 |
14 | @Module
15 | internal abstract class WorkerModule {
16 |
17 | @Binds
18 | abstract fun bindParentWorkerFactory(factory: ParentWorkerFactory): WorkerFactory
19 |
20 | @Binds
21 | @IntoMap
22 | @WorkerKey(NotificationWorker::class)
23 | abstract fun bindUpdateWeatherWorker(worker: NotificationWorker.Factory): ChildWorkerFactory
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/di/modules/ui/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.di.modules.ui
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 one.mann.weatherman.di.annotations.keys.ViewModelKey
9 | import one.mann.weatherman.ui.common.base.ViewModelFactory
10 | import one.mann.weatherman.ui.detail.DetailViewModel
11 | import one.mann.weatherman.ui.main.MainViewModel
12 |
13 | @Module
14 | internal abstract class ViewModelModule {
15 |
16 | @Binds
17 | abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
18 |
19 | @Binds
20 | @IntoMap
21 | @ViewModelKey(MainViewModel::class)
22 | abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
23 |
24 | @Binds
25 | @IntoMap
26 | @ViewModelKey(DetailViewModel::class)
27 | abstract fun bindDetailViewModel(viewModel: DetailViewModel): ViewModel
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/WeatherDb.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import one.mann.weatherman.framework.data.database.entities.City
6 | import one.mann.weatherman.framework.data.database.entities.CurrentWeather
7 | import one.mann.weatherman.framework.data.database.entities.DailyForecast
8 | import one.mann.weatherman.framework.data.database.entities.HourlyForecast
9 |
10 | /* Created by Psmann. */
11 |
12 | @Database(
13 | entities = [
14 | City::class,
15 | CurrentWeather::class,
16 | DailyForecast::class,
17 | HourlyForecast::class
18 | ],
19 | version = 1,
20 | exportSchema = false
21 | )
22 | internal abstract class WeatherDb : RoomDatabase() {
23 |
24 | abstract fun weatherDao(): WeatherDao
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/WeatherDbDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database
2 |
3 | import one.mann.domain.models.NotificationData
4 | import one.mann.interactors.data.sources.framework.DatabaseDataSource
5 | import one.mann.weatherman.framework.data.database.entities.CurrentWeather
6 | import one.mann.weatherman.framework.data.database.entities.DailyForecast
7 | import one.mann.weatherman.framework.data.database.entities.HourlyForecast
8 | import javax.inject.Inject
9 | import one.mann.domain.models.weather.Weather as DomainWeather
10 |
11 | /* Created by Psmann. */
12 |
13 | internal class WeatherDbDataSource @Inject constructor(db: WeatherDb) : DatabaseDataSource {
14 |
15 | private val dao = db.weatherDao()
16 |
17 | /** Insert a new City along with its weather information (i.e. CurrentWeather, DailyForecasts and HourlyForecasts) */
18 | override suspend fun insertCityAndWeather(weather: DomainWeather) {
19 | dao.insertCity(weather.mapToDbCity())
20 | dao.insertCurrentWeather(weather.mapToDbCurrentWeather())
21 | dao.insertDailyForecasts(weather.mapToDbDailyForecasts())
22 | dao.insertHourlyForecasts(weather.mapToDbHourlyForecasts())
23 | }
24 |
25 | /** Get all the notification data from the database */
26 | override suspend fun getNotificationData(): NotificationData {
27 | val userCity = dao.getCityNameForUserLocation()
28 | val todayForecast = dao.getTodayForecastForUserLocation(userCity.cityId)
29 | val currentWeatherWithHourlyForecasts = dao.getHourlyForecastsForUserLocation(userCity.cityId)
30 |
31 | return currentWeatherWithHourlyForecasts.mapToDomain(userCity.cityName, todayForecast)
32 | }
33 |
34 | /** Get all Cities, CurrentWeathers, DailyForecasts and HourlyForecasts and pass them as Domain Weather objects */
35 | override suspend fun getAllCitiesAndWeathers(): List {
36 | val cities = dao.getAllCities()
37 | val weathers = mutableListOf()
38 |
39 | cities.forEach {
40 | val currentWeather = dao.getCurrentWeather(it.cityId).currentWeather
41 | val dailyForecasts = dao.getCDailyForecasts(it.cityId).getSortedForecast()
42 | val hourlyForecasts = dao.getHourlyForecasts(it.cityId).getSortedForecast()
43 | weathers.add(it.mapToDomainWeather(currentWeather, dailyForecasts, hourlyForecasts))
44 | }
45 |
46 | return weathers
47 | }
48 |
49 | /** Update the lastChecked value in CurrentWeather entity for all the rows */
50 | override suspend fun updateLastChecked(lastChecked: Long) {
51 | val updatedCurrentWeathers = dao.getAllCurrentWeather().map { it.copy(lastChecked = lastChecked) }
52 | dao.updateCurrentWeathers(updatedCurrentWeathers)
53 | }
54 |
55 | /** Update All CurrentWeathers, DailyForecasts and HourlyForecasts */
56 | override suspend fun updateAllWeathers(weathers: List) {
57 | val currentWeathersDb = mutableListOf()
58 | val dailyForecastsDb = mutableListOf()
59 | val hourlyForecastsDb = mutableListOf()
60 |
61 | weathers.forEach {
62 | currentWeathersDb.add(it.mapToDbCurrentWeather())
63 | dailyForecastsDb.addAll(it.mapToDbDailyForecasts())
64 | hourlyForecastsDb.addAll(it.mapToDbHourlyForecasts())
65 | }
66 | // Only update user city in the database as only it can change (i.e. if user location changes)
67 | dao.updateCity(weathers[0].mapToDbCity())
68 | dao.updateCurrentWeathers(currentWeathersDb)
69 | dao.updateDailyForecasts(dailyForecastsDb)
70 | dao.updateHourlyForecasts(hourlyForecastsDb)
71 | }
72 |
73 | /** Delete a City as well as all the weather information associated with it */
74 | override suspend fun deleteCityAndWeather(cityId: String) {
75 | dao.deleteCity(cityId)
76 | dao.deleteCurrentWeather(cityId)
77 | dao.deleteDailyForecasts(cityId)
78 | dao.deleteHourlyForecasts(cityId)
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/City.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | /* Created by Psmann. */
7 |
8 | @Entity
9 | internal data class City(
10 | @PrimaryKey(autoGenerate = false) val cityId: String,
11 | val cityName: String,
12 | val coordinatesLat: Float,
13 | val coordinatesLong: Float,
14 | val timezone: String,
15 | val timeCreated: Long // Used to order the list
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/CurrentWeather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | /* Created by Psmann. */
7 |
8 | @Entity
9 | internal data class CurrentWeather(
10 | @PrimaryKey(autoGenerate = true) val weatherId: Int = 0,
11 | val currentTemperature: Float,
12 | val feelsLike: Float,
13 | val pressure: Int,
14 | val humidity: Int,
15 | val description: String,
16 | val iconId: Int,
17 | val sunrise: Long,
18 | val sunset: Long,
19 | val countryFlag: String,
20 | val clouds: Int,
21 | val windSpeed: Float,
22 | val windDirection: Int,
23 | val lastUpdated: Long,
24 | val visibility: Float,
25 | val dayLength: String,
26 | val lastChecked: Long,
27 | val sunPosition: Float,
28 | val cityId: String
29 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/DailyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | /* Created by Psmann. */
7 |
8 | @Entity
9 | internal data class DailyForecast(
10 | @PrimaryKey(autoGenerate = true) val dailyId: Int = 0,
11 | val date: Long,
12 | val minTemp: Float,
13 | val maxTemp: Float,
14 | val iconId: Int,
15 | val cityId: String
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/HourlyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | /* Created by Psmann. */
7 |
8 | @Entity
9 | internal data class HourlyForecast(
10 | @PrimaryKey(autoGenerate = true) val hourlyId: Int = 0,
11 | val time: Long,
12 | val temperature: Float,
13 | val iconId: Int,
14 | val sunPosition: Float,
15 | val cityId: String
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/relations/CityWithCurrentWeather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities.relations
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 | import one.mann.weatherman.framework.data.database.entities.City
6 | import one.mann.weatherman.framework.data.database.entities.CurrentWeather
7 |
8 | /* Created by Psmann. */
9 |
10 | internal data class CityWithCurrentWeather(
11 | @Embedded val city: City,
12 | @Relation(
13 | parentColumn = "cityId",
14 | entityColumn = "cityId"
15 | )
16 | val currentWeather: CurrentWeather
17 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/relations/CityWithDailyForecasts.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities.relations
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 | import one.mann.weatherman.framework.data.database.entities.City
6 | import one.mann.weatherman.framework.data.database.entities.DailyForecast
7 |
8 | /* Created by Psmann. */
9 |
10 | internal data class CityWithDailyForecasts(
11 | @Embedded val city: City,
12 | @Relation(
13 | parentColumn = "cityId",
14 | entityColumn = "cityId"
15 | )
16 | val dailyForecasts: List
17 | ) {
18 | /** Returns a list sorted in ascending order using variable 'date' */
19 | fun getSortedForecast(): List = dailyForecasts.sortedBy { it.date }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/relations/CityWithHourlyForecasts.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities.relations
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 | import one.mann.weatherman.framework.data.database.entities.City
6 | import one.mann.weatherman.framework.data.database.entities.HourlyForecast
7 |
8 | /* Created by Psmann. */
9 |
10 | internal data class CityWithHourlyForecasts(
11 | @Embedded val city: City,
12 | @Relation(
13 | parentColumn = "cityId",
14 | entityColumn = "cityId"
15 | )
16 | val hourlyForecasts: List
17 | ) {
18 | /** Returns a list sorted in ascending order using variable 'time' */
19 | fun getSortedForecast(): List = hourlyForecasts.sortedBy { it.time }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/database/entities/relations/CurrentWeatherWithHourlyForecasts.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.database.entities.relations
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 | import one.mann.weatherman.framework.data.database.entities.CurrentWeather
6 | import one.mann.weatherman.framework.data.database.entities.HourlyForecast
7 |
8 | /* Created by Psmann. */
9 |
10 | internal data class CurrentWeatherWithHourlyForecasts(
11 | @Embedded val currentWeather: CurrentWeather,
12 | @Relation(
13 | parentColumn = "cityId",
14 | entityColumn = "cityId"
15 | )
16 | val hourlyForecasts: List
17 | ) {
18 | /** Returns a list sorted in ascending order using variable 'time' */
19 | fun getSortedForecast(): List = hourlyForecasts.sortedBy { it.time }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/location/FusedLocationDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.location
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Handler
5 | import android.os.HandlerThread
6 | import com.google.android.gms.location.*
7 | import kotlinx.coroutines.android.asCoroutineDispatcher
8 | import kotlinx.coroutines.suspendCancellableCoroutine
9 | import one.mann.domain.logic.truncate
10 | import one.mann.domain.models.location.Location
11 | import one.mann.interactors.data.sources.framework.DeviceLocationSource
12 | import javax.inject.Inject
13 | import kotlin.coroutines.resume
14 |
15 | /* Created by Psmann. */
16 |
17 | /** Data source for device GPS location */
18 | internal class FusedLocationDataSource @Inject constructor(private val client: FusedLocationProviderClient) :
19 | DeviceLocationSource {
20 |
21 | @SuppressLint("MissingPermission") // Already being checked
22 | override suspend fun getLocation(): Location = suspendCancellableCoroutine { continuation ->
23 | val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10 * 1000L).build()
24 | val locationCallback = object : LocationCallback() {
25 | override fun onLocationResult(locationResult: LocationResult) {
26 | for (location in locationResult.locations) {
27 | client.removeLocationUpdates(this)
28 | continuation.resume(
29 | Location(
30 | listOf(
31 | location.latitude.toFloat(),
32 | location.longitude.toFloat()
33 | )
34 | ).truncate()
35 | )
36 | }
37 | }
38 | }
39 | // Handler thread for requestLocationUpdates() since it doesn't seem to run on current thread anymore
40 | val handlerThread = HandlerThread("LocationUpdate-Thread").apply { start() }
41 | Handler(handlerThread.looper).asCoroutineDispatcher()
42 | client.requestLocationUpdates(locationRequest, locationCallback, handlerThread.looper)
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/data/preferences/SettingsPreferencesDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.data.preferences
2 |
3 | import android.content.SharedPreferences
4 | import one.mann.interactors.data.sources.framework.PreferencesDataSource
5 | import one.mann.weatherman.common.LAST_UPDATED_DEFAULT
6 | import one.mann.weatherman.common.LAST_UPDATED_KEY
7 | import one.mann.weatherman.common.SETTINGS_UNITS_DEFAULT
8 | import one.mann.weatherman.common.SETTINGS_UNITS_KEY
9 | import javax.inject.Inject
10 |
11 | /* Created by Psmann. */
12 |
13 | internal class SettingsPreferencesDataSource @Inject constructor(private val preferences: SharedPreferences) :
14 | PreferencesDataSource {
15 |
16 | override suspend fun getUnits(): String = preferences.getString(SETTINGS_UNITS_KEY, SETTINGS_UNITS_DEFAULT)!!
17 |
18 | override suspend fun getLastUpdated(): Long = preferences.getLong(LAST_UPDATED_KEY, LAST_UPDATED_DEFAULT)
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/service/workers/NotificationWorker.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.service.workers
2 |
3 | import android.content.Context
4 | import androidx.work.CoroutineWorker
5 | import androidx.work.WorkerParameters
6 | import one.mann.domain.models.location.LocationType
7 | import one.mann.interactors.usecases.UpdateWeather
8 | import one.mann.weatherman.framework.service.workers.factory.ChildWorkerFactory
9 | import one.mann.weatherman.ui.common.util.isLocationEnabled
10 | import one.mann.weatherman.ui.notification.WeatherNotification
11 | import javax.inject.Inject
12 |
13 | /* Created by Psmann. */
14 |
15 | internal class NotificationWorker(
16 | private val updateWeather: UpdateWeather,
17 | private val weatherNotification: WeatherNotification,
18 | private val context: Context,
19 | params: WorkerParameters
20 | ) : CoroutineWorker(context, params) {
21 |
22 | /** Tasks are enqueued inside a single Worker because PeriodicWork doesn't allow chaining of Workers */
23 | override suspend fun doWork(): Result = try {
24 | // Use GPS if location services are enabled otherwise use previously saved location
25 | updateWeather.invoke(if (context.isLocationEnabled()) LocationType.DEVICE else LocationType.DB)
26 | // Show notification
27 | weatherNotification.show()
28 | Result.success()
29 | } catch (e: Exception) {
30 | Result.failure()
31 | }
32 |
33 | class Factory @Inject constructor(
34 | private val updateWeather: UpdateWeather,
35 | private val weatherNotification: WeatherNotification
36 | ) : ChildWorkerFactory {
37 |
38 | override fun create(appContext: Context, params: WorkerParameters): CoroutineWorker {
39 | return NotificationWorker(updateWeather, weatherNotification, appContext, params)
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/service/workers/factory/ChildWorkerFactory.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.service.workers.factory
2 |
3 | import android.content.Context
4 | import androidx.work.CoroutineWorker
5 | import androidx.work.WorkerParameters
6 |
7 | /* Created by Psmann. */
8 |
9 | internal interface ChildWorkerFactory {
10 |
11 | fun create(appContext: Context, params: WorkerParameters): CoroutineWorker
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/framework/service/workers/factory/ParentWorkerFactory.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.framework.service.workers.factory
2 |
3 | import android.content.Context
4 | import androidx.work.ListenableWorker
5 | import androidx.work.WorkerFactory
6 | import androidx.work.WorkerParameters
7 | import javax.inject.Inject
8 | import javax.inject.Provider
9 |
10 | /* Created by Psmann. */
11 |
12 | internal class ParentWorkerFactory @Inject constructor(
13 | private val workerFactory: Map, @JvmSuppressWildcards Provider>
14 | ) : WorkerFactory() {
15 |
16 | override fun createWorker(
17 | appContext: Context,
18 | workerClassName: String,
19 | workerParameters: WorkerParameters
20 | ): ListenableWorker? {
21 | val factoryEntry = workerFactory.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
22 |
23 | // Use custom factory if available else use default implementation
24 | return if (factoryEntry != null) {
25 | val factoryProvider = factoryEntry.value
26 | factoryProvider.get().create(appContext, workerParameters)
27 | } else {
28 | val workerClass = Class.forName(workerClassName).asSubclass(ListenableWorker::class.java)
29 | val constructor = workerClass.getDeclaredConstructor(Context::class.java, WorkerParameters::class.java)
30 | constructor.newInstance(appContext, workerParameters)
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/base/BaseUiModelWithState.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.base
2 |
3 |
4 | /* Created by Psmann. */
5 |
6 | interface BaseUiModelWithState where T : BaseUiModelWithState {
7 |
8 | fun resetState(): T
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.base
2 |
3 | import androidx.annotation.CallSuper
4 | import androidx.lifecycle.ViewModel
5 | import kotlinx.coroutines.*
6 | import kotlin.coroutines.CoroutineContext
7 |
8 | /* Created by Psmann. */
9 |
10 | internal abstract class BaseViewModel : ViewModel(), CoroutineScope {
11 |
12 | private val job: Job = SupervisorJob() // SupervisorJob doesn't get cancelled when a child coroutine crashes
13 | protected open val exceptionResponse: (String) -> Unit = {} // Handle response to coroutine exception
14 | private val exceptionHandler = CoroutineExceptionHandler { _, e -> exceptionResponse(e.message.toString()) }
15 | override val coroutineContext: CoroutineContext
16 | get() = Dispatchers.Main + job + exceptionHandler
17 |
18 | /** Super must be called if overridden */
19 | @CallSuper
20 | override fun onCleared() {
21 | job.cancel()
22 | super.onCleared()
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/base/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.base
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import javax.inject.Inject
6 | import javax.inject.Provider
7 |
8 | /* Created by Psmann. */
9 |
10 | internal class ViewModelFactory @Inject constructor(
11 | private val viewModel: Map, @JvmSuppressWildcards Provider>
12 | ) : ViewModelProvider.Factory {
13 |
14 | @Suppress("UNCHECKED_CAST")
15 | override fun create(modelClass: Class): T = viewModel[modelClass]?.get() as T
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/models/City.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.models
2 |
3 | /* Created by Psmann. */
4 |
5 | internal data class City(
6 | val cityId: String = "",
7 | val cityName: String = "",
8 | val coordinates: String = ""
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/models/CurrentWeather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.models
2 |
3 | /* Created by Psmann. */
4 |
5 | internal data class CurrentWeather(
6 | val currentTemperature: String = "",
7 | val feelsLike: String = "",
8 | val pressure: String = "",
9 | val humidity: String = "",
10 | val description: String = "",
11 | val iconId: Int = 0,
12 | var sunrise: String = "",
13 | var sunset: String = "",
14 | val countryFlag: String = "",
15 | val clouds: String = "",
16 | val windSpeed: String = "",
17 | val windDirection: String = "",
18 | var lastUpdated: String = "",
19 | val visibility: String = "",
20 | val dayLength: String = "",
21 | val lastChecked: String = "",
22 | val sunPosition: Float = 0f
23 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/models/DailyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.models
2 |
3 | /* Created by Psmann. */
4 |
5 | internal data class DailyForecast(
6 | val forecastDate: String = "",
7 | val minTemp: String = "",
8 | val maxTemp: String = "",
9 | val forecastIconId: Int = 0
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/models/HourlyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.models
2 |
3 | /* Created by Psmann. */
4 |
5 | internal class HourlyForecast(
6 | val forecastTime: String = "",
7 | val temperature: String = "",
8 | val forecastIconId: Int = 0,
9 | val sunPosition: Float = 0f
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/models/NotificationData.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.models
2 |
3 | /* Created by Psmann. */
4 |
5 | internal data class NotificationData(
6 | val cityName: String = "",
7 | val description: String = "",
8 | val currentTemp: String = "",
9 | val day1MinTemp: String = "",
10 | val day1MaxTemp: String = "",
11 | val iconId: Int = 0,
12 | val sunPosition: Float = 0f,
13 | val humidity: String = "",
14 | val hour03Time: String = "",
15 | val hour03IconId: Int = 0,
16 | val hour03SunPosition: Float = 0f,
17 | val hour06Time: String = "",
18 | val hour06IconId: Int = 0,
19 | val hour06SunPosition: Float = 0f,
20 | val hour09Time: String = "",
21 | val hour09IconId: Int = 0,
22 | val hour09SunPosition: Float = 0f,
23 | val hour12Time: String = "",
24 | val hour12IconId: Int = 0,
25 | val hour12SunPosition: Float = 0f,
26 | val hour15Time: String = "",
27 | val hour15IconId: Int = 0,
28 | val hour15SunPosition: Float = 0f
29 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/models/Weather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.models
2 |
3 | /* Created by Psmann. */
4 |
5 | internal data class Weather(
6 | val city: City = City(),
7 | val currentWeather: CurrentWeather = CurrentWeather(),
8 | val dailyForecasts: List = List(7) { DailyForecast() },
9 | val hourlyForecasts: List = List(7) { HourlyForecast() }
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/util/UiDataMappers.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.util
2 |
3 | import one.mann.domain.logic.*
4 | import one.mann.weatherman.ui.common.models.*
5 | import one.mann.domain.models.NotificationData as DomainNotificationData
6 | import one.mann.domain.models.weather.Weather as DomainWeather
7 |
8 | /* Created by Psmann. */
9 |
10 | internal fun DomainWeather.mapToUiWeather(): Weather {
11 | val dailyForecastsUi = dailyForecasts.map {
12 | DailyForecast(
13 | epochToDay(it.forecastDate, city.timezone),
14 | it.minTemp.roundOff().addSuffix(DEGREES),
15 | it.maxTemp.roundOff().addSuffix(DEGREES),
16 | it.forecastIconId
17 | )
18 | }
19 | val hourlyForecastsUi = hourlyForecasts.map {
20 | HourlyForecast(
21 | epochToHour(it.forecastTime, city.timezone),
22 | it.temperature.roundOff().addSuffix(DEGREES),
23 | it.forecastIconId,
24 | it.sunPosition
25 | )
26 | }
27 |
28 | return Weather(
29 | City(
30 | city.cityId,
31 | currentWeather.cityName,
32 | listOf(city.coordinatesLat, city.coordinatesLong).coordinatesInString()
33 | ),
34 | CurrentWeather(
35 | currentWeather.currentTemperature.addSuffix(DEGREES),
36 | feelsLike.addSuffix(DEGREES),
37 | currentWeather.pressure.addSuffix(HECTOPASCAL),
38 | currentWeather.humidity.addSuffix(PERCENT),
39 | currentWeather.description,
40 | currentWeather.iconId,
41 | epochToTime(currentWeather.sunrise, city.timezone),
42 | epochToTime(currentWeather.sunset, city.timezone),
43 | currentWeather.countryFlag,
44 | currentWeather.clouds.addSuffix(PERCENT),
45 | currentWeather.windSpeed.addSuffix(if (currentWeather.units == IMPERIAL) MILES_PER_HOUR else KM_PER_HOUR),
46 | currentWeather.windDirection.addSuffix(DEGREES),
47 | epochToDate(currentWeather.lastUpdated, city.timezone),
48 | currentWeather.visibility.addSuffix(if (currentWeather.units == IMPERIAL) MILES else KILO_METERS),
49 | dayLength,
50 | epochToDate(lastChecked, city.timezone),
51 | sunPosition
52 | ),
53 | dailyForecastsUi,
54 | hourlyForecastsUi
55 | )
56 | }
57 |
58 | internal fun DomainNotificationData.mapToUiNotificationData(): NotificationData = NotificationData(
59 | cityName,
60 | description,
61 | currentTemp.roundOff().addSuffix(DEGREES),
62 | day1MinTemp.roundOff().addSuffix(DEGREES),
63 | day1MaxTemp.roundOff().addSuffix(DEGREES),
64 | iconId,
65 | sunPosition,
66 | humidity.addSuffix(PERCENT),
67 | epochToHour(hour03Time, ""),
68 | hour03IconId,
69 | hour03SunPosition,
70 | epochToHour(hour06Time, ""),
71 | hour06IconId,
72 | hour06SunPosition,
73 | epochToHour(hour09Time, ""),
74 | hour09IconId,
75 | hour09SunPosition,
76 | epochToHour(hour12Time, ""),
77 | hour12IconId,
78 | hour12SunPosition,
79 | epochToHour(hour15Time, ""),
80 | hour15IconId,
81 | hour15SunPosition
82 | )
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/common/util/UiHelpers.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.common.util
2 |
3 | import one.mann.weatherman.R
4 | import one.mann.weatherman.api.openweathermap.dayIcons
5 | import one.mann.weatherman.api.openweathermap.nightIcons
6 |
7 | /* Created by Psmann. */
8 |
9 | /** Set layout background depending upon time of day and weather conditions */
10 | internal fun getGradient(sunPosition: Float = 0.1f, isOvercast: Boolean = false): Int = when (sunPosition) {
11 | in -0.035..0.055, in 0.945..1.035 -> // Dawn-Sunrise and Sunset-Twilight
12 | if (isOvercast) R.drawable.background_gradient_sunrise_clouds
13 | else R.drawable.background_gradient_sunrise_clear
14 | in 0.055..0.945 -> // Day
15 | if (isOvercast) R.drawable.background_gradient_day_clouds
16 | else R.drawable.background_gradient_day_clear
17 | else -> // Night
18 | if (isOvercast) R.drawable.background_gradient_night_clouds
19 | else R.drawable.background_gradient_night_clear
20 | }
21 |
22 | /** Get file name for vector resource */
23 | internal fun getUri(code: Int, sunPosition: Float): String {
24 | return if (sunPosition in 0.0..1.0) dayIcons(code) else nightIcons(code)
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/detail/DetailUiModel.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.detail
2 |
3 | import one.mann.domain.models.ErrorType
4 | import one.mann.weatherman.ui.common.base.BaseUiModelWithState
5 | import one.mann.weatherman.ui.common.models.Weather
6 |
7 | /* Created by Psmann. */
8 |
9 | /**
10 | * @property weatherData: Weather data
11 | * @property viewState: Current state of the view
12 | */
13 | internal data class DetailUiModel(
14 | val weatherData: List = listOf(),
15 | val viewState: State = State.Idle
16 | ) : BaseUiModelWithState {
17 |
18 | override fun resetState(): DetailUiModel = copy(viewState = State.Idle)
19 |
20 | sealed class State {
21 | // Idle state, no change
22 | object Idle : State()
23 |
24 | // Set whether data is being refreshed or not
25 | object Refreshing : State()
26 |
27 | // Pass error type to a Toast
28 | data class ShowError(val errorType: ErrorType) : State()
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/detail/adapters/WeatherViewHolder.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.detail.adapters
2 |
3 | import android.view.View
4 | import androidx.recyclerview.widget.RecyclerView
5 | import one.mann.weatherman.databinding.*
6 |
7 | /* Created by Psmann. */
8 |
9 | internal sealed class WeatherViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
10 |
11 | class Current(itemView: View) : WeatherViewHolder(itemView) {
12 | val binding: ItemWeatherCurrentBinding = ItemWeatherCurrentBinding.bind(itemView)
13 | }
14 |
15 | class Conditions(itemView: View) : WeatherViewHolder(itemView) {
16 | val binding: ItemWeatherConditionsBinding = ItemWeatherConditionsBinding.bind(itemView)
17 | }
18 |
19 | class SunCycle(itemView: View) : WeatherViewHolder(itemView) {
20 | val binding: ItemWeatherSunCycleBinding = ItemWeatherSunCycleBinding.bind(itemView)
21 | }
22 |
23 | class HourlyForecast(itemView: View) : WeatherViewHolder(itemView) {
24 | val binding: ItemWeatherForecastHourlyBinding = ItemWeatherForecastHourlyBinding.bind(itemView)
25 | }
26 |
27 | class DailyForecast(itemView: View) : WeatherViewHolder(itemView) {
28 | val binding: ItemWeatherForecastDailyBinding = ItemWeatherForecastDailyBinding.bind(itemView)
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/main/MainUiModel.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.main
2 |
3 | import one.mann.domain.models.CitySearchResult
4 | import one.mann.domain.models.ErrorType
5 | import one.mann.domain.models.ViewPagerUpdateType
6 | import one.mann.weatherman.ui.common.base.BaseUiModelWithState
7 | import one.mann.weatherman.ui.common.models.Weather
8 |
9 | /* Created by Psmann. */
10 |
11 | /**
12 | * @property weatherData: Weather data
13 | * @property cityCount: Number of cities
14 | * @property citySearchResult: City search data
15 | * @property viewState: Current state of the view
16 | */
17 | internal data class MainUiModel(
18 | val weatherData: List = listOf(),
19 | val cityCount: Int = -1,
20 | val citySearchResult: List = listOf(),
21 | val viewState: State = State.Loading
22 | ) : BaseUiModelWithState {
23 |
24 | override fun resetState(): MainUiModel = copy(viewState = State.Idle)
25 |
26 | sealed class State {
27 | // Idle state, no change
28 | object Idle : State()
29 |
30 | // Set whether view is visible or not
31 | object Loading : State()
32 |
33 | // Set whether data is being refreshed or not
34 | object Refreshing : State()
35 |
36 | // Pass error type to a Toast
37 | data class ShowError(val errorType: ErrorType) : State()
38 |
39 | // Update ViewPager with animation
40 | data class UpdateViewPager(val updateType: ViewPagerUpdateType) : State()
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/main/adapters/MainViewPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.main.adapters
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentManager
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.viewpager2.adapter.FragmentStateAdapter
7 | import one.mann.weatherman.ui.main.CityFragment
8 |
9 | /* Created by Psmann. */
10 |
11 | class MainViewPagerAdapter(fm: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fm, lifecycle) {
12 |
13 | private var pageCount = 1
14 |
15 | override fun getItemCount(): Int = pageCount
16 |
17 | override fun createFragment(position: Int): Fragment = CityFragment.newInstance(position)
18 |
19 | fun updatePages(newCount: Int) {
20 | pageCount = newCount
21 | notifyDataSetChanged()
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/main/adapters/SearchCityRecyclerAdapter.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.main.adapters
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import one.mann.domain.models.CitySearchResult
8 | import one.mann.weatherman.R
9 | import one.mann.weatherman.databinding.ItemCitySearchBinding
10 | import one.mann.weatherman.ui.common.util.inflateView
11 |
12 | /* Created by Psmann. */
13 |
14 | internal class SearchCityRecyclerAdapter(val onClick: (searchResult: CitySearchResult) -> Unit) :
15 | RecyclerView.Adapter() {
16 |
17 | private var citySearchList = listOf()
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CitySearchViewHolder {
20 | return CitySearchViewHolder(parent.inflateView(R.layout.item_city_search))
21 | }
22 |
23 | override fun onBindViewHolder(holder: CitySearchViewHolder, position: Int) {
24 | holder.binding.apply {
25 | cityResult1TextView.text = citySearchList[position].cityLine1
26 | cityResult2TextView.text = citySearchList[position].cityLine2
27 | countryFlagTextView.text = citySearchList[position].countryFlag
28 | root.setOnClickListener { onClick(citySearchList[position]) }
29 | }
30 | }
31 |
32 | override fun getItemCount(): Int = citySearchList.size
33 |
34 | @SuppressLint("NotifyDataSetChanged") // Should not impact performance much
35 | fun update(searchList: List = listOf()) {
36 | citySearchList = searchList
37 | notifyDataSetChanged()
38 | }
39 |
40 | class CitySearchViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
41 | val binding: ItemCitySearchBinding = ItemCitySearchBinding.bind(itemView)
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/one/mann/weatherman/ui/settings/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package one.mann.weatherman.ui.settings
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.preference.PreferenceFragmentCompat
6 | import one.mann.weatherman.R
7 | import one.mann.weatherman.databinding.ActivitySettingsBinding
8 |
9 | /* Created by Psmann. */
10 |
11 | internal class SettingsActivity : AppCompatActivity() {
12 |
13 | private val binding by lazy { ActivitySettingsBinding.inflate(layoutInflater) }
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContentView(binding.root)
18 | supportFragmentManager
19 | .beginTransaction()
20 | .replace(R.id.settings_frame_layout, SettingsFragment())
21 | .commit()
22 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
23 | }
24 |
25 | class SettingsFragment : PreferenceFragmentCompat() {
26 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
27 | setPreferencesFromResource(R.xml.root_preferences, rootKey)
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_slide_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_slide_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_gradient_day_clear.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_gradient_day_clouds.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_gradient_night_clear.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_gradient_night_clouds.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_gradient_sunrise_clear.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_gradient_sunrise_clouds.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_notification.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ll_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/location_current.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/menu_add.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/menu_more.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/menu_remove.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/menu_settings.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_cloud_cover.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_humidity.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_pressure.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_sun_position.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/drawable/weather_parameter_sun_position.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_time.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_visibility.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_wind_deg.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_parameter_wind_speed.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_clear_day.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_clear_night.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloud_unknown.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloudy.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloudy_day_1.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloudy_day_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloudy_day_3.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloudy_night_1.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
20 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloudy_night_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
20 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_cloudy_night_3.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
20 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_hazy.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_rainy_1.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
65 |
71 |
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_rainy_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
65 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_rainy_3.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
65 |
71 |
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_rainy_4.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_rainy_5.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_rainy_6.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_rainy_7.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_snowy_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
59 |
65 |
71 |
77 |
83 |
89 |
90 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_snowy_4.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_snowy_5.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_snowy_6.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
54 |
60 |
66 |
72 |
78 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/weather_type_thunder.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/activity_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
24 |
25 |
29 |
30 |
34 |
35 |
36 |
37 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/fragment_city.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
21 |
22 |
34 |
35 |
44 |
45 |
55 |
56 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/view_search_city.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
24 |
25 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
23 |
24 |
28 |
29 |
33 |
34 |
35 |
36 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_settings.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_city.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
21 |
22 |
37 |
38 |
49 |
50 |
62 |
63 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_city_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
35 |
36 |
49 |
50 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_weather_sun_cycle.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
16 |
17 |
26 |
27 |
36 |
37 |
45 |
46 |
56 |
57 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_search_city.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
24 |
25 |
36 |
37 |
--------------------------------------------------------------------------------
/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/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - Imperial
6 | - Metric
7 |
8 |
9 |
10 |
11 |
12 | - imperial
13 | - metric
14 |
15 |
16 |
17 |
18 |
19 | - Once a day
20 | - Every 12 Hours
21 | - Every 6 Hours
22 | - Every 3 Hours
23 | - Every Hour
24 |
25 |
26 |
27 |
28 |
29 | - 24
30 | - 12
31 | - 6
32 | - 3
33 | - 1
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3949ab
4 | #1a237e
5 | #203CE7
6 |
7 | #47E0F7FA
8 | #79AAAAAA
9 |
10 |
11 | #01579B
12 | #03A9F4
13 | #B3E5FC
14 | #02508D
15 | #3D7997
16 | #B0BEC5
17 | #211352
18 | #59497A
19 | #4A4A64
20 | #33303D
21 | #524B63
22 | #575758
23 | #E65100
24 | #D38005
25 | #F89833
26 | #6D2703
27 | #BB6F00
28 | #E29C32
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #0091CF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WeatherMan
3 | Temperature
4 | Feels Like
5 | Max
6 | Min
7 | Pressure
8 | Humidity
9 | Last Updated
10 | Last Checked
11 | Location
12 | (
13 | )
14 | Clouds
15 | Wind Speed
16 | Wind Direction
17 | Visibility
18 | Sunrise
19 | Sunset
20 | Length of Day:
21 | Current location can\'t be removed
22 | GPS is needed for current location detection
23 | Permission is required for location detection
24 | Could not connect to the server
25 | No internet connection
26 | Weather updated for previous location
27 | Weather Icon
28 | Location settings are not available on this device
29 | This city has already been added. Please choose a different city.
30 | Add City
31 | Remove City
32 | Network Error:\n
33 | Only 10 cities can be added!\nRemove one before adding another
34 | Settings
35 | Settings
36 | More
37 | Location added
38 | Location removed
39 | Remove City Location
40 | Do you want to remove this location?
41 | Yes
42 | No
43 | Got it
44 | Details
45 | Swipe left or right to toggle between cities
46 | Humidity
47 | /
48 |
49 |
50 | Units
51 | Notifications
52 | Weather Units
53 | Turn Notifications On and Off
54 | Notification Frequency
55 | Set how often you\'d like to receive notifications
56 | Shows notifications whenever current currentWeather is updated
57 | Enter city name
58 | Location Pin
59 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
20 |
21 |
22 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/root_preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 |
9 | dependencies {
10 | // Gradle plugin
11 | classpath 'com.android.tools.build:gradle:7.4.2'
12 | // Kotlin plugin
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0"
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 |
23 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
24 | kotlinOptions.jvmTarget = JavaVersion.VERSION_11
25 | }
26 | }
27 |
28 | task clean(type: Delete) {
29 | delete rootProject.buildDir
30 | }
31 |
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/logic/Constants.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.logic
2 |
3 | /* Created by Psmann. */
4 |
5 | const val FLAG_OFFSET = 0x1F1E6 // Regional Indicator Symbol for letter A
6 | const val ASCII_OFFSET = 0x41 // Uppercase letter A
7 | const val DURATION_PATTERN = "H 'Hours and' m 'Minutes'"
8 | const val DATE_PATTERN = "d MMM, h:mm aa"
9 | const val DAY_PATTERN = "EEEE" // Full-form
10 | const val HOUR_PATTERN = "h aa"
11 | const val TIME_PATTERN = "h:mm aa"
12 | const val CELSIUS = "C"
13 | const val FAHRENHEIT = "F"
14 | const val HECTOPASCAL = " hPa"
15 | const val PERCENT = " %"
16 | const val KILO_METERS = " km"
17 | const val MILES = " mi"
18 | const val KM_PER_HOUR = " km/h"
19 | const val MILES_PER_HOUR = " mph"
20 | const val DEGREES = "°"
21 | const val IMPERIAL = "imperial"
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/CitySearchResult.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models
2 |
3 | /* Created by Psmann. */
4 |
5 | data class CitySearchResult(
6 | val cityLine1: String = "",
7 | val cityLine2: String = "",
8 | val latitude: Float = 0f,
9 | val longitude: Float = 0f,
10 | val countryFlag: String = ""
11 | )
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/Direction.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models
2 |
3 | /* Created by Psmann. */
4 |
5 | enum class Direction {
6 | UP, LEFT
7 | }
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/ErrorType.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models
2 |
3 | /* Created by Psmann. */
4 |
5 | sealed class ErrorType {
6 | object NoInternet : ErrorType()
7 | object NoGps : ErrorType()
8 | object NoLocation : ErrorType()
9 | object CityAlreadyExists : ErrorType()
10 | data class NoResponse(val message: String = "") : ErrorType()
11 | }
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/NotificationData.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models
2 |
3 | /* Created by Psmann. */
4 |
5 | data class NotificationData(
6 | val cityName: String = "",
7 | val description: String = "",
8 | val currentTemp: Float = 0f,
9 | val day1MinTemp: Float = 0f,
10 | val day1MaxTemp: Float = 0f,
11 | val iconId: Int = 0,
12 | val sunPosition: Float = 0f,
13 | val humidity: Int = 0,
14 | val hour03Time: Long = 0,
15 | val hour03IconId: Int = 0,
16 | val hour03SunPosition: Float = 0f,
17 | val hour06Time: Long = 0,
18 | val hour06IconId: Int = 0,
19 | val hour06SunPosition: Float = 0f,
20 | val hour09Time: Long = 0,
21 | val hour09IconId: Int = 0,
22 | val hour09SunPosition: Float = 0f,
23 | val hour12Time: Long = 0,
24 | val hour12IconId: Int = 0,
25 | val hour12SunPosition: Float = 0f,
26 | val hour15Time: Long = 0,
27 | val hour15IconId: Int = 0,
28 | val hour15SunPosition: Float = 0f
29 | )
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/UnitsType.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models
2 |
3 | /* Created by Psmann. */
4 |
5 | enum class UnitsType {
6 | TEMPERATURE, WIND, VISIBILITY
7 | }
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/ViewPagerUpdateType.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models
2 |
3 | /* Created by Psmann. */
4 |
5 | enum class ViewPagerUpdateType {
6 | SET_SIZE, ADD_ITEM, REMOVE_ITEM, NO_CHANGE
7 | }
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/location/Location.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.location
2 |
3 | /* Created by Psmann. */
4 |
5 | data class Location(val coordinates: List = listOf(0f, 0f))
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/location/LocationServicesResponse.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.location
2 |
3 | /* Created by Psmann. */
4 |
5 | enum class LocationServicesResponse {
6 | NO_NETWORK, ENABLED, DISABLED, UNAVAILABLE
7 | }
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/location/LocationType.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.location
2 |
3 | /* Created by Psmann. */
4 |
5 | enum class LocationType {
6 | DEVICE, DB
7 | }
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/weather/City.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.weather
2 |
3 | /* Created by Psmann. */
4 |
5 | data class City(
6 | val cityId: String = "",
7 | val coordinatesLat: Float = 0f,
8 | val coordinatesLong: Float = 0f,
9 | val timezone: String = "",
10 | val timeCreated: Long = 0
11 | )
12 |
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/weather/CurrentWeather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.weather
2 |
3 | /* Created by Psmann. */
4 |
5 | data class CurrentWeather(
6 | val weatherId: Int = 0,
7 | val cityName: String = "",
8 | val currentTemperature: Float = 0f,
9 | val pressure: Int = 0,
10 | val humidity: Int = 0,
11 | val description: String = "",
12 | val iconId: Int = 0,
13 | var sunrise: Long = 0,
14 | var sunset: Long = 0,
15 | val countryFlag: String = "",
16 | val clouds: Int = 0,
17 | val windSpeed: Float = 0f,
18 | val windDirection: Int = 0,
19 | var lastUpdated: Long = 0,
20 | val visibility: Float = 0f,
21 | val units: String = ""
22 | )
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/weather/DailyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.weather
2 |
3 | /* Created by Psmann. */
4 |
5 | data class DailyForecast(
6 | val dailyId: Int = 0,
7 | val forecastDate: Long = 0,
8 | val minTemp: Float = 0f,
9 | val maxTemp: Float = 0f,
10 | val forecastIconId: Int = 0
11 | )
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/weather/HourlyForecast.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.weather
2 |
3 | /* Created by Psmann. */
4 |
5 | data class HourlyForecast(
6 | val hourlyId: Int = 0,
7 | val forecastTime: Long = 0,
8 | val temperature: Float = 0f,
9 | val forecastIconId: Int = 0,
10 | val sunPosition: Float = 0f
11 | )
--------------------------------------------------------------------------------
/domain/src/main/java/one/mann/domain/models/weather/Weather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.domain.models.weather
2 |
3 | /* Created by Psmann. */
4 |
5 | data class Weather(
6 | val city: City = City(),
7 | val currentWeather: CurrentWeather = CurrentWeather(),
8 | val dailyForecasts: List = listOf(),
9 | val hourlyForecasts: List = listOf(),
10 | val feelsLike: Float = 0f,
11 | val dayLength: String = "",
12 | val lastChecked: Long = 0,
13 | val sunPosition: Float = 0f
14 | )
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Wed Sep 08 01:17:41 EDT 2021
2 | #
3 | ## For more details on how to configure your build environment visit
4 | # http://www.gradle.org/docs/current/userguide/build_environment.html
5 | #
6 | # Specifies the JVM arguments used for the daemon process.
7 | # The setting is particularly useful for tweaking memory settings.
8 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
9 | org.gradle.jvmargs=-Xmx2g -XX:MaxPermSize=2g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC
10 | #
11 | # When configured, Gradle will run in incubating parallel mode.
12 | # This option should only be used with decoupled projects. More details, visit
13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
14 | org.gradle.parallel=true
15 | #
16 | # Gradle Build cache
17 | # https://docs.gradle.org/current/userguide/build_cache.html
18 | org.gradle.caching=true
19 | #
20 | # Gradle Configuration cache (experimental feature)
21 | # https://docs.gradle.org/nightly/userguide/configuration_cache.html
22 | #org.gradle.unsafe.configuration-cache=true
23 | #
24 | # AndroidX libraries
25 | # https://developer.android.com/jetpack/androidx
26 | android.useAndroidX=true
27 | #
28 | # Kapt settings to improve build speed
29 | # https://kotlinlang.org/docs/kapt.html#improving-the-speed-of-builds-that-use-kapt
30 | # To run kapt tasks in parallel
31 | kapt.use.worker.api=true
32 | # Caching for annotation processors classloaders (experimental)
33 | # Cache size set to 2 as two modules (:app and :interactors) use kapt
34 | kapt.classloaders.cache.size=2
35 | kapt.include.compile.classpath=false
36 | #
37 | kotlin.incremental.useClasspathSnapshot=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psmann/WeatherMan/25e9af3f3675b93510efae12892be51c03e646ea/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 13 03:48:49 EDT 2020
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-7.5-all.zip
7 |
--------------------------------------------------------------------------------
/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 location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling 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 |
--------------------------------------------------------------------------------
/interactors/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/interactors/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 | apply plugin: 'kotlin-kapt'
3 |
4 | dependencies {
5 | // Modules
6 | api project(":domain")
7 |
8 | // Kotlin Coroutines
9 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
10 |
11 | // Dagger 2
12 | implementation 'com.google.dagger:dagger:2.45'
13 | kapt 'com.google.dagger:dagger-compiler:2.45'
14 | }
15 |
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/DataMappers.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data
2 |
3 | import one.mann.domain.logic.*
4 | import one.mann.domain.models.UnitsType
5 | import one.mann.domain.models.UnitsType.*
6 | import one.mann.domain.models.weather.*
7 |
8 | /* Created by Psmann. */
9 |
10 | /** Transform API data and map to domain Weather model */
11 | internal fun mapToDomainWeather(
12 | city: City,
13 | currentWeather: CurrentWeather,
14 | dailyForecasts: List,
15 | hourlyForecasts: List
16 | ): Weather {
17 |
18 | val sunriseTime = epochToMinutes(currentWeather.sunrise, city.timezone)
19 | val sunsetTime = epochToMinutes(currentWeather.sunset, city.timezone)
20 | val hourlyForecastsWithSunPosition = hourlyForecasts.map {
21 | it.copy(
22 | sunPosition = sunPositionBias(
23 | sunriseTime,
24 | sunsetTime,
25 | epochToMinutes(it.forecastTime, city.timezone)
26 | )
27 | )
28 | }
29 |
30 | return Weather(
31 | city,
32 | currentWeather.copy(countryFlag = countryCodeToEmoji(currentWeather.countryFlag)),
33 | dailyForecasts,
34 | hourlyForecastsWithSunPosition,
35 | feelsLike(currentWeather.currentTemperature, currentWeather.humidity, currentWeather.windSpeed),
36 | lengthOfDay(currentWeather.sunrise, currentWeather.sunset),
37 | System.currentTimeMillis(),
38 | sunPositionBias(sunriseTime, sunsetTime, epochToMinutes(System.currentTimeMillis(), city.timezone)),
39 | ).applyUnits(currentWeather.units, true)
40 | }
41 |
42 | /** Apply units (Imperial or Metric) to weather parameters */
43 | internal fun Weather.applyUnits(units: String, isDataFromApi: Boolean): Weather {
44 | // Use setUnits() if data is retrieved from the API, otherwise use changeUnits()
45 | fun Float.set(units: String, type: UnitsType): Float {
46 | return if (isDataFromApi) setUnits(units, type) else changeUnits(units, type)
47 | }
48 |
49 | val currentWeatherWithUnits = currentWeather.copy(
50 | currentTemperature = currentWeather.currentTemperature.set(units, TEMPERATURE),
51 | windSpeed = currentWeather.windSpeed.set(units, WIND),
52 | visibility = currentWeather.visibility.set(units, VISIBILITY)
53 | )
54 | val dailyForecastsWithUnits = dailyForecasts.map {
55 | it.copy(
56 | minTemp = it.minTemp.set(units, TEMPERATURE),
57 | maxTemp = it.maxTemp.set(units, TEMPERATURE)
58 | )
59 | }
60 | val hourlyForecastsWithUnits = hourlyForecasts.map { it.copy(temperature = it.temperature.set(units, TEMPERATURE)) }
61 |
62 | return copy(
63 | currentWeather = currentWeatherWithUnits,
64 | dailyForecasts = dailyForecastsWithUnits,
65 | hourlyForecasts = hourlyForecastsWithUnits,
66 | feelsLike = feelsLike.set(units, TEMPERATURE)
67 | )
68 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/repositories/CitySearchRepository.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data.repositories
2 |
3 | import one.mann.domain.models.CitySearchResult
4 | import one.mann.interactors.data.sources.api.CitySearchDataSource
5 | import javax.inject.Inject
6 |
7 | /* Created by Psmann. */
8 |
9 | class CitySearchRepository @Inject constructor(private val citySearchDataSource: CitySearchDataSource) {
10 |
11 | suspend fun getCitySearch(query: String): List = citySearchDataSource.getCitySearch(query)
12 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/sources/api/CitySearchDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data.sources.api
2 |
3 | import one.mann.domain.models.CitySearchResult
4 |
5 | /* Created by Psmann. */
6 |
7 | interface CitySearchDataSource {
8 |
9 | suspend fun getCitySearch(cityNameQuery: String): List
10 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/sources/api/TimezoneDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data.sources.api
2 |
3 | import one.mann.domain.models.location.Location
4 |
5 | /* Created by Psmann. */
6 |
7 | interface TimezoneDataSource {
8 |
9 | suspend fun getTimezone(location: Location): String
10 |
11 | suspend fun getAllTimezone(locations: List): List
12 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/sources/api/WeatherDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data.sources.api
2 |
3 | import one.mann.domain.models.location.Location
4 | import one.mann.domain.models.weather.CurrentWeather
5 | import one.mann.domain.models.weather.DailyForecast
6 | import one.mann.domain.models.weather.HourlyForecast
7 |
8 | /* Created by Psmann. */
9 |
10 | interface WeatherDataSource {
11 |
12 | suspend fun getCurrentWeather(location: Location): CurrentWeather
13 |
14 | suspend fun getDailyForecasts(location: Location): List
15 |
16 | suspend fun getHourlyForecasts(location: Location): List
17 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/sources/framework/DatabaseDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data.sources.framework
2 |
3 | import one.mann.domain.models.NotificationData
4 | import one.mann.domain.models.weather.Weather
5 |
6 | /* Created by Psmann. */
7 |
8 | interface DatabaseDataSource {
9 |
10 | suspend fun insertCityAndWeather(weather: Weather)
11 |
12 | suspend fun getAllCitiesAndWeathers(): List
13 |
14 | suspend fun getNotificationData(): NotificationData
15 |
16 | suspend fun updateLastChecked(lastChecked: Long)
17 |
18 | suspend fun updateAllWeathers(weathers: List)
19 |
20 | suspend fun deleteCityAndWeather(cityId: String)
21 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/sources/framework/DeviceLocationSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data.sources.framework
2 |
3 | import one.mann.domain.models.location.Location
4 |
5 | /* Created by Psmann. */
6 |
7 | interface DeviceLocationSource {
8 |
9 | suspend fun getLocation(): Location
10 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/data/sources/framework/PreferencesDataSource.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.data.sources.framework
2 |
3 | /* Created by Psmann. */
4 |
5 | interface PreferencesDataSource {
6 |
7 | suspend fun getUnits(): String
8 |
9 | suspend fun getLastUpdated(): Long
10 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/usecases/AddCity.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.usecases
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import one.mann.domain.models.location.Location
6 | import one.mann.interactors.data.repositories.WeatherRepository
7 | import javax.inject.Inject
8 |
9 | /* Created by Psmann. */
10 |
11 | class AddCity @Inject constructor(private val weatherRepository: WeatherRepository) {
12 |
13 | suspend fun invoke(apiLocation: Location? = null) = withContext(Dispatchers.IO) {
14 | weatherRepository.createCity(apiLocation)
15 | }
16 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/usecases/ChangeUnits.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.usecases
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import one.mann.interactors.data.applyUnits
6 | import one.mann.interactors.data.repositories.WeatherRepository
7 | import one.mann.interactors.data.sources.framework.PreferencesDataSource
8 | import javax.inject.Inject
9 |
10 | /* Created by Psmann. */
11 |
12 | class ChangeUnits @Inject constructor(
13 | private val weatherRepository: WeatherRepository,
14 | private val prefsData: PreferencesDataSource
15 | ) {
16 |
17 | suspend fun invoke() = withContext(Dispatchers.IO) {
18 | val units = prefsData.getUnits()
19 | weatherRepository.updateAllWeather(weatherRepository.readAllWeather().map { it.applyUnits(units, false) })
20 | }
21 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/usecases/GetAllWeather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.usecases
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import one.mann.domain.models.weather.Weather
6 | import one.mann.interactors.data.repositories.WeatherRepository
7 | import javax.inject.Inject
8 |
9 | /* Created by Psmann. */
10 |
11 | class GetAllWeather @Inject constructor(private val weatherRepository: WeatherRepository) {
12 |
13 | suspend fun invoke(): List = withContext(Dispatchers.IO) { weatherRepository.readAllWeather() }
14 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/usecases/GetCitySearch.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.usecases
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import one.mann.domain.models.CitySearchResult
6 | import one.mann.interactors.data.repositories.CitySearchRepository
7 | import javax.inject.Inject
8 |
9 | /* Created by Psmann. */
10 |
11 | class GetCitySearch @Inject constructor(private val citySearchRepository: CitySearchRepository) {
12 |
13 | suspend fun invoke(query: String): List = withContext(Dispatchers.IO) {
14 | citySearchRepository.getCitySearch(query)
15 | }
16 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/usecases/GetNotificationData.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.usecases
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import one.mann.domain.models.NotificationData
6 | import one.mann.interactors.data.repositories.WeatherRepository
7 | import javax.inject.Inject
8 |
9 | /* Created by Psmann. */
10 |
11 | class GetNotificationData @Inject constructor(private val weatherRepository: WeatherRepository) {
12 |
13 | suspend fun invoke(): NotificationData = withContext(Dispatchers.IO) { weatherRepository.readNotificationData() }
14 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/usecases/RemoveCity.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.usecases
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import one.mann.interactors.data.repositories.WeatherRepository
6 | import javax.inject.Inject
7 |
8 | /* Created by Psmann. */
9 |
10 | class RemoveCity @Inject constructor(private val weatherRepository: WeatherRepository) {
11 |
12 | suspend fun invoke(cityId: String) = withContext(Dispatchers.IO) { weatherRepository.deleteCity(cityId) }
13 | }
--------------------------------------------------------------------------------
/interactors/src/main/java/one/mann/interactors/usecases/UpdateWeather.kt:
--------------------------------------------------------------------------------
1 | package one.mann.interactors.usecases
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 | import one.mann.domain.models.location.LocationType
6 | import one.mann.interactors.data.repositories.WeatherRepository
7 | import javax.inject.Inject
8 |
9 | /* Created by Psmann. */
10 |
11 | class UpdateWeather @Inject constructor(private val weatherRepository: WeatherRepository) {
12 |
13 | suspend fun invoke(locationType: LocationType): Boolean = withContext(Dispatchers.IO) {
14 | weatherRepository.tryApiDataSyncWithResult(locationType)
15 | }
16 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':interactors', ':domain'
2 |
3 | // Configuration for Gradle local build cache
4 | buildCache {
5 | local {
6 | // Directory to use for caching
7 | directory = new File(rootDir, 'gradle-build-cache')
8 | // Clear cache every day
9 | removeUnusedEntriesAfterDays = 1
10 | }
11 | }
--------------------------------------------------------------------------------