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
/local.properties
/.idea/workspace.xml
/.idea/libraries
/.idea/caches
.DS_Store
/build
/captures
.gradle/
build/
# Signing files
.signing/
# 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
/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-ui-ktx:2.3.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'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
// Unit testing
@@ -108,8 +109,8 @@ dependencies {
// android unit testing and espresso
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:rules:1.4.1-alpha06'
//mock websever for testing retrofit responses
testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
@@ -117,7 +118,6 @@ dependencies {
//mockito and livedata testing
testImplementation 'org.mockito:mockito-inline:2.13.0'
implementation 'android.arch.core:core-testing'
androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
// Mockk
def mockk_ver = "1.10.5"

View File

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

View File

@@ -1,5 +1,12 @@
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.rule.ActivityTestRule
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.Rule
open class BaseTest() {
open class BaseTest {
lateinit var testApp: TestAppClass
@@ -21,6 +28,12 @@ open class BaseTest() {
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) {
@@ -32,8 +45,7 @@ open class BaseTest() {
}
@After
fun tearDown() {
}
fun tearDown() {}
open fun setupFeed() {}
}

View File

@@ -1,6 +1,11 @@
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 com.appttude.h_mal.atlas_weather.monoWeather.robot.homeScreen
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
val idArray = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
return PendingIntent.getBroadcast(
context, seconds, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT)

View File

@@ -1,5 +1,6 @@
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.network.Api
@@ -26,14 +27,16 @@ const val LOCATION_PERMISSION_REQUEST = 505
class AppClass : BaseAppClass() {
override fun createNetworkModule(): Api {
override fun createNetworkModule(): WeatherApi {
return NetworkModule().invoke<WeatherApi>(
NetworkConnectionInterceptor(this),
QueryParamsInterceptor(),
loggingInterceptor
)
) as WeatherApi
}
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
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.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.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.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
@@ -31,11 +24,11 @@ abstract class BaseAppClass : Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this@BaseAppClass))
bind() from singleton { createNetworkModule() as WeatherApi}
bind() from singleton { createNetworkModule() }
bind() from singleton { createLocationModule() }
bind() from singleton { Gson() }
bind() from singleton { AppDatabase(instance()) }
bind() from singleton { createRoomDatabase() }
bind() from singleton { PreferenceProvider(instance()) }
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
bind() from singleton { SettingsRepositoryImpl(instance()) }
@@ -43,7 +36,8 @@ abstract class BaseAppClass : Application(), KodeinAware {
bind() from provider { ApplicationViewModelFactory(instance(), instance()) }
}
abstract fun createNetworkModule() : Api
abstract fun createNetworkModule(): WeatherApi
abstract fun createLocationModule(): LocationProvider
abstract fun createRoomDatabase(): AppDatabase
}

View File

@@ -24,5 +24,4 @@ class Converter(context: Context): KodeinAware{
return gson.fromJson(string, FullWeather::class.java)
}
}

View File

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

View File

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

View File

@@ -18,7 +18,8 @@
android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar">
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
@@ -30,13 +31,15 @@
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.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</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>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_ENABLED" />
@@ -48,9 +51,6 @@
android:resource="@xml/new_app_widget_info" />
</receiver>
<service
android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetJobServiceIntent"
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.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
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.displayToast
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 kotlin.properties.Delegates
abstract class BaseFragment() : Fragment(), KodeinAware {
abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId), KodeinAware {
override val kodein by kodein()
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.*
class WorldItemFragment : BaseFragment() {
class WorldItemFragment : BaseFragment(R.layout.fragment_home) {
private val viewModel by getFragmentViewModel<WorldViewModel>()
private var param1: String? = null
@@ -24,12 +24,6 @@ class WorldItemFragment : BaseFragment() {
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?) {
super.onViewCreated(view, savedInstanceState)

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.home
import android.Manifest
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
@@ -9,9 +10,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
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.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.home.adapter.WeatherRecyclerAdapter
import com.appttude.h_mal.atlas_weather.utils.displayToast
@@ -24,39 +26,29 @@ import kotlinx.android.synthetic.main.fragment_home.*
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
class HomeFragment : BaseFragment() {
class HomeFragment : BaseFragment(R.layout.fragment_home) {
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")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerAdapter = WeatherRecyclerAdapter {
val directions =
HomeFragmentDirections.actionHomeFragmentToFurtherDetailsFragment(it)
navigateTo(directions)
}
val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
navigateToFurtherDetails(it)
})
forecast_listview.apply {
layoutManager = LinearLayoutManager(context)
adapter = recyclerAdapter
}
forecast_listview.adapter = recyclerAdapter
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
PermissionsDeclarationDialog(requireContext()).showDialog(agreeCallback = {
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
viewModel.fetchData()
}
})
swipe_refresh.apply {
setOnRefreshListener {
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
viewModel.fetchData()
isRefreshing = true
}
@@ -73,16 +65,13 @@ class HomeFragment : BaseFragment() {
}
@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) {
override fun permissionsGranted() {
viewModel.fetchData()
displayToast("Permission granted")
} else {
displayToast("Permission denied")
}
}
}
private fun navigateToFurtherDetails(forecast: Forecast){
val directions = HomeFragmentDirections
.actionHomeFragmentToFurtherDetailsFragment(forecast)
navigateTo(directions)
}
}

View File

@@ -6,20 +6,28 @@ import android.appwidget.AppWidgetManager.*
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat.checkSelfPermission
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 kotlinx.android.synthetic.monoWeather.permissions_declaration_dialog.*
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
message = readFromResources(R.string.widget_declaration)
// 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.
setResult(RESULT_CANCELED)
@@ -36,6 +44,10 @@ class WidgetLocationPermissionActivity : AppCompatActivity() {
}
setContentView(R.layout.permissions_declaration_dialog)
findViewById<TextView>(R.id.declaration_text).apply {
text = buildMessage()
movementMethod = LinkMovementMethod.getInstance()
}
submit.setOnClickListener {
if (checkSelfPermission(this, ACCESS_COARSE_LOCATION) != PERMISSION_GRANTED) {
@@ -44,6 +56,8 @@ class WidgetLocationPermissionActivity : AppCompatActivity() {
submitWidget()
}
}
cancel.setOnClickListener { finish() }
}
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.*
class AddLocationFragment : BaseFragment() {
class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
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?) {
super.onViewCreated(view, savedInstanceState)

View File

@@ -22,7 +22,7 @@ import kotlinx.android.synthetic.main.fragment_add_location.world_recycler
* A simple [Fragment] subclass.
* create an instance of this fragment.
*/
class WorldFragment : BaseFragment() {
class WorldFragment : BaseFragment(R.layout.fragment__two) {
private val viewModel by getFragmentViewModel<WorldViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -31,12 +31,6 @@ class WorldFragment : BaseFragment() {
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?) {
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
class WorldRecyclerAdapter(
val itemClick: (WeatherDisplay) -> Unit,
val itemLongClick: (String) -> Unit
private val itemClick: (WeatherDisplay) -> Unit,
private val itemLongClick: (String) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var weather: MutableList<WeatherDisplay> = mutableListOf()
@@ -78,23 +78,28 @@ class WorldRecyclerAdapter(
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)
var conditionTV: TextView = listItemView.findViewById(R.id.db_condition)
var weatherIV: ImageView = listItemView.findViewById(R.id.db_icon)
var avgTempTV: TextView = listItemView.findViewById(R.id.db_main_temp)
var tempUnit: TextView = listItemView.findViewById(R.id.db_temp_unit)
private val locationTV: TextView = cellView.findViewById(R.id.db_location)
private val conditionTV: TextView = cellView.findViewById(R.id.db_condition)
private val weatherIV: ImageView = cellView.findViewById(R.id.db_icon)
private val avgTempTV: TextView = cellView.findViewById(R.id.db_main_temp)
private val tempUnit: TextView = cellView.findViewById(R.id.db_temp_unit)
fun bindData(weather: WeatherDisplay?){
locationTV.text = weather?.displayName
conditionTV.text = weather?.description
weatherIV.loadImage(weather?.iconURL)
avgTempTV.text = weather?.forecast?.get(0)?.mainTemp
override fun bindData(data: WeatherDisplay?){
locationTV.text = data?.displayName
conditionTV.text = data?.description
weatherIV.loadImage(data?.iconURL)
avgTempTV.text = data?.forecast?.get(0)?.mainTemp
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,24 +2,21 @@ package com.appttude.h_mal.atlas_weather.monoWeather.widget
import android.app.Activity
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.TaskStackBuilder
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import android.widget.RemoteViews
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.core.app.JobIntentService
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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() {
@@ -45,9 +42,12 @@ abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentSer
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val idArray = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
return PendingIntent.getBroadcast(
this, seconds, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
/**
@@ -59,7 +59,7 @@ abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentSer
return TaskStackBuilder.create(this)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
}
fun setImageView(

View File

@@ -2,12 +2,12 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:paddingBottom="@dimen/activity_vertical_margin">
<ImageView
android:id="@+id/imageView"
@@ -19,33 +19,48 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2"
app:srcCompat="@drawable/location_permission_decl" />
app:srcCompat="@drawable/cloud_symbol" />
<TextView
android:id="@+id/declaration_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="64dp"
android:layout_marginRight="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView"
android:layout_marginLeft="64dp"
android:layout_marginRight="64dp"
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
android:id="@+id/submit"
android:layout_width="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_constraintHorizontal_bias="0.9"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.85"
app:layout_constraintHorizontal_bias="0.9"
app:layout_constraintVertical_bias="0.85" />
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/white"
android:text="@string/cancel"
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>

View File

@@ -8,5 +8,6 @@
<string name="title_home">Home</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>