diff --git a/.gitignore b/.gitignore index 0d6cc89a..79212f48 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,9 @@ app/release/output.json .DS_Store environment.properties + +#commitlint +node_modules +package-lock.json +commitlint.config.js +package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8997c11f..7bc256f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,51 @@ All notable changes to this project will be documented in this file. ### 1.0.6 (2020-09-15) -### Bug Fixes - * Return to healthy state implemented for alerted contacts (close contacts) after 14 days. -* Multi-language - co-official languages -* Texts and design adjustments to meet Ministry of Health requirements -* Privacy policies and conditions of use adjustments +* Multi-language - co-official languages. +* Texts and design adjustments to meet Ministry of Health requirements. +* Privacy policies and conditions of use adjustments. * Accessibility improvements. -* Bug fixing \ No newline at end of file +* Bug fixing. + +### 1.0.7 (2020-10-08) + +* Added symptom/PCR date in positive communication. +* Send fake positive communications. +* DP-3T version upgrade. +* Improved accessibility. +* Bug fixing. + +### 1.1.0 (2020-10-29) + +* Implemented functionalities for interoperability with other European applications. +* Implemented review functionality for new privacy policy and terms of use. +* New language settings functionality. +* Improved accessibility. +* Bug fixing. + +### 1.2.0 (2020-12-17) + +* DP-3T version upgrade. +* Radar COVID statistics view. +* Application status information view. +* Opening reminder notification. +* Confinement counter. +* Share link for easy download. +* Added French language. +* Get positive code from SMS. +* Accessibility improvements: + * Increased text size and contrast. + * Changed links style. + * Tagged elements on screen. + * Incorporated headings. + +### 1.3.0 (2021-02-17) + +* Changed privacy policy. +* Anonymous metrics report to measure application efficiency. +* Added Romanian language. +* Bug fixing. +* Accessibility improvements: + * Fixed "Status Change" header in healed pop-up. + * Added closing icon on popups. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..30c5c9e6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +soporte.radarcovid@economia.gob.es. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index d9a570b0..8b2b9f49 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- +

