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

2
.idea/kotlinc.xml generated
View File

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

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
@@ -56,3 +62,62 @@ fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) {
})
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)

View File

@@ -1,24 +1,21 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
kotlin_version = '1.5.20'
}
repositories {
maven { url "https://www.jitpack.io" }
}
dependencies {
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.4.1")
classpath ('com.android.tools.build:gradle:7.2.2')
classpath ("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20")
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$NAVIGATION_VERSION"
classpath "com.android.tools.build:gradle:$GRADLE_PLUGIN_VERSION"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_GRADLE_PLUGIN"
}
}
plugins {
id 'com.android.application' version '7.2.2' apply false
id 'com.android.library' version '7.2.2' apply false
id 'com.google.gms.google-services' version '4.3.15' apply false
id 'androidx.navigation.safeargs.kotlin' version '2.4.0' apply false
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
id 'com.android.application' version "$GRADLE_PLUGIN_VERSION" apply false
id 'com.android.library' version "$GRADLE_PLUGIN_VERSION" apply false
id 'com.google.gms.google-services' version "$GOOGLE_SERVICES" apply false
id 'org.jetbrains.kotlin.android' version "$KOTLIN_VERSION" apply false
id 'com.autonomousapps.dependency-analysis' version "$GRADLE_ANALYZE_VERSION"
}
task clean(type: Delete) {

View File

@@ -42,4 +42,48 @@ platform :android do
json_key: "google-play-key.json",
package_name: "com.appttude.h_mal.atlas_weather")
end
desc "Capture screenshots on MonoWeather"
lane :screenGrabMonoWeather do
build_android_app(
task: 'assemble',
build_type: 'Debug',
flavor: 'MonoWeather',
)
build_android_app(
task: 'assemble',
build_type: 'AndroidTest',
flavor: 'MonoWeather',
)
screengrab(
app_package_name: "com.appttude.h_mal.monoWeather",
locales: ["en-UK"],
app_apk_path: "app/build/outputs/apk/monoWeather/debug/app-monoWeather-debug.apk",
tests_apk_path: "app/build/outputs/apk/androidTest/monoWeather/debug/app-monoWeather-debug-androidTest.apk",
test_instrumentation_runner: "com.appttude.h_mal.atlas_weather.application.TestRunner",
use_tests_in_packages: "com.appttude.h_mal.atlas_weather.snapshot"
)
end
desc "Capture screenshots on AtlasWeather"
lane :screenGrabAtlasWeather do
build_android_app(
task: 'assemble',
build_type: 'Debug',
flavor: 'AtlasWeather',
)
build_android_app(
task: 'assemble',
build_type: 'AndroidTest',
flavor: 'AtlasWeather',
)
screengrab(
app_package_name: "com.appttude.h_mal.atlas_weather",
locales: ["en-UK"],
app_apk_path: "app/build/outputs/apk/atlasWeather/debug/app-atlasWeather-debug.apk",
tests_apk_path: "app/build/outputs/apk/androidTest/atlasWeather/debug/app-atlasWeather-debug-androidTest.apk",
test_instrumentation_runner: "com.appttude.h_mal.atlas_weather.application.TestRunner",
use_tests_in_packages: "com.appttude.h_mal.atlas_weather.snapshot"
)
end
end

56
fastlane/README.md Normal file
View File

@@ -0,0 +1,56 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android deployMonoWeather
```sh
[bundle exec] fastlane android deployMonoWeather
```
Deploy a new Mono Weather version to the Google Play
### android deployAtlasWeather
```sh
[bundle exec] fastlane android deployAtlasWeather
```
Deploy a new Atlas Weather version to the Google Play
### android screenGrabMonoWeather
```sh
[bundle exec] fastlane android screenGrabMonoWeather
```
Capture screenshots on MonoWeather
### android screenGrabAtlasWeather
```sh
[bundle exec] fastlane android screenGrabAtlasWeather
```
Capture screenshots on AtlasWeather
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@@ -9,11 +9,67 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Plugin versions
ANDROID_CORE = 1.13.1
CUSTOM_VIEW = 1.1.0
CARD_VIEW = 1.0.0
FRAGMENT_VERSION = 1.8.0
MATERIAL_VERSION = 1.12.0
APP_COMPAT = 1.7.0
CONSTR_LAYOUT_VERSION = 2.1.4
ANDROID_LIFECYCLE = 2.8.2
RECYCLER_VIEW = 1.3.2
SWIPE_REFRESH = 1.1.0
PERMISSIONS_DISPATCHER = 4.9.2
TOMTOM_SEARCH = 2.4771
TOMTOM_MAP = 2.4807
NAVIGATION_VERSION = 2.7.7
PREFERENCES_VERSION = 1.2.1
RETROFIT_VERSION = 2.9.0
OKHTTP_VERSION = 4.9.0
MOKITO_INLINE_VERSION = 2.13.0
CORE_TEST_VERSION = 2.2.0
MOCKK_VERSION = 1.10.5
TEST_JUNIT_VERSION = 1.2.0
TEST_RUNNER_VERSION = 1.5.2
ESPRESSO_VERSION = 3.6.0
HAMCREST_VERSION = 2.2
JUNIT_VERSION = 4.13.2
KODEIN_VERSION = 6.2.1
ROOM_VERSION = 2.6.1
KOTLINX_COROUTINES = 1.6.1
TEST_KTX_VERSION = 1.6.0
ANDROIDX_TEST = 1.6.0
TEST_MONITOR = 1.7.0
GOOGLE_PLAY_SERVICE = 21.3.0
GOOGLE_SERVICES = 4.3.15
GSON = 2.10.1
GUAVA = 33.2.1-android
ANDROID_LIBRARY = 8.5.0
ANDROID_APPLICATION = 8.5.0
GRADLE_PLUGIN_VERSION = 8.5.0
KOTLIN_VERSION = 2.0.0
KOTLIN_GRADLE_PLUGIN = 1.6.21
GRADLE_ANALYZE_VERSION = 1.20.0
# Android configuration
TARGET_SDK_VERSION = 34
MIN_SDK_VERSION = 26
# Gradle parameters
org.gradle.jvmargs = -Xmx1536m
# AndroidX
android.useAndroidX = true
android.enableJetifier = true

View File

@@ -1,6 +1,6 @@
#Wed Jul 26 10:14:37 BST 2023
#Tue Jun 25 17:02:21 BST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

64
readme.md Normal file
View File

@@ -0,0 +1,64 @@
# Weather-apps
Weather-apps contains two weather apps - Atlas weather and Mono weather. They are both simple and user-friendly Android applications that provides current weather information and forecasts. With a sleek design and accurate data, to keeps you updated on the latest weather conditions in your area.
## Features
- **Current Weather**: Get real-time weather updates including temperature, humidity, wind speed, and atmospheric pressure.
- **Forecast**: View detailed weather forecasts for the next 7 days.
- **Location-Based Updates**: Automatically fetch weather data based on your current location.
- **Search Functionality**: Search for weather information in different cities around the world.
- **Notifications**: Receive weather alerts and notifications for significant weather changes.
- **Customizable Settings**: Choose between Celsius and Fahrenheit, and set your preferred update frequency.
- **Customizable Home screen widget**: Add a home screen widget to give you regular updates on forecast.
## Screenshots
### Atlas Weather
![Home Screen](screenshots/atlas/home.png)
![Forecast Screen](screenshots/atlas/forecast.png)
![Settings Screen](screenshots/atlas/settings.png)
### Mono Weather
![Home Screen](screenshots/mono/home.png)
![Forecast Screen](screenshots/mono/forecast.png)
![Settings Screen](screenshots/mono/settings.png)
## Usage
1. Upon launching the app, you will be prompted to allow location access. Grant the necessary permissions.
2. The home screen will display the current weather information for your location.
3. Swipe left or tap on the forecast tab to view the 7-day weather forecast.
4. Use the search icon to look up weather information for other cities.
5. Access the settings menu to customize your preferences.
## Permissions
The app requires the following permissions:
- **Location**: To provide accurate weather information based on your current location.
- **Internet**: To fetch weather data from the server.
## API
Weather-apps
- uses the [OpenWeatherMap API](https://openweathermap.org/api) to retrieve weather data.
- uses the [TomTom Search API](https://developer.tomtom.com/search-api/documentation/product-information/introduction) to retrieve geolocation data.
## Contributing
Contributions are welcome! Please follow these steps:
1. Fork the repository.
2. Create a new branch: `git checkout -b feature/your-feature-name`
3. Make your changes and commit them: `git commit -m 'Add some feature'`
4. Push to the branch: `git push origin feature/your-feature-name`
5. Create a pull request.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgements
- [OpenWeatherMap](https://openweathermap.org) for the weather data API.
- [TomTom Search API](https://developer.tomtom.com/search-api/documentation/product-information/introduction) for the geolocation API.

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
screenshots/atlas/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
screenshots/mono/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB