release to main (#39)

- change location retrieval accuracy
 - change location retrieval caching from location provider
 - Imperial units added
 - atlas weather notification fix (only for lower versions)
 - Minor lint fixes
 - Upgrade gradle dependencies to versions accepted by android 33
 - upgrade android gradle to 8.5
 - upgrade application to android 34
 - upgraded all library dependencies
 - Snapshot tests added for readme.md
 - UI corrections during snapshots
 - Weather API successfully replaces
This commit is contained in:
2024-10-02 00:28:18 +01:00
committed by GitHub
parent 4e8745c903
commit 3c36236df7
138 changed files with 55601 additions and 2587 deletions

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.6.0" /> <option name="version" value="2.0.0" />
</component> </component>
</project> </project>

View File

@@ -1,7 +1,6 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'kotlin-android-extensions'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'androidx.navigation.safeargs' id 'androidx.navigation.safeargs'
} }
@@ -13,16 +12,14 @@ def relKeyAlias = System.getenv("RELEASE_KEY_ALIAS")
def keystorePath = System.getenv('PWD') + "/app/keystore.jks" def keystorePath = System.getenv('PWD') + "/app/keystore.jks"
def keystore = file(keystorePath).exists() ? file(keystorePath) : null def keystore = file(keystorePath).exists() ? file(keystorePath) : null
android { android {
lintOptions { namespace 'com.appttude.h_mal.atlas_weather'
abortOnError false compileSdk = Integer.parseInt(TARGET_SDK_VERSION)
}
defaultConfig { defaultConfig {
applicationId "com.appttude.h_mal.atlas_weather" applicationId "com.appttude.h_mal.atlas_weather"
compileSdk 33 minSdkVersion MIN_SDK_VERSION
minSdkVersion 26 targetSdkVersion TARGET_SDK_VERSION
targetSdkVersion 33 versionCode 6
versionCode 5 versionName "3.1"
versionName "3.0"
testInstrumentationRunner "com.appttude.h_mal.atlas_weather.application.TestRunner" testInstrumentationRunner "com.appttude.h_mal.atlas_weather.application.TestRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -35,6 +32,10 @@ android {
buildConfigField "String", "ParamOne", System.getenv('WEATHER_API') buildConfigField "String", "ParamOne", System.getenv('WEATHER_API')
buildConfigField "String", "ParamTwo", System.getenv('SEARCH_API') buildConfigField "String", "ParamTwo", System.getenv('SEARCH_API')
} }
packagingOptions {
resources.excludes.add("META-INF/*")
}
} }
android { android {
sourceSets { sourceSets {
@@ -78,20 +79,23 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
freeCompilerArgs += [ freeCompilerArgs += [
'-Xjvm-default=enable' '-Xjvm-default=all-compatibility'
] ]
} }
flavorDimensions "default" buildFeatures {
flavorDimensions = ["version"]
}
productFlavors { productFlavors {
atlasWeather { atlasWeather {
dimension "version"
applicationId "com.appttude.h_mal.atlas_weather" applicationId "com.appttude.h_mal.atlas_weather"
versionCode 5 versionCode 5
versionName "3.0.0" versionName "3.0.0"
} }
monoWeather { monoWeather {
dimension "version"
applicationId "com.appttude.h_mal.monoWeather" applicationId "com.appttude.h_mal.monoWeather"
versionCode 7 versionCode 7
versionName "4.2.0" versionName "4.2.0"
} }
@@ -108,105 +112,113 @@ android {
} }
} }
} }
lint {
abortOnError false
}
testBuildType "debug"
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.6.1' implementation "androidx.appcompat:appcompat:$APP_COMPAT"
implementation 'com.google.android.material:material:1.2.1' implementation "com.google.android.material:material:$MATERIAL_VERSION"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "androidx.constraintlayout:constraintlayout:$CONSTR_LAYOUT_VERSION"
implementation 'androidx.fragment:fragment:1.2.0' implementation "androidx.fragment:fragment:$FRAGMENT_VERSION"
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.fragment:fragment-ktx:$FRAGMENT_VERSION"
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' implementation "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION"
implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation "androidx.preference:preference:$PREFERENCES_VERSION"
implementation "com.google.android.gms:play-services-location:21.0.1" implementation "androidx.core:core:$ANDROID_CORE"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.customview:customview:$CUSTOM_VIEW"
implementation 'androidx.cardview:cardview:1.0.0' implementation "androidx.cardview:cardview:$CARD_VIEW"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' implementation "androidx.lifecycle:lifecycle-common:$ANDROID_LIFECYCLE"
implementation 'androidx.preference:preference:1.2.1' implementation "androidx.lifecycle:lifecycle-livedata-core:$ANDROID_LIFECYCLE"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$ANDROID_LIFECYCLE"
implementation "androidx.lifecycle:lifecycle-viewmodel:$ANDROID_LIFECYCLE"
implementation "androidx.recyclerview:recyclerview:$RECYCLER_VIEW"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$SWIPE_REFRESH"
implementation "com.google.code.gson:gson:$GSON"
implementation "com.google.guava:guava:$GUAVA"
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
// force upgrade to 1.1.0 because its required by androidTestImplementation,
// and without this statement AGP will silently downgrade to tracing:1.0.0
implementation "androidx.tracing:tracing:1.1.0"
/ * Google play services * /
implementation "com.google.android.gms:play-services-location:$GOOGLE_PLAY_SERVICE"
/ * Unit testing * / / * Unit testing * /
testImplementation 'junit:junit:4.13.2' testImplementation "junit:junit:$JUNIT_VERSION"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" androidTestImplementation project(path: ':app')
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testRuntimeOnly "org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_VERSION"
implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$KOTLIN_VERSION"
/ * Fragment Navigation * / androidTestImplementation "junit:junit:$JUNIT_VERSION"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLINX_COROUTINES"
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$KOTLINX_COROUTINES"
testImplementation "androidx.cardview:cardview:$CARD_VIEW"
testImplementation "com.tomtom.online:sdk-maps-ui-extensions:$TOMTOM_MAP"
/ * Navigation * /
implementation "androidx.navigation:navigation-common:$NAVIGATION_VERSION"
implementation "androidx.navigation:navigation-fragment:$NAVIGATION_VERSION"
implementation "androidx.navigation:navigation-runtime:$NAVIGATION_VERSION"
implementation "androidx.navigation:navigation-ui:$NAVIGATION_VERSION"
/ * android unit testing and espresso * / / * android unit testing and espresso * /
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation "androidx.test:rules:$ANDROIDX_TEST"
androidTestImplementation "androidx.test:core:1.5.0" androidTestImplementation "androidx.test:core:$ANDROIDX_TEST"
androidTestImplementation "androidx.test:monitor:$TEST_MONITOR"
/ * Android Espresso * / androidTestImplementation "androidx.test.ext:junit:$TEST_JUNIT_VERSION"
def testJunitVersion = "1.1.5" androidTestRuntimeOnly "org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_VERSION"
def testRunnerVersion = "1.5.2" androidTestImplementation "androidx.test.espresso:espresso-core:$ESPRESSO_VERSION"
def espressoVersion = "3.5.1" androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$ESPRESSO_VERSION"
androidTestImplementation "androidx.test.ext:junit:$testJunitVersion" androidTestImplementation "androidx.test:runner:$TEST_RUNNER_VERSION"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-contrib:$ESPRESSO_VERSION"
androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion" androidTestImplementation "org.hamcrest:hamcrest:$HAMCREST_VERSION"
implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLINX_COROUTINES"
androidTestImplementation "androidx.test:runner:$testRunnerVersion" androidTestImplementation "androidx.cardview:cardview:$CARD_VIEW"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" androidTestImplementation 'com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:3.1.2'
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" androidTestImplementation "com.tomtom.online:sdk-maps-ui-extensions:$TOMTOM_MAP"
androidTestImplementation "org.hamcrest:hamcrest:2.2" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$KOTLIN_VERSION"
atlasWeatherImplementation "androidx.cardview:cardview:$CARD_VIEW"
/ * mock websever for testing retrofit responses * / / * mock websever for testing retrofit responses * /
testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
/ * mockito and livedata testing * / / * mockito and livedata testing * /
testImplementation 'org.mockito:mockito-inline:2.13.0' testImplementation "org.mockito:mockito-inline:$MOKITO_INLINE_VERSION"
implementation 'androidx.arch.core:core-testing:2.2.0' testImplementation "androidx.arch.core:core-testing:$CORE_TEST_VERSION"
androidTestImplementation "androidx.arch.core:core-testing:$CORE_TEST_VERSION"
/ * MockK * / / * MockK * /
def mockk_ver = "1.10.5" testImplementation "io.mockk:mockk:$MOCKK_VERSION"
testImplementation "io.mockk:mockk:$mockk_ver" androidTestImplementation "io.mockk:mockk-android:$MOCKK_VERSION"
androidTestImplementation "io.mockk:mockk-android:$mockk_ver" / * Retrofit & Okhttp * /
implementation "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
/ * Retrofit * / implementation "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
def retrofit_ver = "2.9.0" implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION"
implementation "com.squareup.retrofit2:retrofit:$retrofit_ver" implementation "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver" / * Kodein Dependency Injection * /
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" implementation "org.kodein.di:kodein-di-generic-jvm:$KODEIN_VERSION"
implementation "org.kodein.di:kodein-di-framework-android-x:$KODEIN_VERSION"
/ * Shared prefs * / implementation "org.kodein.di:kodein-di-core-jvm:$KODEIN_VERSION"
def prefs_ver = "1.2.0" implementation "org.kodein.di:kodein-di-framework-android-core:$KODEIN_VERSION"
implementation "androidx.preference:preference-ktx:$prefs_ver"
/ *Kodein Dependency Injection * /
def kodein_version = "6.2.1"
implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version"
implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
/ * Room database * / / * Room database * /
def room_version = "2.4.3" runtimeOnly "androidx.room:room-runtime:$ROOM_VERSION"
implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$ROOM_VERSION"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-common:$ROOM_VERSION"
implementation 'androidx.sqlite:sqlite:2.2.0'
/ * Picasso * / / * Picasso * /
implementation 'com.squareup.picasso:picasso:2.71828' implementation 'com.squareup.picasso:picasso:2.71828'
/ * coroutine * / / * coroutine * /
def coroutine_version = "1.3.9" runtimeOnly "org.jetbrains.kotlinx:kotlinx-coroutines-android:$KOTLINX_COROUTINES"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLINX_COROUTINES"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$KOTLINX_COROUTINES"
/ * tomtom search * / / * tomtom search * /
def tomtom_version = "2.4771" implementation "com.tomtom.online:sdk-search:$TOMTOM_SEARCH"
implementation "com.tomtom.online:sdk-search:$tomtom_version" implementation "com.tomtom.online:sdk-maps:$TOMTOM_MAP"
implementation "com.tomtom.online:sdk-maps:2.4807" implementation "com.tomtom.online:sdk-search-core:$TOMTOM_SEARCH"
monoWeatherImplementation "com.tomtom.online:sdk-maps-ui-extensions:$TOMTOM_MAP"
/ * coroutines support for firebase operations * / implementation "com.tomtom.online:sdk-search-core:$TOMTOM_SEARCH"
def coroutines_google_ver = "1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines_google_ver"
/ * Picasso * / / * Picasso * /
implementation 'com.squareup.picasso:picasso:2.71828' implementation 'com.squareup.picasso:picasso:2.71828'
/ * screenshot library * / / * screenshot library * /
androidTestImplementation 'tools.fastlane:screengrab:2.1.1' androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
/ * Permissions dispatcher * / / * Permissions dispatcher * /
def dispatcher_ver = "4.9.2" implementation "com.github.permissions-dispatcher:permissionsdispatcher:$PERMISSIONS_DISPATCHER"
implementation "com.github.permissions-dispatcher:permissionsdispatcher:${dispatcher_ver}" kapt "com.github.permissions-dispatcher:permissionsdispatcher-processor:$PERMISSIONS_DISPATCHER"
kapt "com.github.permissions-dispatcher:permissionsdispatcher-processor:${dispatcher_ver}" implementation "com.github.permissions-dispatcher:permissionsdispatcher-annotation:$PERMISSIONS_DISPATCHER"
} }

View File

@@ -1,4 +0,0 @@
{
"cod": 401,
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."
}

View File

@@ -0,0 +1 @@
No account found with API key 'wrong api key'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import tools.fastlane.screengrab.Screengrab import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
import tools.fastlane.screengrab.locale.LocaleTestRule
@Suppress("EmptyMethod") @Suppress("EmptyMethod")
open class BaseTest<A : Activity>( open class BaseTest<A : Activity>(
@@ -47,9 +48,15 @@ open class BaseTest<A : Activity>(
@get:Rule @get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION) var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@get:Rule
var writePermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@get:Rule @get:Rule
var snapshotRule: SnapshotRule = SnapshotRule() var snapshotRule: SnapshotRule = SnapshotRule()
@Rule @JvmField
val localeTestRule = LocaleTestRule()
@Before @Before
fun setUp() { fun setUp() {
Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy()) Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())
@@ -73,8 +80,8 @@ open class BaseTest<A : Activity>(
afterLaunch() afterLaunch()
} }
fun stubEndpoint(url: String, stub: Stubs, code: Int = 200) { fun stubEndpoint(url: String, stub: Stubs, code: Int = 200, extension: String = ".json") {
testApp.stubUrl(url, stub.id, code) testApp.stubUrl(url, stub.id, code, extension)
} }
fun unstubEndpoint(url: String) { fun unstubEndpoint(url: String) {
@@ -85,8 +92,18 @@ open class BaseTest<A : Activity>(
testApp.stubLocation(location, lat, long) testApp.stubLocation(location, lat, long)
} }
fun clearLocation(location: String) {
testApp.removeLocation(location)
}
fun clearLocation(lat: Double, long: Double) {
testApp.removeLocation(lat, long)
}
fun clearPrefs() = prefs.clearPrefs() fun clearPrefs() = prefs.clearPrefs()
fun clearDatabase() = testApp.clearDatabase()
fun getActivity() = testActivity fun getActivity() = testActivity
@After @After
@@ -108,7 +125,6 @@ open class BaseTest<A : Activity>(
}) })
} }
@Suppress("DEPRECATION")
fun checkToastMessage(message: String) { fun checkToastMessage(message: String) {
Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView))) Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView)))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed())) .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))

