├── .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 |