Commit 885f7847 authored by Javinator9889's avatar Javinator9889

Merge branch 'development' into 'master'

Development

See merge request Javinator9889/handwashing-reminder!8
parents a6aef515 e5a814b3
......@@ -28,7 +28,10 @@ Currently, the application supports:
+ Send notifications at specific time. This feature was developed using
the Android's
[WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager).
[WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
but because the notifications were not working as expected (they were almost
always delayed) the app now uses AlarmManager for waking the device at
specific time, even if it is in Doze mode.
+ Detect user activity and send a notification when ends an specific
one. For example, if he gets out of a vehicle, or has just finished
running, etc. This was developed using Google's [Activity Recognition
......
......@@ -42,7 +42,7 @@ android {
applicationId "com.javinator9889.handwashingreminder"
minSdkVersion 17
targetSdkVersion 29
versionCode 118
versionCode 121
versionName "1.1.2-${gitCommitHash}"
multiDexEnabled true
resConfigs "en", "es"
......@@ -78,6 +78,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
multiDexEnabled = true
signingConfig signingConfigs.release
versionNameSuffix "-stable"
debuggable false
jniDebuggable false
......@@ -171,7 +172,7 @@ dependencies {
// https://developer.android.com/jetpack/androidx/releases/lifecycle#declaring_dependencies
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
// https://developer.android.com/reference/kotlin/androidx/preference/package-summary
implementation 'androidx.preference:preference:1.1.1'
api 'androidx.preference:preference-ktx:1.1.1'
// https://github.com/bumptech/glide
api 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
......
......@@ -20,6 +20,7 @@ package com.javinator9889.handwashingreminder.activities
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.animation.Animation
import android.view.animation.AnimationUtils
......@@ -27,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenCreated
import androidx.lifecycle.whenStarted
import androidx.preference.PreferenceManager
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.play.core.splitcompat.SplitCompat
......@@ -41,7 +43,6 @@ import com.javinator9889.handwashingreminder.application.HandwashingApplication
import com.javinator9889.handwashingreminder.emoji.EmojiLoader
import com.javinator9889.handwashingreminder.gms.ads.AdLoader
import com.javinator9889.handwashingreminder.gms.ads.AdsEnabler
import com.javinator9889.handwashingreminder.gms.vendor.BillingService
import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler
import com.javinator9889.handwashingreminder.utils.*
import com.javinator9889.handwashingreminder.utils.Preferences.Companion.ADS_ENABLED
......@@ -65,13 +66,16 @@ class LauncherActivity : AppCompatActivity() {
private var launchOnInstall = false
private var launchFromNotification = false
private var canFinishActivity = false
private lateinit var sharedPreferences: SharedPreferences
private lateinit var app: HandwashingApplication
private lateinit var initDeferred: Deferred<Unit>
init {
lifecycleScope.launch {
whenCreated {
app = HandwashingApplication.getInstance()
app = HandwashingApplication.instance
sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this@LauncherActivity)
with(intent) {
notNull {
launchFromNotification =
......@@ -141,7 +145,7 @@ class LauncherActivity : AppCompatActivity() {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == DYNAMIC_FEATURE_INSTALL_RESULT_CODE) {
EmojiLoader.get(this)
if (app.sharedPreferences.getBoolean(ADS_ENABLED, true)) {
if (sharedPreferences.getBoolean(ADS_ENABLED, true)) {
when (resultCode) {
Activity.RESULT_OK -> {
initAds()
......@@ -183,9 +187,9 @@ class LauncherActivity : AppCompatActivity() {
private fun installRequiredModules() {
val modules = ArrayList<String>(MODULE_COUNT)
val googleApi = GoogleApiAvailability.getInstance()
if (app.sharedPreferences.getBoolean(ADS_ENABLED, true))
if (sharedPreferences.getBoolean(ADS_ENABLED, true))
modules += Ads.MODULE_NAME
if (!app.sharedPreferences.getBoolean(APP_INIT_KEY, false)) {
if (!sharedPreferences.getBoolean(APP_INIT_KEY, false)) {
modules += AppIntro.MODULE_NAME
launchOnInstall = true
}
......@@ -247,7 +251,7 @@ class LauncherActivity : AppCompatActivity() {
Timber.d("Setting-up security providers")
Security.insertProviderAt(Conscrypt.newProvider(), 1)
Timber.d("Setting-up activity recognition")
if (app.sharedPreferences.getBoolean(
if (sharedPreferences.getBoolean(
Preferences.ACTIVITY_TRACKING_ENABLED, false
) && with(GoogleApiAvailability.getInstance()) {
isGooglePlayServicesAvailable(this@LauncherActivity) ==
......@@ -258,8 +262,6 @@ class LauncherActivity : AppCompatActivity() {
} else {
app.activityHandler.disableActivityTracker()
}
Timber.d("Initializing Billing Service")
app.billingService = BillingService(this)
with(AlarmHandler(this)) {
scheduleAllAlarms()
}
......@@ -306,13 +308,13 @@ class LauncherActivity : AppCompatActivity() {
}
}
firebaseAnalytics.setAnalyticsCollectionEnabled(
app.sharedPreferences.getBoolean(
sharedPreferences.getBoolean(
Preferences.ANALYTICS_ENABLED,
true
)
)
firebasePerformance.isPerformanceCollectionEnabled =
app.sharedPreferences.getBoolean(
sharedPreferences.getBoolean(
Preferences.PERFORMANCE_ENABLED,
true
)
......
......@@ -125,9 +125,14 @@ class MainActivity : ActionBarBase(),
menu.selectedItemId = R.id.diseases
onNavigationItemSelected(menu.menu.findItem(R.id.diseases))
} else {
if (activeFragment == R.id.diseases)
if (activeFragment == R.id.diseases) {
with(fragments[activeFragment].get()!! as DiseasesFragment) {
onBackPressed()
}
fragments.clear()
super.onBackPressed()
else {
finish()
} else {
val washingHandsFragment = fragments[activeFragment].get()
?: createFragmentForId(R.id.handwashing)
as WashingHandsFragment
......
......@@ -16,7 +16,7 @@
*
* Created by Javinator9889 on 19/04/20 - Handwashing reminder.
*/
package com.javinator9889.handwashingreminder.collections
package com.javinator9889.handwashingreminder.activities.views.fragments.diseases
import android.os.Bundle
import androidx.annotation.LayoutRes
......@@ -24,19 +24,26 @@ import com.javinator9889.handwashingreminder.R
import com.javinator9889.handwashingreminder.activities.base.BaseFragmentView
import com.javinator9889.handwashingreminder.activities.views.viewmodels.ParsedHTMLText
import kotlinx.android.synthetic.main.disease_description.*
import kotlin.properties.Delegates
internal const val ARG_TITLE = "bundle:title"
internal const val ARG_SDESC = "bundle:description:short"
internal const val ARG_LDESC = "bundle:description:long"
internal const val ARG_PROVIDER = "bundle:provider"
internal const val ARG_WEBSITE = "bundle:website"
internal const val ARG_ANIMATION_ID = "bundle:animation:id"
internal const val ARG_HTML_TEXT = "bundle:text:html"
class DiseaseDescriptionFragment(
private val parsedHTMLText: ParsedHTMLText,
private val animId: Int
) : BaseFragmentView() {
class DiseaseDescriptionFragment : BaseFragmentView() {
@get:LayoutRes
override val layoutId: Int = R.layout.disease_description
private lateinit var parsedHTMLText: ParsedHTMLText
private var animId by Delegates.notNull<Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
......@@ -45,26 +52,26 @@ class DiseaseDescriptionFragment(
outState.putCharSequence(ARG_LDESC, longDescription.text)
outState.putCharSequence(ARG_PROVIDER, provider.text)
outState.putCharSequence(ARG_WEBSITE, website.text)
outState.putParcelable(ARG_HTML_TEXT, parsedHTMLText)
outState.putInt(ARG_ANIMATION_ID, animId)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
animatedView.setAnimation(animId)
if (savedInstanceState != null) {
title.text = savedInstanceState.getCharSequence(ARG_TITLE)
shortDescription.text =
savedInstanceState.getCharSequence(ARG_SDESC)
longDescription.text = savedInstanceState.getCharSequence(ARG_LDESC)
provider.text = savedInstanceState.getCharSequence(ARG_PROVIDER)
website.text = savedInstanceState.getCharSequence(ARG_WEBSITE)
} else {
title.text = parsedHTMLText.name
shortDescription.text = parsedHTMLText.shortDescription
longDescription.text = parsedHTMLText.longDescription
provider.text =
getString(R.string.written_by, parsedHTMLText.provider)
website.text =
getString(R.string.available_at, parsedHTMLText.website)
if (savedInstanceState != null || arguments != null) {
val data = (savedInstanceState ?: arguments)!!
parsedHTMLText = data.getParcelable(ARG_HTML_TEXT)!!
animId = data.getInt(ARG_ANIMATION_ID)
animatedView.setAnimation(animId)
title.text = data.getCharSequence(ARG_TITLE) ?: parsedHTMLText.name
shortDescription.text = data.getCharSequence(ARG_SDESC)
?: parsedHTMLText.shortDescription
longDescription.text = data.getCharSequence(ARG_LDESC)
?: parsedHTMLText.longDescription
provider.text = data.getCharSequence(ARG_PROVIDER)
?: getString(R.string.written_by, parsedHTMLText.provider)
website.text = data.getCharSequence(ARG_WEBSITE)
?: getString(R.string.available_at, parsedHTMLText.website)
}
}
}
\ No newline at end of file
......@@ -16,7 +16,7 @@
*
* Created by Javinator9889 on 19/04/20 - Handwashing reminder.
*/
package com.javinator9889.handwashingreminder.collections
package com.javinator9889.handwashingreminder.activities.views.fragments.diseases
import android.os.Bundle
import androidx.annotation.LayoutRes
......@@ -24,16 +24,22 @@ import com.javinator9889.handwashingreminder.R
import com.javinator9889.handwashingreminder.activities.base.BaseFragmentView
import com.javinator9889.handwashingreminder.activities.views.viewmodels.ParsedHTMLText
import kotlinx.android.synthetic.main.simple_text_view.*
import kotlin.properties.Delegates
internal const val ARG_SYMPTOMS = "bundle:symptoms"
internal const val ARG_PREVENTION = "bundle:prevention"
internal const val ARG_POSITION = "bundle:item:position"
class DiseaseExtraInformationFragment(
private val position: Int,
private val parsedHTMLText: ParsedHTMLText
) : BaseFragmentView() {
class DiseaseExtraInformationFragment : BaseFragmentView() {
@get:LayoutRes
override val layoutId: Int = R.layout.simple_text_view
private var position by Delegates.notNull<Int>()
private lateinit var parsedHTMLText: ParsedHTMLText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
......@@ -41,25 +47,23 @@ class DiseaseExtraInformationFragment(
outState.putCharSequence(ARG_SYMPTOMS, text.text)
else if (position == 2)
outState.putCharSequence(ARG_PREVENTION, text.text)
outState.putInt(ARG_POSITION, position)
outState.putParcelable(ARG_HTML_TEXT, parsedHTMLText)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState != null)
text.text = when (position) {
1 -> savedInstanceState.getCharSequence(
ARG_SYMPTOMS
)
2 -> savedInstanceState.getCharSequence(
ARG_PREVENTION
)
else -> ""
}
else
if (savedInstanceState != null || arguments != null) {
val data = (savedInstanceState ?: arguments)!!
position = data.getInt(ARG_POSITION)
parsedHTMLText = data.getParcelable(ARG_HTML_TEXT)!!
text.text = when (position) {
1 -> parsedHTMLText.symptoms
2 -> parsedHTMLText.prevention
1 -> data.getCharSequence(ARG_SYMPTOMS)
?: parsedHTMLText.symptoms
2 -> data.getCharSequence(ARG_PREVENTION)
?: parsedHTMLText.prevention
else -> ""
}
}
}
}
\ No newline at end of file
......@@ -43,6 +43,7 @@ import com.mikepenz.fastadapter.listeners.ClickEventHook
import kotlinx.android.synthetic.main.diseases_list.*
import kotlinx.android.synthetic.main.diseases_list.view.*
import kotlinx.coroutines.launch
import timber.log.Timber
class DiseasesFragment : BaseFragmentView() {
override val layoutId: Int = R.layout.diseases_list
......@@ -93,6 +94,23 @@ class DiseasesFragment : BaseFragmentView() {
adapter = fastAdapter
}
fastAdapter.addEventHook(DiseaseClickEventHook())
fastAdapter.withSavedInstanceState(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
fastAdapter.saveInstanceState(outState)
}
fun onBackPressed() {
try {
diseasesContainer.adapter = null
diseasesAdapter.clear()
} catch (e: Exception) {
Timber.w(e, "Exception when calling 'onBackPressed'")
} finally {
onDestroy()
}
}
private inner class DiseaseClickEventHook : ClickEventHook<Disease>() {
......
......@@ -33,7 +33,7 @@ class Ads : AbstractItem<Ads.ViewHolder>() {
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
class ViewHolder(v: View) : FastAdapter.ViewHolder<Ads>(v) {
private val ads = HandwashingApplication.getInstance().adLoader
private val ads = HandwashingApplication.instance.adLoader
private val container = v.findViewById<FrameLayout>(R.id.adsContainer)
override fun bindView(item: Ads, payloads: List<Any>) {
......
......@@ -89,7 +89,7 @@ class ActivityCheckbox : CheckBoxPreference {
firstCheck = false
return
}
with(HandwashingApplication.getInstance()) {
with(HandwashingApplication.instance) {
if (checked) {
activityHandler.startTrackingActivity()
} else {
......
......@@ -82,7 +82,7 @@ class ActivityMultiSelectList : MultiSelectListPreference {
}
private fun reloadActivityHandler() {
with(HandwashingApplication.getInstance()) {
with(HandwashingApplication.instance) {
activityHandler.reload()
}
}
......
......@@ -48,8 +48,7 @@ private const val LIVEDATA_KEY = "videomodel:livedata"
class VideoModel(
private val state: SavedStateHandle, private val position: Int
) : ViewModel() {
private val cachePath: File =
HandwashingApplication.getInstance().applicationContext.cacheDir
private val cachePath: File = HandwashingApplication.instance.applicationContext.cacheDir
val videos: LiveData<String> = liveData {
emitSource(state.getLiveData(LIVEDATA_KEY, loadVideo()))
}
......
......@@ -76,7 +76,7 @@ class WashingHandsModel(
}
private suspend fun processStringArray(@ArrayRes array: Int): CharSequence =
with(HandwashingApplication.getInstance()) {
with(HandwashingApplication.instance) {
with(EmojiLoader.get(this)) {
try {
this.await()
......
......@@ -27,7 +27,6 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.javinator9889.handwashingreminder.gms.activity.ActivityHandler
import com.javinator9889.handwashingreminder.gms.ads.AdLoader
import com.javinator9889.handwashingreminder.gms.vendor.BillingService
import com.javinator9889.handwashingreminder.utils.LogReportTree
import com.javinator9889.handwashingreminder.utils.isDebuggable
import javinator9889.localemanager.application.BaseApplication
......@@ -38,17 +37,11 @@ import timber.log.Timber
class HandwashingApplication : BaseApplication() {
var adLoader: AdLoader? = null
lateinit var billingService: BillingService
lateinit var activityHandler: ActivityHandler
lateinit var sharedPreferences: SharedPreferences
lateinit var firebaseInitDeferred: Deferred<Unit>
companion object {
private lateinit var instance: HandwashingApplication
fun getInstance(): HandwashingApplication {
return this.instance
}
lateinit var instance: HandwashingApplication
}
override fun attachBaseContext(base: Context?) {
......@@ -63,7 +56,6 @@ class HandwashingApplication : BaseApplication() {
override fun onCreate() {
super.onCreate()
instance = this
sharedPreferences = getCustomSharedPreferences(this)
activityHandler = ActivityHandler(this)
firebaseInitDeferred = initFirebaseAppAsync()
}
......
......@@ -18,22 +18,33 @@
*/
package com.javinator9889.handwashingreminder.collections
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.javinator9889.handwashingreminder.activities.views.fragments.diseases.ARG_ANIMATION_ID
import com.javinator9889.handwashingreminder.activities.views.fragments.diseases.ARG_HTML_TEXT
import com.javinator9889.handwashingreminder.activities.views.fragments.diseases.ARG_POSITION
import com.javinator9889.handwashingreminder.activities.views.fragments.diseases.DiseaseDescriptionFragment
import com.javinator9889.handwashingreminder.activities.views.fragments.diseases.DiseaseExtraInformationFragment
import com.javinator9889.handwashingreminder.activities.views.viewmodels.ParsedHTMLText
class DiseaseTextAdapter(
fm: FragmentActivity,
private val animId: Int,
private val parsedHTMLText: ParsedHTMLText
) :
FragmentStateAdapter(fm) {
) : FragmentStateAdapter(fm) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment = when (position) {
0 -> DiseaseDescriptionFragment(parsedHTMLText, animId)
1, 2 -> DiseaseExtraInformationFragment(position, parsedHTMLText)
0 -> DiseaseDescriptionFragment()
1, 2 -> DiseaseExtraInformationFragment()
else -> Fragment()
}.apply {
val bundle = Bundle(3)
bundle.putInt(ARG_POSITION, position)
bundle.putInt(ARG_ANIMATION_ID, animId)
bundle.putParcelable(ARG_HTML_TEXT, parsedHTMLText)
arguments = bundle
}
}
\ No newline at end of file
......@@ -46,7 +46,7 @@ class PrivacyTermsCollectionAdapter(fm: FragmentActivity) :
}
private const val ARG_POSITION = "item:position"
internal const val ARG_POSITION = "item:position"
class PolicyTextViewFragment : BaseFragment() {
override fun onCreateView(
......
......@@ -21,6 +21,7 @@ package com.javinator9889.handwashingreminder.jobs
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import com.javinator9889.handwashingreminder.application.HandwashingApplication
import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler
import com.javinator9889.handwashingreminder.utils.Preferences
......@@ -29,8 +30,8 @@ import timber.log.Timber
class BootCompletedJob : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
val app = HandwashingApplication.getInstance()
val preferences = app.sharedPreferences
val app = HandwashingApplication.instance
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (preferences.getBoolean(
Preferences.ACTIVITY_TRACKING_ENABLED,
false
......
......@@ -72,7 +72,7 @@ abstract class ScheduledNotificationWorker(context: Context) {
.currentTimeMillis() - startTime}ms"
)
} catch (e: Exception) {
with(HandwashingApplication.getInstance()) {
with(HandwashingApplication.instance) {
// Don't use so much resources, wait at most half a second until
// Firebase initializes or continue with execution.
// Firebase is only needed for Timber (Crashlytics) so until
......
......@@ -45,7 +45,7 @@ fun isAtLeast(version: AndroidVersion): Boolean =
Build.VERSION.SDK_INT >= version.code
fun isHighPerformingDevice(): Boolean {
with(HandwashingApplication.getInstance()) {
with(HandwashingApplication.instance) {
val activityManager =
getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val isLowRamDevice =
......@@ -59,7 +59,7 @@ fun isHighPerformingDevice(): Boolean {
}
fun isConnected(): Boolean {
val connectivityManager = HandwashingApplication.getInstance()
val connectivityManager = HandwashingApplication.instance
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (isAtLeast(AndroidVersion.M)) {
val network = connectivityManager.activeNetwork ?: return false
......@@ -79,7 +79,7 @@ fun isConnected(): Boolean {
}
fun isDebuggable(): Boolean =
(0 != HandwashingApplication.getInstance().applicationInfo.flags and
(0 != HandwashingApplication.instance.applicationInfo.flags and
ApplicationInfo.FLAG_DEBUGGABLE)
fun getDeviceInfo(): String = with(StringBuilder()) {
......
......@@ -27,7 +27,7 @@ import com.javinator9889.handwashingreminder.application.HandwashingApplication
* @return A float value to represent px equivalent to dp depending on device density
*/
fun dpToPx(dp: Float): Float {
val context = HandwashingApplication.getInstance().applicationContext
val context = HandwashingApplication.instance.applicationContext
return dp * context.resources.displayMetrics.density
}
......@@ -38,6 +38,6 @@ fun dpToPx(dp: Float): Float {
* @return A float value to represent dp equivalent to px value
*/
fun pxToDp(px: Float): Float {
val context = HandwashingApplication.getInstance().applicationContext
val context = HandwashingApplication.instance.applicationContext
return px / context.resources.displayMetrics.density
}
\ No newline at end of file
......@@ -30,6 +30,7 @@ import androidx.annotation.Keep
import androidx.core.content.edit
import androidx.core.util.set
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.github.paolorotolo.appintro.AppIntro2
import com.github.paolorotolo.appintro.AppIntroViewPager
import com.google.android.gms.common.ConnectionResult.SUCCESS
......@@ -165,8 +166,8 @@ class IntroActivity : AppIntro2(),
override fun onDonePressed(currentFragment: Fragment?) {
super.onDonePressed(currentFragment)
val app = HandwashingApplication.getInstance()
val sharedPreferences = app.sharedPreferences
val app = HandwashingApplication.instance
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
sharedPreferences.edit(commit = true) {
timeConfigSlide.itemAdapter.adapterItems.forEach { item ->
val time = "${item.hours}:${item.minutes}"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment