├── .gitignore ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── app.properties.example ├── build.gradle ├── proguard-rules.pro └── src │ ├── global │ └── java │ │ └── intl │ │ └── who │ │ └── covid19 │ │ └── CountryDefaults.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── intl │ │ │ └── who │ │ │ └── covid19 │ │ │ ├── Api.java │ │ │ ├── App.java │ │ │ ├── BeaconService.java │ │ │ ├── DataQueue.java │ │ │ ├── Encounter.java │ │ │ ├── EncounterQueue.java │ │ │ ├── FcmService.java │ │ │ ├── ICountryDefaults.java │ │ │ ├── LocalNotificationReceiver.java │ │ │ ├── Location.java │ │ │ ├── LocationQueue.java │ │ │ ├── Prefs.java │ │ │ ├── UploadService.java │ │ │ └── ui │ │ │ ├── AboutActivity.java │ │ │ ├── AddressActivity.java │ │ │ ├── ConfirmDialog.java │ │ │ ├── FaceIdActivity.java │ │ │ ├── HomeActivity.java │ │ │ ├── HomeFragment.java │ │ │ ├── MapFragment.java │ │ │ ├── PhoneVerificationActivity.java │ │ │ ├── PrivacyPolicyActivity.java │ │ │ ├── ProfileFragment.java │ │ │ ├── ProtectActivity.java │ │ │ ├── QuarantineStartActivity.java │ │ │ ├── StatusActivity.java │ │ │ ├── SymptomsActivity.java │ │ │ └── WelcomeActivity.java │ └── res │ │ ├── color │ │ └── home_tab.xml │ │ ├── drawable-xxhdpi │ │ ├── about_footer.png │ │ ├── chevron_right.png │ │ ├── home_about.png │ │ ├── home_home.png │ │ ├── home_info.png │ │ ├── home_map.png │ │ ├── home_profile.png │ │ ├── home_protect.png │ │ ├── home_symptoms.png │ │ ├── ic_bluetooth.png │ │ ├── ic_notification.png │ │ ├── ic_notification_scan.png │ │ ├── ic_notification_warning.png │ │ ├── logo.png │ │ ├── map_bubble.png │ │ ├── map_button.png │ │ ├── map_drag.png │ │ ├── map_pin.png │ │ ├── profile.png │ │ ├── protect1.png │ │ ├── protect2.png │ │ ├── protect3.png │ │ ├── protect4.png │ │ ├── protect5.png │ │ ├── protect6.png │ │ ├── protect7.png │ │ ├── status.png │ │ ├── status_bluetooth.png │ │ ├── status_location.png │ │ ├── status_wifi.png │ │ └── world.png │ │ ├── drawable │ │ ├── bg_btn_blue.xml │ │ ├── bg_btn_green.xml │ │ ├── bg_btn_ltblue.xml │ │ ├── bg_btn_red.xml │ │ ├── bg_btn_white.xml │ │ ├── bg_btn_white_framed.xml │ │ ├── bg_gray.xml │ │ ├── bg_stats.xml │ │ ├── bg_status.xml │ │ ├── doctor.xml │ │ ├── ic_check_green.xml │ │ ├── ic_check_red.xml │ │ ├── ic_edit.xml │ │ └── ic_search.xml │ │ ├── font │ │ ├── inter_bold.otf │ │ ├── inter_light.otf │ │ ├── poppins_bold.otf │ │ └── poppins_light.otf │ │ ├── layout │ │ ├── activity_about.xml │ │ ├── activity_address.xml │ │ ├── activity_faceid.xml │ │ ├── activity_home.xml │ │ ├── activity_phone_verification.xml │ │ ├── activity_privacy_policy.xml │ │ ├── activity_protect.xml │ │ ├── activity_quarantine_start.xml │ │ ├── activity_status.xml │ │ ├── activity_symptoms.xml │ │ ├── activity_welcome.xml │ │ ├── dialog_confirm.xml │ │ ├── fragment_home.xml │ │ ├── fragment_map.xml │ │ ├── fragment_profile.xml │ │ ├── view_county_stat.xml │ │ ├── view_map_bubble.xml │ │ ├── view_stats.xml │ │ └── view_welcome_header.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── raw │ │ ├── keep.xml │ │ └── privacy.html │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── network.xml │ │ └── remote_config_defaults.xml │ └── slovakia │ ├── AndroidManifest.xml │ ├── java │ └── intl │ │ └── who │ │ └── covid19 │ │ └── CountryDefaults.java │ └── res │ ├── drawable-xxhdpi │ ├── about_footer.png │ └── mzsr.png │ ├── layout │ └── view_welcome_header.xml │ ├── raw │ ├── counties.json │ └── privacy.html │ └── values │ └── strings.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | 11 | app/app.keystore 12 | app/app.properties 13 | google-services.json 14 | app/src/*/res/raw/innovatrics_license.lic 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android application to help fight COVID-19 2 | 3 | This app is aiming at helping fight COVID-19 spread by collecting anonymous data about people meeting each other. 4 | 5 | In the basic scenario, the device is emitting an iBeacon signal (Bluetooth low energy) and at the same time listens to iBeacons around you. Thus creating an anonymous mesh of who met whom and when. This data is collected on server and when a person is positively diagnosed with SARS-CoV-2 (the infamous "corona" virus causing COVID-19 disease), the server will notify via push all the devices that were in a close and significant proximity with that person. 6 | 7 | Alternatively, the user can flag themselves as quarantined in which case the app will regularly check their GPS location and warn them in case they leave the quarantine. 8 | 9 | ## Country-specific customisation steps 10 | * Rename app/app.properties.example to app/app.properties and fill in your local-specific values 11 | * Register your app in Google Firebase console and copy google-services.json file to app folder 12 | * Create a flavor in app/build.gradle for your country 13 | * Copy CountryDefaults.java from the global flavour into yours and implement your specifics 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.properties.example: -------------------------------------------------------------------------------- 1 | apiUrl= 2 | beaconUuid= 3 | keystoreFile= 4 | keystorePassword= 5 | keyAlias= 6 | keyPassword= 7 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.google.gms.google-services' 3 | apply plugin: 'io.fabric' 4 | 5 | def props = new Properties() 6 | props.load(new FileInputStream(project.file('app.properties'))) 7 | 8 | android { 9 | signingConfigs { 10 | release { 11 | storeFile file(props['keystoreFile']) 12 | storePassword props['keystorePassword'] 13 | keyAlias props['keyAlias'] 14 | keyPassword props['keyPassword'] 15 | } 16 | } 17 | compileSdkVersion 29 18 | buildToolsVersion "29.0.2" 19 | defaultConfig { 20 | applicationId 'int.covid19' 21 | minSdkVersion 21 22 | targetSdkVersion 29 23 | versionCode 1 24 | versionName '1.0.0' 25 | buildConfigField('java.util.UUID', 'BEACON_UUID', 'java.util.UUID.fromString("' + props['beaconUuid'] + '")') 26 | resValue("string", "defaultApiUrl", props['apiUrl']) 27 | } 28 | buildTypes { 29 | debug { 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | signingConfig signingConfigs.release 32 | } 33 | release { 34 | minifyEnabled true 35 | shrinkResources true 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | signingConfig signingConfigs.release 38 | } 39 | } 40 | flavorDimensions 'country' 41 | productFlavors { 42 | global { 43 | dimension 'country' 44 | } 45 | slovakia { 46 | dimension 'country' 47 | applicationId 'sk.nczi.covid19' 48 | versionCode 5 49 | versionName '1.1.0' 50 | versionNameSuffix '-sk' 51 | buildConfigField('String', 'PHONE_VERIFICATION_API_URL', '"' + props['phoneVerificationApiUrl'] + '"') 52 | } 53 | } 54 | compileOptions { 55 | sourceCompatibility = 1.8 56 | targetCompatibility = 1.8 57 | } 58 | } 59 | 60 | dependencies { 61 | implementation 'androidx.appcompat:appcompat:1.1.0' 62 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 63 | implementation 'com.google.code.gson:gson:2.8.6' 64 | implementation 'com.google.android.gms:play-services-nearby:17.0.0' 65 | implementation 'com.google.android.gms:play-services-location:17.0.0' 66 | implementation 'com.google.android.libraries.places:places:2.2.0' 67 | implementation 'com.google.android.material:material:1.1.0' 68 | implementation 'com.google.firebase:firebase-analytics:17.3.0' 69 | implementation 'com.google.firebase:firebase-messaging:20.1.4' 70 | implementation 'com.google.firebase:firebase-config:19.1.3' 71 | implementation 'com.google.maps.android:android-maps-utils:1.0.2' 72 | implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' 73 | implementation 'com.github.ialokim:android-phone-field:0.2.3' 74 | implementation 'sk.turn:beacons-android:1.5.4' 75 | implementation 'sk.turn:http:1.7.0' 76 | implementation 'org.hashids:hashids:1.0.3' 77 | compileOnly 'com.innovatrics.android:dot:2.15.1' 78 | // Only include the below dependency for flavors where CountryDefaults.useFaceId() returns true 79 | //globalImplementation 'com.innovatrics.android:dot:2.15.1' 80 | implementation 'com.auth0.android:jwtdecode:2.0.0' 81 | } 82 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. (Like Crashlytics) 17 | -keepattributes SourceFile,LineNumberTable 18 | # Keep Crashlytics annotations 19 | -keepattributes *Annotation* 20 | 21 | # If you keep the line number information, uncomment this to 22 | # hide the original source file name. 23 | -renamesourcefileattribute SourceFile 24 | 25 | -keepclassmembers class intl.who.covid19.Encounter { 26 | (); 27 | !static ; 28 | } 29 | -keepclassmembers class intl.who.covid19.Location { 30 | (); 31 | !static ; 32 | } 33 | -keepclassmembers class intl.who.covid19.Api$* { 34 | (); 35 | !static ; 36 | } 37 | -keepclassmembers class intl.who.covid19.CountryDefaults$* { 38 | (); 39 | !static ; 40 | } 41 | -keepclassmembers class intl.who.covid19.ui.MapFragment$* { 42 | (); 43 | !static ; 44 | } 45 | 46 | # Innovatrics rules 47 | -dontwarn com.sun.jna.** 48 | -dontwarn com.innovatrics.commons.pc.** 49 | # JNA 50 | -keep class com.sun.jna.** { *; } 51 | # Innovatrics IFace 52 | -keep class com.innovatrics.iface.** { *; } 53 | -------------------------------------------------------------------------------- /app/src/global/java/intl/who/covid19/CountryDefaults.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.content.Context; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | import intl.who.covid19.ui.HomeFragment; 31 | import intl.who.covid19.ui.MapFragment; 32 | import intl.who.covid19.ui.PhoneVerificationActivity; 33 | 34 | public class CountryDefaults implements ICountryDefaults { 35 | public CountryDefaults(Context context) { } 36 | @Override 37 | public boolean verifyPhoneNumberAtStart() { return false; } 38 | @Override 39 | public boolean showQuarantineStartPicker() { return false; } 40 | @Override 41 | public boolean useFaceId() { return false; } 42 | @Override 43 | public boolean sendLocationInQuarantine() { return false; } 44 | @Override 45 | public boolean sendQuarantineLeft() { return false; } 46 | @Override 47 | public boolean roundEncounterTimestampToDays() { return false; } 48 | @Override 49 | public String getCountryCode() { return "XX"; } 50 | @Override 51 | public double getCenterLat() { return 49; } 52 | @Override 53 | public double getCenterLng() { return 17; } 54 | @Override 55 | public double getCenterZoom() { return 4; } 56 | 57 | @Override 58 | public void getStats(Context context, App.Callback callback) { 59 | callback.onCallback(null); 60 | } 61 | 62 | @Override 63 | public void getCountyStats(Context context, App.Callback> callback) { 64 | callback.onCallback(new ArrayList<>()); 65 | } 66 | 67 | @Override 68 | public void sendVerificationCodeText(Context context, String phoneNumber, App.Callback callback) { 69 | callback.onCallback(new UnsupportedOperationException("This operation is not supported or not implemented in this flavor")); 70 | } 71 | 72 | @Override 73 | public int getVerificationCodeLength() { return 4; } 74 | 75 | @Override 76 | public void checkVerificationCode(Context context, String phoneNumber, String code, App.Callback callback) { 77 | callback.onCallback(null); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 57 | 61 | 65 | 69 | 73 | 77 | 81 | 86 | 90 | 91 | 94 | 97 | 100 | 103 | 106 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 121 | 124 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/Api.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.annotation.SuppressLint; 26 | import android.content.Context; 27 | import android.net.Uri; 28 | import android.os.AsyncTask; 29 | 30 | import com.google.gson.Gson; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | import sk.turn.http.Http; 36 | 37 | public class Api { 38 | public interface Listener { 39 | void onResponse(int status, String response); 40 | } 41 | private static class Response { 42 | private int status; 43 | private String body; 44 | Response(int status, String body) { 45 | this.status = status; 46 | this.body = body; 47 | } 48 | } 49 | public static class ProfileRequest { 50 | private String deviceId; 51 | private String pushToken; 52 | private String phoneNumber; 53 | private String locale; 54 | public ProfileRequest(String deviceUid, String pushToken, String phoneNumber) { 55 | deviceId = deviceUid; 56 | this.pushToken = pushToken; 57 | this.phoneNumber = phoneNumber; 58 | locale = java.util.Locale.getDefault().toString(); 59 | } 60 | } 61 | public static class ProfileResponse { 62 | public long profileId; 63 | public String deviceId; 64 | } 65 | public static class ContactRequest { 66 | private final String sourceDeviceId; 67 | private final long sourceProfileId; 68 | public final ArrayList connections = new ArrayList<>(); 69 | public ContactRequest(String sourceDeviceId, long sourceProfileId) { 70 | this.sourceDeviceId = sourceDeviceId; 71 | this.sourceProfileId = sourceProfileId; 72 | } 73 | } 74 | private static class AuthTokenRequest { 75 | private final String deviceId; 76 | private final long profileId; 77 | private String startDate; 78 | private String endDate; 79 | private String covidPass; 80 | public AuthTokenRequest(String deviceId, long profileId, String startDate, String endDate, String covidPass) { 81 | this.deviceId = deviceId; 82 | this.profileId = profileId; 83 | this.startDate = startDate; 84 | this.endDate = endDate; 85 | this.covidPass = covidPass; 86 | } 87 | } 88 | public static class LocationRequest { 89 | private final String deviceId; 90 | private final long profileId; 91 | public final List locations = new ArrayList<>(); 92 | public LocationRequest(String deviceId, long profileId) { 93 | this.deviceId = deviceId; 94 | this.profileId = profileId; 95 | } 96 | } 97 | public static class QuarantineLeftRequest { 98 | private final String deviceId; 99 | private final long profileId; 100 | private final double latitude; 101 | private final double longitude; 102 | private final int accuracy; 103 | private final long recordTimestamp; 104 | public QuarantineLeftRequest(String deviceId, long profileId, android.location.Location location) { 105 | this.deviceId = deviceId; 106 | this.profileId = profileId; 107 | latitude = location.getLatitude(); 108 | longitude = location.getLongitude(); 109 | accuracy = (int) location.getAccuracy(); 110 | recordTimestamp = location.getTime() / 1000; 111 | } 112 | } 113 | public static class QuarantineInfoResponse { 114 | public boolean isInQuarantine; 115 | public String quarantineBeginning; 116 | public String quarantineEnd; 117 | } 118 | 119 | private final App app; 120 | public Api(Context context) { 121 | app = App.get(context); 122 | } 123 | 124 | public void createProfile(ProfileRequest request, Listener listener) { 125 | send("profile", Http.PUT, request, listener); 126 | } 127 | public void sendContacts(ContactRequest request, Listener listener) { 128 | send("profile/contacts", Http.POST, request, listener); 129 | } 130 | public void confirmQuarantine(String covidId, String startDate, String endDate, Listener listener) { 131 | send("profile/quarantine", Http.POST, new AuthTokenRequest(app.prefs().getString(Prefs.DEVICE_UID, null), 132 | app.prefs().getLong(Prefs.DEVICE_ID, 0L), startDate, endDate, covidId), listener); 133 | } 134 | public void sendLocations(LocationRequest request, Listener listener) { 135 | send("profile/location", Http.POST, request, listener); 136 | } 137 | public void quarantineLeft(QuarantineLeftRequest request, Listener listener) { 138 | send("profile/areaexit", Http.POST, request, listener); 139 | } 140 | public void getQuarantineInfo(Listener listener) { 141 | send("profile/quarantine" + 142 | "?profileId=" + Http.urlEncode(String.valueOf(app.prefs().getLong(Prefs.DEVICE_ID, 0L))) + 143 | "&deviceId=" + Http.urlEncode(app.prefs().getString(Prefs.DEVICE_UID, "")), Http.GET, null, listener); 144 | } 145 | 146 | @SuppressLint("StaticFieldLeak") 147 | private void send(String action, String method, Object request, final Listener listener) { 148 | new AsyncTask() { 149 | @Override 150 | protected Response doInBackground(Void... voids) { 151 | try { 152 | String data = new Gson().toJson(request); 153 | App.log("API > " + method + " " + action + " " + data); 154 | Uri uri = Uri.withAppendedPath(app.apiUri(), action); 155 | Http http = new Http(uri.toString(), method) 156 | .addHeader("Content-Type", "application/json"); 157 | if (request != null) { 158 | http.setData(data); 159 | } 160 | int code = http.send().getResponseCode(); 161 | String response = http.getResponseString(); 162 | App.log("API < " + code + " " + response + (code == 200 ? "" : " " + http.getResponseMessage())); 163 | return new Response(http.getResponseCode(), response); 164 | } catch (Exception e) { 165 | App.log("API failed " + e); 166 | return new Response(-1, e.getMessage()); 167 | } 168 | } 169 | @Override 170 | protected void onPostExecute(Response response) { 171 | if (listener != null) { 172 | listener.onResponse(response.status, response.body); 173 | } 174 | } 175 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/DataQueue.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.content.Context; 26 | import android.os.AsyncTask; 27 | 28 | import com.google.gson.Gson; 29 | 30 | import java.io.File; 31 | import java.io.FileReader; 32 | import java.io.FileWriter; 33 | import java.lang.reflect.Type; 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | /*package*/ abstract class DataQueue { 38 | 39 | public interface SendListener { 40 | void onSent(); 41 | } 42 | 43 | private static class SaveTask extends AsyncTask { 44 | private final File file; 45 | private final List data; 46 | SaveTask(File file, List data) { 47 | this.file = file; 48 | this.data = data; 49 | } 50 | @Override 51 | protected Boolean doInBackground(Void... params) { 52 | try (FileWriter fileWriter = new FileWriter(file)) { 53 | // FIXME: This is blocking the main thread that want to add an item 54 | synchronized (data) { 55 | new Gson().toJson(data, fileWriter); 56 | } 57 | return true; 58 | } catch (Exception e) { 59 | return false; 60 | } 61 | } 62 | } 63 | 64 | protected final Context context; 65 | private final String filename; 66 | private final List data; 67 | private boolean autoSave = true; 68 | 69 | public DataQueue(Context context, String filename) { 70 | this.context = context; 71 | this.filename = filename; 72 | // Load from file 73 | File dataFile = new File(context.getFilesDir(), filename); 74 | List data = null; 75 | try { 76 | data = new Gson().fromJson(new FileReader(dataFile), getListType()); 77 | } catch (Exception e) { 78 | App.log("Can't load data from " + getClass().getSimpleName() + " file: " + dataFile + "; " + e); 79 | } 80 | if (data == null) { 81 | data = new ArrayList<>(); 82 | } 83 | this.data = data; 84 | } 85 | 86 | public void setAutoSave(boolean autoSave) { 87 | this.autoSave = autoSave; 88 | } 89 | 90 | public void add(T item) { 91 | synchronized (data) { 92 | data.add(item); 93 | if (autoSave) { 94 | save(); 95 | } 96 | } 97 | } 98 | 99 | public void save() { 100 | autoSave = true; 101 | new SaveTask<>(new File(context.getFilesDir(), filename), data).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 102 | } 103 | 104 | public void send(SendListener listener) { 105 | final List dataToSend; 106 | synchronized (data) { 107 | dataToSend = new ArrayList<>(data); 108 | data.clear(); 109 | save(); 110 | } 111 | if (dataToSend.isEmpty()) { 112 | listener.onSent(); 113 | return; 114 | } 115 | makeSendRequest(dataToSend, (status, response) -> { 116 | if (status != 200) { 117 | synchronized (data) { 118 | data.addAll(0, dataToSend); 119 | save(); 120 | } 121 | } 122 | listener.onSent(); 123 | }); 124 | } 125 | 126 | protected abstract Type getListType(); 127 | protected abstract void makeSendRequest(List data, Api.Listener listener); 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/Encounter.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.location.Location; 26 | 27 | import androidx.annotation.Nullable; 28 | 29 | import java.math.BigDecimal; 30 | import java.math.RoundingMode; 31 | import java.util.Calendar; 32 | import java.util.Locale; 33 | 34 | public class Encounter { 35 | 36 | // Do not rename these fields! They are named to match API 37 | private long seenProfileId; 38 | private long timestamp; 39 | private String duration; 40 | private Double latitude; 41 | private Double longitude; 42 | private Integer accuracy; 43 | 44 | public Encounter() { } 45 | public Encounter(long seenProfileId, boolean roundTimestampToDays) { 46 | this.seenProfileId = seenProfileId; 47 | Calendar cal = Calendar.getInstance(); 48 | if (roundTimestampToDays) { 49 | cal.set(Calendar.HOUR_OF_DAY, 0); 50 | cal.set(Calendar.MINUTE, 0); 51 | cal.set(Calendar.SECOND, 1); 52 | cal.set(Calendar.MILLISECOND, 0); 53 | } 54 | timestamp = cal.getTimeInMillis() / 1000; 55 | } 56 | 57 | public long getSeenProfileId() { 58 | return seenProfileId; 59 | } 60 | public long getTimestamp() { 61 | return timestamp; 62 | } 63 | public void setDuration(long duration) { 64 | this.duration = String.format(Locale.ROOT, "%02d:%02d:%02d", duration / 3600, (duration % 3600) / 60, duration % 60); 65 | } 66 | public void setLocation(@Nullable Location location, int forceAccuracy) { 67 | if (location == null || forceAccuracy < 0) { 68 | latitude = null; 69 | longitude = null; 70 | accuracy = null; 71 | return; 72 | } 73 | // round down to specified precision 74 | latitude = BigDecimal.valueOf(location.getLatitude()).setScale(forceAccuracy, RoundingMode.HALF_UP).doubleValue(); 75 | longitude = BigDecimal.valueOf(location.getLongitude()).setScale(forceAccuracy, RoundingMode.HALF_UP).doubleValue(); 76 | accuracy = null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/EncounterQueue.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.content.Context; 26 | 27 | import com.google.gson.reflect.TypeToken; 28 | 29 | import java.lang.reflect.Type; 30 | import java.util.List; 31 | 32 | public class EncounterQueue extends DataQueue { 33 | 34 | private static final Type LIST_TYPE = new TypeToken>(){}.getType(); 35 | 36 | public EncounterQueue(Context context) { 37 | super(context, "encounters.json"); 38 | } 39 | 40 | @Override 41 | protected Type getListType() { 42 | return LIST_TYPE; 43 | } 44 | 45 | @Override 46 | protected void makeSendRequest(List data, Api.Listener listener) { 47 | final Api.ContactRequest request = new Api.ContactRequest(App.get(context).prefs().getString(Prefs.DEVICE_UID, null), 48 | App.get(context).prefs().getLong(Prefs.DEVICE_ID, 0)); 49 | request.connections.addAll(data); 50 | new Api(context).sendContacts(request, listener); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/FcmService.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.content.SharedPreferences; 26 | 27 | import androidx.annotation.NonNull; 28 | 29 | import com.google.firebase.messaging.FirebaseMessagingService; 30 | import com.google.firebase.messaging.RemoteMessage; 31 | 32 | public class FcmService extends FirebaseMessagingService { 33 | @Override 34 | public void onNewToken(@NonNull String s) { 35 | SharedPreferences prefs = App.get(this).prefs(); 36 | prefs.edit().putString(Prefs.FCM_TOKEN, s).apply(); 37 | // Check if we're agreed and registered and if so, send the token to API 38 | if (prefs.getBoolean(Prefs.TERMS, false)) { 39 | Api.ProfileRequest request = new Api.ProfileRequest(prefs.getString(Prefs.DEVICE_UID, null), 40 | s, prefs.getString(Prefs.PHONE_NUMBER, null)); 41 | new Api(this).createProfile(request, null); 42 | } 43 | } 44 | 45 | @Override 46 | public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { 47 | // TODO LATER Implement 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ICountryDefaults.java: -------------------------------------------------------------------------------- 1 | package intl.who.covid19; 2 | 3 | import android.content.Context; 4 | 5 | import java.util.List; 6 | 7 | import intl.who.covid19.ui.HomeFragment; 8 | import intl.who.covid19.ui.MapFragment; 9 | import intl.who.covid19.ui.PhoneVerificationActivity; 10 | 11 | public interface ICountryDefaults { 12 | /** 13 | * Whether to ask the user for his phone number and verify it at first launch of the app. 14 | */ 15 | boolean verifyPhoneNumberAtStart(); 16 | 17 | /** 18 | * Whether to allow the user to enter his start of the quarantine when entering quarantine. 19 | */ 20 | boolean showQuarantineStartPicker(); 21 | 22 | /** 23 | * Whether to use face ID when entering quarantine and later with random checks via a push message. 24 | * If you return true in your flavor, don't forget to include an implementation dependency in the module build.gradle 25 | */ 26 | boolean useFaceId(); 27 | 28 | /** 29 | * Whether to send location information while in quarantine. 30 | */ 31 | boolean sendLocationInQuarantine(); 32 | 33 | /** 34 | * Whether to notify API when user left the quarantine. 35 | */ 36 | boolean sendQuarantineLeft(); 37 | 38 | /** 39 | * Whether to notify API when user left the quarantine. 40 | */ 41 | boolean roundEncounterTimestampToDays(); 42 | 43 | /** 44 | * Two-letter code of current country (uppercase) 45 | */ 46 | String getCountryCode(); 47 | 48 | /** 49 | * Country center latitude. 50 | */ 51 | double getCenterLat(); 52 | 53 | /** 54 | * Country center longitude. 55 | */ 56 | double getCenterLng(); 57 | 58 | /** 59 | * Zoom to cover whole (most of the) country. 60 | */ 61 | double getCenterZoom(); 62 | 63 | /** 64 | * Load country-specific stats and return them in the callback. Be sure to call the callback on the main UI thread. 65 | * It is recommended to cache the stats locally in preferences and only reload them once a day/hour/etc. 66 | * @param context 67 | * @param callback 68 | */ 69 | void getStats(Context context, App.Callback callback); 70 | 71 | /** 72 | * Load per-county stats for the country and return them in the callback. Be sure to call the callback on the main UI thread. 73 | * It is recommended to cache the stats locally in preferences and only reload them once a day/hour/etc. 74 | * @param context 75 | * @param callback 76 | */ 77 | void getCountyStats(Context context, App.Callback> callback); 78 | 79 | /** 80 | * Request a service to send a verification code to the specified phone number. 81 | * @param context 82 | * @param phoneNumber The phone number to send the code to in international format (E164). 83 | * @param callback Callback with optional exception, send with null for success. 84 | */ 85 | void sendVerificationCodeText(Context context, String phoneNumber, App.Callback callback); 86 | 87 | /** 88 | * The length of the phone number verification code. 89 | */ 90 | int getVerificationCodeLength(); 91 | 92 | /** 93 | * 94 | * @param context 95 | * @param code The code to verify with the server. 96 | * @param callback Callback with quarantine data, send with null for error. 97 | */ 98 | void checkVerificationCode(Context context, String phoneNumber, String code, App.Callback callback); 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/LocalNotificationReceiver.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.app.AlarmManager; 26 | import android.app.Notification; 27 | import android.app.PendingIntent; 28 | import android.content.BroadcastReceiver; 29 | import android.content.Context; 30 | import android.content.Intent; 31 | import android.os.Build; 32 | 33 | import androidx.core.app.NotificationCompat; 34 | import androidx.core.app.NotificationManagerCompat; 35 | import androidx.core.content.res.ResourcesCompat; 36 | 37 | import java.util.Calendar; 38 | 39 | import intl.who.covid19.ui.WelcomeActivity; 40 | 41 | public class LocalNotificationReceiver extends BroadcastReceiver { 42 | private static final int ID_QUARANTINE = 1; 43 | 44 | public static void scheduleNotification(Context context) { 45 | AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 46 | if (alarmManager == null) { 47 | return; 48 | } 49 | if (App.get(context).isInQuarantine()) { 50 | Calendar cal = Calendar.getInstance(); 51 | if (cal.get(Calendar.HOUR_OF_DAY) >= 9) { 52 | cal.add(Calendar.DATE, 1); 53 | } 54 | cal.set(Calendar.HOUR_OF_DAY, 9); 55 | cal.set(Calendar.MINUTE, 0); 56 | cal.set(Calendar.SECOND, 0); 57 | cal.set(Calendar.MILLISECOND, 0); 58 | App.log("Scheduling alarm local notification for " + cal.getTime()); 59 | if (Build.VERSION.SDK_INT >= 23) { 60 | alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), getPendingIntent(context)); 61 | } else { 62 | alarmManager.setExact(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), getPendingIntent(context)); 63 | } 64 | } else { 65 | App.log("Canceling alarm local notification"); 66 | alarmManager.cancel(getPendingIntent(context)); 67 | } 68 | } 69 | 70 | private static PendingIntent getPendingIntent(Context context) { 71 | return PendingIntent.getBroadcast(context, ID_QUARANTINE, new Intent(context, LocalNotificationReceiver.class), PendingIntent.FLAG_UPDATE_CURRENT); 72 | } 73 | 74 | @Override 75 | public void onReceive(Context context, Intent intent) { 76 | if (!App.get(context).isInQuarantine()) { 77 | return; 78 | } 79 | // Schedule notification for next day 80 | scheduleNotification(context); 81 | // Show the notification 82 | String text = context.getString(R.string.notification_quarantineInfo_text, App.get(context).getDaysLeftInQuarantine()); 83 | Notification notification = new NotificationCompat.Builder(context, App.NOTIFICATION_CHANNEL_ALARM) 84 | .setAutoCancel(true) 85 | .setColor(ResourcesCompat.getColor(context.getResources(), R.color.red, null)) 86 | .setSmallIcon(R.drawable.ic_notification) 87 | .setContentTitle(context.getString(R.string.notification_quarantineInfo_title)) 88 | .setContentText(text) 89 | .setContentIntent(PendingIntent.getActivity(context, 1, new Intent(context, WelcomeActivity.class) 90 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_UPDATE_CURRENT)) 91 | .setStyle(new NotificationCompat.BigTextStyle().bigText(text)) 92 | .setPriority(NotificationCompat.PRIORITY_HIGH) 93 | .build(); 94 | NotificationManagerCompat.from(context).notify(App.NOTIFICATION_ID_QUARANTINE_INFO, notification); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/Location.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import androidx.annotation.NonNull; 26 | 27 | import com.google.gson.annotations.SerializedName; 28 | 29 | public class Location { 30 | 31 | // Do not rename these fields! They are named to match API 32 | private double latitude; 33 | private double longitude; 34 | private Integer accuracy; 35 | @SerializedName(value = "recordTimestamp", alternate = { "timestamp" }) // For back-compatibility 36 | private long recordTimestamp; 37 | 38 | public Location() { } 39 | public Location(@NonNull android.location.Location src) { 40 | latitude = src.getLatitude(); 41 | longitude = src.getLongitude(); 42 | accuracy = src.hasAccuracy() ? (int)src.getAccuracy() : null; 43 | recordTimestamp = src.getTime() / 1000; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/LocationQueue.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.content.Context; 26 | 27 | import com.google.gson.reflect.TypeToken; 28 | 29 | import java.lang.reflect.Type; 30 | import java.util.List; 31 | 32 | public class LocationQueue extends DataQueue { 33 | 34 | private static final Type LIST_TYPE = new TypeToken>(){}.getType(); 35 | 36 | public LocationQueue(Context context) { 37 | super(context, "locations.json"); 38 | } 39 | 40 | @Override 41 | protected Type getListType() { 42 | return LIST_TYPE; 43 | } 44 | 45 | @Override 46 | protected void makeSendRequest(List data, Api.Listener listener) { 47 | final Api.LocationRequest request = new Api.LocationRequest( 48 | App.get(context).prefs().getString(Prefs.DEVICE_UID, null), 49 | App.get(context).prefs().getLong(Prefs.DEVICE_ID, 0)); 50 | request.locations.addAll(data); 51 | new Api(context).sendLocations(request, listener); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/Prefs.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | public class Prefs { 26 | 27 | private static final int TERMS_VERSION = 1; 28 | 29 | /** boolean (whether terms are already agreed) */ 30 | public static final String TERMS = "terms-v" + TERMS_VERSION; 31 | /** String (device-generated UUID) */ 32 | public static final String DEVICE_UID = "deviceUid"; 33 | /** long (server assigned sequential ID) */ 34 | public static final String DEVICE_ID = "deviceId"; 35 | /** long (local health authority assigned ID) */ 36 | public static final String COVID_ID = "covidId"; 37 | /** String (current Firebase Cloud Messaging token) */ 38 | public static final String FCM_TOKEN = "fcmToken"; 39 | /** String (confirmed phone number) */ 40 | public static final String PHONE_NUMBER = "phoneNumber"; 41 | /** String (phone number verification code - may need to be sent later when confirming quarantine or disease) */ 42 | public static final String PHONE_NUMBER_VERIFICATION_CODE = "phoneNumberVerificationCode"; 43 | /** double (latitude of home address) */ 44 | public static final String HOME_LAT = "homeLat"; 45 | /** double (longitude of home address) */ 46 | public static final String HOME_LNG = "homeLng"; 47 | /** String (address of home) */ 48 | public static final String HOME_ADDRESS = "homeAddress"; 49 | /** long (date/time of when the quarantine ends in milliseconds) */ 50 | public static final String QUARANTINE_ENDS = "quarantineEnds"; 51 | /** long (date/time of when the quarantine end has been last checked in milliseconds) */ 52 | public static final String QUARANTINE_ENDS_LAST_CHECK = "quarantineEndsLastCheck"; 53 | /** byte[] (face template data) */ 54 | public static final String FACE_TEMPLATE_DATA = "faceTemplateData"; 55 | /** boolean (whether face check is required prior to opening an app) */ 56 | public static final String REQUIRED_FACE_CHECK = "requiredFaceCheck"; 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/UploadService.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19; 24 | 25 | import android.app.job.JobInfo; 26 | import android.app.job.JobParameters; 27 | import android.app.job.JobScheduler; 28 | import android.app.job.JobService; 29 | import android.content.ComponentName; 30 | import android.content.Context; 31 | import android.content.Intent; 32 | import android.content.ServiceConnection; 33 | import android.os.Build; 34 | import android.os.IBinder; 35 | 36 | public class UploadService extends JobService { 37 | 38 | private static final int JOB_ID_ENCOUNTERS = 1; 39 | private static final int JOB_ID_LOCATIONS = 2; 40 | 41 | public static void start(Context context) { 42 | App.log("UploadService: start"); 43 | long sendingPeriodMinutes = App.get(context).getRemoteConfig().getLong(App.RC_BATCH_SEDNING_FREQUENCY); 44 | long periodMillis = (BuildConfig.DEBUG ? 1 : sendingPeriodMinutes) * 60_000; 45 | scheduleJob(context, JOB_ID_ENCOUNTERS, periodMillis); 46 | scheduleJob(context, JOB_ID_LOCATIONS, periodMillis); 47 | } 48 | 49 | private static void scheduleJob(Context context, int jobId, long periodMillis) { 50 | JobScheduler scheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); 51 | for (JobInfo info : scheduler.getAllPendingJobs()) { 52 | if (info.getId() == jobId && info.getIntervalMillis() == periodMillis) { 53 | App.log("UploadService: job ("+jobId+") already scheduled"); 54 | return; 55 | } 56 | } 57 | 58 | JobInfo.Builder builder = new JobInfo.Builder(jobId, new ComponentName(context, UploadService.class)); 59 | builder.setPersisted(true); 60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 61 | builder.setPeriodic(periodMillis, periodMillis / 6); 62 | } else { 63 | builder.setPeriodic(periodMillis); 64 | } 65 | int result = scheduler.schedule(builder.build()); 66 | if (result <= 0) { 67 | throw new RuntimeException("Can't schedule upload job ("+jobId+"), result = " + result); 68 | } 69 | } 70 | 71 | private final BeaconConnection beaconConnection = new BeaconConnection(); 72 | private JobParameters currentEncJob; 73 | private boolean binding; 74 | 75 | @Override 76 | public boolean onStartJob(JobParameters params) { 77 | App.log("UploadService: onStartJob, id = " + params.getJobId()); 78 | switch (params.getJobId()) { 79 | case JOB_ID_ENCOUNTERS: 80 | currentEncJob = params; 81 | if (!bindService(new Intent(this, BeaconService.class), beaconConnection, 0)) { 82 | App.log("UploadService: Can't bind to BeaconService"); 83 | return false; 84 | } 85 | binding = true; 86 | return true; 87 | 88 | case JOB_ID_LOCATIONS: 89 | App.get(this).getLocationQueue().send(() -> { 90 | App.log("UploadService: on locations sent"); 91 | jobFinished(params, false); 92 | }); 93 | return true; 94 | 95 | } 96 | return false; 97 | } 98 | 99 | @Override 100 | public boolean onStopJob(JobParameters params) { 101 | App.log("UploadService: onStopJob"); 102 | switch (params.getJobId()) { 103 | case JOB_ID_ENCOUNTERS: 104 | if (binding) { 105 | binding = false; 106 | try { 107 | unbindService(beaconConnection); 108 | } catch (RuntimeException ex) { 109 | // service already unbound 110 | } 111 | } 112 | currentEncJob = null; 113 | break; 114 | } 115 | return false; 116 | } 117 | 118 | private class BeaconConnection implements ServiceConnection { 119 | @Override 120 | public void onServiceConnected(ComponentName name, IBinder service) { 121 | App.log("UploadService: onServiceConnected"); 122 | BeaconService.Binder beacon = (BeaconService.Binder) service; 123 | beacon.cutLiveEncounters(); 124 | 125 | JobParameters localParams = currentEncJob; 126 | if (localParams != null) { 127 | App.get(UploadService.this).getEncounterQueue().send(() -> { 128 | App.log("UploadService: on encounters sent"); 129 | jobFinished(localParams, false); 130 | }); 131 | } 132 | 133 | binding = false; 134 | unbindService(beaconConnection); 135 | } 136 | @Override 137 | public void onServiceDisconnected(ComponentName name) { 138 | App.log("UploadService: onServiceDisconnected"); 139 | binding = false; 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/AboutActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.os.Bundle; 26 | 27 | import androidx.annotation.Nullable; 28 | import androidx.appcompat.app.AppCompatActivity; 29 | 30 | import intl.who.covid19.R; 31 | 32 | public class AboutActivity extends AppCompatActivity { 33 | @Override 34 | protected void onCreate(@Nullable Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_about); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/AddressActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.annotation.SuppressLint; 26 | import android.content.Intent; 27 | import android.location.Address; 28 | import android.location.Geocoder; 29 | import android.os.AsyncTask; 30 | import android.os.Bundle; 31 | import android.view.View; 32 | import android.widget.TextView; 33 | 34 | import androidx.annotation.Nullable; 35 | import androidx.appcompat.app.AppCompatActivity; 36 | 37 | import com.google.android.gms.maps.CameraUpdateFactory; 38 | import com.google.android.gms.maps.GoogleMap; 39 | import com.google.android.gms.maps.OnMapReadyCallback; 40 | import com.google.android.gms.maps.SupportMapFragment; 41 | import com.google.android.gms.maps.model.LatLng; 42 | import com.google.android.libraries.places.api.model.Place; 43 | import com.google.android.libraries.places.api.model.TypeFilter; 44 | import com.google.android.libraries.places.widget.Autocomplete; 45 | import com.google.android.libraries.places.widget.model.AutocompleteActivityMode; 46 | 47 | import java.util.Arrays; 48 | import java.util.List; 49 | 50 | import intl.who.covid19.App; 51 | import intl.who.covid19.R; 52 | 53 | public class AddressActivity extends AppCompatActivity implements OnMapReadyCallback { 54 | public static final String EXTRA_ADDRESS = "intl.who.covid19.EXTRA_ADDRESS"; 55 | public static final String EXTRA_LAT = "intl.who.covid19.EXTRA_LAT"; 56 | public static final String EXTRA_LNG = "intl.who.covid19.EXTRA_LNG"; 57 | private static final int DEFAULT_ZOOM = 17; 58 | private static final int REQUEST_AUTOCOMPLETE = 1; 59 | 60 | private GoogleMap map; 61 | private String address; 62 | 63 | @Override 64 | protected void onCreate(@Nullable Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | setContentView(R.layout.activity_address); 67 | ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_map)).getMapAsync(this); 68 | new ConfirmDialog(this, getString(R.string.address_info)) 69 | .setButton1(getString(R.string.app_ok), R.drawable.bg_btn_blue, null) 70 | .show(); 71 | } 72 | 73 | @Override 74 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 75 | if (requestCode == REQUEST_AUTOCOMPLETE && resultCode == RESULT_OK && data != null) { 76 | Place place = Autocomplete.getPlaceFromIntent(data); 77 | address = place.getAddress(); 78 | this.findViewById(R.id.textView_address).setText("\n" + address); 79 | if (map != null) { 80 | map.animateCamera(CameraUpdateFactory.newLatLngZoom(place.getLatLng(), DEFAULT_ZOOM)); 81 | } 82 | } 83 | super.onActivityResult(requestCode, resultCode, data); 84 | } 85 | 86 | @SuppressLint("StaticFieldLeak") 87 | @Override 88 | public void onMapReady(GoogleMap googleMap) { 89 | map = googleMap; 90 | App.get(this).getFusedLocationClient().getLastLocation().addOnSuccessListener(result -> { 91 | if (result != null) { 92 | map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(result.getLatitude(), result.getLongitude()), DEFAULT_ZOOM)); 93 | } 94 | }); 95 | map.setOnCameraIdleListener(() -> { 96 | LatLng center = map.getCameraPosition().target; 97 | address = null; 98 | findViewById(R.id.progressBar).setVisibility(View.VISIBLE); 99 | new AsyncTask() { 100 | @Override 101 | protected String doInBackground(Void... voids) { 102 | try { 103 | List
addresses = new Geocoder(AddressActivity.this) 104 | .getFromLocation(center.latitude, center.longitude, 1); 105 | if (addresses.size() > 0) { 106 | Address address = addresses.get(0); 107 | StringBuilder strAddress = new StringBuilder(); 108 | for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) { 109 | strAddress.append(address.getAddressLine(i)); 110 | if (i < address.getMaxAddressLineIndex()) { 111 | strAddress.append(", "); 112 | } 113 | } 114 | return strAddress.toString(); 115 | } 116 | } catch (Exception e) { } 117 | return Math.round(center.latitude * 100_000) / 100_000.0 + ", " + Math.round(center.longitude * 100_000) / 100_000.0; 118 | } 119 | @Override 120 | protected void onPostExecute(String address) { 121 | if (center.equals(map.getCameraPosition().target)) { 122 | findViewById(R.id.progressBar).setVisibility(View.GONE); 123 | AddressActivity.this.address = address; 124 | AddressActivity.this.findViewById(R.id.textView_address).setText("\n" + address); 125 | } 126 | } 127 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 128 | }); 129 | } 130 | 131 | public void onButtonSearch(View v) { 132 | startActivityForResult(new Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, Arrays.asList(Place.Field.ADDRESS, Place.Field.LAT_LNG)) 133 | .setCountry(App.get(this).getCountryDefaults().getCountryCode()) 134 | .setTypeFilter(TypeFilter.ADDRESS) 135 | .setHint(getString(R.string.home_address)) 136 | .build(this), REQUEST_AUTOCOMPLETE); 137 | } 138 | 139 | public void onButtonPick(View v) { 140 | if (address == null) { 141 | return; 142 | } 143 | new ConfirmDialog(this, getString(R.string.address_confirm, address)) 144 | .setButton1(getString(R.string.address_confirm_confirm), R.drawable.bg_btn_green, view -> { 145 | LatLng center = map.getCameraPosition().target; 146 | setResult(RESULT_OK, new Intent() 147 | .putExtras(getIntent()) 148 | .putExtra(EXTRA_ADDRESS, address) 149 | .putExtra(EXTRA_LAT, center.latitude) 150 | .putExtra(EXTRA_LNG, center.longitude)); 151 | finish(); 152 | }) 153 | .setButton2(getString(R.string.address_confirm_change), R.drawable.bg_btn_red, null) 154 | .show(); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/ConfirmDialog.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.content.Context; 26 | import android.view.View; 27 | import android.widget.Button; 28 | import android.widget.TextView; 29 | 30 | import androidx.annotation.DrawableRes; 31 | import androidx.annotation.Nullable; 32 | 33 | import com.google.android.material.bottomsheet.BottomSheetDialog; 34 | 35 | import intl.who.covid19.R; 36 | 37 | public class ConfirmDialog extends BottomSheetDialog { 38 | public ConfirmDialog(Context context, String text) { 39 | super(context); 40 | setContentView(R.layout.dialog_confirm); 41 | this.findViewById(R.id.textView_text).setText(text); 42 | } 43 | 44 | public ConfirmDialog setButton1(String text, @DrawableRes int background, @Nullable View.OnClickListener onClick) { 45 | setButton(R.id.button1, text, background, onClick); 46 | return this; 47 | } 48 | 49 | public ConfirmDialog setButton2(String text, @DrawableRes int background, @Nullable View.OnClickListener onClick) { 50 | setButton(R.id.button2, text, background, onClick); 51 | return this; 52 | } 53 | 54 | private void setButton(int id, String text, @DrawableRes int background, @Nullable View.OnClickListener onClick) { 55 | Button button = findViewById(id); 56 | if (button == null) { 57 | return; 58 | } 59 | button.setText(text); 60 | button.setBackgroundResource(background); 61 | button.setOnClickListener(v -> { 62 | dismiss(); 63 | if (onClick != null) { 64 | onClick.onClick(v); 65 | } 66 | }); 67 | button.setVisibility(View.VISIBLE); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/FaceIdActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.app.Activity; 26 | import android.os.Bundle; 27 | import android.view.View; 28 | import android.widget.TextView; 29 | 30 | import androidx.annotation.Nullable; 31 | import androidx.annotation.StringRes; 32 | import androidx.appcompat.app.AlertDialog; 33 | import androidx.appcompat.app.AppCompatActivity; 34 | import androidx.appcompat.widget.AppCompatImageView; 35 | import androidx.fragment.app.Fragment; 36 | 37 | import com.innovatrics.android.dot.Dot; 38 | import com.innovatrics.android.dot.dto.FaceCaptureArguments; 39 | import com.innovatrics.android.dot.dto.Photo; 40 | import com.innovatrics.android.dot.face.DetectedFace; 41 | import com.innovatrics.android.dot.facecapture.steps.CaptureState; 42 | import com.innovatrics.android.dot.utils.LicenseUtils; 43 | import com.innovatrics.android.dot.verification.TemplateVerifier; 44 | 45 | import intl.who.covid19.App; 46 | import intl.who.covid19.Prefs; 47 | import intl.who.covid19.R; 48 | 49 | public class FaceIdActivity extends AppCompatActivity implements Dot.Listener { 50 | /** Whether to show the registration/learning flow (false/missing) or verification flow (true) */ 51 | public static final String EXTRA_LEARN = "intl.who.covid19.EXTRA_LEARN"; 52 | 53 | public static class FaceCaptureFragment extends com.innovatrics.android.dot.fragment.FaceCaptureFragment { 54 | @Override 55 | protected void onCameraInitFailed() { 56 | onFailed(R.string.faceid_failCameraInit, true); 57 | } 58 | @Override 59 | protected void onCameraAccessFailed() { 60 | onFailed(R.string.faceid_failCameraAccess, false); 61 | } 62 | @Override 63 | protected void onNoCameraPermission() { 64 | onFailed(R.string.faceid_failCameraPermission, true); 65 | } 66 | @Override 67 | protected void onCaptureStateChange(CaptureState captureState, Photo photo) { } 68 | @Override 69 | protected void onCaptureSuccess(DetectedFace detectedFace) { 70 | Activity activity = getActivity(); 71 | if (activity instanceof FaceIdActivity) { 72 | ((FaceIdActivity) activity).onFaceDetected(detectedFace); 73 | } 74 | } 75 | @Override 76 | protected void onCaptureFail() { 77 | onFailed(R.string.faceid_failCapture, true); 78 | } 79 | private void onFailed(@StringRes int error, boolean retry) { 80 | Activity activity = getActivity(); 81 | if (activity instanceof FaceIdActivity) { 82 | ((FaceIdActivity) activity).showResult(false, getString(error), retry); 83 | } 84 | } 85 | } 86 | 87 | @Override 88 | protected void onCreate(@Nullable Bundle savedInstanceState) { 89 | super.onCreate(savedInstanceState); 90 | setContentView(R.layout.activity_faceid); 91 | if (!Dot.getInstance().isInitialized()) { 92 | int licenseResId = getResources().getIdentifier("innovatrics_license", "raw", getPackageName()); 93 | if (licenseResId == 0) { 94 | new AlertDialog.Builder(this) 95 | .setTitle(R.string.app_name) 96 | .setMessage("Missing innovatrics license file in raw/innovatrics_license") 97 | .setPositiveButton(R.string.app_ok, (d, w) -> finish()) 98 | .show(); 99 | } else { 100 | Dot.getInstance().initAsync(LicenseUtils.loadRawLicense(this, licenseResId), this, 101 | (float) App.get(this).getRemoteConfig().getDouble(App.RC_FACEID_CONFIDENCE_THRESHOLD)); 102 | } 103 | } 104 | } 105 | 106 | @Override 107 | protected void onDestroy() { 108 | super.onDestroy(); 109 | Dot.getInstance().closeAsync(this); 110 | } 111 | 112 | @Override 113 | public void onInitSuccess() { 114 | getWindow().getDecorView().post(() -> { 115 | findViewById(R.id.progressBar).setVisibility(View.GONE); 116 | if (isLearning()) { 117 | findViewById(R.id.layout_intro).setVisibility(View.VISIBLE); 118 | } else { 119 | showFaceDetectionFragment(); 120 | } 121 | }); 122 | } 123 | 124 | @Override 125 | public void onInitFail(final String message) { 126 | getWindow().getDecorView().post(() -> { 127 | new AlertDialog.Builder(FaceIdActivity.this) 128 | .setTitle(R.string.app_name) 129 | .setMessage("Invalid innovatrics license: " + message) 130 | .setPositiveButton(R.string.app_ok, (d, w) -> finish()) 131 | .show(); 132 | }); 133 | } 134 | 135 | @Override 136 | public void onClosed() { 137 | } 138 | 139 | public void onButtonStart(View v) { 140 | findViewById(R.id.layout_intro).setVisibility(View.GONE); 141 | showFaceDetectionFragment(); 142 | } 143 | 144 | private void showFaceDetectionFragment() { 145 | final Bundle arguments = new Bundle(); 146 | Fragment fragment = new FaceCaptureFragment(); 147 | arguments.putSerializable(FaceCaptureFragment.ARGUMENTS, new FaceCaptureArguments.Builder() 148 | .lightScoreThreshold(.4) 149 | .build()); 150 | fragment.setArguments(arguments); 151 | getSupportFragmentManager().beginTransaction().replace(R.id.layout_container, fragment).commit(); 152 | } 153 | 154 | private void removeFaceDetectionFragment() { 155 | for (Fragment fragment : getSupportFragmentManager().getFragments()) { 156 | getSupportFragmentManager().beginTransaction().remove(fragment).commit(); 157 | } 158 | } 159 | 160 | private void onFaceDetected(DetectedFace face) { 161 | if (isLearning()) { 162 | // Save detected face template data 163 | App.get(this).prefs().edit().putString(Prefs.FACE_TEMPLATE_DATA, bytesToHex(face.createTemplate().getTemplate())).apply(); 164 | showResult(true, getString(R.string.faceid_success), false); 165 | } else { 166 | // Compare with saved face template 167 | byte[] savedTemplate = hexToBytes(App.get(this).prefs().getString(Prefs.FACE_TEMPLATE_DATA, "")); 168 | try { 169 | float score = new TemplateVerifier().match(savedTemplate, face.createTemplate().getTemplate()); 170 | if (score >= App.get(this).getRemoteConfig().getDouble(App.RC_FACEID_MATCH_THRESHOLD)) { 171 | showResult(true, getString(R.string.faceid_success), false); 172 | } else { 173 | showResult(false, getString(R.string.faceid_failVerify), true); 174 | } 175 | } catch (Exception e) { 176 | showResult(false, e.getMessage(), true); 177 | } 178 | } 179 | } 180 | 181 | private void showResult(boolean success, String message, boolean retry) { 182 | removeFaceDetectionFragment(); 183 | findViewById(R.id.layout_result).setVisibility(View.VISIBLE); 184 | this.findViewById(R.id.imageView_result).setImageResource(success ? R.drawable.ic_check_green : R.drawable.ic_check_red); 185 | this.findViewById(R.id.textView_result_title).setText(success ? R.string.faceid_thankYou : R.string.faceid_sorry); 186 | this.findViewById(R.id.textView_result_text).setText(message); 187 | findViewById(R.id.button_continue).setOnClickListener(v -> { 188 | if (success) { 189 | setResult(RESULT_OK); 190 | finish(); 191 | } else if (retry) { 192 | findViewById(R.id.layout_result).setVisibility(View.GONE); 193 | showFaceDetectionFragment(); 194 | } else { 195 | finish(); 196 | } 197 | }); 198 | } 199 | 200 | private boolean isLearning() { 201 | return getIntent().getBooleanExtra(EXTRA_LEARN, false); 202 | } 203 | 204 | private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); 205 | private String bytesToHex(byte[] bytes) { 206 | char[] hexChars = new char[bytes.length * 2]; 207 | for (int j = 0; j < bytes.length; j++) { 208 | int v = bytes[j] & 0xFF; 209 | hexChars[j * 2] = HEX_ARRAY[v >>> 4]; 210 | hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; 211 | } 212 | return new String(hexChars); 213 | } 214 | private static byte[] hexToBytes(String s) { 215 | int len = s.length(); 216 | byte[] data = new byte[len / 2]; 217 | for(int i = 0; i < len; i += 2){ 218 | data[i/2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); 219 | } 220 | return data; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/HomeActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.content.Intent; 26 | import android.os.Bundle; 27 | 28 | import androidx.annotation.NonNull; 29 | import androidx.appcompat.app.AppCompatActivity; 30 | import androidx.fragment.app.Fragment; 31 | import androidx.fragment.app.FragmentPagerAdapter; 32 | import androidx.viewpager.widget.ViewPager; 33 | 34 | import com.google.android.material.tabs.TabLayout; 35 | 36 | import intl.who.covid19.BeaconService; 37 | import intl.who.covid19.R; 38 | import intl.who.covid19.UploadService; 39 | 40 | public class HomeActivity extends AppCompatActivity { 41 | /** boolean Whether to ask the user immediately if he's coming from abroad */ 42 | public static final String EXTRA_ASK_QUARANTINE = "intl.who.covid19.EXTRA_CHECK_QUARANTINE"; 43 | 44 | private HomeFragment homeFragment; 45 | private MapFragment mapFragment; 46 | private ProfileFragment profileFragment; 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.activity_home); 52 | homeFragment = new HomeFragment(); 53 | mapFragment = new MapFragment(); 54 | profileFragment = new ProfileFragment(); 55 | ViewPager viewPager = findViewById(R.id.viewPager); 56 | viewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager(), 0) { 57 | @Override 58 | public int getCount() { 59 | return 3; 60 | } 61 | @NonNull 62 | @Override 63 | public Fragment getItem(int position) { 64 | switch (position) { 65 | case 1: return mapFragment; 66 | case 2: return profileFragment; 67 | default: return homeFragment; 68 | } 69 | } 70 | }); 71 | TabLayout tabLayout = findViewById(R.id.tabLayout); 72 | tabLayout.setupWithViewPager(viewPager); 73 | tabLayout.getTabAt(0).setIcon(R.drawable.home_home); 74 | tabLayout.getTabAt(1).setIcon(R.drawable.home_map); 75 | tabLayout.getTabAt(2).setIcon(R.drawable.home_profile); 76 | startService(new Intent(this, BeaconService.class)); 77 | UploadService.start(this); 78 | if (savedInstanceState == null && getIntent().getBooleanExtra(EXTRA_ASK_QUARANTINE, false)) { 79 | new ConfirmDialog(this, getString(R.string.home_checkQuarantine)) 80 | .setButton1(getString(R.string.app_yes), R.drawable.bg_btn_red, v -> homeFragment.onButtonQuarantine()) 81 | .setButton2(getString(R.string.app_no), R.drawable.bg_btn_green, null) 82 | .show(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/PhoneVerificationActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.app.Activity; 26 | import android.content.Intent; 27 | import android.content.SharedPreferences; 28 | import android.os.Bundle; 29 | import android.text.Editable; 30 | import android.text.TextWatcher; 31 | import android.view.View; 32 | import android.view.inputmethod.InputMethodManager; 33 | import android.widget.Button; 34 | import android.widget.CheckBox; 35 | import android.widget.EditText; 36 | import android.widget.ProgressBar; 37 | import android.widget.TextView; 38 | 39 | import androidx.appcompat.app.AlertDialog; 40 | import androidx.appcompat.app.AppCompatActivity; 41 | 42 | import com.github.ialokim.phonefield.PhoneInputLayout; 43 | 44 | import java.io.IOException; 45 | 46 | import intl.who.covid19.Api; 47 | import intl.who.covid19.App; 48 | import intl.who.covid19.Prefs; 49 | import intl.who.covid19.R; 50 | 51 | public class PhoneVerificationActivity extends AppCompatActivity { 52 | public static class QuarantineDetails { 53 | public String covidId; 54 | public String quarantineStart; 55 | public String quarantineEnd; 56 | } 57 | 58 | public static final String EXTRA_SHOW_EXPLANATION = "intl.who.covid19.ui.EXTRA_SHOW_EXPLANATION"; 59 | 60 | private PhoneInputLayout phoneInput; 61 | private EditText editTextCode; 62 | private CheckBox checkBoxPrivacyConsent; 63 | private Button buttonDone; 64 | private ProgressBar progressBar; 65 | 66 | @Override 67 | protected void onCreate(Bundle savedInstanceState) { 68 | super.onCreate(savedInstanceState); 69 | setContentView(R.layout.activity_phone_verification); 70 | 71 | phoneInput = findViewById(R.id.phoneInput); 72 | editTextCode = findViewById(R.id.editText_code); 73 | checkBoxPrivacyConsent = findViewById(R.id.checkBox_privacyConsent); 74 | buttonDone = findViewById(R.id.button_done); 75 | progressBar = findViewById(R.id.progressBar); 76 | 77 | phoneInput.setDefaultCountry(App.get(this).getCountryDefaults().getCountryCode()); 78 | editTextCode.addTextChangedListener(new TextWatcher() { 79 | @Override 80 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 81 | @Override 82 | public void onTextChanged(CharSequence s, int start, int before, int count) { } 83 | @Override 84 | public void afterTextChanged(Editable s) { 85 | if (s.length() == App.get(PhoneVerificationActivity.this).getCountryDefaults().getVerificationCodeLength()) { 86 | hideKeyboard(); 87 | confirmVerificationCode(s.toString()); 88 | } 89 | } 90 | }); 91 | ((TextView) findViewById(R.id.textView_text)).setText(getIntent().getBooleanExtra(EXTRA_SHOW_EXPLANATION, false) ? 92 | R.string.phoneVerification_explanation : R.string.phoneVerification_text); 93 | // Check if we have the phone number and verification code stored and confirm immediately if so 94 | String verificationCode = App.get(this).prefs().getString(Prefs.PHONE_NUMBER_VERIFICATION_CODE, null); 95 | if (verificationCode != null) { 96 | confirmVerificationCode(verificationCode); 97 | } 98 | } 99 | 100 | @Override 101 | public void onBackPressed() { 102 | if (editTextCode.getVisibility() == View.VISIBLE) { 103 | new AlertDialog.Builder(this) 104 | .setTitle(R.string.phoneVerification_title) 105 | .setMessage(R.string.phoneVerification_leaveProcess) 106 | .setPositiveButton(R.string.app_yes, (dialog, which) -> super.onBackPressed()) 107 | .setNegativeButton(R.string.app_no, null) 108 | .show(); 109 | } else { 110 | super.onBackPressed(); 111 | } 112 | } 113 | 114 | public void onPrivacy(View v) { 115 | startActivity(new Intent(this, PrivacyPolicyActivity.class)); 116 | } 117 | 118 | public void onButtonDone(View view) { 119 | // checks if the field is valid 120 | if (!phoneInput.isValid()) { 121 | phoneInput.setError(getString(R.string.phoneVerification_invalidNumber)); 122 | } else if (!checkBoxPrivacyConsent.isChecked()) { 123 | phoneInput.setError(null); 124 | new AlertDialog.Builder(this) 125 | .setTitle(R.string.app_name) 126 | .setMessage(R.string.phoneVerification_notAgreed) 127 | .setPositiveButton(R.string.app_ok, null) 128 | .show(); 129 | } else { 130 | phoneInput.setError(null); 131 | hideKeyboard(); 132 | showVerificationDialog(phoneInput.getPhoneNumberE164()); 133 | } 134 | } 135 | 136 | private void showVerificationDialog(String phoneNumber) { 137 | // verify phone number by user 138 | new AlertDialog.Builder(this) 139 | .setTitle(R.string.phoneVerification_confirmTitle) 140 | .setMessage(getString(R.string.phoneVerification_confirmText, phoneNumber)) 141 | .setPositiveButton(R.string.app_yes, (dialog, which) -> requestVerificationCode(phoneNumber)) 142 | .setNegativeButton(R.string.app_no, (dialog, which) -> phoneInput.getEditText().selectAll()) 143 | .show(); 144 | } 145 | 146 | private void requestVerificationCode(String phoneNumber) { 147 | progressBar.setVisibility(View.VISIBLE); 148 | buttonDone.setVisibility(View.GONE); 149 | SharedPreferences prefs = App.get(this).prefs(); 150 | App.get(this).getCountryDefaults().sendVerificationCodeText(this, phoneNumber, exception -> { 151 | if (isFinishing()) { 152 | return; 153 | } 154 | progressBar.setVisibility(View.GONE); 155 | if (exception != null) { 156 | buttonDone.setVisibility(View.VISIBLE); 157 | new AlertDialog.Builder(this) 158 | .setTitle(R.string.app_name) 159 | .setMessage(exception instanceof IOException ? getString(R.string.app_apiFailed, exception.getMessage()) : exception.getMessage()) 160 | .setPositiveButton(android.R.string.ok, null) 161 | .show(); 162 | } else { 163 | // Update the text and show code input instead of phone number input 164 | this.findViewById(R.id.textView_text).setText(R.string.phoneVerification_enterCode); 165 | phoneInput.setVisibility(View.GONE); 166 | checkBoxPrivacyConsent.setVisibility(View.GONE); 167 | editTextCode.setVisibility(View.VISIBLE); 168 | } 169 | }); 170 | } 171 | 172 | private void confirmVerificationCode(String code) { 173 | progressBar.setVisibility(View.VISIBLE); 174 | editTextCode.setEnabled(false); 175 | App.get(this).getCountryDefaults().checkVerificationCode(this, phoneInput.getPhoneNumberE164(), code, quarantineDetails -> { 176 | if (isFinishing()) { 177 | return; 178 | } 179 | if (quarantineDetails == null) { 180 | editTextCode.setText(""); 181 | progressBar.setVisibility(View.GONE); 182 | editTextCode.setEnabled(true); 183 | new AlertDialog.Builder(this) 184 | .setTitle(R.string.app_name) 185 | .setMessage(getString(R.string.phoneVerification_wrongCode)) 186 | .setPositiveButton(android.R.string.ok, null) 187 | .show(); 188 | } else { 189 | long qEnd = App.setEndOfDay(App.parseIsoDate(quarantineDetails.quarantineEnd)); 190 | App.get(this).prefs().edit() 191 | .putString(Prefs.PHONE_NUMBER, phoneInput.getPhoneNumberE164()) 192 | .putString(Prefs.PHONE_NUMBER_VERIFICATION_CODE, code) 193 | .putString(Prefs.COVID_ID, quarantineDetails.covidId) 194 | .putLong(Prefs.QUARANTINE_ENDS, qEnd) 195 | .apply(); 196 | // Send the data to our API 197 | new Api(PhoneVerificationActivity.this).confirmQuarantine(quarantineDetails.covidId, quarantineDetails.quarantineStart, 198 | quarantineDetails.quarantineEnd, (status, response) -> { 199 | // Do we really care if this fails? 200 | setResult(RESULT_OK, new Intent().putExtras(getIntent())); 201 | finish(); 202 | }); 203 | } 204 | }); 205 | } 206 | 207 | private void hideKeyboard() { 208 | InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); 209 | if (imm != null) { 210 | imm.hideSoftInputFromWindow(phoneInput.getEditText().getWindowToken(), 0); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/PrivacyPolicyActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.os.Bundle; 26 | import android.text.Html; 27 | import android.widget.TextView; 28 | 29 | import androidx.annotation.Nullable; 30 | import androidx.appcompat.app.AppCompatActivity; 31 | 32 | import java.io.IOException; 33 | import java.io.InputStream; 34 | 35 | import intl.who.covid19.R; 36 | 37 | public class PrivacyPolicyActivity extends AppCompatActivity { 38 | @Override 39 | protected void onCreate(@Nullable Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_privacy_policy); 42 | try (InputStream inputStream = getResources().openRawResource(R.raw.privacy)) { 43 | byte[] buffer = new byte[inputStream.available()]; 44 | inputStream.read(buffer); 45 | TextView text = findViewById(R.id.textView_text); 46 | text.setText(Html.fromHtml(new String(buffer))); 47 | } catch (IOException e) { 48 | // Well, not much to do here... 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/ProfileFragment.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.content.Intent; 26 | import android.os.Bundle; 27 | import android.text.Html; 28 | import android.view.LayoutInflater; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.widget.TextView; 32 | 33 | import androidx.annotation.NonNull; 34 | import androidx.annotation.Nullable; 35 | import androidx.fragment.app.Fragment; 36 | 37 | import intl.who.covid19.App; 38 | import intl.who.covid19.Prefs; 39 | import intl.who.covid19.R; 40 | 41 | public class ProfileFragment extends Fragment { 42 | @Nullable 43 | @Override 44 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 45 | return inflater.inflate(R.layout.fragment_profile, container, false); 46 | } 47 | 48 | @Override 49 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 50 | super.onViewCreated(view, savedInstanceState); 51 | ((TextView) view.findViewById(R.id.textView_code)).setText(App.get(view.getContext()).prefs().getString(Prefs.COVID_ID, "")); 52 | ((TextView) view.findViewById(R.id.textView_attribution)).setText(Html.fromHtml(getString(R.string.welcome_attribution))); 53 | view.findViewById(R.id.button_aboutApp).setOnClickListener(v -> startActivity(new Intent(getContext(), AboutActivity.class))); 54 | view.findViewById(R.id.textView_privacy).setOnClickListener(v -> startActivity(new Intent(getContext(), PrivacyPolicyActivity.class))); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/ProtectActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.os.Bundle; 26 | 27 | import androidx.annotation.Nullable; 28 | import androidx.appcompat.app.AppCompatActivity; 29 | 30 | import intl.who.covid19.R; 31 | 32 | public class ProtectActivity extends AppCompatActivity { 33 | @Override 34 | protected void onCreate(@Nullable Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_protect); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/QuarantineStartActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.content.Intent; 26 | import android.os.Bundle; 27 | import android.view.View; 28 | import android.widget.DatePicker; 29 | import android.widget.TextView; 30 | 31 | import androidx.annotation.Nullable; 32 | import androidx.appcompat.app.AppCompatActivity; 33 | 34 | import java.text.DateFormat; 35 | import java.util.Calendar; 36 | 37 | import intl.who.covid19.App; 38 | import intl.who.covid19.R; 39 | 40 | public class QuarantineStartActivity extends AppCompatActivity { 41 | public static final String EXTRA_QUARANTINE_START = "intl.who.covid19.ui.EXTRA_QUARANTINE_START"; 42 | 43 | private long startTime = System.currentTimeMillis(); 44 | 45 | @Override 46 | protected void onCreate(@Nullable Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | setContentView(R.layout.activity_quarantine_start); 49 | ((TextView) findViewById(R.id.textView_date)).setText(DateFormat.getDateInstance(DateFormat.MEDIUM).format(startTime)); 50 | DatePicker datePicker = findViewById(R.id.datePicker); 51 | Calendar cal = Calendar.getInstance(); 52 | datePicker.setMinDate(System.currentTimeMillis() - ((int) App.get(this).getRemoteConfig().getDouble(App.RC_QUARANTINE_DURATION) - 1) * 24 * 3_600_000L); 53 | datePicker.setMaxDate(System.currentTimeMillis()); 54 | datePicker.init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), (view, year, monthOfYear, dayOfMonth) -> { 55 | cal.set(Calendar.YEAR, year); 56 | cal.set(Calendar.MONTH, monthOfYear); 57 | cal.set(Calendar.DAY_OF_MONTH, dayOfMonth); 58 | findViewById(R.id.layout_datePicker).setVisibility(View.GONE); 59 | startTime = cal.getTimeInMillis(); 60 | ((TextView) findViewById(R.id.textView_date)).setText(DateFormat.getDateInstance(DateFormat.MEDIUM).format(startTime)); 61 | }); 62 | } 63 | 64 | @Override 65 | public void onBackPressed() { 66 | if (findViewById(R.id.layout_datePicker).getVisibility() == View.VISIBLE) { 67 | findViewById(R.id.layout_datePicker).setVisibility(View.GONE); 68 | } else { 69 | super.onBackPressed(); 70 | } 71 | } 72 | 73 | public void onPickDate(View view) { 74 | findViewById(R.id.layout_datePicker).setVisibility(View.VISIBLE); 75 | } 76 | 77 | public void onButtonContinue(View view) { 78 | setResult(RESULT_OK, new Intent().putExtras(getIntent()).putExtra(EXTRA_QUARANTINE_START, startTime)); 79 | finish(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/StatusActivity.java: -------------------------------------------------------------------------------- 1 | package intl.who.covid19.ui; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothManager; 5 | import android.content.Intent; 6 | import android.content.IntentSender; 7 | import android.content.pm.PackageManager; 8 | import android.location.LocationManager; 9 | import android.net.ConnectivityManager; 10 | import android.net.NetworkInfo; 11 | import android.os.Bundle; 12 | import android.provider.Settings; 13 | import android.text.Html; 14 | import android.widget.Button; 15 | import android.widget.TextView; 16 | 17 | import androidx.annotation.Nullable; 18 | import androidx.appcompat.app.AppCompatActivity; 19 | import androidx.core.content.ContextCompat; 20 | 21 | import com.google.android.gms.common.api.ApiException; 22 | import com.google.android.gms.common.api.ResolvableApiException; 23 | import com.google.android.gms.location.LocationRequest; 24 | import com.google.android.gms.location.LocationServices; 25 | import com.google.android.gms.location.LocationSettingsRequest; 26 | import com.google.android.gms.location.LocationSettingsStatusCodes; 27 | 28 | import intl.who.covid19.App; 29 | import intl.who.covid19.R; 30 | 31 | public class StatusActivity extends AppCompatActivity { 32 | @Override 33 | protected void onCreate(@Nullable Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_status); 36 | ((TextView) findViewById(R.id.textView_bluetooth)).setText(Html.fromHtml(getString(R.string.status_bluetooth))); 37 | ((TextView) findViewById(R.id.textView_location)).setText(Html.fromHtml(getString(R.string.status_location))); 38 | ((TextView) findViewById(R.id.textView_internet)).setText(Html.fromHtml(getString(R.string.status_internet))); 39 | } 40 | 41 | @Override 42 | protected void onResume() { 43 | super.onResume(); 44 | updateUi(); 45 | } 46 | 47 | @Override 48 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 49 | super.onActivityResult(requestCode, resultCode, data); 50 | updateUi(); 51 | } 52 | 53 | private void updateUi() { 54 | int dp4 = (int) (4 * getResources().getDisplayMetrics().density); 55 | // Check BLE support / BT enabled 56 | final boolean bleSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); 57 | BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); 58 | BluetoothAdapter bluetoothAdapter = bluetoothManager == null ? null : bluetoothManager.getAdapter(); 59 | final boolean btEnabled = bluetoothAdapter != null && bluetoothAdapter.isEnabled(); 60 | Button buttonBluetooth = findViewById(R.id.button_bluetooth); 61 | buttonBluetooth.setBackgroundResource(bleSupported && btEnabled ? R.drawable.bg_btn_green : R.drawable.bg_btn_red); 62 | buttonBluetooth.setPadding(dp4 * 2, dp4, dp4 * 2, dp4); 63 | buttonBluetooth.setText(!bleSupported ? R.string.status_unsupported : !btEnabled ? R.string.status_enable : R.string.status_enabled); 64 | buttonBluetooth.setOnClickListener(v -> { 65 | if (bleSupported && !btEnabled) { 66 | startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); 67 | } 68 | }); 69 | // Check GPS support / enabled 70 | final boolean gpsSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS); 71 | LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); 72 | final boolean gpsEnabled = locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); 73 | // Check permissions 74 | boolean granted = true; 75 | for (String permission : App.PERMISSIONS) { 76 | if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { 77 | granted = false; 78 | break; 79 | } 80 | } 81 | boolean permissionsGranted = granted; 82 | Button buttonLocation = findViewById(R.id.button_location); 83 | buttonLocation.setBackgroundResource(gpsSupported && gpsEnabled && permissionsGranted ? R.drawable.bg_btn_green : R.drawable.bg_btn_red); 84 | buttonLocation.setPadding(dp4 * 2, dp4, dp4 * 2, dp4); 85 | buttonLocation.setText(!gpsSupported ? R.string.status_unsupported : !gpsEnabled || !permissionsGranted ? R.string.status_enable : R.string.status_enabled); 86 | buttonLocation.setOnClickListener(v -> { 87 | if (gpsSupported && !gpsEnabled) { 88 | LocationRequest locationRequest = LocationRequest.create(); 89 | locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); 90 | LocationSettingsRequest req = new LocationSettingsRequest.Builder().addLocationRequest(locationRequest).build(); 91 | LocationServices.getSettingsClient(this).checkLocationSettings(req).addOnCompleteListener(this, task -> { 92 | try { 93 | task.getResult(ApiException.class); 94 | } catch (ApiException e) { 95 | switch (e.getStatusCode()) { 96 | case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: 97 | try { 98 | ((ResolvableApiException) e).startResolutionForResult(this, 0); 99 | } catch (IntentSender.SendIntentException ex) { 100 | } 101 | break; 102 | case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: 103 | updateUi(); 104 | break; 105 | } 106 | } 107 | }); 108 | } else if (gpsEnabled && !permissionsGranted) { 109 | requestPermissions(App.PERMISSIONS, 0); 110 | } 111 | }); 112 | // Internet connection check 113 | ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); 114 | NetworkInfo activeNetworkInfo = connectivityManager != null ? connectivityManager.getActiveNetworkInfo() : null; 115 | boolean networkConnected = activeNetworkInfo != null && activeNetworkInfo.isConnected(); 116 | Button buttonInternet = findViewById(R.id.button_internet); 117 | buttonInternet.setBackgroundResource(networkConnected ? R.drawable.bg_btn_green : R.drawable.bg_btn_red); 118 | buttonInternet.setPadding(dp4 * 2, dp4, dp4 * 2, dp4); 119 | buttonInternet.setText(!networkConnected ? R.string.status_activate : R.string.status_active); 120 | buttonInternet.setOnClickListener(v -> { 121 | if (!networkConnected) { 122 | startActivity(new Intent(Settings.ACTION_SETTINGS)); 123 | } 124 | }); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/SymptomsActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.os.Bundle; 26 | import android.text.Html; 27 | import android.widget.TextView; 28 | 29 | import androidx.annotation.Nullable; 30 | import androidx.appcompat.app.AppCompatActivity; 31 | 32 | import intl.who.covid19.R; 33 | 34 | public class SymptomsActivity extends AppCompatActivity { 35 | @Override 36 | protected void onCreate(@Nullable Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.activity_symptoms); 39 | TextView textViewText = findViewById(R.id.textView_text); 40 | textViewText.setText(Html.fromHtml(getString(R.string.symptoms_text))); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/intl/who/covid19/ui/WelcomeActivity.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright (c) 2020 Sygic a.s. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | package intl.who.covid19.ui; 24 | 25 | import android.bluetooth.BluetoothAdapter; 26 | import android.bluetooth.BluetoothManager; 27 | import android.content.Context; 28 | import android.content.Intent; 29 | import android.content.IntentSender; 30 | import android.content.SharedPreferences; 31 | import android.content.pm.PackageManager; 32 | import android.location.LocationManager; 33 | import android.os.Build; 34 | import android.os.Bundle; 35 | import android.text.Html; 36 | import android.view.View; 37 | import android.widget.TextView; 38 | 39 | import androidx.annotation.NonNull; 40 | import androidx.annotation.Nullable; 41 | import androidx.annotation.RequiresApi; 42 | import androidx.appcompat.app.AlertDialog; 43 | import androidx.appcompat.app.AppCompatActivity; 44 | 45 | import com.google.android.gms.common.api.ApiException; 46 | import com.google.android.gms.common.api.ResolvableApiException; 47 | import com.google.android.gms.location.LocationRequest; 48 | import com.google.android.gms.location.LocationServices; 49 | import com.google.android.gms.location.LocationSettingsRequest; 50 | import com.google.android.gms.location.LocationSettingsStatusCodes; 51 | import com.google.gson.Gson; 52 | 53 | import org.hashids.Hashids; 54 | 55 | import java.util.ArrayList; 56 | import java.util.List; 57 | 58 | import intl.who.covid19.Api; 59 | import intl.who.covid19.App; 60 | import intl.who.covid19.Prefs; 61 | import intl.who.covid19.R; 62 | 63 | public class WelcomeActivity extends AppCompatActivity { 64 | 65 | private static final int REQUEST_CODE_PERMISSIONS = 1; 66 | private static final int REQUEST_CODE_ENABLE_BT = 2; 67 | private static final int REQUEST_CODE_ENABLE_GPS = 3; 68 | private static final int REQUEST_CODE_PHONE_NUMBER = 4; 69 | 70 | private boolean skipGpsCheck; 71 | 72 | @Override 73 | protected void onCreate(Bundle savedInstanceState) { 74 | super.onCreate(savedInstanceState); 75 | 76 | // this must be root activity 77 | if (!isTaskRoot()) { 78 | finishAffinity(); 79 | startActivity(new Intent(this, getClass())); 80 | return; 81 | } 82 | 83 | setContentView(R.layout.activity_welcome); 84 | ((TextView) findViewById(R.id.textView_attribution)).setText(Html.fromHtml(getString(R.string.welcome_attribution))); 85 | if (areTermsAgreed()) { 86 | checkDeviceId(); // Do not check permissions here, leave it for the status view on home fragment 87 | } 88 | } 89 | 90 | /// Navigation 91 | 92 | public void onButtonAgree(View v) { 93 | agreeTerms(); 94 | checkPermissionsAndContinue(); 95 | } 96 | 97 | public void onPrivacy(View v) { 98 | startActivity(new Intent(this, PrivacyPolicyActivity.class)); 99 | } 100 | 101 | private void checkPermissionsAndContinue() { 102 | // Check BT enabled 103 | final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 104 | BluetoothAdapter bluetoothAdapter = bluetoothManager == null ? null : bluetoothManager.getAdapter(); 105 | if (bluetoothAdapter != null && !bluetoothAdapter.isEnabled()) { 106 | startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_CODE_ENABLE_BT); 107 | return; 108 | } 109 | 110 | if (!skipGpsCheck && App.get(this).isInQuarantine()) { 111 | boolean gpsSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS); 112 | LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); 113 | final boolean gpsEnabled = locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); 114 | if (gpsSupported && !gpsEnabled) { 115 | requestGpsLocationEnabled(); 116 | return; 117 | } 118 | } 119 | 120 | // Check required permissions 121 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 122 | checkPermissions(); 123 | return; 124 | } 125 | // Go to information screen 126 | checkDeviceId(); 127 | } 128 | 129 | private void checkDeviceId() { 130 | SharedPreferences prefs = App.get(this).prefs(); 131 | if (prefs.getLong(Prefs.DEVICE_ID, 0) != 0) { 132 | navigateNext(false); 133 | return; 134 | } 135 | // Register the device on server 136 | findViewById(R.id.button_agree).setVisibility(View.INVISIBLE); 137 | findViewById(R.id.progressBar).setVisibility(View.VISIBLE); 138 | Api.ProfileRequest request = new Api.ProfileRequest(prefs.getString(Prefs.DEVICE_UID, null), 139 | prefs.getString(Prefs.FCM_TOKEN, null), null); 140 | new Api(this).createProfile(request, (status, response) -> { 141 | if (isFinishing()) { 142 | return; 143 | } 144 | if (status != 200) { 145 | findViewById(R.id.button_agree).setVisibility(View.VISIBLE); 146 | findViewById(R.id.progressBar).setVisibility(View.GONE); 147 | // Show error 148 | new AlertDialog.Builder(this) 149 | .setTitle(R.string.app_name) 150 | .setMessage(getString(R.string.app_apiFailed, (status + " " + response).trim())) 151 | .setPositiveButton(android.R.string.ok, null) 152 | .show(); 153 | } else { 154 | Api.ProfileResponse resp = new Gson().fromJson(response, Api.ProfileResponse.class); 155 | App.get(this).prefs().edit() 156 | .putLong(Prefs.DEVICE_ID, resp.profileId) 157 | .putString(Prefs.COVID_ID, new Hashids("COVID-19 super-secure and unguessable hashids salt", 6, "ABCDEFGHJKLMNPQRSTUVXYZ23456789").encode(resp.profileId)) 158 | .apply(); 159 | verifyPhoneNumber(); 160 | } 161 | }); 162 | } 163 | 164 | private void verifyPhoneNumber() { 165 | if (App.get(this).getCountryDefaults().verifyPhoneNumberAtStart()) { 166 | startActivityForResult(new Intent(this, PhoneVerificationActivity.class) 167 | .putExtra(PhoneVerificationActivity.EXTRA_SHOW_EXPLANATION, true), REQUEST_CODE_PHONE_NUMBER); 168 | } else { 169 | navigateNext(true); 170 | } 171 | } 172 | 173 | private void navigateNext(boolean newProfile) { 174 | startActivity(new Intent(this, HomeActivity.class).putExtra(HomeActivity.EXTRA_ASK_QUARANTINE, newProfile)); 175 | finish(); 176 | } 177 | 178 | @Override 179 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 180 | switch (requestCode) { 181 | case REQUEST_CODE_ENABLE_BT: 182 | case REQUEST_CODE_ENABLE_GPS: 183 | if (resultCode == RESULT_OK) { 184 | checkPermissionsAndContinue(); 185 | } 186 | return; 187 | case REQUEST_CODE_PHONE_NUMBER: 188 | navigateNext(true); 189 | return; 190 | } 191 | super.onActivityResult(requestCode, resultCode, data); 192 | } 193 | 194 | private void requestGpsLocationEnabled() { 195 | LocationRequest locationRequest = LocationRequest.create(); 196 | locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); 197 | LocationSettingsRequest req = new LocationSettingsRequest.Builder().addLocationRequest(locationRequest).build(); 198 | LocationServices.getSettingsClient(this).checkLocationSettings(req).addOnCompleteListener(this, task -> { 199 | try { 200 | task.getResult(ApiException.class); 201 | } catch (ApiException e) { 202 | switch (e.getStatusCode()) { 203 | case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: 204 | try { 205 | ((ResolvableApiException) e).startResolutionForResult(this, REQUEST_CODE_ENABLE_GPS); 206 | } catch (IntentSender.SendIntentException ex) { 207 | } 208 | break; 209 | case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: 210 | skipGpsCheck = true; 211 | checkPermissionsAndContinue(); 212 | break; 213 | } 214 | } 215 | }); 216 | } 217 | 218 | /// Check permissions 219 | 220 | @RequiresApi(api = Build.VERSION_CODES.M) 221 | private void checkPermissions() { 222 | List toAcquire = new ArrayList<>(); 223 | for (String perm : App.PERMISSIONS) { 224 | if (checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) { 225 | toAcquire.add(perm); 226 | } 227 | } 228 | if (toAcquire.isEmpty()) { 229 | // all permissions granted 230 | checkDeviceId(); 231 | return; 232 | } 233 | requestPermissions(toAcquire.toArray(new String[0]), REQUEST_CODE_PERMISSIONS); 234 | } 235 | 236 | @Override 237 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 238 | if (requestCode == REQUEST_CODE_PERMISSIONS) { 239 | for (int i = 0; i < permissions.length; i++) { 240 | int result = grantResults[i]; 241 | if (result != PackageManager.PERMISSION_GRANTED) { 242 | App.log("Permission " + permissions[i] + " not granted, result = " + result); 243 | return; 244 | } 245 | } 246 | // all permissions granted 247 | checkDeviceId(); 248 | return; 249 | } 250 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 251 | } 252 | 253 | /// Preferences 254 | 255 | private boolean areTermsAgreed() { 256 | return App.get(this).prefs().getBoolean(Prefs.TERMS, false); 257 | } 258 | private void agreeTerms() { 259 | App.get(this).prefs().edit().putBoolean(Prefs.TERMS, true).apply(); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /app/src/main/res/color/home_tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/about_footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/about_footer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/chevron_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/chevron_right.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/home_about.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/home_home.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/home_info.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/home_map.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/home_profile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_protect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/home_protect.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_symptoms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/home_symptoms.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_bluetooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/ic_bluetooth.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification_scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/ic_notification_scan.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/ic_notification_warning.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/map_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/map_bubble.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/map_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/map_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/map_drag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/map_drag.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/map_pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/map_pin.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/profile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/protect1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/protect1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/protect2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/protect2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/protect3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/protect3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/protect4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/protect4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/protect5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/protect5.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/protect6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/protect6.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/protect7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/protect7.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/status.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/status_bluetooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/status_bluetooth.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/status_location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/status_location.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/status_wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/status_wifi.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/drawable-xxhdpi/world.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_ltblue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn_white_framed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_gray.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_stats.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_status.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/doctor.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 43 | 50 | 53 | 56 | 63 | 70 | 73 | 76 | 83 | 86 | 89 | 92 | 95 | 98 | 99 | 101 | 104 | 105 | 108 | 111 | 114 | 117 | 120 | 123 | 126 | 129 | 132 | 135 | 138 | 141 | 144 | 147 | 150 | 151 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_green.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_red.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/font/inter_bold.otf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/font/inter_light.otf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/font/poppins_bold.otf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CovidWorld/android/2710f09142c30d8eec3fa5ba6b8a680476d557a8/app/src/main/res/font/poppins_light.otf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 21 | 22 | 28 | 29 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_address.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 27 | 28 | 46 | 47 | 55 | 56 |