## Introduction @@ -21,7 +21,9 @@ These are the tools used to develop the solution: Clone this repository and import into **Android Studio**. +```bash git clone https://github.com/RadarCOVID/radar-covid-android.git +``` ## Building ### Create APK diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index bdecaa02..fcd9c595 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -7,7 +7,7 @@ RadarCOVID/radar-covid-android software. In the event that we overlooked to list a required notice, please bring this to our attention by contacting us via this email: -soporte.radarcovid@covid19.gob.es +soporte.radarcovid@economia.gob.es Components: diff --git a/app/build.gradle b/app/build.gradle index 45e8a6a0..245e1406 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,6 +12,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.protobuf' android { compileSdkVersion 29 @@ -21,8 +22,9 @@ android { applicationId "es.gob.radarcovid" minSdkVersion 23 targetSdkVersion 29 - versionCode 6 - versionName "1.0.6" + versionCode 11 + versionName "1.4.0" + resConfigs "en", "es", "ro", "ca", "gl", "eu", "fr" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -111,6 +113,13 @@ android { buildConfigField 'String', 'PUBLIC_KEY_VERIFICATION', '"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHYk1CQUdCeXFHU000OUFnRUdCU3VCQkFBakE0R0dBQVFCbUlXU0ptdGVGNkh2VnI0M1V5SzliZStlNkpPQgpDRjlVaXpMeis4a3padkVEc25nMGl3VEF3UVB0QzdBMDlzQjVMM3EwSUl1N250Yzd4U1VqSUdTakZvd0JXL0xPCnFtMTBYQ1NkUWNZT3BMTi85dUI1emZKVUZOY3B6Ynk4dDAzSlg3TUZiYi9vQm1pcFNNNHptSm1UajR3Qm9XZ2sKRlF6ZEJHcnAwR2laUU9WVXRtUT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="' buildConfigField 'String', 'CERTIFICATE_PIN', '"sha256/BZkFfM++uLfyl8jDpVoLvQWjyGm0xNAZRjEXy9jX798="' + buildConfigField 'String', 'CERTIFICATE_PIN_NEW', '"sha256/Bf/HXif22OO34dBnWpk7AEktor0HAzDwetIWrzJ39D4="' + + buildConfigField("String", "SAFETY_NET_API_KEY", loadProperty("pre.safetyNet")) + + buildConfigField "String", "ENTRY_QR_CODE_PREFIX", '"https://radarcovidpre.covid19.gob.es/qr"' + buildConfigField "String", "QR_CODE_HOST_NAME", '"radarcovidpre.covid19.gob.es"' + manifestPlaceholders = [qrCodeHostName: "radarcovidpre.covid19.gob.es"] } pro { dimension = 'environment' @@ -125,6 +134,13 @@ android { buildConfigField 'String', 'PUBLIC_KEY_VERIFICATION', '"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHYk1CQUdCeXFHU000OUFnRUdCU3VCQkFBakE0R0dBQVFBWjM1ZzlhN1M2MjdXMVlpOEVsVmdNS012dkdUUAo5R0hiUHZHTzhLekNLQk84WTZOc0JSTHlJeWUwZmdxR0ZXM2Z5dHVxcnFSNi9wSllDUWFXN1IyUnY3OEF4OXJhCmlYbmRVSmVyVk9KSHJRaFgxbnMrTjZxaVUxT0I4a3dUaWVuaCtuZDVVbXZUN24vK3hod3djK1RYa1lnNDBxOVcKUTRiVjBMbHRWbGRUSUlTK1QxOD0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="' buildConfigField 'String', 'CERTIFICATE_PIN', '"sha256/V5NaZx/jvh7NR/Ne6tXTw/g2Pcu9ztv08prWsUh7czI="' + buildConfigField 'String', 'CERTIFICATE_PIN_NEW', '"sha256/9psJkMemKz3JXVfw1TiLRoitC2eswaL9FfwF8JFbwrk="' + + buildConfigField("String", "SAFETY_NET_API_KEY", loadProperty("pro.safetyNet")) + + buildConfigField "String", "ENTRY_QR_CODE_PREFIX", '"https://radarcovid.covid19.gob.es/qr"' + buildConfigField "String", "QR_CODE_HOST_NAME", '"radarcovid.covid19.gob.es"' + manifestPlaceholders = [qrCodeHostName: "radarcovid.covid19.gob.es"] } } @@ -158,6 +174,20 @@ def existProjectProperties() { return propertiesFile.exists() } +def loadProperty(String propertyName) { + String property = "\"DEFAULT_VALUE\"" + def propertiesFile = project.rootProject.file("project.properties") + if (propertiesFile.exists()) { + def properties = new Properties() + properties.load(new FileInputStream(propertiesFile)) + property = properties.getProperty(propertyName) + } + if (property == null) + return "\"DEFAULT_VALUE\"" + else + return property +} + def loadCredential(String environmentVariableName, String propertyName) { String credential = System.getProperty(environmentVariableName) if (credential == null) { @@ -179,6 +209,28 @@ def loadCredential(String environmentVariableName, String propertyName) { return credential } +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.0.0' + } + plugins { + javalite { + artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' + } + } + generateProtoTasks { + all().each { task -> + task.builtins { + remove javanone + } + task.plugins { + javalite {} + } + } + } +} + + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) // The google exposure library, required for DP3T, is resolved by the aar located in libs folder @@ -187,6 +239,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.1' + implementation 'androidx.lifecycle:lifecycle-process:2.3.0' + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' testImplementation 'junit:junit:4.13' testImplementation "org.mockito:mockito-core:3.5.6" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" @@ -195,11 +249,8 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation "org.mockito:mockito-core:3.5.6" - // Recommended: Add the Firebase SDK for Google Analytics. - implementation 'com.google.firebase:firebase-analytics-ktx:17.5.0' - //Views - implementation 'com.google.android.material:material:1.2.0' + implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation "org.funktionale:funktionale-either:1.2" @@ -230,22 +281,35 @@ dependencies { implementation "com.squareup.retrofit2:converter-scalars:$rootProject.retrofitVersion" //WorkerManager - implementation 'androidx.work:work-runtime:2.4.0' + implementation 'androidx.work:work-runtime:2.5.0-beta01' //JWT Token - implementation 'io.jsonwebtoken:jjwt-api:0.11.1' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.1' - runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.11.1') { + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.11.2') { exclude group: 'org.json', module: 'json' } implementation 'org.bouncycastle:bcprov-jdk15on:1.65' //DP3T - implementation 'org.dpppt:dp3t-sdk-android:1.0.3' + implementation 'org.dpppt:dp3t-sdk-android:2.0.0' implementation 'com.google.android.gms:play-services-tasks:17.2.0' //DP3T aar dependencies - implementation 'com.google.android.gms:play-services-base:17.4.0' + implementation 'com.google.android.gms:play-services-base:17.5.0' implementation 'androidx.security:security-crypto:1.1.0-alpha02' + //SafetyNet + implementation 'com.google.android.gms:play-services-safetynet:17.0.0' + + // google vision gradle + implementation 'com.google.android.gms:play-services-vision:20.1.3' + + //CrowdNotifier + implementation 'org.crowdnotifier:crowdnotifier-sdk-android:2.1.0' + implementation 'com.google.protobuf:protobuf-lite:3.0.1' + + //Biometrics + implementation "androidx.biometric:biometric-ktx:1.2.0-alpha03" + } \ No newline at end of file diff --git a/app/libs/play-services-nearby-18.0.3-eap.aar b/app/libs/play-services-nearby-18.0.3-eap.aar deleted file mode 100644 index e962ff05..00000000 Binary files a/app/libs/play-services-nearby-18.0.3-eap.aar and /dev/null differ diff --git a/app/libs/play-services-nearby-exposurenotification-1.7.2-eap.aar b/app/libs/play-services-nearby-exposurenotification-1.7.2-eap.aar new file mode 100644 index 00000000..a2ec17e3 Binary files /dev/null and b/app/libs/play-services-nearby-exposurenotification-1.7.2-eap.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 32dc58e8..0253471c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -29,3 +29,5 @@ @com.squareup.otto.Subscribe public *; @com.squareup.otto.Produce public *; } + +-keep class com.sun.jna.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8f7cf67..e0dd9834 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,9 +14,11 @@ package="es.gob.radarcovid"> + + - + + + + + + + - @@ -63,7 +70,19 @@ + + + + - \ No newline at end of file + diff --git a/app/src/main/java/es/gob/radarcovid/RadarCovidApplication.kt b/app/src/main/java/es/gob/radarcovid/RadarCovidApplication.kt index f11f92c1..3b72d3af 100644 --- a/app/src/main/java/es/gob/radarcovid/RadarCovidApplication.kt +++ b/app/src/main/java/es/gob/radarcovid/RadarCovidApplication.kt @@ -10,10 +10,19 @@ package es.gob.radarcovid +import android.content.IntentFilter +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.android.AndroidInjector import dagger.android.support.DaggerApplication import es.gob.radarcovid.common.base.broadcast.ExposureStatusChangeBroadcastReceiver import es.gob.radarcovid.common.di.component.DaggerApplicationComponent +import es.gob.radarcovid.datamanager.repository.PreferencesRepository +import es.gob.radarcovid.features.worker.FakeInfectionReportWorker +import es.gob.radarcovid.features.worker.VenueMatcherWorker import io.reactivex.rxjava3.plugins.RxJavaPlugins import okhttp3.CertificatePinner import org.dpppt.android.sdk.DP3T @@ -23,30 +32,41 @@ import javax.inject.Inject import javax.inject.Named -class RadarCovidApplication : DaggerApplication() { +class RadarCovidApplication : DaggerApplication(), LifecycleObserver { @Inject lateinit var certificatePinner: CertificatePinner + @Inject + lateinit var preferencesRepository: PreferencesRepository + @Inject @Named("userAgent") lateinit var userAgent: String override fun onCreate() { super.onCreate() + ProcessLifecycleOwner.get().lifecycle.addObserver(this) initRxJavaSettings() DP3T.init( this, - ApplicationInfo(packageName, BuildConfig.REPORT_URL, BuildConfig.BUCKET_URL), + ApplicationInfo(BuildConfig.REPORT_URL, BuildConfig.BUCKET_URL), SignatureUtil.getPublicKeyFromBase64OrThrow(BuildConfig.PUBLIC_KEY), BuildConfig.DEBUG ) DP3T.setCertificatePinner(certificatePinner) - DP3T.setUserAgent(userAgent) + DP3T.setUserAgent { userAgent } + DP3T.setErrorNotificationGracePeriod(0) + + FakeInfectionReportWorker.start(this, preferencesRepository) registerReceiver(ExposureStatusChangeBroadcastReceiver(), DP3T.getUpdateIntentFilter()) + LocalBroadcastManager.getInstance(this).registerReceiver( + ExposureStatusChangeBroadcastReceiver(), + IntentFilter(VenueMatcherWorker.ACTION_NEW_VENUE_EXPOSURE_NOTIFICATION) + ) } @@ -62,4 +82,14 @@ class RadarCovidApplication : DaggerApplication() { } } + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onAppBackgrounded() { + preferencesRepository.setApplicationActive(false) + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onAppForegrounded() { + preferencesRepository.setApplicationActive(true) + } + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/base/BaseActivity.kt b/app/src/main/java/es/gob/radarcovid/common/base/BaseActivity.kt index d6e6949f..bd691e0d 100644 --- a/app/src/main/java/es/gob/radarcovid/common/base/BaseActivity.kt +++ b/app/src/main/java/es/gob/radarcovid/common/base/BaseActivity.kt @@ -11,7 +11,15 @@ package es.gob.radarcovid.common.base import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.view.View +import android.view.accessibility.AccessibilityManager import android.view.inputmethod.InputMethodManager +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import dagger.android.support.DaggerAppCompatActivity import es.gob.radarcovid.BuildConfig import es.gob.radarcovid.R @@ -47,6 +55,19 @@ abstract class BaseActivity : DaggerAppCompatActivity() { progressBar?.dismissWithAnimation() } + fun hideLoadingWithCommonError() { + val title = labelManager.getText("ALERT_GENERIC_ERROR_TITLE", R.string.error_generic_title) + .toString() + val message = labelManager.getText("ALERT_GENERIC_ERROR", R.string.error_common_message) + .toString() + val button = labelManager.getText( + "ALERT_ACCEPT_BUTTON", + R.string.accept + ).toString() + + progressBar?.showError(title, message, button) + } + fun hideLoadingWithError(error: Throwable) { val title = if (error.message == null) labelManager.getText("ALERT_GENERIC_ERROR_TITLE", R.string.error_generic_title) @@ -116,4 +137,33 @@ abstract class BaseActivity : DaggerAppCompatActivity() { } } + fun isAccessibilityEnabled() = + (getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager).isEnabled + + fun setAccessibilityAction(view: View, action: String) { + ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + // A custom action description. For example, you could use "pause" + // to have TalkBack speak "double-tap to pause." + val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, action + ) + info.addAction(customClick) + } + }) + } + + fun vibratePhone() { + val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + if (Build.VERSION.SDK_INT >= 26) { + vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + vibrator.vibrate(200) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/base/BaseFragment.kt b/app/src/main/java/es/gob/radarcovid/common/base/BaseFragment.kt index 136a0ed0..98a9023c 100644 --- a/app/src/main/java/es/gob/radarcovid/common/base/BaseFragment.kt +++ b/app/src/main/java/es/gob/radarcovid/common/base/BaseFragment.kt @@ -10,6 +10,8 @@ package es.gob.radarcovid.common.base +import android.content.Context +import android.view.accessibility.AccessibilityManager import dagger.android.support.DaggerFragment import es.gob.radarcovid.R import es.gob.radarcovid.common.view.CMDialog @@ -44,6 +46,19 @@ abstract class BaseFragment : DaggerFragment() { progressBar?.dismissWithAnimation() } + fun hideLoadingWithCommonError() { + val title = labelManager.getText("ALERT_GENERIC_ERROR_TITLE", R.string.error_generic_title) + .toString() + val message = labelManager.getText("ALERT_GENERIC_ERROR", R.string.error_common_message) + .toString() + val button = labelManager.getText( + "ALERT_ACCEPT_BUTTON", + R.string.accept + ).toString() + + progressBar?.showError(title, message, button) + } + fun hideLoadingWithError(error: Throwable) { val title = if (error.message == null) labelManager.getText("ALERT_GENERIC_ERROR_TITLE", R.string.error_generic_title) @@ -101,4 +116,9 @@ abstract class BaseFragment : DaggerFragment() { .show() } + fun isAccessibilityEnabled(): Boolean = + activity?.let { + (it.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager).isEnabled + } ?: false + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/base/Constants.kt b/app/src/main/java/es/gob/radarcovid/common/base/Constants.kt index 8c2c3e5c..9b9159ff 100644 --- a/app/src/main/java/es/gob/radarcovid/common/base/Constants.kt +++ b/app/src/main/java/es/gob/radarcovid/common/base/Constants.kt @@ -10,11 +10,25 @@ package es.gob.radarcovid.common.base +import es.gob.radarcovid.BuildConfig + object Constants { - const val URL_PRIVACY_POLICY = - "https://radarcovid.covid19.gob.es/terms-of-service/privacy-policy.html" - const val URL_USAGE_CONDITIONS = - "https://radarcovid.covid19.gob.es/terms-of-service/use-conditions.html" + const val SO_NAME = "Android" + + const val DATE_FORMAT = "dd/MM/yyyy" + + const val INCOMING_CODE_QUERY_PARAM = "code" + const val HOST_REPORT = "report" + const val HOST_QR_CODE = BuildConfig.QR_CODE_HOST_NAME + + const val NOTIFICATION_REMINDER_DEFAULT = 1440 + + const val KPI_MATCH_CONFIRMED = "MATCH_CONFIRMED" + const val ANALYTICS_PERIOD_DEFAULT = 1440 + + //Encrypted shared preferences + const val ENCRYPTED_PREFERENCES_NAME = "app_encrypted_preferences" + const val ENCRYPTED_PREFERENCES_KEY_SIZE = 256 } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/base/SafeClickListener.kt b/app/src/main/java/es/gob/radarcovid/common/base/SafeClickListener.kt new file mode 100644 index 00000000..36591aa7 --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/base/SafeClickListener.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.base + +import android.os.SystemClock +import android.view.View + +class SafeClickListener( + private var defaultInterval: Int = 1000, + private val onSafeCLick: (View) -> Unit +) : View.OnClickListener { + private var lastTimeClicked: Long = 0 + override fun onClick(v: View) { + if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) { + return + } + lastTimeClicked = SystemClock.elapsedRealtime() + onSafeCLick(v) + } +} diff --git a/app/src/main/java/es/gob/radarcovid/common/base/broadcast/ExposureStatusChangeBroadcastReceiver.kt b/app/src/main/java/es/gob/radarcovid/common/base/broadcast/ExposureStatusChangeBroadcastReceiver.kt index d4e36944..159946cf 100644 --- a/app/src/main/java/es/gob/radarcovid/common/base/broadcast/ExposureStatusChangeBroadcastReceiver.kt +++ b/app/src/main/java/es/gob/radarcovid/common/base/broadcast/ExposureStatusChangeBroadcastReceiver.kt @@ -30,6 +30,7 @@ import es.gob.radarcovid.datamanager.usecase.GetHealingTimeUseCase import es.gob.radarcovid.datamanager.utils.LabelManager import es.gob.radarcovid.features.splash.view.SplashActivity import es.gob.radarcovid.features.worker.HealerWorker +import es.gob.radarcovid.features.worker.VenueMatcherWorker import es.gob.radarcovid.models.domain.ExposureInfo import org.dpppt.android.sdk.DP3T import javax.inject.Inject @@ -62,9 +63,10 @@ class ExposureStatusChangeBroadcastReceiver : DaggerBroadcastReceiver() { ) showHighExposureNotification(it) } - }, 1000) + }, 2000) } DP3T.ACTION_UPDATE -> BUS.post(EventExposureStatusChange()) + VenueMatcherWorker.ACTION_NEW_VENUE_EXPOSURE_NOTIFICATION -> BUS.post(EventExposureStatusChange()) } } diff --git a/app/src/main/java/es/gob/radarcovid/common/base/utils/JwtTokenUtils.kt b/app/src/main/java/es/gob/radarcovid/common/base/utils/JwtTokenUtils.kt index c1e9f0c0..93180666 100644 --- a/app/src/main/java/es/gob/radarcovid/common/base/utils/JwtTokenUtils.kt +++ b/app/src/main/java/es/gob/radarcovid/common/base/utils/JwtTokenUtils.kt @@ -23,7 +23,9 @@ class JwtTokenUtils @Inject constructor() { fun getOnset(token: String): Date { val jwt = parseToken(token) - val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).apply { + timeZone = TimeZone.getTimeZone("UTC") + } return formatter.parse(jwt.body["onset"] as? String ?: "") ?: Date() } diff --git a/app/src/main/java/es/gob/radarcovid/common/di/component/ApplicationComponent.kt b/app/src/main/java/es/gob/radarcovid/common/di/component/ApplicationComponent.kt index 50f8630b..98128e97 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/component/ApplicationComponent.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/component/ApplicationComponent.kt @@ -16,17 +16,14 @@ import dagger.Component import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule import es.gob.radarcovid.RadarCovidApplication -import es.gob.radarcovid.common.di.module.ActivitiesModule -import es.gob.radarcovid.common.di.module.ServicesModule -import es.gob.radarcovid.common.di.module.NetworkModule -import es.gob.radarcovid.common.di.module.RepositoryModule -import es.gob.radarcovid.common.di.module.ViewsModule +import es.gob.radarcovid.common.di.module.* import es.gob.radarcovid.common.di.scope.PerApplication import javax.inject.Named @PerApplication @Component( modules = [NetworkModule::class, + EncryptedSharedPreferencesModule::class, RepositoryModule::class, ActivitiesModule::class, ViewsModule::class, diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/ActivitiesModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/ActivitiesModule.kt index 6934bd2f..976c8500 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/module/ActivitiesModule.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/ActivitiesModule.kt @@ -18,12 +18,22 @@ import es.gob.radarcovid.features.covidreport.form.di.CovidReportModule import es.gob.radarcovid.features.covidreport.form.view.CovidReportActivity import es.gob.radarcovid.features.exposure.di.ExposureModule import es.gob.radarcovid.features.exposure.view.ExposureActivity +import es.gob.radarcovid.features.information.di.InformationModule +import es.gob.radarcovid.features.information.view.InformationActivity import es.gob.radarcovid.features.main.di.MainModule import es.gob.radarcovid.features.main.view.MainActivity import es.gob.radarcovid.features.onboarding.di.OnboardingModule import es.gob.radarcovid.features.onboarding.view.OnboardingActivity +import es.gob.radarcovid.features.qrcodescan.di.QRScanModule +import es.gob.radarcovid.features.qrcodescan.view.QRScanActivity import es.gob.radarcovid.features.splash.di.SplashModule import es.gob.radarcovid.features.splash.view.SplashActivity +import es.gob.radarcovid.features.stats.di.StatsModule +import es.gob.radarcovid.features.stats.view.StatsFragment +import es.gob.radarcovid.features.venuerecord.di.VenueRecordModule +import es.gob.radarcovid.features.venuerecord.view.VenueRecordActivity +import es.gob.radarcovid.features.venuevisited.di.VenueVisitedModule +import es.gob.radarcovid.features.venuevisited.view.VenueVisitedActivity @Module abstract class ActivitiesModule { @@ -45,11 +55,31 @@ abstract class ActivitiesModule { abstract fun bindsExposureActivity(): ExposureActivity @PerActivity - @ContributesAndroidInjector(modules = [CovidReportModule::class]) + @ContributesAndroidInjector(modules = [CovidReportModule::class, CovidReportFragmentModule::class]) abstract fun bindsReportActivity(): CovidReportActivity + @PerActivity + @ContributesAndroidInjector(modules = [InformationModule::class, InformationModule::class]) + abstract fun bindsInformationActivity(): InformationActivity + + @PerActivity + @ContributesAndroidInjector(modules = [StatsModule::class]) + abstract fun bindsStatsActivity(): StatsFragment + @PerActivity @ContributesAndroidInjector abstract fun bindsConfirmationActivity(): ConfirmationActivity + @PerActivity + @ContributesAndroidInjector(modules = [QRScanModule::class]) + abstract fun bindsQRScanActivity(): QRScanActivity + + @PerActivity + @ContributesAndroidInjector(modules = [VenueRecordModule::class, VenueRecordFragmentsModule::class]) + abstract fun bindsVenueRecordActivity(): VenueRecordActivity + + @PerActivity + @ContributesAndroidInjector(modules = [VenueVisitedModule::class]) + abstract fun bindsVenueVisitedActivity(): VenueVisitedActivity + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/CovidReportFragmentModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/CovidReportFragmentModule.kt new file mode 100644 index 00000000..9f2447ca --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/CovidReportFragmentModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.di.module + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import es.gob.radarcovid.common.di.scope.PerFragment +import es.gob.radarcovid.features.covidreport.form.pages.step0.view.Step0MyHealthFragment +import es.gob.radarcovid.features.covidreport.form.pages.step0.di.Step0MyHealthModule +import es.gob.radarcovid.features.covidreport.form.pages.step1.view.Step1MyHealthFragment +import es.gob.radarcovid.features.covidreport.form.pages.step1.di.Step1MyHealthModule +import es.gob.radarcovid.features.covidreport.form.pages.step2.di.Step2MyHealthModule +import es.gob.radarcovid.features.covidreport.form.pages.step2.view.Step2MyHealthFragment + +@Module +abstract class CovidReportFragmentModule { + + @PerFragment + @ContributesAndroidInjector(modules = [Step0MyHealthModule::class]) + abstract fun bindsStep0Fragments(): Step0MyHealthFragment + + @PerFragment + @ContributesAndroidInjector(modules = [Step1MyHealthModule::class]) + abstract fun bindsStep1Fragments(): Step1MyHealthFragment + + @PerFragment + @ContributesAndroidInjector(modules = [Step2MyHealthModule::class]) + abstract fun bindsStep2Fragments(): Step2MyHealthFragment +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/EncryptedSharedPreferencesModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/EncryptedSharedPreferencesModule.kt new file mode 100644 index 00000000..642670ac --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/EncryptedSharedPreferencesModule.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.di.module + +import android.content.Context +import android.content.SharedPreferences +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import dagger.Module +import dagger.Provides +import es.gob.radarcovid.common.base.Constants.ENCRYPTED_PREFERENCES_KEY_SIZE +import es.gob.radarcovid.common.base.Constants.ENCRYPTED_PREFERENCES_NAME +import es.gob.radarcovid.common.di.scope.PerApplication +import javax.inject.Named + +@Module +class EncryptedSharedPreferencesModule { + + + @Provides + @PerApplication + fun providesEncryptedSharedPreferences(@Named("applicationContext") application: Context): SharedPreferences? { + try { + + val keyGenParameterSpec = KeyGenParameterSpec.Builder( + MasterKey.DEFAULT_MASTER_KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(ENCRYPTED_PREFERENCES_KEY_SIZE) + .build() + + val masterKeyAlias = MasterKey.Builder(application, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + .setKeyGenParameterSpec(keyGenParameterSpec) + .build() + + return EncryptedSharedPreferences.create( + application, + ENCRYPTED_PREFERENCES_NAME, + masterKeyAlias, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + + ) + + } catch (ex: Exception) { + return null + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/MainFragmentsModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/MainFragmentsModule.kt index 3b244803..7e8cb922 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/module/MainFragmentsModule.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/MainFragmentsModule.kt @@ -19,6 +19,10 @@ import es.gob.radarcovid.features.home.di.HomeModule import es.gob.radarcovid.features.home.view.HomeFragment import es.gob.radarcovid.features.mydata.di.MyDataModule import es.gob.radarcovid.features.mydata.view.MyDataFragment +import es.gob.radarcovid.features.settings.di.SettingsModule +import es.gob.radarcovid.features.settings.view.SettingsFragment +import es.gob.radarcovid.features.venue.di.VenueModule +import es.gob.radarcovid.features.venue.view.VenueFragment @Module abstract class MainFragmentsModule { @@ -35,4 +39,12 @@ abstract class MainFragmentsModule { @ContributesAndroidInjector(modules = [HelplineModule::class]) abstract fun bindsHelplineFragment(): HelplineFragment + @PerFragment + @ContributesAndroidInjector(modules = [SettingsModule::class, SettingsFragmentsModule::class]) + abstract fun bindsSettingsFragment(): SettingsFragment + + @PerFragment + @ContributesAndroidInjector(modules = [VenueModule::class]) + abstract fun bindsVenueFragment(): VenueFragment + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/NetworkModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/NetworkModule.kt index 783d0f16..3f1f4ffd 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/module/NetworkModule.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/NetworkModule.kt @@ -10,12 +10,18 @@ package es.gob.radarcovid.common.di.module +import android.content.Context +import android.content.SharedPreferences +import com.google.gson.GsonBuilder import dagger.Module import dagger.Provides import es.gob.radarcovid.BuildConfig +import es.gob.radarcovid.common.base.Constants.DATE_FORMAT import es.gob.radarcovid.common.di.scope.PerApplication import es.gob.radarcovid.datamanager.api.ApiInterface import es.gob.radarcovid.datamanager.api.UserAgentInterceptor +import es.gob.radarcovid.datamanager.attestation.AttestationClient +import es.gob.radarcovid.datamanager.attestation.SafetyNetAttestationClient import es.gob.radarcovid.datamanager.repository.BuildInfoRepository import es.gob.radarcovid.datamanager.repository.SystemInfoRepository import okhttp3.CertificatePinner @@ -28,6 +34,7 @@ import retrofit2.converter.scalars.ScalarsConverterFactory import java.net.URI import javax.inject.Named + @Module class NetworkModule { @@ -55,6 +62,7 @@ class NetworkModule { fun providesCertificatePinner(): CertificatePinner = CertificatePinner.Builder() .add(URI(BuildConfig.API_URL).host, BuildConfig.CERTIFICATE_PIN) + .add(URI(BuildConfig.API_URL).host, BuildConfig.CERTIFICATE_PIN_NEW) .build() @Provides @@ -76,7 +84,13 @@ class NetworkModule { fun providesRetrofit(httpClient: OkHttpClient): Retrofit.Builder = Retrofit.Builder() .client(httpClient) .addConverterFactory(ScalarsConverterFactory.create()) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory( + GsonConverterFactory.create( + GsonBuilder() + .setDateFormat(DATE_FORMAT) + .create() + ) + ) @Provides @PerApplication @@ -85,4 +99,18 @@ class NetworkModule { .build() .create(ApiInterface::class.java) + + @Provides + @PerApplication + fun providesAttestationClient(@Named("applicationContext") application: Context): AttestationClient = SafetyNetAttestationClient( + application, + SafetyNetAttestationClient.AttestationParameters( + apiKey = BuildConfig.SAFETY_NET_API_KEY, + apkPackageName = application.packageName, + requiresBasicIntegrity = false, + requiresCtsProfile = false, + requiresHardwareAttestation = false + ) + ) + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/OnboardingFragmentsModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/OnboardingFragmentsModule.kt index 9ccc6d9e..df56fc4d 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/module/OnboardingFragmentsModule.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/OnboardingFragmentsModule.kt @@ -13,8 +13,6 @@ package es.gob.radarcovid.common.di.module import dagger.Module import dagger.android.ContributesAndroidInjector import es.gob.radarcovid.common.di.scope.PerFragment -import es.gob.radarcovid.features.locale.di.LocaleSelectionModule -import es.gob.radarcovid.features.locale.view.LocaleSelectionFragment import es.gob.radarcovid.features.onboarding.pages.legal.di.LegalInfoModule import es.gob.radarcovid.features.onboarding.pages.legal.view.LegalInfoFragment import es.gob.radarcovid.features.onboarding.pages.welcome.di.WelcomeModule @@ -24,15 +22,11 @@ import es.gob.radarcovid.features.onboarding.pages.welcome.view.WelcomeFragment abstract class OnboardingFragmentsModule { @PerFragment - @ContributesAndroidInjector(modules = [WelcomeModule::class]) + @ContributesAndroidInjector(modules = [WelcomeModule::class, WelcomeFragmentsModule::class]) abstract fun bindsWelcomeFragment(): WelcomeFragment @PerFragment @ContributesAndroidInjector(modules = [LegalInfoModule::class]) abstract fun bindsLegalInfoFragment(): LegalInfoFragment - @PerFragment - @ContributesAndroidInjector(modules = [LocaleSelectionModule::class]) - abstract fun bindsLocaleSelectionFragment(): LocaleSelectionFragment - } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/RepositoryModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/RepositoryModule.kt index 97fd7277..c7493e79 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/module/RepositoryModule.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/RepositoryModule.kt @@ -31,12 +31,13 @@ class RepositoryModule { @Provides @PerApplication - fun providesExposureStatusRepository(repository: ExposureStatusRepositoryImpl): ExposureStatusRepository = + fun providesFakeInfectionReportRepository(repository: FakeInfectionReportRepositoryImpl): FakeInfectionReportRepository = repository @Provides @PerApplication - fun providesDomainRepository(repository: DomainRepositoryImpl): DomainRepository = repository + fun providesExposureStatusRepository(repository: ExposureStatusRepositoryImpl): ExposureStatusRepository = + repository @Provides @PerApplication @@ -45,10 +46,14 @@ class RepositoryModule { @Provides @PerApplication - fun providesExampleRepository(repository: ExampleRepositoryImpl): ExampleRepository = repository + fun providesApiRepository(repository: ApiRepositoryImpl): ApiRepository = repository + + @Provides + @PerApplication + fun providesCrowdNotifierRepository(repository: CrowdNotifierRepositoryImpl): CrowdNotifierRepository = repository @Provides @PerApplication - fun providesApiRepository(repository: ApiRepositoryImpl): ApiRepository = repository + fun providesEncryptedPreferencesRepository(repository: EncryptedPreferencesRepositoryImpl): EncryptedPreferencesRepository = repository } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/ServicesModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/ServicesModule.kt index 64c8d55f..4a4b3217 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/module/ServicesModule.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/ServicesModule.kt @@ -14,11 +14,15 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import es.gob.radarcovid.common.base.broadcast.ExposureStatusChangeBroadcastReceiver import es.gob.radarcovid.common.di.scope.PerService -import es.gob.radarcovid.features.worker.HealerWorker +import es.gob.radarcovid.features.worker.* @Module abstract class ServicesModule { + @PerService + @ContributesAndroidInjector + abstract fun bindsFakeInfectedReportWorker(): FakeInfectionReportWorker + @PerService @ContributesAndroidInjector abstract fun bindsHealerWorker(): HealerWorker @@ -27,4 +31,20 @@ abstract class ServicesModule { @ContributesAndroidInjector abstract fun bindsExposureStatusChangeBroadcastReceiver(): ExposureStatusChangeBroadcastReceiver + @PerService + @ContributesAndroidInjector + abstract fun bindsReminderWorker(): ReminderWorker + + @PerService + @ContributesAndroidInjector + abstract fun bindsAnalyticsWorker(): AnalyticsWorker + + @PerService + @ContributesAndroidInjector + abstract fun bindsVenueRecordWorker(): VenueRecordWorker + + @PerService + @ContributesAndroidInjector + abstract fun bindsVenueMatcherWorker(): VenueMatcherWorker + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/SettingsFragmentsModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/SettingsFragmentsModule.kt new file mode 100644 index 00000000..2a6b6fda --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/SettingsFragmentsModule.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.di.module + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import es.gob.radarcovid.common.di.scope.PerSubFragment +import es.gob.radarcovid.features.locale.di.LocaleSelectionModule +import es.gob.radarcovid.features.locale.view.LocaleSelectionFragment + +@Module +abstract class SettingsFragmentsModule { + + @PerSubFragment + @ContributesAndroidInjector(modules = [LocaleSelectionModule::class]) + abstract fun bindsLocaleSelectionFragment(): LocaleSelectionFragment +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/VenueRecordFragmentsModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/VenueRecordFragmentsModule.kt new file mode 100644 index 00000000..bfa6b0ff --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/VenueRecordFragmentsModule.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.di.module + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import es.gob.radarcovid.common.di.scope.PerFragment +import es.gob.radarcovid.features.venuerecord.pages.confirmrecord.di.ConfirmRecordModule +import es.gob.radarcovid.features.venuerecord.pages.confirmrecord.view.ConfirmRecordFragment +import es.gob.radarcovid.features.venuerecord.pages.checkout.di.CheckOutModule +import es.gob.radarcovid.features.venuerecord.pages.checkout.view.CheckOutFragment +import es.gob.radarcovid.features.venuerecord.pages.errorcapturedcode.di.ErrorCapturedCodeModule +import es.gob.radarcovid.features.venuerecord.pages.errorcapturedcode.view.ErrorCapturedCodeFragment +import es.gob.radarcovid.features.venuerecord.pages.checkin.di.CheckInModule +import es.gob.radarcovid.features.venuerecord.pages.checkin.view.CheckInFragment +import es.gob.radarcovid.features.venuerecord.pages.recordsuccess.di.RecordSuccessModule +import es.gob.radarcovid.features.venuerecord.pages.recordsuccess.view.RecordSuccessFragment + +@Module +abstract class VenueRecordFragmentsModule { + + @PerFragment + @ContributesAndroidInjector(modules = [ConfirmRecordModule::class]) + abstract fun bindsCapturedCodeFragment(): ConfirmRecordFragment + + @PerFragment + @ContributesAndroidInjector(modules = [ErrorCapturedCodeModule::class]) + abstract fun bindsErrorCapturedCodeFragment(): ErrorCapturedCodeFragment + + @PerFragment + @ContributesAndroidInjector(modules = [CheckInModule::class]) + abstract fun bindsRecordInitiatedFragment(): CheckInFragment + + @PerFragment + @ContributesAndroidInjector(modules = [CheckOutModule::class]) + abstract fun bindsCheckOutFragment(): CheckOutFragment + + @PerFragment + @ContributesAndroidInjector(modules = [RecordSuccessModule::class]) + abstract fun bindsRecordSuccessFragment(): RecordSuccessFragment + +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/ViewsModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/ViewsModule.kt index e3058927..68aaec1d 100644 --- a/app/src/main/java/es/gob/radarcovid/common/di/module/ViewsModule.kt +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/ViewsModule.kt @@ -13,7 +13,11 @@ package es.gob.radarcovid.common.di.module import dagger.Module import dagger.android.ContributesAndroidInjector import es.gob.radarcovid.common.view.* +import es.gob.radarcovid.common.view.adapter.SingleChoiceAdapter +import es.gob.radarcovid.features.home.view.ShareDialog +import es.gob.radarcovid.features.information.view.InformationDialog import es.gob.radarcovid.features.main.view.ExposureHealedDialog +import es.gob.radarcovid.features.stats.view.StatsCountriesDialog @Module abstract class ViewsModule { @@ -36,6 +40,37 @@ abstract class ViewsModule { @ContributesAndroidInjector abstract fun bindsLowRiskInfoDialog(): ExposureHealedDialog + @ContributesAndroidInjector + abstract fun bindShareDialog(): ShareDialog + @ContributesAndroidInjector abstract fun bindsLabelCheckBox(): LabelCheckBox + + @ContributesAndroidInjector + abstract fun bindsLabelConstraintLayout(): LabelConstraintLayout + + @ContributesAndroidInjector + abstract fun bindsMoreInfoButton(): MoreInfoButton + + @ContributesAndroidInjector + abstract fun bindsLabelRadioButton(): LabelRadioButton + + @ContributesAndroidInjector + abstract fun bindsStepsProgress(): StepsProgress + + @ContributesAndroidInjector + abstract fun bindsSingleChoiceAdapter(): SingleChoiceAdapter + + @ContributesAndroidInjector + abstract fun bindsLegalTermsDialog(): LegalTermsDialog + + @ContributesAndroidInjector + abstract fun bindsInformationDialog(): InformationDialog + + @ContributesAndroidInjector + abstract fun bindsStatsCountriesDialog(): StatsCountriesDialog + + @ContributesAndroidInjector + abstract fun bindsSegmentedControlLabelButton(): SegmentedControlLabelButton + } \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/di/module/WelcomeFragmentsModule.kt b/app/src/main/java/es/gob/radarcovid/common/di/module/WelcomeFragmentsModule.kt new file mode 100644 index 00000000..8e028c60 --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/di/module/WelcomeFragmentsModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.di.module + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import es.gob.radarcovid.common.di.scope.PerSubFragment +import es.gob.radarcovid.features.locale.di.LocaleSelectionModule +import es.gob.radarcovid.features.locale.view.LocaleSelectionFragment + + +@Module +abstract class WelcomeFragmentsModule { + + @PerSubFragment + @ContributesAndroidInjector(modules = [LocaleSelectionModule::class]) + abstract fun bindsLocaleSelectionFragment(): LocaleSelectionFragment + +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/extensions/CharSequence.kt b/app/src/main/java/es/gob/radarcovid/common/extensions/CharSequence.kt new file mode 100644 index 00000000..8da2fbe1 --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/extensions/CharSequence.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.extensions + +import android.text.Spannable +import android.text.Spanned +import android.text.TextPaint +import android.text.style.URLSpan +import android.text.style.UnderlineSpan + +fun CharSequence.removeUnderline(): Spanned { + val s = this as Spannable + for (u in s.getSpans(0, s.length, URLSpan::class.java)) { + s.setSpan(object : UnderlineSpan() { + override fun updateDrawState(tp: TextPaint) { + tp.isUnderlineText = false + } + }, s.getSpanStart(u), s.getSpanEnd(u), 0) + } + return s +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/extensions/Date.kt b/app/src/main/java/es/gob/radarcovid/common/extensions/Date.kt index e370ee4c..aa4a4825 100644 --- a/app/src/main/java/es/gob/radarcovid/common/extensions/Date.kt +++ b/app/src/main/java/es/gob/radarcovid/common/extensions/Date.kt @@ -12,11 +12,98 @@ package es.gob.radarcovid.common.extensions import java.text.SimpleDateFormat import java.util.* +import java.util.concurrent.TimeUnit const val DATE_FORMAT_VERBOSE = "dd.MM.yyyy" const val DATE_FORMAT_TIMESTAMP = "dd/MM/yyyy HH:mm:ss" +const val DATE_FORMAT = "dd/MM/yyyy" +const val DATE_DAY_FORMAT = "MMMM d" fun Date.format(): String = SimpleDateFormat(DATE_FORMAT_VERBOSE, Locale.getDefault()).format(this) fun Date.toTimeStamp(): String = - SimpleDateFormat(DATE_FORMAT_TIMESTAMP, Locale.getDefault()).format(this) \ No newline at end of file + SimpleDateFormat(DATE_FORMAT_TIMESTAMP, Locale.getDefault()).format(this) + +fun Date.getDayString(): String = + SimpleDateFormat("dd", Locale.getDefault()).format(this) + +fun Date.getNameDayString(locale: String): String = + SimpleDateFormat("EEEE", Locale.forLanguageTag(locale)).format(this).capitalize() + +fun Date.geMonthNameDefault(): String = + SimpleDateFormat("MMMM", Locale.getDefault()).format(this).capitalize() + +fun Date.geMonthName(locale: String): String = + SimpleDateFormat("MMMM", Locale.forLanguageTag(locale)).format(this).capitalize() + +fun Date.getYearString(): String = + SimpleDateFormat("yyyy", Locale.getDefault()).format(this) + +fun Date.getHourString(): String = + SimpleDateFormat("HH:mm", Locale.getDefault()).format(this) + +fun Date.getDayAndMonth(locale: String): String = + SimpleDateFormat("d MMMM", Locale.forLanguageTag(locale)).format(this).capitalizeWord() + +fun Date.toDateFormat(): String = + SimpleDateFormat(DATE_FORMAT, Locale.getDefault()).format(this) + +fun Date.isToday(): Boolean { + Calendar.getInstance().apply { + return this.get(Calendar.DAY_OF_YEAR) == get(Calendar.DAY_OF_YEAR) + } +} + +fun Date.isYesterday(): Boolean { + Calendar.getInstance().apply { + return this.get(Calendar.DAY_OF_YEAR) - get(Calendar.DAY_OF_YEAR) == 1 + } +} + +fun Date.add(field: Int, amount: Int): Date { + Calendar.getInstance().apply { + time = this@add + add(field, amount) + return time + } +} + +fun Date.addYears(years: Int): Date { + return add(Calendar.YEAR, years) +} + +fun Date.addMonths(months: Int): Date { + return add(Calendar.MONTH, months) +} + +fun Date.addDays(days: Int): Date { + return add(Calendar.DAY_OF_MONTH, days) +} + +fun Date.addHours(hours: Int): Date { + return add(Calendar.HOUR_OF_DAY, hours) +} + +fun Date.addMinutes(minutes: Int): Date { + return add(Calendar.MINUTE, minutes) +} + +fun Date.addSeconds(seconds: Int): Date { + return add(Calendar.SECOND, seconds) +} + +fun Date.getDaysAgo(): Long { + val millisElapsed = System.currentTimeMillis() - time + return TimeUnit.MILLISECONDS.toDays(millisElapsed) +} + +fun Date.getTimeElapsed(date: Date): String { + val millisElapsed = date.time - time + val hours = TimeUnit.MILLISECONDS.toHours(millisElapsed) + val minutes = TimeUnit.MILLISECONDS.toMinutes(millisElapsed) + return if (hours <= 0) { + "$minutes'" + } else { + "${hours}h" + } +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/extensions/String.kt b/app/src/main/java/es/gob/radarcovid/common/extensions/String.kt index a32ee01b..6b6e843d 100644 --- a/app/src/main/java/es/gob/radarcovid/common/extensions/String.kt +++ b/app/src/main/java/es/gob/radarcovid/common/extensions/String.kt @@ -12,6 +12,7 @@ package es.gob.radarcovid.common.extensions import android.text.Spanned import androidx.core.text.HtmlCompat +import java.util.* fun String?.default(default: String): String = when { this == null -> default @@ -20,3 +21,14 @@ fun String?.default(default: String): String = when { } fun String?.parseHtml(): Spanned = HtmlCompat.fromHtml(this ?: "", HtmlCompat.FROM_HTML_MODE_LEGACY) + +fun String.capitalizeWord(): String { + val words: List = split(" ") + var capitalizeWord = "" + words.forEach { + val first = it.substring(0, 1) + val afterFirst = it.substring(1) + capitalizeWord += first.toUpperCase(Locale.ROOT) + afterFirst + " " + } + return capitalizeWord.trim { it <= ' ' } +} diff --git a/app/src/main/java/es/gob/radarcovid/common/extensions/View.kt b/app/src/main/java/es/gob/radarcovid/common/extensions/View.kt new file mode 100644 index 00000000..74fd496d --- /dev/null +++ b/app/src/main/java/es/gob/radarcovid/common/extensions/View.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 Gobierno de España + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package es.gob.radarcovid.common.extensions + +import android.view.View +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import es.gob.radarcovid.common.base.SafeClickListener + +fun View.setSafeOnClickListener(onSafeClick: (View) -> Unit) { + val safeClickListener = SafeClickListener { + onSafeClick(it) + } + setOnClickListener(safeClickListener) +} + +fun View.setAccessibilityAction(action: String) { + ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + // A custom action description. For example, you could use "pause" + // to have TalkBack speak "double-tap to pause." + val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, action + ) + info.addAction(customClick) + } + }) +} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/view/CMCheckBox.kt b/app/src/main/java/es/gob/radarcovid/common/view/CMCheckBox.kt deleted file mode 100644 index a6b5bd36..00000000 --- a/app/src/main/java/es/gob/radarcovid/common/view/CMCheckBox.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2020 Gobierno de España - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - */ - -package es.gob.radarcovid.common.view - -import android.content.Context -import android.text.method.LinkMovementMethod -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.CompoundButton -import android.widget.LinearLayout -import androidx.core.content.ContextCompat -import es.gob.radarcovid.R -import kotlinx.android.synthetic.main.view_cmcheckbox.view.* - -class CMCheckBox @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { - - var isChecked: Boolean = false - set(value) { - field = value - checkBox.isChecked = value - } - get() = checkBox.isChecked - - init { - orientation = HORIZONTAL - LayoutInflater.from(context).inflate(R.layout.view_cmcheckbox, this) - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CMCheckBox) - val text = typedArray.getText(R.styleable.CMCheckBox_android_text) - typedArray.recycle() - textView.text = text - } - - fun setOnCheckedChangeListener(listener: CompoundButton.OnCheckedChangeListener?) { - checkBox.setOnCheckedChangeListener(listener) - } - - fun setText(text: CharSequence?) { - textView.text = text - textView.movementMethod = LinkMovementMethod.getInstance() - textView.highlightColor = ContextCompat.getColor(context, R.color.black_28) - } - -} \ No newline at end of file diff --git a/app/src/main/java/es/gob/radarcovid/common/view/CMDialog.kt b/app/src/main/java/es/gob/radarcovid/common/view/CMDialog.kt index d23833d2..e7bc2af3 100644 --- a/app/src/main/java/es/gob/radarcovid/common/view/CMDialog.kt +++ b/app/src/main/java/es/gob/radarcovid/common/view/CMDialog.kt @@ -16,24 +16,20 @@ import android.graphics.drawable.ColorDrawable import android.view.LayoutInflater import android.view.View import android.widget.Button -import android.widget.ImageButton -import android.widget.TextView import androidx.appcompat.app.AlertDialog import es.gob.radarcovid.R - object CMDialog { class Builder(private val context: Context) { private val view: View = LayoutInflater.from(context).inflate(R.layout.dialog, null) - private val textViewTitle = view.findViewById(R.id.textViewDialogTitle) + private val textViewTitle = view.findViewById(R.id.textViewDialogTitle) private val textViewDescription = - view.findViewById(R.id.textViewDialogDescription) + view.findViewById(R.id.textViewDialogDescription) private val buttonOk = view.findViewById