Refactor flavours (#17)

- Fastlane completed
 - Circleci config completed
 - Flavours build completed
This commit is contained in:
2023-08-07 20:17:08 +01:00
committed by GitHub
parent 4a37b724a6
commit baabebd40d
121 changed files with 1326 additions and 1586 deletions

View File

@@ -14,18 +14,17 @@ commands:
description: checkout repo and android dependencies description: checkout repo and android dependencies
steps: steps:
- checkout - checkout
- restore_cache: - android/restore-gradle-cache
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} build_gradle:
- run: description: Build the gradle
name: Chmod permissions steps:
command: sudo chmod +x ./gradlew - android/restore-gradle-cache
- run: - run:
name: Download Dependencies name: Download Dependencies
command: ./gradlew androidDependencies command: |
- save_cache: sudo chmod +x ./gradlew
paths: ./gradlew androidDependencies
- ~/.gradle - android/save-gradle-cache
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
run_tests: run_tests:
description: run tests for flavour specified description: run tests for flavour specified
parameters: parameters:
@@ -34,8 +33,11 @@ commands:
default: "AtlasWeather" default: "AtlasWeather"
steps: steps:
# The next step will run the unit tests # The next step will run the unit tests
- android/run-tests: - build_gradle
test-command: ./gradlew test<< parameters.flavour >>DebugUnitTest --continue - run:
name: Run non-instrumentation unit tests
command: |
./gradlew test<< parameters.flavour >>DebugUnitTest --continue
- store_artifacts: - store_artifacts:
path: app/build/reports path: app/build/reports
destination: reports destination: reports
@@ -48,27 +50,47 @@ commands:
type: string type: string
default: "AtlasWeather" default: "AtlasWeather"
steps: steps:
- build_gradle
- android/start-emulator-and-run-tests: - android/start-emulator-and-run-tests:
post-emulator-launch-assemble-command: ./gradlew assemble<< parameters.flavour >>DebugAndroidTest post-emulator-launch-assemble-command: ./gradlew assemble<< parameters.flavour >>DebugAndroidTest
test-command: ./gradlew connected<< parameters.flavour >>DebugAndroidTest test-command: ./gradlew connected<< parameters.flavour >>DebugAndroidTest
system-image: system-images;android-25;google_apis;x86 system-image: system-images;android-26;google_apis;x86
max-tries: 1 pull-data: true
kill-emulators: false pull-data-path: /storage/emulated/0/Android/data/
- run: pull-data-target: ~/app-data
name: Pull screenshots from device restore-gradle-cache-prefix: v1a
command: |
mkdir ~/screenshots
adb pull /storage/emulated/0/Android/data/com.appttude.h_mal.atlas_weather/files/screengrab/en-US/images/screenshots ~/screenshots
when: on_fail
# store test reports # store test reports
- store_artifacts: - store_artifacts:
path: app/build/reports/androidTests/connected path: app/build/reports/androidTests/connected
destination: reports destination: reports
# store screenshots for failed ui tests # store screenshots for failed ui tests
- store_artifacts: - store_artifacts:
path: ~/screenshots path: ~/app-data
destination: screenshots destination: screenshots
- store_test_results:
path: app/build/outputs/androidTest-results/connected
deploy_to_play_store:
description: deploy to playstore based on flavour
parameters:
flavour:
type: string
default: "AtlasWeather"
steps:
# The next step will run the unit tests
- android/decode-keystore:
keystore-location: "./app/keystore.jks"
- run:
name: Setup playstore key
command: |
echo "$GOOGLE_PLAY_KEY" > "google-play-key.json"
- build_gradle
- run:
name: Run fastlane command to deploy to playstore
command: |
pwd
bundle exec fastlane deploy<< parameters.flavour >>
- store_test_results:
path: fastlane/report.xml
# Define a job to be invoked later in a workflow. # Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs # See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs: jobs:
@@ -90,39 +112,56 @@ jobs:
- setup_repo - setup_repo
- run_tests: - run_tests:
flavour: << parameters.flavour >> flavour: << parameters.flavour >>
ui-test-and-release: - run_ui_tests:
# Parameters used for determining flavour: << parameters.flavour >>
deploy-to-playstore:
parameters: parameters:
flavour: flavour:
type: string type: string
default: "AtlasWeather" default: "Driver"
executor: docker:
name: android/android-machine - image: cimg/android:2023.07-browsers
tag: 2023.05.1 auth:
username: ${DOCKER_USERNAME}
password: ${DOCKER_PASSWORD}
steps: steps:
- setup_repo - setup_repo
- run_ui_tests - deploy_to_play_store:
- run: flavour: << parameters.flavour >>
name: Setup variables for release
command: |
echo "$RELEASE_KEYSTORE_BASE64" | base64 --decode > "android/app/release_keystore.jks"
echo "$GOOGLE_PLAY_KEY" > "android/playstore.json"
# And finally run the release build
- run:
name: Assemble and Upload to PlayStore
command: |
pwd
bundle exec fastlane deploy<< parameters.flavour >>
# Invoke jobs via workflows # Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows: workflows:
version: 2 version: 2
build-release-mono:
jobs:
- build-and-test:
context: appttude
flavour: "MonoWeather"
filters:
branches:
ignore:
- main_atlas
- deploy-to-playstore:
context: appttude
flavour: "MonoWeather"
filters:
branches:
only:
- main_mono
requires:
- build-and-test
build-release-atlas: build-release-atlas:
jobs: jobs:
- build-and-test: - build-and-test:
context: appttude
flavour: "AtlasWeather" flavour: "AtlasWeather"
- ui-test-and-release: filters:
branches:
ignore:
- main_mono
- deploy-to-playstore:
context: appttude
flavour: "AtlasWeather" flavour: "AtlasWeather"
filters: filters:
branches: branches:
@@ -130,14 +169,3 @@ workflows:
- main_atlas - main_atlas
requires: requires:
- build-and-test - build-and-test
build-release-mono:
jobs:
- build-and-test:
flavour: "MonoWeather"
- ui-test-and-release:
flavour: "MonoWeather"
filters:
branches:
only: main_admin
requires:
- build-and-test

8
.gitignore vendored
View File

@@ -88,3 +88,11 @@ gen-external-apklibs
.idea/assetWizardSettings.xml .idea/assetWizardSettings.xml
.idea/gradle.xml .idea/gradle.xml
.idea/jarRepositorie .idea/jarRepositorie
# Gem/fastlane
/Gemfile.lock
/fastlane/report.xml
# Google play files
/google-play-key.json
/.idea/androidTestResultsUserPreferences.xml

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="-2146704034">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-2008434490">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-409920851">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="170536241">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="287238248">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="408375334">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1127175145">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1256180664">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_2_API_27" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -1,24 +1,13 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS"> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement> <arrangement>
<rules> <rules>
<section> <section>
@@ -127,5 +116,8 @@
</rules> </rules>
</arrangement> </arrangement>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme> </code_scheme>
</component> </component>

6
.idea/kotlinc.xml generated Normal file
View File

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

21
.idea/navEditor.xml generated
View File

@@ -79,6 +79,18 @@
</LayoutPositions> </LayoutPositions>
</value> </value>
</entry> </entry>
<entry key="settings_fragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="12" />
<option name="y" value="12" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="worldItemFragment"> <entry key="worldItemFragment">
<value> <value>
<LayoutPositions> <LayoutPositions>
@@ -88,6 +100,15 @@
<option name="y" value="408" /> <option name="y" value="408" />
</Point> </Point>
</option> </option>
<option name="myPositions">
<map>
<entry key="action_worldItemFragment_to_furtherDetailsFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions> </LayoutPositions>
</value> </value>
</entry> </entry>

3
Gemfile Normal file
View File

@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

View File

@@ -61,6 +61,9 @@ android {
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
freeCompilerArgs += [
'-Xjvm-default=enable'
]
} }
flavorDimensions "default" flavorDimensions "default"
@@ -71,7 +74,7 @@ android {
versionName "3.0.0" versionName "3.0.0"
} }
monoWeather { monoWeather {
applicationId "com.appttude.h_mal.atlas_weather.monoWeather" applicationId "com.appttude.h_mal.monoWeather"
versionCode 5 versionCode 5
versionName "3.0.0" versionName "3.0.0"
} }
@@ -106,20 +109,21 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0' implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
implementation 'androidx.preference:preference:1.2.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2' androidTestImplementation 'com.android.support.test:rules:1.0.2'
// Unit testing / * Unit testing * /
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testImplementation "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" implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// android unit testing and espresso / * android unit testing and espresso * /
androidTestImplementation 'androidx.test:rules:1.4.1-alpha06' androidTestImplementation 'androidx.test:rules:1.4.1-alpha06'
androidTestImplementation "androidx.test:core:1.4.0" androidTestImplementation "androidx.test:core:1.4.0"
/ * Android Espresso */ / * Android Espresso * /
def testJunitVersion = "1.1.5" def testJunitVersion = "1.1.5"
def testRunnerVersion = "1.5.2" def testRunnerVersion = "1.5.2"
def espressoVersion = "3.5.1" def espressoVersion = "3.5.1"
@@ -131,59 +135,59 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
androidTestImplementation "org.hamcrest:hamcrest:2.2" androidTestImplementation "org.hamcrest:hamcrest:2.2"
//mock websever for testing retrofit responses / * mock websever for testing retrofit responses * /
testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0" testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
//mockito and livedata testing / * mockito and livedata testing * /
testImplementation 'org.mockito:mockito-inline:2.13.0' testImplementation 'org.mockito:mockito-inline:2.13.0'
implementation 'androidx.arch.core:core-testing:2.2.0' implementation 'androidx.arch.core:core-testing:2.2.0'
// Mockk / * MockK * /
def mockk_ver = "1.10.5" def mockk_ver = "1.10.5"
testImplementation "io.mockk:mockk:$mockk_ver" testImplementation "io.mockk:mockk:$mockk_ver"
androidTestImplementation "io.mockk:mockk-android:$mockk_ver" androidTestImplementation "io.mockk:mockk-android:$mockk_ver"
// Retrofit / * Retrofit * /
def retrofit_ver = "2.8.1" def retrofit_ver = "2.8.1"
implementation "com.squareup.retrofit2:retrofit:$retrofit_ver" implementation "com.squareup.retrofit2:retrofit:$retrofit_ver"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver" implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
// Shared prefs / * Shared prefs * /
def prefs_ver = "1.1.1" def prefs_ver = "1.1.1"
implementation "androidx.preference:preference-ktx:$prefs_ver" implementation "androidx.preference:preference-ktx:$prefs_ver"
//Kodein Dependency Injection / *Kodein Dependency Injection * /
def kodein_version = "6.2.1" def kodein_version = "6.2.1"
implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" 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-framework-android-x:$kodein_version"
// Room database / * Room database * /
def room_version = "2.3.0-alpha03" def room_version = "2.3.0-alpha03"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
// Picasso / * Picasso * /
implementation 'com.squareup.picasso:picasso:2.71828' implementation 'com.squareup.picasso:picasso:2.71828'
// coroutine / * coroutine * /
def coroutine_version = "1.3.9" def coroutine_version = "1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
// tomtom search / * tomtom search * /
def tomtom_version = "2.4771" def tomtom_version = "2.4771"
implementation "com.tomtom.online:sdk-search:$tomtom_version" implementation "com.tomtom.online:sdk-search:$tomtom_version"
implementation "com.tomtom.online:sdk-maps:2.4807" implementation "com.tomtom.online:sdk-maps:2.4807"
/* coroutines support for firebase operations */ / * coroutines support for firebase operations * /
def coroutines_google_ver = "1.1.1" def coroutines_google_ver = "1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines_google_ver" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines_google_ver"
/ * Glide */ / * Picasso * /
implementation 'com.github.bumptech.glide:glide:4.12.0' implementation 'com.squareup.picasso:picasso:2.71828'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
/ * screenshot library */ / * screenshot library * /
androidTestImplementation 'tools.fastlane:screengrab:2.1.1' androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
} }

View File

@@ -0,0 +1,90 @@
package com.appttude.h_mal.atlas_weather
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
import com.appttude.h_mal.atlas_weather.utils.Stubs
import kotlinx.coroutines.runBlocking
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
open class BaseTest<A : Activity>(
private val activity: Class<A>,
private val intentBundle: Bundle? = null,
) {
lateinit var scenario: ActivityScenario<A>
private lateinit var testApp: TestAppClass
private lateinit var testActivity: Activity
@get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@get:Rule
var snapshotRule: SnapshotRule = SnapshotRule()
@Before
fun setUp() {
Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())
val startIntent =
Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity)
if (intentBundle != null) {
startIntent.replaceExtras(intentBundle)
}
testApp = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
runBlocking {
beforeLaunch()
}
scenario = ActivityScenario.launch(startIntent)
scenario.onActivity {
testActivity = it
}
afterLaunch()
}
fun stubEndpoint(url: String, stub: Stubs) {
testApp.stubUrl(url, stub.id)
}
fun unstubEndpoint(url: String) {
testApp.removeUrlStub(url)
}
fun getActivity() = testActivity
@After
fun tearDown() {
testFinished()
}
open fun beforeLaunch() {}
open fun afterLaunch() {}
open fun testFinished() {}
fun waitFor(delay: Long) {
Espresso.onView(ViewMatchers.isRoot()).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> = ViewMatchers.isRoot()
override fun getDescription(): String = "wait for $delay milliseconds"
override fun perform(uiController: UiController, v: View?) {
uiController.loopMainThreadForAtLeast(delay)
}
})
}
}

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.utils package com.appttude.h_mal.atlas_weather
import android.content.res.Resources import android.content.res.Resources
import android.view.View import android.view.View
@@ -16,14 +16,9 @@ import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import com.appttude.h_mal.atlas_weather.helpers.checkErrorMessage
import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.appttude.h_mal.atlas_weather.helpers.checkImage
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.appttude.h_mal.atlas_weather.checkErrorMessage
import com.appttude.h_mal.atlas_weather.checkImage
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anything import org.hamcrest.CoreMatchers.anything
@@ -46,6 +41,8 @@ open class BaseTestRobot {
fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId)) fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId))
fun matchDisplayed(resId: Int): ViewInteraction = matchView(resId).check(matches(isDisplayed()))
fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction
.check(matches(withText(text))) .check(matches(withText(text)))
@@ -142,7 +139,7 @@ open class BaseTestRobot {
Resources.getSystem().getString(resId) Resources.getSystem().getString(resId)
fun pullToRefresh(resId: Int){ fun pullToRefresh(resId: Int){
onView(allOf(withId(resId), ViewMatchers.isDisplayed())).perform(swipeDown()) onView(allOf(withId(resId), isDisplayed())).perform(swipeDown())
} }
fun selectDateInPicker(year: Int, month: Int, day: Int) { fun selectDateInPicker(year: Int, month: Int, day: Int) {

View File

@@ -1,141 +0,0 @@
//package com.appttude.h_mal.atlas_weather
//
//import android.Manifest
//import android.R
//import android.app.Activity
//import android.content.Context
//import android.os.Build
//import android.view.View
//import android.view.WindowManager
//import androidx.annotation.StringRes
//import androidx.test.core.app.ActivityScenario
//import androidx.test.espresso.*
//import androidx.test.espresso.Espresso.onView
//import androidx.test.espresso.assertion.ViewAssertions.matches
//import androidx.test.espresso.matcher.ViewMatchers.*
//import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
//import androidx.test.rule.GrantPermissionRule
//import com.appttude.h_mal.atlas_weather.atlasWeather.ui.BaseActivity
////import h_mal.appttude.com.driver.base.BaseActivity
//import com.appttude.h_mal.atlas_weather.helpers.BaseViewAction
//import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
//import org.hamcrest.CoreMatchers
//import org.hamcrest.Description
//import org.hamcrest.Matcher
//import org.hamcrest.TypeSafeMatcher
//import org.hamcrest.core.AllOf
//import org.junit.After
//import org.junit.Before
//import org.junit.Rule
//import tools.fastlane.screengrab.Screengrab
//import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
//import tools.fastlane.screengrab.locale.LocaleTestRule
//
//
//open class BaseUiTest<T : BaseActivity>(
// private val activity: Class<T>
//) {
//
// private lateinit var mActivityScenarioRule: ActivityScenario<T>
// private var mIdlingResource: IdlingResource? = null
//
// private lateinit var currentActivity: Activity
//
// @get:Rule
// var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
//
// @get:Rule
// var snapshotRule: SnapshotRule = SnapshotRule()
//
// @Rule
// @JvmField
// var localeTestRule = LocaleTestRule()
//
// @Before
// fun setup() {
// Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())
// beforeLaunch()
// mActivityScenarioRule = ActivityScenario.launch(activity)
// mActivityScenarioRule.onActivity {
//// mIdlingResource = it.getIdlingResource()!!
//// IdlingRegistry.getInstance().register(mIdlingResource)
// afterLaunch(it)
// }
// }
//
// @After
// fun tearDown() {
// mIdlingResource?.let {
// IdlingRegistry.getInstance().unregister(it)
// }
// }
//
// fun getResourceString(@StringRes stringRes: Int): String {
// return getInstrumentation().targetContext.resources.getString(
// stringRes
// )
// }
//
// fun waitFor(delay: Long) {
// onView(isRoot()).perform(object : ViewAction {
// override fun getConstraints(): Matcher<View> = isRoot()
// override fun getDescription(): String = "wait for $delay milliseconds"
// override fun perform(uiController: UiController, v: View?) {
// uiController.loopMainThreadForAtLeast(delay)
// }
// })
// }
//
// open fun beforeLaunch() {}
// open fun afterLaunch(context: Context) {}
//
//
// @Suppress("DEPRECATION")
// fun checkToastMessage(message: String) {
// onView(withText(message)).inRoot(object : TypeSafeMatcher<Root>() {
// override fun describeTo(description: Description?) {
// description?.appendText("is toast")
// }
//
// override fun matchesSafely(root: Root): Boolean {
// root.run {
// if (windowLayoutParams.get().type == WindowManager.LayoutParams.TYPE_TOAST) {
// decorView.run {
// if (windowToken === applicationWindowToken) {
// // windowToken == appToken means this window isn't contained by any other windows.
// // if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
// return true
// }
// }
// }
// }
// return false
// }
// }
// ).check(matches(isDisplayed()))
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// waitFor(3500)
// }
// }
//
// fun checkSnackBarDisplayedByMessage(message: String) {
// onView(
// CoreMatchers.allOf(
// withId(com.google.android.material.R.id.snackbar_text),
// withText(message)
// )
// ).check(matches(isDisplayed()))
// }
//
// private fun getCurrentActivity(): Activity {
// onView(AllOf.allOf(withId(R.id.content), isDisplayed()))
// .perform(object : BaseViewAction() {
// override fun setPerform(uiController: UiController?, view: View?) {
// if (view?.context is Activity) {
// currentActivity = view.context as Activity
// }
// }
// })
// return currentActivity
// }
//}

View File

@@ -1,14 +0,0 @@
package com.appttude.h_mal.atlas_weather.helpers
import android.os.Environment
import java.io.File
/**
* File paths for images on device
*/
fun getImagePath(imageConst: String): String {
return File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
"/Camera/images/$imageConst"
).absolutePath
}

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather package com.appttude.h_mal.atlas_weather.helpers
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.view.View import android.view.View

View File

@@ -1,33 +0,0 @@
package com.appttude.h_mal.atlas_weather.helpers
import android.content.ClipData
import android.content.ClipData.Item
import android.net.Uri
import java.io.File
object DataHelper {
fun createClipItem(filePath: String) = Item(
Uri.fromFile(
File(filePath)
)
)
fun createClipData(item: Item, mimeType: String = "text/uri-list") =
ClipData(null, arrayOf(mimeType), item)
fun createClipData(filePath: String) = createClipData(createClipItem(filePath))
fun createClipData(filePaths: Array<String>): ClipData {
val clipData = createClipData(filePaths[0])
val remainingFiles = filePaths.copyOfRange(1, filePaths.size - 1)
clipData.addFilePaths(remainingFiles)
return clipData
}
fun createClipData(uri: Uri) = createClipData(Item(uri))
fun ClipData.addFilePaths(filePaths: Array<String>) {
filePaths.forEach { addItem(createClipItem(it)) }
}
}

View File

@@ -1,9 +1,10 @@
package com.appttude.h_mal.atlas_weather.data.location package com.appttude.h_mal.atlas_weather.instrumentationTests
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.model.types.LocationType import com.appttude.h_mal.atlas_weather.model.types.LocationType
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.hamcrest.Matcher.* import org.hamcrest.Matcher.*

View File

@@ -1,67 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
import android.Manifest
import android.app.Activity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.After
import org.junit.Rule
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
open class BaseTest<A : Activity> {
lateinit var testApp: TestAppClass
@get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@get:Rule
var snapshotRule: SnapshotRule = SnapshotRule()
@Rule
@JvmField
var mActivityTestRule: ActivityTestRule<A> =
object : ActivityTestRule<A>(getGenericClassAt<A>(0).java) {
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())
testApp =
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
setupFeed()
}
override fun afterActivityLaunched() {
// Dismiss dialog
onView(withText("AGREE")).inRoot(isDialog()).check(matches(isDisplayed()))
.perform(ViewActions.click())
}
}
fun stubEndpoint(url: String, stub: Stubs) {
testApp.stubUrl(url, stub.id)
}
fun unstubEndpoint(url: String) {
testApp.removeUrlStub(url)
}
@After
fun tearDown() {
}
open fun setupFeed() {}
}

View File

@@ -1,84 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
import android.Manifest
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4ClassRunner::class)
class HomePageUITestScenario : BaseMainScenario() {
override fun setupFeed() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
}
@Test
fun loadApp_validWeatherResponse_returnsValidPage() {
homeScreen {
verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location")
}
}
}
open class BaseMainScenario {
lateinit var scenario: ActivityScenario<MainActivity>
private lateinit var testApp: TestAppClass
@get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@Before
fun setUp() {
scenario = launch(MainActivity::class.java)
scenario.onActivity {
runBlocking {
testApp = it.application as TestAppClass
setupFeed()
}
}
// Dismiss dialog on start up
Espresso.onView(ViewMatchers.withText("AGREE"))
.inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
}
fun stubEndpoint(url: String, stub: Stubs) {
testApp.stubUrl(url, stub.id)
}
fun unstubEndpoint(url: String) {
testApp.removeUrlStub(url)
}
@After
fun tearDown() {
testFinished()
}
open fun setupFeed() {}
open fun testFinished() {}
}

View File

@@ -1,30 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.widget
import android.appwidget.AppWidgetManager
import android.content.Intent
import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.rule.ActivityTestRule
import com.appttude.h_mal.atlas_weather.R
import org.junit.Rule
import org.junit.Test
class WidgetLocationPermissionActivityTest {
@Rule
@JvmField
var mActivityTestRule : ActivityTestRule<WidgetLocationPermissionActivity> =
ActivityTestRule(WidgetLocationPermissionActivity::class.java, false, false)
@Test
fun demo_test() {
val startIntent = Intent().apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 112)
}
mActivityTestRule.launchActivity(startIntent)
Espresso.onView((ViewMatchers.withId(R.id.declaration_text))).check(matches(isDisplayed()));
}
}

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.robot 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.R
import com.appttude.h_mal.atlas_weather.utils.BaseTestRobot
fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
class HomeScreenRobot : BaseTestRobot() { class HomeScreenRobot : BaseTestRobot() {

View File

@@ -1,29 +1,22 @@
package com.appttude.h_mal.atlas_weather.tests package com.appttude.h_mal.atlas_weather.tests
import androidx.test.rule.GrantPermissionRule import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.robot.homeScreen import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.monoWeather.testsuite.BaseTest import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.Rule
import org.junit.Test import org.junit.Test
class HomePageUITest : BaseTest<MainActivity>() { class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
@Rule override fun beforeLaunch() {
@JvmField
var mGrantPermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.ACCESS_COARSE_LOCATION")
override fun setupFeed() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid) stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
} }
@Test @Test
fun loadApp_validWeatherResponse_returnsValidPage() { fun loadApp_validWeatherResponse_returnsValidPage() {
homeScreen { homeScreen {
waitFor(2000)
verifyCurrentTemperature(2) verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location") verifyCurrentLocation("Mock Location")
} }

View File

@@ -1,25 +0,0 @@
package com.appttude.h_mal.atlas_weather.tests
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.monoWeather.testsuite.BaseTest
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.Rule
import org.junit.Test
class HomePageUITest : BaseTest<MainActivity>() {
override fun setupFeed() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
}
@Test
fun loadApp_validWeatherResponse_returnsValidPage() {
homeScreen {
verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location")
}
}
}

View File

@@ -0,0 +1,20 @@
package com.appttude.h_mal.monoWeather
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity
open class MonoBaseTest: BaseTest<MainActivity>(MainActivity::class.java) {
override fun afterLaunch() {
// Dismiss dialog on start up
Espresso.onView(ViewMatchers.withText("AGREE"))
.inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
}
}

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.robot package com.appttude.h_mal.monoWeather.robot
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.utils.BaseTestRobot import com.appttude.h_mal.atlas_weather.BaseTestRobot
fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
class HomeScreenRobot : BaseTestRobot() { class HomeScreenRobot : BaseTestRobot() {

View File

@@ -0,0 +1,11 @@
package com.appttude.h_mal.monoWeather.robot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.BaseTestRobot
fun widgetPermissionScreen(func: WidgetPermissionScreenRobot.() -> Unit) =
WidgetPermissionScreenRobot().apply { func() }
class WidgetPermissionScreenRobot : BaseTestRobot() {
fun declarationDisplayed() = matchDisplayed(R.id.declaration_text)
}

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.monoWeather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.monoWeather.MonoBaseTest
import org.junit.Test
class HomePageUITest : MonoBaseTest() {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
}
@Test
fun loadApp_validWeatherResponse_returnsValidPage() {
homeScreen {
verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location")
}
}
}

View File

@@ -0,0 +1,26 @@
package com.appttude.h_mal.monoWeather.tests
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import androidx.test.rule.ActivityTestRule
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.monoWeather.robot.widgetPermissionScreen
import com.appttude.h_mal.monoWeather.ui.widget.WidgetLocationPermissionActivity
import org.junit.Test
class WidgetLocationPermissionActivityTest : BaseTest<WidgetLocationPermissionActivity>(
activity = WidgetLocationPermissionActivity::class.java,
intentBundle = Bundle().apply {
putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, 112)
}
) {
@Test
fun demo_test() {
widgetPermissionScreen {
declarationDisplayed()
}
}
}

View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="com.appttude.h_mal.atlas_weather"
tools:node="merge">
<application <application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass" android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
@@ -14,7 +12,7 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:node="merge"> tools:node="merge">
<activity android:name=".atlasWeather.ui.MainActivity" <activity android:name=".ui.MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar" android:theme="@style/AppTheme.NoActionBar"
@@ -26,18 +24,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".atlasWeather.ui.settings.UnitSettingsActivity"
android:label="Settings"
android:exported="true"/>
<receiver <receiver
android:name=".atlasWeather.notification.NotificationReceiver" android:name=".notification.NotificationReceiver"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:exported="true"/> android:exported="true"/>
<receiver android:name=".atlasWeather.widget.NewAppWidget" <receiver android:name=".widget.NewAppWidget"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -49,11 +41,6 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/new_app_widget_info" /> android:resource="@xml/new_app_widget_info" />
</receiver> </receiver>
<service
android:name=".atlasWeather.widget.WidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application> </application>
</manifest> </manifest>

View File

@@ -1,55 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.view.View
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.hide
import com.appttude.h_mal.atlas_weather.utils.show
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
abstract class BaseFragment: Fragment(){
// toggle visibility of progress spinner while async operations are taking place
fun progressBarStateObserver(progressBar: View) = Observer<Event<Boolean>> {
when(it.getContentIfNotHandled()){
true -> {
progressBar.show()
}
false -> {
progressBar.hide()
}
}
}
// display a toast when operation fails
fun errorObserver() = Observer<Event<String>> {
it.getContentIfNotHandled()?.let { message ->
displayToast(message)
}
}
@SuppressLint("MissingPermission")
fun getPermissionResult(
permission: String,
permissionCode: Int,
permissionGranted: () -> Unit
){
if (ActivityCompat.checkSelfPermission(requireContext(), permission) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(permission), permissionCode)
return
}else{
CoroutineScope(Dispatchers.Main).launch{
permissionGranted.invoke()
}
}
}
}

View File

@@ -1,65 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.settings.UnitSettingsActivity
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.atlasWeather.activity_main.*
class MainActivity : BaseActivity(){
lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
setSupportActionBar(toolbar)
navHost = supportFragmentManager
.findFragmentById(R.id.container) as NavHostFragment
val navController = navHost.navController
navController.setGraph(R.navigation.main_navigation)
setupBottomBar(navView, navController)
}
private fun setupBottomBar(navView: BottomNavigationView, navController: NavController) {
val tabs = setOf(R.id.nav_home, R.id.nav_world)
val appBarConfiguration = AppBarConfiguration(tabs)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) {
R.id.action_settings -> {
val i = Intent(this, UnitSettingsActivity::class.java)
startActivity(i)
return true
}
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -1,99 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
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.atlasWeather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.atlasWeather.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.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import kotlinx.android.synthetic.main.fragment_home.*
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance
/**
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
class HomeFragment : BaseFragment(), KodeinAware {
override val kodein by kodein()
private val factory by instance<ApplicationViewModelFactory>()
private val viewModel by activityViewModels<MainViewModel> { factory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home, container, false)
}
@SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerAdapter = WeatherRecyclerAdapter {
val directions =
HomeFragmentDirections.actionHomeFragmentToFurtherDetailsFragment(it)
navigateTo(directions)
}
forecast_listview.apply {
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
}
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){
viewModel.fetchData()
}
swipe_refresh.apply {
setOnRefreshListener {
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){
viewModel.fetchData()
}
isRefreshing = true
}
}
viewModel.weatherLiveData.observe(viewLifecycleOwner) {
recyclerAdapter.addCurrent(it)
}
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
viewModel.operationState.observe(viewLifecycleOwner){
swipe_refresh.isRefreshing = false
}
}
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_PERMISSION_REQUEST) {
if (grantResults.isNotEmpty()
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
viewModel.fetchData()
displayToast("Permission granted")
} else {
displayToast("Permission denied")
}
}
}
}

View File

@@ -1,88 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.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.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.preference.PreferenceActivity
import android.preference.PreferenceFragment
import androidx.preference.PreferenceManager
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.atlasWeather.notification.NotificationReceiver
import com.appttude.h_mal.atlas_weather.atlasWeather.widget.NewAppWidget
import java.util.*
class UnitSettingsActivity : PreferenceActivity() {
private var prefListener: OnSharedPreferenceChangeListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.prefs, false)
fragmentManager.beginTransaction().replace(android.R.id.content, MyPreferenceFragment()).commit()
//listener on changed sort order preference:
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
prefListener = OnSharedPreferenceChangeListener { _, key ->
if (key == "temp_units") {
val intent = Intent(baseContext, NewAppWidget::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(application).getAppWidgetIds(ComponentName(application, NewAppWidget::class.java))
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
}
if (key == "notif_boolean") {
setupNotificationBroadcaster(baseContext)
}
if (key == "widget_black_background"){
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
val widgetManager = AppWidgetManager.getInstance(this)
val ids = widgetManager.getAppWidgetIds(ComponentName(this, NewAppWidget::class.java))
AppWidgetManager.getInstance(this).notifyAppWidgetViewDataChanged(ids, R.id.whole_widget_view)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
}
}
prefs.registerOnSharedPreferenceChangeListener(prefListener)
}
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 onBackPressed() {
super.onBackPressed()
}
// @Override
// public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
// Log.i(TAG, "onSharedPreferenceChanged: " + s);
// if (s == "temp_units"){
// Intent intent = new Intent(getBaseContext(), NewAppWidget.class);
// intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
//
// int[] ids = AppWidgetManager.getInstance(getApplication()).getAppWidgetIds(new ComponentName(getApplication(), NewAppWidget.class));
// intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
// sendBroadcast(intent);
// }
// }
class MyPreferenceFragment : PreferenceFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.prefs)
}
}
}

View File

@@ -1,38 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
import android.app.Activity
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
abstract class BaseWidgetClass : AppWidgetProvider(){
fun createRemoteView(context: Context, @LayoutRes id: Int): RemoteViews {
return RemoteViews(context.packageName, id)
}
fun AppWidgetProvider.createUpdatePendingIntent(context: Context, appWidgetId: Int): PendingIntent? {
val seconds = (System.currentTimeMillis() / 1000L).toInt()
val intentUpdate = Intent(context, this::class.java)
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val idArray = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
return PendingIntent.getBroadcast(
context, seconds, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT)
}
fun <T: Activity> createClickingPendingIntent(context: Context, activityClass: Class<T>): PendingIntent {
val clickIntentTemplate = Intent(context, activityClass)
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
}

View File

@@ -1,77 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
import kotlinx.coroutines.runBlocking
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
class MyWidgetRemoteViewsFactory(
private val context: Context,
val intent: Intent
) : RemoteViewsFactory{
private val TAG = "MyWidgetRemoteViewsFactory"
private val kodein = LateInitKodein()
private val helper : ServicesHelper by kodein.instance()
private var appWidgetId: Int? = 0
private var list: List<InnerWidgetData>? = null
init {
appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID)
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
}
override fun onCreate() {}
override fun onDataSetChanged() {
runBlocking {
list = helper.getWidgetInnerWeather()
}
}
override fun onDestroy() {}
override fun getCount(): Int = list?.size ?: 5
override fun getViewAt(i: Int): RemoteViews {
val rv = RemoteViews(context.packageName, R.layout.widget_item)
if (list.isNullOrEmpty()) return rv
list?.get(i)?.let {
rv.setTextViewText(R.id.widget_item_day, it.date)
rv.setImageViewBitmap(R.id.widget_item_image, it.icon)
rv.setTextViewText(R.id.widget_item_temp_high, it.highTemp)
rv.setOnClickFillInIntent(R.id.widget_item_layout, intent)
}
return rv
}
override fun getLoadingView(): RemoteViews {
return RemoteViews(context.packageName, R.layout.widget_item_loading)
}
override fun getViewTypeCount(): Int = 1
override fun getItemId(i: Int): Long = i.toLong()
override fun hasStableIds(): Boolean {
return true
}
}

View File

@@ -1,141 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
import android.Manifest
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import android.widget.RemoteViews
import androidx.core.app.ActivityCompat
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
/**
* Implementation of App Widget functionality.
*/
private val TAG = NewAppWidget::class.java.simpleName
class NewAppWidget : BaseWidgetClass() {
private val kodein = LateInitKodein()
private val helper : ServicesHelper by kodein.instance()
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
// There may be multiple widgets active, so update all of them
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return
}
CoroutineScope(Dispatchers.IO).launch {
val results = helper.fetchData()
if (results) return@launch
val weatherWidgetCurrent = helper.getWidgetWeather()
withContext(Dispatchers.Main){
for (appWidgetId in appWidgetIds) {
val updatePendingIntent = createUpdatePendingIntent(context, appWidgetId)
val views = createRemoteView(context, R.layout.new_app_widget)
bindView(context, appWidgetId, views, updatePendingIntent, weatherWidgetCurrent)
}
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
}
}
override fun onEnabled(context: Context) {
try {
val appWidgetManager = AppWidgetManager.getInstance(context)
val thisAppWidget = ComponentName(context.packageName, NewAppWidget::class.java.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_listview)
} catch (e: Exception) {
Log.e(TAG, "onEnabled: ", e)
}
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action ==
AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val thisAppWidget = ComponentName(context.packageName, NewAppWidget::class.java.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_listview)
}
super.onReceive(context, intent)
}
private fun createForecastListIntent(
context: Context,
appWidgetId: Int
): Intent {
return Intent(context, WidgetRemoteViewsService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
}
private fun bindView(
context: Context,
appWidgetId: Int,
views: RemoteViews,
updatePendingIntent: PendingIntent?,
weather: WidgetData?){
val appWidgetManager = AppWidgetManager.getInstance(context)
views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
weather?.let {
val intent = createForecastListIntent(
context,
appWidgetId
)
views.setRemoteAdapter(R.id.widget_listview, intent)
views.setTextViewText(R.id.widget_main_temp, it.currentTemp)
views.setTextViewText(R.id.widget_feel_temp, "°C")
views.setTextViewText(R.id.widget_current_location, it.location)
views.setImageViewResource(R.id.location_icon, R.drawable.location_flag)
// views.setImageViewBitmap(R.id.widget_current_icon, it.icon)
val clickPendingIntentTemplate = createClickingPendingIntent(context, MainActivity::class.java)
views.setPendingIntentTemplate(R.id.widget_listview, clickPendingIntentTemplate)
views.setOnClickPendingIntent(R.id.widget_current_icon, updatePendingIntent)
views.setOnClickPendingIntent(R.id.widget_current_location, updatePendingIntent)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)
return
}
Log.i(TAG, "onPostExecute: weather is empty")
views.setTextViewText(R.id.widget_current_location, "Refresh")
views.setImageViewResource(R.id.widget_current_icon, R.drawable.widget_error_icon)
views.setImageViewResource(R.id.location_icon, R.drawable.refreshing)
views.setOnClickPendingIntent(R.id.widget_current_icon, updatePendingIntent)
views.setOnClickPendingIntent(R.id.widget_current_location, updatePendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}

View File

@@ -1,10 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
import android.content.Intent
import android.widget.RemoteViewsService
class WidgetRemoteViewsService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return MyWidgetRemoteViewsFactory(applicationContext, intent)
}
}

View File

@@ -1,15 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
import android.app.Activity
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
fun <T: Activity> createClickingPendingIntent(context: Context, activityClass: Class<T>): PendingIntent {
val clickIntentTemplate = Intent(context, activityClass)
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.notification package com.appttude.h_mal.atlas_weather.notification
import android.graphics.Bitmap import android.graphics.Bitmap

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.notification package com.appttude.h_mal.atlas_weather.notification
import android.Manifest import android.Manifest
import android.app.Notification import android.app.Notification
@@ -12,13 +12,10 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.MainActivity 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.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.utils.displayToast import com.appttude.h_mal.atlas_weather.utils.displayToast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui package com.appttude.h_mal.atlas_weather.ui
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.atlas_weather.ui
import com.appttude.h_mal.atlas_weather.R
val tabs = setOf(R.id.nav_home, R.id.nav_world)

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui package com.appttude.h_mal.atlas_weather.ui
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter.WeatherRecyclerAdapter import com.appttude.h_mal.atlas_weather.ui.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.navigateTo import com.appttude.h_mal.atlas_weather.utils.navigateTo
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.details package com.appttude.h_mal.atlas_weather.ui.details
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.dialog package com.appttude.h_mal.atlas_weather.ui.dialog
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
@@ -14,7 +14,6 @@ interface DeclarationBuilder{
fun Context.readFromResources(@StringRes id: Int) = resources.getString(id) fun Context.readFromResources(@StringRes id: Int) = resources.getString(id)
@RequiresApi(Build.VERSION_CODES.N)
fun buildMessage(): CharSequence? { fun buildMessage(): CharSequence? {
val link1 = "<font color='blue'><a href=\"$link\">here</a></font>" val link1 = "<font color='blue'><a href=\"$link\">here</a></font>"
val message = "$message See my privacy policy: $link1" val message = "$message See my privacy policy: $link1"

View File

@@ -1,13 +1,10 @@
package com.appttude.h_mal.atlas_weather.monoWeather.dialog package com.appttude.h_mal.atlas_weather.ui.dialog
import android.content.Context import android.content.Context
import android.os.Build
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.appttude.h_mal.atlas_weather.R
class PermissionsDeclarationDialog(context: Context) : BaseDeclarationDialog(context) { class PermissionsDeclarationDialog(context: Context) : BaseDeclarationDialog(context) {

View File

@@ -0,0 +1,109 @@
package com.appttude.h_mal.atlas_weather.ui.home
import android.Manifest
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.Navigation.findNavController
import androidx.navigation.ui.onNavDestinationSelected
import androidx.recyclerview.widget.LinearLayoutManager
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.model.forecast.Forecast
import com.appttude.h_mal.atlas_weather.ui.dialog.PermissionsDeclarationDialog
import com.appttude.h_mal.atlas_weather.ui.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import com.appttude.h_mal.monoWeather.ui.BaseFragment
import kotlinx.android.synthetic.main.fragment_home.*
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance
/**
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
class HomeFragment : BaseFragment(R.layout.fragment_home) {
private val viewModel by getFragmentViewModel<MainViewModel>()
@SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
navigateToFurtherDetails(it)
})
forecast_listview.apply {
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
}
swipe_refresh.apply {
setOnRefreshListener {
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
viewModel.fetchData()
isRefreshing = true
}
}
}
viewModel.weatherLiveData.observe(viewLifecycleOwner) {
recyclerAdapter.addCurrent(it)
}
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
viewModel.operationRefresh.observe(viewLifecycleOwner) { it ->
it.getContentIfNotHandled()?.let {
swipe_refresh.isRefreshing = false
}
}
viewModel.operationState.observe(viewLifecycleOwner) {
swipe_refresh.isRefreshing = false
}
}
@SuppressLint("MissingPermission")
override fun onStart() {
super.onStart()
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
viewModel.fetchData()
}
}
@SuppressLint("MissingPermission")
override fun permissionsGranted() {
viewModel.fetchData()
}
private fun navigateToFurtherDetails(forecast: Forecast) {
val directions = HomeFragmentDirections
.actionHomeFragmentToFurtherDetailsFragment(forecast)
navigateTo(directions)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate the menu; this adds items to the action bar if it is present.
inflater.inflate(R.menu.menu_main, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navController = findNavController(requireActivity(), R.id.container)
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
}
}

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter package com.appttude.h_mal.atlas_weather.ui.home.adapter
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter package com.appttude.h_mal.atlas_weather.ui.home.adapter
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter package com.appttude.h_mal.atlas_weather.ui.home.adapter
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter package com.appttude.h_mal.atlas_weather.ui.home.adapter
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter package com.appttude.h_mal.atlas_weather.ui.home.adapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.View import android.view.View

View File

@@ -0,0 +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
class SettingsFragment : PreferenceFragmentCompat() {
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") {
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)
}
}
}
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

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.world package com.appttude.h_mal.atlas_weather.ui.world
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -7,28 +7,20 @@ import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.observe import androidx.lifecycle.observe
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.utils.displayToast import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.goBack import com.appttude.h_mal.atlas_weather.utils.goBack
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import com.appttude.h_mal.monoWeather.ui.BaseFragment
import kotlinx.android.synthetic.main.activity_add_forecast.* import kotlinx.android.synthetic.main.activity_add_forecast.*
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
class AddLocationFragment : BaseFragment(), KodeinAware { class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
override val kodein by kodein()
private val factory by instance<ApplicationViewModelFactory>()
private val viewModel by viewModels<WorldViewModel> { factory } private val viewModel by getFragmentViewModel<WorldViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.activity_add_forecast, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.world package com.appttude.h_mal.atlas_weather.ui.world
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -9,12 +9,10 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.observe import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.world.WorldRecyclerAdapter
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.world.WorldFragmentDirections
import com.appttude.h_mal.atlas_weather.utils.navigateTo import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import com.appttude.h_mal.monoWeather.ui.BaseFragment
import kotlinx.android.synthetic.main.fragment_add_location.* import kotlinx.android.synthetic.main.fragment_add_location.*
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein import org.kodein.di.android.x.kodein
@@ -25,17 +23,8 @@ import org.kodein.di.generic.instance
* A simple [Fragment] subclass. * A simple [Fragment] subclass.
* create an instance of this fragment. * create an instance of this fragment.
*/ */
class WorldFragment : BaseFragment(), KodeinAware { class WorldFragment : BaseFragment(R.layout.fragment_add_location) {
override val kodein by kodein() val viewModel by getFragmentViewModel<WorldViewModel>()
private val factory by instance<ApplicationViewModelFactory>()
val viewModel by viewModels<WorldViewModel> { factory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_add_location, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.world package com.appttude.h_mal.atlas_weather.ui.world
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="@color/colour_two"
android:centerColor="@color/colour_three"
android:endColor="@color/colour_four"
android:type="linear"
android:angle="45"/>
</shape>

Binary file not shown.

View File

@@ -76,7 +76,7 @@
android:id="@+id/icon_main_4" android:id="@+id/icon_main_4"
android:layout_width="64dp" android:layout_width="64dp"
android:layout_height="64dp" android:layout_height="64dp"
tools:src="@drawable/day_305" /> tools:srcCompat="@drawable/water_drop" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"

View File

@@ -48,7 +48,9 @@
android:id="@+id/list_icon" android:id="@+id/list_icon"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:src="@drawable/day_305" /> android:adjustViewBounds="true"
tools:layout_width="32dp"
tools:srcCompat="@drawable/water_drop" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"

View File

@@ -8,21 +8,17 @@
<fragment <fragment
android:id="@+id/nav_home" android:id="@+id/nav_home"
android:name="com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.HomeFragment" android:name="com.appttude.h_mal.atlas_weather.ui.home.HomeFragment"
android:label="Home" android:label="Home"
tools:layout="@layout/fragment_home"> tools:layout="@layout/fragment_home">
<action <action
android:id="@+id/action_homeFragment_to_furtherDetailsFragment" android:id="@+id/action_homeFragment_to_furtherDetailsFragment"
app:destination="@id/furtherDetailsFragment" app:destination="@id/furtherDetailsFragment" />
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_open_exit" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/furtherDetailsFragment" android:id="@+id/furtherDetailsFragment"
android:name="com.appttude.h_mal.atlas_weather.atlasWeather.ui.details.FurtherInfoFragment" android:name="com.appttude.h_mal.atlas_weather.ui.details.FurtherInfoFragment"
android:label="Further Details"> android:label="Further Details">
<argument <argument
android:name="forecast" android:name="forecast"
@@ -31,44 +27,36 @@
<fragment <fragment
android:id="@+id/nav_world" android:id="@+id/nav_world"
android:name="com.appttude.h_mal.atlas_weather.atlasWeather.ui.world.WorldFragment" android:name="com.appttude.h_mal.atlas_weather.ui.world.WorldFragment"
android:label="World" android:label="World"
tools:layout="@layout/fragment__two"> tools:layout="@layout/fragment__two">
<action <action
android:id="@+id/action_worldFragment_to_addLocationFragment" android:id="@+id/action_worldFragment_to_addLocationFragment"
app:destination="@id/addLocationFragment" app:destination="@id/addLocationFragment"/>
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_open_exit" />
<action <action
android:id="@+id/action_worldFragment_to_worldItemFragment" android:id="@+id/action_worldFragment_to_worldItemFragment"
app:destination="@id/worldItemFragment" app:destination="@id/worldItemFragment" />
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_open_exit" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/addLocationFragment" android:id="@+id/addLocationFragment"
android:name="com.appttude.h_mal.atlas_weather.atlasWeather.ui.world.AddLocationFragment" android:name="com.appttude.h_mal.atlas_weather.ui.world.AddLocationFragment"
android:label="Add Weather Location" android:label="Add Weather Location"
tools:layout="@layout/activity_add_forecast" /> tools:layout="@layout/activity_add_forecast" />
<fragment <fragment
android:id="@+id/worldItemFragment" android:id="@+id/worldItemFragment"
android:name="com.appttude.h_mal.atlas_weather.atlasWeather.ui.WorldItemFragment" android:name="com.appttude.h_mal.atlas_weather.ui.WorldItemFragment"
android:label="Overview" android:label="Overview"
tools:layout="@layout/fragment_home"> tools:layout="@layout/fragment_home">
<action <action
android:id="@+id/action_worldItemFragment_to_furtherDetailsFragment" android:id="@+id/action_worldItemFragment_to_furtherDetailsFragment"
app:destination="@id/furtherDetailsFragment" app:destination="@id/furtherDetailsFragment" />
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_open_exit" />
<argument <argument
android:name="weatherDisplay" android:name="weatherDisplay"
app:argType="com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay" /> app:argType="com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay" />
</fragment> </fragment>
<fragment
android:id="@+id/settings_fragment"
android:name="com.appttude.h_mal.atlas_weather.ui.settings.SettingsFragment"
android:label="SettingsFragment" />
</navigation> </navigation>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</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>
</resources>

View File

@@ -3,24 +3,27 @@
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@android:color/black</item> <item name="colorPrimary">@android:color/transparent</item>
<item name="colorPrimaryDark">@color/colour_four</item> <item name="colorPrimaryDark">@color/colour_four</item>
<item name="colorAccent">@color/colour_one</item> <item name="colorAccent">@color/colour_one</item>
<item name="android:windowBackground">@drawable/gradient</item>
<item name="fontFamily">sans-serif-light</item> <item name="fontFamily">sans-serif-light</item>
<item name="android:textColor">@color/colorAccent</item>
</style> </style>
<style name="AppTheme.NoActionBar"> <style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
<item name="android:windowBackground">@drawable/gradient</item>
</style> </style>
<style name="TextAppearance.AppCompat.Widget.ActionBar.Title" parent="@android:style/TextAppearance"> <style name="TextAppearance.AppCompat.Widget.ActionBar.Title" parent="@android:style/TextAppearance">
<item name="android:fontFamily">@font/archeologicaps</item>
<!--<item name="android:textColor">@color/colour_five</item>--> <!--<item name="android:textColor">@color/colour_five</item>-->
</style> </style>
<style name="titlebar" parent="@android:style/TextAppearance"> <style name="titlebar" parent="@android:style/TextAppearance">
<item name="android:fontFamily">@font/archeologicaps</item>
<!--<item name="android:textColor">@color/colour_five</item>--> <!--<item name="android:textColor">@color/colour_five</item>-->
</style> </style>
@@ -42,12 +45,4 @@
<item name="android:textSize">32sp</item> <item name="android:textSize">32sp</item>
</style> </style>
<style name="icon_style__further_deatils">
<item name="android:layout_width">64dp</item>
<item name="android:layout_height">64dp</item>
<item name="android:adjustViewBounds">true</item>
<item name="android:layout_gravity">center</item>
<item name="android:tint">@color/colorAccent</item>
</style>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.appttude.h_mal.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity" android:configure="com.appttude.h_mal.atlas_weather.ui.widget.WidgetLocationPermissionActivity"
android:initialKeyguardLayout="@layout/weather_app_widget" android:initialKeyguardLayout="@layout/weather_app_widget"
android:initialLayout="@layout/weather_app_widget" android:initialLayout="@layout/weather_app_widget"
android:minHeight="110.0dp" android:minHeight="110.0dp"

View File

@@ -1,11 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.appttude.h_mal.atlas_weather"> package="com.appttude.h_mal.atlas_weather">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_ALARM" /> <uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application android:networkSecurityConfig="@xml/network_security_config" />
<uses-feature <uses-feature
android:name="android.hardware.location" android:name="android.hardware.location"

View File

@@ -1,27 +1,12 @@
package com.appttude.h_mal.atlas_weather.application package com.appttude.h_mal.atlas_weather.application
import androidx.room.RoomDatabase
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.Api
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor 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.interceptors.QueryParamsInterceptor
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
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.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.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
const val LOCATION_PERMISSION_REQUEST = 505 const val LOCATION_PERMISSION_REQUEST = 505

View File

@@ -25,7 +25,7 @@ import kotlin.coroutines.suspendCoroutine
class LocationProviderImpl( class LocationProviderImpl(
val applicationContext: Context private val applicationContext: Context
) : LocationProvider, LocationHelper(applicationContext) { ) : LocationProvider, LocationHelper(applicationContext) {
private var locationManager = private var locationManager =
applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager? applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?

View File

@@ -1,6 +1,5 @@
package com.appttude.h_mal.atlas_weather.data.network.networkUtils package com.appttude.h_mal.atlas_weather.data.network.networkUtils
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient

View File

@@ -3,11 +3,11 @@ package com.appttude.h_mal.atlas_weather.model.forecast
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.model.weather.DailyWeather import com.appttude.h_mal.atlas_weather.model.weather.DailyWeather
import com.appttude.h_mal.atlas_weather.utils.parcelableCreator
import com.appttude.h_mal.atlas_weather.utils.toDayName import com.appttude.h_mal.atlas_weather.utils.toDayName
import com.appttude.h_mal.atlas_weather.utils.toDayString import com.appttude.h_mal.atlas_weather.utils.toDayString
import com.appttude.h_mal.atlas_weather.utils.toTime import com.appttude.h_mal.atlas_weather.utils.toTime
data class Forecast( data class Forecast(
val date: String?, val date: String?,
val day: String?, val day: String?,
@@ -23,7 +23,7 @@ data class Forecast(
val sunrise: String?, val sunrise: String?,
val sunset: String?, val sunset: String?,
val cloud: String? val cloud: String?
): Parcelable { ): Parcelable{
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readString(), parcel.readString(),
@@ -39,7 +39,9 @@ data class Forecast(
parcel.readString(), parcel.readString(),
parcel.readString(), parcel.readString(),
parcel.readString(), parcel.readString(),
parcel.readString()) parcel.readString()
) {
}
constructor(dailyWeather: DailyWeather) : this( constructor(dailyWeather: DailyWeather) : this(
dailyWeather.dt?.toDayString(), dailyWeather.dt?.toDayString(),
@@ -72,13 +74,20 @@ data class Forecast(
parcel.writeString(uvi) parcel.writeString(uvi)
parcel.writeString(sunrise) parcel.writeString(sunrise)
parcel.writeString(sunset) parcel.writeString(sunset)
parcel.writeString(cloud)
} }
override fun describeContents(): Int { override fun describeContents(): Int {
return 0 return 0
} }
companion object{ companion object CREATOR : Parcelable.Creator<Forecast> {
@JvmField val CREATOR = parcelableCreator(::Forecast) override fun createFromParcel(parcel: Parcel): Forecast {
return Forecast(parcel)
}
override fun newArray(size: Int): Array<Forecast?> {
return arrayOfNulls(size)
}
} }
} }

View File

@@ -1,5 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.forecast package com.appttude.h_mal.atlas_weather.model.forecast
import android.os.Parcel
import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.Hour import com.appttude.h_mal.atlas_weather.model.weather.Hour
@@ -20,7 +22,26 @@ data class WeatherDisplay(
val lat: Double = 0.00, val lat: Double = 0.00,
val lon: Double = 0.00, val lon: Double = 0.00,
var displayName: String? var displayName: String?
){ ): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readValue(Double::class.java.classLoader) as? Double,
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.createTypedArrayList(Hour),
parcel.createTypedArrayList(Forecast),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readDouble(),
parcel.readDouble(),
parcel.readString()
) {
}
constructor(entity: EntityItem) : this( constructor(entity: EntityItem) : this(
entity.weather.current?.temp, entity.weather.current?.temp,
@@ -39,5 +60,38 @@ data class WeatherDisplay(
entity.weather.lon, entity.weather.lon,
entity.weather.locationString entity.weather.locationString
) )
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeValue(averageTemp)
parcel.writeString(unit)
parcel.writeString(location)
parcel.writeString(iconURL)
parcel.writeString(description)
parcel.writeTypedList(hourly)
parcel.writeTypedList(forecast)
parcel.writeString(windSpeed)
parcel.writeString(windDirection)
parcel.writeString(precipitation)
parcel.writeString(humidity)
parcel.writeString(clouds)
parcel.writeDouble(lat)
parcel.writeDouble(lon)
parcel.writeString(displayName)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<WeatherDisplay> {
override fun createFromParcel(parcel: Parcel): WeatherDisplay {
return WeatherDisplay(parcel)
}
override fun newArray(size: Int): Array<WeatherDisplay?> {
return arrayOfNulls(size)
}
}
} }

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
data class Current( data class Current(
val dt: Int? = null, val dt: Int? = null,
@@ -20,9 +21,9 @@ data class Current(
val id: Int? = null, val id: Int? = null,
val humidity: Int? = null, val humidity: Int? = null,
val windSpeed: Double? = null val windSpeed: Double? = null
){ ) {
constructor(dailyItem: Current): this( constructor(dailyItem: Current) : this(
dailyItem.dt, dailyItem.dt,
dailyItem.sunrise, dailyItem.sunrise,
dailyItem.sunset, dailyItem.sunset,
@@ -34,7 +35,7 @@ data class Current(
dailyItem.feelsLike, dailyItem.feelsLike,
dailyItem.windDeg, dailyItem.windDeg,
dailyItem.dewPoint, dailyItem.dewPoint,
dailyItem.weather?.get(0)?.icon?.let { "https://openweathermap.org/img/wn/${it}@4x.png" }, generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon),
dailyItem.weather?.get(0)?.description, dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main, dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id, dailyItem.weather?.get(0)?.id,

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
data class DailyWeather( data class DailyWeather(
@@ -39,7 +40,7 @@ data class DailyWeather(
dailyItem.dewPoint, dailyItem.dewPoint,
dailyItem.windSpeed, dailyItem.windSpeed,
dailyItem.windDeg, dailyItem.windDeg,
dailyItem.weather?.get(0)?.icon?.let { "https://openweathermap.org/img/wn/${it}@4x.png" }, generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon),
dailyItem.weather?.get(0)?.description, dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main, dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id, dailyItem.weather?.get(0)?.id,

View File

@@ -1,5 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import android.os.Parcel
import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour
@@ -8,12 +10,38 @@ data class Hour(
val dt: Int? = null, val dt: Int? = null,
val temp: Double? = null, val temp: Double? = null,
val icon: String? = null val icon: String? = null
){ ): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Double::class.java.classLoader) as? Double,
parcel.readString()
) {
}
constructor(hour: ForecastHour) : this( constructor(hour: ForecastHour) : this(
hour.dt, hour.dt,
hour.temp, hour.temp,
generateIconUrlString(hour.weather?.getOrNull(0)?.icon) generateIconUrlString(hour.weather?.getOrNull(0)?.icon)
) )
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeValue(dt)
parcel.writeValue(temp)
parcel.writeString(icon)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Hour> {
override fun createFromParcel(parcel: Parcel): Hour {
return Hour(parcel)
}
override fun newArray(size: Int): Array<Hour?> {
return arrayOfNulls(size)
}
}
} }

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui package com.appttude.h_mal.monoWeather.ui
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
@@ -14,11 +14,11 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.utils.Event import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.utils.displayToast import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.hide import com.appttude.h_mal.atlas_weather.utils.hide
import com.appttude.h_mal.atlas_weather.utils.show import com.appttude.h_mal.atlas_weather.utils.show
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -27,7 +27,8 @@ import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import kotlin.properties.Delegates import kotlin.properties.Delegates
abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId), KodeinAware { abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId),
KodeinAware {
override val kodein by kodein() override val kodein by kodein()
val factory by instance<ApplicationViewModelFactory>() val factory by instance<ApplicationViewModelFactory>()
@@ -43,9 +44,11 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentL
// toggle visibility of progress spinner while async operations are taking place // toggle visibility of progress spinner while async operations are taking place
fun progressBarStateObserver(progressBar: View) = Observer<Event<Boolean>> { fun progressBarStateObserver(progressBar: View) = Observer<Event<Boolean>> {
when (it.getContentIfNotHandled()) { it.getContentIfNotHandled()?.let { i ->
true -> progressBar.fadeIn() if (i)
false -> progressBar.fadeOut() progressBar.fadeIn()
else
progressBar.fadeOut()
} }
} }
@@ -72,7 +75,11 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentL
permissionCode: Int, permissionCode: Int,
permissionGranted: () -> Unit permissionGranted: () -> Unit
) { ) {
if (ActivityCompat.checkSelfPermission(requireContext(), permission) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.checkSelfPermission(
requireContext(),
permission
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(arrayOf(permission), permissionCode) requestPermissions(arrayOf(permission), permissionCode)
return return
} else { } else {
@@ -123,11 +130,16 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentL
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) { override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_PERMISSION_REQUEST) { if (requestCode == LOCATION_PERMISSION_REQUEST) {
if (grantResults.isNotEmpty() && if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) { grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
permissionsGranted() permissionsGranted()
displayToast("Permission granted") displayToast("Permission granted")
} else { } else {

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui package com.appttude.h_mal.atlas_weather.ui
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@@ -11,7 +11,6 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.monoWeather.ui.settings.UnitSettingsActivity
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.main.activity_main_navigation.* import kotlinx.android.synthetic.main.activity_main_navigation.*
@@ -35,29 +34,17 @@ class MainActivity : AppCompatActivity() {
} }
private fun setupBottomBar(navView: BottomNavigationView, navController: NavController) { private fun setupBottomBar(navView: BottomNavigationView, navController: NavController) {
val tabs = setOf(R.id.nav_home, R.id.nav_world)
val appBarConfiguration = AppBarConfiguration(tabs) val appBarConfiguration = AppBarConfiguration(tabs)
setupActionBarWithNavController(navController, appBarConfiguration) setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController) navView.setupWithNavController(navController)
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will // Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long // automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml. // as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) { when (item.itemId) {
R.id.action_settings -> {
val i = Intent(this, UnitSettingsActivity::class.java)
startActivity(i)
return true
}
android.R.id.home -> onBackPressed() android.R.id.home -> onBackPressed()
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)

View File

@@ -4,9 +4,9 @@ package com.appttude.h_mal.atlas_weather.utils
fun generateIconUrlString(icon: String?): String?{ fun generateIconUrlString(icon: String?): String?{
return icon?.let { return icon?.let {
StringBuilder() StringBuilder()
.append("https://openweathermap.org/img/wn/") .append("http://openweathermap.org/img/wn/")
.append(it) .append(it)
.append("@4x.png") .append("@2x.png")
.toString() .toString()
} }
} }

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.atlas_weather.utils
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -10,7 +11,7 @@ import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.bumptech.glide.Glide import com.squareup.picasso.Picasso
fun View.show() { fun View.show() {
this.visibility = View.VISIBLE this.visibility = View.VISIBLE
@@ -33,15 +34,9 @@ fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater
.inflate(layoutId, this, false) .inflate(layoutId, this, false)
fun ImageView.loadImage(url: String?){ fun ImageView.loadImage(url: String?){
val c = Glide.with(this) Picasso.get().load(url)
.load(url) .placeholder(R.drawable.ic_baseline_cloud_queue_24)
viewTreeObserver.addOnPreDrawListener {
c.override(width, height)
true
}
c.placeholder(R.drawable.ic_baseline_cloud_queue_24)
.error(R.drawable.ic_baseline_cloud_off_24) .error(R.drawable.ic_baseline_cloud_off_24)
.fitCenter()
.into(this) .into(this)
} }

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
class ApplicationViewModelFactory( class ApplicationViewModelFactory(
private val locationProvider: LocationProvider, private val locationProvider: LocationProvider,
private val repository: RepositoryImpl private val repository: RepositoryImpl

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget package com.appttude.h_mal.atlas_weather.widget
import android.app.Activity import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent

View File

@@ -1,11 +1,10 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget package com.appttude.h_mal.atlas_weather.widget
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import com.appttude.h_mal.atlas_weather.widget.WidgetJobServiceIntent.Companion.enqueueWork
import com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetJobServiceIntent.Companion.enqueueWork
/** /**
* Implementation of App Widget functionality. * Implementation of App Widget functionality.

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget package com.appttude.h_mal.atlas_weather.widget
import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint import android.annotation.SuppressLint
@@ -13,12 +13,12 @@ import android.widget.RemoteViews
import android.os.Build import android.os.Build
import androidx.core.app.ActivityCompat.checkSelfPermission import androidx.core.app.ActivityCompat.checkSelfPermission
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.widget.WidgetState.*
import com.appttude.h_mal.atlas_weather.widget.WidgetState.Companion.getWidgetState
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetState.*
import com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetState.Companion.getWidgetState
import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable
import com.appttude.h_mal.atlas_weather.utils.tryOrNullSuspended import com.appttude.h_mal.atlas_weather.utils.tryOrNullSuspended
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget package com.appttude.h_mal.atlas_weather.widget
enum class WidgetState { enum class WidgetState {
NO_LOCATION, NO_LOCATION,

View File

@@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
tools:context="com.appttude.h_mal.atlas_weather.monoWeather.ui.world.AddLocationFragment"> tools:context="com.appttude.h_mal.atlas_weather.ui.world.AddLocationFragment">
<LinearLayout <LinearLayout

View File

@@ -29,7 +29,7 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/tabs_menu" /> app:menu="@menu/tabs_menu" />
<fragment <androidx.fragment.app.FragmentContainerView
android:id="@+id/container" android:id="@+id/container"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.appttude.h_mal.atlas_weather.monoWeather.ui.world.WorldFragment"> tools:context="com.appttude.h_mal.atlas_weather.ui.world.WorldFragment">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.appttude.h_mal.atlas_weather.monoWeather.ui.world.AddLocationFragment"> tools:context="com.appttude.h_mal.atlas_weather.ui.world.AddLocationFragment">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -26,7 +26,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@android:color/black" android:background="@android:color/black"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="gone">
<ProgressBar <ProgressBar
android:layout_gravity="center" android:layout_gravity="center"
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"
@@ -34,6 +34,4 @@
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</FrameLayout> </FrameLayout>
</RelativeLayout> </RelativeLayout>

View File

@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".legacy.ui.home.MainActivity"> tools:context=".legacy.ui.home.MainActivity">
<item <item
android:id="@+id/action_settings" android:id="@+id/settings_fragment"
android:orderInCategory="100" android:orderInCategory="100"
android:title="@string/action_settings" android:title="@string/action_settings"
android:icon="@drawable/ic_round_settings_24" android:icon="@drawable/ic_round_settings_24"

View File

@@ -27,4 +27,12 @@
<item name="android:textColor">#ffffff</item> <item name="android:textColor">#ffffff</item>
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
</style> </style>
<style name="icon_style__further_deatils">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:adjustViewBounds">true</item>
<item name="android:layout_gravity">center</item>
<item name="android:tint">@color/colorAccent</item>
</style>
</resources> </resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">openweathermap.org</domain>
</domain-config>
</network-security-config>

View File

@@ -2,9 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application <application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass" android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:allowBackup="true" android:allowBackup="true"
@@ -15,7 +12,7 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:node="merge"> tools:node="merge">
<activity <activity
android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity" android:name="com.appttude.h_mal.atlas_weather.ui.MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar" android:theme="@style/AppTheme.NoActionBar"
@@ -27,18 +24,15 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.settings.UnitSettingsActivity"
android:label="Settings" />
<activity android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity" <activity android:name="com.appttude.h_mal.monoWeather.ui.widget.WidgetLocationPermissionActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.NewAppWidget" <receiver android:name="com.appttude.h_mal.atlas_weather.widget.NewAppWidget"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -52,7 +46,7 @@
</receiver> </receiver>
<service <service
android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetJobServiceIntent" android:name="com.appttude.h_mal.atlas_weather.widget.WidgetJobServiceIntent"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:permission="android.permission.BIND_JOB_SERVICE" />
</application> </application>

View File

@@ -1,53 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.settings
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.preference.PreferenceActivity
import android.preference.PreferenceFragment
import androidx.preference.PreferenceManager
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.monoWeather.widget.NewAppWidget
class UnitSettingsActivity : PreferenceActivity() {
private var prefListener: OnSharedPreferenceChangeListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.prefs_screen, false)
fragmentManager.beginTransaction().replace(android.R.id.content, MyPreferenceFragment()).commit()
//listener on changed sort order preference:
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
prefListener = OnSharedPreferenceChangeListener { _, key ->
if (key == "temp_units") {
val intent = Intent(baseContext, NewAppWidget::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(application).getAppWidgetIds(ComponentName(application, NewAppWidget::class.java))
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
}
if (key == "widget_black_background"){
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
val widgetManager = AppWidgetManager.getInstance(this)
val ids = widgetManager.getAppWidgetIds(ComponentName(this, NewAppWidget::class.java))
AppWidgetManager.getInstance(this).notifyAppWidgetViewDataChanged(ids, R.id.whole_widget_view)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
}
}
prefs.registerOnSharedPreferenceChangeListener(prefListener)
}
class MyPreferenceFragment : PreferenceFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.prefs_screen)
}
}
}

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.atlas_weather.ui
import com.appttude.h_mal.atlas_weather.R
val tabs = setOf(R.id.nav_home, R.id.nav_world)

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.monoWeather.dialog
import android.content.Context
import android.content.res.Resources
import android.os.Build
import android.text.Html
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
interface DeclarationBuilder{
val link: String
val message: String
fun Context.readFromResources(@StringRes id: Int) = resources.getString(id)
fun buildMessage(): CharSequence? {
val link1 = "<font color='blue'><a href=\"$link\">here</a></font>"
val message = "$message See my privacy policy: $link1"
return Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY)
}
}

View File

@@ -0,0 +1,45 @@
package com.appttude.h_mal.monoWeather.dialog
import android.content.Context
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
class PermissionsDeclarationDialog(context: Context) : BaseDeclarationDialog(context) {
override val link: String = "https://sites.google.com/view/hmaldev/home/monochrome"
override val message: String = "Hi, thank you for downloading my app. Google play isn't letting me upload my app to the Playstore until I have a privacy declaration :(. My app is basically used to demonstrate my code=ing to potential employers and others. I do NOT store or process any information. The location permission in the app is there just to provide the end user with weather data."
}
abstract class BaseDeclarationDialog(val context: Context): DeclarationBuilder {
abstract override val link: String
abstract override val message: String
lateinit var dialog: AlertDialog
fun showDialog(agreeCallback: () -> Unit = { }, disagreeCallback: () -> Unit = { }) {
val myMessage = buildMessage()
val builder = AlertDialog.Builder(context)
.setPositiveButton("agree") { _, _ ->
agreeCallback()
}
.setNegativeButton("disagree") { _, _ ->
disagreeCallback()
}
.setMessage(myMessage)
.setCancelable(false)
dialog = builder.create()
dialog.show()
// Make the textview clickable. Must be called after show()
val msgTxt = dialog.findViewById<View>(android.R.id.message) as TextView?
msgTxt?.movementMethod = LinkMovementMethod.getInstance()
}
fun dismiss() = dialog.dismiss()
}

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui package com.appttude.h_mal.monoWeather.ui
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView

View File

@@ -1,14 +1,12 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui package com.appttude.h_mal.monoWeather.ui
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.WeatherRecyclerAdapter import com.appttude.h_mal.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.navigateTo import com.appttude.h_mal.atlas_weather.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.details package com.appttude.h_mal.monoWeather.ui.details
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View File

@@ -1,22 +1,21 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.home package com.appttude.h_mal.monoWeather.ui.home
import android.Manifest
import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.observe import androidx.navigation.Navigation.findNavController
import androidx.navigation.ui.onNavDestinationSelected
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
import com.appttude.h_mal.atlas_weather.monoWeather.dialog.PermissionsDeclarationDialog import com.appttude.h_mal.monoWeather.dialog.PermissionsDeclarationDialog
import com.appttude.h_mal.atlas_weather.monoWeather.ui.BaseFragment import com.appttude.h_mal.monoWeather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.WeatherRecyclerAdapter import com.appttude.h_mal.monoWeather.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.utils.navigateTo
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
@@ -35,6 +34,7 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
val recyclerAdapter = WeatherRecyclerAdapter(itemClick = { val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
navigateToFurtherDetails(it) navigateToFurtherDetails(it)
@@ -86,4 +86,14 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
.actionHomeFragmentToFurtherDetailsFragment(forecast) .actionHomeFragmentToFurtherDetailsFragment(forecast)
navigateTo(directions) navigateTo(directions)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate the menu; this adds items to the action bar if it is present.
inflater.inflate(R.menu.menu_main, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navController = findNavController(requireActivity(), R.id.container)
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
}
} }

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter package com.appttude.h_mal.monoWeather.ui.home.adapter
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@@ -17,7 +17,7 @@ class ViewHolderCurrent(listItemView: View) : RecyclerView.ViewHolder(listItemVi
var tempUnit: TextView = listItemView.findViewById(R.id.temp_unit_4) var tempUnit: TextView = listItemView.findViewById(R.id.temp_unit_4)
fun bindData(weather: WeatherDisplay?){ fun bindData(weather: WeatherDisplay?){
locationTV.text = weather?.location locationTV.text = weather?.displayName
conditionTV.text = weather?.description conditionTV.text = weather?.description
weatherIV.loadImage(weather?.iconURL) weatherIV.loadImage(weather?.iconURL)
avgTempTV.text = weather?.averageTemp?.toInt().toString() avgTempTV.text = weather?.averageTemp?.toInt().toString()

View File

@@ -1,14 +1,14 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter package com.appttude.h_mal.monoWeather.ui.home.adapter
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.monoWeather.ui.EmptyViewHolder import com.appttude.h_mal.monoWeather.ui.EmptyViewHolder
import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.forecast.ViewHolderForecast import com.appttude.h_mal.monoWeather.ui.home.adapter.forecast.ViewHolderForecast
import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily import com.appttude.h_mal.monoWeather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily
import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.further.ViewHolderFurtherDetails import com.appttude.h_mal.monoWeather.ui.home.adapter.further.ViewHolderFurtherDetails
import com.appttude.h_mal.atlas_weather.utils.generateView import com.appttude.h_mal.atlas_weather.utils.generateView
class WeatherRecyclerAdapter( class WeatherRecyclerAdapter(

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.forecast package com.appttude.h_mal.monoWeather.ui.home.adapter.forecast
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView

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