Readme and screenshot (#35)

- 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
 - readme.md added
 - Snapshot tests added for readme.md
 - UI corrections during snapshots
This commit is contained in:
2024-07-02 19:36:36 +01:00
committed by GitHub
parent 9df83eb083
commit 3bb2ce70fa
85 changed files with 1402 additions and 746 deletions

View File

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

View File

@@ -30,6 +30,7 @@ import org.junit.Before
import org.junit.Rule
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
import tools.fastlane.screengrab.locale.LocaleTestRule
@Suppress("EmptyMethod")
open class BaseTest<A : Activity>(
@@ -47,9 +48,15 @@ open class BaseTest<A : Activity>(
@get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@get:Rule
var writePermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@get:Rule
var snapshotRule: SnapshotRule = SnapshotRule()
@Rule @JvmField
val localeTestRule = LocaleTestRule()
@Before
fun setUp() {
Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())

View File

@@ -31,7 +31,7 @@ open class BaseTestRobot {
fun goBack() = Espresso.pressBack()
fun fillEditText(resId: Int, text: String?): ViewInteraction =
fun fillEditText(resId: Int, text: String): ViewInteraction =
onView(withId(resId)).perform(
ViewActions.replaceText(text),
ViewActions.closeSoftKeyboard()
@@ -129,6 +129,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 =
onView(withId(resId)).check(matches(checkErrorMessage(errorMessage)))

View File

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

View File

@@ -0,0 +1,67 @@
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 java.io.BufferedReader
class TestAppClass : AtlasApp() {
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) {
val iStream =
InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath.json")
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)
}
}

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,55 @@
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
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 = when (unitType) {
UnitType.METRIC -> "Metric"
UnitType.IMPERIAL -> "Imperial"
}
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,50 @@
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 org.junit.Test
import tools.fastlane.screengrab.Screengrab
@SmallTest
@TargetApi(27)
class SnapshotCaptureTest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubLocation("London", 51.51, -0.13)
clearPrefs()
}
@Test
fun homeAndFurtherInfoPageCapture() {
weatherScreen {
isDisplayed()
Screengrab.screenshot("HomeScreen")
tapDayInformationByPosition(4)
}
furtherInfoScreen {
isDisplayed()
Screengrab.screenshot("FurtherInfoScreen")
}
}
@Test
fun settingsPageCapture() {
weatherScreen {
isDisplayed()
openMenuItem()
}
settingsScreen {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Imperial)
Screengrab.screenshot("SettingsScreen")
}
}
}

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 java.io.BufferedReader
class TestAppClass : BaseAppClass() {
class TestAppClass : MonoApp() {
private val idlingResources = CountingIdlingResource("Data_loader")
private val mockingNetworkInterceptor = MockingNetworkInterceptor(idlingResources)
lateinit var database: AppDatabase
lateinit var locationProvider: MockLocationProvider
private val locationProvider: MockLocationProvider = MockLocationProvider()
override fun onCreate() {
super.onCreate()
@@ -38,12 +38,12 @@ class TestAppClass : BaseAppClass() {
}
override fun createLocationModule(): LocationProvider {
locationProvider = MockLocationProvider()
return locationProvider
}
override fun createRoomDatabase(): AppDatabase {
database = Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java)
database = Room.inMemoryDatabaseBuilder(applicationContext, AppDatabase::class.java)
.allowMainThreadQueries()
.addTypeConverter(Converter(this))
.build()
return database

View File

@@ -0,0 +1,51 @@
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.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("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubLocation("London", 51.51, -0.13)
clearPrefs()
}
@Test
fun homeAndFurtherInfoPageCapture() {
weatherScreen {
isDisplayed()
Screengrab.screenshot("HomeScreen")
tapDayInformationByPosition(4)
}
furtherInfoScreen {
isDisplayed()
Screengrab.screenshot("FurtherInfoScreen")
}
}
@Test
fun settingsPageCapture() {
weatherScreen {
isDisplayed()
openMenuItem()
}
settingsScreen {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", 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,6 +11,7 @@ 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
@@ -45,4 +46,10 @@ class SettingsScreen : BaseTestRobot() {
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

@@ -2,6 +2,8 @@ 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 weatherScreen(func: WeatherScreen.() -> Unit) = WeatherScreen().apply { func() }
class WeatherScreen : BaseTestRobot() {
@@ -16,4 +18,8 @@ class WeatherScreen : BaseTestRobot() {
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

@@ -5,6 +5,7 @@ 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.monoWeather.robot.weatherScreen
import org.junit.Ignore
import org.junit.Test
class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) {
@@ -21,6 +22,7 @@ class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) {
}
}
@Ignore("Test is flakey - must investigate")
@Test
fun invalidKeyWeatherResponse_swipeToRefresh_returnsValidPage() {
weatherScreen {

View File

@@ -5,9 +5,11 @@ 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.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
class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
@@ -25,6 +27,22 @@ class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
}
}
@Test
fun loadApp_validWeatherResponse_viewFurtherDetailsPage() {
weatherScreen {
isDisplayed()
verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location")
tapDayInformationByPosition(4)
}
furtherInfoScreen {
isDisplayed()
verifyMaxTemperature(12)
verifyAverageTemperature(9)
}
}
@Test
fun loadApp_changeToImperial_returnsValidPage() {
weatherScreen {

View File

@@ -2,8 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:name="com.appttude.h_mal.atlas_weather.application.AtlasApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -27,23 +29,8 @@
</activity>
<receiver
android:name=".notification.NotificationReceiver"
android:exported="true"
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>
android:name=".service.notification.NotificationReceiver"
android:exported="false"/>
</application>
</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,46 @@
package com.appttude.h_mal.atlas_weather.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.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
open class AtlasApp : AppClass() {
private lateinit var notificationService: NotificationService
override val flavourModule = super.flavourModule.copy {
bind() from singleton {
NotificationHelper(
instance(),
instance(),
)
}
bind() from singleton {
NotificationService(this@AtlasApp).apply { notificationService = this }
}
bind() from provider {
ApplicationViewModelFactory(
this@AtlasApp,
instance(),
instance(),
instance(),
instance()
)
}
}
// override fun onCreate() {
// super.onCreate()
// notificationService.schedulePushNotifications()
// }
fun scheduleNotifications() = notificationService.schedulePushNotifications()
fun unscheduleNotifications() = notificationService.unschedulePushNotifications()
}

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

View File

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

View File

@@ -1,7 +1,9 @@
package com.appttude.h_mal.atlas_weather.ui.home
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.POST_NOTIFICATIONS
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
@@ -10,9 +12,10 @@ import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation.findNavController
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.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.application.AtlasApp
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.WeatherDisplay
@@ -21,7 +24,7 @@ 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.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import kotlinx.android.synthetic.main.fragment_home.*
import permissions.dispatcher.NeedsPermission
import permissions.dispatcher.OnNeverAskAgain
import permissions.dispatcher.OnPermissionDenied
@@ -37,14 +40,15 @@ import permissions.dispatcher.RuntimePermissions
@RuntimePermissions
class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
lateinit var recyclerAdapter: WeatherRecyclerAdapter
private lateinit var recyclerAdapter: WeatherRecyclerAdapter
private lateinit var swipeRefresh: SwipeRefreshLayout
@SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
swipe_refresh.apply {
swipeRefresh = view.findViewById<SwipeRefreshLayout>(R.id.swipe_refresh).apply {
setOnRefreshListener {
showLocationWithPermissionCheck()
isRefreshing = true
@@ -55,7 +59,9 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
navigateToFurtherDetails(it)
})
forecast_listview.adapter = recyclerAdapter
view.findViewById<RecyclerView>(R.id.forecast_listview).adapter = recyclerAdapter
scheduleNotification()
}
@SuppressLint("MissingPermission")
@@ -66,7 +72,7 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
override fun onSuccess(data: Any?) {
super.onSuccess(data)
swipe_refresh.isRefreshing = false
swipeRefresh.isRefreshing = false
if (data is WeatherDisplay) {
recyclerAdapter.addCurrent(data)
@@ -75,7 +81,7 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
override fun onFailure(error: Any?) {
super.onFailure(error)
swipe_refresh.isRefreshing = false
swipeRefresh.isRefreshing = false
}
private fun navigateToFurtherDetails(forecast: Forecast) {
@@ -93,12 +99,21 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
}
@Deprecated("Deprecated in Java")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
fun scheduleNotification() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
sendNotification()
} else {
(requireActivity().application as AtlasApp).scheduleNotifications()
}
}
@SuppressLint("MissingPermission")
@NeedsPermission(ACCESS_COARSE_LOCATION)
fun showLocation() {
@@ -123,4 +138,29 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
fun onLocationNeverAskAgain() {
displayToast("Location permissions have been to never ask again")
}
@SuppressLint("MissingPermission")
@NeedsPermission(POST_NOTIFICATIONS)
fun sendNotification() {
(requireActivity().application as AtlasApp).scheduleNotifications()
}
@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
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.notification.NotificationReceiver
import com.appttude.h_mal.atlas_weather.widget.NewAppWidget
import java.util.Calendar
import com.appttude.h_mal.atlas_weather.base.BasePreferencesFragment
import com.appttude.h_mal.atlas_weather.utils.displayToast
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?) {
setPreferencesFromResource(R.xml.prefs, rootKey)
override fun preferenceChanged(key: String) {
when (key) {
//listener on changed sort order preference:
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
prefs.registerOnSharedPreferenceChangeListener { _, key ->
if (key == "temp_units") {
val intent = Intent(requireContext(), NewAppWidget::class.java)
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)
"temp_units" -> viewModel.refreshWeatherData()
"notif_boolean" -> {
// TODO: update notification
// viewModel.updateWidget()
// displayToast("Widget background has been updates")
}
}
}
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
)
override fun onSuccess(data: Any?) {
super.onSuccess(data)
if (data is String) displayToast(data)
}
}
}
// 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.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.base.BaseFragment
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.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) {
@@ -16,8 +16,11 @@ class AddLocationFragment : BaseFragment<WorldViewModel>(R.layout.activity_add_f
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val submit = view.findViewById<Button>(R.id.submit)
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()) {
submit.error = "Location cannot be blank"
return@setOnClickListener

View File

@@ -4,13 +4,13 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
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.base.BaseFragment
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.viewmodel.WorldViewModel
import kotlinx.android.synthetic.atlasWeather.fragment_add_location.floatingActionButton
import kotlinx.android.synthetic.atlasWeather.fragment_add_location.world_recycler
import com.google.android.material.floatingactionbutton.FloatingActionButton
/**
@@ -29,19 +29,21 @@ class WorldFragment : BaseFragment<WorldViewModel>(R.layout.fragment_add_locatio
navigateTo(direction)
}
world_recycler.apply {
view.findViewById<RecyclerView>(R.id.world_recycler).apply {
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
}
floatingActionButton.setOnClickListener {
view.findViewById<FloatingActionButton>(R.id.floatingActionButton).setOnClickListener {
navigateTo(R.id.action_worldFragment_to_addLocationFragment)
}
}
@Suppress("UNCHECKED_CAST")
override fun onSuccess(data: Any?) {
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() {

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
android:id="@+id/settings_fragment"
android:name="com.appttude.h_mal.atlas_weather.ui.settings.SettingsFragment"
android:label="SettingsFragment" />
android:label="Settings" />
</navigation>

View File

@@ -2,11 +2,13 @@
<resources>
<color name="colorPrimary">#3F51B5</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_two">#5F8E7B</color>
<color name="colour_three">#B3C0CA</color>
<color name="colour_four">#8C98AD</color>
<color name="colour_five">#2E3532</color>
<color name="weather_cell_colour">@android:color/transparent</color>
</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"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- <PreferenceCategory android:title="Units">-->
<!-- <ListPreference-->
<!-- android:defaultValue="°C"-->
<!-- android:entries="@array/list_preference_temp"-->
<!-- android:entryValues="@array/list_preference_temp"-->
<!-- 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>-->
<ListPreference
android:title="@string/weather_units"
android:entries="@array/units"
android:entryValues="@array/units"
android:defaultValue="Metric"
android:key="UnitType"
/>
<PreferenceCategory android:title="Notification Settings">
<SwitchPreference
android:defaultValue="true"
android:key="notif_boolean"
android:title="Notification" />
</PreferenceCategory>
<PreferenceCategory android:title="Widget Settings">
<SwitchPreference
android:defaultValue="false"
android:key="widget_black_background"
android:title="Set widget background black" />
android:title="Notifications enabled" />
</PreferenceCategory>
</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"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.appttude.h_mal.atlas_weather">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<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="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

View File

@@ -9,20 +9,23 @@ 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.room.AppDatabase
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.google.gson.Gson
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
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 {
import(parentModule)
import(flavourModule)
}
val parentModule = Kodein.Module("Parent Module", allowSilentOverride = true) {
import(androidXModule(this@BaseAppClass))
bind() from singleton { createNetworkModule() }
@@ -35,7 +38,10 @@ abstract class BaseAppClass : Application(), KodeinAware {
bind() from singleton { SettingsRepositoryImpl(instance()) }
bind() from singleton { ServicesHelper(instance(), instance(), instance()) }
bind() from singleton { WeatherSource(instance(), instance()) }
bind() from provider { ApplicationViewModelFactory(this@BaseAppClass, instance(), instance(),instance()) }
}
open val flavourModule = Kodein.Module("Flavour") {
import(parentModule)
}
abstract fun createNetworkModule(): WeatherApi

View File

@@ -1,5 +1,6 @@
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.network.NetworkModule
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.room.AppDatabase
const val LOCATION_PERMISSION_REQUEST = 505
class AppClass : BaseAppClass() {
open class AppClass : BaseAppClass() {
override fun createNetworkModule(): WeatherApi {
return NetworkModule().invoke<WeatherApi>(
@@ -20,7 +19,7 @@ class AppClass : BaseAppClass() {
) as WeatherApi
}
override fun createLocationModule() = LocationProviderImpl(this)
override fun createLocationModule(): LocationProvider = LocationProviderImpl(this)
override fun createRoomDatabase(): AppDatabase = AppDatabase(this)

View File

@@ -77,6 +77,7 @@ abstract class BaseActivity : AppCompatActivity(), KodeinAware {
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
loadingView?.hide()
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.helper.GenericsHelper.getGenericClassAt
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.android.x.kodein
import org.kodein.di.generic.instance

View File

@@ -6,11 +6,10 @@ import androidx.annotation.XmlRes
import androidx.fragment.app.createViewModelLazy
import androidx.preference.PreferenceFragmentCompat
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.helper.GenericsHelper.getGenericClassAt
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.android.x.kodein
import org.kodein.di.generic.instance
@@ -48,7 +47,7 @@ abstract class BasePreferencesFragment<V : BaseAndroidViewModel>(@XmlRes private
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
prefs.registerOnSharedPreferenceChangeListener { _, s ->
preferenceChanged(s)
s?.let { preferenceChanged(s) }
}
}

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.base.BaseActivity
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.main.activity_main_navigation.toolbar
class MainActivity : BaseActivity() {
@@ -21,7 +21,7 @@ class MainActivity : BaseActivity() {
setContentView(R.layout.activity_main_navigation)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
setSupportActionBar(toolbar)
setSupportActionBar(findViewById(R.id.toolbar))
navHost = supportFragmentManager
.findFragmentById(R.id.container) as NavHostFragment

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.atlas_weather.utils
import android.app.Activity
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -11,7 +12,12 @@ import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.Toast
import androidx.annotation.AnimRes
import androidx.annotation.IdRes
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.squareup.picasso.Picasso
@@ -55,4 +61,63 @@ fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) {
override fun onAnimationRepeat(a: 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"?>
<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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:layout_margin="24dp"
android:orientation="vertical">
@@ -33,8 +30,8 @@
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/submit"
android:textColor="#ffffff"
android:textColor="@color/colour_one"
android:textStyle="bold" />
</LinearLayout>
</RelativeLayout>
</FrameLayout>

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,8 @@
<string name="unit_key">Units</string>
<string name="widget_black_background">widget_black_background</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">
<item>Metric</item>

View File

@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
<application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:name="com.appttude.h_mal.atlas_weather.application.MonoApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
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 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.location.LocationProvider
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(

View File

@@ -0,0 +1,19 @@
package com.appttude.h_mal.atlas_weather.application
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
open class MonoApp : AppClass() {
override val flavourModule = super.flavourModule.copy {
bind() from provider {
ApplicationViewModelFactory(
this@MonoApp,
instance(),
instance(),
instance(),
)
}
}
}

View File

@@ -4,14 +4,16 @@ package com.appttude.h_mal.monoWeather.ui
import android.os.Bundle
import android.view.View
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.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.base.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import com.appttude.h_mal.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
import kotlinx.android.synthetic.main.fragment_home.forecast_listview
import kotlinx.android.synthetic.main.fragment_home.swipe_refresh
class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
@@ -19,6 +21,7 @@ class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
private var retrievedLocationName: String? = null
private lateinit var recyclerAdapter: WeatherRecyclerAdapter
private lateinit var swipeRefresh: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -35,12 +38,12 @@ class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
navigateTo(directions)
}
forecast_listview.apply {
view.findViewById<RecyclerView>(R.id.forecast_listview).apply {
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
}
swipe_refresh.apply {
swipeRefresh = view.findViewById<SwipeRefreshLayout>(R.id.swipe_refresh).apply {
setOnRefreshListener {
retrievedLocationName?.let {
viewModel.fetchDataForSingleLocation(it)
@@ -57,11 +60,11 @@ class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
recyclerAdapter.addCurrent(data)
}
super.onSuccess(data)
swipe_refresh.isRefreshing = false
swipeRefresh.isRefreshing = false
}
override fun onFailure(error: Any?) {
super.onFailure(error)
swipe_refresh.isRefreshing = false
swipeRefresh.isRefreshing = false
}
}

View File

@@ -5,9 +5,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import android.widget.TextView
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
import kotlinx.android.synthetic.main.activity_further_info.*
/**
@@ -34,18 +34,18 @@ class FurtherInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
maxtemp.text = param1?.mainTemp.appendWith(requireContext().getString(R.string.degrees))
averagetemp.text =
view.findViewById<TextView>(R.id.maxtemp).text = param1?.mainTemp.appendWith(requireContext().getString(R.string.degrees))
view.findViewById<TextView>(R.id.averagetemp).text =
param1?.averageTemp.appendWith(requireContext().getString(R.string.degrees))
minimumtemp.text =
view.findViewById<TextView>(R.id.minimumtemp).text =
param1?.minorTemp.appendWith(requireContext().getString(R.string.degrees))
windtext.text = param1?.windText.appendWith(" km")
preciptext.text = param1?.precipitation.appendWith(" %")
cloudtext.text = param1?.cloud.appendWith(" %")
humiditytext.text = param1?.humidity.appendWith(" %")
uvtext.text = param1?.uvi
sunrisetext.text = param1?.sunrise
sunsettext.text = param1?.sunset
view.findViewById<TextView>(R.id.windtext).text = param1?.windText.appendWith(" km")
view.findViewById<TextView>(R.id.preciptext).text = param1?.precipitation.appendWith(" %")
view.findViewById<TextView>(R.id.cloudtext).text = param1?.cloud.appendWith(" %")
view.findViewById<TextView>(R.id.humiditytext).text = param1?.humidity.appendWith(" %")
view.findViewById<TextView>(R.id.uvtext).text = param1?.uvi
view.findViewById<TextView>(R.id.sunrisetext).text = param1?.sunrise
view.findViewById<TextView>(R.id.sunsettext).text = param1?.sunset
}
fun String?.appendWith(suffix: String): String? {

View File

@@ -10,6 +10,8 @@ import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation.findNavController
import androidx.navigation.ui.onNavDestinationSelected
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.base.BaseFragment
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
@@ -19,7 +21,7 @@ import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import com.appttude.h_mal.monoWeather.dialog.PermissionsDeclarationDialog
import com.appttude.h_mal.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
import kotlinx.android.synthetic.main.fragment_home.*
import permissions.dispatcher.NeedsPermission
import permissions.dispatcher.OnNeverAskAgain
import permissions.dispatcher.OnPermissionDenied
@@ -36,13 +38,14 @@ import permissions.dispatcher.RuntimePermissions
class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
lateinit var recyclerAdapter: WeatherRecyclerAdapter
lateinit var swipeRefresh: SwipeRefreshLayout
@SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
swipe_refresh.apply {
swipeRefresh = view.findViewById<SwipeRefreshLayout>(R.id.swipe_refresh).apply {
setOnRefreshListener {
showLocationWithPermissionCheck()
isRefreshing = true
@@ -53,7 +56,7 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
navigateToFurtherDetails(it)
})
forecast_listview.adapter = recyclerAdapter
view.findViewById<RecyclerView>(R.id.forecast_listview).adapter = recyclerAdapter
}
@SuppressLint("MissingPermission")
@@ -64,7 +67,7 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
override fun onSuccess(data: Any?) {
super.onSuccess(data)
swipe_refresh.isRefreshing = false
swipeRefresh.isRefreshing = false
if (data is WeatherDisplay) {
recyclerAdapter.addCurrent(data)
@@ -73,7 +76,7 @@ class HomeFragment : BaseFragment<MainViewModel>(R.layout.fragment_home) {
override fun onFailure(error: Any?) {
super.onFailure(error)
swipe_refresh.isRefreshing = false
swipeRefresh.isRefreshing = false
}
private fun navigateToFurtherDetails(forecast: Forecast) {

View File

@@ -4,10 +4,12 @@ import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.utils.generateView
import kotlinx.android.synthetic.monoWeather.mono_item_two_cell.view.mono_item_cell
import kotlinx.android.synthetic.monoWeather.mono_item_two_cell.view.mono_text_cell
class GridAdapter(
@@ -20,8 +22,8 @@ class GridAdapter(
val item = getItem(position)
return view.apply {
mono_item_cell.setImageResource(item!!.first)
mono_text_cell.text = item.second
findViewById<ImageView>(R.id.mono_item_cell).setImageResource(item!!.first)
findViewById<TextView>(R.id.mono_text_cell).text = item.second
}
}
}

View File

@@ -11,6 +11,7 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.widget.Button
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
@@ -18,8 +19,8 @@ import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.monoWeather.dialog.DeclarationBuilder
import com.appttude.h_mal.monoWeather.dialog.PermissionsDeclarationDialog
import kotlinx.android.synthetic.monoWeather.permissions_declaration_dialog.cancel
import kotlinx.android.synthetic.monoWeather.permissions_declaration_dialog.submit
import permissions.dispatcher.NeedsPermission
import permissions.dispatcher.OnNeverAskAgain
import permissions.dispatcher.OnPermissionDenied
@@ -60,7 +61,7 @@ class WidgetLocationPermissionActivity : AppCompatActivity(), DeclarationBuilder
movementMethod = LinkMovementMethod.getInstance()
}
submit.setOnClickListener {
findViewById<Button>(R.id.submit).setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
showBackgroundLocationWithPermissionCheck()
} else {
@@ -68,7 +69,7 @@ class WidgetLocationPermissionActivity : AppCompatActivity(), DeclarationBuilder
}
}
cancel.setOnClickListener { finish() }
findViewById<Button>(R.id.cancel).setOnClickListener { finish() }
}
private fun submitWidget() {

View File

@@ -2,14 +2,16 @@ package com.appttude.h_mal.monoWeather.ui.world
import android.os.Bundle
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.base.BaseFragment
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.hideKeyboard
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) {
@@ -17,10 +19,11 @@ class AddLocationFragment : BaseFragment<WorldViewModel>(R.layout.activity_add_f
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
submit.setOnClickListener {
val locationName = location_name_tv.text?.trim()?.toString()
view.findViewById<Button>(R.id.submit).setOnClickListener {
val locationNameView = view.findViewById<TextView>(R.id.location_name_tv)
val locationName = locationNameView.text?.trim()?.toString()
if (locationName.isNullOrBlank()) {
location_name_tv.error = "Location cannot be blank"
locationNameView.error = "Location cannot be blank"
return@setOnClickListener
}
viewModel.fetchDataForSingleLocationSearch(locationName)

View File

@@ -2,17 +2,18 @@ package com.appttude.h_mal.monoWeather.ui.world
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.fragment.app.Fragment
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.model.forecast.WeatherDisplay
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.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import com.appttude.h_mal.monoWeather.ui.world.WorldFragmentDirections.actionWorldFragmentToWorldItemFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.monoWeather.fragment__two.floatingActionButton
import kotlinx.android.synthetic.monoWeather.fragment__two.world_recycler
import com.google.android.material.floatingactionbutton.FloatingActionButton
/**
@@ -47,12 +48,12 @@ class WorldFragment : BaseFragment<WorldViewModel>(R.layout.fragment__two) {
.show()
}
world_recycler.apply {
view.findViewById<RecyclerView>(R.id.world_recycler).apply {
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
}
floatingActionButton.setOnClickListener {
view.findViewById<FloatingActionButton>(R.id.floatingActionButton).setOnClickListener {
navigateTo(R.id.action_worldFragment_to_addLocationFragment)
}

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

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

View File

@@ -5,4 +5,6 @@
<color name="colour_one">#E8D0DD</color>
<color name="colour_four">#8C98AD</color>
<color name="weather_cell_colour">@android:color/transparent</color>
</resources>

View File

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