diff --git a/ads/build.gradle b/ads/build.gradle index 12eb5ec..1a212ef 100644 --- a/ads/build.gradle +++ b/ads/build.gradle @@ -27,25 +27,9 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':app') - implementation "androidx.core:core-ktx:1.2.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - // https://developer.android.com/kotlin/ktx#play-core - implementation 'com.google.android.play:core-ktx:1.7.0' // https://firebase.google.com/docs/admob/android/quick-start#import_the_mobile_ads_sdk implementation 'com.google.firebase:firebase-ads:19.1.0' - // https://developer.android.com/studio/build/multidex - implementation 'androidx.multidex:multidex:2.0.1' - // https://github.com/JakeWharton/timber - implementation 'com.jakewharton.timber:timber:4.7.1' - // https://firebase.google.com/docs/android/setup#add-sdks - implementation 'com.google.firebase:firebase-core:17.3.0' - implementation 'com.google.firebase:firebase-common-ktx:19.3.0' - implementation 'com.google.firebase:firebase-analytics:17.3.0' - implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04' - implementation 'com.google.firebase:firebase-perf:19.0.6' - // https://developer.android.com/jetpack/androidx/releases/cardview - implementation 'androidx.cardview:cardview:1.0.0' } repositories { mavenCentral() diff --git a/app/build.gradle b/app/build.gradle index 25c689b..9d365b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,8 +42,8 @@ android { applicationId "com.javinator9889.handwashingreminder" minSdkVersion 17 targetSdkVersion 29 - versionCode 107 - versionName "1.1.1-${gitCommitHash}" + versionCode 117 + versionName "1.1.2-${gitCommitHash}" multiDexEnabled true resConfigs "en", "es" @@ -83,13 +83,17 @@ android { jniDebuggable false renderscriptDebuggable false zipAlignEnabled true + /*firebaseCrashlytics { + // When manually initializing Firebase, Crashlytics mapping upload does not work + mappingFileUploadEnabled false + }*/ } } dexOptions { preDexLibraries true javaMaxHeapSize "1G" } - dynamicFeatures = [":appintro", ":ads", ":bundledemoji", ":okhttp", ":okhttplegacy"] + dynamicFeatures = [":appintro", ":ads", ":bundledemoji"] compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -104,58 +108,58 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + api 'androidx.appcompat:appcompat:1.1.0' + api 'androidx.core:core-ktx:1.2.0' + api 'androidx.legacy:legacy-support-v4:1.0.0' + api 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // https://github.com/Javinator9889/LocaleManager - implementation 'com.github.javinator9889:localemanager:1.1X' + api 'com.github.javinator9889:localemanager:1.1X' // https://material.io/develop/android/docs/getting-started/ - implementation 'com.google.android.material:material:1.1.0' + api 'com.google.android.material:material:1.1.0' // https://developers.google.com/android/guides/setup implementation 'com.google.android.gms:play-services-location:17.0.0' // https://developer.android.com/jetpack/androidx/releases/annotation - implementation 'androidx.annotation:annotation:1.1.0' + api 'androidx.annotation:annotation:1.1.0' // https://developer.android.com/jetpack/androidx/releases/cardview - implementation 'androidx.cardview:cardview:1.0.0' + api 'androidx.cardview:cardview:1.0.0' // https://developer.android.com/jetpack/androidx/releases/recyclerview - implementation 'androidx.recyclerview:recyclerview:1.1.0' + api 'androidx.recyclerview:recyclerview:1.1.0' // https://developer.android.com/studio/build/multidex - implementation 'androidx.multidex:multidex:2.0.1' + api 'androidx.multidex:multidex:2.0.1' // https://github.com/mikepenz/Android-Iconics - implementation 'com.mikepenz:iconics-core:5.0.2' - implementation 'com.mikepenz:iconics-views:5.0.2' + api 'com.mikepenz:iconics-core:5.0.2' + api 'com.mikepenz:iconics-views:5.0.2' //noinspection GradleDependency - implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar' - implementation 'com.mikepenz:ionicons-typeface:2.0.1.5-kotlin@aar' + api 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar' + api 'com.mikepenz:ionicons-typeface:2.0.1.5-kotlin@aar' // https://github.com/mikepenz/AboutLibraries implementation "com.mikepenz:aboutlibraries-core:${latestAboutLibsRelease}" implementation "com.mikepenz:aboutlibraries:${latestAboutLibsRelease}" // https://developer.android.com/kotlin/ktx#play-core - implementation 'com.google.android.play:core-ktx:1.7.0' + api 'com.google.android.play:core:1.7.2' + api 'com.google.android.play:core-ktx:1.7.0' // https://developer.android.com/kotlin/ktx#collection implementation 'androidx.collection:collection-ktx:1.1.0' // https://kotlinlang.org/docs/reference/reflection.html implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" // https://firebase.google.com/docs/android/setup#add-sdks - implementation 'com.google.firebase:firebase-common-ktx:19.3.0' - implementation 'com.google.firebase:firebase-analytics:17.3.0' - implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04' - implementation 'com.google.firebase:firebase-perf:19.0.6' + api 'com.google.firebase:firebase-common-ktx:19.3.0' + api 'com.google.firebase:firebase-analytics:17.3.0' + api 'com.google.firebase:firebase-crashlytics:17.0.0' + api 'com.google.firebase:firebase-perf:19.0.6' // http://airbnb.io/lottie/#/android?id=getting-started - implementation "com.airbnb.android:lottie:3.4.0" + api "com.airbnb.android:lottie:3.4.0" // https://firebase.google.com/docs/remote-config/use-config-android implementation 'com.google.firebase:firebase-config:19.1.3' implementation 'com.google.firebase:firebase-config-ktx:19.1.3' - // https://developer.android.com/jetpack/androidx/releases/work#declaring_dependencies - implementation 'androidx.work:work-runtime-ktx:2.3.4' // https://mvnrepository.com/artifact/androidx.emoji/emoji/1.0.0 - implementation 'androidx.emoji:emoji:1.0.0' - implementation 'androidx.emoji:emoji-appcompat:1.0.0' + api 'androidx.emoji:emoji:1.0.0' + api 'androidx.emoji:emoji-appcompat:1.0.0' // https://github.com/mikepenz/FastAdapter implementation "com.mikepenz:fastadapter:${latestFastAdapterRelease}" // https://developer.android.com/kotlin/ktx#lifecycle @@ -167,13 +171,13 @@ dependencies { // https://developer.android.com/kotlin/ktx#livedata implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' // https://github.com/JakeWharton/timber - implementation 'com.jakewharton.timber:timber:4.7.1' + api 'com.jakewharton.timber:timber:4.7.1' // 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' // https://github.com/bumptech/glide - implementation 'com.github.bumptech.glide:glide:4.11.0' + api 'com.github.bumptech.glide:glide:4.11.0' kapt 'com.github.bumptech.glide:compiler:4.11.0' // https://github.com/afollestad/material-dialogs/ implementation 'com.afollestad.material-dialogs:core:3.3.0' @@ -188,4 +192,7 @@ dependencies { implementation 'com.squareup.okio:okio:2.5.0' // https://github.com/google/conscrypt/ implementation 'org.conscrypt:conscrypt-android:2.4.0' + // https://github.com/deano2390/MaterialShowcaseView + implementation 'com.github.deano2390:MaterialShowcaseView:1.3.4' } +apply plugin: 'com.google.gms.google-services' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 4d1d5e2..7d0df06 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -80,6 +80,7 @@ #data models -keep class com.javinator9889.handwashingreminder.collections.** { *;} -# prevent Crashlytics obfuscation --keep class com.google.firebase.crashlytics.** { *; } --dontwarn com.google.firebase.crashlytics.** +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4984aa..50c40d1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,32 +22,27 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" + android:hardwareAccelerated="true" tools:ignore="LockedOrientationActivity"> - + - - - - - + + + + + - + android:exported="true" /> - + @@ -75,7 +68,20 @@ - + + + + + + { + SplitInstallHelper.updateAppInfo(this) if (++currentModule >= moduleCount) { dynamic_content_title.text = getString(R.string.done) setResultWithIntent(Activity.RESULT_OK) diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/LauncherActivity.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/LauncherActivity.kt index f229aa9..4c57aa7 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/LauncherActivity.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/LauncherActivity.kt @@ -42,7 +42,7 @@ 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.workers.WorkHandler +import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler import com.javinator9889.handwashingreminder.utils.* import com.javinator9889.handwashingreminder.utils.Preferences.Companion.ADS_ENABLED import com.javinator9889.handwashingreminder.utils.Preferences.Companion.APP_INIT_KEY @@ -189,10 +189,6 @@ class LauncherActivity : AppCompatActivity() { modules += AppIntro.MODULE_NAME launchOnInstall = true } - /*modules += if (isAtLeast(AndroidVersion.LOLLIPOP)) - OkHttp.MODULE_NAME - else - OkHttpLegacy.MODULE_NAME*/ if (googleApi.isGooglePlayServicesAvailable( this, GOOGLE_PLAY_SERVICES_MIN_VERSION @@ -240,7 +236,11 @@ class LauncherActivity : AppCompatActivity() { } private suspend fun initVariables() { - app.firebaseInitDeferred.await() + // Wait at most 3 seconds for Firebase to initialize. Then continue + // with the app initialization + withTimeoutOrNull(3_000L) { + app.firebaseInitDeferred.await() + } Timber.d("Firebase initialized correctly") Timber.d("Initializing Iconics") Iconics.init(this) @@ -260,8 +260,8 @@ class LauncherActivity : AppCompatActivity() { } Timber.d("Initializing Billing Service") app.billingService = BillingService(this) - with(WorkHandler(this)) { - enqueuePeriodicNotificationsWorker() + with(AlarmHandler(this)) { + scheduleAllAlarms() } Timber.d("Adding periodic notifications if not enqueued yet") Timber.d("Setting-up Firebase custom properties") diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/MainActivity.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/MainActivity.kt index e6be2f5..b2a1b8a 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/MainActivity.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/MainActivity.kt @@ -22,11 +22,13 @@ import android.os.Bundle import android.util.SparseArray import android.view.MenuItem import androidx.annotation.IdRes +import androidx.core.content.edit import androidx.core.util.forEach import androidx.core.util.set import androidx.core.view.forEach import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction +import androidx.preference.PreferenceManager import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.ktx.Firebase @@ -38,23 +40,28 @@ import com.javinator9889.handwashingreminder.activities.views.fragments.diseases import com.javinator9889.handwashingreminder.activities.views.fragments.news.NewsFragment import com.javinator9889.handwashingreminder.activities.views.fragments.settings.SettingsView import com.javinator9889.handwashingreminder.activities.views.fragments.washinghands.WashingHandsFragment -import com.javinator9889.handwashingreminder.application.HandwashingApplication +import com.javinator9889.handwashingreminder.custom.libraries.AppRate +import com.javinator9889.handwashingreminder.utils.Preferences +import com.javinator9889.handwashingreminder.utils.isDebuggable import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.ionicons.Ionicons import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.how_to_wash_hands_layout.* import timber.log.Timber +import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence +import uk.co.deanwild.materialshowcaseview.ShowcaseConfig import java.lang.ref.WeakReference import kotlin.concurrent.thread import kotlin.properties.Delegates +internal const val ARG_CURRENT_ITEM = "bundle:args:current_item" + class MainActivity : ActionBarBase(), BottomNavigationView.OnNavigationItemSelectedListener { override val layoutId: Int = R.layout.activity_main private val fragments: SparseArray> = SparseArray(4) private var activeFragment by Delegates.notNull<@IdRes Int>() - private lateinit var app: HandwashingApplication @AddTrace(name = "onCreateMainView") override fun onCreate(savedInstanceState: Bundle?) { @@ -68,11 +75,24 @@ class MainActivity : ActionBarBase(), delegateMenuIcons(menu) val ids = arrayOf(R.id.diseases, R.id.handwashing, R.id.news, R.id.settings) - for (id in ids) - createFragmentForId(id) - activeFragment = R.id.diseases menu.setOnNavigationItemSelectedListener(this) - initFragmentView() + loadTutorial() + suggestRating() + if (savedInstanceState != null) { + for (id in ids) { + val fragment = supportFragmentManager.getFragment( + savedInstanceState, + id.toString() + ) ?: createFragmentForId(id) + fragments[id] = WeakReference(fragment) + } + activeFragment = savedInstanceState.getInt(ARG_CURRENT_ITEM) + } else { + for (id in ids) + createFragmentForId(id) + activeFragment = R.id.diseases + initFragmentView() + } } protected fun delegateMenuIcons(menu: BottomNavigationView) { @@ -135,6 +155,18 @@ class MainActivity : ActionBarBase(), return onItemSelected(item.itemId) } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + fragments.forEach { id, reference -> + reference.get()?.let { + supportFragmentManager.putFragment( + outState, id.toString(), it + ) + } + } + outState.putInt(ARG_CURRENT_ITEM, activeFragment) + } + protected fun onItemSelected(@IdRes id: Int): Boolean { return try { loadFragment(id) @@ -179,6 +211,67 @@ class MainActivity : ActionBarBase(), }.commit() } + private fun loadTutorial() { + val preferences = + with(PreferenceManager.getDefaultSharedPreferences(this)) { + if (getBoolean(Preferences.INITIAL_TUTORIAL_DONE, false)) + return + else this + } + val config = ShowcaseConfig() + config.delay = 500L + with(MaterialShowcaseSequence(this)) { + setConfig(config) + val dismissText = getString(R.string.got_it) + val diseasesText = getString(R.string.diseases_intro) + val handwashingText = getString(R.string.handwashing_intro) + val newsText = getString(R.string.news_intro) + val settingsText = getString(R.string.settings_intro) + addSequenceItem( + findViewById(R.id.diseases), + diseasesText, + dismissText + ) + addSequenceItem( + findViewById(R.id.handwashing), + handwashingText, + dismissText + ) + addSequenceItem( + findViewById(R.id.news), + newsText, + dismissText + ) + addSequenceItem( + findViewById(R.id.settings), + settingsText, + dismissText + ) + var itemCount = 0 + setOnItemDismissedListener { _, _ -> + if (itemCount++ == 3) + preferences.edit { + putBoolean(Preferences.INITIAL_TUTORIAL_DONE, true) + } + } + start() + } + } + + private fun suggestRating() { + with(AppRate(this)) { + if (!isDebuggable()) { + setMinDaysUntilPrompt(2L) + setMinLaunchesUntilPrompt(5) + } + dialogTitle = R.string.rate_text_title + dialogMessage = R.string.rate_app_message + positiveButtonText = R.string.rate_text + negativeButtonText = R.string.rate_do_not_show + init() + } + } + private fun createFragmentForId(@IdRes id: Int): Fragment { val fragment = when (id) { R.id.diseases -> DiseasesFragment() diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/base/SplitCompatBaseActivity.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/base/SplitCompatBaseActivity.kt index 3856c23..90f8850 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/base/SplitCompatBaseActivity.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/base/SplitCompatBaseActivity.kt @@ -30,11 +30,12 @@ abstract class SplitCompatBaseActivity : BaseAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - splitInstallManager = SplitInstallManagerFactory.create(this) + splitInstallManager = + SplitInstallManagerFactory.create(applicationContext) } override fun attachBaseContext(base: Context) { super.attachBaseContext(base) - SplitCompat.install(this) + SplitCompat.install(base) } } \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/diseases/DiseasesFragment.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/diseases/DiseasesFragment.kt index 6b30736..a396383 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/diseases/DiseasesFragment.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/diseases/DiseasesFragment.kt @@ -47,10 +47,10 @@ import kotlinx.coroutines.launch class DiseasesFragment : BaseFragmentView() { override val layoutId: Int = R.layout.diseases_list private lateinit var parsedHTMLTexts: List + private lateinit var fastAdapter: FastAdapter private val upperAdsAdapter: ItemAdapter = ItemAdapter() private val lowerAdsAdapter: ItemAdapter = ItemAdapter() private val diseasesAdapter: ItemAdapter = ItemAdapter() - private lateinit var fastAdapter: FastAdapter private val informationFactory = DiseaseInformationFactory() private val informationViewModel: DiseaseInformationViewModel by viewModels { SavedViewModelFactory(informationFactory, this) @@ -96,11 +96,9 @@ class DiseasesFragment : BaseFragmentView() { } private inner class DiseaseClickEventHook : ClickEventHook() { - override fun onBind(viewHolder: RecyclerView.ViewHolder): View? { - return if (viewHolder is Disease.ViewHolder) - viewHolder.cardContainer + override fun onBind(viewHolder: RecyclerView.ViewHolder) = + if (viewHolder is Disease.ViewHolder) viewHolder.cardContainer else null - } override fun onClick( v: View, diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/ActivityMultiSelectList.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/ActivityMultiSelectList.kt index b9fb6a6..374d356 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/ActivityMultiSelectList.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/ActivityMultiSelectList.kt @@ -48,11 +48,16 @@ class ActivityMultiSelectList : MultiSelectListPreference { override fun notifyChanged() { super.notifyChanged() - loadSummary() - if (!isFirstCall) + if (!isFirstCall) { + loadSummary() reloadActivityHandler() - else - isFirstCall = false + } + } + + override fun onSetInitialValue(defaultValue: Any?) { + super.onSetInitialValue(defaultValue) + isFirstCall = false + loadSummary() } private fun loadSummary() { diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/SettingsView.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/SettingsView.kt index 12a7dc0..ad9823c 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/SettingsView.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/SettingsView.kt @@ -41,6 +41,7 @@ import com.javinator9889.handwashingreminder.application.HandwashingApplication import com.javinator9889.handwashingreminder.emoji.EmojiLoader import com.javinator9889.handwashingreminder.gms.ads.AdsEnabler import com.javinator9889.handwashingreminder.gms.splitservice.SplitInstallService +import com.javinator9889.handwashingreminder.jobs.alarms.Alarms import com.javinator9889.handwashingreminder.listeners.OnPurchaseFinishedListener import com.javinator9889.handwashingreminder.utils.* import com.mikepenz.aboutlibraries.LibsBuilder @@ -285,6 +286,7 @@ class SettingsView : PreferenceFragmentCompat(), emojiCompat = emojiLoader.await() breakfast?.let { it.icon = icon(Ionicons.Icon.ion_coffee) + it.alarm = Alarms.BREAKFAST_ALARM try { it.title = emojiCompat.process(getText(R.string.breakfast_pref_title)) @@ -299,6 +301,7 @@ class SettingsView : PreferenceFragmentCompat(), } lunch?.let { it.icon = icon(Ionicons.Icon.ion_android_restaurant) + it.alarm = Alarms.LUNCH_ALARM try { it.title = emojiCompat.process(getText(R.string.lunch_pref_title)) @@ -313,6 +316,7 @@ class SettingsView : PreferenceFragmentCompat(), } dinner?.let { it.icon = icon(Ionicons.Icon.ion_ios_moon_outline) + it.alarm = Alarms.DINNER_ALARM try { it.title = emojiCompat.process(getText(R.string.dinner_pref_title)) diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/TimePickerPreference.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/TimePickerPreference.kt index 6e9ba48..f265205 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/TimePickerPreference.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/fragments/settings/TimePickerPreference.kt @@ -23,7 +23,8 @@ import android.content.Context import android.util.AttributeSet import android.widget.TimePicker import androidx.preference.EditTextPreference -import com.javinator9889.handwashingreminder.jobs.workers.WorkHandler +import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler +import com.javinator9889.handwashingreminder.jobs.alarms.Alarms import com.javinator9889.handwashingreminder.utils.formatTime class TimePickerPreference : EditTextPreference, @@ -37,7 +38,7 @@ class TimePickerPreference : EditTextPreference, context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) : super(context, attrs, defStyleAttr, defStyleRes) - + lateinit var alarm: Alarms lateinit var summaryText: CharSequence private fun setSummary(hours: Int, minutes: Int): String { @@ -70,8 +71,8 @@ class TimePickerPreference : EditTextPreference, override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) { val time = setSummary(hourOfDay, minute) text = time - with(WorkHandler(context)) { - enqueuePeriodicNotificationsWorker(true) + with(AlarmHandler(context)) { + scheduleAlarm(alarm) } } } \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/viewmodels/VideoModel.kt b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/viewmodels/VideoModel.kt index dbd3a77..d025e8f 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/viewmodels/VideoModel.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/activities/views/viewmodels/VideoModel.kt @@ -22,7 +22,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData -import com.google.firebase.perf.metrics.AddTrace import com.javinator9889.handwashingreminder.application.HandwashingApplication import com.javinator9889.handwashingreminder.network.HttpDownloader import com.javinator9889.handwashingreminder.utils.Videos.URI.FILENAME @@ -30,6 +29,7 @@ import com.javinator9889.handwashingreminder.utils.Videos.URI.HASH import com.javinator9889.handwashingreminder.utils.Videos.URI.URL import com.javinator9889.handwashingreminder.utils.Videos.URI.VideoList import com.javinator9889.handwashingreminder.utils.isConnected +import com.javinator9889.handwashingreminder.utils.trace import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext @@ -63,7 +63,9 @@ class VideoModel( val hash = video[HASH] val filename = video[FILENAME] if (url != null && hash != null && filename != null) { - file = downloadVideo(url, hash, filename) + file = trace("videoDownload") { + downloadVideo(url, hash, filename) + } } } var isVideoDownloaded = true @@ -74,7 +76,6 @@ class VideoModel( return file.name } - @AddTrace(name = "videoDownload") private suspend fun downloadVideo( url: String, hash: String, @@ -88,7 +89,7 @@ class VideoModel( file.createNewFile() val hashingSink = HashingSink.sha256(file.sink()) do { - val okHttpDownloader = HttpDownloader.newInstance() + val okHttpDownloader = HttpDownloader() with(okHttpDownloader.downloadFile(url)) { hashingSink.buffer().use { it.writeAll(this) diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/application/HandwashingApplication.kt b/app/src/main/java/com/javinator9889/handwashingreminder/application/HandwashingApplication.kt index 4929eaf..dbb90f9 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/application/HandwashingApplication.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/application/HandwashingApplication.kt @@ -20,12 +20,9 @@ package com.javinator9889.handwashingreminder.application import android.content.Context import android.content.SharedPreferences -import android.util.Log import androidx.multidex.MultiDex import androidx.preference.PreferenceManager import com.google.android.play.core.splitcompat.SplitCompat -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions import com.google.firebase.crashlytics.FirebaseCrashlytics import com.javinator9889.handwashingreminder.gms.activity.ActivityHandler import com.javinator9889.handwashingreminder.gms.ads.AdLoader @@ -56,7 +53,7 @@ class HandwashingApplication : BaseApplication() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) MultiDex.install(base) - SplitCompat.install(this) + SplitCompat.install(base) } /** @@ -67,27 +64,12 @@ class HandwashingApplication : BaseApplication() { instance = this sharedPreferences = getCustomSharedPreferences(this) activityHandler = ActivityHandler(this) - /*if (isDebuggable()) { - Timber.plant(Timber.DebugTree()) - Timber.d("Application is in DEBUG mode") - with(FirebaseCrashlytics.getInstance()) { - setCrashlyticsCollectionEnabled(false) - } - } else { - Timber.plant(LogReportTree()) - }*/ firebaseInitDeferred = initFirebaseAppAsync() - Log.d("Application", "Deferred Firebase Instantiating") } private fun initFirebaseAppAsync(): Deferred { return GlobalScope.async { withContext(Dispatchers.IO) { - FirebaseApp.initializeApp( - this@HandwashingApplication, - FirebaseOptions - .fromResource(this@HandwashingApplication)!! - ) if (isDebuggable()) { Timber.plant(Timber.DebugTree()) Timber.d("Application is in DEBUG mode") diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/AppRate.kt b/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/AppRate.kt new file mode 100644 index 0000000..e68d344 --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/AppRate.kt @@ -0,0 +1,321 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 29/04/20 - Handwashing reminder. + * + * Based on AppRate open source library: https://github.com/msfjarvis/AppRate + */ +@file:Suppress("unused") +package com.javinator9889.handwashingreminder.custom.libraries + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager.NameNotFoundException +import android.net.Uri +import android.text.format.DateUtils +import android.widget.Toast +import androidx.annotation.StringRes +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.callbacks.onCancel +import timber.log.Timber + +class AppRate(private val hostActivity: Activity) { + private val preferences: SharedPreferences = + hostActivity.getSharedPreferences(PrefsContract.SHARED_PREFS_NAME, 0) + + private var minLaunchesUntilPrompt: Long = 0 + private var minDaysUntilPrompt: Long = 0 + private var customDialog: MaterialDialog? = null + private var positiveActionCallback: () -> Unit = + { throw NoCustomCallbackException() } + private var negativeActionCallback: () -> Unit? = + { throw NoCustomCallbackException() } + + private var showIfHasCrashed = true + + @StringRes + var dialogTitle: Int? = null + + @StringRes + var dialogMessage: Int? = null + + @StringRes + var positiveButtonText: Int? = null + + @StringRes + var negativeButtonText: Int? = null + + /** + * @param minLaunchesUntilPrompt The minimum number of times the + * * user lunches the application before showing the rate dialog.

+ * * Default value is 0 times. + * * + * @return This [AppRate] object to allow chaining. + */ + fun setMinLaunchesUntilPrompt(minLaunchesUntilPrompt: Long): AppRate { + this.minLaunchesUntilPrompt = minLaunchesUntilPrompt + return this + } + + /** + * @param minDaysUntilPrompt The minimum number of days before showing the rate dialog.

+ * * Default value is 0 days. + * * + * @return This [AppRate] object to allow chaining. + */ + fun setMinDaysUntilPrompt(minDaysUntilPrompt: Long): AppRate { + this.minDaysUntilPrompt = minDaysUntilPrompt + return this + } + + /** + * @param showIfCrash If `false` the rate dialog will + * * not be shown if the application has crashed once.

+ * * Default value is `false`. + * * + * @return This [AppRate] object to allow chaining. + */ + fun setShowIfAppHasCrashed(showIfCrash: Boolean): AppRate { + showIfHasCrashed = showIfCrash + preferences.edit() + .putBoolean(PrefsContract.PREF_DONT_SHOW_IF_CRASHED, showIfCrash) + .apply() + return this + } + + /** + * Use this method if you want to customize the style and content of the rate dialog.

+ * When using the [MaterialDialog] you should use: + * + * * [MaterialDialog.positiveButton] for the **rate** button. + * * [MaterialDialog.negativeButton] for the **never rate** button. + * + * @param customDialog The custom dialog you want to use as the rate dialog. + * * + * @return This [AppRate] object to allow chaining. + */ + fun setCustomDialog(customDialog: MaterialDialog): AppRate { + this.customDialog = customDialog + return this + } + + /** + * Display the rate dialog if needed. + */ + fun init() { + + Timber.d("Init AppRate") + + if (preferences.getBoolean(PrefsContract.PREF_DONT_SHOW_AGAIN, false) || + preferences.getBoolean(PrefsContract.PREF_APP_HAS_CRASHED, false) && + !showIfHasCrashed + ) { + return + } + + if (!showIfHasCrashed) { + initExceptionHandler() + } + + val editor = preferences.edit() + + // Get and increment launch counter. + val launchCount = + preferences.getLong(PrefsContract.PREF_LAUNCH_COUNT, 0) + 1 + editor.putLong(PrefsContract.PREF_LAUNCH_COUNT, launchCount) + + // Get date of first launch. + var dateFirstLaunch: Long? = + preferences.getLong(PrefsContract.PREF_DATE_FIRST_LAUNCH, 0) + if (dateFirstLaunch == 0L) { + dateFirstLaunch = System.currentTimeMillis() + editor.putLong( + PrefsContract.PREF_DATE_FIRST_LAUNCH, + dateFirstLaunch + ) + } + + // Show the rate dialog if needed. + if (launchCount >= minLaunchesUntilPrompt) { + if (System.currentTimeMillis() >= dateFirstLaunch!! + minDaysUntilPrompt * DateUtils.DAY_IN_MILLIS) { + showDefaultDialog() + } + } + + editor.apply() + } + + /** + * Initialize the [ExceptionHandler]. + */ + private fun initExceptionHandler() { + + Timber.d("Init AppRate ExceptionHandler") + + val currentHandler = Thread.getDefaultUncaughtExceptionHandler() + + // Don't register again if already registered. + if (currentHandler !is ExceptionHandler) { + + // Register default exceptions handler. + Thread.setDefaultUncaughtExceptionHandler( + ExceptionHandler( + currentHandler, + hostActivity + ) + ) + } + } + + /** + * Shows the default rate dialog. + */ + private fun showDefaultDialog() { + + Timber.d("Create default dialog.") + + val title = dialogTitle?.let { hostActivity.getText(it) } + ?: "Rate ${getApplicationName(hostActivity.applicationContext)}" + val message = dialogMessage?.let { hostActivity.getText(it) } + ?: "If you enjoy using ${getApplicationName(hostActivity.applicationContext)}," + + " please take a moment to rate it. Thanks for your support!" + val rate = + positiveButtonText?.let { hostActivity.getText(it) } ?: "Rate it !" + val dismiss = + negativeButtonText?.let { hostActivity.getText(it) } ?: "No thanks" + + MaterialDialog(hostActivity).apply { + title(text = title.toString()) + message(text = message) + positiveButton(text = rate) { onPositive() } + negativeButton(text = dismiss) { onNegative() } + onCancel { onCancel() } + show() + } + } + + /** + * Method called when the positiveButton callback + * is invoked. + */ + private fun onPositive() { + try { + positiveActionCallback() + } catch (ignored: NoCustomCallbackException) { + try { + hostActivity.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=" + hostActivity.packageName) + ) + ) + } catch (ignored: ActivityNotFoundException) { + Toast.makeText( + hostActivity, + "No Play Store installed on device", + Toast.LENGTH_SHORT + ).show() + } + } + + preferences.edit().putBoolean(PrefsContract.PREF_DONT_SHOW_AGAIN, true) + .apply() + } + + /** + * Method called when the negativeButton callback + * is invoked. + */ + private fun onNegative() { + try { + negativeActionCallback() + } catch (ignored: NoCustomCallbackException) { + preferences.edit() + .putBoolean(PrefsContract.PREF_DONT_SHOW_AGAIN, true).apply() + } + } + + private fun onCancel() { + preferences.edit() + .putLong( + PrefsContract.PREF_DATE_FIRST_LAUNCH, + System.currentTimeMillis() + ) + .putLong(PrefsContract.PREF_LAUNCH_COUNT, 0) + .apply() + } + + /** + * @param negativeActionCallback A Kotlin unit invoked when the negative action + * callback is triggered in the dialog. + * + * @return This [AppRate] object to allow chaining. + */ + fun setOnNegativeCallback(negativeActionCallback: () -> Unit): AppRate { + this.negativeActionCallback = negativeActionCallback + return this + } + + /** + * @param positiveActionCallback A Kotlin unit invoked when the positive action + * callback is triggered in the dialog. + * + * @return This [AppRate] object to allow chaining. + */ + fun setOnPositiveCallback(positiveActionCallback: () -> Unit): AppRate { + this.positiveActionCallback = positiveActionCallback + return this + } + + class NoCustomCallbackException : Exception() + + companion object { + + private const val TAG = "AppRate" + + /** + * Reset all the data collected about number of launches and days until first launch. + * @param context A context. + */ + fun reset(context: Context) { + context.getSharedPreferences(PrefsContract.SHARED_PREFS_NAME, 0) + .edit().clear().apply() + Timber.d( "Cleared AppRate shared preferences.") + } + + /** + * @param context A context of the current application. + * * + * @return The application name of the current application. + */ + private fun getApplicationName(context: Context): String { + val packageManager = context.packageManager + val applicationInfo: ApplicationInfo? + applicationInfo = try { + packageManager.getApplicationInfo(context.packageName, 0) + } catch (ignored: NameNotFoundException) { + null + } + + return (if (applicationInfo != null) + packageManager.getApplicationLabel(applicationInfo) else "(unknown)") as String + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/ExceptionHandler.kt b/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/ExceptionHandler.kt new file mode 100644 index 0000000..239f729 --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/ExceptionHandler.kt @@ -0,0 +1,36 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 29/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.custom.libraries + +import android.content.Context +import android.content.SharedPreferences + +class ExceptionHandler// Constructor. +internal constructor(private val defaultExceptionHandler: Thread.UncaughtExceptionHandler, context: Context) : + Thread.UncaughtExceptionHandler { + private val preferences: SharedPreferences = + context.getSharedPreferences(PrefsContract.SHARED_PREFS_NAME, 0) + + override fun uncaughtException(thread: Thread, throwable: Throwable) { + preferences.edit().putBoolean(PrefsContract.PREF_APP_HAS_CRASHED, true).apply() + + // Call the original handler. + defaultExceptionHandler.uncaughtException(thread, throwable) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/PrefsContract.kt b/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/PrefsContract.kt new file mode 100644 index 0000000..38df518 --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/custom/libraries/PrefsContract.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 29/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.custom.libraries + +object PrefsContract { + + internal const val SHARED_PREFS_NAME = "apprate_prefs" + + internal const val PREF_APP_HAS_CRASHED = "pref_app_has_crashed" + internal const val PREF_DATE_FIRST_LAUNCH = "date_firstlaunch" + internal const val PREF_LAUNCH_COUNT = "launch_count" + internal const val PREF_DONT_SHOW_AGAIN = "dont_show_again" + const val PREF_DONT_SHOW_IF_CRASHED = "pref_dont_show_if_crashed" +} diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/emoji/EmojiConfig.kt b/app/src/main/java/com/javinator9889/handwashingreminder/emoji/EmojiConfig.kt index 3a8136d..cf4c64b 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/emoji/EmojiConfig.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/emoji/EmojiConfig.kt @@ -36,7 +36,10 @@ object EmojiConfig { context, GOOGLE_PLAY_SERVICES_MIN_VERSION ) == ConnectionResult.SUCCESS || - !isModuleInstalled(context, BundledEmoji.MODULE_NAME) + !isModuleInstalled( + context.applicationContext, + BundledEmoji.MODULE_NAME + ) ) { with( FontRequest( diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/gms/activity/ActivityReceiver.kt b/app/src/main/java/com/javinator9889/handwashingreminder/gms/activity/ActivityReceiver.kt index c9c49c1..c23808f 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/gms/activity/ActivityReceiver.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/gms/activity/ActivityReceiver.kt @@ -30,7 +30,10 @@ import com.javinator9889.handwashingreminder.R import com.javinator9889.handwashingreminder.emoji.EmojiLoader import com.javinator9889.handwashingreminder.notifications.NotificationsHandler import com.javinator9889.handwashingreminder.utils.ACTIVITY_CHANNEL_ID -import kotlinx.coroutines.* +import com.javinator9889.handwashingreminder.utils.goAsync +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class ActivityReceiver : BroadcastReceiver() { /** @@ -56,76 +59,71 @@ class ActivityReceiver : BroadcastReceiver() { R.string.activity_notification_channel_desc ) ) - putNotification( - notificationHandler, - emojiLoader, - event.activityType, - context - ) + goAsync { + putNotification( + notificationHandler, + emojiLoader, + event.activityType, + context + ) + } break } } } - private fun putNotification( + private suspend fun putNotification( notificationsHandler: NotificationsHandler, emojiLoader: CompletableDeferred, detectedActivity: Int, - context: Context, - coroutineScope: CoroutineScope = GlobalScope + context: Context ) { - val result = goAsync() - coroutineScope.launch { - try { - val notificationContent = when (detectedActivity) { - DetectedActivity.WALKING -> - NotificationContent( - R.string.activity_notification_walk, - R.string.activity_notification_walk_content - ) - DetectedActivity.RUNNING -> - NotificationContent( - R.string.activity_notification_run, - R.string.activity_notifications_run_content - ) - DetectedActivity.ON_BICYCLE -> - NotificationContent( - R.string.activity_notification_cycling, - R.string.activity_notification_cycling_content - ) - DetectedActivity.IN_VEHICLE -> - NotificationContent( - R.string.activity_notification_vehicle, - R.string.activity_notification_vehicle_content - ) - else -> throw IllegalArgumentException( - "Activity not recognized" - ) - } - val emojiCompat = emojiLoader.await() - val title = emojiCompat.process( - context.getText(notificationContent.title) + val notificationContent = when (detectedActivity) { + DetectedActivity.WALKING -> + NotificationContent( + R.string.activity_notification_walk, + R.string.activity_notification_walk_content ) - val content = emojiCompat.process( - context.getText(notificationContent.content) + DetectedActivity.RUNNING -> + NotificationContent( + R.string.activity_notification_run, + R.string.activity_notifications_run_content ) - withContext(Dispatchers.Main) { - notificationsHandler.createNotification( - R.drawable.ic_stat_handwashing, - R.drawable.handwashing_app_logo, - title, - content, - longContent = content - ) - } - } finally { - result.finish() - } + DetectedActivity.ON_BICYCLE -> + NotificationContent( + R.string.activity_notification_cycling, + R.string.activity_notification_cycling_content + ) + DetectedActivity.IN_VEHICLE -> + NotificationContent( + R.string.activity_notification_vehicle, + R.string.activity_notification_vehicle_content + ) + else -> throw IllegalArgumentException( + "Activity not recognized" + ) + } + val emojiCompat = emojiLoader.await() + var title = context.getText(notificationContent.title) + var content = context.getText(notificationContent.content) + try { + title = emojiCompat.process(title) + content = emojiCompat.process(content) + } catch (_: IllegalStateException) { + } + withContext(Dispatchers.Main) { + notificationsHandler.createNotification( + iconDrawable = R.drawable.ic_stat_handwashing, + largeIcon = R.drawable.handwashing_app_logo, + title = title, + content = content, + longContent = content + ) } } +} - private data class NotificationContent( - @StringRes val title: Int, - @StringRes val content: Int - ) -} \ No newline at end of file +private data class NotificationContent( + @StringRes val title: Int, + @StringRes val content: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/graphics/LottieAdaptedPerformanceAnimationView.kt b/app/src/main/java/com/javinator9889/handwashingreminder/graphics/LottieAdaptedPerformanceAnimationView.kt index 5f6af41..708d9e9 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/graphics/LottieAdaptedPerformanceAnimationView.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/graphics/LottieAdaptedPerformanceAnimationView.kt @@ -35,11 +35,11 @@ class LottieAdaptedPerformanceAnimationView : LottieAnimationView, init { addLottieOnCompositionLoadedListener(this) enableMergePathsForKitKatAndAbove(true) + setCacheComposition(true) } - override fun getDuration(): Long { - return if (isHighPerformingDevice()) super.getDuration() else 100L - } + override fun getDuration(): Long = + if (isHighPerformingDevice()) super.getDuration() else 100L override fun onCompositionLoaded(composition: LottieComposition?) { if (!isHighPerformingDevice()) { diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/graphics/RecyclingImageView.java b/app/src/main/java/com/javinator9889/handwashingreminder/graphics/RecyclingImageView.java index c19ce18..194d5c9 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/graphics/RecyclingImageView.java +++ b/app/src/main/java/com/javinator9889/handwashingreminder/graphics/RecyclingImageView.java @@ -20,6 +20,8 @@ import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; /** @@ -27,6 +29,8 @@ * being displayed. */ public class RecyclingImageView extends AppCompatImageView { + @DrawableRes + private Integer mSavedDrawableRes = null; public RecyclingImageView(Context context) { super(context); @@ -36,11 +40,39 @@ public RecyclingImageView(Context context, AttributeSet attrs) { super(context, attrs); } + public void setSavedDrawableRes(@Nullable @DrawableRes Integer mSavedDrawableRes) { + this.mSavedDrawableRes = mSavedDrawableRes; + } + + @Nullable + @DrawableRes + public Integer getSavedDrawableRes() { + return mSavedDrawableRes; + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + if (mSavedDrawableRes != null && + visibility == VISIBLE && + getDrawable() == null) { + try { + GlideApp.with(this) + .load(mSavedDrawableRes) + .centerInside() + .into(this); + } catch (Exception ignored) { + setImageResource(mSavedDrawableRes); + } + } else if (visibility == INVISIBLE || visibility == GONE) + onDetachedFromWindow(); + } + /** * @see android.widget.ImageView#onDetachedFromWindow() */ @Override - protected void onDetachedFromWindow() { + public void onDetachedFromWindow() { // This has been detached from Window, so clear the drawable setImageDrawable(null); diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/BootCompletedJob.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/BootCompletedJob.kt index 69d0cdd..86c662e 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/BootCompletedJob.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/BootCompletedJob.kt @@ -22,13 +22,13 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.javinator9889.handwashingreminder.application.HandwashingApplication -import com.javinator9889.handwashingreminder.jobs.workers.WorkHandler +import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler import com.javinator9889.handwashingreminder.utils.Preferences import timber.log.Timber class BootCompletedJob : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { val app = HandwashingApplication.getInstance() val preferences = app.sharedPreferences if (preferences.getBoolean( @@ -39,15 +39,9 @@ class BootCompletedJob : BroadcastReceiver() { app.activityHandler.startTrackingActivity() else app.activityHandler.disableActivityTracker() - try { - Timber.d("Enqueuing notifications as the device has rebooted") - with(WorkHandler(requireNotNull(context))) { - enqueuePeriodicNotificationsWorker() - } - } catch (_: IllegalArgumentException) { - Timber.w( - "Context is null so notifications cannot be scheduled" - ) + Timber.d("Enqueuing notifications as the device has rebooted") + with(AlarmHandler(context)) { + scheduleAllAlarms() } } } diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/ShareReceiver.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/ShareReceiver.kt new file mode 100644 index 0000000..17b1bd7 --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/ShareReceiver.kt @@ -0,0 +1,65 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 27/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.jobs + +import android.content.* +import com.javinator9889.handwashingreminder.R +import com.javinator9889.handwashingreminder.utils.getUriFromRes +import timber.log.Timber + +class ShareReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Timber.d("Receiver broadcast") + with(Intent.createChooser(Intent().apply { + action = Intent.ACTION_SEND + putExtra( + Intent.EXTRA_TEXT, + context.getText(R.string.share_text) + ) + putExtra( + Intent.EXTRA_TITLE, + context.getText(R.string.share_title) + ) + ClipData.Item( + getUriFromRes( + context, + R.drawable.handwashing_app_logo + ) + ) + clipData = ClipData( + ClipDescription( + context.getString(R.string.share_label), + arrayOf("image/*") + ), + ClipData.Item( + getUriFromRes( + context, + R.drawable.handwashing_app_logo + ) + ) + ) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + + type = "text/plain" + }, null)) { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/UpdateReceiver.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/UpdateReceiver.kt index df52419..6627b93 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/UpdateReceiver.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/UpdateReceiver.kt @@ -21,21 +21,15 @@ package com.javinator9889.handwashingreminder.jobs import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.javinator9889.handwashingreminder.jobs.workers.WorkHandler +import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler import timber.log.Timber class UpdateReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == Intent.ACTION_MY_PACKAGE_REPLACED) { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) { Timber.d("Package updated so rescheduling jobs") - try { - with(WorkHandler(requireNotNull(context))) { - enqueuePeriodicNotificationsWorker(true) - } - } catch (_: IllegalArgumentException) { - Timber.w( - "Context is null so notifications cannot be rescheduled" - ) + with(AlarmHandler(context)) { + scheduleAllAlarms() } } } diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/AlarmHandler.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/AlarmHandler.kt new file mode 100644 index 0000000..e1c9da6 --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/AlarmHandler.kt @@ -0,0 +1,90 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 23/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.jobs.alarms + +import android.app.AlarmManager +import android.app.AlarmManager.RTC_WAKEUP +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.annotation.IntRange +import androidx.core.app.AlarmManagerCompat +import androidx.preference.PreferenceManager +import com.javinator9889.handwashingreminder.utils.timeAt +import timber.log.Timber + +internal const val IDENTIFIER = "intent:id" + +class AlarmHandler(private val context: Context) { + private val alarmManager = + context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + fun scheduleAlarm(alarm: Alarms) { + try { + cancelAlarm(alarm) + val pendingIntent = createPendingIntentForAlarm(alarm) + val alarmTime = getTimeForAlarm(alarm) + val scheduleTime = timeAt(alarmTime.hour, alarmTime.minute) + AlarmManagerCompat.setExactAndAllowWhileIdle( + alarmManager, RTC_WAKEUP, scheduleTime, pendingIntent + ) + } catch (_: IllegalStateException) { + Timber.i("Time values are not initialized yet") + } + } + + fun scheduleAllAlarms() { + cancelAllAlarms() + for (alarm in Alarms.values()) + scheduleAlarm(alarm) + } + + fun cancelAlarm(alarm: Alarms) { + val pendingIntent = createPendingIntentForAlarm(alarm) + alarmManager.cancel(pendingIntent) + } + + fun cancelAllAlarms() { + for (alarm in Alarms.values()) + cancelAlarm(alarm) + } + + private fun createPendingIntentForAlarm(alarm: Alarms): PendingIntent { + return with(Intent(context, AlarmReceiver::class.java)) { + putExtra(IDENTIFIER, alarm.identifier) + PendingIntent.getBroadcast(context, alarm.code, this, 0) + } + } + + private fun getTimeForAlarm(alarm: Alarms): ScheduleTimeData { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + val savedTime = preferences.getString(alarm.preferenceKey, "") + if (savedTime.isNullOrBlank()) + throw IllegalStateException("Time value cannot be null") + val splitTime = savedTime.split(":") + val hour = Integer.parseInt(splitTime[0]) + val minute = Integer.parseInt(splitTime[1]) + return ScheduleTimeData(hour, minute) + } +} + +private data class ScheduleTimeData( + @IntRange(from = 0, to = 23) val hour: Int, + @IntRange(from = 0, to = 23) val minute: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/AlarmReceiver.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/AlarmReceiver.kt new file mode 100644 index 0000000..70b6bfa --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/AlarmReceiver.kt @@ -0,0 +1,40 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 23/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.jobs.alarms + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.javinator9889.handwashingreminder.jobs.workers.BreakfastNotificationWorker +import com.javinator9889.handwashingreminder.jobs.workers.DinnerNotificationWorker +import com.javinator9889.handwashingreminder.jobs.workers.LunchNotificationWorker +import com.javinator9889.handwashingreminder.utils.goAsync + +class AlarmReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val worker = when (intent.getStringExtra(IDENTIFIER)) { + Alarms.BREAKFAST_ALARM.identifier -> + BreakfastNotificationWorker(context) + Alarms.LUNCH_ALARM.identifier -> LunchNotificationWorker(context) + Alarms.DINNER_ALARM.identifier -> DinnerNotificationWorker(context) + else -> return + } + goAsync { worker.doWork() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/Alarms.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/Alarms.kt new file mode 100644 index 0000000..4353aa2 --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/alarms/Alarms.kt @@ -0,0 +1,31 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 23/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.jobs.alarms + +import com.javinator9889.handwashingreminder.utils.Preferences + +enum class Alarms( + val identifier: String, + val code: Int, + val preferenceKey: String +) { + BREAKFAST_ALARM("alarms:breakfast", 0, Preferences.BREAKFAST_TIME), + LUNCH_ALARM("alarms:lunch", 1, Preferences.LUNCH_TIME), + DINNER_ALARM("alarms:dinner", 2, Preferences.DINNER_TIME) +} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/AbstractNotificationsWorker.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/AbstractNotificationsWorker.kt deleted file mode 100644 index 29f7136..0000000 --- a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/AbstractNotificationsWorker.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright © 2020 - present | Handwashing reminder by Javinator9889 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - * - * Created by Javinator9889 on 22/04/20 - Handwashing reminder. - */ -package com.javinator9889.handwashingreminder.jobs.workers - -import android.content.Context -import androidx.annotation.ArrayRes -import androidx.annotation.IntRange -import androidx.annotation.StringRes -import androidx.preference.PreferenceManager -import androidx.work.* -import com.javinator9889.handwashingreminder.R -import com.javinator9889.handwashingreminder.application.HandwashingApplication -import com.javinator9889.handwashingreminder.emoji.EmojiLoader -import com.javinator9889.handwashingreminder.notifications.NotificationsHandler -import com.javinator9889.handwashingreminder.utils.TIME_CHANNEL_ID -import com.javinator9889.handwashingreminder.utils.runAt -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import timber.log.Timber -import java.util.concurrent.TimeUnit - -data class WorkScheduleParams( - @IntRange(from = 0, to = 23) val hour: Int, - @IntRange(from = 0, to = 59) val minute: Int -) - -abstract class AbstractNotificationsWorker( - context: Context, - params: WorkerParameters -) : CoroutineWorker(context, params) { - protected var maxRetries = 5 - protected var shouldScheduleNext = true - protected var workConstraints = with(Constraints.Builder()) { - setRequiredNetworkType(NetworkType.NOT_REQUIRED) - setRequiresBatteryNotLow(false) - setRequiresCharging(false) - setRequiresDeviceIdle(false) - setRequiresStorageNotLow(false) - build() - } - protected abstract val clazz: Class - protected abstract val workUniqueName: String - protected abstract val preferencesKey: String - protected abstract val titleRes: Int - protected abstract val commentsRes: Int - protected val workParams: WorkScheduleParams - get() { - val preferences = - PreferenceManager.getDefaultSharedPreferences(applicationContext) - val time = preferences.getString(preferencesKey, "") - if (time == "" || time == null) - throw IllegalStateException("Time value cannot be null") - val splitTime = time.split(":") - val hour = Integer.parseInt(splitTime[0]) - val minute = Integer.parseInt(splitTime[1]) - return WorkScheduleParams(hour, minute) - } - - override suspend fun doWork(): Result = coroutineScope { - with(HandwashingApplication.getInstance()) { - withTimeoutOrNull(10_000L) { - firebaseInitDeferred.await() - } - } - shouldScheduleNext = true - var data: Data? = null - try { - data = work() - Result.success() - } catch (e: Exception) { - catchBlock(e) - } finally { - if (shouldScheduleNext) { - scheduleNext(data) - } - } - } - - protected open suspend fun work(): Data? { - val emojiLoader = EmojiLoader.get(applicationContext) - val notificationsHandler = NotificationsHandler( - context = applicationContext, - channelId = TIME_CHANNEL_ID, - channelName = getString(R.string.time_notification_channel_name), - channelDesc = getString(R.string.time_notification_channel_desc) - ) - val emojiCompat = emojiLoader.await() - var title: CharSequence - var content: CharSequence - try { - title = emojiCompat.process(getText(titleRes)) - content = emojiCompat.process( - getStringArray(commentsRes).toList().random() - ) - } catch (_: IllegalStateException) { - title = getText(titleRes) - content = getStringArray(commentsRes).toList().random() - } - withContext(Dispatchers.Main) { - notificationsHandler.createNotification( - iconDrawable = R.drawable.ic_stat_handwashing, - largeIcon = R.drawable.handwashing_app_logo, - title = title, - content = content, - longContent = content - ) - } - return null - } - - protected fun catchBlock(e: Exception): Result { - if (runAttemptCount >= maxRetries) { - Timber.d("Exceeded max attempts: $maxRetries") - return Result.failure() - } - return when (e.cause) { - is IllegalStateException -> { - Timber.w(e, "IllegalStateException on worker class") - shouldScheduleNext = false - Result.retry() - } - else -> { - Timber.e(e, "Uncaught exception on worker class") - Result.failure() - } - } - } - - protected fun scheduleNext(data: Data?) { - val workManager = WorkManager.getInstance(applicationContext) - val nextExecutionDelay = runAt(workParams.hour, workParams.minute) - Timber.d("Executing $workUniqueName in ${nextExecutionDelay / 1000} s") - val jobRequest = with(OneTimeWorkRequest.Builder(clazz)) { - data?.let { setInputData(it) } - setInitialDelay(nextExecutionDelay, TimeUnit.MILLISECONDS) - setConstraints(workConstraints) - build() - } - workManager.enqueueUniqueWork( - workUniqueName, ExistingWorkPolicy.REPLACE, jobRequest - ) - } - - private fun getString(@StringRes resId: Int): String = - applicationContext.getString(resId) - - private fun getText(@StringRes resId: Int): CharSequence = - applicationContext.getText(resId) - - private fun getStringArray(@ArrayRes resId: Int): Array = - applicationContext.resources.getStringArray(resId) -} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/BreakfastWorker.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/BreakfastNotificationWorker.kt similarity index 58% rename from app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/BreakfastWorker.kt rename to app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/BreakfastNotificationWorker.kt index 252fad2..e10187d 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/BreakfastWorker.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/BreakfastNotificationWorker.kt @@ -14,23 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. * - * Created by Javinator9889 on 22/04/20 - Handwashing reminder. + * Created by Javinator9889 on 23/04/20 - Handwashing reminder. */ package com.javinator9889.handwashingreminder.jobs.workers import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerParameters import com.javinator9889.handwashingreminder.R -import com.javinator9889.handwashingreminder.utils.Preferences -import com.javinator9889.handwashingreminder.utils.Workers +import com.javinator9889.handwashingreminder.jobs.alarms.Alarms -class BreakfastWorker(context: Context, params: WorkerParameters) : - AbstractNotificationsWorker(context, params) { - override val clazz: Class = - BreakfastWorker::class.java - override val workUniqueName: String = Workers.BREAKFAST_UUID - override val preferencesKey: String = Preferences.BREAKFAST_TIME +class BreakfastNotificationWorker(context: Context) : + ScheduledNotificationWorker(context) { + override val alarm: Alarms = Alarms.BREAKFAST_ALARM override val titleRes: Int = R.string.breakfast_title - override val commentsRes: Int = R.array.breakfast_comments + override val contentsRes: Int = R.array.breakfast_comments } \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/DinnerWorker.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/DinnerNotificationWorker.kt similarity index 58% rename from app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/DinnerWorker.kt rename to app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/DinnerNotificationWorker.kt index 8465623..b706a26 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/DinnerWorker.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/DinnerNotificationWorker.kt @@ -14,22 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. * - * Created by Javinator9889 on 22/04/20 - Handwashing reminder. + * Created by Javinator9889 on 23/04/20 - Handwashing reminder. */ package com.javinator9889.handwashingreminder.jobs.workers import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerParameters import com.javinator9889.handwashingreminder.R -import com.javinator9889.handwashingreminder.utils.Preferences -import com.javinator9889.handwashingreminder.utils.Workers +import com.javinator9889.handwashingreminder.jobs.alarms.Alarms -class DinnerWorker(context: Context, params: WorkerParameters) : - AbstractNotificationsWorker(context, params) { - override val clazz: Class = DinnerWorker::class.java - override val workUniqueName: String = Workers.DINNER_UUID - override val preferencesKey: String = Preferences.DINNER_TIME +class DinnerNotificationWorker(context: Context) : + ScheduledNotificationWorker(context) { + override val alarm: Alarms = Alarms.DINNER_ALARM override val titleRes: Int = R.string.dinner_title - override val commentsRes: Int = R.array.dinner_comments + override val contentsRes: Int = R.array.dinner_comments } \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/LunchWorker.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/LunchNotificationWorker.kt similarity index 58% rename from app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/LunchWorker.kt rename to app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/LunchNotificationWorker.kt index 404c102..c75f2d0 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/LunchWorker.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/LunchNotificationWorker.kt @@ -14,22 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. * - * Created by Javinator9889 on 22/04/20 - Handwashing reminder. + * Created by Javinator9889 on 23/04/20 - Handwashing reminder. */ package com.javinator9889.handwashingreminder.jobs.workers import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerParameters import com.javinator9889.handwashingreminder.R -import com.javinator9889.handwashingreminder.utils.Preferences -import com.javinator9889.handwashingreminder.utils.Workers +import com.javinator9889.handwashingreminder.jobs.alarms.Alarms -class LunchWorker(context: Context, params: WorkerParameters) : - AbstractNotificationsWorker(context, params) { - override val clazz: Class = LunchWorker::class.java - override val workUniqueName: String = Workers.LUNCH_UUID - override val preferencesKey: String = Preferences.LUNCH_TIME +class LunchNotificationWorker(context: Context) : + ScheduledNotificationWorker(context) { + override val alarm: Alarms = Alarms.LUNCH_ALARM override val titleRes: Int = R.string.lunch_title - override val commentsRes: Int = R.array.lunch_comments + override val contentsRes: Int = R.array.lunch_comments } \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/ScheduledNotificationWorker.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/ScheduledNotificationWorker.kt new file mode 100644 index 0000000..6335e8f --- /dev/null +++ b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/ScheduledNotificationWorker.kt @@ -0,0 +1,102 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 23/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.jobs.workers + +import android.content.Context +import androidx.annotation.ArrayRes +import androidx.annotation.StringRes +import com.javinator9889.handwashingreminder.R +import com.javinator9889.handwashingreminder.application.HandwashingApplication +import com.javinator9889.handwashingreminder.emoji.EmojiLoader +import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler +import com.javinator9889.handwashingreminder.jobs.alarms.Alarms +import com.javinator9889.handwashingreminder.notifications.NotificationsHandler +import com.javinator9889.handwashingreminder.utils.TIME_CHANNEL_ID +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import timber.log.Timber + +abstract class ScheduledNotificationWorker(context: Context) { + protected val context: Context = context.applicationContext + protected abstract val alarm: Alarms + protected abstract val titleRes: Int + protected abstract val contentsRes: Int + + suspend fun doWork() = coroutineScope { + try { + val startTime = System.currentTimeMillis() + val emojiLoader = EmojiLoader.get(context) + val notificationsHandler = NotificationsHandler( + context = context, + channelId = TIME_CHANNEL_ID, + channelName = getString(R.string.time_notification_channel_name), + channelDesc = getString(R.string.time_notification_channel_desc) + ) + val emojiCompat = emojiLoader.await() + var title = getText(titleRes) + var content = + getStringArray(contentsRes).toList().random() as CharSequence + try { + title = emojiCompat.process(title) + content = emojiCompat.process(content) + } catch (_: IllegalStateException) { } + withContext(Dispatchers.Main) { + notificationsHandler.createNotification( + iconDrawable = R.drawable.ic_stat_handwashing, + largeIcon = R.drawable.handwashing_app_logo, + title = title, + content = content, + longContent = content + ) + } + Timber.d( + "Posting a notification took: ${System + .currentTimeMillis() - startTime}ms" + ) + } catch (e: Exception) { + with(HandwashingApplication.getInstance()) { + // 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 + // here there is no need to wait + withTimeoutOrNull(500L) { + firebaseInitDeferred.await() + } + } + Timber.e(e, "Unhandled exception on worker class") + // We don't want to keep using CPU at this time if the request + // fails so schedule next execution + } finally { + with(AlarmHandler(context)) { + scheduleAlarm(alarm) + } + } + } + + private fun getString(@StringRes resId: Int): String = + context.getString(resId) + + private fun getText(@StringRes resId: Int): CharSequence = + context.getText(resId) + + private fun getStringArray(@ArrayRes resId: Int): Array = + context.resources.getStringArray(resId) +} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/WorkHandler.kt b/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/WorkHandler.kt deleted file mode 100644 index b04539d..0000000 --- a/app/src/main/java/com/javinator9889/handwashingreminder/jobs/workers/WorkHandler.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright © 2020 - present | Handwashing reminder by Javinator9889 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - * - * Created by Javinator9889 on 11/04/20 - Handwashing reminder. - */ -package com.javinator9889.handwashingreminder.jobs.workers - -import android.content.Context -import androidx.preference.PreferenceManager -import androidx.work.* -import com.javinator9889.handwashingreminder.utils.Preferences -import com.javinator9889.handwashingreminder.utils.Workers -import com.javinator9889.handwashingreminder.utils.runAt -import timber.log.Timber -import java.util.concurrent.TimeUnit - -data class Who( - val uuid: String, - val id: Int, - val clazz: Class -) - -class WorkHandler(private val context: Context) { - private val workManager: WorkManager - get() = WorkManager.getInstance(context) - - fun enqueuePeriodicNotificationsWorker(forceUpdate: Boolean = false) { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - - val breakfastTime = - preferences.getString(Preferences.BREAKFAST_TIME, "")!! - val lunchTime = preferences.getString(Preferences.LUNCH_TIME, "")!! - val dinnerTime = preferences.getString(Preferences.DINNER_TIME, "")!! - if (breakfastTime == "" || lunchTime == "" || dinnerTime == "") { - Timber.e("The scheduled times are not initialized") - return - } - val times = arrayOf(breakfastTime, lunchTime, dinnerTime) - times.forEach { time -> - val splittedTime = time.split(":") - val hour = Integer.parseInt(splittedTime[0].trim()) - val minute = Integer.parseInt(splittedTime[1].trim()) - - val timeDiff = runAt(hour, minute) - val who = when (time) { - breakfastTime -> Who( - Workers.BREAKFAST_UUID, - Workers.BREAKFAST, - BreakfastWorker::class.java - ) - lunchTime -> Who( - Workers.LUNCH_UUID, - Workers.LUNCH, - LunchWorker::class.java - ) - dinnerTime -> Who( - Workers.DINNER_UUID, - Workers.DINNER, - DinnerWorker::class.java - ) - else -> { - Timber.e("Unmatched time: $time against $times") - return - } - } - Timber.i( - "Scheduled activity ${who.uuid} in $timeDiff ms" - ) - - val jobRequest = createJobRequest(timeDiff, who.clazz) - - val policy = if (forceUpdate) - ExistingWorkPolicy.REPLACE - else - ExistingWorkPolicy.KEEP - - with(workManager) { - enqueueUniqueWork( - who.uuid, - policy, - jobRequest - ) - } - } - } - - private fun createJobRequest( - initialDelayMillis: Long, - clazz: Class, - inputData: Data? = null - ): OneTimeWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.NOT_REQUIRED) - .setRequiresBatteryNotLow(false) - .setRequiresCharging(false) - .setRequiresDeviceIdle(false) - .setRequiresStorageNotLow(false) - .build() - return with(OneTimeWorkRequest.Builder(clazz)) { - setInitialDelay(initialDelayMillis, TimeUnit.MILLISECONDS) - inputData?.let { setInputData(it) } - setConstraints(constraints) - setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, - 15000L, - TimeUnit.MILLISECONDS - ) - build() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/network/HttpDownloader.kt b/app/src/main/java/com/javinator9889/handwashingreminder/network/HttpDownloader.kt index fb2eb75..ad4dced 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/network/HttpDownloader.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/network/HttpDownloader.kt @@ -18,18 +18,27 @@ */ package com.javinator9889.handwashingreminder.network -import com.javinator9889.handwashingreminder.network.okhttp.OkHttpDownloader as Downloader +import okhttp3.CacheControl +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.BufferedSource +import java.io.IOException -object HttpDownloader { - fun newInstance(): OkHttpDownloader { - /*val className = if (isAtLeast(AndroidVersion.LOLLIPOP)) - "${OkHttp.PACKAGE_NAME}.${OkHttp.CLASS_NAME}\$${OkHttp.PROVIDER_NAME}" - else - "${OkHttpLegacy.PACKAGE_NAME}.${OkHttpLegacy - .CLASS_NAME}\$${OkHttpLegacy.PROVIDER_NAME}" - val okHttpProvider = Class.forName(className).kotlin.objectInstance - as OkHttpDownloader.Provider - return okHttpProvider.newInstance()*/ - return Downloader.newInstance() +class HttpDownloader : OkHttpDownloader { + private val client = OkHttpClient() + + override fun downloadFile(url: String): BufferedSource { + val request = with(Request.Builder()) { + url(url) + cacheControl(CacheControl.FORCE_NETWORK) + build() + } + with(client.newCall(request).execute()) { + if (!isSuccessful) { + close() + throw IOException("Unexpected code $this") + } + return body()!!.source() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/network/okhttp/OkHttpDownloader.kt b/app/src/main/java/com/javinator9889/handwashingreminder/network/okhttp/OkHttpDownloader.kt deleted file mode 100644 index 763497d..0000000 --- a/app/src/main/java/com/javinator9889/handwashingreminder/network/okhttp/OkHttpDownloader.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2020 - present | Handwashing reminder by Javinator9889 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - * - * Created by Javinator9889 on 21/04/20 - Handwashing reminder. - */ -package com.javinator9889.handwashingreminder.network.okhttp - -import com.javinator9889.handwashingreminder.network.OkHttpDownloader -import okhttp3.CacheControl -import okhttp3.OkHttpClient -import okhttp3.Request -import okio.BufferedSource -import java.io.IOException - -class OkHttpDownloader : OkHttpDownloader { - private val client = OkHttpClient() - - companion object Provider : OkHttpDownloader.Provider { - override fun newInstance(): OkHttpDownloader = OkHttpDownloader() - } - - override fun downloadFile(url: String): BufferedSource { - val request = with(Request.Builder()) { - url(url) - cacheControl(CacheControl.FORCE_NETWORK) - build() - } - with(client.newCall(request).execute()) { - if (!isSuccessful) { - close() - throw IOException("Unexpected code $this") - } - return body()!!.source() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/notifications/NotificationsHandler.kt b/app/src/main/java/com/javinator9889/handwashingreminder/notifications/NotificationsHandler.kt index c891965..e16ec20 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/notifications/NotificationsHandler.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/notifications/NotificationsHandler.kt @@ -33,9 +33,11 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.javinator9889.handwashingreminder.R import com.javinator9889.handwashingreminder.activities.FAST_START_KEY import com.javinator9889.handwashingreminder.activities.LauncherActivity import com.javinator9889.handwashingreminder.activities.PENDING_INTENT_CODE +import com.javinator9889.handwashingreminder.jobs.ShareReceiver import com.javinator9889.handwashingreminder.utils.AndroidVersion import com.javinator9889.handwashingreminder.utils.Preferences import com.javinator9889.handwashingreminder.utils.isAtLeast @@ -122,7 +124,8 @@ class NotificationsHandler( longContent: CharSequence? = null ) { val notifyIntent = Intent(context, LauncherActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK putExtra(FAST_START_KEY, true) } val notifyPendingIntent = PendingIntent.getActivity( @@ -131,6 +134,12 @@ class NotificationsHandler( notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT ) + val sharePendingIntent = PendingIntent.getBroadcast( + context, + 0, + Intent(context, ShareReceiver::class.java), + 0 + ) with(NotificationCompat.Builder(context, channelId)) { setSmallIcon(iconDrawable) setLargeIcon(largeIcon) @@ -139,6 +148,11 @@ class NotificationsHandler( setPriority(priority) setVibrate(vibrationPattern) setContentIntent(notifyPendingIntent) + addAction( + R.drawable.ic_share_black, + context.getString(R.string.share), + sharePendingIntent + ) setAutoCancel(true) longContent.notNull { setStyle(NotificationCompat.BigTextStyle().bigText(longContent)) diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/utils/Android.kt b/app/src/main/java/com/javinator9889/handwashingreminder/utils/Android.kt index 255e8ed..7e0a8d7 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/utils/Android.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/utils/Android.kt @@ -19,18 +19,26 @@ package com.javinator9889.handwashingreminder.utils import android.app.ActivityManager +import android.content.BroadcastReceiver import android.content.ContentResolver import android.content.Context import android.content.pm.ApplicationInfo +import android.graphics.Rect import android.net.ConnectivityManager import android.net.NetworkCapabilities.* import android.net.Uri import android.os.Build +import android.view.View import androidx.annotation.AnyRes import com.google.android.play.core.splitinstall.SplitInstallManager import com.google.android.play.core.splitinstall.SplitInstallManagerFactory +import com.google.firebase.perf.FirebasePerformance +import com.google.firebase.perf.metrics.Trace import com.javinator9889.handwashingreminder.BuildConfig import com.javinator9889.handwashingreminder.application.HandwashingApplication +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch fun isAtLeast(version: AndroidVersion): Boolean = @@ -108,3 +116,38 @@ fun isModuleInstalled(context: Context, module: String): Boolean = fun isModuleInstalled(manager: SplitInstallManager, module: String): Boolean = module in manager.installedModules + +// https://github.com/romannurik/muzei/blob/master/extensions/src/main/java/com/google/android/apps/muzei/util/BroadcastReceiverExt.kt +fun BroadcastReceiver.goAsync( + coroutineScope: CoroutineScope = GlobalScope, + block: suspend () -> Unit +) { + val result = goAsync() + coroutineScope.launch { + try { + block() + } finally { + // Always call finish, even if the coroutine scope was cancelled + result.finish() + } + } +} + +inline fun trace(name: String, block: (Trace) -> T): T { + with(FirebasePerformance.startTrace(name)) { + return try { + block(this) + } finally { + stop() + } + } +} + +fun T.isViewVisible(container: View?): Boolean { + if (container == null) return true + val scrollBounds = Rect() + container.getDrawingRect(scrollBounds) + + val bottom = y + height + return scrollBounds.top < y && scrollBounds.bottom > bottom +} diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/utils/Constants.kt b/app/src/main/java/com/javinator9889/handwashingreminder/utils/Constants.kt index 287ad44..07886d4 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/utils/Constants.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/utils/Constants.kt @@ -42,6 +42,7 @@ class Preferences { DetectedActivity.WALKING.toString() ) const val DONATIONS = "donations" + const val INITIAL_TUTORIAL_DONE = "app:tutorial:is_done" } } diff --git a/app/src/main/java/com/javinator9889/handwashingreminder/utils/Time.kt b/app/src/main/java/com/javinator9889/handwashingreminder/utils/Time.kt index 1bb51da..f49c0c6 100644 --- a/app/src/main/java/com/javinator9889/handwashingreminder/utils/Time.kt +++ b/app/src/main/java/com/javinator9889/handwashingreminder/utils/Time.kt @@ -19,9 +19,7 @@ package com.javinator9889.handwashingreminder.utils import androidx.annotation.IntRange -import java.time.Duration -import java.time.LocalDateTime -import java.time.LocalTime +import java.time.* import java.time.temporal.ChronoUnit import java.util.* import kotlin.math.abs @@ -48,10 +46,10 @@ fun runAt( .withMinute(alarmTime.minute) abs(Duration.between(LocalDateTime.now(), now).toMillis()) } else { - // get now time and truncate it to minutes + // get current time val now = Calendar.getInstance() - // clone now time truncated to minutes and set the specified hour and - // minute in the new Calendar object + // get again current time but truncate it to minutes and with the + // specified hour:minute provided val alarm = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, hour) set(Calendar.MINUTE, minute) @@ -67,3 +65,46 @@ fun runAt( abs(alarm.timeInMillis - now.timeInMillis) } +fun timeAt( + @IntRange(from = 0, to = 23) hour: Int, + @IntRange(from = 0, to = 59) minute: Int +): Long = + if (isAtLeast(AndroidVersion.O)) { + // trigger at hour:minute + val alarmTime = LocalTime.of(hour, minute) + // obtain local date-time with fixed timezone (system default) + var now = LocalDateTime.now() + .atZone(ZoneId.systemDefault()) + .truncatedTo(ChronoUnit.MINUTES) + val nowTime = now.toLocalTime() + // check if is the same time or if today's time has passed so + // then schedule for next day + if (nowTime == alarmTime || nowTime.isAfter(alarmTime)) { + now = now.plusDays(1) + } + val scheduledTime = now + .withHour(alarmTime.hour) + .withMinute(alarmTime.minute) + .withZoneSameInstant(ZoneOffset.UTC) + scheduledTime.toInstant().toEpochMilli() + } else { + // get now time and set to system's millis + val now = Calendar.getInstance() + .apply { timeInMillis = System.currentTimeMillis() } + // clone now calendar epoch time and set the specified hour and + // minute in the new Calendar object + val alarm = (now.clone() as Calendar).apply { + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + val nowTime = now.time + val alarmTime = alarm.time + // check if they are the same time or if today's time has passed so + // then schedule for next day + if (nowTime == alarmTime || nowTime.after(alarmTime)) { + alarm.add(Calendar.HOUR_OF_DAY, 24) + } + alarm.timeInMillis + } diff --git a/app/src/main/res/drawable/ic_share_black.xml b/app/src/main/res/drawable/ic_share_black.xml new file mode 100644 index 0000000..790a740 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_black.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout-land/under_construction.xml b/app/src/main/res/layout-land/under_construction.xml new file mode 100644 index 0000000..f49bd2a --- /dev/null +++ b/app/src/main/res/layout-land/under_construction.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/wash_your_hands_demo.xml b/app/src/main/res/layout-land/wash_your_hands_demo.xml new file mode 100644 index 0000000..aec8d00 --- /dev/null +++ b/app/src/main/res/layout-land/wash_your_hands_demo.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/wash_your_hands_first_slide.xml b/app/src/main/res/layout-land/wash_your_hands_first_slide.xml new file mode 100644 index 0000000..8711067 --- /dev/null +++ b/app/src/main/res/layout-land/wash_your_hands_first_slide.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index deb4cad..f7bc7c0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,39 +1,45 @@ - + android:layout_height="match_parent"> - + - + - + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/dynamic_content_pb.xml b/app/src/main/res/layout/dynamic_content_pb.xml index 477293c..27e8018 100644 --- a/app/src/main/res/layout/dynamic_content_pb.xml +++ b/app/src/main/res/layout/dynamic_content_pb.xml @@ -7,13 +7,11 @@ android:background="#F0EEF3"> + app:layout_constraintTop_toBottomOf="@+id/dynamic_content_title" + app:lottie_autoPlay="true" + app:lottie_loop="true" + app:lottie_rawRes="@raw/hamster" /> + app:tabIndicatorHeight="0dp" + app:tabMaxWidth="32dp" + app:tabMode="auto" /> - diff --git a/app/src/main/res/layout/splash_screen.xml b/app/src/main/res/layout/splash_screen.xml index 2a807ad..91f5235 100644 --- a/app/src/main/res/layout/splash_screen.xml +++ b/app/src/main/res/layout/splash_screen.xml @@ -7,7 +7,7 @@ + + - - - Intro de Handwashing reminder Política de privacidad Términos y condiciones Permite que la aplicación @@ -45,7 +44,6 @@ Permite que la aplicación monitorice el rendimiento de la misma y envíe datos anónimos para mejorar la experiencia de usuario - Anuncios Ad Construyendo tu aplicación… @@ -251,4 +249,20 @@ Prevención Escrito por %s Disponible en: %s + Listo + Aquí tienes información sobre algunas + enfermedades + Aquí puedes aprender a cómo lavarte + completamente las manos + ¿Buscando las últimas noticias? Aquí + estarán disponibles (todavía está bajo desarrollo) + Finalmente, aquí podrás configurar la + aplicación para que encaje con tus gustos y preferencias + Valorar Handwashing reminder + ¿Estás disfrutando Handwashing reminder? + Valora la aplicación en la Play Store y apoya el desarrollo + 😄. ¡De esta manera podrás ayudar a más gente! Solo te llevará + unos segundos 😉 + Valorar + No preguntar de nuevo diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8741206..a244424 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,7 +36,7 @@ try to wash your hands as soon as you can for avoiding diseases 💦👏 - Handwashing reminder intro + Handwashing reminder intro Privacy policy Terms and conditions Allow the application to @@ -45,7 +45,7 @@ Allow the application to track its performance and send anonymous data for improving in-app experience - Ads + Ads Ad Building your app… We are adding new @@ -277,4 +277,20 @@ Available at: %s OkHttp Ok HTTP Legacy + Got it! + Here you have information about some + diseases + Here you can learn how to completely wash + your hands + Looking for latest news? Here you will + have it (is still under construction) + Finally, here you will be able to + change the application settings so it can fit your requirements + Rate Handwashing reminder + Do you enjoy Handwashing reminder? + Consider rating the application on Play Store and supporting + the development 😄. In this way you can help more + people! It only takes a few seconds 😉 + Rate! + Don\'t ask again diff --git a/appintro/build.gradle b/appintro/build.gradle index 5dbb6a6..d26d497 100644 --- a/appintro/build.gradle +++ b/appintro/build.gradle @@ -40,41 +40,9 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':app') - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - // https://developers.google.com/android/guides/setup - implementation 'com.google.android.gms:play-services-location:17.0.0' - // https://material.io/develop/android/docs/getting-started/ - implementation 'com.google.android.material:material:1.1.0' - // https://github.com/Javinator9889/LocaleManager - implementation 'com.github.javinator9889:localemanager:1.1X' + // https://github.com/AppIntro/AppIntro implementation 'com.github.AppIntro:AppIntro:5.1.0' - // https://developer.android.com/jetpack/androidx/releases/cardview - implementation 'androidx.cardview:cardview:1.0.0' - // https://developer.android.com/jetpack/androidx/releases/recyclerview - implementation 'androidx.recyclerview:recyclerview:1.1.0' - // https://github.com/mikepenz/Android-Iconics - implementation 'com.mikepenz:iconics-core:5.0.2' - implementation 'com.mikepenz:iconics-views:5.0.2' - //noinspection GradleDependency - implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar' - implementation 'com.mikepenz:ionicons-typeface:2.0.1.5-kotlin@aar' - // https://developer.android.com/kotlin/ktx#play-core - implementation 'com.google.android.play:core-ktx:1.7.0' - // https://developer.android.com/studio/build/multidex - implementation 'androidx.multidex:multidex:2.0.1' - // http://airbnb.io/lottie/#/android?id=getting-started - implementation "com.airbnb.android:lottie:3.4.0" - // https://github.com/JakeWharton/timber - implementation 'com.jakewharton.timber:timber:4.7.1' - // https://firebase.google.com/docs/android/setup#add-sdks - implementation 'com.google.firebase:firebase-common-ktx:19.3.0' - implementation 'com.google.firebase:firebase-analytics:17.3.0' - implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04' - implementation 'com.google.firebase:firebase-perf:19.0.6' - // https://github.com/bumptech/glide - implementation 'com.github.bumptech.glide:glide:4.11.0' + // https://github.com/mikepenz/FastAdapter + implementation "com.mikepenz:fastadapter:${latestFastAdapterRelease}" } diff --git a/appintro/src/main/AndroidManifest.xml b/appintro/src/main/AndroidManifest.xml index 78760b8..e08107a 100644 --- a/appintro/src/main/AndroidManifest.xml +++ b/appintro/src/main/AndroidManifest.xml @@ -14,12 +14,12 @@ - - + + + + + + diff --git a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/IntroActivity.kt b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/IntroActivity.kt index 9b4275f..39b6830 100644 --- a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/IntroActivity.kt +++ b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/IntroActivity.kt @@ -27,14 +27,9 @@ import android.os.Bundle import android.view.View import android.widget.FrameLayout import androidx.annotation.Keep -import androidx.core.app.ActivityCompat -import androidx.core.app.ActivityOptionsCompat import androidx.core.content.edit -import androidx.core.util.Pair -import androidx.core.util.forEach +import androidx.core.util.set import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.github.paolorotolo.appintro.AppIntro2 import com.github.paolorotolo.appintro.AppIntroViewPager import com.google.android.gms.common.ConnectionResult.SUCCESS @@ -45,25 +40,25 @@ import com.google.android.play.core.splitinstall.SplitInstallManagerFactory import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.perf.FirebasePerformance import com.javinator9889.handwashingreminder.activities.MainActivity -import com.javinator9889.handwashingreminder.appintro.config.TimeConfigActivity import com.javinator9889.handwashingreminder.appintro.custom.SliderPageBuilder import com.javinator9889.handwashingreminder.appintro.fragments.AnimatedAppIntro import com.javinator9889.handwashingreminder.appintro.fragments.SlidePolicyFragment import com.javinator9889.handwashingreminder.appintro.fragments.TimeConfigIntroFragment -import com.javinator9889.handwashingreminder.appintro.timeconfig.TimeConfigViewHolder +import com.javinator9889.handwashingreminder.appintro.fragments.TimeContainer +import com.javinator9889.handwashingreminder.appintro.timeconfig.TimeConfigItem import com.javinator9889.handwashingreminder.appintro.utils.AnimatedResources import com.javinator9889.handwashingreminder.application.HandwashingApplication -import com.javinator9889.handwashingreminder.jobs.workers.WorkHandler -import com.javinator9889.handwashingreminder.listeners.ViewHolder +import com.javinator9889.handwashingreminder.jobs.alarms.AlarmHandler import com.javinator9889.handwashingreminder.utils.* import kotlinx.android.synthetic.main.animated_intro.* import timber.log.Timber import com.javinator9889.handwashingreminder.appintro.R as RIntro +const val TIME_CONFIG_REQUEST_CODE = 16 + @Keep class IntroActivity : AppIntro2(), - ViewHolder.OnItemClickListener, AppIntroViewPager.OnNextPageRequestedListener, View.OnClickListener { private lateinit var activitySlide: Fragment @@ -100,10 +95,18 @@ class IntroActivity : AppIntro2(), .build() addSlide(secondSlide) - timeConfigSlide = TimeConfigIntroFragment() - timeConfigSlide.bgColor = Color.WHITE - timeConfigSlide.listener = this - timeConfigSlide.fromActivity = this + var timeFragment: TimeConfigIntroFragment? = null + if (savedInstanceState != null) { + timeFragment = + supportFragmentManager.getFragment( + savedInstanceState, + TimeConfigIntroFragment::class.simpleName!! + ) as TimeConfigIntroFragment? + } + if (timeFragment == null) { + timeConfigSlide = TimeConfigIntroFragment() + timeConfigSlide.bgColor = Color.WHITE + } else timeConfigSlide = timeFragment addSlide(timeConfigSlide) val gms = GoogleApiAvailability.getInstance() @@ -117,13 +120,22 @@ class IntroActivity : AppIntro2(), addSlide(activitySlide) } - policySlide = SlidePolicyFragment().apply { - title = this@IntroActivity - .getString(RIntro.string.privacy_policy_title) - animatedDrawable = AnimatedResources.PRIVACY - titleColor = Color.DKGRAY - bgColor = Color.WHITE + var policyConfig: SlidePolicyFragment? = null + if (savedInstanceState != null) { + policyConfig = supportFragmentManager.getFragment( + savedInstanceState, + SlidePolicyFragment::class.simpleName!! + ) as SlidePolicyFragment? } + if (policyConfig == null) { + policySlide = SlidePolicyFragment().apply { + title = this@IntroActivity + .getString(RIntro.string.privacy_policy_title) + animatedDrawable = AnimatedResources.PRIVACY + titleColor = Color.DKGRAY + bgColor = Color.WHITE + } + } else policySlide = policyConfig addSlide(policySlide) showSkipButton(false) @@ -133,12 +145,30 @@ class IntroActivity : AppIntro2(), nextButton.setOnClickListener(this) } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + runCatching { + supportFragmentManager.putFragment( + outState, + TimeConfigIntroFragment::class.simpleName!!, + timeConfigSlide + ) + } + runCatching { + supportFragmentManager.putFragment( + outState, + SlidePolicyFragment::class.simpleName!!, + policySlide + ) + } + } + override fun onDonePressed(currentFragment: Fragment?) { super.onDonePressed(currentFragment) val app = HandwashingApplication.getInstance() val sharedPreferences = app.sharedPreferences sharedPreferences.edit(commit = true) { - timeConfigSlide.rvItems.forEach { item -> + timeConfigSlide.itemAdapter.adapterItems.forEach { item -> val time = "${item.hours}:${item.minutes}" when (item.id) { TimeConfig.BREAKFAST_ID -> @@ -181,9 +211,10 @@ class IntroActivity : AppIntro2(), app.activityHandler.startTrackingActivity() else app.activityHandler.disableActivityTracker() - with(WorkHandler(this)) { - enqueuePeriodicNotificationsWorker() + with(AlarmHandler(this)) { + scheduleAllAlarms() } + cacheDir.run { deleteRecursively() } val firebaseAnalytics = FirebaseAnalytics.getInstance(this) with(Bundle(2)) { putBoolean( @@ -215,78 +246,38 @@ class IntroActivity : AppIntro2(), this.finish() } - override fun onItemClick( - viewHolder: RecyclerView.ViewHolder?, - view: View?, - position: Int, - id: Long - ) { - if (viewHolder == null || viewHolder !is TimeConfigViewHolder) - return - val intent = Intent(this, TimeConfigActivity::class.java) - val options = if (isAtLeast(AndroidVersion.LOLLIPOP)) { - val pairs = mutableListOf>() - val items = HashMap(6).apply { - this[TimeConfigActivity.VIEW_TITLE_NAME] = viewHolder.title - this[TimeConfigActivity.INFO_IMAGE_NAME] = viewHolder.image - this[TimeConfigActivity.USER_TIME_ICON] = viewHolder.clockIcon - this[TimeConfigActivity.USER_TIME_HOURS] = viewHolder.hours - this[TimeConfigActivity.USER_DDOT] = viewHolder.ddot - this[TimeConfigActivity.USER_TIME_MINUTES] = viewHolder.minutes - } - val lm = - timeConfigSlide.recyclerView.layoutManager as LinearLayoutManager - if (position <= lm.findLastCompletelyVisibleItemPosition()) { - items.onEach { - pairs.add(Pair.create(it.value, it.key)) - } - } - ActivityOptionsCompat.makeSceneTransitionAnimation( - this, - *pairs.toTypedArray() - ) - } else { - null - } - intent.putExtra( - "title", viewHolder.title.text - ) - intent.putExtra( - "hours", viewHolder.hours.text - ) - intent.putExtra( - "minutes", viewHolder.minutes.text - ) - intent.putExtra("id", id) - ActivityCompat.startActivityForResult( - this, intent, id.toInt(), options?.toBundle() - ) - } - override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { super.onActivityResult(requestCode, resultCode, data) - if (data == null) + if (data == null || requestCode != TIME_CONFIG_REQUEST_CODE) return - val view: TimeConfigViewHolder = when (requestCode.toLong()) { - TimeConfig.BREAKFAST_ID -> { - timeConfigSlide.viewItems[TimeConfig.BREAKFAST_ID.toInt()] - } - TimeConfig.LUNCH_ID -> { - timeConfigSlide.viewItems[TimeConfig.LUNCH_ID.toInt()] + val id = data.getLongExtra("id", 0L) + if (timeConfigSlide.isInitialized) { + val position = data.getIntExtra("position", 0) + val hours = data.getStringExtra("hours") + val minutes = data.getStringExtra("minutes") + val titleText = when(id) { + TimeConfig.BREAKFAST_ID -> getString(RIntro.string.breakfast) + TimeConfig.LUNCH_ID -> getString(RIntro.string.lunch) + TimeConfig.DINNER_ID -> getString(RIntro.string.dinner) + else -> "" } - TimeConfig.DINNER_ID -> { - timeConfigSlide.viewItems[TimeConfig.DINNER_ID.toInt()] - } - else -> null - } as TimeConfigViewHolder - view.hours.text = data.getStringExtra("hours") - view.minutes.text = data.getStringExtra("minutes") - view.saveContentToTextViews() - setSwipeLock() + timeConfigSlide.itemAdapter[position] = TimeConfigItem( + getString(RIntro.string.time_config_title_tpl, titleText), + id, hours, minutes + ) + timeConfigSlide.fastAdapter.notifyAdapterItemChanged(position) + setSwipeLock() + } else { + timeConfigSlide.propertyContainer[id.toInt()] = + TimeContainer( + data.getStringExtra("hours"), + data.getStringExtra("minutes") + ) + } } override fun onSlideChanged( @@ -300,13 +291,15 @@ class IntroActivity : AppIntro2(), setSwipeLock(false) } if (oldFragment == activitySlide) - askForPermissions( - this, - Permission( - Manifest.permission.ACTIVITY_RECOGNITION, - PERMISSIONS_REQUEST_CODE + if (isAtLeast(AndroidVersion.Q)) { + askForPermissions( + this, + Permission( + Manifest.permission.ACTIVITY_RECOGNITION, + PERMISSIONS_REQUEST_CODE + ) ) - ) + } if (newFragment is AnimatedAppIntro || newFragment is SlidePolicyFragment ) @@ -316,8 +309,8 @@ class IntroActivity : AppIntro2(), private fun setSwipeLock() { var swipeLock = false - timeConfigSlide.viewItems.forEach { _, value -> - if (value.hours.text == "" && value.minutes.text == "") { + timeConfigSlide.itemAdapter.adapterItems.forEach { item -> + if (item.hours.isNullOrEmpty() && item.minutes.isNullOrEmpty()) { swipeLock = true return@forEach } @@ -331,9 +324,7 @@ class IntroActivity : AppIntro2(), return when (timeConfigIntroFragment) { timeConfigSlide -> { var isTimeSet = true - if (timeConfigSlide.rvItems.size != 3) - return false - timeConfigSlide.rvItems.forEach { item -> + timeConfigSlide.itemAdapter.adapterItems.forEach { item -> val hours = item.hours val minutes = item.minutes if (hours == "" || minutes == "") { diff --git a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/fragments/TimeConfigIntroFragment.kt b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/fragments/TimeConfigIntroFragment.kt index bc3bd14..0f6a5ef 100644 --- a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/fragments/TimeConfigIntroFragment.kt +++ b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/fragments/TimeConfigIntroFragment.kt @@ -18,107 +18,147 @@ */ package com.javinator9889.handwashingreminder.appintro.fragments -import android.content.Context +import android.content.Intent import android.graphics.Color import android.os.Bundle import android.util.SparseArray import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.core.util.forEach +import androidx.core.app.ActivityCompat +import androidx.core.app.ActivityOptionsCompat +import androidx.core.util.Pair +import androidx.core.util.set import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.github.paolorotolo.appintro.AppIntroBaseFragment -import com.google.android.play.core.splitcompat.SplitCompat import com.javinator9889.handwashingreminder.appintro.R -import com.javinator9889.handwashingreminder.appintro.timeconfig.TimeConfigAdapter -import com.javinator9889.handwashingreminder.appintro.timeconfig.TimeConfigContent -import com.javinator9889.handwashingreminder.appintro.timeconfig.TimeConfigViewHolder -import com.javinator9889.handwashingreminder.listeners.ViewHolder +import com.javinator9889.handwashingreminder.appintro.TIME_CONFIG_REQUEST_CODE +import com.javinator9889.handwashingreminder.appintro.timeconfig.TimeConfigActivity +import com.javinator9889.handwashingreminder.appintro.timeconfig.TimeConfigItem +import com.javinator9889.handwashingreminder.utils.AndroidVersion import com.javinator9889.handwashingreminder.utils.TimeConfig -import com.javinator9889.handwashingreminder.utils.notNull +import com.javinator9889.handwashingreminder.utils.isAtLeast +import com.javinator9889.handwashingreminder.utils.isViewVisible +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.adapters.ItemAdapter +import com.mikepenz.fastadapter.listeners.ClickEventHook +import kotlinx.android.synthetic.main.time_card_view.view.* import kotlinx.android.synthetic.main.time_config.view.* -import com.javinator9889.handwashingreminder.R as RBase class TimeConfigIntroFragment : AppIntroBaseFragment() { - private lateinit var rvAdapter: TimeConfigAdapter - lateinit var rvItems: Array - lateinit var recyclerView: RecyclerView - lateinit var fromActivity: AppCompatActivity var bgColor: Int = Color.WHITE - var listener: ViewHolder.OnItemClickListener? = null - val viewItems = SparseArray(3) - + var isInitialized = false + val propertyContainer = SparseArray(3) + lateinit var recyclerView: RecyclerView + lateinit var fastAdapter: FastAdapter + lateinit var itemAdapter: ItemAdapter - override fun onAttach(context: Context) { - super.onAttach(context) - SplitCompat.installActivity(activity) + init { + propertyContainer[TimeConfig.BREAKFAST_ID.toInt()] = TimeContainer() + propertyContainer[TimeConfig.LUNCH_ID.toInt()] = TimeContainer() + propertyContainer[TimeConfig.DINNER_ID.toInt()] = TimeContainer() } + @Suppress("unchecked_cast") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val breakfast = getString(R.string.breakfast) val lunch = getString(R.string.lunch) val dinner = getString(R.string.dinner) - rvItems = arrayOf( - TimeConfigContent( - getString(R.string.time_config_title_tpl, breakfast), - TimeConfig.BREAKFAST_ID - ), - TimeConfigContent( - getString(R.string.time_config_title_tpl, lunch), - TimeConfig.LUNCH_ID - ), - TimeConfigContent( - getString(R.string.time_config_title_tpl, dinner), - TimeConfig.DINNER_ID + itemAdapter = ItemAdapter().apply { + val items = listOf( + TimeConfigItem( + getString(R.string.time_config_title_tpl, breakfast), + TimeConfig.BREAKFAST_ID, + propertyContainer[TimeConfig.BREAKFAST_ID.toInt()].hours, + propertyContainer[TimeConfig.BREAKFAST_ID.toInt()].minutes + ), + TimeConfigItem( + getString(R.string.time_config_title_tpl, lunch), + TimeConfig.LUNCH_ID, + propertyContainer[TimeConfig.LUNCH_ID.toInt()].hours, + propertyContainer[TimeConfig.LUNCH_ID.toInt()].minutes + ), + TimeConfigItem( + getString(R.string.time_config_title_tpl, dinner), + TimeConfig.DINNER_ID, + propertyContainer[TimeConfig.DINNER_ID.toInt()].hours, + propertyContainer[TimeConfig.DINNER_ID.toInt()].minutes + ) ) - ) - rvAdapter = - TimeConfigAdapter( - rvItems, - listener, - viewItems - ) - } - - @Suppress("unchecked_cast") - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - savedInstanceState.notNull { - val restoredItems = it.getParcelableArray("rvItems") - as Array - restoredItems.forEachIndexed { i, timeConfigContent -> - rvItems[i].hours = timeConfigContent.hours - rvItems[i].minutes = timeConfigContent.minutes - } - viewItems.forEach { _, value -> value.loadContentToTextViews() } + setNewList(items) } } - override fun onSaveInstanceState(outState: Bundle) { - outState.putParcelableArray("rvItems", rvItems) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val view = inflater.inflate(layoutId, container, false) - val rvManager = LinearLayoutManager(context) - fromActivity.setSupportActionBar(view.findViewById(RBase.id.toolbar)) - recyclerView = - view.cardsView.apply { - setHasFixedSize(true) - layoutManager = rvManager - adapter = rvAdapter - } - recyclerView.setBackgroundColor(bgColor) + ): View = inflater.inflate(layoutId, container, false) - return view + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val manager = LinearLayoutManager(context) + fastAdapter = FastAdapter.with(itemAdapter) + recyclerView = view.cardsView.apply { + setHasFixedSize(true) + layoutManager = manager + adapter = fastAdapter + setBackgroundColor(bgColor) + } + isInitialized = true + fastAdapter.addEventHook(object : ClickEventHook() { + override fun onBind(viewHolder: RecyclerView.ViewHolder) = + if (viewHolder is TimeConfigItem.ViewHolder) viewHolder.cardView + else null + + override fun onClick( + v: View, + position: Int, + fastAdapter: FastAdapter, + item: TimeConfigItem + ) { + val intent = Intent(context, TimeConfigActivity::class.java) + val options = if (isAtLeast(AndroidVersion.LOLLIPOP)) { + val pairs = mutableListOf>() + val items = HashMap(6).apply { + this[TimeConfigActivity.VIEW_TITLE_NAME] = v.title + this[TimeConfigActivity.INFO_IMAGE_NAME] = v.infoImage + this[TimeConfigActivity.USER_TIME_ICON] = v.clockIcon + this[TimeConfigActivity.USER_TIME_HOURS] = v.hours + this[TimeConfigActivity.USER_DDOT] = v.ddot + this[TimeConfigActivity.USER_TIME_MINUTES] = v.minutes + } + items.onEach { + if (it.value.isViewVisible(recyclerView)) + pairs.add(Pair.create(it.value, it.key)) + } + ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + *pairs.toTypedArray() + ) + } else { + null + } + intent.apply { + putExtra("title", v.title.text) + putExtra("hours", v.hours.text) + putExtra("minutes", v.minutes.text) + putExtra("id", item.id) + putExtra("position", position) + } + ActivityCompat.startActivityForResult( + requireActivity(), + intent, + TIME_CONFIG_REQUEST_CODE, + options?.toBundle() + ) + } + }) } override fun getLayoutId(): Int = R.layout.time_config -} \ No newline at end of file +} + +data class TimeContainer(val hours: String? = "", val minutes: String? = "") \ No newline at end of file diff --git a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/config/TimeConfigActivity.kt b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigActivity.kt similarity index 86% rename from appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/config/TimeConfigActivity.kt rename to appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigActivity.kt index 2244612..03e903c 100644 --- a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/config/TimeConfigActivity.kt +++ b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigActivity.kt @@ -16,7 +16,7 @@ * * Created by Javinator9889 on 19/03/20 - Handwashing reminder. */ -package com.javinator9889.handwashingreminder.appintro.config +package com.javinator9889.handwashingreminder.appintro.timeconfig import android.app.Activity import android.app.TimePickerDialog @@ -40,6 +40,7 @@ import com.javinator9889.handwashingreminder.utils.formatTime import com.javinator9889.handwashingreminder.utils.isAtLeast import timber.log.Timber import java.util.* +import kotlin.properties.Delegates class TimeConfigActivity : ActionBarBase(), @@ -64,6 +65,8 @@ class TimeConfigActivity : private lateinit var hours: TextView private lateinit var minutes: TextView private lateinit var clockIcon: ImageView + private var id by Delegates.notNull() + private var position by Delegates.notNull() data class Time(val hour: Int, val minute: Int) @@ -92,19 +95,33 @@ class TimeConfigActivity : ddot.setOnClickListener(this) minutes.setOnClickListener(this) clockIcon.setOnClickListener(this) - ViewCompat.setTransitionName(title, VIEW_TITLE_NAME) - ViewCompat.setTransitionName(image, INFO_IMAGE_NAME) - ViewCompat.setTransitionName(hours, USER_TIME_HOURS) - ViewCompat.setTransitionName(ddot, USER_DDOT) - ViewCompat.setTransitionName(minutes, USER_TIME_MINUTES) - ViewCompat.setTransitionName(clockIcon, USER_TIME_ICON) + ViewCompat.setTransitionName(title, + VIEW_TITLE_NAME + ) + ViewCompat.setTransitionName(image, + INFO_IMAGE_NAME + ) + ViewCompat.setTransitionName(hours, + USER_TIME_HOURS + ) + ViewCompat.setTransitionName(ddot, + USER_DDOT + ) + ViewCompat.setTransitionName(minutes, + USER_TIME_MINUTES + ) + ViewCompat.setTransitionName(clockIcon, + USER_TIME_ICON + ) if (savedInstanceState != null || intent.extras != null) { val data = savedInstanceState ?: intent.extras val sHours = data!!.getCharSequence("hours") val sMinutes = data.getCharSequence("minutes") title.text = data.getCharSequence("title") - val imageRes = when (data.getLong("id")) { + id = data.getLong("id", TimeConfig.BREAKFAST_ID) + position = data.getInt("position", 0) + val imageRes = when (id) { TimeConfig.BREAKFAST_ID -> R.drawable.ic_breakfast TimeConfig.LUNCH_ID -> R.drawable.ic_lunch TimeConfig.DINNER_ID -> R.drawable.ic_dinner @@ -142,7 +159,10 @@ class TimeConfigActivity : private fun getHours(): Time { val tpHour = Integer.parseInt(hours.text.toString()) val tpMinute = Integer.parseInt(minutes.text.toString()) - return Time(tpHour, tpMinute) + return Time( + tpHour, + tpMinute + ) } override fun onSaveInstanceState(outState: Bundle) { @@ -153,6 +173,8 @@ class TimeConfigActivity : outState.putCharSequence("hours", hour) outState.putCharSequence("minutes", minute) outState.putCharSequence("title", title.text) + outState.putLong("id", id) + outState.putInt("position", position) } override fun onBackPressed() { @@ -160,6 +182,8 @@ class TimeConfigActivity : val tpTime = getHours() intent.putExtra("hours", formatTime(tpTime.hour)) intent.putExtra("minutes", formatTime(tpTime.minute)) + intent.putExtra("id", id) + intent.putExtra("position", position) setResult(Activity.RESULT_OK, intent) if (isAtLeast(AndroidVersion.LOLLIPOP)) finishAfterTransition() diff --git a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigAdapter.kt b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigAdapter.kt deleted file mode 100644 index 372616c..0000000 --- a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigAdapter.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright © 2020 - present | Handwashing reminder by Javinator9889 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - * - * Created by Javinator9889 on 18/03/20 - Handwashing reminder. - */ -package com.javinator9889.handwashingreminder.appintro.timeconfig - -import android.content.Context -import android.util.SparseArray -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.util.set -import androidx.recyclerview.widget.RecyclerView -import com.javinator9889.handwashingreminder.appintro.R -import com.javinator9889.handwashingreminder.listeners.ViewHolder - -class TimeConfigAdapter( - private val dataset: Array, - private val listener: ViewHolder.OnItemClickListener?, - private val viewItems: SparseArray -) : - RecyclerView.Adapter() { - private var height = 0 - private lateinit var context: Context - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): TimeConfigViewHolder { - context = parent.context - val timeConfig = LayoutInflater.from(parent.context) - .inflate(R.layout.time_card_view, parent, false) - height = parent.measuredHeight / 3 - return TimeConfigViewHolder(timeConfig) - } - - override fun onBindViewHolder(holder: TimeConfigViewHolder, position: Int) { - holder.bind( - dataset[position].title, - dataset[position].id, - listener, - height, - dataset[position] - ) - viewItems[dataset[position].id.toInt()] = holder - } - - override fun getItemCount(): Int = dataset.size -} \ No newline at end of file diff --git a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigContent.kt b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigContent.kt deleted file mode 100644 index f48db8c..0000000 --- a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigContent.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2020 - present | Handwashing reminder by Javinator9889 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - * - * Created by Javinator9889 on 18/03/20 - Handwashing reminder. - */ -package com.javinator9889.handwashingreminder.appintro.timeconfig - -import android.os.Parcel -import android.os.Parcelable - -data class TimeConfigContent(val title: String, val id: Long) : Parcelable { - var hours: String = "" - var minutes: String = "" - - constructor(parcel: Parcel) : this( - parcel.readString()!!, parcel.readLong() - ) { - hours = parcel.readString() ?: "" - minutes = parcel.readString() ?: "" - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(title) - parcel.writeLong(id) - parcel.writeString(hours) - parcel.writeString(minutes) - } - - override fun describeContents(): Int = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): TimeConfigContent { - return TimeConfigContent( - parcel - ) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigItem.kt b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigItem.kt new file mode 100644 index 0000000..1720ca2 --- /dev/null +++ b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigItem.kt @@ -0,0 +1,90 @@ +/* + * Copyright © 2020 - present | Handwashing reminder by Javinator9889 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + * + * Created by Javinator9889 on 26/04/20 - Handwashing reminder. + */ +package com.javinator9889.handwashingreminder.appintro.timeconfig + +import android.view.View +import android.widget.TextView +import androidx.annotation.LayoutRes +import androidx.cardview.widget.CardView +import com.javinator9889.handwashingreminder.appintro.R +import com.javinator9889.handwashingreminder.graphics.GlideApp +import com.javinator9889.handwashingreminder.graphics.RecyclingImageView +import com.javinator9889.handwashingreminder.utils.TimeConfig +import com.javinator9889.handwashingreminder.utils.notNull +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.items.AbstractItem +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.ionicons.Ionicons +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.iconics.view.IconicsImageView + +class TimeConfigItem( + val title: CharSequence, + val id: Long, + var hours: String? = "", + var minutes: String? = "" +) : AbstractItem() { + @LayoutRes + override val layoutRes: Int = R.layout.time_card_view + override val type: Int = R.id.timeCard + + override fun getViewHolder(v: View) = ViewHolder(v) + + class ViewHolder(private val view: View) : + FastAdapter.ViewHolder(view) { + private val title: TextView = view.findViewById(R.id.title) + private val hours: TextView = view.findViewById(R.id.hours) + private val ddot: TextView = view.findViewById(R.id.ddot) + private val minutes: TextView = view.findViewById(R.id.minutes) + private val image: RecyclingImageView = view.findViewById(R.id.infoImage) + private val clockIcon: IconicsImageView = view.findViewById(R.id.clockIcon) + val cardView: CardView = view.findViewById(R.id.timeCard) + + override fun bindView(item: TimeConfigItem, payloads: List) { + title.text = item.title + hours.text = item.hours + minutes.text = item.minutes + ddot.text = view.context.getString(R.string.double_dot) + clockIcon.icon = + IconicsDrawable(view.context, Ionicons.Icon.ion_android_time) + .apply { sizeDp = 16 } + when (item.id) { + TimeConfig.BREAKFAST_ID -> R.drawable.ic_breakfast + TimeConfig.LUNCH_ID -> R.drawable.ic_lunch + TimeConfig.DINNER_ID -> R.drawable.ic_dinner + else -> null + }.notNull { + GlideApp.with(view) + .load(it) + .centerInside() + .into(image) + image.savedDrawableRes = it + } + } + + override fun unbindView(item: TimeConfigItem) { + title.text = null + hours.text = null + ddot.text = null + minutes.text = null + image.onDetachedFromWindow() + clockIcon.icon = null + } + } +} \ No newline at end of file diff --git a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigViewHolder.kt b/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigViewHolder.kt deleted file mode 100644 index d4e02ae..0000000 --- a/appintro/src/main/java/com/javinator9889/handwashingreminder/appintro/timeconfig/TimeConfigViewHolder.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright © 2020 - present | Handwashing reminder by Javinator9889 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - * - * Created by Javinator9889 on 18/03/20 - Handwashing reminder. - */ -package com.javinator9889.handwashingreminder.appintro.timeconfig - -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.annotation.DrawableRes -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.card.MaterialCardView -import com.javinator9889.handwashingreminder.appintro.R -import com.javinator9889.handwashingreminder.graphics.GlideApp -import com.javinator9889.handwashingreminder.listeners.ViewHolder -import com.javinator9889.handwashingreminder.utils.TimeConfig -import com.mikepenz.iconics.view.IconicsImageView -import timber.log.Timber - - -class TimeConfigViewHolder(private val view: View) : - RecyclerView.ViewHolder(view), - View.OnClickListener { - val title: TextView = view.findViewById(R.id.title) - val hours: TextView = view.findViewById(R.id.hours) - val ddot: TextView = view.findViewById(R.id.ddot) - val minutes: TextView = view.findViewById(R.id.minutes) - val image: ImageView = view.findViewById(R.id.infoImage) - val clockIcon: IconicsImageView = view.findViewById(R.id.clockIcon) - private val card = view.findViewById(R.id.timeCard) - private var listener: ViewHolder.OnItemClickListener? = null - private var height: Int? = null - private lateinit var content: TimeConfigContent - var id = 0L - - init { - card.setOnClickListener(this) - title.setOnClickListener(this) - hours.setOnClickListener(this) - minutes.setOnClickListener(this) - image.setOnClickListener(this) - } - - override fun onClick(v: View?) { - when (v) { - card, title, hours, minutes, image -> { - saveContentToTextViews() - this.listener?.onItemClick( - this, - card, - adapterPosition, - id - ) - } - } - } - - - fun bind( - title: CharSequence, - id: Long, - listener: ViewHolder.OnItemClickListener?, - height: Int?, - content: TimeConfigContent - ) { - this.id = id - this.title.text = title - this.listener = listener - this.height = height - this.content = content - val imageRes = when (id) { - TimeConfig.BREAKFAST_ID -> R.drawable.ic_breakfast - TimeConfig.LUNCH_ID -> R.drawable.ic_lunch - TimeConfig.DINNER_ID -> R.drawable.ic_dinner - else -> null - } - adaptCardHeight() - loadImageView(imageRes) - loadContentToTextViews() - } - - fun loadContentToTextViews() { - hours.text = content.hours - minutes.text = content.minutes - } - - fun saveContentToTextViews() { - content.hours = hours.text.toString() - content.minutes = minutes.text.toString() - } - - private fun adaptCardHeight() { - if (height == null) - return - card.minimumHeight = height as Int - } - - private fun loadImageView(@DrawableRes imageRes: Int?) { - if (imageRes != null) - try { - GlideApp.with(view) - .load(imageRes) - .centerInside() - .into(image) - } catch (e: Exception) { - Timber.e(e, "Error while loading Glide view") - image.setImageResource(imageRes) - } - } -} \ No newline at end of file diff --git a/appintro/src/main/res/layout-land/time_card_view_expanded.xml b/appintro/src/main/res/layout-land/time_card_view_expanded.xml new file mode 100644 index 0000000..e91e868 --- /dev/null +++ b/appintro/src/main/res/layout-land/time_card_view_expanded.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +