Playstore permissions declaration (#12)

* Declaration builder created

Took 2 hours 34 minutes


Took 2 minutes

* - Fixed android S issues
 - Dialog box completed
 - Widget creation activity UI test added

Took 1 hour 53 minutes

* - Popup for google playstore permissions added
- UI tests with stubbing added
- linting clean ups
- changes to fragments and base fragment

Took 3 hours 57 minutes
This commit is contained in:
2022-06-09 23:35:57 +01:00
committed by GitHub
parent d7534d585e
commit f7244ee015
31 changed files with 449 additions and 229 deletions

97
.gitignore vendored
View File

@@ -1,11 +1,90 @@
*.iml ### AndroidStudio ###
# Covers files to be ignored for android development using Android Studio.
# Built application files
*.apk
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle .gradle
/local.properties .gradle/
/.idea/workspace.xml build/
/.idea/libraries
/.idea/caches # Signing files
.DS_Store .signing/
/build
/captures # Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio
/*/build/
/*/local.properties
/*/out
/*/*/build
/*/*/production
captures/
.navigation/
*.ipr
*~
*.swp
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Android Patch
gen-external-apklibs
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild .externalNativeBuild
/projectFilesBackup
# IntelliJ IDEA
*.iml
*.iws
/out/
# User-specific configurations
.idea/caches/
.idea/libraries/
.idea/shelf/
.idea/workspace.xml
.idea/tasks.xml
.idea/.name
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/encodings.xml
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
.idea/datasources.xml
.idea/dataSources.ids
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
.idea/assetWizardSettings.xml
.idea/gradle.xml
.idea/jarRepositorie

Binary file not shown.

20
.idea/gradle.xml generated
View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

48
.idea/misc.xml generated
View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="12">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="7" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="11">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

11
.idea/modules.xml generated
View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Altas_-_Weather.iml" filepath="$PROJECT_DIR$/.idea/modules/Altas_-_Weather.iml" group="Altas_-_Weather" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Altas_-_Weather-app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Altas_-_Weather-app.iml" group="Altas_-_Weather/app" />
<module fileurl="file://$PROJECT_DIR$/Weather_app.iml" filepath="$PROJECT_DIR$/Weather_app.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@@ -97,6 +97,7 @@ dependencies {
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
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'
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
@@ -108,8 +109,8 @@ dependencies {
// android unit testing and espresso // android unit testing and espresso
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
implementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test:rules:1.4.1-alpha06'
//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"
@@ -117,7 +118,6 @@ dependencies {
//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 'android.arch.core:core-testing' implementation 'android.arch.core:core-testing'
androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
// Mockk // Mockk
def mockk_ver = "1.10.5" def mockk_ver = "1.10.5"

View File

@@ -1,16 +1,17 @@
package com.appttude.h_mal.atlas_weather.application package com.appttude.h_mal.atlas_weather.application
import androidx.room.Room
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.espresso.idling.CountingIdlingResource
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.data.location.MockLocationProvider import com.appttude.h_mal.atlas_weather.data.location.MockLocationProvider
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.MockingNetworkInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.MockingNetworkInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.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.room.AppDatabase
import com.appttude.h_mal.atlas_weather.data.room.Converter
import java.io.BufferedReader import java.io.BufferedReader
class TestAppClass : BaseAppClass() { class TestAppClass : BaseAppClass() {
@@ -22,17 +23,23 @@ class TestAppClass : BaseAppClass() {
IdlingRegistry.getInstance().register(idlingResources) IdlingRegistry.getInstance().register(idlingResources)
} }
override fun createNetworkModule(): Api { override fun createNetworkModule(): WeatherApi {
return NetworkModule().invoke<WeatherApi>( return NetworkModule().invoke<WeatherApi>(
mockingNetworkInterceptor,
NetworkConnectionInterceptor(this), NetworkConnectionInterceptor(this),
QueryParamsInterceptor(), QueryParamsInterceptor(),
loggingInterceptor, loggingInterceptor
mockingNetworkInterceptor ) as WeatherApi
)
} }
override fun createLocationModule() = MockLocationProvider() override fun createLocationModule() = MockLocationProvider()
override fun createRoomDatabase(): AppDatabase {
return Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java)
.addTypeConverter(Converter(this))
.build()
}
fun stubUrl(url: String, rawPath: String) { fun stubUrl(url: String, rawPath: String) {
val id = resources.getIdentifier(rawPath, "raw", packageName) val id = resources.getIdentifier(rawPath, "raw", packageName)
val iStream = resources.openRawResource(id) val iStream = resources.openRawResource(id)
@@ -40,7 +47,7 @@ class TestAppClass : BaseAppClass() {
mockingNetworkInterceptor.addUrlStub(url = url, data = data) mockingNetworkInterceptor.addUrlStub(url = url, data = data)
} }
fun removeUrlStub(url: String){ fun removeUrlStub(url: String) {
mockingNetworkInterceptor.removeUrlStub(url = url) mockingNetworkInterceptor.removeUrlStub(url = url)
} }

View File

@@ -1,5 +1,12 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
import android.content.Intent
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass import com.appttude.h_mal.atlas_weather.application.TestAppClass
@@ -8,7 +15,7 @@ import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.After import org.junit.After
import org.junit.Rule import org.junit.Rule
open class BaseTest() { open class BaseTest {
lateinit var testApp: TestAppClass lateinit var testApp: TestAppClass
@@ -21,6 +28,12 @@ open class BaseTest() {
testApp = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass testApp = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
setupFeed() setupFeed()
} }
override fun afterActivityLaunched() {
// Dismiss dialog
onView(withText("AGREE")).inRoot(isDialog()).check(matches(isDisplayed())).perform(ViewActions.click())
}
} }
fun stubEndpoint(url: String, stub: Stubs) { fun stubEndpoint(url: String, stub: Stubs) {
@@ -32,8 +45,7 @@ open class BaseTest() {
} }
@After @After
fun tearDown() { fun tearDown() {}
}
open fun setupFeed() {} open fun setupFeed() {}
} }

View File

@@ -1,6 +1,11 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
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.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.monoWeather.robot.homeScreen import com.appttude.h_mal.atlas_weather.monoWeather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs

View File

@@ -0,0 +1,85 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
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.monoWeather.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() {
@Rule
@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)
}
@Test
fun loadApp_validWeatherResponse_returnsValidPage() {
homeScreen {
verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location")
}
}
}
open class BaseMainScenario {
lateinit var scenario: ActivityScenario<MainActivity>
private lateinit var testApp : TestAppClass
@Before
fun setUp() {
scenario = launch(MainActivity::class.java)
scenario.moveToState(Lifecycle.State.INITIALIZED)
scenario.onActivity {
runBlocking {
testApp = it.application as TestAppClass
setupFeed()
}
}
scenario.moveToState(Lifecycle.State.CREATED).onActivity {
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() {}
open fun setupFeed() {}
}

View File

@@ -0,0 +1,29 @@
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>(WidgetLocationPermissionActivity::class.java, false, false)
@Test
fun demo_test() {
val i = Intent()
i.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 112)
mActivityTestRule.launchActivity(i)
Espresso.onView((ViewMatchers.withId(R.id.declaration_text))).check(matches(isDisplayed()));
}
}

View File

@@ -22,6 +22,7 @@ abstract class BaseWidgetClass : AppWidgetProvider(){
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val idArray = intArrayOf(appWidgetId) val idArray = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray) intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
return PendingIntent.getBroadcast( return PendingIntent.getBroadcast(
context, seconds, intentUpdate, context, seconds, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT) PendingIntent.FLAG_UPDATE_CURRENT)

View File

@@ -1,5 +1,6 @@
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.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.Api
@@ -26,14 +27,16 @@ const val LOCATION_PERMISSION_REQUEST = 505
class AppClass : BaseAppClass() { class AppClass : BaseAppClass() {
override fun createNetworkModule(): Api { override fun createNetworkModule(): WeatherApi {
return NetworkModule().invoke<WeatherApi>( return NetworkModule().invoke<WeatherApi>(
NetworkConnectionInterceptor(this), NetworkConnectionInterceptor(this),
QueryParamsInterceptor(), QueryParamsInterceptor(),
loggingInterceptor loggingInterceptor
) ) as WeatherApi
} }
override fun createLocationModule() = LocationProviderImpl(this) override fun createLocationModule() = LocationProviderImpl(this)
override fun createRoomDatabase(): AppDatabase = AppDatabase(this)
} }

View File

@@ -1,15 +1,8 @@
package com.appttude.h_mal.atlas_weather.application package com.appttude.h_mal.atlas_weather.application
import android.app.Application import android.app.Application
import androidx.test.espresso.idling.CountingIdlingResource
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.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.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.QueryParamsInterceptor
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.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
@@ -31,11 +24,11 @@ abstract class BaseAppClass : Application(), KodeinAware {
override val kodein = Kodein.lazy { override val kodein = Kodein.lazy {
import(androidXModule(this@BaseAppClass)) import(androidXModule(this@BaseAppClass))
bind() from singleton { createNetworkModule() as WeatherApi} bind() from singleton { createNetworkModule() }
bind() from singleton { createLocationModule() } bind() from singleton { createLocationModule() }
bind() from singleton { Gson() } bind() from singleton { Gson() }
bind() from singleton { AppDatabase(instance()) } bind() from singleton { createRoomDatabase() }
bind() from singleton { PreferenceProvider(instance()) } bind() from singleton { PreferenceProvider(instance()) }
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) } bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
bind() from singleton { SettingsRepositoryImpl(instance()) } bind() from singleton { SettingsRepositoryImpl(instance()) }
@@ -43,7 +36,8 @@ abstract class BaseAppClass : Application(), KodeinAware {
bind() from provider { ApplicationViewModelFactory(instance(), instance()) } bind() from provider { ApplicationViewModelFactory(instance(), instance()) }
} }
abstract fun createNetworkModule() : Api abstract fun createNetworkModule(): WeatherApi
abstract fun createLocationModule() : LocationProvider abstract fun createLocationModule(): LocationProvider
abstract fun createRoomDatabase(): AppDatabase
} }

View File

@@ -10,19 +10,18 @@ import org.kodein.di.android.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
@ProvidedTypeConverter @ProvidedTypeConverter
class Converter(context: Context): KodeinAware{ class Converter(context: Context) : KodeinAware {
override val kodein by kodein(context) override val kodein by kodein(context)
private val gson by instance<Gson>() private val gson by instance<Gson>()
@TypeConverter @TypeConverter
fun fullWeatherToString(fullWeather: FullWeather): String{ fun fullWeatherToString(fullWeather: FullWeather): String {
return gson.toJson(fullWeather) return gson.toJson(fullWeather)
} }
@TypeConverter @TypeConverter
fun stringToFullWeather(string: String): FullWeather{ fun stringToFullWeather(string: String): FullWeather {
return gson.fromJson(string, FullWeather::class.java) return gson.fromJson(string, FullWeather::class.java)
} }
} }

View File

@@ -2,7 +2,8 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -13,6 +14,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/forecast_listview" android:id="@+id/forecast_listview"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/db_list_item"> tools:listitem="@layout/db_list_item">
</androidx.recyclerview.widget.RecyclerView> </androidx.recyclerview.widget.RecyclerView>

View File

@@ -14,4 +14,6 @@
<string name="min">Min:</string> <string name="min">Min:</string>
<string name="max">Max:</string> <string name="max">Max:</string>
<string name="average">Average:</string> <string name="average">Average:</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
</resources> </resources>

View File

@@ -18,7 +18,8 @@
android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity" android:name="com.appttude.h_mal.atlas_weather.monoWeather.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"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -30,13 +31,15 @@
android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.settings.UnitSettingsActivity" android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.settings.UnitSettingsActivity"
android:label="Settings" /> android:label="Settings" />
<activity android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity"> <activity android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity"
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.monoWeather.widget.NewAppWidget"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_ENABLED" /> <action android:name="android.appwidget.action.APPWIDGET_ENABLED" />
@@ -48,9 +51,6 @@
android:resource="@xml/new_app_widget_info" /> android:resource="@xml/new_app_widget_info" />
</receiver> </receiver>
<service
android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service <service
android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetJobServiceIntent" android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetJobServiceIntent"
android:exported="true" android:exported="true"

View File

@@ -0,0 +1,23 @@
package com.appttude.h_mal.atlas_weather.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)
@RequiresApi(Build.VERSION_CODES.N)
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,44 @@
package com.appttude.h_mal.atlas_weather.monoWeather.dialog
import android.content.Context
import android.os.Build
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import com.appttude.h_mal.atlas_weather.R
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
fun showDialog(agreeCallback: () -> Unit = { }, disagreeCallback: () -> Unit = { Unit }) {
val myMessage = buildMessage()
val builder = AlertDialog.Builder(context)
.setPositiveButton("agree") { _, _ ->
agreeCallback()
}
.setNegativeButton("disagree") { _, _ ->
disagreeCallback()
}
.setMessage(myMessage)
.setCancelable(false)
val alertDialog = builder.create()
alertDialog.show()
// Make the textview clickable. Must be called after show()
val msgTxt = alertDialog.findViewById<View>(android.R.id.message) as TextView?
msgTxt?.movementMethod = LinkMovementMethod.getInstance()
}
}

View File

@@ -2,15 +2,18 @@ package com.appttude.h_mal.atlas_weather.monoWeather.ui
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.LayoutRes
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer 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.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
@@ -24,7 +27,7 @@ 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() : Fragment(), 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>()
@@ -119,4 +122,21 @@ abstract class BaseFragment() : Fragment(), KodeinAware {
} }
} }
@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) {
permissionsGranted()
displayToast("Permission granted")
} else {
permissionsRefused()
displayToast("Permission denied")
}
}
}
open fun permissionsGranted() {}
open fun permissionsRefused() {}
} }

View File

@@ -14,7 +14,7 @@ import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
class WorldItemFragment : BaseFragment() { class WorldItemFragment : BaseFragment(R.layout.fragment_home) {
private val viewModel by getFragmentViewModel<WorldViewModel>() private val viewModel by getFragmentViewModel<WorldViewModel>()
private var param1: String? = null private var param1: String? = null
@@ -24,12 +24,6 @@ class WorldItemFragment : BaseFragment() {
param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
} }
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)
}
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,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.home package com.appttude.h_mal.atlas_weather.monoWeather.ui.home
import android.Manifest import android.Manifest
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
@@ -9,9 +10,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.observe import androidx.lifecycle.observe
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.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.monoWeather.dialog.PermissionsDeclarationDialog
import com.appttude.h_mal.atlas_weather.monoWeather.ui.BaseFragment import com.appttude.h_mal.atlas_weather.monoWeather.ui.BaseFragment
import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.WeatherRecyclerAdapter import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.displayToast import com.appttude.h_mal.atlas_weather.utils.displayToast
@@ -24,39 +26,29 @@ import kotlinx.android.synthetic.main.fragment_home.*
* A simple [Fragment] subclass. * A simple [Fragment] subclass.
* create an instance of this fragment. * create an instance of this fragment.
*/ */
class HomeFragment : BaseFragment() { class HomeFragment : BaseFragment(R.layout.fragment_home) {
private val viewModel by getFragmentViewModel<MainViewModel>() private val viewModel by getFragmentViewModel<MainViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
@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)
val recyclerAdapter = WeatherRecyclerAdapter { val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
val directions = navigateToFurtherDetails(it)
HomeFragmentDirections.actionHomeFragmentToFurtherDetailsFragment(it) })
navigateTo(directions)
}
forecast_listview.apply { forecast_listview.adapter = recyclerAdapter
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
}
PermissionsDeclarationDialog(requireContext()).showDialog(agreeCallback = {
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){ getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
viewModel.fetchData() viewModel.fetchData()
} }
})
swipe_refresh.apply { swipe_refresh.apply {
setOnRefreshListener { setOnRefreshListener {
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){ getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
viewModel.fetchData() viewModel.fetchData()
isRefreshing = true isRefreshing = true
} }
@@ -73,16 +65,13 @@ class HomeFragment : BaseFragment() {
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) { override fun permissionsGranted() {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) viewModel.fetchData()
if (requestCode == LOCATION_PERMISSION_REQUEST) { }
if (grantResults.isNotEmpty()
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { private fun navigateToFurtherDetails(forecast: Forecast){
viewModel.fetchData() val directions = HomeFragmentDirections
displayToast("Permission granted") .actionHomeFragmentToFurtherDetailsFragment(forecast)
} else { navigateTo(directions)
displayToast("Permission denied")
}
}
} }
} }

View File