View File

@@ -17,7 +17,12 @@ import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import com.appttude.h_mal.atlas_weather.helpers.checkErrorMessage import com.appttude.h_mal.atlas_weather.helpers.checkErrorMessage
import com.appttude.h_mal.atlas_weather.helpers.checkImage import com.appttude.h_mal.atlas_weather.helpers.checkImage
@@ -31,7 +36,7 @@ open class BaseTestRobot {
fun goBack() = Espresso.pressBack() fun goBack() = Espresso.pressBack()
fun fillEditText(resId: Int, text: String?): ViewInteraction = fun fillEditText(resId: Int, text: String): ViewInteraction =
onView(withId(resId)).perform( onView(withId(resId)).perform(
ViewActions.replaceText(text), ViewActions.replaceText(text),
ViewActions.closeSoftKeyboard() ViewActions.closeSoftKeyboard()
@@ -60,7 +65,7 @@ open class BaseTestRobot {
.atPosition(position).perform(click()) .atPosition(position).perform(click())
} }
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction {
return matchView(recyclerId) return matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -73,7 +78,7 @@ open class BaseTestRobot {
fun <VH : ViewHolder> scrollToRecyclerItem( fun <VH : ViewHolder> scrollToRecyclerItem(
recyclerId: Int, recyclerId: Int,
resIdForString: Int resIdForString: Int
): ViewInteraction? { ): ViewInteraction {
return matchView(recyclerId) return matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -86,7 +91,7 @@ open class BaseTestRobot {
fun <VH : ViewHolder> scrollToRecyclerItemByPosition( fun <VH : ViewHolder> scrollToRecyclerItemByPosition(
recyclerId: Int, recyclerId: Int,
position: Int position: Int
): ViewInteraction? { ): ViewInteraction {
return matchView(recyclerId) return matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -129,6 +134,16 @@ open class BaseTestRobot {
) )
} }
fun <VH : ViewHolder> clickSubViewInRecycler(
recyclerId: Int,
position: Int,
) {
scrollToRecyclerItemByPosition<VH>(recyclerId, position)
?.perform(
RecyclerViewActions.actionOnItemAtPosition<VH>(position, click())
)
}
fun checkErrorOnTextEntry(resId: Int, errorMessage: String): ViewInteraction = fun checkErrorOnTextEntry(resId: Int, errorMessage: String): ViewInteraction =
onView(withId(resId)).check(matches(checkErrorMessage(errorMessage))) onView(withId(resId)).check(matches(checkErrorMessage(errorMessage)))

View File

@@ -22,6 +22,16 @@ class MockLocationProvider : LocationProvider {
fun addLocationToList(name: String, lat: Double, long: Double) { fun addLocationToList(name: String, lat: Double, long: Double) {
val latLong = Pair(lat, long) val latLong = Pair(lat, long)
feedMap.put(name, latLong) feedMap[name] = latLong
}
fun removeLocationFromList(name: String) {
feedMap.remove(name)
}
fun removeLocationFromList(lat: Double, long: Double) {
feedMap.filterValues { it.first == lat && it.second == long }.keys.firstOrNull()?.let {
feedMap.remove(it)
}
} }
} }

View File

@@ -16,9 +16,9 @@ class MockingNetworkInterceptor(
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
idlingResource.increment() idlingResource.increment()
val original = chain.request() val original = chain.request()
val originalHttpUrl = original.url.toString().split("?")[0] val originalHttpUrl = original.url.toString()
feedMap[originalHttpUrl]?.let { responsePair -> feedMap[feedMap.keys.first { originalHttpUrl.contains(it) }]?.let { responsePair ->
val code = responsePair.second val code = responsePair.second
val jsonBody = responsePair.first val jsonBody = responsePair.first

View File

@@ -1,5 +1,6 @@
package com.appttude.h_mal.atlas_weather.utils package com.appttude.h_mal.atlas_weather.utils
const val baseUrl = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/"
enum class Stubs( enum class Stubs(
val id: String val id: String
) { ) {
@@ -7,5 +8,5 @@ enum class Stubs(
Imperial("valid_response_imperial"), Imperial("valid_response_imperial"),
WrongLocation("wrong_location_response"), WrongLocation("wrong_location_response"),
InvalidKey("invalid_api_key_response"), InvalidKey("invalid_api_key_response"),
Sydney("valid_response_metric_sydney") Sydney("valid_response_metric_sydney"),
} }

View File

@@ -13,7 +13,7 @@ fun <T> LiveData<T>.getOrAwaitValue(
var data: T? = null var data: T? = null
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val observer = object : Observer<T> { val observer = object : Observer<T> {
override fun onChanged(o: T?) { override fun onChanged(o: T) {
data = o data = o
latch.countDown() latch.countDown()
this@getOrAwaitValue.removeObserver(this) this@getOrAwaitValue.removeObserver(this)

View File

@@ -16,12 +16,12 @@ import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.data.room.Converter import com.appttude.h_mal.atlas_weather.data.room.Converter
import java.io.BufferedReader import java.io.BufferedReader
class TestAppClass : BaseAppClass() { class TestAppClass : AppClass() {
private val idlingResources = CountingIdlingResource("Data_loader") private val idlingResources = CountingIdlingResource("Data_loader")
private val mockingNetworkInterceptor = MockingNetworkInterceptor(idlingResources) private val mockingNetworkInterceptor = MockingNetworkInterceptor(idlingResources)
lateinit var database: AppDatabase lateinit var database: AppDatabase
lateinit var locationProvider: MockLocationProvider private val locationProvider: MockLocationProvider = MockLocationProvider()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -38,20 +38,20 @@ class TestAppClass : BaseAppClass() {
} }
override fun createLocationModule(): LocationProvider { override fun createLocationModule(): LocationProvider {
locationProvider = MockLocationProvider()
return locationProvider return locationProvider
} }
override fun createRoomDatabase(): AppDatabase { override fun createRoomDatabase(): AppDatabase {
database = Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java) database = Room.inMemoryDatabaseBuilder(applicationContext, AppDatabase::class.java)
.allowMainThreadQueries()
.addTypeConverter(Converter(this)) .addTypeConverter(Converter(this))
.build() .build()
return database return database
} }
fun stubUrl(url: String, rawPath: String, code: Int = 200) { fun stubUrl(url: String, rawPath: String, code: Int = 200, extension: String = ".json") {
val iStream = val iStream =
InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath.json") InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath$extension")
val data = iStream.bufferedReader().use(BufferedReader::readText) val data = iStream.bufferedReader().use(BufferedReader::readText)
mockingNetworkInterceptor.addUrlStub(url = url, data = data, code = code) mockingNetworkInterceptor.addUrlStub(url = url, data = data, code = code)
} }
@@ -64,4 +64,15 @@ class TestAppClass : BaseAppClass() {
locationProvider.addLocationToList(location, lat, long) locationProvider.addLocationToList(location, lat, long)
} }
fun removeLocation(location: String) {
locationProvider.removeLocationFromList(location)
}
fun removeLocation(lat: Double, long: Double) {
locationProvider.removeLocationFromList(lat, long)
}
fun clearDatabase() {
database.getWeatherDao().deleteAll()
}
} }

View File

@@ -0,0 +1,35 @@
package com.appttude.h_mal.atlas_weather.robot
import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
fun furtherInfoScreen(func: FurtherInfoScreen.() -> Unit) = FurtherInfoScreen().apply { func() }
class FurtherInfoScreen : BaseTestRobot() {
fun verifyMaxTemperature(temperature: Int) =
matchText(R.id.maxtemp, StringBuilder().append(temperature).append("°").toString())
fun verifyAverageTemperature(temperature: Int) =
matchText(R.id.averagetemp, StringBuilder().append(temperature).append("°").toString())
fun verifyMinTemperature(temperature: Int) =
matchText(R.id.minimumtemp, StringBuilder().append(temperature).append("°").toString())
fun verifyWindSpeed(speedText: String) =
matchText(R.id.windtext, speedText)
fun verifyHumidity(humidity: Int) =
matchText(R.id.humiditytext, humidity.toString())
fun verifyPrecipitation(precipitation: Int) =
matchText(R.id.preciptext, precipitation.toString())
fun verifyCloudCoverage(coverage: Int) =
matchText(R.id.cloudtext, coverage.toString())
fun verifyUvIndex(uv: Int) =
matchText(R.id.uvtext, uv.toString())
fun verifySunrise(sunrise: String) =
matchText(R.id.sunrisetext, sunrise)
fun verifySunset(sunset: String) =
matchText(R.id.sunsettext, sunset)
fun refresh() = pullToRefresh(R.id.swipe_refresh)
fun isDisplayed() = matchViewWaitFor(R.id.maxtemp)
}

View File

@@ -0,0 +1,53 @@
package com.appttude.h_mal.atlas_weather.robot
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.types.UnitType.Companion.getLabel
fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() }
class SettingsScreen : BaseTestRobot() {
fun selectWeatherUnits(unitType: UnitType) {
onView(withId(androidx.preference.R.id.recycler_view))
.perform(
RecyclerViewActions.actionOnItem<ViewHolder>(
ViewMatchers.hasDescendant(withText(R.string.weather_units)),
click()))
val label = unitType.getLabel()
onView(withText(label))
.inRoot(isDialog())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(click())
}
fun verifyCurrentTemperature(temperature: Int) =
matchText(R.id.temp_main_4, temperature.toString())
fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location)
fun refresh() = pullToRefresh(R.id.swipe_refresh)
fun verifyUnableToRetrieve() {
matchText(R.id.header_text, R.string.retrieve_warning)
matchText(R.id.body_text, R.string.empty_retrieve_warning)
}
fun isDisplayed() {
waitForView(
withText("Metric")
)
}
}

View File

@@ -0,0 +1,24 @@
package com.appttude.h_mal.atlas_weather.robot
import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily
fun weatherScreen(func: WeatherScreen.() -> Unit) = WeatherScreen().apply { func() }
class WeatherScreen : BaseTestRobot() {
fun verifyCurrentTemperature(temperature: Int) =
matchText(R.id.temp_main_4, temperature.toString())
fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location)
fun refresh() = pullToRefresh(R.id.swipe_refresh)
fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4)
fun verifyUnableToRetrieve() {
matchText(R.id.header_text, R.string.retrieve_warning)
matchText(R.id.body_text, R.string.empty_retrieve_warning)
}
fun tapDayInformationByPosition(position: Int) {
clickSubViewInRecycler<ViewHolderForecastDaily>(R.id.forecast_listview, position)
}
}

View File

@@ -0,0 +1,58 @@
package com.appttude.h_mal.atlas_weather.snapshot
import android.annotation.TargetApi
import androidx.test.filters.SmallTest
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.robot.furtherInfoScreen
import com.appttude.h_mal.atlas_weather.robot.settingsScreen
import com.appttude.h_mal.atlas_weather.robot.weatherScreen
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import org.junit.Test
import tools.fastlane.screengrab.Screengrab
@SmallTest
@TargetApi(27)
class SnapshotCaptureTest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint(baseUrl, Stubs.Metric)
stubLocation("London", 51.51, -0.13)
clearPrefs()
}
override fun testFinished() {
super.testFinished()
clearLocation("London")
clearDatabase()
clearPrefs()
}
@Test
fun homeAndFurtherInfoPageCapture() {
weatherScreen {
isDisplayed()
Screengrab.screenshot("HomeScreen")
tapDayInformationByPosition(4)
}
furtherInfoScreen {
isDisplayed()
Screengrab.screenshot("FurtherInfoScreen")
}
}
@Test
fun settingsPageCapture() {
weatherScreen {
isDisplayed()
openMenuItem()
}
settingsScreen {
stubEndpoint(baseUrl, Stubs.Imperial)
Screengrab.screenshot("SettingsScreen")
}
}
}

View File

@@ -1,36 +1,27 @@
package com.appttude.h_mal.atlas_weather.tests package com.appttude.h_mal.atlas_weather.tests
import com.appttude.h_mal.atlas_weather.BaseTest import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.ui.MainActivity import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.robot.homeScreen import com.appttude.h_mal.atlas_weather.utils.baseUrl
import org.junit.Test import org.junit.Test
class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) { class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() { override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.InvalidKey, 400) stubEndpoint(baseUrl, Stubs.InvalidKey, 400, ".txt")
}
@Test
fun loadApp_invalidKeyWeatherResponse_returnsEmptyViewPage() {
homeScreen {
waitFor(2000)
// verify empty
verifyUnableToRetrieve()
}
} }
@Test @Test
fun invalidKeyWeatherResponse_swipeToRefresh_returnsValidPage() { fun invalidKeyWeatherResponse_swipeToRefresh_returnsValidPage() {
homeScreen { homeScreen {
waitFor(2000)
// verify empty // verify empty
verifyUnableToRetrieve() verifyUnableToRetrieve()
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric) stubEndpoint(baseUrl, Stubs.Metric)
refresh() refresh()
verifyCurrentTemperature(2) verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location") verifyCurrentLocation("Mock Location")
} }
} }

View File

@@ -5,19 +5,20 @@ import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.robot.homeScreen import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.ui.MainActivity import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import org.junit.Test import org.junit.Test
class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) { class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() { override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric) stubEndpoint(baseUrl, Stubs.Metric)
} }
@Test @Test
fun loadApp_validWeatherResponse_returnsValidPage() { fun loadApp_validWeatherResponse_returnsValidPage() {
homeScreen { homeScreen {
isDisplayed() isDisplayed()
verifyCurrentTemperature(2) verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location") verifyCurrentLocation("Mock Location")
} }
} }

View File

@@ -0,0 +1,81 @@
package com.appttude.h_mal.atlas_weather.application
import androidx.room.Room
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.location.MockLocationProvider
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.interceptors.MockingNetworkInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.data.room.Converter
import org.kodein.di.LazyKodein
import java.io.BufferedReader
class TestAppClass : AppClass() {
override val kodein: LazyKodein = super.kodein
private val idlingResources = CountingIdlingResource("Data_loader")
private val mockingNetworkInterceptor = MockingNetworkInterceptor(idlingResources)
lateinit var database: AppDatabase
private val locationProvider: MockLocationProvider = MockLocationProvider()
override fun onCreate() {
super.onCreate()
IdlingRegistry.getInstance().register(idlingResources)
}
override fun createNetworkModule(): WeatherApi {
return NetworkModule().invoke<WeatherApi>(
mockingNetworkInterceptor,
NetworkConnectionInterceptor(this),
QueryParamsInterceptor(),
loggingInterceptor
) as WeatherApi
}
override fun createLocationModule(): LocationProvider {
return locationProvider
}
override fun createRoomDatabase(): AppDatabase {
database = Room.inMemoryDatabaseBuilder(applicationContext, AppDatabase::class.java)
.allowMainThreadQueries()
.addTypeConverter(Converter(this))
.build()
return database
}
fun stubUrl(url: String, rawPath: String, code: Int = 200, extension: String = ".json") {
val iStream =
InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath$extension")
val data = iStream.bufferedReader().use(BufferedReader::readText)
mockingNetworkInterceptor.addUrlStub(url = url, data = data, code = code)
}
fun removeUrlStub(url: String) {
mockingNetworkInterceptor.removeUrlStub(url = url)
}
fun stubLocation(location: String, lat: Double = 0.00, long: Double = 0.00) {
locationProvider.addLocationToList(location, lat, long)
}
fun removeLocation(location: String) {
locationProvider.removeLocationFromList(location)
}
fun removeLocation(lat: Double, long: Double) {
locationProvider.removeLocationFromList(lat, long)
}
fun clearDatabase() {
database.getWeatherDao().deleteAll()
}
}

View File

@@ -0,0 +1,59 @@
package com.appttude.h_mal.atlas_weather.snapshot
import android.annotation.TargetApi
import androidx.test.filters.SmallTest
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.furtherInfoScreen
import com.appttude.h_mal.monoWeather.robot.settingsScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Test
import tools.fastlane.screengrab.Screengrab
@SmallTest
@TargetApi(27)
class SnapshotCaptureTest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint(baseUrl, Stubs.Metric)
stubLocation("London", 51.5064, -0.12721)
clearPrefs()
}
override fun testFinished() {
super.testFinished()
clearLocation("London")
clearDatabase()
clearPrefs()
}
@Test
fun homeAndFurtherInfoPageCapture() {
weatherScreen {
isDisplayed()
Screengrab.screenshot("HomeScreen")
tapDayInformationByPosition(4)
}
furtherInfoScreen {
isDisplayed()
Screengrab.screenshot("FurtherInfoScreen")
}
}
@Test
fun settingsPageCapture() {
weatherScreen {
isDisplayed()
openMenuItem()
}
settingsScreen {
stubEndpoint(baseUrl, Stubs.Imperial)
Screengrab.screenshot("SettingsScreen")
}
}
}

View File

@@ -0,0 +1,37 @@
package com.appttude.h_mal.monoWeather.robot
import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.monoWeather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily
import com.appttude.h_mal.monoWeather.ui.home.adapter.further.ViewHolderFurtherDetails
fun furtherInfoScreen(func: FurtherInfoScreen.() -> Unit) = FurtherInfoScreen().apply { func() }
class FurtherInfoScreen : BaseTestRobot() {
fun verifyMaxTemperature(temperature: Int) =
matchText(R.id.maxtemp, StringBuilder().append(temperature).append("°").toString())
fun verifyAverageTemperature(temperature: Int) =
matchText(R.id.averagetemp, StringBuilder().append(temperature).append("°").toString())
fun verifyMinTemperature(temperature: Int) =
matchText(R.id.minimumtemp, StringBuilder().append(temperature).append("°").toString())
fun verifyWindSpeed(speedText: String) =
matchText(R.id.windtext, speedText)
fun verifyHumidity(humidity: Int) =
matchText(R.id.humiditytext, humidity.toString())
fun verifyPrecipitation(precipitation: Int) =
matchText(R.id.preciptext, precipitation.toString())
fun verifyCloudCoverage(coverage: Int) =
matchText(R.id.cloudtext, coverage.toString())
fun verifyUvIndex(uv: Int) =
matchText(R.id.uvtext, uv.toString())
fun verifySunrise(sunrise: String) =
matchText(R.id.sunrisetext, sunrise)
fun verifySunset(sunset: String) =
matchText(R.id.sunsettext, sunset)
fun refresh() = pullToRefresh(R.id.swipe_refresh)
fun isDisplayed() = matchViewWaitFor(R.id.maxtemp)
}

View File

@@ -11,7 +11,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import com.appttude.h_mal.atlas_weather.BaseTestRobot import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import com.appttude.h_mal.atlas_weather.model.types.UnitType import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.types.UnitType.Companion.getLabel
fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() } fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() }
@@ -23,10 +25,7 @@ class SettingsScreen : BaseTestRobot() {
RecyclerViewActions.actionOnItem<ViewHolder>( RecyclerViewActions.actionOnItem<ViewHolder>(
ViewMatchers.hasDescendant(withText(R.string.weather_units)), ViewMatchers.hasDescendant(withText(R.string.weather_units)),
click())) click()))
val label = when (unitType) { val label = unitType.getLabel()
UnitType.METRIC -> "Metric"
UnitType.IMPERIAL -> "Imperial"
}
onView(withText(label)) onView(withText(label))
.inRoot(isDialog()) .inRoot(isDialog())
@@ -45,4 +44,10 @@ class SettingsScreen : BaseTestRobot() {
matchText(R.id.header_text, R.string.retrieve_warning) matchText(R.id.header_text, R.string.retrieve_warning)
matchText(R.id.body_text, R.string.empty_retrieve_warning) matchText(R.id.body_text, R.string.empty_retrieve_warning)
} }
fun isDisplayed() {
waitForView(
withText("Metric")
)
}
} }

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.monoWeather.robot
import com.appttude.h_mal.atlas_weather.BaseTestRobot import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.monoWeather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily
fun weatherScreen(func: WeatherScreen.() -> Unit) = WeatherScreen().apply { func() } fun weatherScreen(func: WeatherScreen.() -> Unit) = WeatherScreen().apply { func() }
class WeatherScreen : BaseTestRobot() { class WeatherScreen : BaseTestRobot() {
@@ -16,4 +17,8 @@ class WeatherScreen : BaseTestRobot() {
matchText(R.id.header_text, R.string.retrieve_warning) matchText(R.id.header_text, R.string.retrieve_warning)
matchText(R.id.body_text, R.string.empty_retrieve_warning) matchText(R.id.body_text, R.string.empty_retrieve_warning)
} }
fun tapDayInformationByPosition(position: Int) {
clickSubViewInRecycler<ViewHolderForecastDaily>(R.id.forecast_listview, position)
}
} }

View File

@@ -4,21 +4,17 @@ package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.weatherScreen import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runners.MethodSorters
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) { class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() { override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.InvalidKey, 400) stubEndpoint(baseUrl, Stubs.InvalidKey, 400, ".txt")
}
@Test
fun loadApp_invalidKeyWeatherResponse_returnsEmptyViewPage() {
weatherScreen {
// verify empty
verifyUnableToRetrieve()
}
} }
@Test @Test
@@ -27,10 +23,11 @@ class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) {
// verify empty // verify empty
verifyUnableToRetrieve() verifyUnableToRetrieve()
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric) stubEndpoint(baseUrl, Stubs.Metric)
refresh() refresh()
verifyCurrentTemperature(2) verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location") verifyCurrentLocation("Mock Location")
} }
} }
} }

View File

@@ -2,17 +2,17 @@ package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.ui.MainActivity import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.monoWeather.robot.settingsScreen import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.furtherInfoScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Test import org.junit.Test
class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) { class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() { override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric) stubEndpoint(baseUrl, Stubs.Metric)
clearPrefs() clearPrefs()
} }
@@ -20,29 +20,24 @@ class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
fun loadApp_validWeatherResponse_returnsValidPage() { fun loadApp_validWeatherResponse_returnsValidPage() {
weatherScreen { weatherScreen {
isDisplayed() isDisplayed()
verifyCurrentTemperature(2) verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location") verifyCurrentLocation("Mock Location")
} }
} }
@Test @Test
fun loadApp_changeToImperial_returnsValidPage() { fun loadApp_validWeatherResponse_viewFurtherDetailsPage() {
weatherScreen { weatherScreen {
isDisplayed() isDisplayed()
verifyCurrentTemperature(2) verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location") verifyCurrentLocation("Mock Location")
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Imperial) tapDayInformationByPosition(4)
openMenuItem()
} }
settingsScreen { furtherInfoScreen {
selectWeatherUnits(UnitType.IMPERIAL)
goBack()
}
weatherScreen {
isDisplayed() isDisplayed()
refresh() verifyMaxTemperature(15)
verifyCurrentTemperature(58) verifyAverageTemperature(11)
verifyCurrentLocation("Mock Location")
} }
} }
} }

View File

@@ -0,0 +1,40 @@
package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.settingsScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Test
class SettingsPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint(baseUrl, Stubs.Metric)
clearPrefs()
}
@Test
fun loadApp_changeToImperial_returnsValidPage() {
weatherScreen {
isDisplayed()
verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location")
stubEndpoint(baseUrl, Stubs.Imperial)
openMenuItem()
}
settingsScreen {
selectWeatherUnits(UnitType.IMPERIAL)
goBack()
}
weatherScreen {
isDisplayed()
refresh()
verifyCurrentTemperature(56)
verifyCurrentLocation("Mock Location")
}
}
}

View File

@@ -4,6 +4,7 @@ package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.ContainerRobot.Tab.WORLD import com.appttude.h_mal.monoWeather.robot.ContainerRobot.Tab.WORLD
import com.appttude.h_mal.monoWeather.robot.addLocation import com.appttude.h_mal.monoWeather.robot.addLocation
import com.appttude.h_mal.monoWeather.robot.container import com.appttude.h_mal.monoWeather.robot.container
@@ -14,7 +15,8 @@ import org.junit.Test
class WorldPageUITest : BaseTest<MainActivity>(MainActivity::class.java) { class WorldPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() { override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric) stubEndpoint(baseUrl, Stubs.Metric)
stubLocation("London", 51.5064,-0.12721)
} }
@Test @Test
@@ -26,8 +28,8 @@ class WorldPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
clickFab() clickFab()
} }
addLocation { addLocation {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Sydney) stubEndpoint(baseUrl, Stubs.Sydney)
stubLocation("Sydney", -33.89, -151.12) stubLocation("Sydney",-33.8696,151.207)
setLocation("Sydney") setLocation("Sydney")
submit() submit()
} }
@@ -36,7 +38,7 @@ class WorldPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
} }
weatherScreen { weatherScreen {
isDisplayed() isDisplayed()
verifyCurrentTemperature(12) verifyCurrentTemperature(16)
verifyCurrentLocation("Sydney") verifyCurrentLocation("Sydney")
} }
} }

View File

@@ -2,8 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -27,23 +28,8 @@
</activity> </activity>
<receiver <receiver
android:name=".notification.NotificationReceiver" android:name=".service.notification.NotificationReceiver"
android:exported="true" android:exported="false"/>
android:parentActivityName=".MainActivity" />
<receiver
android:name=".widget.NewAppWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_ENABLED" />
<action android:name="com.example.h_mal.weather_app.app.ACTION_DATA_UPDATED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/new_app_widget_info" />
</receiver>
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,46 @@
package com.appttude.h_mal.atlas_weather.application
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.service.notification.NotificationService
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import com.appttude.h_mal.atlas_weather.viewmodel.SettingsViewModel
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
class ApplicationViewModelFactory(
private val application: Application,
private val locationProvider: LocationProvider,
private val source: WeatherSource,
private val settingsRepository: SettingsRepository,
private val notificationService: NotificationService
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
with(modelClass) {
return when {
isAssignableFrom(WorldViewModel::class.java) -> WorldViewModel(
locationProvider,
source
)
isAssignableFrom(MainViewModel::class.java) -> MainViewModel(
locationProvider,
source
)
isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(
application, locationProvider, source, settingsRepository, notificationService
)
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
}
}
}

View File

@@ -0,0 +1,37 @@
package com.appttude.h_mal.atlas_weather.application
import android.app.Application
import com.appttude.h_mal.atlas_weather.service.notification.NotificationHelper
import com.appttude.h_mal.atlas_weather.service.notification.NotificationService
import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
fun getFlavourModule(application: Application) = FlavourModule(application).build()
class FlavourModule(val application: Application) {
fun build() = Kodein.Module("Flavour") {
bind() from singleton {
NotificationHelper(
instance(),
instance(),
)
}
bind() from singleton {
NotificationService(application)
}
bind() from provider {
ApplicationViewModelFactory(
application,
instance(),
instance(),
instance(),
instance()
)
}
}
}

View File

@@ -1,8 +0,0 @@
package com.appttude.h_mal.atlas_weather.notification
import android.graphics.Bitmap
data class NotificationData(
val temp: String,
val icon: Bitmap
)

View File

@@ -1,72 +0,0 @@
package com.appttude.h_mal.atlas_weather.notification
import android.Manifest
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.utils.displayToast
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
/**
* Created by h_mal on 29/04/2018.
* Updated by h_mal on 27/11/2020
*/
const val NOTIFICATION_CHANNEL_ID = "my_notification_channel_1"
class NotificationReceiver : BroadcastReceiver() {
private val kodein = LateInitKodein()
private val helper: ServicesHelper by kodein.instance()
override fun onReceive(context: Context, intent: Intent) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
context.displayToast("Please enable location permissions")
return
}
// notification validation
}
private fun pushNotif(context: Context?, weather: FullWeather) {
val notificationIntent = Intent(context, MainActivity::class.java)
val stackBuilder = TaskStackBuilder.create(context).apply {
addParentStack(MainActivity::class.java)
addNextIntent(notificationIntent)
}
val pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
val notification = builder.setContentTitle("Weather App")
.setContentText(weather.current?.main + "°C")
.setSmallIcon(R.mipmap.ic_notif) //change icon
// .setLargeIcon(Icon.createWithResource(context, getImageResource(forecastItem.getCurrentForecast().getIconURL(), context)))
.setAutoCancel(true)
.setContentIntent(pendingIntent).build()
builder.setChannelId(NOTIFICATION_CHANNEL_ID)
val notificationManager =
context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(0, notification)
}
}

View File

@@ -0,0 +1,26 @@
package com.appttude.h_mal.atlas_weather.service.notification
import android.Manifest
import androidx.annotation.RequiresPermission
import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
class NotificationHelper(
private val weatherSource: WeatherSource,
private val locationProvider: LocationProvider
) {
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
suspend fun fetchData(): FullWeather? {
return try {
// Get location
val latLong = locationProvider.getCurrentLatLong()
weatherSource.getWeather(latLon = latLong)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}

View File

@@ -0,0 +1,117 @@
package com.appttude.h_mal.atlas_weather.service.notification
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import androidx.annotation.RequiresPermission
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
/**
* Created by h_mal on 29/04/2018.
* Updated by h_mal on 27/11/2020
*/
const val NOTIFICATION_CHANNEL_ID = "my_notification_channel_1"
const val NOTIFICATION_ID = 505
class NotificationReceiver : BroadcastReceiver() {
private val kodein = LateInitKodein()
private val helper: NotificationHelper by kodein.instance()
override fun onReceive(context: Context, intent: Intent) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
pushNotification(context)
} else {
context.displayToast("Please enable location permissions")
}
}
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
private fun pushNotification(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
// Retrieve weather data
val weather = runBlocking { helper.fetchData() } ?: return@launch
// Build notification
val notificationIntent = Intent(context, MainActivity::class.java)
val stackBuilder = TaskStackBuilder.create(context).apply {
addParentStack(MainActivity::class.java)
addNextIntent(notificationIntent)
}
val pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
val bmp: Bitmap = runBlocking { Picasso.get().load(weather.current?.icon).get() }
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_notif)
.setLargeIcon(bmp)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setContentTitle("My notification")
.setContentText("Much longer text that cannot fit one line...")
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Much longer text that cannot fit one line..."))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is not in the Support Library.
val name = context.getString(R.string.channel_name)
val descriptionText = context.getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system.
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
// grantResults: IntArray)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return@with
}
// notificationId is a unique int for each notification that you must define.
notify(NOTIFICATION_ID, builder.build())
}
}
}
}

View File

@@ -0,0 +1,65 @@
package com.appttude.h_mal.atlas_weather.service.notification
import android.app.AlarmManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Context.ALARM_SERVICE
import android.content.Intent
import android.icu.util.Calendar
import android.icu.util.GregorianCalendar
import androidx.core.app.NotificationManagerCompat
class NotificationService(context: Context) {
private val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager
private val alarmPendingIntent by lazy {
val intent = Intent(context, NotificationReceiver::class.java)
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
private val notificationManager = NotificationManagerCompat.from(context)
fun schedulePushNotifications() {
val calendar = getCalendarForNotification()
alarmManager.setWindow(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_HOUR,
alarmPendingIntent
)
}
fun unschedulePushNotifications() {
alarmManager.cancel(alarmPendingIntent)
}
fun areNotificationsEnabled() = when {
notificationManager.areNotificationsEnabled().not() -> false
else -> {
notificationManager.notificationChannels.firstOrNull { channel ->
channel.importance == NotificationManager.IMPORTANCE_NONE
} == null
}
}
private fun getCalendarForNotification(): Calendar {
// return GregorianCalendar.getInstance().apply {
// if (get(Calendar.HOUR_OF_DAY) >= HOUR_TO_SHOW_PUSH) {
// add(Calendar.DAY_OF_MONTH, 1)
// }
//
// set(Calendar.HOUR_OF_DAY, HOUR_TO_SHOW_PUSH)
// set(Calendar.MINUTE, 0)
// set(Calendar.SECOND, 0)
// set(Calendar.MILLISECOND, 0)
// }
return GregorianCalendar.getInstance().apply {
// add(Calendar.MINUTE, 1)
add(Calendar.SECOND, 10)
}
}
}

View File

@@ -6,11 +6,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.ui.home.adapter.WeatherRecyclerAdapter import com.appttude.h_mal.atlas_weather.ui.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.navigateTo import com.appttude.h_mal.atlas_weather.utils.navigateTo
import kotlinx.android.synthetic.main.fragment_home.*
class WorldItemFragment : Fragment() { class WorldItemFragment : Fragment() {
@@ -40,7 +41,7 @@ class WorldItemFragment : Fragment() {
param1?.let { recyclerAdapter.addCurrent(it) } param1?.let { recyclerAdapter.addCurrent(it) }
forecast_listview.apply { view.findViewById<RecyclerView>(R.id.forecast_listview).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter adapter = recyclerAdapter
} }

View File

@@ -4,10 +4,11 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
import kotlinx.android.synthetic.main.activity_further_info.*
private const val WEATHER = "param1" private const val WEATHER = "param1"
@@ -36,14 +37,12 @@ class FurtherInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
maxtemp.text = param1?.mainTemp view.findViewById<TextView>(R.id.maxtemp).text = param1?.mainTemp
averagetemp.text = param1?.averageTemp view.findViewById<TextView>(R.id.averagetemp).text = param1?.averageTemp
minimumtemp.text = param1?.minorTemp view.findViewById<TextView>(R.id.minimumtemp).text = param1?.minorTemp
windtext.text = param1?.windText view.findViewById<TextView>(R.id.windtext).text = param1?.windText
preciptext.text = param1?.precipitation view.findViewById<TextView>(R.id.preciptext).text = param1?.precipitation
humiditytext.text = param1?.humidity view.findViewById<TextView>(R.id.sunrisetext).text = param1?.sunrise
uvtext.text = param1?.uvi view.findViewById<TextView>(R.id.sunsettext).text = param1?.sunset
sunrisetext.text = param1?.sunrise
sunsettext.text = param1?.sunset
} }
} }

View File

@@ -1,7 +1,9 @@
package com.appttude.h_mal.atlas_weather.ui.home package com.appttude.h_mal.atlas_weather.ui.home
import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.POST_NOTIFICATIONS
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@@ -10,18 +12,19 @@ import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.Navigation.findNavController import androidx.navigation.Navigation.findNavController
import androidx.navigation.ui.onNavDestinationSelected import androidx.navigation.ui.onNavDestinationSelected
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.base.BaseFragment import com.appttude.h_mal.atlas_weather.base.BaseFragment
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.service.notification.NotificationService
import com.appttude.h_mal.atlas_weather.ui.dialog.PermissionsDeclarationDialog import com.appttude.h_mal.atlas_weather.ui.dialog.PermissionsDeclarationDialog
import com.appttude.h_mal.atlas_weather.ui.home.adapter.WeatherRecyclerAdapter import com.appttude.h_mal.atlas_weather.ui.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.displayToast import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.navigateTo import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import kotlinx.android.synthetic.main.fragment_home.* import org.kodein.di.generic.instance
import permissions.dispatcher.NeedsPermission import permissions.dispatcher.NeedsPermission
import permissions.dispatcher.OnNeverAskAgain import permissions.dispatcher.OnNeverAskAgain
import permissions.dispatcher.OnPermissionDenied import permissions.dispatcher.OnPermissionDenied
@@ -37,14 +40,17 @@ import permissions.dispatcher.RuntimePermissions
@RuntimePermissions @RuntimePermissions
class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) { class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
lateinit var recyclerAdapter: WeatherRecyclerAdapter private val notificationService by instance<NotificationService>()
private lateinit var recyclerAdapter: WeatherRecyclerAdapter
private lateinit var swipeRefresh: SwipeRefreshLayout
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
swipe_refresh.apply { swipeRefresh = view.findViewById<SwipeRefreshLayout>(R.id.swipe_refresh).apply {
setOnRefreshListener { setOnRefreshListener {
showLocationWithPermissionCheck() showLocationWithPermissionCheck()
isRefreshing = true isRefreshing = true
@@ -55,7 +61,9 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
navigateToFurtherDetails(it) navigateToFurtherDetails(it)
}) })
forecast_listview.adapter = recyclerAdapter view.findViewById<RecyclerView>(R.id.forecast_listview).adapter = recyclerAdapter
scheduleNotification()
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@@ -66,7 +74,7 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
override fun onSuccess(data: Any?) { override fun onSuccess(data: Any?) {
super.onSuccess(data) super.onSuccess(data)
swipe_refresh.isRefreshing = false swipeRefresh.isRefreshing = false
if (data is WeatherDisplay) { if (data is WeatherDisplay) {
recyclerAdapter.addCurrent(data) recyclerAdapter.addCurrent(data)
@@ -75,7 +83,7 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
override fun onFailure(error: Any?) { override fun onFailure(error: Any?) {
super.onFailure(error) super.onFailure(error)
swipe_refresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
private fun navigateToFurtherDetails(forecast: Forecast) { private fun navigateToFurtherDetails(forecast: Forecast) {
@@ -93,12 +101,25 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item) return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { @Deprecated("Deprecated in Java")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method // NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults) onRequestPermissionsResult(requestCode, grantResults)
} }
fun scheduleNotification() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
sendNotification()
} else {
notificationService.schedulePushNotifications()
}
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@NeedsPermission(ACCESS_COARSE_LOCATION) @NeedsPermission(ACCESS_COARSE_LOCATION)
fun showLocation() { fun showLocation() {
@@ -123,4 +144,29 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
fun onLocationNeverAskAgain() { fun onLocationNeverAskAgain() {
displayToast("Location permissions have been to never ask again") displayToast("Location permissions have been to never ask again")
} }
@SuppressLint("MissingPermission")
@NeedsPermission(POST_NOTIFICATIONS)
fun sendNotification() {
notificationService.schedulePushNotifications()
}
@OnShowRationale(POST_NOTIFICATIONS)
fun showRationaleForNotification(request: PermissionRequest) {
// PermissionsDeclarationDialog(requireContext()).showDialog({
// request.proceed()
// }, {
// request.cancel()
// })
}
@OnPermissionDenied(POST_NOTIFICATIONS)
fun onNotificationDenied() {
displayToast("Notification permissions have been denied")
}
@OnNeverAskAgain(POST_NOTIFICATIONS)
fun onNotificationNeverAskAgain() {
displayToast("Notification permissions have been to never ask again")
}
} }

View File

@@ -1,73 +1,61 @@
package com.appttude.h_mal.atlas_weather.ui.settings package com.appttude.h_mal.atlas_weather.ui.settings
import android.app.AlarmManager
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.notification.NotificationReceiver import com.appttude.h_mal.atlas_weather.base.BasePreferencesFragment
import com.appttude.h_mal.atlas_weather.widget.NewAppWidget import com.appttude.h_mal.atlas_weather.utils.displayToast
import java.util.Calendar import com.appttude.h_mal.atlas_weather.viewmodel.SettingsViewModel
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : BasePreferencesFragment<SettingsViewModel>(R.xml.prefs) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun preferenceChanged(key: String) {
setPreferencesFromResource(R.xml.prefs, rootKey) when (key) {
//listener on changed sort order preference: "temp_units" -> viewModel.refreshWeatherData()
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) "notif_boolean" -> {
prefs.registerOnSharedPreferenceChangeListener { _, key -> // TODO: update notification
if (key == "temp_units") { // viewModel.updateWidget()
val intent = Intent(requireContext(), NewAppWidget::class.java) // displayToast("Widget background has been updates")
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(requireContext())
.getAppWidgetIds(ComponentName(requireContext(), NewAppWidget::class.java))
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
requireContext().sendBroadcast(intent)
}
if (key == "notif_boolean") {
setupNotificationBroadcaster(requireContext())
}
if (key == "widget_black_background") {
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
val widgetManager = AppWidgetManager.getInstance(requireContext())
val ids =
widgetManager.getAppWidgetIds(
ComponentName(
requireContext(),
NewAppWidget::class.java
)
)
AppWidgetManager.getInstance(requireContext())
.notifyAppWidgetViewDataChanged(ids, R.id.whole_widget_view)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
requireContext().sendBroadcast(intent)
} }
} }
} }
fun setupNotificationBroadcaster(context: Context) { override fun onSuccess(data: Any?) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager super.onSuccess(data)
val notificationIntent = Intent(context, NotificationReceiver::class.java) if (data is String) displayToast(data)
val broadcast = PendingIntent.getBroadcast(
context, 100, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val cal: Calendar = Calendar.getInstance()
cal.set(Calendar.HOUR_OF_DAY, 6)
cal.set(Calendar.MINUTE, 8)
cal.set(Calendar.SECOND, 5)
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP,
cal.timeInMillis,
AlarmManager.INTERVAL_DAY,
broadcast
)
} }
} }
// override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// setPreferencesFromResource(R.xml.prefs, rootKey)
//
// //listener on changed sort order preference:
// val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
// prefs.registerOnSharedPreferenceChangeListener { _, key ->
// if (key == "temp_units") {
//
// }
// if (key == "notif_boolean") {
// setupNotificationBroadcaster(requireContext())
// }
// }
// }
//
// fun setupNotificationBroadcaster(context: Context) {
// val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
// val notificationIntent = Intent(context, NotificationReceiver::class.java)
// val broadcast = PendingIntent.getBroadcast(
// context, 100, notificationIntent,
// PendingIntent.FLAG_UPDATE_CURRENT
// )
// val cal: Calendar = Calendar.getInstance()
// cal.set(Calendar.HOUR_OF_DAY, 6)
// cal.set(Calendar.MINUTE, 8)
// cal.set(Calendar.SECOND, 5)
// alarmManager.setRepeating(
// AlarmManager.RTC_WAKEUP,
// cal.timeInMillis,
// AlarmManager.INTERVAL_DAY,
// broadcast
// )
// }
//}

View File

@@ -2,13 +2,13 @@ package com.appttude.h_mal.atlas_weather.ui.world
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Button
import android.widget.TextView
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.base.BaseFragment import com.appttude.h_mal.atlas_weather.base.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.displayToast import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.goBack import com.appttude.h_mal.atlas_weather.utils.goBack
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import kotlinx.android.synthetic.main.activity_add_forecast.location_name_tv
import kotlinx.android.synthetic.main.activity_add_forecast.submit
class AddLocationFragment : BaseFragment<WorldViewModel>(R.layout.activity_add_forecast) { class AddLocationFragment : BaseFragment<WorldViewModel>(R.layout.activity_add_forecast) {
@@ -16,8 +16,11 @@ class AddLocationFragment : BaseFragment<WorldViewModel>(R.layout.activity_add_f
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val submit = view.findViewById<Button>(R.id.submit)
submit.setOnClickListener { submit.setOnClickListener {
val locationName = location_name_tv.text?.trim()?.toString() val locationName =
view.findViewById<TextView>(R.id.location_name_tv).text?.trim()?.toString()
if (locationName.isNullOrBlank()) { if (locationName.isNullOrBlank()) {
submit.error = "Location cannot be blank" submit.error = "Location cannot be blank"
return@setOnClickListener return@setOnClickListener

View File

@@ -4,13 +4,13 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.base.BaseFragment import com.appttude.h_mal.atlas_weather.base.BaseFragment
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.utils.navigateTo import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import kotlinx.android.synthetic.atlasWeather.fragment_add_location.floatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.android.synthetic.atlasWeather.fragment_add_location.world_recycler
/** /**
@@ -29,19 +29,21 @@ class WorldFragment : BaseFragment<WorldViewModel>(R.layout.fragment_add_locatio
navigateTo(direction) navigateTo(direction)
} }
world_recycler.apply { view.findViewById<RecyclerView>(R.id.world_recycler).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter adapter = recyclerAdapter
} }
floatingActionButton.setOnClickListener { view.findViewById<FloatingActionButton>(R.id.floatingActionButton).setOnClickListener {
navigateTo(R.id.action_worldFragment_to_addLocationFragment) navigateTo(R.id.action_worldFragment_to_addLocationFragment)
} }
} }
@Suppress("UNCHECKED_CAST")
override fun onSuccess(data: Any?) { override fun onSuccess(data: Any?) {
super.onSuccess(data) super.onSuccess(data)
if (data is List<*>) recyclerAdapter.addCurrent(data as List<WeatherDisplay>) if (data is List<*>)
recyclerAdapter.addCurrent(data as List<WeatherDisplay>)
} }
override fun onResume() { override fun onResume() {

View File

@@ -0,0 +1,58 @@
package com.appttude.h_mal.atlas_weather.viewmodel
import android.Manifest
import android.app.Application
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import com.appttude.h_mal.atlas_weather.application.BaseAppClass
import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseAndroidViewModel
import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.service.notification.NotificationService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Locale
class SettingsViewModel(
application: Application,
private val locationProvider: LocationProvider,
private val weatherSource: WeatherSource,
private val settingsRepository: SettingsRepository,
private val notificationService: NotificationService
) : BaseAndroidViewModel(application) {
private fun getContext() = getApplication<BaseAppClass>().applicationContext
fun refreshWeatherData() {
onStart()
job = CoroutineScope(Dispatchers.IO).launch {
try {
if (ActivityCompat.checkSelfPermission(
getContext(),
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
// Get location
val latLong = locationProvider.getCurrentLatLong()
weatherSource.forceFetchWeather(latLong)
}
val units = settingsRepository.getUnitType().name.lowercase(Locale.ROOT)
onSuccess("Units have been changes to $units")
} catch (e: Exception) {
e.printStackTrace()
onError(e.message ?: "Retrieving weather failed")
}
}
}
fun toggleNotifications() {
if (notificationService.areNotificationsEnabled()) {
notificationService.unschedulePushNotifications()
} else {
notificationService.schedulePushNotifications()
}
}
}

View File

@@ -1,320 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="12dp"
android:layout_weight="2">
<ImageView
style="@style/icon_style__further_details"
android:src="@drawable/somethingnew" />
</FrameLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="@string/max" />
<TextView
android:id="@+id/maxtemp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
tools:text="11" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="@string/average" />
<TextView
android:id="@+id/averagetemp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
tools:text="11" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="@string/min" />
<TextView
android:id="@+id/minimumtemp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
tools:text="11" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="12dp"
android:layout_weight="2">
<ImageView
style="@style/icon_style__further_details"
android:src="@drawable/breeze" />
</FrameLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_weight="3">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="Wind: " />
<TextView
android:id="@+id/windtext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="7mph" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="12dp"
android:layout_weight="2">
<ImageView
style="@style/icon_style__further_details"
android:src="@drawable/water_drop" />
</FrameLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="Humidity: " />
<TextView
android:id="@+id/humiditytext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="85%" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="Precip: " />
<TextView
android:id="@+id/preciptext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="11mm" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="12dp"
android:layout_weight="2">
<ImageView
style="@style/icon_style__further_details"
android:src="@drawable/sunrise" />
</FrameLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="UV: " />
<TextView
android:id="@+id/uvtext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="7" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="Sunrise:" />
<TextView
android:id="@+id/sunrisetext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="05:30am" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="Sunset:" />
<TextView
android:id="@+id/sunsettext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="06:12pm" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -58,5 +58,5 @@
<fragment <fragment
android:id="@+id/settings_fragment" android:id="@+id/settings_fragment"
android:name="com.appttude.h_mal.atlas_weather.ui.settings.SettingsFragment" android:name="com.appttude.h_mal.atlas_weather.ui.settings.SettingsFragment"
android:label="SettingsFragment" /> android:label="Settings" />
</navigation> </navigation>

View File

@@ -2,11 +2,13 @@
<resources> <resources>
<color name="colorPrimary">#3F51B5</color> <color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color> <color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color> <color name="colorAccent">@android:color/white</color>
<color name="colour_one">#E8D0DD</color> <color name="colour_one">#E8D0DD</color>
<color name="colour_two">#5F8E7B</color> <color name="colour_two">#5F8E7B</color>
<color name="colour_three">#B3C0CA</color> <color name="colour_three">#B3C0CA</color>
<color name="colour_four">#8C98AD</color> <color name="colour_four">#8C98AD</color>
<color name="colour_five">#2E3532</color> <color name="colour_five">#2E3532</color>
<color name="weather_cell_colour">@android:color/transparent</color>
</resources> </resources>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.appttude.h_mal.atlas_weather.ui.widget.WidgetLocationPermissionActivity"
android:initialKeyguardLayout="@layout/weather_app_widget"
android:initialLayout="@layout/weather_app_widget"
android:minWidth="320.0dp"
android:minHeight="110.0dp"
android:minResizeWidth="320.0dp"
android:minResizeHeight="110.0dp"
android:previewImage="@drawable/widget_screenshot"
android:resizeMode="vertical"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen">
</appwidget-provider>

View File

@@ -1,42 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
<!-- <PreferenceCategory android:title="Units">--> android:title="@string/weather_units"
<!-- <ListPreference--> android:entries="@array/units"
<!-- android:defaultValue="°C"--> android:entryValues="@array/units"
<!-- android:entries="@array/list_preference_temp"--> android:defaultValue="Metric"
<!-- android:entryValues="@array/list_preference_temp"--> android:key="UnitType"
<!-- android:key="temp_units"--> />
<!-- android:title="Temperature Units" />-->
<!-- <ListPreference-->
<!-- android:defaultValue="kph"-->
<!-- android:entries="@array/list_preference_wind"-->
<!-- android:entryValues="@array/list_preference_wind_values"-->
<!-- android:key="wind_units"-->
<!-- android:title="Wind Units" />-->
<!-- <ListPreference-->
<!-- android:defaultValue="mm"-->
<!-- android:entries="@array/list_preference_precip"-->
<!-- android:entryValues="@array/list_preference_precip_values"-->
<!-- android:key="precip_units"-->
<!-- android:title="Precipitation Units" />-->
<!-- <ListPreference-->
<!-- android:defaultValue="km"-->
<!-- android:entries="@array/list_preference_vis"-->
<!-- android:entryValues="@array/list_preference_vis_values"-->
<!-- android:key="vis_units"-->
<!-- android:title="Visibility Units" />-->
<!-- </PreferenceCategory>-->
<PreferenceCategory android:title="Notification Settings"> <PreferenceCategory android:title="Notification Settings">
<SwitchPreference <SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="notif_boolean" android:key="notif_boolean"
android:title="Notification" /> android:title="Notifications enabled" />
</PreferenceCategory>
<PreferenceCategory android:title="Widget Settings">
<SwitchPreference
android:defaultValue="false"
android:key="widget_black_background"
android:title="Set widget background black" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@@ -0,0 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Allows storing screenshots on external storage, where it can be accessed by ADB -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- Allows changing locales -->
<uses-permission
android:name="android.permission.CHANGE_CONFIGURATION"
tools:ignore="ProtectedPermissions" />
<!-- Allows changing SystemUI demo mode -->
<uses-permission
android:name="android.permission.DUMP"
tools:ignore="ProtectedPermissions" />
</manifest>

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="com.appttude.h_mal.atlas_weather">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" /> <uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
@@ -18,6 +18,7 @@
android:required="true" /> android:required="true" />
<application <application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="MissingApplicationIcon" /> tools:ignore="MissingApplicationIcon" />

View File

@@ -1,5 +1,6 @@
package com.appttude.h_mal.atlas_weather.application package com.appttude.h_mal.atlas_weather.application
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
@@ -8,9 +9,7 @@ import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInt
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
const val LOCATION_PERMISSION_REQUEST = 505 open class AppClass : BaseAppClass() {
class AppClass : BaseAppClass() {
override fun createNetworkModule(): WeatherApi { override fun createNetworkModule(): WeatherApi {
return NetworkModule().invoke<WeatherApi>( return NetworkModule().invoke<WeatherApi>(
@@ -20,7 +19,7 @@ class AppClass : BaseAppClass() {
) as WeatherApi ) as WeatherApi
} }
override fun createLocationModule() = LocationProviderImpl(this) override fun createLocationModule(): LocationProvider = LocationProviderImpl(this)
override fun createRoomDatabase(): AppDatabase = AppDatabase(this) override fun createRoomDatabase(): AppDatabase = AppDatabase(this)

View File

@@ -3,29 +3,33 @@ package com.appttude.h_mal.atlas_weather.application
import android.app.Application import android.app.Application
import com.appttude.h_mal.atlas_weather.data.WeatherSource import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.network.Api
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.google.gson.Gson import com.google.gson.Gson
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind import org.kodein.di.generic.bind
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton import org.kodein.di.generic.singleton
abstract class BaseAppClass : Application(), KodeinAware { abstract class BaseAppClass : Application(), KodeinAware {
// Kodein creation of modules to be retrieve within the app // Kodein aware to initialise the classes used for DI
override val kodein = Kodein.lazy { override val kodein = Kodein.lazy {
import(parentModule)
import(getFlavourModule(application = this@BaseAppClass))
}
val parentModule = Kodein.Module("Parent Module", allowSilentOverride = true) {
import(androidXModule(this@BaseAppClass)) import(androidXModule(this@BaseAppClass))
bind() from singleton { createNetworkModule() } bind() from singleton { createNetworkModule() as WeatherApi }
bind() from singleton { createLocationModule() } bind() from singleton { createLocationModule() }
bind() from singleton { Gson() } bind() from singleton { Gson() }
@@ -35,10 +39,9 @@ abstract class BaseAppClass : Application(), KodeinAware {
bind() from singleton { SettingsRepositoryImpl(instance()) } bind() from singleton { SettingsRepositoryImpl(instance()) }
bind() from singleton { ServicesHelper(instance(), instance(), instance()) } bind() from singleton { ServicesHelper(instance(), instance(), instance()) }
bind() from singleton { WeatherSource(instance(), instance()) } bind() from singleton { WeatherSource(instance(), instance()) }
bind() from provider { ApplicationViewModelFactory(this@BaseAppClass, instance(), instance(),instance()) }
} }
abstract fun createNetworkModule(): WeatherApi abstract fun createNetworkModule(): Api
abstract fun createLocationModule(): LocationProvider abstract fun createLocationModule(): LocationProvider
abstract fun createRoomDatabase(): AppDatabase abstract fun createRoomDatabase(): AppDatabase

View File

@@ -77,6 +77,7 @@ abstract class BaseActivity : AppCompatActivity(), KodeinAware {
} }
@Deprecated("Deprecated in Java")
override fun onBackPressed() { override fun onBackPressed() {
loadingView?.hide() loadingView?.hide()
super.onBackPressed() super.onBackPressed()

View File

@@ -8,7 +8,7 @@ import androidx.fragment.app.createViewModelLazy
import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseViewModel import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseViewModel
import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
import com.appttude.h_mal.atlas_weather.model.ViewState import com.appttude.h_mal.atlas_weather.model.ViewState
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory import com.appttude.h_mal.atlas_weather.application.ApplicationViewModelFactory
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance

View File

@@ -6,11 +6,10 @@ import androidx.annotation.XmlRes
import androidx.fragment.app.createViewModelLazy import androidx.fragment.app.createViewModelLazy
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseAndroidViewModel import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseAndroidViewModel
import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
import com.appttude.h_mal.atlas_weather.model.ViewState import com.appttude.h_mal.atlas_weather.model.ViewState
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory import com.appttude.h_mal.atlas_weather.application.ApplicationViewModelFactory
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
@@ -48,7 +47,7 @@ abstract class BasePreferencesFragment<V : BaseAndroidViewModel>(@XmlRes private
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
prefs.registerOnSharedPreferenceChangeListener { _, s -> prefs.registerOnSharedPreferenceChangeListener { _, s ->
preferenceChanged(s) s?.let { preferenceChanged(s) }
} }
} }

View File

@@ -5,8 +5,8 @@ import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.LocationType import com.appttude.h_mal.atlas_weather.model.types.LocationType
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.utils.getSymbol
import java.io.IOException import java.io.IOException
class WeatherSource( class WeatherSource(
@@ -33,12 +33,14 @@ class WeatherSource(
} }
@Throws(IOException::class) @Throws(IOException::class)
suspend fun forceFetchWeather(latLon: Pair<Double, Double>, suspend fun forceFetchWeather(
locationType: LocationType = LocationType.Town): FullWeather { latLon: Pair<Double, Double>,
locationType: LocationType = LocationType.Town
): FullWeather {
// get data from database // get data from database
val weatherEntity = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION) val weatherEntity = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
// check unit type - if same do nothing // check unit type - if same do nothing
val units = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F" val units = repository.getUnitType().getSymbol()
if (weatherEntity.weather.temperatureUnit == units) return weatherEntity.weather if (weatherEntity.weather.temperatureUnit == units) return weatherEntity.weather
// load data for forced // load data for forced
return fetchWeather( return fetchWeather(
@@ -55,11 +57,13 @@ class WeatherSource(
// Get weather from api // Get weather from api
val weather = repository val weather = repository
.getWeatherFromApi(latLon.first.toString(), latLon.second.toString()) .getWeatherFromApi(latLon.first.toString(), latLon.second.toString())
val lat = weather.latitude ?: latLon.first
val long = weather.longitude ?: latLon.second
val currentLocation = val currentLocation =
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, locationType) locationProvider.getLocationNameFromLatLong(lat, long, locationType)
val unit = repository.getUnitType() val unit = repository.getUnitType().getSymbol()
val fullWeather = FullWeather(weather).apply { val fullWeather = weather.mapData().apply {
temperatureUnit = if (unit == UnitType.METRIC) "°C" else "°F" temperatureUnit = unit
locationString = currentLocation locationString = currentLocation
} }
val entityItem = EntityItem(locationName, fullWeather) val entityItem = EntityItem(locationName, fullWeather)

View File

@@ -1,6 +1,6 @@
package com.appttude.h_mal.atlas_weather.data.network package com.appttude.h_mal.atlas_weather.data.network
class NetworkModule : BaseNetworkModule() { class NetworkModule : BaseNetworkModule() {
override fun baseUrl(): String = "https://api.openweathermap.org/data/2.5/" override fun baseUrl(): String = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/"
} }

View File

@@ -1,9 +1,7 @@
package com.appttude.h_mal.atlas_weather.data.network package com.appttude.h_mal.atlas_weather.data.network
import org.json.JSONException import retrofit2.HttpException
import org.json.JSONObject
import retrofit2.Response import retrofit2.Response
import java.io.IOException
abstract class ResponseUnwrap { abstract class ResponseUnwrap {
@@ -15,18 +13,7 @@ abstract class ResponseUnwrap {
if (response.isSuccessful) { if (response.isSuccessful) {
return response.body()!! return response.body()!!
} else { } else {
val error = response.errorBody()?.string() throw HttpException(response)
val errorMessage = error?.let {
try {
JSONObject(it).getString("message")
} catch (e: JSONException) {
e.printStackTrace()
null
}
} ?: "Error Code: ${response.code()}"
throw IOException(errorMessage)
} }
} }
} }

View File

@@ -1,20 +1,20 @@
package com.appttude.h_mal.atlas_weather.data.network package com.appttude.h_mal.atlas_weather.data.network
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface WeatherApi : Api { interface WeatherApi : Api {
@GET("onecall?") @GET("{location}")
suspend fun getFromApi( suspend fun getFromApi(
@Query("lat") query: String, @Path("location") location: String,
@Query("lon") lon: String, @Query("contentType") exclude: String = "json",
@Query("exclude") exclude: String = "minutely", @Query("unitGroup") units: String = "metric"
@Query("units") units: String = "metric" ): Response<WeatherApiResponse>
): Response<WeatherResponse>
} }

View File

@@ -16,7 +16,7 @@ class QueryParamsInterceptor : Interceptor {
val original = chain.request() val original = chain.request()
val url = original.url.newBuilder() val url = original.url.newBuilder()
.addQueryParameter("appid", id) .addQueryParameter("key", id)
.build() .build()
// Request customization: add request headers // Request customization: add request headers

View File

@@ -1,11 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.network.networkUtils package com.appttude.h_mal.atlas_weather.data.network.networkUtils
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor
import com.google.gson.GsonBuilder
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.lang.reflect.Modifier
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
val loggingInterceptor = HttpLoggingInterceptor().apply { val loggingInterceptor = HttpLoggingInterceptor().apply {
@@ -34,6 +36,13 @@ fun buildOkHttpClient(
return builder.build() return builder.build()
} }
fun createGsonConverterFactory(): GsonConverterFactory {
val gson = GsonBuilder()
.excludeFieldsWithModifiers(Modifier.TRANSIENT)
.create()
return GsonConverterFactory.create(gson)
}
fun <T> createRetrofit( fun <T> createRetrofit(
baseUrl: String, baseUrl: String,
okHttpClient: OkHttpClient, okHttpClient: OkHttpClient,
@@ -42,7 +51,7 @@ fun <T> createRetrofit(
return Retrofit.Builder() return Retrofit.Builder()
.client(okHttpClient) .client(okHttpClient)
.baseUrl(baseUrl) .baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(createGsonConverterFactory())
.build() .build()
.create(service) .create(service)
} }

View File

@@ -1,48 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Current(
@field:SerializedName("sunrise")
val sunrise: Int? = null,
@field:SerializedName("temp")
val temp: Double? = null,
@field:SerializedName("visibility")
val visibility: Int? = null,
@field:SerializedName("uvi")
val uvi: Double? = null,
@field:SerializedName("pressure")
val pressure: Int? = null,
@field:SerializedName("clouds")
val clouds: Int? = null,
@field:SerializedName("feels_like")
val feelsLike: Double? = null,
@field:SerializedName("dt")
val dt: Int? = null,
@field:SerializedName("wind_deg")
val windDeg: Int? = null,
@field:SerializedName("dew_point")
val dewPoint: Double? = null,
@field:SerializedName("sunset")
val sunset: Int? = null,
@field:SerializedName("weather")
val weather: List<WeatherItem?>? = null,
@field:SerializedName("humidity")
val humidity: Int? = null,
@field:SerializedName("wind_speed")
val windSpeed: Double? = null
)

View File

@@ -1,51 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class DailyItem(
@field:SerializedName("sunrise")
val sunrise: Int? = null,
@field:SerializedName("temp")
val temp: Temp? = null,
@field:SerializedName("uvi")
val uvi: Double? = null,
@field:SerializedName("pressure")
val pressure: Int? = null,
@field:SerializedName("clouds")
val clouds: Int? = null,
@field:SerializedName("feels_like")
val feelsLike: FeelsLike? = null,
@field:SerializedName("dt")
val dt: Int? = null,
@field:SerializedName("pop")
val pop: Double? = null,
@field:SerializedName("wind_deg")
val windDeg: Int? = null,
@field:SerializedName("dew_point")
val dewPoint: Double? = null,
@field:SerializedName("sunset")
val sunset: Int? = null,
@field:SerializedName("weather")
val weather: List<WeatherItem?>? = null,
@field:SerializedName("humidity")
val humidity: Int? = null,
@field:SerializedName("wind_speed")
val windSpeed: Double? = null,
@field:SerializedName("rain")
val rain: Double? = null
)

View File

@@ -1,18 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class FeelsLike(
@field:SerializedName("eve")
val eve: Double? = null,
@field:SerializedName("night")
val night: Double? = null,
@field:SerializedName("day")
val day: Double? = null,
@field:SerializedName("morn")
val morn: Double? = null
)

View File

@@ -1,48 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Hour(
@field:SerializedName("sunrise")
val sunrise: Int? = null,
@field:SerializedName("temp")
val temp: Double? = null,
@field:SerializedName("visibility")
val visibility: Int? = null,
@field:SerializedName("uvi")
val uvi: Double? = null,
@field:SerializedName("pressure")
val pressure: Int? = null,
@field:SerializedName("clouds")
val clouds: Int? = null,
@field:SerializedName("feels_like")
val feelsLike: Double? = null,
@field:SerializedName("dt")
val dt: Int? = null,
@field:SerializedName("wind_deg")
val windDeg: Int? = null,
@field:SerializedName("dew_point")
val dewPoint: Double? = null,
@field:SerializedName("sunset")
val sunset: Int? = null,
@field:SerializedName("weather")
val weather: List<WeatherItem?>? = null,
@field:SerializedName("humidity")
val humidity: Int? = null,
@field:SerializedName("wind_speed")
val windSpeed: Double? = null
)

View File

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Response(
@field:SerializedName("current")
val current: Current? = null,
@field:SerializedName("timezone")
val timezone: String? = null,
@field:SerializedName("timezone_offset")
val timezoneOffset: Int? = null,
@field:SerializedName("daily")
val daily: List<DailyItem?>? = null,
@field:SerializedName("lon")
val lon: Double? = null,
@field:SerializedName("lat")
val lat: Double? = null
)

View File

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Temp(
@field:SerializedName("min")
val min: Double? = null,
@field:SerializedName("max")
val max: Double? = null,
@field:SerializedName("eve")
val eve: Double? = null,
@field:SerializedName("night")
val night: Double? = null,
@field:SerializedName("day")
val day: Double? = null,
@field:SerializedName("morn")
val morn: Double? = null
)

View File

@@ -1,18 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class WeatherItem(
@field:SerializedName("icon")
val icon: String? = null,
@field:SerializedName("description")
val description: String? = null,
@field:SerializedName("main")
val main: String? = null,
@field:SerializedName("id")
val id: Int? = null
)

View File

@@ -1,28 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class WeatherResponse(
@field:SerializedName("current")
val current: Current? = null,
@field:SerializedName("timezone")
val timezone: String? = null,
@field:SerializedName("timezone_offset")
val timezoneOffset: Int? = null,
@field:SerializedName("hourly")
val hourly: List<Hour>? = null,
@field:SerializedName("daily")
val daily: List<DailyItem>? = null,
@field:SerializedName("lon")
val lon: Double = 0.00,
@field:SerializedName("lat")
val lat: Double = 0.00
)

View File

@@ -0,0 +1,16 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class Alerts(
@SerializedName("event") var event: String? = null,
@SerializedName("headline") var headline: String? = null,
@SerializedName("ends") var ends: String? = null,
@SerializedName("endsEpoch") var endsEpoch: Int? = null,
@SerializedName("onset") var onset: String? = null,
@SerializedName("onsetEpoch") var onsetEpoch: Int? = null,
@SerializedName("id") var id: String? = null,
@SerializedName("language") var language: String? = null,
@SerializedName("link") var link: String? = null,
@SerializedName("description") var description: String? = null,
)

View File

@@ -0,0 +1,36 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class CurrentConditions(
@SerializedName("datetime") var datetime: String? = null,
@SerializedName("datetimeEpoch") var datetimeEpoch: Int? = null,
@SerializedName("temp") var temp: Double? = null,
@SerializedName("feelslike") var feelslike: Double? = null,
@SerializedName("humidity") var humidity: Double? = null,
@SerializedName("dew") var dew: Double? = null,
@SerializedName("precip") var precip: Double? = null,
@SerializedName("precipprob") var precipprob: Double? = null,
@SerializedName("snow") var snow: Int? = null,
@SerializedName("snowdepth") var snowdepth: Int? = null,
@SerializedName("preciptype") var preciptype: ArrayList<String> = arrayListOf(),
@SerializedName("windgust") var windgust: Double? = null,
@SerializedName("windspeed") var windspeed: Double? = null,
@SerializedName("winddir") var winddir: Double? = null,
@SerializedName("pressure") var pressure: Double? = null,
@SerializedName("visibility") var visibility: Double? = null,
@SerializedName("cloudcover") var cloudcover: Double? = null,
@SerializedName("solarradiation") var solarradiation: Double? = null,
@SerializedName("solarenergy") var solarenergy: Double? = null,
@SerializedName("uvindex") var uvindex: Int? = null,
@SerializedName("conditions") var conditions: String? = null,
@SerializedName("icon") var icon: String? = null,
@SerializedName("stations") var stations: ArrayList<String> = arrayListOf(),
@SerializedName("source") var source: String? = null,
@SerializedName("sunrise") var sunrise: String? = null,
@SerializedName("sunriseEpoch") var sunriseEpoch: Int? = null,
@SerializedName("sunset") var sunset: String? = null,
@SerializedName("sunsetEpoch") var sunsetEpoch: Int? = null,
@SerializedName("moonphase") var moonphase: Double? = null
)

View File

@@ -0,0 +1,45 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class Days(
@SerializedName("datetime") var datetime: String? = null,
@SerializedName("datetimeEpoch") var datetimeEpoch: Int? = null,
@SerializedName("tempmax") var tempmax: Double? = null,
@SerializedName("tempmin") var tempmin: Double? = null,
@SerializedName("temp") var temp: Double? = null,
@SerializedName("feelslikemax") var feelslikemax: Double? = null,
@SerializedName("feelslikemin") var feelslikemin: Double? = null,
@SerializedName("feelslike") var feelslike: Double? = null,
@SerializedName("dew") var dew: Double? = null,
@SerializedName("humidity") var humidity: Double? = null,
@SerializedName("precip") var precip: Number? = null,
@SerializedName("precipprob") var precipprob: Double? = null,
@SerializedName("precipcover") var precipcover: Double? = null,
@SerializedName("preciptype") var preciptype: ArrayList<String> = arrayListOf(),
@SerializedName("snow") var snow: Int? = null,
@SerializedName("snowdepth") var snowdepth: Int? = null,
@SerializedName("windgust") var windgust: Double? = null,
@SerializedName("windspeed") var windspeed: Double? = null,
@SerializedName("winddir") var winddir: Double? = null,
@SerializedName("pressure") var pressure: Double? = null,
@SerializedName("cloudcover") var cloudcover: Double? = null,
@SerializedName("visibility") var visibility: Double? = null,
@SerializedName("solarradiation") var solarradiation: Double? = null,
@SerializedName("solarenergy") var solarenergy: Double? = null,
@SerializedName("uvindex") var uvindex: Int? = null,
@SerializedName("severerisk") var severerisk: Int? = null,
@SerializedName("sunrise") var sunrise: String? = null,
@SerializedName("sunriseEpoch") var sunriseEpoch: Int? = null,
@SerializedName("sunset") var sunset: String? = null,
@SerializedName("sunsetEpoch") var sunsetEpoch: Int? = null,
@SerializedName("moonphase") var moonphase: Double? = null,
@SerializedName("conditions") var conditions: String? = null,
@SerializedName("description") var description: String? = null,
@SerializedName("icon") var icon: String? = null,
@SerializedName("stations") var stations: ArrayList<String> = arrayListOf(),
@SerializedName("source") var source: String? = null,
@SerializedName("hours") var hours: ArrayList<Hours> = arrayListOf()
)

View File

@@ -0,0 +1,32 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class Hours(
@SerializedName("datetime") var datetime: String? = null,
@SerializedName("datetimeEpoch") var datetimeEpoch: Int? = null,
@SerializedName("temp") var temp: Double? = null,
@SerializedName("feelslike") var feelslike: Double? = null,
@SerializedName("humidity") var humidity: Double? = null,
@SerializedName("dew") var dew: Double? = null,
@SerializedName("precip") var precip: Number? = null,
@SerializedName("precipprob") var precipprob: Double? = null,
@SerializedName("snow") var snow: Int? = null,
@SerializedName("snowdepth") var snowdepth: Int? = null,
@SerializedName("preciptype") var preciptype: ArrayList<String> = arrayListOf(),
@SerializedName("windgust") var windgust: Double? = null,
@SerializedName("windspeed") var windspeed: Double? = null,
@SerializedName("winddir") var winddir: Double? = null,
@SerializedName("pressure") var pressure: Double? = null,
@SerializedName("visibility") var visibility: Double? = null,
@SerializedName("cloudcover") var cloudcover: Double? = null,
@SerializedName("solarradiation") var solarradiation: Double? = null,
@SerializedName("solarenergy") var solarenergy: Double? = null,
@SerializedName("uvindex") var uvindex: Int? = null,
@SerializedName("severerisk") var severerisk: Int? = null,
@SerializedName("conditions") var conditions: String? = null,
@SerializedName("icon") var icon: String? = null,
@SerializedName("stations") var stations: ArrayList<String> = arrayListOf(),
@SerializedName("source") var source: String? = null
)

View File

@@ -0,0 +1,40 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.appttude.h_mal.atlas_weather.model.DataMapper
import com.appttude.h_mal.atlas_weather.model.weather.Current
import com.appttude.h_mal.atlas_weather.model.weather.DailyWeather
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.google.gson.annotations.SerializedName
import com.appttude.h_mal.atlas_weather.model.weather.Hour as FullWeatherHour
data class WeatherApiResponse(
@SerializedName("queryCost") var queryCost: Int? = null,
@SerializedName("latitude") var latitude: Double? = null,
@SerializedName("longitude") var longitude: Double? = null,
@SerializedName("resolvedAddress") var resolvedAddress: String? = null,
@SerializedName("address") var address: String? = null,
@SerializedName("timezone") var timezone: String? = null,
@SerializedName("tzoffset") var tzoffset: Int? = null,
@SerializedName("description") var description: String? = null,
@SerializedName("days") var days: ArrayList<Days> = arrayListOf(),
@SerializedName("alerts") var alerts: ArrayList<Alerts> = arrayListOf(),
@SerializedName("currentConditions") var currentConditions: CurrentConditions? = CurrentConditions()
): DataMapper<FullWeather> {
override fun mapData(): FullWeather {
val hours = mutableListOf(days[0].hours).apply { add(days[1].hours) }.flatten().subList(0,23).map { FullWeatherHour(it) }.toList()
val collectedDays = mutableListOf(days.subList(0,7)).flatten().map { DailyWeather(it) }.toList()
return FullWeather(
current = Current(currentConditions),
timezone = timezone,
timezoneOffset = tzoffset,
hourly = hours,
daily = collectedDays,
lat = latitude,
lon = longitude,
locationString = address,
temperatureUnit = null
)
}
}

View File

@@ -1,13 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.repository package com.appttude.h_mal.atlas_weather.data.repository
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.UnitType import com.appttude.h_mal.atlas_weather.model.types.UnitType
interface Repository { interface Repository {
suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse suspend fun getWeatherFromApi(lat: String, long: String): WeatherApiResponse
suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem)
suspend fun saveWeatherListToRoom(list: List<EntityItem>) suspend fun saveWeatherListToRoom(list: List<EntityItem>)
fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>> fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>>

View File

@@ -2,7 +2,7 @@ package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.data.network.ResponseUnwrap import com.appttude.h_mal.atlas_weather.data.network.ResponseUnwrap
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
@@ -20,8 +20,9 @@ class RepositoryImpl(
override suspend fun getWeatherFromApi( override suspend fun getWeatherFromApi(
lat: String, lat: String,
long: String long: String
): WeatherResponse { ): WeatherApiResponse {
return responseUnwrap { api.getFromApi(lat, long, units = prefs.getUnitsType().name.lowercase()) } val unit = if (prefs.getUnitsType() == UnitType.METRIC) "metric" else "us"
return responseUnwrap { api.getFromApi(location = "$lat,$long", units = unit) }
} }
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) { override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {

View File

@@ -1,7 +1,9 @@
package com.appttude.h_mal.atlas_weather.data.room package com.appttude.h_mal.atlas_weather.data.room
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
@@ -32,4 +34,7 @@ interface WeatherDao {
@Query("DELETE FROM EntityItem WHERE id = :userId") @Query("DELETE FROM EntityItem WHERE id = :userId")
fun deleteEntry(userId: String): Int fun deleteEntry(userId: String): Int
@VisibleForTesting
@Query("DELETE FROM EntityItem")
fun deleteAll(): Int
} }

View File

@@ -11,14 +11,13 @@ import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetError import com.appttude.h_mal.atlas_weather.model.widget.WidgetError
import com.appttude.h_mal.atlas_weather.model.widget.WidgetState import com.appttude.h_mal.atlas_weather.model.widget.WidgetState
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.utils.getSymbol
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.Target import com.squareup.picasso.Target
@@ -45,9 +44,11 @@ class ServicesHelper(
// Get weather from api // Get weather from api
val weather = repository val weather = repository
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString()) .getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
val lat = weather.latitude ?: latLong.first
val long = weather.longitude ?: latLong.second
val currentLocation = val currentLocation =
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon) locationProvider.getLocationNameFromLatLong(lat, long)
val fullWeather = FullWeather(weather).apply { val fullWeather = weather.mapData().apply {
temperatureUnit = "°C" temperatureUnit = "°C"
locationString = currentLocation locationString = currentLocation
} }
@@ -105,8 +106,11 @@ class ServicesHelper(
return WidgetState.HasError(error) return WidgetState.HasError(error)
} }
val lat = weather.latitude ?: latLong.first
val long = weather.longitude ?: latLong.second
val currentLocation = try { val currentLocation = try {
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon) locationProvider.getLocationNameFromLatLong(lat, long)
} catch (e: IOException) { } catch (e: IOException) {
val data = getWidgetWeatherCollection() val data = getWidgetWeatherCollection()
data?.let { data?.let {
@@ -120,8 +124,8 @@ class ServicesHelper(
return WidgetState.HasError(error) return WidgetState.HasError(error)
} }
val fullWeather = FullWeather(weather).apply { val fullWeather = weather.mapData().apply {
temperatureUnit = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F" temperatureUnit = repository.getUnitType().getSymbol()
locationString = currentLocation locationString = currentLocation
} }
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather) val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
@@ -140,7 +144,7 @@ class ServicesHelper(
result.weather.let { result.weather.let {
val bitmap = it.current?.icon val bitmap = it.current?.icon
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon) val location = locationProvider.getLocationNameFromLatLong(it.lat!!, it.lon!!)
val temp = it.current?.temp?.toInt().toString() val temp = it.current?.temp?.toInt().toString()
WidgetData(location, bitmap, temp, epoc) WidgetData(location, bitmap, temp, epoc)
@@ -177,7 +181,7 @@ class ServicesHelper(
val widgetData = result.weather.let { val widgetData = result.weather.let {
val bitmap = it.current?.icon val bitmap = it.current?.icon
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon) val location = locationProvider.getLocationNameFromLatLong(it.lat!!, it.lon!!)
val temp = it.current?.temp?.toInt().toString() val temp = it.current?.temp?.toInt().toString()
val epoc = System.currentTimeMillis() val epoc = System.currentTimeMillis()
@@ -186,7 +190,7 @@ class ServicesHelper(
val list = mutableListOf<InnerWidgetCellData>() val list = mutableListOf<InnerWidgetCellData>()
result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather -> result.weather.daily?.drop(1)?.dropLast(1)?.forEach { dailyWeather ->
val day = dailyWeather.dt?.toSmallDayName() val day = dailyWeather.dt?.toSmallDayName()
val icon = dailyWeather.icon val icon = dailyWeather.icon
val temp = dailyWeather.max?.toInt().toString() val temp = dailyWeather.max?.toInt().toString()
@@ -216,7 +220,7 @@ class ServicesHelper(
val list = mutableListOf<InnerWidgetCellData>() val list = mutableListOf<InnerWidgetCellData>()
result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather -> result.weather.daily?.drop(1)?.dropLast(1)?.forEach { dailyWeather ->
val day = dailyWeather.dt?.toSmallDayName() val day = dailyWeather.dt?.toSmallDayName()
val icon = dailyWeather.icon val icon = dailyWeather.icon
val temp = dailyWeather.max?.toInt().toString() val temp = dailyWeather.max?.toInt().toString()

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.atlas_weather.model
interface DataMapper <T: Any> {
fun mapData(): T
}

View File

@@ -0,0 +1,28 @@
package com.appttude.h_mal.atlas_weather.model
enum class IconMapper(val label: String) {
snow("13d"),
snow_showers_day("13d"),
snow_showers_night("13n"),
thunder_rain("11d"),
thunder_showers_day("11d"),
thunder_showers_night("11n"),
rain("10d"),
showers_day("10d"),
showers_night("10n"),
fog("50d"),
wind("50d"),
cloudy("04d"),
partly_cloudy_day("03d"),
partly_cloudy_night("03n"),
clear_day("01d"),
clear_night("01n");
companion object{
fun findIconCode(iconId: String?): String? {
val label = iconId?.replace("-", "_")
val enumName = IconMapper.entries.find { it.name == label }
return enumName?.label
}
}
}

View File

@@ -40,8 +40,7 @@ data class Forecast(
parcel.readString(), parcel.readString(),
parcel.readString(), parcel.readString(),
parcel.readString() parcel.readString()
) { )
}
constructor(dailyWeather: DailyWeather) : this( constructor(dailyWeather: DailyWeather) : this(
dailyWeather.dt?.toDayString(), dailyWeather.dt?.toDayString(),

View File

@@ -10,7 +10,7 @@ data class WeatherDisplay(
val averageTemp: Double?, val averageTemp: Double?,
var unit: String?, var unit: String?,
var location: String?, var location: String?,
val iconURL: String?, var iconURL: String?,
val description: String?, val description: String?,
val hourly: List<Hour>?, val hourly: List<Hour>?,
val forecast: List<Forecast>?, val forecast: List<Forecast>?,
@@ -40,8 +40,7 @@ data class WeatherDisplay(
parcel.readDouble(), parcel.readDouble(),
parcel.readDouble(), parcel.readDouble(),
parcel.readString() parcel.readString()
) { )
}
constructor(entity: EntityItem) : this( constructor(entity: EntityItem) : this(
entity.weather.current?.temp, entity.weather.current?.temp,
@@ -56,8 +55,8 @@ data class WeatherDisplay(
entity.weather.daily?.get(0)?.pop?.times(100)?.toInt()?.toString(), entity.weather.daily?.get(0)?.pop?.times(100)?.toInt()?.toString(),
entity.weather.current?.humidity?.toString(), entity.weather.current?.humidity?.toString(),
entity.weather.current?.clouds?.toString(), entity.weather.current?.clouds?.toString(),
entity.weather.lat, entity.weather.lat!!,
entity.weather.lon, entity.weather.lon!!,
entity.weather.locationString entity.weather.locationString
) )

View File

@@ -8,11 +8,15 @@ enum class UnitType {
companion object { companion object {
fun getByName(name: String?): UnitType? { fun getByName(name: String?): UnitType? {
return values().firstOrNull { return entries.firstOrNull {
it.name.lowercase(Locale.ROOT) == name?.lowercase( it.name.lowercase(Locale.ROOT) == name?.lowercase(
Locale.ROOT Locale.ROOT
) )
} }
} }
fun UnitType.getLabel() = name.lowercase().replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
} }
} }

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current import com.appttude.h_mal.atlas_weather.data.network.response.weather.CurrentConditions
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
data class Current( data class Current(
@@ -23,23 +24,23 @@ data class Current(
val windSpeed: Double? = null val windSpeed: Double? = null
) { ) {
constructor(dailyItem: Current) : this( constructor(currentConditions: CurrentConditions?) : this(
dailyItem.dt, dt = currentConditions?.datetimeEpoch,
dailyItem.sunrise, sunrise = currentConditions?.sunriseEpoch,
dailyItem.sunset, sunset = currentConditions?.sunsetEpoch,
dailyItem.temp, temp = currentConditions?.temp,
dailyItem.visibility, visibility = currentConditions?.visibility?.toInt(),
dailyItem.uvi, uvi = currentConditions?.uvindex?.toDouble(),
dailyItem.pressure, pressure = currentConditions?.pressure?.toInt(),
dailyItem.clouds, clouds = currentConditions?.cloudcover?.toInt(),
dailyItem.feelsLike, feelsLike = currentConditions?.feelslike,
dailyItem.windDeg, windDeg = currentConditions?.winddir?.toInt(),
dailyItem.dewPoint, dewPoint = currentConditions?.dew,
generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon), icon = generateIconUrlString(IconMapper.findIconCode(currentConditions?.icon)),
dailyItem.weather?.get(0)?.description, description = currentConditions?.conditions,
dailyItem.weather?.get(0)?.main, main = currentConditions?.conditions,
dailyItem.weather?.get(0)?.id, id = currentConditions?.datetimeEpoch,
dailyItem.humidity, humidity = currentConditions?.humidity?.toInt(),
dailyItem.windSpeed windSpeed = currentConditions?.windspeed
) )
} }

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem import com.appttude.h_mal.atlas_weather.data.network.response.weather.Days
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
@@ -27,28 +28,29 @@ data class DailyWeather(
val rain: Double? val rain: Double?
) { ) {
constructor(dailyItem: DailyItem) : this( constructor(days: Days) : this(
dailyItem.dt, days.datetimeEpoch,
dailyItem.sunrise, days.sunriseEpoch,
dailyItem.sunset, days.sunsetEpoch,
dailyItem.temp?.min, days.tempmin,
dailyItem.temp?.max, days.tempmax,
dailyItem.temp?.day, days.temp,
dailyItem.feelsLike?.day, days.feelslike,
dailyItem.pressure, days.pressure?.toInt(),
dailyItem.humidity, days.humidity?.toInt(),
dailyItem.dewPoint, days.dew,
dailyItem.windSpeed, days.windspeed,
dailyItem.windDeg, days.winddir?.toInt(),
generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon), generateIconUrlString(
dailyItem.weather?.get(0)?.description, IconMapper.findIconCode(days.icon)
dailyItem.weather?.get(0)?.main, ),
dailyItem.weather?.get(0)?.id, days.description,
dailyItem.clouds, days.conditions,
dailyItem.pop, days.datetimeEpoch,
dailyItem.uvi, days.cloudcover?.toInt(),
dailyItem.rain days.precipprob,
days.uvindex?.toDouble(),
days.precip?.toDouble()
) )
} }

View File

@@ -1,29 +1,15 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
data class FullWeather( data class FullWeather(
val current: Current? = null, val current: Current? = null,
val timezone: String? = null, val timezone: String? = null,
val timezoneOffset: Int? = null, val timezoneOffset: Int? = null,
val hourly: List<Hour>? = null, val hourly: List<Hour>? = null,
val daily: List<DailyWeather>? = null, val daily: List<DailyWeather>? = null,
val lon: Double = 0.00, val lon: Double? = null,
val lat: Double = 0.00, val lat: Double? = null,
var locationString: String? = null, var locationString: String? = null,
var temperatureUnit: String? = null var temperatureUnit: String? = null
) { )
constructor(weatherResponse: WeatherResponse) : this(
weatherResponse.current?.let { Current(it) },
weatherResponse.timezone,
weatherResponse.timezoneOffset,
weatherResponse.hourly?.subList(0, 23)?.map { Hour(it) },
weatherResponse.daily?.map { DailyWeather(it) },
weatherResponse.lon,
weatherResponse.lat
)
}

View File

@@ -2,8 +2,9 @@ package com.appttude.h_mal.atlas_weather.model.weather
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour import com.appttude.h_mal.atlas_weather.data.network.response.weather.Hours as WeatherHour
data class Hour( data class Hour(
@@ -16,13 +17,13 @@ data class Hour(
parcel.readValue(Int::class.java.classLoader) as? Int, parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Double::class.java.classLoader) as? Double, parcel.readValue(Double::class.java.classLoader) as? Double,
parcel.readString() parcel.readString()
) { )
}
constructor(hour: ForecastHour) : this(
hour.dt, constructor(weatherHour: WeatherHour) : this(
hour.temp, weatherHour.datetimeEpoch,
generateIconUrlString(hour.weather?.getOrNull(0)?.icon) weatherHour.temp,
generateIconUrlString(IconMapper.findIconCode(weatherHour.icon))
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {

View File

@@ -10,7 +10,7 @@ import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.base.BaseActivity import com.appttude.h_mal.atlas_weather.base.BaseActivity
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.main.activity_main_navigation.toolbar
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
@@ -21,7 +21,7 @@ class MainActivity : BaseActivity() {
setContentView(R.layout.activity_main_navigation) setContentView(R.layout.activity_main_navigation)
val navView: BottomNavigationView = findViewById(R.id.nav_view) val navView: BottomNavigationView = findViewById(R.id.nav_view)
setSupportActionBar(toolbar) setSupportActionBar(findViewById(R.id.toolbar))
navHost = supportFragmentManager navHost = supportFragmentManager
.findFragmentById(R.id.container) as NavHostFragment .findFragmentById(R.id.container) as NavHostFragment

View File

@@ -1,5 +1,7 @@
package com.appttude.h_mal.atlas_weather.utils package com.appttude.h_mal.atlas_weather.utils
import com.appttude.h_mal.atlas_weather.model.types.UnitType
fun generateIconUrlString(icon: String?): String? { fun generateIconUrlString(icon: String?): String? {
return icon?.let { return icon?.let {
@@ -9,4 +11,6 @@ fun generateIconUrlString(icon: String?): String? {
.append("@2x.png") .append("@2x.png")
.toString() .toString()
} }
} }
fun UnitType.getSymbol(): String = if (this == UnitType.METRIC) "°C" else "°F"

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.atlas_weather.utils
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -11,7 +12,12 @@ import android.view.inputmethod.InputMethodManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AnimRes import androidx.annotation.AnimRes
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.recyclerview.widget.RecyclerView
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
@@ -55,4 +61,63 @@ fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) {
override fun onAnimationRepeat(a: Animation?) {} override fun onAnimationRepeat(a: Animation?) {}
}) })
startAnimation(animation) startAnimation(animation)
}
class BindViewDelegate<T>(
private val createView: () -> T,
private val getLifecycle: () -> Lifecycle
) : Lazy<T>, LifecycleObserver {
private var view: T? = null
private val lifecycle: Lifecycle?
get() = try {
getLifecycle()
} catch (e: IllegalStateException) {
e.message?.let { Log.e("BindViewDelegate", it) }
null
}
override val value: T
get() {
if (view == null) {
lifecycle?.removeObserver(this)
view = createView()
lifecycle?.addObserver(this)
}
@Suppress("UNCHECKED_CAST")
return view as T
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
reset()
}
private fun reset() {
lifecycle?.removeObserver(this)
view = null
}
override fun isInitialized(): Boolean = view != null
}
fun <T : View> Fragment.bindView(@IdRes resource: Int): Lazy<T> = BindViewDelegate(
createView = { requireView().findViewById<T>(resource) },
getLifecycle = { viewLifecycleOwner.lifecycle }
)
fun <T : View> Activity.bindView(@IdRes res: Int): Lazy<T> {
@Suppress("UNCHECKED_CAST")
return lazy(LazyThreadSafetyMode.NONE) { findViewById<T>(res) }
}
fun <T : View> View.bindView(@IdRes res: Int): Lazy<T> {
@Suppress("UNCHECKED_CAST")
return lazy(LazyThreadSafetyMode.NONE) { findViewById<T>(res) }
}
fun <T : View> RecyclerView.ViewHolder.bindView(@IdRes res: Int): Lazy<T> {
@Suppress("UNCHECKED_CAST")
return lazy(LazyThreadSafetyMode.NONE) { itemView.findViewById<T>(res) }
} }

View File

@@ -1,16 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_gravity="center"
android:layout_centerVertical="true"
android:layout_margin="24dp" android:layout_margin="24dp"
android:orientation="vertical"> android:orientation="vertical">
@@ -33,8 +30,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:text="@string/submit" android:text="@string/submit"
android:textColor="#ffffff" android:textColor="@color/colour_one"
android:textStyle="bold" /> android:textStyle="bold" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </FrameLayout>

View File

@@ -54,7 +54,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="3" android:layout_weight="3"
tools:text="85%" /> tools:text="33" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -73,7 +73,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="3" android:layout_weight="3"
tools:ignore="InOrMmUsage" tools:ignore="InOrMmUsage"
tools:text="11mm" /> tools:text="30" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -92,7 +92,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="3" android:layout_weight="3"
tools:ignore="InOrMmUsage" tools:ignore="InOrMmUsage"
tools:text="11mm" /> tools:text="27" />
</LinearLayout> </LinearLayout>

View File

@@ -7,7 +7,7 @@
android:paddingTop="6dp" android:paddingTop="6dp"
android:paddingRight="24dp" android:paddingRight="24dp"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:background="@color/colorPrimaryDark" android:background="@color/weather_cell_colour"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView

View File

@@ -14,7 +14,6 @@
android:id="@+id/forecast_listview" android:id="@+id/forecast_listview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:backgroundTint="@color/colorPrimaryDark"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/db_list_item" /> tools:listitem="@layout/db_list_item" />

View File

@@ -34,6 +34,8 @@
<string name="unit_key">Units</string> <string name="unit_key">Units</string>
<string name="widget_black_background">widget_black_background</string> <string name="widget_black_background">widget_black_background</string>
<string name="weather_units">Weather units</string> <string name="weather_units">Weather units</string>
<string name="channel_name">channel name</string>
<string name="channel_description">channel description</string>
<string-array name="units"> <string-array name="units">
<item>Metric</item> <item>Metric</item>

View File

@@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<application <application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.viewmodel package com.appttude.h_mal.atlas_weather.application
import android.app.Application import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -6,6 +6,9 @@ import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.atlas_weather.data.WeatherSource import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import com.appttude.h_mal.atlas_weather.viewmodel.SettingsViewModel
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
class ApplicationViewModelFactory( class ApplicationViewModelFactory(

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.atlas_weather.application
import android.app.Application
import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
fun getFlavourModule(application: Application) = FlavourModule(application).build()
class FlavourModule(val application: Application) {
fun build() = Kodein.Module("Flavour") {
bind() from provider {
ApplicationViewModelFactory(
application,
instance(),
instance(),
instance(),
)
}
}
}

Some files were not shown because too many files have changed in this diff Show More