@@ -6,20 +6,28 @@ import android.appwidget.AppWidgetManager.*
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
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.monoWeather.dialog.DeclarationBuilder
import com.appttude.h_mal.atlas_weather.utils.displayToast import com.appttude.h_mal.atlas_weather.utils.displayToast
import kotlinx.android.synthetic.monoWeather.permissions_declaration_dialog.* import kotlinx.android.synthetic.monoWeather.permissions_declaration_dialog.*
const val PERMISSION_CODE = 401 const val PERMISSION_CODE = 401
class WidgetLocationPermissionActivity : AppCompatActivity() { class WidgetLocationPermissionActivity : AppCompatActivity(), DeclarationBuilder {
override val link: String = "https://sites.google.com/view/hmaldev/home/monochrome"
override var message: String = ""
private var mAppWidgetId = INVALID_APPWIDGET_ID private var mAppWidgetId = INVALID_APPWIDGET_ID
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
message = readFromResources(R.string.widget_declaration)
// Set the result to CANCELED. This will cause the widget host to cancel // Set the result to CANCELED. This will cause the widget host to cancel
// out of the widget placement if the user presses the back button. // out of the widget placement if the user presses the back button.
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
@@ -36,6 +44,10 @@ class WidgetLocationPermissionActivity : AppCompatActivity() {
} }
setContentView(R.layout.permissions_declaration_dialog) setContentView(R.layout.permissions_declaration_dialog)
findViewById<TextView>(R.id.declaration_text).apply {
text = buildMessage()
movementMethod = LinkMovementMethod.getInstance()
}
submit.setOnClickListener { submit.setOnClickListener {
if (checkSelfPermission(this, ACCESS_COARSE_LOCATION) != PERMISSION_GRANTED) { if (checkSelfPermission(this, ACCESS_COARSE_LOCATION) != PERMISSION_GRANTED) {
@@ -44,6 +56,8 @@ class WidgetLocationPermissionActivity : AppCompatActivity() {
submitWidget() submitWidget()
} }
} }
cancel.setOnClickListener { finish() }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {

View File

@@ -14,16 +14,10 @@ import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
import kotlinx.android.synthetic.main.activity_add_forecast.* import kotlinx.android.synthetic.main.activity_add_forecast.*
class AddLocationFragment : BaseFragment() { class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
private val viewModel by getFragmentViewModel<WorldViewModel>() 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

@@ -22,7 +22,7 @@ import kotlinx.android.synthetic.main.fragment_add_location.world_recycler
* A simple [Fragment] subclass. * A simple [Fragment] subclass.
* create an instance of this fragment. * create an instance of this fragment.
*/ */
class WorldFragment : BaseFragment() { class WorldFragment : BaseFragment(R.layout.fragment__two) {
private val viewModel by getFragmentViewModel<WorldViewModel>() private val viewModel by getFragmentViewModel<WorldViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -31,12 +31,6 @@ class WorldFragment : BaseFragment() {
viewModel.fetchAllLocations() viewModel.fetchAllLocations()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment__two, 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

@@ -12,8 +12,8 @@ import com.appttude.h_mal.atlas_weather.utils.generateView
import com.appttude.h_mal.atlas_weather.utils.loadImage import com.appttude.h_mal.atlas_weather.utils.loadImage
class WorldRecyclerAdapter( class WorldRecyclerAdapter(
val itemClick: (WeatherDisplay) -> Unit, private val itemClick: (WeatherDisplay) -> Unit,
val itemLongClick: (String) -> Unit private val itemLongClick: (String) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var weather: MutableList<WeatherDisplay> = mutableListOf() var weather: MutableList<WeatherDisplay> = mutableListOf()
@@ -78,23 +78,28 @@ class WorldRecyclerAdapter(
return if (weather.size == 0) 1 else weather.size return if (weather.size == 0) 1 else weather.size
} }
internal class WorldHolderCurrent(listItemView: View) : RecyclerView.ViewHolder(listItemView) { internal class WorldHolderCurrent(cellView: View) : BaseViewHolder<WeatherDisplay>(cellView) {
var locationTV: TextView = listItemView.findViewById(R.id.db_location) private val locationTV: TextView = cellView.findViewById(R.id.db_location)
var conditionTV: TextView = listItemView.findViewById(R.id.db_condition) private val conditionTV: TextView = cellView.findViewById(R.id.db_condition)
var weatherIV: ImageView = listItemView.findViewById(R.id.db_icon) private val weatherIV: ImageView = cellView.findViewById(R.id.db_icon)
var avgTempTV: TextView = listItemView.findViewById(R.id.db_main_temp) private val avgTempTV: TextView = cellView.findViewById(R.id.db_main_temp)
var tempUnit: TextView = listItemView.findViewById(R.id.db_temp_unit) private val tempUnit: TextView = cellView.findViewById(R.id.db_temp_unit)
fun bindData(weather: WeatherDisplay?){ override fun bindData(data: WeatherDisplay?){
locationTV.text = weather?.displayName locationTV.text = data?.displayName
conditionTV.text = weather?.description conditionTV.text = data?.description
weatherIV.loadImage(weather?.iconURL) weatherIV.loadImage(data?.iconURL)
avgTempTV.text = weather?.forecast?.get(0)?.mainTemp avgTempTV.text = data?.forecast?.get(0)?.mainTemp
tempUnit.text = itemView.context.getString(R.string.degrees) tempUnit.text = itemView.context.getString(R.string.degrees)
} }
} }
abstract class BaseViewHolder<T : Any>(cellView: View) : RecyclerView.ViewHolder(cellView) {
abstract fun bindData(data : T?)
}
} }

View File

@@ -2,31 +2,28 @@ package com.appttude.h_mal.atlas_weather.monoWeather.widget
import android.app.Activity import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.core.app.JobIntentService import androidx.core.app.JobIntentService
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentService(){ abstract class BaseWidgetServiceIntentClass<T : AppWidgetProvider> : JobIntentService() {
lateinit var appWidgetManager: AppWidgetManager lateinit var appWidgetManager: AppWidgetManager
lateinit var appWidgetIds: IntArray lateinit var appWidgetIds: IntArray
fun initBaseWidget(componentName: ComponentName){ fun initBaseWidget(componentName: ComponentName) {
appWidgetManager = AppWidgetManager.getInstance(baseContext) appWidgetManager = AppWidgetManager.getInstance(baseContext)
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName) appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
} }
@@ -45,21 +42,24 @@ abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentSer
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val idArray = intArrayOf(appWidgetId) val idArray = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray) intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
return PendingIntent.getBroadcast(
this, seconds, intentUpdate, return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT) PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT)
}
} }
/** /**
* create a pending intent used to navigate to activity: * create a pending intent used to navigate to activity:
* @param activityClass * @param activityClass
*/ */
fun <T: Activity> createClickingPendingIntent(activityClass: Class<T>): PendingIntent { fun <T : Activity> createClickingPendingIntent(activityClass: Class<T>): PendingIntent {
val clickIntentTemplate = Intent(this, activityClass) val clickIntentTemplate = Intent(this, activityClass)
return TaskStackBuilder.create(this) return TaskStackBuilder.create(this)
.addNextIntentWithParentStack(clickIntentTemplate) .addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
} }
fun setImageView( fun setImageView(
@@ -67,7 +67,7 @@ abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentSer
views: RemoteViews, views: RemoteViews,
@IdRes viewId: Int, @IdRes viewId: Int,
appWidgetId: Int appWidgetId: Int
){ ) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
Picasso.get().load(path).into(views, viewId, intArrayOf(appWidgetId)) Picasso.get().load(path).into(views, viewId, intArrayOf(appWidgetId))
} }

View File

@@ -2,12 +2,12 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin">
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView <ImageView
android:id="@+id/imageView" android:id="@+id/imageView"
@@ -19,33 +19,48 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2" app:layout_constraintVertical_bias="0.2"
app:srcCompat="@drawable/location_permission_decl" /> app:srcCompat="@drawable/cloud_symbol" />
<TextView <TextView
android:id="@+id/declaration_text"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="64dp"
android:layout_marginRight="64dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView" app:layout_constraintTop_toBottomOf="@id/imageView"
android:layout_marginLeft="64dp"
android:layout_marginRight="64dp"
app:layout_constraintVertical_bias="0.2" app:layout_constraintVertical_bias="0.2"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."/> tools:text="@string/widget_declaration" />
<Button <Button
android:id="@+id/submit" android:id="@+id/submit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:backgroundTint="@android:color/white"
android:text="@string/ok"
android:textColor="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.9"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.85" app:layout_constraintVertical_bias="0.85" />
app:layout_constraintHorizontal_bias="0.9"
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/white" android:backgroundTint="@android:color/white"
android:text="@string/cancel"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:text="Ok" /> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.85" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -8,5 +8,6 @@
<string name="title_home">Home</string> <string name="title_home">Home</string>
<string name="title_world">World</string> <string name="title_world">World</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="widget_declaration">When using the app widget with app will require the use of background location services. Just to confirm the location data is used just to provide the end user with widget data. The use of background location permission is to update the widget at 30 minute intervals. </string>
</resources> </resources>