diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb6ee01..3ce1f798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [2.12.5] - 2021-11-01 +### Changed +- Fixes, improvements and extensions of config notifications [#744](https://github.com/rokwire/safer-illinois-app/issues/744). + +## [2.12.4] - 2021-10-29 +### Added +- Implemented config notifications [#744](https://github.com/rokwire/safer-illinois-app/issues/744). +### Deleted +- Removed Exposure plugins, service and related UI [#742](https://github.com/rokwire/safer-illinois-app/issues/742). +### Changed +- Update encrypt plugin to latest available version [#745](https://github.com/rokwire/safer-illinois-app/issues/745). + +## [2.12.3] - 2021-10-06 +### Changed +- Handled exempt of vaccination and vaccintation suspended status in vaccination widget [#739](https://github.com/rokwire/safer-illinois-app/issues/739). + +## [2.12.2] - 2021-10-04 +### Added +- Added vaccination exempt support in UIN Overrides [#733](https://github.com/rokwire/safer-illinois-app/issues/733). +- Check for expired vaccines in vaccination widget and isVaccinated getter [#729](https://github.com/rokwire/safer-illinois-app/issues/729). +### Changed +- Simplify VaccineBoosterInterval logic until we really need different intervals for different manufacturers [#737](https://github.com/rokwire/safer-illinois-app/issues/737). +### Deleted +- Remove "max-weekdays-extent" from test monitor interval until it is required [#737](https://github.com/rokwire/safer-illinois-app/issues/737). + ## [2.11.5] - 2021-10-01 ### Fixed - Fixed null pointer crash [#725](https://github.com/rokwire/safer-illinois-app/issues/725). +## [2.12.1] - 2021-10-01 +### Added +- Added SECURITY.md. +- Added booster intervals for vaccines [#729](https://github.com/rokwire/safer-illinois-app/issues/729). + +## [2.12.0] - 2021-09-30 + ## [2.11.4] - 2021-10-01 ### Changed - Show when the vaccine will become effective in vaccination widget [#720](https://github.com/rokwire/safer-illinois-app/issues/720). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..01b02b78 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Patches for [ **safer-illinois-app** ] will only be applied to the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 2.12.0 | :white_check_mark: | +| 2.11.3 | :white_check_mark: | +| 2.11.2 | :x: | +| 2.11.1 | :x: | +| 2.11.0 | :x: | +| 2.10.38 | :white_check_mark: | +| < 2.10.38 | :x: | + +## Reporting a Bug or Vulnerability + +Vulnerabilities can be responsibly disclosed to [securitysupport@illinois.edu](mailto:securitysupport@illinois.edu). + +Bugs can be reported in a GIT repository via GIT issues. \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 6e76aac2..641c68da 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -163,9 +163,6 @@ dependencies { implementation 'com.google.zxing:core:3.3.0' //Use zxing 3.3.0 because we have minSdk < 24 implementation ('com.journeyapps:zxing-android-embedded:4.1.0@aar') { transitive = false } - // BLESSED - BLE library used for Exposures - implementation 'com.github.weliem:blessed-android:1.19' - implementation 'com.google.android.gms:play-services-vision-common:19.0.2' // Temporary fix Gradle 4.2.0 & https://stackoverflow.com/questions/67612499/could-not-find-com-google-firebasefirebase-ml-vision diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6fb21f5d..16a6aed6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -28,21 +28,12 @@ - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/java/at/favre/lib/crypto/HKDF.java b/android/app/src/main/java/at/favre/lib/crypto/HKDF.java deleted file mode 100644 index 5ddf1f50..00000000 --- a/android/app/src/main/java/at/favre/lib/crypto/HKDF.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright 2017 Patrick Favre-Bulle - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package at.favre.lib.crypto; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import java.nio.ByteBuffer; - -/** - * A standards-compliant implementation of RFC 5869 - * for HMAC-based Key Derivation Function. - *

- * HKDF follows the "extract-then-expand" paradigm, where the KDF - * logically consists of two modules. The first stage takes the input - * keying material and "extracts" from it a fixed-length pseudorandom - * key K. The second stage "expands" the key K into several additional - * pseudorandom keys (the output of the KDF). - *

- * HKDF was first described by Hugo Krawczyk. - *

- * This implementation is thread safe without the need for synchronization. - *

- * Simple Example: - *

- *     byte[] pseudoRandomKey = HKDF.fromHmacSha256().extract(null, lowEntropyInput);
- *     byte[] outputKeyingMaterial = HKDF.fromHmacSha256().expand(pseudoRandomKey, null, 64);
- * 
- * - * @see RFC 5869 - * @see Cryptographic Extraction and Key Derivation: - * The HKDF Scheme - * @see Wikipedia: HKDF - */ -@SuppressWarnings("WeakerAccess") -public final class HKDF { - /** - * Cache instances - */ - private static HKDF hkdfHmacSha256; - private static HKDF hkdfHmacSha512; - - private final HkdfMacFactory macFactory; - - private HKDF(HkdfMacFactory macFactory) { - this.macFactory = macFactory; - } - - /** - * Return a shared instance using HMAC with Sha256. - * Even though shared, this instance is thread-safe. - * - * @return HKDF instance - */ - public static HKDF fromHmacSha256() { - if (hkdfHmacSha256 == null) { - hkdfHmacSha256 = from(HkdfMacFactory.Default.hmacSha256()); - } - return hkdfHmacSha256; - } - - /** - * Return a shared instance using HMAC with Sha512. - * Even though shared, this instance is thread-safe. - * - * @return HKDF instance - */ - public static HKDF fromHmacSha512() { - if (hkdfHmacSha512 == null) { - hkdfHmacSha512 = from(HkdfMacFactory.Default.hmacSha512()); - } - return hkdfHmacSha512; - } - - /** - * Create a new HKDF instance for given macFactory. - * - * @param macFactory used for HKDF - * @return a new instance of HKDF - */ - public static HKDF from(HkdfMacFactory macFactory) { - return new HKDF(macFactory); - } - - /** - * Step 1 of RFC 5869 (Section 2.2) - *

- * The first stage takes the input keying material and "extracts" from it a fixed-length pseudorandom - * key K. The goal of the "extract" stage is to "concentrate" and provide a more uniformly unbiased and higher entropy but smaller output. - * This is done by utilising the diffusion properties of cryptographic MACs. - *

- * About Salts (from RFC 5869): - *

- * HKDF is defined to operate with and without random salt. This is - * done to accommodate applications where a salt value is not available. - * We stress, however, that the use of salt adds significantly to the - * strength of HKDF, ensuring independence between different uses of the - * hash function, supporting "source-independent" extraction, and - * strengthening the analytical results that back the HKDF design. - *
- * - * @param salt optional salt value (a non-secret random value) (can be null) - * if not provided, it is set to an array of hash length of zeros. - * @param inputKeyingMaterial data to be extracted (IKM) - * - * @return a new byte array pseudo random key (of hash length in bytes) (PRK) which can be used to expand - * @see RFC 5869 Section 2.2 - */ - public byte[] extract(byte[] salt, byte[] inputKeyingMaterial) { - return extract(macFactory.createSecretKey(salt), inputKeyingMaterial); - } - - /** - * Use this if you require {@link SecretKey} types by your security framework. See also - * {@link HkdfMacFactory#createSecretKey(byte[])}. - *

- * See {@link #extract(byte[], byte[])} for description. - * - * @param salt optional salt value (a non-secret random value) (can be null) - * @param inputKeyingMaterial data to be extracted (IKM) - * @return a new byte array pseudo random key (of hash length in bytes) (PRK) which can be used to expand - */ - public byte[] extract(SecretKey salt, byte[] inputKeyingMaterial) { - return new Extractor(macFactory).execute(salt, inputKeyingMaterial); - } - - /** - * Step 2 of RFC 5869 (Section 2.3) - *

- * To "expand" the generated output of an already reasonably random input such as an existing shared key into a larger - * cryptographically independent output, thereby producing multiple keys deterministically from that initial shared key, - * so that the same process may produce those same secret keys safely on multiple devices, as long as the same inputs - * are used. - *

- * About Info (from RFC 5869): - *

- * While the 'info' value is optional in the definition of HKDF, it is - * often of great importance in applications. Its main objective is to - * bind the derived key material to application- and context-specific - * information. For example, 'info' may contain a protocol number, - * algorithm identifiers, user identities, etc. In particular, it may - * prevent the derivation of the same keying material for different - * contexts (when the same input key material (IKM) is used in such - * different contexts). - *
- * - * @param pseudoRandomKey a pseudo random key of at least hmac hash length in bytes (usually, the output from the extract step) - * @param info optional context and application specific information; may be null - * @param outLengthBytes length of output keying material in bytes - * @return new byte array of output keying material (OKM) - * @see RFC 5869 Section 2.3 - */ - public byte[] expand(byte[] pseudoRandomKey, byte[] info, int outLengthBytes) { - return expand(macFactory.createSecretKey(pseudoRandomKey), info, outLengthBytes); - } - - /** - * Use this if you require {@link SecretKey} types by your security framework. See also - * {@link HkdfMacFactory#createSecretKey(byte[])}. - *

- * See {@link #expand(byte[], byte[], int)} for description. - * - * @param pseudoRandomKey a pseudo random key of at least hmac hash length in bytes (usually, the output from the extract step) - * @param info optional context and application specific information; may be null - * @param outLengthBytes length of output keying material in bytes - * @return new byte array of output keying material (OKM) - */ - public byte[] expand(SecretKey pseudoRandomKey, byte[] info, int outLengthBytes) { - return new Expander(macFactory).execute(pseudoRandomKey, info, outLengthBytes); - } - - /** - * Convenience method for extract & expand in a single method - * - * @param saltExtract optional salt value (a non-secret random value); - * @param inputKeyingMaterial data to be extracted (IKM) - * @param infoExpand optional context and application specific information; may be null - * @param outLengthByte length of output keying material in bytes - * @return new byte array of output keying material (OKM) - */ - public byte[] extractAndExpand(byte[] saltExtract, byte[] inputKeyingMaterial, byte[] infoExpand, int outLengthByte) { - return extractAndExpand(macFactory.createSecretKey(saltExtract), inputKeyingMaterial, infoExpand, outLengthByte); - } - - /** - * Convenience method for extract & expand in a single method - * - * @param saltExtract optional salt value (a non-secret random value); - * @param inputKeyingMaterial data to be extracted (IKM) - * @param infoExpand optional context and application specific information; may be null - * @param outLengthByte length of output keying material in bytes - * @return new byte array of output keying material (OKM) - */ - public byte[] extractAndExpand(SecretKey saltExtract, byte[] inputKeyingMaterial, byte[] infoExpand, int outLengthByte) { - return new Expander(macFactory).execute(macFactory.createSecretKey( - new Extractor(macFactory).execute(saltExtract, inputKeyingMaterial)), - infoExpand, outLengthByte); - } - - /** - * Get the used mac factory - * - * @return factory - */ - HkdfMacFactory getMacFactory() { - return macFactory; - } - - /* ************************************************************************** IMPL */ - - static final class Extractor { - private final HkdfMacFactory macFactory; - - Extractor(HkdfMacFactory macFactory) { - this.macFactory = macFactory; - } - - /** - * Step 1 of RFC 5869 - * - * @param salt optional salt value (a non-secret random value); - * if not provided, it is set to an array of hash length of zeros. - * @param inputKeyingMaterial data to be extracted (IKM) - * - * @return a new byte array pseudorandom key (of hash length in bytes) (PRK) which can be used to expand - */ - byte[] execute(SecretKey salt, byte[] inputKeyingMaterial) { - if (salt == null) { - salt = macFactory.createSecretKey(new byte[macFactory.getMacLengthBytes()]); - } - - if (inputKeyingMaterial == null || inputKeyingMaterial.length <= 0) { - throw new IllegalArgumentException("provided inputKeyingMaterial must be at least of size 1 and not null"); - } - - Mac mac = macFactory.createInstance(salt); - return mac.doFinal(inputKeyingMaterial); - } - } - - static final class Expander { - private final HkdfMacFactory macFactory; - - Expander(HkdfMacFactory macFactory) { - this.macFactory = macFactory; - } - - /** - * Step 2 of RFC 5869. - * - * @param pseudoRandomKey a pseudorandom key of at least hmac hash length in bytes (usually, the output from the extract step) - * @param info optional context and application specific information; may be null - * @param outLengthBytes length of output keying material in bytes (must be <= 255 * mac hash length) - * @return new byte array of output keying material (OKM) - */ - byte[] execute(SecretKey pseudoRandomKey, byte[] info, int outLengthBytes) { - - if (outLengthBytes <= 0) { - throw new IllegalArgumentException("out length bytes must be at least 1"); - } - - if (pseudoRandomKey == null) { - throw new IllegalArgumentException("provided pseudoRandomKey must not be null"); - } - - Mac hmacHasher = macFactory.createInstance(pseudoRandomKey); - - if (info == null) { - info = new byte[0]; - } - - /* - The output OKM is calculated as follows: - N = ceil(L/HashLen) - T = T(1) | T(2) | T(3) | ... | T(N) - OKM = first L bytes of T - where: - T(0) = empty string (zero length) - T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) - T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) - T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) - ... - */ - - byte[] blockN = new byte[0]; - - int iterations = (int) Math.ceil(((double) outLengthBytes) / ((double) hmacHasher.getMacLength())); - - if (iterations > 255) { - throw new IllegalArgumentException("out length must be maximal 255 * hash-length; requested: " + outLengthBytes + " bytes"); - } - - ByteBuffer buffer = ByteBuffer.allocate(outLengthBytes); - int remainingBytes = outLengthBytes; - int stepSize; - - for (int i = 0; i < iterations; i++) { - hmacHasher.update(blockN); - hmacHasher.update(info); - hmacHasher.update((byte) (i + 1)); - - blockN = hmacHasher.doFinal(); - - stepSize = Math.min(remainingBytes, blockN.length); - - buffer.put(blockN, 0, stepSize); - remainingBytes -= stepSize; - } - - return buffer.array(); - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/at/favre/lib/crypto/HkdfMacFactory.java b/android/app/src/main/java/at/favre/lib/crypto/HkdfMacFactory.java deleted file mode 100644 index 29833713..00000000 --- a/android/app/src/main/java/at/favre/lib/crypto/HkdfMacFactory.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2017 Patrick Favre-Bulle - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package at.favre.lib.crypto; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; - -/** - * Factory class for creating {@link Mac} hashers - */ -public interface HkdfMacFactory { - - /** - * Creates a new instance of Hmac with given key, i.e. it must already be initialized - * with {@link Mac#init(Key)}. - * - * @param key the key used, must not be null - * @return a new mac instance - */ - Mac createInstance(SecretKey key); - - /** - * Get the length of the mac output in bytes - * - * @return the length of mac output in bytes - */ - int getMacLengthBytes(); - - /** - * Creates a secret key from a byte raw key material to be used with {@link #createInstance(SecretKey)} - * - * @param rawKeyMaterial the raw key - * @return wrapped as secret key instance or null if input is null or empty - */ - SecretKey createSecretKey(byte[] rawKeyMaterial); - - /** - * Default implementation - */ - @SuppressWarnings("WeakerAccess") - final class Default implements HkdfMacFactory { - private final String macAlgorithmName; - private final Provider provider; - - /** - * Creates a factory creating HMAC with SHA-256 - * - * @return factory - */ - public static HkdfMacFactory hmacSha256() { - return new Default("HmacSHA256", null); - } - - /** - * Creates a factory creating HMAC with SHA-512 - * - * @return factory - */ - public static HkdfMacFactory hmacSha512() { - return new Default("HmacSHA512", null); - } - - /** - * Creates a factory creating HMAC with SHA-1 - * - * @return factory - * @deprecated sha1 with HMAC should be fine, but not recommended for new protocols; see https://crypto.stackexchange.com/questions/26510/why-is-hmac-sha1-still-considered-secure - */ - @Deprecated - public static HkdfMacFactory hmacSha1() { - return new Default("HmacSHA1", null); - } - - /** - * Creates a mac factory - * - * @param macAlgorithmName as used by {@link Mac#getInstance(String)} - */ - public Default(String macAlgorithmName) { - this(macAlgorithmName, null); - } - - /** - * Creates a mac factory - * - * @param macAlgorithmName as used by {@link Mac#getInstance(String)} - * @param provider the security provider, see {@link Mac#getInstance(String, Provider)}; may be null to use default - */ - public Default(String macAlgorithmName, Provider provider) { - this.macAlgorithmName = macAlgorithmName; - this.provider = provider; - } - - @Override - public Mac createInstance(SecretKey key) { - try { - Mac mac = createMacInstance(); - mac.init(key); - return mac; - } catch (Exception e) { - throw new IllegalStateException("could not make hmac hasher in hkdf", e); - } - } - - private Mac createMacInstance() { - try { - Mac hmacInstance; - - if (provider == null) { - hmacInstance = Mac.getInstance(macAlgorithmName); - } else { - hmacInstance = Mac.getInstance(macAlgorithmName, provider); - } - - return hmacInstance; - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("defined mac algorithm was not found", e); - } catch (Exception e) { - throw new IllegalStateException("could not create mac instance in hkdf", e); - } - } - - @Override - public int getMacLengthBytes() { - return createMacInstance().getMacLength(); - } - - @Override - public SecretKey createSecretKey(byte[] rawKeyMaterial) { - if (rawKeyMaterial == null || rawKeyMaterial.length <= 0) { - return null; - } - return new SecretKeySpec(rawKeyMaterial, macAlgorithmName); - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/edu/illinois/covid/AppBackupAgent.java b/android/app/src/main/java/edu/illinois/covid/AppBackupAgent.java index d5740355..a013f3d2 100644 --- a/android/app/src/main/java/edu/illinois/covid/AppBackupAgent.java +++ b/android/app/src/main/java/edu/illinois/covid/AppBackupAgent.java @@ -32,9 +32,6 @@ public void onCreate() { SharedPreferencesBackupHelper encryptionHelper = new SharedPreferencesBackupHelper(this, Constants.ENCRYPTION_SHARED_PREFS_FILE_NAME); addHelper(PREFS_BACKUP_KEY, encryptionHelper); - - SharedPreferencesBackupHelper exposureTeksHelper = new SharedPreferencesBackupHelper(this, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME); - addHelper(PREFS_BACKUP_KEY, exposureTeksHelper); } public static void requestBackup(Context context) { diff --git a/android/app/src/main/java/edu/illinois/covid/Constants.java b/android/app/src/main/java/edu/illinois/covid/Constants.java index 6865be2a..832e782c 100644 --- a/android/app/src/main/java/edu/illinois/covid/Constants.java +++ b/android/app/src/main/java/edu/illinois/covid/Constants.java @@ -16,12 +16,8 @@ package edu.illinois.covid; -import android.os.ParcelUuid; - import com.google.android.gms.maps.model.LatLng; -import java.util.UUID; - public class Constants { //Flutter communication methods @@ -49,9 +45,6 @@ public class Constants { static final float SECOND_THRESHOLD_MARKER_ZOOM = 16.89f; static final int MARKER_TITLE_MAX_SYMBOLS_NUMBER = 15; public static final double EXPLORE_LOCATION_THRESHOLD_DISTANCE = 200.0; //meters - public static final float INDOORS_BUILDING_ZOOM = 17.0f; - public static final String ANALYTICS_ROUTE_LOCATION_FORMAT = "{\"latitude\":%f,\"longitude\":%f,\"floor\":%d}"; - public static final String ANALYTICS_USER_LOCATION_FORMAT = "{\"latitude\":%f,\"longitude\":%f,\"floor\":%d,\"timestamp\":%d}"; //Health static final String HEALTH_SHARED_PREFS_FILE_NAME = "health_shared_prefs"; @@ -59,42 +52,6 @@ public class Constants { //Encryption Key static final String ENCRYPTION_SHARED_PREFS_FILE_NAME = "encryption_shared_prefs"; - //Exposure - public static final String EXPOSURE_PLUGIN_METHOD_NAME_START = "start"; - public static final String EXPOSURE_PLUGIN_METHOD_NAME_STOP = "stop"; - public static final String EXPOSURE_PLUGIN_METHOD_NAME_TEKS = "TEKs"; - public static final String EXPOSURE_PLUGIN_METHOD_NAME_TEK_RPIS = "tekRPIs"; - public static final String EXPOSURE_PLUGIN_METHOD_NAME_RPI_LOG = "exposureRPILog"; - public static final String EXPOSURE_PLUGIN_METHOD_NAME_THICK = "exposureThick"; - public static final String EXPOSURE_PLUGIN_METHOD_NAME_RSSI_LOG = "exposureRSSILog"; - public static final String EXPOSURE_PLUGIN_METHOD_NAME_EXPIRE_TEK = "expireTEK"; - public static final String EXPOSURE_PLUGIN_METHOD_EXP_UP_TIME = "exposureUpTime"; - public static final String EXPOSURE_PLUGIN_SETTINGS_PARAM_NAME = "settings"; - public static final String EXPOSURE_PLUGIN_RPI_PARAM_NAME = "rpi"; - public static final String EXPOSURE_PLUGIN_TEK_METHOD_NAME = "tek"; - public static final String EXPOSURE_PLUGIN_TEK_PARAM_NAME = "tek"; - public static final String EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME = "timestamp"; - public static final String EXPOSURE_PLUGIN_UP_TIME_WIN_PARAM_NAME = "upTimeWindow"; - public static final String EXPOSURE_PLUGIN_EXPOSURE_METHOD_NAME = "exposure"; - public static final String EXPOSURE_PLUGIN_DURATION_PARAM_NAME = "duration"; - public static final String EXPOSURE_PLUGIN_RSSI_PARAM_NAME = "rssi"; - public static final String EXPOSURE_PLUGIN_ADDRESS_PARAM_NAME = "address"; - public static final String EXPOSURE_PLUGIN_IOS_RECORD_PARAM_NAME = "isiOSRecord"; - public static final String EXPOSURE_PLUGIN_PERIPHERAL_UUID_PARAM_NAME = "peripheralUuid"; - public static final String EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME = "expirestamp"; - public static final String EXPOSURE_BLE_DEVICE_FOUND = "edu.illinois.rokwire.exposure.ble.FOUND_DEVICE"; - public static final String EXPOSURE_BLE_ACTION_FOUND = "edu.illinois.rokwire.exposure.ble.scan.ACTION_FOUND"; - public static final int EXPOSURE_NO_RSSI_VALUE = 127; - public static final int EXPOSURE_MIN_RSSI_VALUE = -50; - public static final int EXPOSURE_MIN_DURATION_MILLIS = 0; // 0 minute - public static final UUID EXPOSURE_UUID_SERVICE = UUID.fromString("0000CD19-0000-1000-8000-00805F9B34FB"); - public static final ParcelUuid EXPOSURE_PARCEL_SERVICE_UUID = new ParcelUuid(EXPOSURE_UUID_SERVICE); - public static final UUID EXPOSURE_UUID_CHARACTERISTIC = UUID.fromString("1f5bb1de-cdf0-4424-9d43-d8cc81a7f207"); - public static final int EXPOSURE_CONTRACT_NUMBER_LENGTH = 20; - public static final String EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME = "exposure_teks_shared_prefs"; - public static final String EXPOSURE_TEKS_SHARED_PREFS_KEY = "exposure_teks"; - public static final String EXPOSURE_TEK_VERSION = "tekDatabaseVersion"; - //Gallery public static final String GALLERY_PLUGIN_METHOD_NAME_STORE = "store"; public static final String GALLERY_PLUGIN_PARAM_BYTES = "bytes"; diff --git a/android/app/src/main/java/edu/illinois/covid/MainActivity.java b/android/app/src/main/java/edu/illinois/covid/MainActivity.java index 7337604c..e1c0601d 100644 --- a/android/app/src/main/java/edu/illinois/covid/MainActivity.java +++ b/android/app/src/main/java/edu/illinois/covid/MainActivity.java @@ -52,7 +52,6 @@ import java.util.Set; import java.util.UUID; -import edu.illinois.covid.exposure.ExposurePlugin; import edu.illinois.covid.gallery.GalleryPlugin; import edu.illinois.covid.maps.MapActivity; @@ -74,8 +73,6 @@ public class MainActivity extends FlutterActivity implements MethodChannel.Metho private static final String NATIVE_CHANNEL = "edu.illinois.covid/core"; private static MainActivity instance = null; - private ExposurePlugin exposurePlugin; - private HashMap keys; private int preferredScreenOrientation; @@ -94,15 +91,6 @@ protected void onCreate(Bundle savedInstanceState) { initScreenOrientation(); } - @Override - public void onDestroy() { - super.onDestroy(); - // when activity is killed by user through the app manager, stop all exposure-related services - if (exposurePlugin != null) { - exposurePlugin.handleStop(); - } - } - public static MainActivity getInstance() { return instance; } @@ -152,8 +140,6 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { .getRegistry() .registerViewFactory("mapview", new MapViewFactory(this, flutterEngine.getDartExecutor().getBinaryMessenger())); - exposurePlugin = new ExposurePlugin(this); - flutterEngine.getPlugins().add(exposurePlugin); galleryPlugin = new GalleryPlugin(this); flutterEngine.getPlugins().add(galleryPlugin); } @@ -224,10 +210,6 @@ private void requestLocationPermission(MethodChannel.Result result) { public void onResult(boolean granted) { if (granted) { result.success("allowed"); - - if (exposurePlugin != null) { - exposurePlugin.onLocationPermissionGranted(); - } } else { result.success("denied"); } diff --git a/android/app/src/main/java/edu/illinois/covid/Utils.java b/android/app/src/main/java/edu/illinois/covid/Utils.java index 125fe03e..28483196 100644 --- a/android/app/src/main/java/edu/illinois/covid/Utils.java +++ b/android/app/src/main/java/edu/illinois/covid/Utils.java @@ -17,7 +17,6 @@ package edu.illinois.covid; import android.app.AlertDialog; -import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; @@ -77,13 +76,6 @@ public static void showDialog(Context context, String title, String message, alertDialog.show(); } - public static void enabledBluetooth() { - BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (bluetoothAdapter != null) { - bluetoothAdapter.enable(); - } - } - public static class DateTime { static Date getDateTime(String dateTimeString) { diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java b/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java deleted file mode 100644 index 79005a1f..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java +++ /dev/null @@ -1,1487 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattDescriptor; -import android.bluetooth.BluetoothGattService; -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanRecord; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.ParcelUuid; -import android.util.Log; - -import com.welie.blessed.BluetoothCentral; -import com.welie.blessed.BluetoothPeripheral; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.UUID; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.IOException; -import java.util.concurrent.ConcurrentHashMap; - -import androidx.annotation.NonNull; -import at.favre.lib.crypto.HKDF; -import edu.illinois.covid.Constants; -import edu.illinois.covid.MainActivity; -import edu.illinois.covid.R; -import edu.illinois.covid.Utils; -import edu.illinois.covid.exposure.ble.ExposureClient; -import edu.illinois.covid.exposure.ble.ExposureServer; -import edu.illinois.covid.exposure.crypto.AES; -import edu.illinois.covid.exposure.crypto.AES_CTR; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class ExposurePlugin implements MethodChannel.MethodCallHandler, FlutterPlugin { - - private static final String TAG = "ExposurePlugin"; - - private MainActivity activityContext; - private MethodChannel methodChannel; - - private MethodChannel.Result startedResult; - private Object settings; - - private ExposureServer exposureServer; - private boolean serverStarted; - private ExposureClient androidExposureClient; - private boolean clientStarted; - - // iOS specific scanner/client - private BluetoothCentral iosExposureCentral; - private Handler handler = new Handler(); - private Map peripherals; - private Map peripheralsRPIs; - private Map iosExposures; - - // iOS background variable scanner - private static final int iosbgManufacturerID = 76; - private static final byte[] manufacturerDataMask = Utils.Str.hexStringToByteArray("ff00000000000000000000000000002000"); - private static final byte[] manufacturerData = Utils.Str.hexStringToByteArray("0100000000000000000000000000002000"); - private BluetoothAdapter iosBgBluetoothAdapter; - private BluetoothLeScanner iosBgBluoetoothLeScanner; - private List iosBgScanFilters; - private ScanSettings iosBgScanSettings; - private Map peripherals_bg; - private Handler ios_bg_handler = new Handler(); - private Handler mainHandler = new Handler(Looper.getMainLooper()); - private Handler callbackHandler = new Handler(); - private static final String CCC_DESCRIPTOR_UUID = "00002902-0000-1000-8000-00805f9b34fb"; // characteristic notification - private Runnable iosBgScanTimeoutRunnable; // for restarting scan in android 9 - - private Timer expUpTimeTimer; - private Map expUpTimeMap; - private long expStartTime = 0; - - // RPI - private byte[] rpi; - private Timer rpiTimer; - private static final int RPI_REFRESH_INTERVAL_SECS = 10 * 60; // 10 minutes - private static final int TEKRollingPeriod = 144; - - private Timer exposuresTimer; - private long lastNotifyExposureTickTimestamp; - private Map androidExposures; - - private Map> i_TEK_map; - - // Exposure Constants - private static final int EXPOSURE_TIMEOUT_INTERVAL_MILLIS = 2 * 60 * 1000; // 2 minutes - private static final int EXPOSURE_PING_INTERVAL_MILLIS = 60 * 1000; // 1 minute - private static final int EXPOSURE_PROCESS_INTERVAL_MILLIS = 10 * 1000; // 10 secs - private static final int EXPOSURE_NOTIFY_TICK_INTERVAL_MILLIS = 1000; // 1 secs - - // Exposure Settings - private int exposureTimeoutIntervalInMillis; - private int exposurePingIntervalInMillis; - private int exposureProcessIntervalInMillis; - private int exposureMinDurationInMillis; - private int exposureMinRssi = Constants.EXPOSURE_MIN_RSSI_VALUE; - private int exposureExpireDays; - - // Helper constants - private static final String TEK_MAP_KEY = "tek"; - private static final String RPI_MAP_KEY = "rpi"; - private static final String EN_INTERVAL_NUMBER_MAP_KEY = "ENIntervalNumber"; - private static final String I_MAP_KEY = "i"; - private static final String DATABASE_VERSION_KEY = "databaseversion"; - - public ExposurePlugin(MainActivity activity) { - this.activityContext = activity; - - this.peripherals = new HashMap<>(); - this.peripheralsRPIs = new HashMap<>(); - this.iosExposures = new ConcurrentHashMap<>(); - this.androidExposures = new ConcurrentHashMap<>(); - - this.i_TEK_map = loadTeksFromStorage(); - this.peripherals_bg = new HashMap<>(); - - loadExpUpTimeFromStorage(); - } - - //region Public APIs Implementation - - private void handleStart(@NonNull MethodChannel.Result result, Object settings) { - Log.d(TAG, "handleStart: start plugin"); - startedResult = result; - initSettings(settings); - bindExposureServer(); - bindExposureClient(); - } - - public void handleStop() { - Log.d(TAG, "handleStop: stop plugin"); - startedResult = null; - stop(); - } - - private void start() { - Log.d(TAG, "start"); - refreshRpi(); - startAdvertise(); - startRpiTimer(); - startExpUpTimeTimer(); - startScan(); - } - - private void stop() { - Log.d(TAG, "stop"); - stopAdvertise(); - stopScan(); - clearRpi(); - clearExposures(); - stopRpiTimer(); - stopExpUpTimeTimer(); - expUpTimeHit(); - unBindExposureServer(); - unBindExposureClient(); - } - - private void refreshRpi() { - Log.d(TAG, "refreshRpi"); - Map retVal = generateRpi(); - rpi = (byte[]) retVal.get(RPI_MAP_KEY); - byte[] tek = (byte[]) retVal.get(TEK_MAP_KEY); - int i = (int) retVal.get(I_MAP_KEY); - int enIntervalNumber = (int) retVal.get(EN_INTERVAL_NUMBER_MAP_KEY); - Log.d(TAG, "Logged - tek: " + Utils.Base64.encode(tek) + ", i: " + i + ", enIntervalNumber: " + enIntervalNumber); - uploadRPIUpdate(rpi, tek, Utils.DateTime.getCurrentTimeMillisSince1970(), i, enIntervalNumber); - String rpiEncoded = Utils.Base64.encode(rpi); - Log.d(TAG, "final rpi = " + rpiEncoded + "size = " + (rpi != null ? rpi.length : "null")); - if (exposureServer != null) { - exposureServer.setRpi(rpi); - } - } - - private Map generateRpi() { - long currentTimestampInMillis = Utils.DateTime.getCurrentTimeMillisSince1970(); - long currentTimeStampInSecs = currentTimestampInMillis / 1000; - int timestamp = (int) currentTimeStampInSecs; - int ENIntervalNumber = timestamp / RPI_REFRESH_INTERVAL_SECS; - Log.d(TAG, "ENIntervalNumber = " + ENIntervalNumber); - int i = (ENIntervalNumber / TEKRollingPeriod) * TEKRollingPeriod; - int expireIntervalNumber = i + TEKRollingPeriod; - Log.d(TAG, "i = " + i); - - /* if new day, generate a new tek */ - /* if in the rest of the day, using last valid TEK */ - if ((i_TEK_map != null) && !i_TEK_map.isEmpty()) { - Integer lastTimestamp = Collections.max(i_TEK_map.keySet()); - Map lastTek = i_TEK_map.get(lastTimestamp); - Integer lastExpireTime = (lastTek != null) ? lastTek.get(lastTek.keySet().iterator().next()) : null; - if ((lastExpireTime != null) && (lastExpireTime == expireIntervalNumber)) { - i = lastTimestamp; - } else { - i = ENIntervalNumber; - } - } - - Map tek = new HashMap<>(); - if (i_TEK_map.isEmpty() || !i_TEK_map.containsKey(i)) { - byte[] bytes = new byte[16]; - SecureRandom rand = new SecureRandom(); // generating TEK with a cryptographic random number generator - rand.nextBytes(bytes); - tek.put(bytes, expireIntervalNumber); - i_TEK_map.put(i, tek); // putting the TEK map as a value for the current i - - // handling more than 14 (exposureExpireDays) i values in the map - if (i_TEK_map.size() >= exposureExpireDays) { - Iterator it = i_TEK_map.keySet().iterator(); - while (it.hasNext()) { - int key = it.next(); - if (key <= i - exposureExpireDays) - it.remove(); - } - } - - // Save TEKs to storage - saveTeksToStorage(i_TEK_map); - - // Notify TEK - long notifyTimestampInMillis = (long) i * RPI_REFRESH_INTERVAL_SECS * 1000; // in millis - long expireTime = (long) expireIntervalNumber * RPI_REFRESH_INTERVAL_SECS * 1000; - byte[] tekData = tek.keySet().iterator().next(); - notifyTek(tekData, notifyTimestampInMillis, expireTime); - } else { - tek = i_TEK_map.get(i); - } - byte[] rpiTek = (tek != null) ? tek.keySet().iterator().next() : null; - byte[] rpi = generateRpiForIntervalNumber(ENIntervalNumber, rpiTek); - Map retVal = new HashMap<>(); - retVal.put(RPI_MAP_KEY, rpi); - retVal.put(TEK_MAP_KEY, rpiTek); - retVal.put(EN_INTERVAL_NUMBER_MAP_KEY, ENIntervalNumber); - retVal.put(I_MAP_KEY, i); - return retVal; - } - - private byte[] generateRpiForIntervalNumber(int enIntervalNumber, byte[] tek) { - // generating RPIK with salt as null and passing in the correct parameters to the extractandexpand function of HKDF class - // in the file HKDF.java - byte[] salt = null; - byte[] info = "EN-RPIK".getBytes(); - byte[] RPIK = HKDF.fromHmacSha256().extractAndExpand(salt, tek, info, 16); - - // generating AEMK using the same method - info = "EN-AEMK".getBytes(); - byte[] AEMK = HKDF.fromHmacSha256().extractAndExpand(salt, tek, info, 16); - - // creating the padded data for AES encryption of RPIK to get RPI - byte[] padded_data = new byte[16]; - byte[] EN_RPI = "EN-RPI".getBytes(); - ByteBuffer bb = ByteBuffer.allocate(4); - bb.putInt(enIntervalNumber); - byte[] ENIN = bb.array(); - int j = 0; - for (byte b : EN_RPI) { - padded_data[j] = b; - j++; - } - for (j = 6; j <= 11; j++) { - padded_data[j] = 0; - } - for (byte b : ENIN) { - padded_data[j] = b; - j++; - } - - byte[] rpi_byte = AES.encrypt(RPIK, padded_data); - - if (rpi_byte == null) { - Log.w(TAG, "Newly generated rpi_byte is null"); - return null; - } - - byte[] metadata = new byte[4]; - byte[] AEM_byte = new byte[4]; - try { - AEM_byte = AES_CTR.encrypt(AEMK, rpi_byte, metadata); - } catch (Exception e) { - System.out.println("Error while encrypting: " + e.toString()); - } - - byte[] bluetoothpayload = new byte[20]; - System.arraycopy(rpi_byte, 0, bluetoothpayload, 0, rpi_byte.length); - System.arraycopy(AEM_byte, 0, bluetoothpayload, rpi_byte.length, AEM_byte.length); - - return bluetoothpayload; - } - - private void uploadRPIUpdate(byte[] rpi, byte[] parentTek, long updateTime, int i, int ENInvertalNumber) { - String tekString = Utils.Base64.encode(parentTek); - String rpiString = Utils.Base64.encode(rpi); - Map rpiParams = new HashMap<>(); - rpiParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, updateTime); - rpiParams.put(Constants.EXPOSURE_PLUGIN_TEK_PARAM_NAME, tekString); - rpiParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, rpiString); - rpiParams.put("updateType", ""); - rpiParams.put("_i", i); - rpiParams.put("ENInvertalNumber", ENInvertalNumber); - invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_METHOD_NAME_RPI_LOG, rpiParams); - } - - private void clearRpi() { - rpi = null; - } - - private void startAdvertise() { - if (exposureServer != null) { - exposureServer.start(); - } - } - - private void stopAdvertise() { - if (exposureServer != null) { - exposureServer.stop(); - } - } - - private void startScan() { - if (androidExposureClient != null) { - androidExposureClient.startScan(); - } - startIosScan(); - } - - private void stopScan() { - if (androidExposureClient != null) { - androidExposureClient.stopScan(); - } - stopIosScan(); - processExposures(); - } - - private void initSettings(Object settings) { - this.settings = settings; - - // Exposure Timeout Interval - int timeoutIntervalInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceTimeoutInterval", (EXPOSURE_TIMEOUT_INTERVAL_MILLIS / 1000)); // in seconds - this.exposureTimeoutIntervalInMillis = timeoutIntervalInSecs * 1000; //in millis - - // Exposure Ping Interval - int pingIntervalInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServicePingInterval", (EXPOSURE_PING_INTERVAL_MILLIS / 1000)); // in seconds - this.exposurePingIntervalInMillis = pingIntervalInSecs * 1000; //in millis - - // Exposure Process Interval - int processIntervalInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceProcessInterval", (EXPOSURE_PROCESS_INTERVAL_MILLIS / 1000)); // in seconds - this.exposureProcessIntervalInMillis = processIntervalInSecs * 1000; //in millis - - // Exposure Min Duration Interval - int minDurationInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceLogMinDuration", (Constants.EXPOSURE_MIN_DURATION_MILLIS / 1000)); // in seconds - this.exposureMinDurationInMillis = minDurationInSecs * 1000; //in millis - - // Exposure Min RSSI - this.exposureMinRssi = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceMinRSSI", Constants.EXPOSURE_MIN_RSSI_VALUE); - - // Exposure Expire Days - this.exposureExpireDays = Utils.Map.getValueFromPath(settings, "covid19ExposureExpireDays", 14); - } - - //endregion - - //region Single Instance - - int getExposureMinRssi() { - return exposureMinRssi; - } - - //endregion - - //region External RPI implementation - - private void logAndroidExposure(String rpi, int rssi, String deviceAddress) { - long currentTimeStamp = Utils.DateTime.getCurrentTimeMillisSince1970(); - ExposureRecord record = androidExposures.get(rpi); - if (record == null) { - Log.d(TAG, "registered android rpi: " + rpi); - record = new ExposureRecord(currentTimeStamp, rssi); - androidExposures.put(rpi, record); - updateExposuresTimer(); - } else { - record.updateTimeStamp(currentTimeStamp, rssi, exposureMinRssi); - } - notifyExposureTick(rpi, rssi); - notifyExposureRssiLog(rpi, currentTimeStamp, rssi, false, deviceAddress); - } - - private void logIosExposure(String peripheralAddress, int rssi) { - if (Utils.Str.isEmpty(peripheralAddress)) { - return; - } - long currentTimestamp = Utils.DateTime.getCurrentTimeMillisSince1970(); - ExposureRecord record = iosExposures.get(peripheralAddress); - if (record == null) { - // Create new - Log.d(TAG, "Registered ios peripheral: " + peripheralAddress); - record = new ExposureRecord(currentTimestamp, rssi); - iosExposures.put(peripheralAddress, record); - updateExposuresTimer(); - } else { - // Update existing - record.updateTimeStamp(currentTimestamp, rssi, exposureMinRssi); - } - byte[] rpi = peripheralsRPIs.get(peripheralAddress); - String encodedRpi = ""; - if (rpi != null) { - encodedRpi = Utils.Base64.encode(rpi); - notifyExposureTick(encodedRpi, rssi); - } - notifyExposureRssiLog(encodedRpi, currentTimestamp, rssi, true, peripheralAddress); - } - - private void notifyExposureTick(String rpi, int rssi) { - if (Utils.Str.isEmpty(rpi)) { - return; - } - long currentTimestamp = Utils.DateTime.getCurrentTimeMillisSince1970(); - // Do not allow more than one notification per second - if (EXPOSURE_NOTIFY_TICK_INTERVAL_MILLIS <= (currentTimestamp - lastNotifyExposureTickTimestamp)) { - Map exposureTickParams = new HashMap<>(); - exposureTickParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, currentTimestamp); - exposureTickParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, rpi); - exposureTickParams.put(Constants.EXPOSURE_PLUGIN_RSSI_PARAM_NAME, rssi); - invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_METHOD_NAME_THICK, exposureTickParams); - lastNotifyExposureTickTimestamp = currentTimestamp; - } - } - - private void notifyExposureRssiLog(String encodedRpi, long currentTimeStamp, int rssi, boolean isiOS, String address) { - Map rssiParams = new HashMap<>(); - rssiParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, encodedRpi); - rssiParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, currentTimeStamp); - rssiParams.put(Constants.EXPOSURE_PLUGIN_RSSI_PARAM_NAME, rssi); - rssiParams.put(Constants.EXPOSURE_PLUGIN_IOS_RECORD_PARAM_NAME, isiOS); - rssiParams.put(Constants.EXPOSURE_PLUGIN_ADDRESS_PARAM_NAME, address); - invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_METHOD_NAME_RSSI_LOG, rssiParams); - } - - private void processExposures() { - Log.d(TAG, "Process Exposures"); - long currentTimestamp = Utils.DateTime.getCurrentTimeMillisSince1970(); - Set expiredPeripheralAddress = null; - - // 1. Collect all iOS expired records (not updated after exposureTimeoutIntervalInMillis) - if ((iosExposures != null) && !iosExposures.isEmpty()) { - Iterator iosExposuresIterator = iosExposures.keySet().iterator(); - while (iosExposuresIterator.hasNext()) { - String peripheralAddress = iosExposuresIterator.next(); - ExposureRecord record = iosExposures.get(peripheralAddress); - if (record != null) { - long lastHeardInterval = currentTimestamp - record.getTimestampUpdated(); - if (exposureTimeoutIntervalInMillis <= lastHeardInterval) { - Log.d(TAG, "Expired ios exposure: " + peripheralAddress); - if (expiredPeripheralAddress == null) { - expiredPeripheralAddress = new HashSet<>(); - } - expiredPeripheralAddress.add(peripheralAddress); - } else if (exposurePingIntervalInMillis <= lastHeardInterval) { - Log.d(TAG, "ios exposure ping: " + peripheralAddress); - BluetoothPeripheral peripheral = (peripherals != null) ? peripherals.get(peripheralAddress) : null; - if (peripheral != null) { - peripheral.readRemoteRssi(); - } - } - } - } - } - - if ((expiredPeripheralAddress != null) && !expiredPeripheralAddress.isEmpty()) { - // Create copy so that to prevent crash with ConcurrentModificationException. - Set expiredPeripheralAddressCopy = new HashSet<>(expiredPeripheralAddress); - // remove expired records from iosExposures - Iterator expiredPeripheralIterator = expiredPeripheralAddressCopy.iterator(); - while (expiredPeripheralIterator.hasNext()) { - String address = expiredPeripheralIterator.next(); - disconnectIosPeripheral(address); - } - } - - // 2. Collect all Android expired records (not updated after exposureTimeoutIntervalInMillis) - Set expiredRPIs = null; - if((androidExposures != null) && !androidExposures.isEmpty()) { - Iterator androidExposuresIterator = androidExposures.keySet().iterator(); - while (androidExposuresIterator.hasNext()) { - String encodedRpi = androidExposuresIterator.next(); - ExposureRecord record = androidExposures.get(encodedRpi); - if (record != null) { - long lastHeardInterval = currentTimestamp - record.getTimestampUpdated(); - if (exposureTimeoutIntervalInMillis <= lastHeardInterval) { - Log.d(TAG, "Expired android exposure: " + encodedRpi); - if (expiredRPIs == null) { - expiredRPIs = new HashSet<>(); - } - expiredRPIs.add(encodedRpi); - } - } - } - } - - if (expiredRPIs != null) { - // Create copy so that to prevent crash with ConcurrentModificationException. - Set expiredRPIsCopy = new HashSet<>(expiredRPIs); - // remove expired records from androidExposures - Iterator expiredRPIsIterator = expiredRPIsCopy.iterator(); - while (expiredRPIsIterator.hasNext()) { - String encodedRpi = expiredRPIsIterator.next(); - removeAndroidRpi(encodedRpi); - } - } - } - - private void clearExposures() { - if ((iosExposures != null) && !iosExposures.isEmpty()) { - Map iosExposureCopy = new ConcurrentHashMap<>(iosExposures); - for (String address : iosExposureCopy.keySet()) { - disconnectIosPeripheral(address); - } - } - if ((androidExposures != null) && !androidExposures.isEmpty()) { - Map androidExposureCopy = new ConcurrentHashMap<>(androidExposures); - for (String encodedRpi : androidExposureCopy.keySet()) { - removeAndroidRpi(encodedRpi); - } - } - } - - private void disconnectIosPeripheral(String peripheralAddress) { - disconnectIosBgPeripheral(peripheralAddress); - } - - private void removeIosPeripheral(String address) { - if (Utils.Str.isEmpty(address) || (peripherals == null) || (peripherals.isEmpty())) { - return; - } - peripherals.remove(address); - byte[] rpi = (peripheralsRPIs != null) ? peripheralsRPIs.get(address) : null; - if (rpi != null) { - peripheralsRPIs.remove(address); - } - if ((iosExposures == null) || iosExposures.isEmpty()) { - return; - } - ExposureRecord record = iosExposures.get(address); - if (record != null) { - iosExposures.remove(address); - updateExposuresTimer(); - } - if ((rpi != null) && (record != null)) { - String encodedRpi = Utils.Base64.encode(rpi); - notifyExposure(record, encodedRpi, true, address); - } - } - - private void removeAndroidRpi(String rpi) { - if ((androidExposures == null) || androidExposures.isEmpty()) { - return; - } - ExposureRecord record = androidExposures.get(rpi); - if (record != null) { - androidExposures.remove(rpi); - updateExposuresTimer(); - } - - if ((rpi != null) && (record != null)) { - notifyExposure(record, rpi, false, ""); - } - } - - private void notifyExposure(ExposureRecord record, String rpi, boolean isiOS, String peripheralUuid) { - if ((record != null) && (exposureMinDurationInMillis <= record.getDuration())) { - Map exposureParams = new HashMap<>(); - exposureParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, record.getTimestampCreated()); - exposureParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, rpi); - exposureParams.put(Constants.EXPOSURE_PLUGIN_DURATION_PARAM_NAME, record.getDuration()); - exposureParams.put(Constants.EXPOSURE_PLUGIN_IOS_RECORD_PARAM_NAME, isiOS); - exposureParams.put(Constants.EXPOSURE_PLUGIN_PERIPHERAL_UUID_PARAM_NAME, peripheralUuid); - invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_EXPOSURE_METHOD_NAME, exposureParams); - } - } - - //endregion - - //region TEKs - - private void changeTekExpireTime() { - i_TEK_map = loadTeksFromStorage(); - if (i_TEK_map != null) { - Integer currentI = Collections.max(i_TEK_map.keySet()); - Map oldTEK = i_TEK_map.get(currentI); - byte[] tek = (oldTEK != null) ? oldTEK.keySet().iterator().next() : null; - - long currentTimestampInMillis = Utils.DateTime.getCurrentTimeMillisSince1970(); - long currentTimeStampInSecs = currentTimestampInMillis / 1000; - int timestamp = (int) currentTimeStampInSecs; - int ENIntervalNumber = timestamp / RPI_REFRESH_INTERVAL_SECS; - Map newTEK = new HashMap<>(); - newTEK.put(tek, (ENIntervalNumber + 1)); - - i_TEK_map.replace(currentI, newTEK); - saveTeksToStorage(i_TEK_map); - } - } - - private void saveTeksToStorage(Map> teks) { - if (teks != null) { - Map storageTeks = new HashMap<>(); - for (Integer key : teks.keySet()) { - String storageKey = key != null ? Integer.toString(key) : null; - Map value = teks.get(key); - byte[] tek = (value != null) ? value.keySet().iterator().next() : null; - Integer expire = (value != null) ? value.get(tek) : null; - Map tekAndExpireTime = new HashMap<>(); - tekAndExpireTime.put(Utils.Base64.encode(tek), (expire != null ? Integer.toString(expire) : null)); - JSONObject jsonTekAndExpireTime = new JSONObject(tekAndExpireTime); - String storageValue = jsonTekAndExpireTime.toString(); - if (storageKey != null) { - storageTeks.put(storageKey, storageValue); - } - } - JSONObject teksJson = new JSONObject(storageTeks); - String teksString = teksJson.toString(); - Utils.BackupStorage.saveString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY, teksString); - } else { - Utils.BackupStorage.remove(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY); - } - } - - private Map> loadTeksFromStorage() { - Log.d(TAG, "entering loadTeksFromStorage function"); - - //checking database version - boolean dataBaseChangeVersion = false; - String databaseVersion = Utils.BackupStorage.getString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEK_VERSION); - if (Utils.Str.isEmpty(databaseVersion)) { - Log.d(TAG, "no database version found"); - dataBaseChangeVersion = true; - } else { - JSONObject jsonDatabaseVersion = null; - try { - jsonDatabaseVersion = new JSONObject(databaseVersion); - } catch (JSONException e) { - Log.e(TAG, "Failed to parse database version string to json!"); - e.printStackTrace(); - } - if (jsonDatabaseVersion != null) { - String version = jsonDatabaseVersion.optString(DATABASE_VERSION_KEY); - Log.d(TAG, "current TEK database version is " + version); - if (Utils.Str.isEmpty(version) || Integer.parseInt(version) != 2) { - dataBaseChangeVersion = true; - } - } - } - - if (dataBaseChangeVersion) { - Utils.BackupStorage.remove(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY); - } - - Map> teks = new HashMap<>(); - String teksString = Utils.BackupStorage.getString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY); - if (!Utils.Str.isEmpty(teksString)) { - JSONObject teksJson = null; - try { - teksJson = new JSONObject(teksString); - } catch (JSONException e) { - Log.e(TAG, "Failed to parse TEKs string to json!"); - e.printStackTrace(); - } - if (teksJson != null) { - Iterator iterator = teksJson.keys(); - while (iterator.hasNext()) { - String storageKey = iterator.next(); - String storageValue = teksJson.optString(storageKey); - Map tekAndExpireTime = new HashMap<>(); - if (!Utils.Str.isEmpty(storageValue)) { - JSONObject jsonTekAndExpireTime = null; - try { - jsonTekAndExpireTime = new JSONObject(storageValue); - } catch (JSONException e) { - Log.e(TAG, "Failed to parse TEK map string to json!"); - e.printStackTrace(); - } - if (jsonTekAndExpireTime != null) { - Log.d(TAG, "LoadTEK: Found Nested Map"); - Iterator tekIterator = jsonTekAndExpireTime.keys(); - if (tekIterator.hasNext()) { - String tekString = tekIterator.next(); - String expireString = jsonTekAndExpireTime.optString(tekString); - tekAndExpireTime.put(Utils.Base64.decode(tekString), Integer.parseInt(expireString)); - } - } - } - teks.put(Integer.parseInt(storageKey), tekAndExpireTime); - } - } - } - - // update database version - - if (dataBaseChangeVersion) { - Map databaseVersionToStore = new HashMap<>(); - databaseVersionToStore.put(DATABASE_VERSION_KEY, Integer.toString(2)); - JSONObject jsonDatabaseVersion = new JSONObject(databaseVersionToStore); - String databaseVersionString = jsonDatabaseVersion.toString(); - Utils.BackupStorage.saveString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEK_VERSION, databaseVersionString); - } - return teks; - } - - private List> getTeksList() { - List> teksList = new ArrayList<>(); - if ((i_TEK_map != null) && !i_TEK_map.isEmpty()) { - for (Integer tekKey : i_TEK_map.keySet()) { - long timestamp = tekKey.longValue() * RPI_REFRESH_INTERVAL_SECS * 1000; //in millis - Map tek = i_TEK_map.get(tekKey); - byte[] tekData = (tek != null) ? tek.keySet().iterator().next() : null; - String tekString = Utils.Base64.encode(tekData); - Integer expireInteger = (tek != null) ? tek.get(tekData) : null; - long expireTime = (expireInteger != null) ? expireInteger.longValue() * RPI_REFRESH_INTERVAL_SECS * 1000 : 0; //in millis - Map tekMap = new HashMap<>(); - tekMap.put("timestamp", timestamp); - tekMap.put("tek", tekString); - tekMap.put(Constants.EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME, expireTime); - teksList.add(tekMap); - } - } - return teksList; - } - - private Map getRpisForTek(byte[] tek, long timestampInMillis, long expireTime) { - long timestampInSecs = timestampInMillis / 1000; - - long expireTimeInSecs = expireTime / 1000; - - int startENIntervalNumber = (int) (timestampInSecs / RPI_REFRESH_INTERVAL_SECS); - int endENIntervalNumber = (int) (expireTimeInSecs / RPI_REFRESH_INTERVAL_SECS); - - /* handle TEKs without expirestamp (0 or -1), default to 1 day later */ - if (endENIntervalNumber < startENIntervalNumber || endENIntervalNumber > startENIntervalNumber + TEKRollingPeriod) - endENIntervalNumber = startENIntervalNumber + TEKRollingPeriod; - - Map rpiList = new HashMap<>(); - for (int intervalIndex = startENIntervalNumber; intervalIndex <= endENIntervalNumber; intervalIndex++) { - byte[] rpi = generateRpiForIntervalNumber(intervalIndex, tek); - String rpiString = Utils.Base64.encode(rpi); - rpiList.put(rpiString, (long) intervalIndex * RPI_REFRESH_INTERVAL_SECS * 1000); - } - return rpiList; - } - - private void notifyTek(byte[] tek, long timestamp, long expireTime) { - String tekString = Utils.Base64.encode(tek); - Map tekParams = new HashMap<>(); - tekParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, timestamp); - tekParams.put(Constants.EXPOSURE_PLUGIN_TEK_PARAM_NAME, tekString); - tekParams.put(Constants.EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME, expireTime); - invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_TEK_METHOD_NAME, tekParams); - } - - //endregion - - //region Exposure Server - - private void bindExposureServer() { - Intent intent = new Intent(activityContext, ExposureServer.class); - activityContext.bindService(intent, serverConnection, Context.BIND_AUTO_CREATE); - } - - private void unBindExposureServer() { - if (serverStarted) { - activityContext.unbindService(serverConnection); - serverStarted = false; - } - } - - private ServiceConnection serverConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - exposureServer = ((ExposureServer.LocalServerBinder)service).getService(); - if (exposureServer == null) { - return; - } - serverStarted = true; - exposureServer.setCallback(serverCallback); - checkStarted(); - } - public void onServiceDisconnected(ComponentName className) { - serverStarted = false; - exposureServer = null; - } - }; - - private ExposureServer.Callback serverCallback = new ExposureServer.Callback() { - @Override - public void onRequestBluetoothOn() { - ExposurePlugin.this.requestBluetoothOn(); - } - }; - - //endregion - - //region Exposure Client - - private void bindExposureClient() { - Intent intent = new Intent(activityContext, ExposureClient.class); - activityContext.bindService(intent, clientConnection, Context.BIND_AUTO_CREATE); - } - - private void unBindExposureClient() { - if (clientStarted) { - activityContext.unbindService(clientConnection); - clientStarted = false; - } - } - - private ServiceConnection clientConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - androidExposureClient = ((ExposureClient.LocalBinder)service).getService(); - if (androidExposureClient == null) { - return; - } - androidExposureClient.initSettings(settings); - clientStarted = true; - androidExposureClient.setRpiCallback(clientRpiCallback); - checkStarted(); - } - public void onServiceDisconnected(ComponentName className) { - clientStarted = false; - androidExposureClient = null; - } - }; - - private ExposureClient.RpiCallback clientRpiCallback = new ExposureClient.RpiCallback() { - @Override - public void onRpiFound(byte[] rpi, int rssi, String address) { - if ((rpi != null) && (rssi != Constants.EXPOSURE_NO_RSSI_VALUE)) { - String rpiEncoded = Utils.Base64.encode(rpi); - Log.d(TAG, String.format(Locale.getDefault(), "onRpiFound: '%s' / rssi: %d", rpiEncoded, rssi)); - logAndroidExposure(rpiEncoded, rssi, address); - } - } - - @Override - public void onIOSDeviceFound(ScanResult scanResult) { - BluetoothDevice device = (scanResult != null) ? scanResult.getDevice() : null; - String devAddress = (device != null) ? device.getAddress() : null; - if (!Utils.Str.isEmpty(devAddress)) { - if (peripherals_bg.get(devAddress) == null) { - Log.d(TAG, ": ios fg attempting to connect"); - // new device discovered. - peripherals_bg.put(devAddress, device.connectGatt(activityContext, false, iOSBackgroundBluetoothGattCallback)); - } - logIosExposure(devAddress, scanResult.getRssi()); - } - } - }; - - //endregion - - //region iOS specific scanner/client - - private void startIosScan() { - startIosBgScan(); - } - - private void stopIosScan() { - stopIosBgScan(); - } - - //endregion - - //region iOS background specific scanner/client - - private void startIosBgScan() { - Log.d(TAG, "startIosBgScan()"); - try { - if (isIosBgScanning()) { - stopIosBgScan(); - } - if (iosBgBluetoothAdapter == null) { - Object systemService = activityContext.getSystemService(Context.BLUETOOTH_SERVICE); - if ((systemService instanceof BluetoothManager)) { - iosBgBluetoothAdapter = ((BluetoothManager) systemService).getAdapter(); - } - } - if (iosBgBluetoothAdapter == null) { - Log.d(TAG, "ios bg scan bluetooth adapter init failure"); - return; - } - if (iosBgBluoetoothLeScanner == null) { - iosBgBluoetoothLeScanner = iosBgBluetoothAdapter.getBluetoothLeScanner(); - } - if (iosBgBluoetoothLeScanner == null) { - Log.d(TAG, "ios bg scan bluetooth scanner init failure"); - return; - } - startIosBgScanTimer(); - ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder() - .setManufacturerData(iosbgManufacturerID, manufacturerData, manufacturerDataMask); - iosBgScanFilters = Collections.singletonList(scanFilterBuilder.build()); - - ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT); - iosBgScanSettings = scanSettingsBuilder.build(); - - iosBgBluoetoothLeScanner.startScan(iosBgScanFilters, iosBgScanSettings, iOSBackgroundScanCallback); - Log.d(TAG, "Start ios bg scan success!"); - } catch (Exception ex) { - Log.e(TAG, ex.toString()); - ex.printStackTrace(); - // re-try - startIosBgScan(); - } - } - - private void stopIosBgScan() { - stopIosBgScanTimer(); - if (isIosBgScanning()) { - // Check if bluetooth is on. - if (iosBgBluetoothAdapter.isEnabled() && (iosBgBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON)) { - iosBgBluoetoothLeScanner.stopScan(iOSBackgroundScanCallback); - } - Log.d(TAG, "stopped ios bg scanning"); - } else { - Log.d(TAG, "no ios bg scanner is scanning"); - } - iosBgBluoetoothLeScanner = null; - iosBgBluetoothAdapter = null; - iosBgScanFilters = null; - iosBgScanSettings = null; - } - - private boolean isIosBgScanning() { - return ((iosBgBluetoothAdapter != null) && (iosBgBluoetoothLeScanner != null) && - (iosBgScanFilters != null) && (iosBgScanSettings != null)); - } - - private void startIosBgScanTimer() { - stopIosBgScanTimer(); - - iosBgScanTimeoutRunnable = () -> { - Log.d(TAG, "scanning timeout, restarting scan"); - stopIosBgScan(); - // Restart the scan and timer - callbackHandler.postDelayed(this::startIosBgScan, 1_000); - }; - - mainHandler.postDelayed(iosBgScanTimeoutRunnable, 180_000); - } - - private void stopIosBgScanTimer() { - if (iosBgScanTimeoutRunnable != null) { - mainHandler.removeCallbacks(iosBgScanTimeoutRunnable); - iosBgScanTimeoutRunnable = null; - } - } - - private void disconnectIosBgPeripheral(String peripheralAddress) { - if (Utils.Str.isEmpty(peripheralAddress) || (peripherals_bg == null)) { - return; - } - BluetoothGatt ble_gatt = peripherals_bg.get(peripheralAddress); - if (ble_gatt == null) { - Log.d(TAG, "gatt does not exist in bg"); - return; - } - // clean up connection - ble_gatt.close(); - ble_gatt.disconnect(); - removeIosBgPeripheral(peripheralAddress); - } - - private void removeIosBgPeripheral(String address) { - if (Utils.Str.isEmpty(address) || (peripherals_bg == null) || (peripherals_bg.isEmpty())) { - return; - } - peripherals_bg.remove(address); - byte[] rpi = (peripheralsRPIs != null) ? peripheralsRPIs.get(address) : null; - if (rpi != null) { - peripheralsRPIs.remove(address); - } - if ((iosExposures == null) || iosExposures.isEmpty()) { - return; - } - ExposureRecord record = iosExposures.get(address); - if (record != null) { - iosExposures.remove(address); - updateExposuresTimer(); - } - if ((rpi != null) && (record != null)) { - String encodedRpi = Utils.Base64.encode(rpi); - notifyExposure(record, encodedRpi, true, address); - } - } - - private BluetoothGattCallback iOSBackgroundBluetoothGattCallback = new BluetoothGattCallback() { - @Override - public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { - BluetoothGatt a = peripherals_bg.get(gatt.getDevice().getAddress()); - if (newState == 2 && a != null) { - // seems that a delay is needed - int delay = 600; - ios_bg_handler.postDelayed(new Runnable() { - @Override - public void run() { - boolean result = gatt.discoverServices(); - if (!result) { - Log.d(TAG, "DiscoverServices failed to start"); - } - } - }, delay); - - } else if (newState == 0) { - peripherals_bg.remove(gatt.getDevice().getAddress()); - gatt.close(); - } - } - - @Override - public void onServicesDiscovered(BluetoothGatt gatt, int status) { - if (status == 0) { - BluetoothGatt _local_gatt = peripherals_bg.get(gatt.getDevice().getAddress()); - BluetoothGattService _local_service = (_local_gatt != null) ? _local_gatt.getService(Constants.EXPOSURE_UUID_SERVICE) : null; - if (_local_service == null) { - // disconnect? remove from dictionary - peripherals_bg.remove(gatt.getDevice().getAddress()); - gatt.close(); - } else { - // read characteristics from the service - // log ios devices. - BluetoothGattCharacteristic _local_characeristics = _local_service.getCharacteristic(Constants.EXPOSURE_UUID_CHARACTERISTIC); - _local_gatt.readCharacteristic(_local_characeristics); - } - } else { - Log.d(TAG, "service discovery failed.: " + status); - } - } - - @Override - public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, - int status) { - if (status != BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "reading characteristics failed"); - return; - } - if (characteristic.getUuid().equals(Constants.EXPOSURE_UUID_CHARACTERISTIC)) { - byte[] val = characteristic.getValue(); - peripheralsRPIs.put(gatt.getDevice().getAddress(), val); - // copied from blessed image and modified - BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(CCC_DESCRIPTOR_UUID)); - if (descriptor == null) { - peripherals_bg.remove(gatt.getDevice().getAddress()); - gatt.close(); - return; - } - byte[] value; - int properties = characteristic.getProperties(); - if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { - value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; - } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) { - value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; - } else { - peripherals_bg.remove(gatt.getDevice().getAddress()); - gatt.close(); - return; - } - final byte[] finalValue = value; - // First set notification for Gatt object - // turn on or off - if (!gatt.setCharacteristicNotification(characteristic, true)) { - Log.d(TAG, "setCharacteristicNotification failed for characteristic: " - + characteristic.getUuid()); - } - // Then write to descriptor - descriptor.setValue(finalValue); - boolean result; - result = gatt.writeDescriptor(descriptor); - if (!result) { - peripherals_bg.remove(gatt.getDevice().getAddress()); - gatt.close(); - } - else{ - Log.d(TAG, "descriptor written successfully"); - } - } - } - - @Override - public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - if (characteristic.getUuid().equals(Constants.EXPOSURE_UUID_CHARACTERISTIC)) { - byte[] val = characteristic.getValue(); - String encoded = Utils.Base64.encode(val); - Log.d(TAG, "onCharacteristicChange: value: " + encoded + ", device address: " + gatt.getDevice().getAddress()); - peripheralsRPIs.put(gatt.getDevice().getAddress(), val); - } - } - }; - - private final ScanCallback iOSBackgroundScanCallback = new ScanCallback() { - - @Override - public void onScanFailed(int errorCode) { - Log.d(TAG, "iOSBackgroundScanCallback: onScanFailed: " + errorCode); - } - - @Override - public void onBatchScanResults(List results) { - Log.d(TAG, "iOSBackgroundScanCallback: onBatchScanResults: " + ((results != null) ? results.size() : 0)); - } - - @Override - public void onScanResult(int callbackType, ScanResult result) { - super.onScanResult(callbackType, result); - ScanRecord scanrecord = result.getScanRecord(); - List parcelUuids = (scanrecord != null) ? scanrecord.getServiceUuids() : null; - List serviceList = new ArrayList<>(); - if (parcelUuids != null) { - for (int i = 0; i < parcelUuids.size(); i++) { - UUID serviceUUID = parcelUuids.get(i).getUuid(); - if (!serviceList.contains(serviceUUID)) - serviceList.add(serviceUUID); - } - } else { - Log.d(TAG, "parcel UUID is null"); - } - BluetoothDevice device = result.getDevice(); - byte[] manData = (scanrecord != null) ? scanrecord.getManufacturerSpecificData(iosbgManufacturerID) : null; - if (manData != null) { - // 01 - if (manData.length >= 17) { - if (manData[0] == 0x01) { - if (((manData[15] >> 5) & 0x01) == 1) { - String devAddress = device.getAddress(); - // bg device discovered - // equivalently ondiscoverperipherals - if (peripherals_bg.get(devAddress) == null) { - // new device discovered. - peripherals_bg.put(devAddress, - device.connectGatt(activityContext, false, iOSBackgroundBluetoothGattCallback)); - - } - // log ios exposure - logIosExposure(device.getAddress(), result.getRssi()); - } - } - } - } - } - }; - - //endregion - - //region Flutter Start Result - - private void checkStarted() { - if (serverStarted && clientStarted) { - start(); - if (startedResult != null) { - MethodChannel.Result result = startedResult; - startedResult = null; - result.success(true); - } - } - } - - //endregion - - private void startExpUpTimeTimer() { - expStartTime = (long) Utils.DateTime.getCurrentTimeMillisSince1970() / 1000; - long refreshIntervalInMillis = 60 * 1000; - stopExpUpTimeTimer(); - expUpTimeTimer = new Timer(); - expUpTimeTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - expUpTimeHit(); - } - }, refreshIntervalInMillis, refreshIntervalInMillis); - } - - private void stopExpUpTimeTimer() { - if (expUpTimeTimer != null) { - expUpTimeTimer.cancel(); - } - expUpTimeTimer = null; - if (expStartTime != 0) { - long currentTime = (long) Utils.DateTime.getCurrentTimeMillisSince1970() / 1000; - this.expUpTimeMap.put(new Integer((int)expStartTime), new Integer((int)(currentTime - expStartTime))); - saveExpUpTimeToStorage(); - } - } - - private void expUpTimeHit() { - if (expStartTime != 0 && expUpTimeTimer != null) { - long currentTime = (long) Utils.DateTime.getCurrentTimeMillisSince1970() / 1000; - this.expUpTimeMap.put(new Integer((int)expStartTime), new Integer((int)(currentTime - expStartTime))); - saveExpUpTimeToStorage(); - } - } - - private void removeExpiredTime() { - if (expUpTimeMap != null) { - long currentTime = (long) Utils.DateTime.getCurrentTimeMillisSince1970() / 1000; - long _expireTimestamp = currentTime - 168 * 60 * 60; - Iterator it = expUpTimeMap.keySet().iterator(); - while (it.hasNext()) { - Integer _time = it.next(); - if ((_time.intValue() + expUpTimeMap.get(_time).intValue()) < (int) _expireTimestamp) { - it.remove(); - } - } - } - } - - private void loadExpUpTimeFromStorage() { - try - { - File file = new File(activityContext.getFilesDir()+"/exposureUpTime.map"); - if (!file.exists()) - file.createNewFile(); - FileInputStream fileInputStream = new FileInputStream(new File(activityContext.getFilesDir()+"/exposureUpTime.map")); - ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); - this.expUpTimeMap = new HashMap((Map)objectInputStream.readObject()); - } - catch(ClassNotFoundException | IOException | ClassCastException e) { - this.expUpTimeMap = new HashMap(); - e.printStackTrace(); - } - } - - private void saveExpUpTimeToStorage() { - try - { - removeExpiredTime(); - FileOutputStream fos = activityContext.openFileOutput("exposureUpTime.map", Context.MODE_PRIVATE); - ObjectOutputStream oos = new ObjectOutputStream(fos); - oos.writeObject(this.expUpTimeMap); - oos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private Integer expUpDurationInWindow (long upTimeWindow) { - int durationInWindow = 0; - long currentTime = (long) Utils.DateTime.getCurrentTimeMillisSince1970() / 1000; - long _timeWindowInSeconds = upTimeWindow * 60 * 60; - long _expireTimestamp = currentTime - 168 * 60 * 60; - long _startTimestamp = currentTime - _timeWindowInSeconds; - if (expUpTimeMap != null) { - for (Integer _time : expUpTimeMap.keySet()) { - if (_time.intValue() + expUpTimeMap.get(_time).intValue() >= _startTimestamp) { - if (_time.intValue() >= _startTimestamp) { - durationInWindow += expUpTimeMap.get(_time); - } else { - durationInWindow += expUpTimeMap.get(_time).intValue() + _time.intValue() - (int) _startTimestamp; - } - } else if ((_time.intValue() + expUpTimeMap.get(_time).intValue()) < (int)_expireTimestamp) { - expUpTimeMap.remove(_time); - } - } - } - return new Integer(durationInWindow); - } - - //region RPI timer - - private void startRpiTimer() { - long refreshIntervalInMillis = RPI_REFRESH_INTERVAL_SECS * 1000; - stopRpiTimer(); - rpiTimer = new Timer(); - rpiTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - refreshRpi(); - } - }, refreshIntervalInMillis, refreshIntervalInMillis); - } - - private void stopRpiTimer() { - if (rpiTimer != null) { - rpiTimer.cancel(); - } - rpiTimer = null; - } - - //endregion - - //region Exposures timer - - private void updateExposuresTimer() { - int exposuresCount = androidExposures.size() + iosExposures.size(); - if ((exposuresCount > 0) && (exposuresTimer == null)) { - exposuresTimer = new Timer(); - exposuresTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - processExposures(); - } - }, exposureProcessIntervalInMillis, exposureProcessIntervalInMillis); - } else if ((exposuresCount == 0) && (exposuresTimer != null)) { - exposuresTimer.cancel(); - exposuresTimer = null; - } - } - - //endregion - - //region MethodCall - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - String method = call.method; - try { - switch (method) { - case Constants.EXPOSURE_PLUGIN_METHOD_NAME_START: - Object settings = call.argument(Constants.EXPOSURE_PLUGIN_SETTINGS_PARAM_NAME); - handleStart(result, settings); // Result is handled on a latter step - break; - case Constants.EXPOSURE_PLUGIN_METHOD_NAME_STOP: - handleStop(); - result.success(true); - break; - case Constants.EXPOSURE_PLUGIN_METHOD_NAME_TEKS: - boolean removeTeks = Utils.Map.getValueFromPath(call.arguments, "remove", false); - if (removeTeks) { - saveTeksToStorage(null); - result.success(null); - } else { - List> teksList = getTeksList(); - result.success(teksList); - } - break; - case Constants.EXPOSURE_PLUGIN_METHOD_NAME_TEK_RPIS: - Object parameters = call.arguments; - String tekString = Utils.Map.getValueFromPath(parameters, Constants.EXPOSURE_PLUGIN_TEK_PARAM_NAME, null); - byte[] tek = Utils.Base64.decode(tekString); - long timestamp = Utils.Map.getValueFromPath(parameters, Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, -1L); - long expireTime = Utils.Map.getValueFromPath(parameters, Constants.EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME, -1L); - Map rpis = getRpisForTek(tek, timestamp, expireTime); - result.success(rpis); - break; - case Constants.EXPOSURE_PLUGIN_METHOD_EXP_UP_TIME: - long upTimeWindow = Utils.Map.getValueFromPath(call.arguments, Constants.EXPOSURE_PLUGIN_UP_TIME_WIN_PARAM_NAME, 0); - Integer upTimeWindowResult = expUpDurationInWindow(upTimeWindow); - result.success(upTimeWindowResult); - break; - case Constants.EXPOSURE_PLUGIN_METHOD_NAME_EXPIRE_TEK: - changeTekExpireTime(); - result.success(null); - break; - default: - result.success(null); - break; - - } - } catch (IllegalStateException exception) { - String errorMsg = String.format("Ignoring exception '%s'. See https://github.com/flutter/flutter/issues/29092 for details.", exception.toString()); - Log.e(TAG, errorMsg); - exception.printStackTrace(); - } - } - - private void invokeFlutterMethod(String methodName, Object arguments) { - if (methodChannel != null) { - // Run on the ui thread - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> methodChannel.invokeMethod(methodName, arguments)); - } - } - - //endregion - - // region Bluetooth - - public void onLocationPermissionGranted() { - Log.d(TAG, "onLocationPermissionGranted"); - if (androidExposureClient != null) { - androidExposureClient.onLocationPermissionGranted(); - } - } - - private void requestBluetoothOn() { - Log.d(TAG, "requestBluetoothOn"); - - Utils.showDialog(activityContext, activityContext.getString(R.string.app_name), - activityContext.getString(R.string.exposure_request_bluetooth_on_message), - (dialog, which) -> { - //Turn bluetooth on - Utils.enabledBluetooth(); - - }, "Yes", - (dialog, which) -> { - }, "No", - true); - } - - //endregion - - //region Helpers - - private StringBuilder byte_to_hex(byte[] byte_array) { - StringBuilder sb = new StringBuilder(); - if (byte_array != null) { - for (byte b : byte_array) { - sb.append(String.format("%02X ", b)); - } - } - return sb; - } - - //endregion - - // Flutter Plugin - - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - setupChannels(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - disposeChannels(); - } - - private void setupChannels(BinaryMessenger messenger, Context context) { - methodChannel = new MethodChannel(messenger, "edu.illinois.covid/exposure"); - methodChannel.setMethodCallHandler(this); - } - - private void disposeChannels() { - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ExposureRecord.java b/android/app/src/main/java/edu/illinois/covid/exposure/ExposureRecord.java deleted file mode 100644 index 5674e9da..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ExposureRecord.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure; - -import edu.illinois.covid.Constants; - -class ExposureRecord { - private long timestampCreated; - private long timestampUpdated; - private int lastRssi; - private long durationInterval; - - ExposureRecord(long timestamp, int rssi) { - this.timestampCreated = timestamp; - this.timestampUpdated = timestamp; - this.lastRssi = rssi; - this.durationInterval = 0; - } - - void updateTimeStamp(long timestamp, int rssi, int minRssi) { - if ((minRssi <= lastRssi) && (lastRssi != Constants.EXPOSURE_NO_RSSI_VALUE)) { - durationInterval += (timestamp - timestampUpdated); - } - lastRssi = rssi; - timestampUpdated = timestamp; - } - - /** - * @return duration in milliseconds - */ - long getDuration() { - return durationInterval; - } - - long getTimestampCreated() { - return timestampCreated; - } - - long getTimestampUpdated() { - return timestampUpdated; - } -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java deleted file mode 100644 index 99035632..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure.ble; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.Service; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.ScanRecord; -import android.bluetooth.le.ScanResult; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.util.Log; - -import java.util.concurrent.atomic.AtomicBoolean; - -import androidx.core.content.ContextCompat; -import edu.illinois.covid.Constants; -import edu.illinois.covid.R; -import edu.illinois.covid.Utils; -import edu.illinois.covid.exposure.ble.scan.OreoScanner; -import edu.illinois.covid.exposure.ble.scan.PreOreoScanner; - -public class ExposureClient extends Service { - private static final String TAG = "ExposureClient"; - - public class LocalBinder extends Binder { - public ExposureClient getService() { - return ExposureClient.this; - } - } - private final IBinder mBinder = new LocalBinder(); - - private static ExposureClient instance; - - private BluetoothAdapter mBluetoothAdapter; - - private PreOreoScanner preOreoScanner; - private OreoScanner oreoScanner; - - private RpiCallback rpiCallback; - - private AtomicBoolean waitBluetoothOn = new AtomicBoolean(false); - private AtomicBoolean waitLocationPermissionGranted = new AtomicBoolean(false); - - private AtomicBoolean isScanning = new AtomicBoolean(false); - - private Object settings; - - public static ExposureClient getInstance() { - return instance; - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onCreate() { - Object systemService = getSystemService(Context.BLUETOOTH_SERVICE); - if (systemService instanceof BluetoothManager) { - mBluetoothAdapter = ((BluetoothManager) systemService).getAdapter(); - } - if (mBluetoothAdapter == null) { - return; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - oreoScanner = new OreoScanner(getApplicationContext(), mBluetoothAdapter); - } else { - preOreoScanner = new PreOreoScanner(mBluetoothAdapter); - } - startForegroundClientService(); - instance = this; - IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); - registerReceiver(bluetoothReceiver, filter); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log.d(TAG, "onStartCommand - " + startId); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // This call is required to be made after startForegroundService() since API 26 - startForegroundClientService(); - } - - if (intent != null) { - Bundle extras = intent.getExtras(); - if (extras != null) { - Object found = extras.get(Constants.EXPOSURE_BLE_DEVICE_FOUND); - if (found != null) { - if (found instanceof ScanResult) { - ScanResult scanResult = ((ScanResult) found); - onScanResultFound(scanResult); - } else { - Log.d(TAG, "found is not ScanResult"); - } - } else { - Log.d(TAG, "found is null"); - } - } else { - Log.d(TAG, "extras are null"); - } - } else { - Log.d(TAG, "The intent is null"); - } - return START_STICKY; - } - - @Override - public void onDestroy() { - stopService(); - unregisterReceiver(bluetoothReceiver); - } - - public void setRpiCallback(RpiCallback rpiCallback) { - this.rpiCallback = rpiCallback; - } - - @SuppressLint("NewApi") - public void startScan() { - Log.d(TAG, "startScan"); - - //check if bluetooth is on - boolean needsWaitBluetooth = needsWaitBluetooth(); - if (needsWaitBluetooth) { - waitBluetoothOn.set(true); - return; - } - waitBluetoothOn.set(false); - - //check if location permission is granted - boolean needsWaitLocationPermission = needsWaitLocationPermission(); - if (needsWaitLocationPermission) { - waitLocationPermissionGranted.set(true); - return; - } - waitLocationPermissionGranted.set(false); - - startForegroundClientService(); - isScanning.set(true); - - if (preOreoScanner != null) { - preOreoScanner.startScan(new PreOreoScanner.ScannerCallback() { - @Override - public void onDevice(ScanResult result) { - super.onDevice(result); - onScanResultFound(result); - } - }); - } - if (oreoScanner != null) { - oreoScanner.startScan(); - } - } - - public boolean exposureServiceLocalNotificationEnabled() { - return Utils.Map.getValueFromPath(settings, "covid19ExposureServiceLocalNotificationEnabledAndroid", false); - } - - private boolean needsWaitBluetooth() { - if ((mBluetoothAdapter == null) || !mBluetoothAdapter.isEnabled()) { - Log.d(TAG, "processBluetoothCheck needs to wait for bluetooth"); - return true; - } else { - Log.d(TAG, "processBluetoothCheck - bluetooth ready"); - } - return false; - } - - private boolean needsWaitLocationPermission() { - if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "needsWaitLocationPermission - location is not set"); - return true; - } else { - Log.d(TAG, "needsWaitLocationPermission - location ready"); - } - return false; - } - - @SuppressLint("NewApi") - public void stopScan() { - Log.d(TAG, "stopScan"); - - if (preOreoScanner != null) { - preOreoScanner.stopScan(); - } - if (oreoScanner != null) { - oreoScanner.stopScan(); - } - waitBluetoothOn.set(false); - waitLocationPermissionGranted.set(false); - - stopService(); - isScanning.set(false); - } - - public void onLocationPermissionGranted() { - Log.d(TAG, "onLocationPermissionGranted"); - - //start the advertising if it waits for location - Handler handler = new Handler(Looper.getMainLooper()); - Runnable runnable = () -> { - if (waitLocationPermissionGranted.get()) { - startScan(); - } - }; - handler.postDelayed(runnable, 2000); - } - - public void initSettings(Object settings) { - this.settings = settings; - } - - private void onScanResultFound(ScanResult scanResult) { - if (scanResult == null) { - Log.d(TAG, "onScanResultFound: result is null"); - return; - } - ScanRecord scanRecord = scanResult.getScanRecord(); - if (scanRecord == null) { - Log.d(TAG, "onScanResultFound: ScanRecord is null for ScanResult: " + scanResult.toString()); - return; - } - - // Android - check service data - byte[] possibleRpi = scanRecord.getServiceData(Constants.EXPOSURE_PARCEL_SERVICE_UUID); - - if ((possibleRpi != null) && possibleRpi.length == Constants.EXPOSURE_CONTRACT_NUMBER_LENGTH) { - Log.d(TAG, "onScanResultFound: Bytes are found in Android device!"); - String rpiEncoded = Utils.Base64.encode(possibleRpi); - String deviceAddress = (scanResult.getDevice() != null ? scanResult.getDevice().getAddress() : ""); - Log.d(TAG, "onScanResultFound: rpiFound: " + rpiEncoded + " from device address: " + deviceAddress); - if (rpiCallback != null) { - rpiCallback.onRpiFound(possibleRpi, scanResult.getRssi(), deviceAddress); - } - } else if (possibleRpi == null) { - // this could be an ios device - Log.d(TAG, "onScanResultFound: might be ios: " + scanResult.getDevice().getAddress()); - if (rpiCallback != null) { - rpiCallback.onIOSDeviceFound(scanResult); - } - } - } - - private void stopService() { - stopForeground(true); - stopSelf(); - } - - //region BroadcastReceiver - - private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - - if ((action != null) && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { - final int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, - BluetoothAdapter.ERROR); - if (bluetoothState == BluetoothAdapter.STATE_ON) { - Log.d(TAG, "Bluetooth is on"); - //start the advertising if it waits for bluetooth - Handler handler = new Handler(Looper.getMainLooper()); - Runnable runnable = () -> { - if (waitBluetoothOn.get()) { - startScan(); - } - }; - handler.postDelayed(runnable, 2000); - } - } - } - }; - - //endregion - - //region Foreground service - - private void createNotificationChannelIfNeeded() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - CharSequence name = getString(R.string.app_name); - String description = getString(R.string.exposure_notification_channel_description); - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel( - NotificationCreator.getChannelId(), name, importance); - channel.setDescription(description); - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - if (notificationManager != null) { - notificationManager.createNotificationChannel(channel); - } - } - } - - private void startForegroundClientService() { - boolean exposureServiceLocalNotificationEnabled = exposureServiceLocalNotificationEnabled(); - if (exposureServiceLocalNotificationEnabled) { - createNotificationChannelIfNeeded(); - startForeground(NotificationCreator.getOngoingNotificationId(), - NotificationCreator.getNotification(this)); - } - } - - - //endregion - - //region RpiCallback - - public static class RpiCallback { - public void onRpiFound(byte[] rpi, int rssi, String address) {} - public void onIOSDeviceFound(ScanResult scanResult){} - } - - //endregion -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureServer.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureServer.java deleted file mode 100644 index a06fa94e..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureServer.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure.ble; - -import android.app.Service; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.AdvertiseCallback; -import android.bluetooth.le.AdvertiseData; -import android.bluetooth.le.AdvertiseSettings; -import android.bluetooth.le.BluetoothLeAdvertiser; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.ParcelUuid; -import android.util.Log; -import android.widget.Toast; - -import java.util.concurrent.atomic.AtomicBoolean; - -import androidx.annotation.Nullable; -import edu.illinois.covid.BuildConfig; -import edu.illinois.covid.Constants; - -public class ExposureServer extends Service { - - //region Member fields - - private static final String TAG = ExposureServer.class.getSimpleName(); - - public class LocalServerBinder extends Binder { - public ExposureServer getService() { - return ExposureServer.this; - } - } - - private final IBinder binder = new LocalServerBinder(); - - private BluetoothAdapter bluetoothAdapter; - - private AtomicBoolean isAdvertising = new AtomicBoolean(false); - private AtomicBoolean waitBluetoothOn = new AtomicBoolean(false); - - private byte[] rpi; - - private Callback callback; - - //endregion - - //region Service implementation - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return binder; - } - - @Override - public void onCreate() { - Object systemService = getSystemService(Context.BLUETOOTH_SERVICE); - if (systemService instanceof BluetoothManager) { - bluetoothAdapter = ((BluetoothManager) systemService).getAdapter(); - } - if (bluetoothAdapter == null) { - Log.d(TAG, "onCreate: bluetoothAdapter is null"); - return; - } - IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); - registerReceiver(bluetoothReceiver, filter); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } - - @Override - public void onDestroy() { - stopAdvertising(); - unregisterReceiver(bluetoothReceiver); - } - - //endregion - - //region Public APIs - - public void start() { - Log.d(TAG, "start server"); - - if (bluetoothAdapter == null) { - Log.d(TAG, "start - bluetoothAdapter is null"); - return; - } - - // ask for bluetooth if not set - if (!bluetoothAdapter.isEnabled()) { - if (callback != null) - callback.onRequestBluetoothOn(); - } - startAdvertising(); - } - - public void stop() { - Log.d(TAG, "stop server"); - stopAdvertising(); - } - - public void setRpi(byte[] rpi) { - Log.d(TAG, "set rpi"); - this.rpi = rpi; - if (isAdvertising.get()) { - stopAdvertising(); - startAdvertising(); - } - } - - public void setCallback(Callback callback) { - this.callback = callback; - } - - //endregion - - //region Advertising - - private void startAdvertising() { - if ((rpi == null)) { - Log.d(TAG, "startAdvertising: rpi is null! Advertising not started!"); - return; - } - if (!bluetoothAdapter.isEnabled()) { - Log.d(TAG, "startAdvertising: wait for bluetooth to be enabled"); - waitBluetoothOn.set(true); - return; - } - waitBluetoothOn.set(false); - - BluetoothLeAdvertiser advertiser = bluetoothAdapter.getBluetoothLeAdvertiser(); - if (advertiser == null) { - Log.w(TAG, "Device does not support BLE advertisement"); - showToast("Exposure: This device does not support BLE advertisement"); - return; - } - showToast("Exposure: Start advertising"); - - // Use try catch to handle DeadObject exception - try { - AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder(); - settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED); - settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM); - settingsBuilder.setConnectable(true); - settingsBuilder.setTimeout(0); - - ParcelUuid parceluuid = new ParcelUuid(Constants.EXPOSURE_UUID_SERVICE); - AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder(); - dataBuilder.setIncludeDeviceName(false); - dataBuilder.addServiceUuid(parceluuid); - dataBuilder.addServiceData(parceluuid, rpi); - - AdvertiseSettings advertiseSettings = settingsBuilder.build(); - AdvertiseData advertiseData = dataBuilder.build(); - advertiser.startAdvertising(advertiseSettings, advertiseData, advertiseCallback); - } catch (Exception ex) { - String errMsg = "Exposure: start advertising failed!"; - Log.e(TAG, errMsg); - ex.printStackTrace(); - showToast(errMsg); - // re-try - startAdvertising(); - } - } - - private void stopAdvertising() { - showToast("Exposure: Stop advertising"); - waitBluetoothOn.set(false); - if (bluetoothAdapter != null) { - BluetoothLeAdvertiser advertiser = bluetoothAdapter.getBluetoothLeAdvertiser(); - if (advertiser != null) { - advertiser.stopAdvertising(advertiseCallback); - } - } - stopForeground(true); - isAdvertising.set(false); - } - - private AdvertiseCallback advertiseCallback = new AdvertiseCallback() { - - @Override - public void onStartSuccess(AdvertiseSettings settingsInEffect) { - super.onStartSuccess(settingsInEffect); - Log.d(TAG, "AdvertiseCallback onStartSuccess "); - - showToast("Exposure: Start advertising Succeed"); - - isAdvertising.set(true); - } - - @Override - public void onStartFailure(int errorCode) { - super.onStartFailure(errorCode); - Log.e(TAG, "AdvertiseCallback onStartFailure " + errorCode); - - showToast("Exposure: Start advertising failed " + errorCode); - - isAdvertising.set(false); - } - }; - - /** - * Show toasts only for DEBUG builds - * @param message the message that has to be shown - */ - private void showToast(String message) { - if (BuildConfig.DEBUG) { - new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show()); - } - } - - //endregion - - //region Bluetooth Receiver - - private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - - if ((action != null) && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { - final int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - if (bluetoothState == BluetoothAdapter.STATE_ON) { - Log.d(TAG, "Bluetooth is on"); - //start the advertising if it waits for bluetooth - Handler handler = new Handler(Looper.getMainLooper()); - Runnable runnable = () -> { - if (waitBluetoothOn.get()) { - startAdvertising(); - } - }; - handler.postDelayed(runnable, 2000); - } - } - } - }; - - //endregion - - //region Exposure Server Callback - - public static class Callback { - public void onRequestBluetoothOn() {} - } - - //endregion -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java deleted file mode 100644 index d845ec4d..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java +++ /dev/null @@ -1,47 +0,0 @@ -package edu.illinois.covid.exposure.ble; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; - -import androidx.core.app.NotificationCompat; -import edu.illinois.covid.MainActivity; -import edu.illinois.covid.R; - -class NotificationCreator { - private static final int ONGOING_NOTIFICATION_ID = 1; - private static final String CHANNEL_ID = "RokwireContactTracingNotificationChannel"; - private static Notification notification; - - static Notification getNotification(Context context) { - if (context != null) { - if (notification == null) { - Intent notificationIntent = new Intent(context, MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity( - context, - 0, - notificationIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(context.getString(R.string.exposure_notification_title)) - .setSmallIcon(R.drawable.app_icon) - .setContentIntent(pendingIntent) - .setOnlyAlertOnce(true) - .setTicker(context.getString(R.string.exposure_notification_ticker)); - - notification = builder.build(); - } - } - return notification; - } - - static String getChannelId() { - return CHANNEL_ID; - } - - static int getOngoingNotificationId() { - return ONGOING_NOTIFICATION_ID; - } -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/ExposureBleReceiver.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/ExposureBleReceiver.java deleted file mode 100644 index 5a0327d7..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/ExposureBleReceiver.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure.ble.scan; - -import android.annotation.TargetApi; -import android.bluetooth.le.ScanResult; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; - -import java.util.ArrayList; - -import edu.illinois.covid.Constants; -import edu.illinois.covid.exposure.ble.ExposureClient; - -public class ExposureBleReceiver extends BroadcastReceiver { - - private static final String TAG = "ExposureBleReceiver"; - - @TargetApi(Build.VERSION_CODES.O) - @Override - public void onReceive(Context context, Intent intent) { - if (intent == null) { - Log.d(TAG, "onReceive - intent is null"); - return; - } - Log.d(TAG, "onReceive - " + intent.getAction()); - if (Constants.EXPOSURE_BLE_ACTION_FOUND.equals(intent.getAction())) { - ScanResult scanResult = extractData(intent.getExtras()); - if (scanResult == null) { - Log.d(TAG, "The scan result is null"); - } - Intent bleClientIntent = new Intent(context, ExposureClient.class); - bleClientIntent.putExtra(Constants.EXPOSURE_BLE_DEVICE_FOUND, scanResult); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Check if local notifications are enabled for Android. We can start foreground service only if they are enabled. Required for API >= 26 - boolean canStartClientService = (ExposureClient.getInstance() != null) && ExposureClient.getInstance().exposureServiceLocalNotificationEnabled(); - if (canStartClientService) { - context.startForegroundService(bleClientIntent); - } - } else { - context.startService(bleClientIntent); - } - } - } - - private ScanResult extractData(Bundle extras) { - if (extras != null) { - Object list = extras.get("android.bluetooth.le.extra.LIST_SCAN_RESULT"); - if (list != null) { - ArrayList l = (ArrayList) list; - if (l.size() > 0) { - Object firstItem = l.get(0); - if (firstItem instanceof ScanResult) { - return (ScanResult) firstItem; - } else { - Log.d(TAG, "first item is not ScanResult"); - } - } else { - Log.d(TAG, "list is empty"); - } - } else { - Log.d(TAG, "list is null"); - } - } else { - Log.d(TAG, "extras are null"); - } - return null; - } -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/OreoScanner.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/OreoScanner.java deleted file mode 100644 index 36754fe9..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/OreoScanner.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure.ble.scan; - -import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanSettings; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.ParcelUuid; -import android.util.Log; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import androidx.annotation.RequiresApi; -import edu.illinois.covid.Constants; - -public class OreoScanner { - - private static final String TAG = "OreoScanner"; - - private Context context; - private BluetoothAdapter bluetoothAdapter; - - private PendingIntent pendingIntent; - - public OreoScanner(Context context, BluetoothAdapter bluetoothAdapter) { - this.context = context; - this.bluetoothAdapter = bluetoothAdapter; - } - - @RequiresApi(api = Build.VERSION_CODES.O) - public void startScan() { - Log.d(TAG, "Started scan"); - try { - ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder(); - scanFilterBuilder.setServiceUuid(new ParcelUuid(Constants.EXPOSURE_UUID_SERVICE)); - List scanFilters = Collections.singletonList(scanFilterBuilder.build()); - long reportDelay = ((bluetoothAdapter != null) && bluetoothAdapter.isOffloadedScanBatchingSupported()) ? 5 : 0; - - ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(). - setScanMode(ScanSettings.SCAN_MODE_LOW_POWER). - setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES). - setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE). - setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT). - setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED). - setReportDelay(TimeUnit.SECONDS.toMillis(reportDelay)). - setLegacy(true); - - ScanSettings scanSettings = scanSettingsBuilder.build(); - - Intent intent = new Intent(context, ExposureBleReceiver.class); - intent.setAction(Constants.EXPOSURE_BLE_ACTION_FOUND); - pendingIntent = PendingIntent.getBroadcast(context, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT); - - if ((pendingIntent != null) && (bluetoothAdapter != null)) { - BluetoothLeScanner bleScanner = bluetoothAdapter.getBluetoothLeScanner(); - if (bleScanner != null) { - bleScanner.startScan(scanFilters, scanSettings, pendingIntent); - } - } - } catch (Exception ex) { - Log.e(TAG, "Start scan failed:"); - ex.printStackTrace(); - //re-try - startScan(); - } - } - - @RequiresApi(api = Build.VERSION_CODES.O) - public void stopScan() { - if (pendingIntent != null) { - // Check if bluetooth adapter is not null and is turned on. - BluetoothLeScanner bleScanner = ((bluetoothAdapter != null) && bluetoothAdapter.isEnabled() && (bluetoothAdapter.getState() == BluetoothAdapter.STATE_ON)) ? bluetoothAdapter.getBluetoothLeScanner() : null; - if (bleScanner != null) { - bleScanner.stopScan(pendingIntent); - } - pendingIntent = null; - } - } -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/PreOreoScanner.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/PreOreoScanner.java deleted file mode 100644 index 22f22e49..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/PreOreoScanner.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure.ble.scan; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; -import android.os.ParcelUuid; -import android.util.Log; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import edu.illinois.covid.Constants; - -public class PreOreoScanner { - - private static final String TAG = PreOreoScanner.class.getSimpleName(); - - private BluetoothAdapter bluetoothAdapter; - - private ScannerCallback discoverCallback; - - public PreOreoScanner(BluetoothAdapter bluetoothAdapter) { - this.bluetoothAdapter = bluetoothAdapter; - } - - public void startScan(ScannerCallback callback) { - discoverCallback = callback; - - // Use try catch to handle DeadObject exception - try { - ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder(); - scanFilterBuilder.setServiceUuid(new ParcelUuid(Constants.EXPOSURE_UUID_SERVICE)); - List scanFilters = Collections.singletonList(scanFilterBuilder.build()); - long reportDelay = ((bluetoothAdapter != null) && bluetoothAdapter.isOffloadedScanBatchingSupported()) ? 5 : 0; - - ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(). - setScanMode(ScanSettings.SCAN_MODE_LOW_POWER). - setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES). - setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE). - setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT). - setReportDelay(TimeUnit.SECONDS.toMillis(reportDelay)); - - ScanSettings scanSettings = scanSettingsBuilder.build(); - if (bluetoothAdapter != null) { - bluetoothAdapter.getBluetoothLeScanner().startScan(scanFilters, scanSettings, scanCallback); - } - Log.d(TAG, "Started scan"); - } catch (Exception ex) { - Log.e(TAG, "Start scan failed:"); - ex.printStackTrace(); - // re-try - startScan(callback); - } - } - - public void stopScan() { - if (discoverCallback != null) { - // Check if bluetooth adapter is not null and is turned on. - BluetoothLeScanner bleScanner = ((bluetoothAdapter != null) && bluetoothAdapter.isEnabled() && (bluetoothAdapter.getState() == BluetoothAdapter.STATE_ON)) ? bluetoothAdapter.getBluetoothLeScanner() : null; - if (bleScanner != null) { - bleScanner.stopScan(scanCallback); - } - discoverCallback = null; - } - } - - private void onResult(final ScanResult result) { - if (discoverCallback != null) - discoverCallback.onDevice(result); - } - - private ScanCallback scanCallback = new ScanCallback() { - @Override - public void onScanResult(int callbackType, final ScanResult result) { - super.onScanResult(callbackType, result); - onResult(result); - } - - @Override - public void onBatchScanResults(List results) { - super.onBatchScanResults(results); - for (ScanResult result : results) { - onResult(result); - } - } - - @Override - public void onScanFailed(int errorCode) { - super.onScanFailed(errorCode); - Log.e(TAG, "onScanFailed errorCode = " + errorCode); - if (errorCode == SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) { - // re-try - startScan(discoverCallback); - } - } - - }; - - //ScannerCallback - - public static abstract class ScannerCallback { - public void onDevice(ScanResult result) { - } - } -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES.java b/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES.java deleted file mode 100644 index 3ebf6eec..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure.crypto; - -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; - -public class AES { - - private static SecretKeySpec secretKey; - private static byte[] key; - - public static void setKey(byte[] myKey) { - key = myKey; - secretKey = new SecretKeySpec(key, "AES"); - } - - public static byte[] encrypt(byte[] strToEncrypt, byte[] secret) { - try { - setKey(strToEncrypt); - Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - return (cipher.doFinal(secret)); - } catch (Exception e) { - System.out.println("Error while encrypting: " + e.toString()); - } - return null; - } - - public static byte[] decrypt(byte[] strToDecrypt, byte[] secret) { - try { - setKey(strToDecrypt); - Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKey); - return (cipher.doFinal(secret)); - } catch (Exception e) { - System.out.println("Error while decrypting: " + e.toString()); - } - return null; - } -} diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES_CTR.java b/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES_CTR.java deleted file mode 100644 index 2d75dc9d..00000000 --- a/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES_CTR.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package edu.illinois.covid.exposure.crypto; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -public class AES_CTR { - public static String ALGORITHM = "AES"; - private static String AES_CBS_PADDING = "AES/CTR/NoPadding"; - - public static byte[] encrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception { - return AES_CTR.encryptDecrypt(Cipher.ENCRYPT_MODE, key, IV, message); - } - - public static byte[] decrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception { - return AES_CTR.encryptDecrypt(Cipher.DECRYPT_MODE, key, IV, message); - } - - private static byte[] encryptDecrypt(final int mode, final byte[] key, final byte[] IV, final byte[] message) - throws Exception { - final Cipher cipher = Cipher.getInstance(AES_CBS_PADDING); - final SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM); - final IvParameterSpec ivSpec = new IvParameterSpec(IV); - cipher.init(mode, keySpec, ivSpec); - return cipher.doFinal(message); - } -} diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml index 67eb8ac3..8a8b48f8 100644 --- a/android/app/src/main/res/values-es/strings.xml +++ b/android/app/src/main/res/values-es/strings.xml @@ -44,10 +44,4 @@ Mapas - - - Sistema de notificación de exposición - Comprobación del sistema de notificación de exposición - Comprobación del sistema de notificación de exposición - Permitir notificaciones de exposición. ¿Quieres activar Bluetooth? \ No newline at end of file diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml index ce67bbc4..e12f13ee 100644 --- a/android/app/src/main/res/values-ja/strings.xml +++ b/android/app/src/main/res/values-ja/strings.xml @@ -44,10 +44,4 @@ マップ - - - 接触通知システム - 接触通知システムのチェック - 接触通知システムのチェック - 接触通知を許可します。 Bluetoothをオンにしますか? diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml index b561ff51..d52eabbd 100644 --- a/android/app/src/main/res/values-zh/strings.xml +++ b/android/app/src/main/res/values-zh/strings.xml @@ -44,10 +44,4 @@ 地圖 - - - 曝光通知系統 - 曝光通知系統檢查 - 曝光通知系統檢查 - 允許曝光通知. 您要開啟藍牙嗎? \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index e3ed8c59..76b4fa95 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -44,10 +44,4 @@ Maps - - - Exposure Notification system - Exposure Notification system checking - Exposure Notification system checking - Allow exposure notifications. Do you want to turn on Bluetooth? \ No newline at end of file diff --git a/assets/flexUI.json b/assets/flexUI.json index d869212e..e1ee7dce 100644 --- a/assets/flexUI.json +++ b/assets/flexUI.json @@ -1,7 +1,6 @@ { "content": { "onboarding": ["get_started", "organization", "health_disclosure", - "notifications_auth", "location_auth", "bluetooth_auth", "roles", "login_netid", "login_phone", "verify_phone", "confirm_phone", "health_intro", "health_how_it_works", "health_consent", "health_qrcode", "health_final"], @@ -18,7 +17,7 @@ "settings.connected.netid": ["info", "disconnect", "connect"], "settings.connected.phone": ["info", "disconnect", "verify"], "settings.notifications": ["covid19"], - "settings.covid19": ["exposure_notifications", "provider_test_result", "provider_vaccine_info", "qr_code"], + "settings.covid19": ["provider_test_result", "provider_vaccine_info", "qr_code"], "settings.privacy": ["statement"], "settings.account": ["personal_info", "family_members"], diff --git a/assets/health.rules.json b/assets/health.rules.json index 88204d79..105c14ea 100644 --- a/assets/health.rules.json +++ b/assets/health.rules.json @@ -10,6 +10,7 @@ "DefaultTestMonitorInterval": 4, "UndergraduateTestMonitorInterval": 2, "UserTestMonitorInterval": null, + "VaccineBoosterInterval": 180, "TestMonitorInterval": { "condition": "test-interval", "params": { "interval": "UserTestMonitorInterval" }, @@ -19,24 +20,7 @@ "success": "UndergraduateTestMonitorInterval", "fail": "DefaultTestMonitorInterval" } - }, - - "DefaultTestMonitorWeekdaysExtent": ["Fri", "Sat", "Sun", "Mon"], - "UndergraduateTestMonitorWeekdaysExtent": ["Sat", "Sun"], - - "TestMonitorWeekdaysExtent": { - "condition": "test-user", "params": { "card.role": "Undergraduate", "card.student_level": "1U" }, - "success": "UndergraduateTestMonitorWeekdaysExtent", - "fail": "DefaultTestMonitorWeekdaysExtent" - }, - - "Mon": 1, - "Tue": 2, - "Wed": 3, - "Thu": 4, - "Fri": 5, - "Sat": 6, - "Sun": 7 + } }, "constants": { @@ -71,8 +55,7 @@ "color": "#FF4F4F", "name": "code.red.name", "description": "code.red.description", - "long_description": "code.red.long_description", - "reports_exposures": true + "long_description": "code.red.long_description" } ], "info": [ @@ -97,7 +80,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -140,7 +123,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -229,7 +212,7 @@ "test-monitor": { "condition": "require-test", "params": { - "interval": { "min": 0, "max": "TestMonitorInterval", "max-weekdays-extent": "TestMonitorWeekdaysExtent", "scope": "future", "current": true } + "interval": { "min": 0, "max": "TestMonitorInterval", "scope": "future", "current": true } }, "success": "test-fulfilled", "fail": "test-required" @@ -239,7 +222,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -272,7 +255,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -334,6 +317,22 @@ "interval": "UserTestMonitorInterval" }, "success": "vaccinated-suspended", + "fail": { + "condition": "test-interval", + "params": { + "interval": "VaccineBoosterInterval" + }, + "success": "vaccinated-booster", + "fail": "vaccinated" + } + }, + + "vaccinated-booster": { + "condition": "timeout", + "params": { + "interval": { "min": 0, "max": "VaccineBoosterInterval", "scope": "future" } + }, + "success": null, "fail": "vaccinated" }, @@ -346,6 +345,15 @@ "fcm_topic": "vaccinated" }, + "vaccinated-restore": { + "condition": "timeout", + "params": { + "interval": { "min": 0, "max": "VaccineBoosterInterval", "scope": "future" } + }, + "success": null, + "fail": "vaccinated-force" + }, + "vaccinated-force": { "code": "green", "priority": -4, @@ -362,7 +370,7 @@ "notice": "vaccinated.suspended.notice", "reason": "vaccinated.suspended.reason" }, - + "quarantine-on": { "code": "orange", "priority": 10, @@ -375,7 +383,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -383,7 +391,7 @@ "interval": "UserTestMonitorInterval" }, "success": "quarantine-off.unvaccinated", - "fail": "vaccinated-force" + "fail": "vaccinated-restore" }, "fail": "quarantine-off.unvaccinated" }, @@ -460,7 +468,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -522,7 +530,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -530,7 +538,7 @@ "interval": "UserTestMonitorInterval" }, "success": "exempt-off.unvaccinated", - "fail": "vaccinated-force" + "fail": "vaccinated-restore" }, "fail": "exempt-off.unvaccinated" }, @@ -546,7 +554,7 @@ "condition": "require-vaccine", "params": { "interval": { "scope": "past" }, - "vaccine": "effective" + "status": "effective" }, "success": { "condition": "test-interval", @@ -554,7 +562,7 @@ "interval": "UserTestMonitorInterval" }, "success": "release.unvaccinated", - "fail": "vaccinated-force" + "fail": "vaccinated-restore" }, "fail": "release.unvaccinated" }, @@ -951,7 +959,7 @@ "vaccines": { "rules": [ { - "vaccine": "effective", + "vaccine_status": "effective", "status": "vaccinated-handler" } ] diff --git a/assets/strings.en.json b/assets/strings.en.json index 43ced0b2..f04fe6ab 100644 --- a/assets/strings.en.json +++ b/assets/strings.en.json @@ -57,36 +57,6 @@ "panel.onboarding.get_started.image.safer_in_illinois.title": "Safer in Illinois", "panel.onboarding.get_started.image.powered.title": "Powered by Rokwire", - "panel.onboarding.location.label.title": "Turn on Location Services", - "panel.onboarding.location.label.title.hint": "Header 1", - "panel.onboarding.location.label.description": "Background location is required for Bluetooth-based exposure notification to work on your phone", - "panel.onboarding.location.button.allow.title": "Enable Location Services", - "panel.onboarding.location.button.allow.hint": "", - "panel.onboarding.location.button.dont_allow.title": "Not right now", - "panel.onboarding.location.button.dont_allow.hint": "Skip sharing location", - "panel.onboarding.location.label.access_granted": "You have already granted access to this app.", - "panel.onboarding.location.label.access_denied": "You have already denied access to this app.", - - "panel.onboarding.bluetooth.label.title": "Enable Bluetooth", - "panel.onboarding.bluetooth.label.title.hint": "Header 1", - "panel.onboarding.bluetooth.label.description": "Use Bluetooth to alert you to potential exposure to COVID-19.", - "panel.onboarding.bluetooth.button.allow.title": "Enable Bluetooth", - "panel.onboarding.bluetooth.button.allow.hint": "", - "panel.onboarding.bluetooth.button.dont_allow.title": "Not right now", - "panel.onboarding.bluetooth.button.dont_allow.hint": "Skip enabling Bluetooth", - "panel.onboarding.bluetooth.label.access_granted": "You have already granted access to this app.", - "panel.onboarding.bluetooth.label.access_denied": "You have already denied access to this app.", - - "panel.onboarding.notifications.label.title": "Info when you need it", - "panel.onboarding.notifications.label.title.hint": "Header 1", - "panel.onboarding.notifications.label.description1": "Get notified about COVID-19 info", - "panel.onboarding.notifications.label.description2": "This is required for Exposure Notifications to work in background on your phone", - "panel.onboarding.notifications.button.allow.title": "Enable Notifications", - "panel.onboarding.notifications.button.allow.hint": "", - "panel.onboarding.notifications.button.dont_allow.title": "Not right now", - "panel.onboarding.notifications.button.dont_allow.hint": "Skip receiving notifications", - "panel.onboarding.notifications.label.access_granted": "Your settings have been changed.", - "panel.onboarding.roles.label.title": "Who are you?", "panel.onboarding.roles.label.title.hint": "Header 1", "panel.onboarding.roles.label.description": "Select one", @@ -215,7 +185,6 @@ "panel.settings.home.button.test.hint": "", "panel.settings.home.button.get_help.title": "Get Help", "panel.settings.home.button.get_help.hint": "", - "panel.settings.home.covid19.exposure_notifications": "Exposure Notifications", "panel.settings.home.covid19.provider_test_result": "Health Provider Test Results", "panel.settings.home.covid19.provider_vaccine_info": "Health Provider Vaccine Information", "panel.settings.home.covid19.title": "COVID-19", @@ -343,6 +312,12 @@ "panel.covid19home.vaccination.vaccinated.effective.0.description": "Your vaccine will be effective today.", "panel.covid19home.vaccination.vaccinated.effective.1.description": "Your vaccine will be effective tomorrow.", "panel.covid19home.vaccination.vaccinated.effective.n.description": "Your vaccine will be effective after %s days.", + "panel.covid19home.vaccination.expired.title": "Vaccine еxpired", + "panel.covid19home.vaccination.expired.description": "Get a booster dose now.", + "panel.covid19home.vaccination.exempt.title": "Exempt from vaccination", + "panel.covid19home.vaccination.exempt.description": "You are currently exempt from taking COVID-19 vaccines but are required to continue taking tests.", + "panel.covid19home.vaccination.suspended.title": "Vaccination status suspended", + "panel.covid19home.vaccination.suspended.description": "You are currently effectively vaccinated but are required to continue taking tests until further notice.", "panel.covid19home.vaccination.button.appointment.title": "Make an appointment", "panel.covid19home.vaccination.button.appointment.hint": "", @@ -433,18 +408,15 @@ "panel.health.onboarding.covid19.how_it_works.line2.title": "You can use this app to:", "panel.health.onboarding.covid19.how_it_works.line3.title": "Self-report your COVID-19 symptoms and in doing so update your status.", "panel.health.onboarding.covid19.how_it_works.line4.title": "Automatically receive test results and vaccine information from your healthcare provider.", - "panel.health.onboarding.covid19.how_it_works.line5.title": "Allow your phone to send exposure notifications when you’ve been in proximity to people who test positive.", "panel.health.onboarding.covid19.how_it_works.button.next.title": "Next", "panel.health.onboarding.covid19.how_it_works.button.next.hint": "", "panel.health.onboarding.covid19.disclosure.label.title": "Information Usage Disclosure", "panel.health.onboarding.covid19.disclosure.label.description1": "The Safer Illinois app uses:", "panel.health.onboarding.covid19.disclosure.label.splitter1": ". ", - "panel.health.onboarding.covid19.disclosure.label.content1": "1. Bluetooth to enable opt-in exposure notifications of close contact with individuals that test positive for COVID-19.", - "panel.health.onboarding.covid19.disclosure.label.content2": "2. Camera to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content3": "3. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content4": "4. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content5": "5. Location services on your device must be turned on to activate the Bluetooth low energy technology necessary for the exposure notification function of the Application to work in the background. However, the Application does not access, collect, or store any location data, including GPS data. If location services on your device are turned off, the Application will perform the limited functions of storing and providing information about COVID-19 test results, any voluntarily reported symptoms, and building access status.", + "panel.health.onboarding.covid19.disclosure.label.content1": "1. Camera to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content2": "2. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content3": "3. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.description2": "YOUR INFORMATION AND HOW WE USE IT", "panel.health.onboarding.covid19.disclosure.label.description3": "Information we need to provide you COVID-19 test results", "panel.health.onboarding.covid19.disclosure.label.content8": "Name, email address, University ID number (UIN), phone number, and student registration information", @@ -457,20 +429,12 @@ "panel.health.onboarding.covid19.disclosure.label.content12": " - CAMERA", "panel.health.onboarding.covid19.disclosure.label.content13": " - WRITE_EXTERNAL_STORAGE", "panel.health.onboarding.covid19.disclosure.label.content14": " - READ_EXTERNAL_STORAGE", - "panel.health.onboarding.covid19.disclosure.label.description8": "Opt-in exposure notification participation", - "panel.health.onboarding.covid19.disclosure.label.content18": "Your phone transmits and receives anonymous identifying numbers via Bluetooth. This identifier is stored on any phones that come close to you. If someone tests positive for COVID-19, their phone tells our servers the anonymous numbers they have sent for the last 14 days. Your phone will check if you were near that infected users' phone long enough to warrant an exposure notification. This is all done anonymously. Your location is never tracked or stored on our servers. If you have elected to use Exposure Notification, Location Services, and Bluetooth, these capabilities will run in the background when the app is not in use. The following permissions are used to enable this:", - "panel.health.onboarding.covid19.disclosure.label.content19": " - BLUETOOTH", - "panel.health.onboarding.covid19.disclosure.label.content20": " - BLUETOOTH_ADMIN", - "panel.health.onboarding.covid19.disclosure.label.content21": " - LOCATION", "panel.health.onboarding.covid19.disclosure.check_box.label.acknowledge": "Acknowledge", "panel.health.onboarding.covid19.disclosure.button.disclosure.title": "Next", "panel.health.onboarding.covid19.disclosure.button.scroll_to_continue.title": "Scroll to Continue", "panel.health.onboarding.covid19.disclosure.button.disclosure.hint": "", "panel.health.onboarding.covid19.consent.label.title": "Consent for COVID-19 features", - "panel.health.onboarding.covid19.consent.label.description": "Exposure Notifications", - "panel.health.onboarding.covid19.consent.label.content1": "If you consent to exposure notifications, you allow your phone to send an anonymous Bluetooth signal to nearby Safer Illinois app users who are also using this feature. Your phone will receive and record a signal from their phones as well. If one of those users tests positive for COVID-19 in the next 14 days, the app will alert you to your potential exposure and advise you on next steps. Your identity and health status will remain anonymous, as will the identity and health status of all other users.", - "panel.health.onboarding.covid19.consent.check_box.label.exposure": "I consent to participate in the Exposure Notification System (requires Bluetooth to be ON).", "panel.health.onboarding.covid19.consent.check_box.label.test": "I consent to allow my healthcare provider to provide my test results.", "panel.health.onboarding.covid19.consent.check_box.label.vaccine": "I consent to allow my healthcare provider to provide my vaccine information.", "panel.health.onboarding.covid19.consent.label.content2": "Automatic Test Results", diff --git a/assets/strings.es.json b/assets/strings.es.json index b808570d..661bac52 100644 --- a/assets/strings.es.json +++ b/assets/strings.es.json @@ -57,36 +57,6 @@ "panel.onboarding.get_started.image.safer_in_illinois.title": "Más seguro en Illinois", "panel.onboarding.get_started.image.powered.title": "Desarrollado por Rokwire", - "panel.onboarding.location.label.title": "Activar los servicios de ubicación", - "panel.onboarding.location.label.hint": "Encabezado 1", - "panel.onboarding.location.label.description": "Se requiere una ubicación en segundo plano para que la notificación de exposición basada en Bluetooth funcione en su teléfono ", - "panel.onboarding.location.button.allow.title": "Servicio de localización activado", - "panel.onboarding.location.button.allow.hint": "", - "panel.onboarding.location.button.dont_allow.title": "No en este momento", - "panel.onboarding.location.button.dont_allow.hint": "Omitir ubicación para compartir", - "panel.onboarding.location.label.access_granted": "Ya ha otorgado acceso a esta aplicación", - "panel.onboarding.location.label.access_denied": "Ya ha denegado el acceso a esta aplicación", - - "panel.onboarding.bluetooth.label.title": "Habilitar Bluetooth", - "panel.onboarding.bluetooth.label.title.hint": "Encabezado 1", - "panel.onboarding.bluetooth.label.description": "Usar Bluetooth para alertarlo sobre la posible exposición al COVID-19.", - "panel.onboarding.bluetooth.button.allow.title": "Habilitar Bluetooth", - "panel.onboarding.bluetooth.button.allow.hint": "", - "panel.onboarding.bluetooth.button.dont_allow.title": "No en este momento", - "panel.onboarding.bluetooth.button.dont_allow.hint": "Omitir habilitar Bluetooth", - "panel.onboarding.bluetooth.label.access_granted": "Ya ha otorgado acceso a esta aplicación", - "panel.onboarding.bluetooth.label.access_denied": "Ya ha denegado el acceso a esta aplicación", - - "panel.onboarding.notifications.label.title": "Información cuando la necesitas", - "panel.onboarding.notifications.label.hint": "Encabezado 1", - "panel.onboarding.notifications.label.description1": "Reciba notificaciones sobre la información de COVID-19", - "panel.onboarding.notifications.label.description2": "Esto es necesario para que las notificaciones de exposición funcionen en segundo plano en su teléfono", - "panel.onboarding.notifications.button.allow.title": "Permitir notificaciones", - "panel.onboarding.notifications.button.allow.hint": "", - "panel.onboarding.notifications.button.dont_allow.title": "No en este momento", - "panel.onboarding.notifications.button.dont_allow.hint": "Omitir recibir notificaciones", - "panel.onboarding.notifications.label.access_granted": "Su configuración ha sido cambiada.", - "panel.onboarding.roles.label.title": "Quién eres", "panel.onboarding.roles.label.title.hint": "Encabezado 1", "panel.onboarding.roles.label.description": "Select one", @@ -215,7 +185,6 @@ "panel.settings.home.button.test.hint": "", "panel.settings.home.button.get_help.title": "Consigue Ayuda", "panel.settings.home.button.get_help.hint": "", - "panel.settings.home.covid19.exposure_notifications": "Notificaciones de exposición", "panel.settings.home.covid19.provider_test_result": "Resultados de la prueba del proveedor de salud", "panel.settings.home.covid19.provider_vaccine_info": "Información sobre vacunas del proveedor de salud", "panel.settings.home.covid19.title": "COVID-19", @@ -343,6 +312,12 @@ "panel.covid19home.vaccination.vaccinated.effective.0.description": "Su vacuna será efectiva hoy.", "panel.covid19home.vaccination.vaccinated.effective.1.description": "Su vacuna será efectiva mañana.", "panel.covid19home.vaccination.vaccinated.effective.n.description": "Su vacuna será efectiva después de %s días.", + "panel.covid19home.vaccination.expired.title": "Vacuna caducada", + "panel.covid19home.vaccination.expired.description": "Obtenga una dosis de refuerzo ahora.", + "panel.covid19home.vaccination.exempt.title": "Exenta de vacunación", + "panel.covid19home.vaccination.exempt.description": "Actualmente está exento de recibir las vacunas COVID-19, pero debe continuar realizando las pruebas.", + "panel.covid19home.vaccination.suspended.title": "Estado de vacunación suspendido", + "panel.covid19home.vaccination.suspended.description": "Actualmente está vacunado de manera efectiva, pero debe continuar realizando las pruebas hasta nuevo aviso.", "panel.covid19home.vaccination.button.appointment.title": "Haga una cita", "panel.covid19home.vaccination.button.appointment.hint": "", @@ -433,18 +408,15 @@ "panel.health.onboarding.covid19.how_it_works.line2.title": "Proporcione cualquier síntoma de COVID-19 que esté experimentando, reciba o ingrese automáticamente los resultados de las pruebas de su proveedor de atención médica, y permita que su teléfono le envíe notificaciones de exposición a usted y a las personas con las que ha estado en contacto durante los últimos 14 días.", "panel.health.onboarding.covid19.how_it_works.line3.title": "Autoinforme sus síntomas de COVID-19 y, al hacerlo, actualice su estado.", "panel.health.onboarding.covid19.how_it_works.line4.title": "Reciba automáticamente los resultados de las pruebas y la información de la vacuna de su proveedor de atención médica.", - "panel.health.onboarding.covid19.how_it_works.line5.title": "Permita que su teléfono envíe notificaciones de exposición cuando haya estado cerca de personas que dieron positivo en la prueba.", "panel.health.onboarding.covid19.how_it_works.button.next.title": "Próximo", "panel.health.onboarding.covid19.how_it_works.button.next.hint": "", "panel.health.onboarding.covid19.disclosure.label.title": "Information Usage Disclosure", "panel.health.onboarding.covid19.disclosure.label.description1": "The Safer Illinois app uses:", "panel.health.onboarding.covid19.disclosure.label.splitter1": ". ", - "panel.health.onboarding.covid19.disclosure.label.content1": "1. Bluetooth to enable opt-in exposure notifications of close contact with individuals that test positive for COVID-19.", - "panel.health.onboarding.covid19.disclosure.label.content2": "2. Camera to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content3": "3. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content4": "4. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content5": "5. Location services on your device must be turned on to activate the Bluetooth low energy technology necessary for the exposure notification function of the Application to work in the background. However, the Application does not access, collect, or store any location data, including GPS data. If location services on your device are turned off, the Application will perform the limited functions of storing and providing information about COVID-19 test results, any voluntarily reported symptoms, and building access status.", + "panel.health.onboarding.covid19.disclosure.label.content1": "1. Camera to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content2": "2. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content3": "3. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.description2": "YOUR INFORMATION AND HOW WE USE IT", "panel.health.onboarding.covid19.disclosure.label.description3": "Information we need to provide you COVID-19 test results", "panel.health.onboarding.covid19.disclosure.label.content8": "Name, email address, University ID number (UIN), phone number, and student registration information", @@ -457,20 +429,12 @@ "panel.health.onboarding.covid19.disclosure.label.content12": " - CAMERA", "panel.health.onboarding.covid19.disclosure.label.content13": " - WRITE_EXTERNAL_STORAGE", "panel.health.onboarding.covid19.disclosure.label.content14": " - READ_EXTERNAL_STORAGE", - "panel.health.onboarding.covid19.disclosure.label.description8": "Opt-in exposure notification participation", - "panel.health.onboarding.covid19.disclosure.label.content18": "Your phone transmits and receives anonymous identifying numbers via Bluetooth. This identifier is stored on any phones that come close to you. If someone tests positive for COVID-19, their phone tells our servers the anonymous numbers they have sent for the last 14 days. Your phone will check if you were near that infected users' phone long enough to warrant an exposure notification. This is all done anonymously. Your location is never tracked or stored on our servers. If you have elected to use Exposure Notification, Location Services, and Bluetooth, these capabilities will run in the background when the app is not in use. The following permissions are used to enable this:", - "panel.health.onboarding.covid19.disclosure.label.content19": " - BLUETOOTH", - "panel.health.onboarding.covid19.disclosure.label.content20": " - BLUETOOTH_ADMIN", - "panel.health.onboarding.covid19.disclosure.label.content21": " - LOCATION", "panel.health.onboarding.covid19.disclosure.check_box.label.acknowledge": "Acknowledge", "panel.health.onboarding.covid19.disclosure.button.disclosure.title": "Next", "panel.health.onboarding.covid19.disclosure.button.scroll_to_continue.title": "Scroll to Continue", "panel.health.onboarding.covid19.disclosure.button.disclosure.hint": "", "panel.health.onboarding.covid19.consent.label.title": "Consentimientos especiales para las características de COVID-19", - "panel.health.onboarding.covid19.consent.label.description": "Notificaciones de exposición", - "panel.health.onboarding.covid19.consent.label.content1": "Si acepta las notificaciones de exposición, permite que su teléfono envíe una señal anónima de Bluetooth a los usuarios cercanos de la aplicación Safer Illinois que también usan esta función. Su teléfono también recibirá y grabará una señal de sus teléfonos. Si uno de esos usuarios da positivo por COVID-19 en los próximos 14 días, la aplicación lo alertará sobre su posible exposición y le aconsejará sobre los próximos pasos. Su identidad y estado de salud permanecerán anónimos, al igual que la identidad y el estado de salud de todos los demás usuarios.", - "panel.health.onboarding.covid19.consent.check_box.label.exposure": "Doy mi consentimiento para participar en el Sistema de notificación de exposición (requiere que Bluetooth esté activado).", "panel.health.onboarding.covid19.consent.check_box.label.test": "Doy mi consentimiento para que mi proveedor de atención médica proporcione los resultados de mi prueba.", "panel.health.onboarding.covid19.consent.check_box.label.vaccine": "Doy mi consentimiento para que mi proveedor de atención médica proporcione la información de mi vacuna.", "panel.health.onboarding.covid19.consent.label.content2": "Resultados automáticos de prueba", diff --git a/assets/strings.ja.json b/assets/strings.ja.json index 229d5606..daf83db9 100644 --- a/assets/strings.ja.json +++ b/assets/strings.ja.json @@ -57,36 +57,6 @@ "panel.onboarding.get_started.image.safer_in_illinois.title": "Safer Illinois", "panel.onboarding.get_started.image.powered.title": "Powered by Rokwire", - "panel.onboarding.location.label.title": "位置情報サービスをオンにする", - "panel.onboarding.location.label.title.hint": "見出し1", - "panel.onboarding.location.label.description": "Bluetoothに基づく接続通知をスマホで有効にするためにバックグラウンド位置は必要です", - "panel.onboarding.location.button.allow.title": "位置情報サービスを許可する", - "panel.onboarding.location.button.allow.hint": "", - "panel.onboarding.location.button.dont_allow.title": "今はしない", - "panel.onboarding.location.button.dont_allow.hint": "Skip sharing location", - "panel.onboarding.location.label.access_granted": "You have already granted access to this app.", - "panel.onboarding.location.label.access_denied": "You have already denied access to this app.", - - "panel.onboarding.bluetooth.label.title": "Bluetoothを有効にする", - "panel.onboarding.bluetooth.label.title.hint": "見出し1", - "panel.onboarding.bluetooth.label.description": "COVID-19へ接触した可能性がある警告をBluetoothで受け取る。", - "panel.onboarding.bluetooth.button.allow.title": "Bluetoothを有効にする", - "panel.onboarding.bluetooth.button.allow.hint": "", - "panel.onboarding.bluetooth.button.dont_allow.title": "今はしない", - "panel.onboarding.bluetooth.button.dont_allow.hint": "Skip enabling Bluetooth", - "panel.onboarding.bluetooth.label.access_granted": "You have already granted access to this app.", - "panel.onboarding.bluetooth.label.access_denied": "You have already denied access to this app.", - - "panel.onboarding.notifications.label.title": "必要な時に情報をもらう", - "panel.onboarding.notifications.label.title.hint": "見出し1", - "panel.onboarding.notifications.label.description1": "COVID-19情報の通知を受け取る", - "panel.onboarding.notifications.label.description2": "接続通知がスマホのバックグラウンドで実行するために必要です", - "panel.onboarding.notifications.button.allow.title": "通知を有効にする", - "panel.onboarding.notifications.button.allow.hint": "", - "panel.onboarding.notifications.button.dont_allow.title": "今はしない", - "panel.onboarding.notifications.button.dont_allow.hint": "Skip receiving notifications", - "panel.onboarding.notifications.label.access_granted": "Your settings have been changed.", - "panel.onboarding.roles.label.title": "どちら様ですか?", "panel.onboarding.roles.label.title.hint": "見出し1", "panel.onboarding.roles.label.description": "Select one", @@ -215,7 +185,6 @@ "panel.settings.home.button.test.hint": "", "panel.settings.home.button.get_help.title": "助けを得ます", "panel.settings.home.button.get_help.hint": "", - "panel.settings.home.covid19.exposure_notifications": "接続通知", "panel.settings.home.covid19.provider_test_result": "医療提供者のテスト結果", "panel.settings.home.covid19.provider_vaccine_info": "医療提供者のワクチン情報", "panel.settings.home.covid19.title": "COVID-19", @@ -343,6 +312,12 @@ "panel.covid19home.vaccination.vaccinated.effective.0.description": "あなたのワクチンは今日有効になります。", "panel.covid19home.vaccination.vaccinated.effective.1.description": "あなたのワクチンは明日有効になります。", "panel.covid19home.vaccination.vaccinated.effective.n.description": "あなたのワクチンは%s日後に有効になります。", + "panel.covid19home.vaccination.expired.title": "ワクチンの有効期限が切れました", + "panel.covid19home.vaccination.expired.description": "今すぐブースター用量を入手してください。", + "panel.covid19home.vaccination.exempt.title": "予防接種を免除", + "panel.covid19home.vaccination.exempt.description": "現在、COVID-19ワクチンの接種は免除されていますが、引き続き検査を受ける必要があります。", + "panel.covid19home.vaccination.suspended.title": "予防接種のステータスが一時停止されました", + "panel.covid19home.vaccination.suspended.description": "現在、効果的に予防接種を受けていますが、追って通知があるまで検査を続ける必要があります。", "panel.covid19home.vaccination.button.appointment.title": "予約する", "panel.covid19home.vaccination.button.appointment.hint": "", @@ -433,18 +408,15 @@ "panel.health.onboarding.covid19.how_it_works.line2.title": "このアプリでは以下のことができます。", "panel.health.onboarding.covid19.how_it_works.line3.title": "COVID-19の症状を自己申告してステータスを更新する。", "panel.health.onboarding.covid19.how_it_works.line4.title": "医療提供者から検査結果とワクチン情報を自動的に受け取ります。", - "panel.health.onboarding.covid19.how_it_works.line5.title": "検査陽性が出た人に濃厚接触した場合、接続通知を送らせようにする。", "panel.health.onboarding.covid19.how_it_works.button.next.title": "次へ", "panel.health.onboarding.covid19.how_it_works.button.next.hint": "", "panel.health.onboarding.covid19.disclosure.label.title": "個人情報利用の提供", "panel.health.onboarding.covid19.disclosure.label.description1": "Safer Illinois アプリは:", "panel.health.onboarding.covid19.disclosure.label.splitter1": ". ", - "panel.health.onboarding.covid19.disclosure.label.content1": "1. Bluetoothを使用して、陽性検査が出た人との濃厚接触の場合に、接続通知をもらえる機能を有効にする。", - "panel.health.onboarding.covid19.disclosure.label.content2": "2. 写真の使用でユーザーはアプリに自分の暗号化キー(QR コード)を取り込むことができます。", - "panel.health.onboarding.covid19.disclosure.label.content3": "3. 動画の使用でユーザーはアプリに自分の暗号化キー(QR コード)を取り込むことができます。", - "panel.health.onboarding.covid19.disclosure.label.content4": "4. ファイルス (外部ストレージの読み取りと書き込み) の使用でユーザーはアプリに自分の暗号化キー(QR コード)を取り込むことができます。", - "panel.health.onboarding.covid19.disclosure.label.content5": "5. アプリの接触通知がバックグランドで機能するためには、端末の位置情報サービスをオンにして、Bluetoothの省エネモードを有効にする必要があります。 しかし、アプリによって、GPSのデーターを含むいかなる位置情報へのアクセスや位置情報の収集や保存はされません。位置情報サービスをオフにした場合、COVID-19の検査結果の保存と情報提供や自己申告した症状、建物に出入りする許可のステータスなどに関するアプリの機能が制限されます。", + "panel.health.onboarding.covid19.disclosure.label.content1": "1. 写真の使用でユーザーはアプリに自分の暗号化キー(QR コード)を取り込むことができます。", + "panel.health.onboarding.covid19.disclosure.label.content2": "2. 動画の使用でユーザーはアプリに自分の暗号化キー(QR コード)を取り込むことができます。", + "panel.health.onboarding.covid19.disclosure.label.content3": "3. ファイルス (外部ストレージの読み取りと書き込み) の使用でユーザーはアプリに自分の暗号化キー(QR コード)を取り込むことができます。", "panel.health.onboarding.covid19.disclosure.label.description2": "あなたの情報の使えられる方法", "panel.health.onboarding.covid19.disclosure.label.description3": "検査結果をもらうために必要な情報", "panel.health.onboarding.covid19.disclosure.label.content8": "名前、メールアドレス、大学 ID 番号(UIN)、電話番号と学生登録情報", @@ -457,20 +429,12 @@ "panel.health.onboarding.covid19.disclosure.label.content12": " - カメラ", "panel.health.onboarding.covid19.disclosure.label.content13": " - 外部ストレージに書き込む", "panel.health.onboarding.covid19.disclosure.label.content14": " - 外部ストレージに読み取る", - "panel.health.onboarding.covid19.disclosure.label.description8": "接続通知に参加する", - "panel.health.onboarding.covid19.disclosure.label.content18": "携帯電話のBluetoothに経由で匿名である特定番号を送受信します。この匿名番号は、あなたの付近の携帯電話全てに自動的に保存されます。陽性結果が出た場合は、感染した人の携帯から直近14日間に送信した匿名番号がサーバーへ伝えられます。あなたの携帯は感染者の携帯の付近にいた時間を確認し 、必要に応じて接触通知が送られます。この全ては匿名で行われます。あなたの位置情報がサーバー上で追跡または保存されることは一切ありません。接触通知と位置情報サービスとBluetoothの使用を許可した場合、これらの機能はアプリが使われていない時にバックグラウンドで作動します。以下の許可で有効になります。", - "panel.health.onboarding.covid19.disclosure.label.content19": " - BLUETOOTH", - "panel.health.onboarding.covid19.disclosure.label.content20": " - BLUETOOTH_ADMIN", - "panel.health.onboarding.covid19.disclosure.label.content21": " - 位置情報", "panel.health.onboarding.covid19.disclosure.check_box.label.acknowledge": "認める", "panel.health.onboarding.covid19.disclosure.button.disclosure.title": "次へ", "panel.health.onboarding.covid19.disclosure.button.scroll_to_continue.title": "スクロールして続ける", "panel.health.onboarding.covid19.disclosure.button.disclosure.hint": "", "panel.health.onboarding.covid19.consent.label.title": "COVID-19機能に承諾", - "panel.health.onboarding.covid19.consent.label.description": "接続通知", - "panel.health.onboarding.covid19.consent.label.content1": "接続通知に承諾した場合は、スマホがこの機能を使用している近くのSafer Illinoisアプリのユーザーに匿名のBluetooth信号を送信するようになります。同様にスマホは、他のユーザーからの信号を受信して記録します。次の14日以内に他のユーザーがCOVID-19の陽性となった場合、アプリは曝露された可能性があることを警告して次のステップについてアドバイスします。すべてのユーザーの身元と健康ステータスは匿名となります。", - "panel.health.onboarding.covid19.consent.check_box.label.exposure": "接続通知システムに参加することに同意します(Bluetoothをオンにする必要があります)。", "panel.health.onboarding.covid19.consent.check_box.label.test": "私は、私の医療提供者が私の検査結果を提供することを許可することに同意します。", "panel.health.onboarding.covid19.consent.check_box.label.vaccine": "私は、私の医療提供者が私のワクチン情報を提供することを許可することに同意します。", "panel.health.onboarding.covid19.consent.label.content2": "自動検査結果", diff --git a/assets/strings.zh.json b/assets/strings.zh.json index b226aa63..d40145a2 100644 --- a/assets/strings.zh.json +++ b/assets/strings.zh.json @@ -57,36 +57,6 @@ "panel.onboarding.get_started.image.safer_in_illinois.title": "在伊利诺伊州更安全", "panel.onboarding.get_started.image.powered.title": "Powered by Rokwire", - "panel.onboarding.location.label.title": "打开位置服务", - "panel.onboarding.location.label.title.hint": "标题1", - "panel.onboarding.location.label.description": "要在手機上使用基於藍牙的曝光通知,需要背景位置", - "panel.onboarding.location.button.allow.title": "啟用位置服務", - "panel.onboarding.location.button.allow.hint": "", - "panel.onboarding.location.button.dont_allow.title": "暂时不", - "panel.onboarding.location.button.dont_allow.hint": "跳过共享位置", - "panel.onboarding.location.label.access_granted": "您已经授予对此应用程序的访问权限。", - "panel.onboarding.location.label.access_denied": "您已经拒绝访问此应用程序。", - - "panel.onboarding.bluetooth.label.title": "启用蓝牙", - "panel.onboarding.bluetooth.label.title.hint": "标题1", - "panel.onboarding.bluetooth.label.description": "使用藍牙提醒您可能接觸到COVID-19.", - "panel.onboarding.bluetooth.button.allow.title": "启用蓝牙", - "panel.onboarding.bluetooth.button.allow.hint": "", - "panel.onboarding.bluetooth.button.dont_allow.title": "不是现在", - "panel.onboarding.bluetooth.button.dont_allow.hint": "跳过启用蓝牙", - "panel.onboarding.bluetooth.label.access_granted": "您已授予访问此应用程序的权限", - "panel.onboarding.bluetooth.label.access_denied": "您已授予访问此应用程序的权限", - - "panel.onboarding.notifications.label.title": "需要的信息", - "panel.onboarding.notifications.label.hint": "标题1", - "panel.onboarding.notifications.label.description1": "您將收到COVID-19信息", - "panel.onboarding.notifications.label.description2": "這是曝光通知在手機上後台運行所必需的", - "panel.onboarding.notifications.button.allow.title": "啟用位置服務", - "panel.onboarding.notifications.button.allow.hint": "", - "panel.onboarding.notifications.button.dont_allow.title": "暂时不", - "panel.onboarding.notifications.button.dont_allow.hint": "跳过接收通知", - "panel.onboarding.notifications.label.access_granted": "您的设置已更改.", - "panel.onboarding.roles.label.title": "你是", "panel.onboarding.roles.label.title.hint": "标题1", "panel.onboarding.roles.label.description": "Select one", @@ -215,7 +185,6 @@ "panel.settings.home.button.test.hint": "", "panel.settings.home.button.get_help.title": "得到幫助", "panel.settings.home.button.get_help.hint": "", - "panel.settings.home.covid19.exposure_notifications": "接触通知", "panel.settings.home.covid19.provider_test_result": "健康提供者測試結果", "panel.settings.home.covid19.provider_vaccine_info": "衛生提供者疫苗信息", "panel.settings.home.covid19.title": "COVID-19", @@ -343,6 +312,12 @@ "panel.covid19home.vaccination.vaccinated.effective.0.description": "你的疫苗今天會有效。", "panel.covid19home.vaccination.vaccinated.effective.1.description": "你的疫苗明天就會生效。", "panel.covid19home.vaccination.vaccinated.effective.n.description": "您的疫苗將在 %s 天后生效。", + "panel.covid19home.vaccination.expired.title": "疫苗過期", + "panel.covid19home.vaccination.expired.description": "立即接種加強劑量。", + "panel.covid19home.vaccination.exempt.title": "免於接種", + "panel.covid19home.vaccination.exempt.description": "您目前可以免於接種 COVID-19 疫苗,但需要繼續接受檢測。", + "panel.covid19home.vaccination.suspended.title": "疫苗接種狀態暫停", + "panel.covid19home.vaccination.suspended.description": "您目前已有效接種疫苗,但需要繼續接受測試,直至另行通知。", "panel.covid19home.vaccination.button.appointment.title": "預約", "panel.covid19home.vaccination.button.appointment.hint": "", @@ -433,18 +408,15 @@ "panel.health.onboarding.covid19.how_it_works.line2.title": "您可以使用此应用程序:", "panel.health.onboarding.covid19.how_it_works.line3.title": "自我報告您的COVID-19症狀, 並以此狀態更新.", "panel.health.onboarding.covid19.how_it_works.line4.title": "自動從您的醫療保健提供者那裡接收測試結果和疫苗信息。", - "panel.health.onboarding.covid19.how_it_works.line5.title": "当你与检测结果呈阳性的人接触时,允许你的手机发送接触警告.", "panel.health.onboarding.covid19.how_it_works.button.next.title": "下一步", "panel.health.onboarding.covid19.how_it_works.button.next.hint": "", "panel.health.onboarding.covid19.disclosure.label.title": "Information Usage Disclosure", "panel.health.onboarding.covid19.disclosure.label.description1": "The Safer Illinois app uses:", "panel.health.onboarding.covid19.disclosure.label.splitter1": ". ", - "panel.health.onboarding.covid19.disclosure.label.content1": "1. Bluetooth to enable opt-in exposure notifications of close contact with individuals that test positive for COVID-19.", - "panel.health.onboarding.covid19.disclosure.label.content2": "2. Camera to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content3": "3. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content4": "4. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", - "panel.health.onboarding.covid19.disclosure.label.content5": "5. Location services on your device must be turned on to activate the Bluetooth low energy technology necessary for the exposure notification function of the Application to work in the background. However, the Application does not access, collect, or store any location data, including GPS data. If location services on your device are turned off, the Application will perform the limited functions of storing and providing information about COVID-19 test results, any voluntarily reported symptoms, and building access status.", + "panel.health.onboarding.covid19.disclosure.label.content1": "1. Camera to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content2": "2. Photos and Videos to allow a user to import their personal encryption key (QR code) into the app.", + "panel.health.onboarding.covid19.disclosure.label.content3": "3. Files (external storage read and write) to allow a user to import their personal encryption key (QR code) into the app.", "panel.health.onboarding.covid19.disclosure.label.description2": "YOUR INFORMATION AND HOW WE USE IT", "panel.health.onboarding.covid19.disclosure.label.description3": "Information we need to provide you COVID-19 test results", "panel.health.onboarding.covid19.disclosure.label.content8": "Name, email address, University ID number (UIN), phone number, and student registration information", @@ -457,20 +429,12 @@ "panel.health.onboarding.covid19.disclosure.label.content12": " - CAMERA", "panel.health.onboarding.covid19.disclosure.label.content13": " - WRITE_EXTERNAL_STORAGE", "panel.health.onboarding.covid19.disclosure.label.content14": " - READ_EXTERNAL_STORAGE", - "panel.health.onboarding.covid19.disclosure.label.description8": "Opt-in exposure notification participation", - "panel.health.onboarding.covid19.disclosure.label.content18": "Your phone transmits and receives anonymous identifying numbers via Bluetooth. This identifier is stored on any phones that come close to you. If someone tests positive for COVID-19, their phone tells our servers the anonymous numbers they have sent for the last 14 days. Your phone will check if you were near that infected users' phone long enough to warrant an exposure notification. This is all done anonymously. Your location is never tracked or stored on our servers. If you have elected to use Exposure Notification, Location Services, and Bluetooth, these capabilities will run in the background when the app is not in use. The following permissions are used to enable this:", - "panel.health.onboarding.covid19.disclosure.label.content19": " - BLUETOOTH", - "panel.health.onboarding.covid19.disclosure.label.content20": " - BLUETOOTH_ADMIN", - "panel.health.onboarding.covid19.disclosure.label.content21": " - LOCATION", "panel.health.onboarding.covid19.disclosure.check_box.label.acknowledge": "Acknowledge", "panel.health.onboarding.covid19.disclosure.button.disclosure.title": "Next", "panel.health.onboarding.covid19.disclosure.button.scroll_to_continue.title": "Scroll to Continue", "panel.health.onboarding.covid19.disclosure.button.disclosure.hint": "", "panel.health.onboarding.covid19.consent.label.title": "COVID-19功能的特别许可", - "panel.health.onboarding.covid19.consent.label.description": "接触通知", - "panel.health.onboarding.covid19.consent.label.content1": "如果您同意接触通知,則允許您的手機向附近的也在使用此功能的Safer Illinois應用程序用戶發送匿名藍牙信號。 您的電話也將接收並記錄來自其電話的信號。 如果其中一個用戶在接下來的14天內檢測出COVID-19呈陽性,則該應用程序將提醒您可能的暴露情況,並為您提供下一步建議。 您的身份和健康狀態以及所有其他用戶的身份和健康狀態將保持匿名。", - "panel.health.onboarding.covid19.consent.check_box.label.exposure": "我同意参与接触通知系统(需要打开蓝牙).", "panel.health.onboarding.covid19.consent.check_box.label.test": "我同意允許我的醫療保健提供者提供我的測試結果。", "panel.health.onboarding.covid19.consent.check_box.label.vaccine": "我同意允許我的醫療保健提供者提供我的疫苗信息。", "panel.health.onboarding.covid19.consent.label.content2": "自动测试结果", diff --git a/ios/Podfile b/ios/Podfile index 07dff8c1..36ac7fda 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -34,7 +34,6 @@ target 'Runner' do pod 'GoogleMaps', '3.3.0' pod 'ZXingObjC', '3.6.4' - pod 'HKDFKit', '0.0.3' # 'Firebase/MLVisionBarcodeModel' is required by 'firebase_ml_vision' plugin from pubspec.yaml. # Unable to include it in the podfile due to a conflict with FirebaseCore version number. diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fc4f09cf..f0faf8b7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -49,12 +49,10 @@ 26B462C2258126BB0035CA6D /* maps-icon-marker-origin-large@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 26B462C0258126BB0035CA6D /* maps-icon-marker-origin-large@2x.png */; }; 26BF10AF257F8EA800171227 /* MapDirectionsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 26BF10AE257F8EA800171227 /* MapDirectionsController.m */; }; 26BF10B2257F923E00171227 /* MapRoute.m in Sources */ = {isa = PBXBuildFile; fileRef = 26BF10B1257F923E00171227 /* MapRoute.m */; }; - 26C07E2E247677A600E28D43 /* ExposurePlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C07E2D247677A600E28D43 /* ExposurePlugin.m */; }; 26C90CED2361CF480092E07F /* NSDictionary+InaPathKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C90CEC2361CF480092E07F /* NSDictionary+InaPathKey.m */; }; 26C90CF02361D13E0092E07F /* NSDictionary+UIUCConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C90CEE2361D13E0092E07F /* NSDictionary+UIUCConfig.m */; }; 26DD091D2563DE0D003E7F70 /* audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 26DD091C2563DE0D003E7F70 /* audio.mp3 */; }; 26DD091F2563DF35003E7F70 /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 26DD091E2563DF35003E7F70 /* silence.mp3 */; }; - 26ECB3232487938C00479487 /* CommonCrypto+UIUCUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 26ECB3222487938C00479487 /* CommonCrypto+UIUCUtils.m */; }; 26ECB3262487AD0900479487 /* Security+UIUCUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 26ECB3252487AD0900479487 /* Security+UIUCUtils.m */; }; 26FA06BE22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m in Sources */ = {isa = PBXBuildFile; fileRef = 26FA06BC22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m */; }; 26FAB1C922EB3502008E987C /* NSDictionary+UIUCExplore.m in Sources */ = {isa = PBXBuildFile; fileRef = 26FAB1C822EB3502008E987C /* NSDictionary+UIUCExplore.m */; }; @@ -66,7 +64,6 @@ B6201F5B24E2D8080050F7DC /* GalleryPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = B6201F5A24E2D8080050F7DC /* GalleryPlugin.m */; }; B681B2B923DEFD9E00093A67 /* NSData+InaHex.m in Sources */ = {isa = PBXBuildFile; fileRef = B681B2B823DEFD9E00093A67 /* NSData+InaHex.m */; }; B6BDCD37242CCF5F002F7364 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6BDCD39242CCF5F002F7364 /* Localizable.strings */; }; - B6EA372423A7EF76001D78A5 /* Bluetooth+InaUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = B6EA372323A7EF76001D78A5 /* Bluetooth+InaUtils.m */; }; BB7BF12F5D3356D25889D23A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE7B5BCF89DF9B7AC1FD247B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -146,8 +143,6 @@ 26BF10AE257F8EA800171227 /* MapDirectionsController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MapDirectionsController.m; sourceTree = ""; }; 26BF10B0257F923E00171227 /* MapRoute.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapRoute.h; sourceTree = ""; }; 26BF10B1257F923E00171227 /* MapRoute.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MapRoute.m; sourceTree = ""; }; - 26C07E2C247677A600E28D43 /* ExposurePlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExposurePlugin.h; sourceTree = ""; }; - 26C07E2D247677A600E28D43 /* ExposurePlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExposurePlugin.m; sourceTree = ""; }; 26C90CEB2361CF480092E07F /* NSDictionary+InaPathKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+InaPathKey.h"; sourceTree = ""; }; 26C90CEC2361CF480092E07F /* NSDictionary+InaPathKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+InaPathKey.m"; sourceTree = ""; }; 26C90CEE2361D13E0092E07F /* NSDictionary+UIUCConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+UIUCConfig.m"; sourceTree = ""; }; @@ -158,8 +153,6 @@ 26E0DD712525C46D002B7B11 /* Debug-Dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Debug-Dev.xcconfig"; path = "Flutter/Debug-Dev.xcconfig"; sourceTree = ""; }; 26E0DD722525C46D002B7B11 /* Dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Dev.xcconfig; path = Flutter/Dev.xcconfig; sourceTree = ""; }; 26E0DD772525C5BF002B7B11 /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; - 26ECB3212487938C00479487 /* CommonCrypto+UIUCUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CommonCrypto+UIUCUtils.h"; sourceTree = ""; }; - 26ECB3222487938C00479487 /* CommonCrypto+UIUCUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CommonCrypto+UIUCUtils.m"; sourceTree = ""; }; 26ECB3242487AD0900479487 /* Security+UIUCUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Security+UIUCUtils.h"; sourceTree = ""; }; 26ECB3252487AD0900479487 /* Security+UIUCUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Security+UIUCUtils.m"; sourceTree = ""; }; 26FA06BC22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+InaTypedValue.m"; sourceTree = ""; }; @@ -187,8 +180,6 @@ B6BDCD38242CCF5F002F7364 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; B6BDCD3F242CDC89002F7364 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; B6BDCD40242CDCA6002F7364 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = ""; }; - B6EA372223A7EF76001D78A5 /* Bluetooth+InaUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bluetooth+InaUtils.h"; sourceTree = ""; }; - B6EA372323A7EF76001D78A5 /* Bluetooth+InaUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "Bluetooth+InaUtils.m"; sourceTree = ""; }; CAFE96D496EF0DB9C396C975 /* Pods-Runner.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-dev.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile-dev.xcconfig"; sourceTree = ""; }; CE7B5BCF89DF9B7AC1FD247B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E5C7A80130AEBA15C71E4EAB /* Pods-Runner.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-dev.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug-dev.xcconfig"; sourceTree = ""; }; @@ -271,8 +262,6 @@ 26FA06BB22CCA8B5003B78E4 /* Utils */ = { isa = PBXGroup; children = ( - B6EA372223A7EF76001D78A5 /* Bluetooth+InaUtils.h */, - B6EA372323A7EF76001D78A5 /* Bluetooth+InaUtils.m */, 269F83F022D744E200CC11A4 /* CGGeometry+InaUtils.h */, 269F83F122D744E200CC11A4 /* CGGeometry+InaUtils.m */, 268F647822DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.h */, @@ -309,8 +298,6 @@ 26C90CEE2361D13E0092E07F /* NSDictionary+UIUCConfig.m */, 269F83ED22D73EB400CC11A4 /* NSDate+UIUCUtils.h */, 269F83EA22D73EB400CC11A4 /* NSDate+UIUCUtils.m */, - 26ECB3212487938C00479487 /* CommonCrypto+UIUCUtils.h */, - 26ECB3222487938C00479487 /* CommonCrypto+UIUCUtils.m */, 26ECB3242487AD0900479487 /* Security+UIUCUtils.h */, 26ECB3252487AD0900479487 /* Security+UIUCUtils.m */, ); @@ -369,8 +356,6 @@ 2696997F22C3A14A00B3290E /* MapView.m */, 2626D94C22DCB80000F6BC2F /* MapMarkerView.h */, 2626D94D22DCB80000F6BC2F /* MapMarkerView.m */, - 26C07E2C247677A600E28D43 /* ExposurePlugin.h */, - 26C07E2D247677A600E28D43 /* ExposurePlugin.m */, B6201F5924E2D8080050F7DC /* GalleryPlugin.h */, B6201F5A24E2D8080050F7DC /* GalleryPlugin.m */, 26B34034233112320031CF70 /* FlutterCompletion.h */, @@ -618,7 +603,6 @@ "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", - "${BUILT_PRODUCTS_DIR}/HKDFKit/HKDFKit.framework", "${BUILT_PRODUCTS_DIR}/MTBBarcodeScanner/MTBBarcodeScanner.framework", "${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", @@ -663,7 +647,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HKDFKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MTBBarcodeScanner.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", @@ -730,7 +713,6 @@ 26C90CF02361D13E0092E07F /* NSDictionary+UIUCConfig.m in Sources */, 269F83F222D744E200CC11A4 /* CGGeometry+InaUtils.m in Sources */, 26BF10AF257F8EA800171227 /* MapDirectionsController.m in Sources */, - B6EA372423A7EF76001D78A5 /* Bluetooth+InaUtils.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 268F647D22DF09D900A85AFD /* NSArray+InaTypedValue.m in Sources */, 2696995D22C38B4000B3290E /* AppKeys.m in Sources */, @@ -741,7 +723,6 @@ B681B2B923DEFD9E00093A67 /* NSData+InaHex.m in Sources */, 26FA06BE22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m in Sources */, 2626D94B22DC99C800F6BC2F /* NSString+InaJson.m in Sources */, - 26C07E2E247677A600E28D43 /* ExposurePlugin.m in Sources */, 26C90CED2361CF480092E07F /* NSDictionary+InaPathKey.m in Sources */, 26ECB3262487AD0900479487 /* Security+UIUCUtils.m in Sources */, 26FAB1C922EB3502008E987C /* NSDictionary+UIUCExplore.m in Sources */, @@ -751,7 +732,6 @@ 26B3403A2331195C0031CF70 /* UILabel+InaMeasure.m in Sources */, 26FAB1CC22EB35C7008E987C /* NSDate+InaUtils.m in Sources */, 2696998022C3A14A00B3290E /* MapView.m in Sources */, - 26ECB3232487938C00479487 /* CommonCrypto+UIUCUtils.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m index 5022f994..a7c96fe4 100644 --- a/ios/Runner/AppDelegate.m +++ b/ios/Runner/AppDelegate.m @@ -24,7 +24,6 @@ #import "MapView.h" #import "MapController.h" #import "MapDirectionsController.h" -#import "ExposurePlugin.h" #import "GalleryPlugin.h" #import "NSArray+InaTypedValue.h" @@ -32,7 +31,6 @@ #import "NSDictionary+UIUCConfig.h" #import "CGGeometry+InaUtils.h" #import "UIColor+InaParse.h" -#import "Bluetooth+InaUtils.h" #import "Security+UIUCUtils.h" #import @@ -42,7 +40,6 @@ #import #import #import -#import static NSString* const kFIRMessagingFCMTokenNotification = @"com.firebase.iid.notif.fcm-token"; @@ -58,7 +55,7 @@ @interface LaunchScreenView : UIView UIInterfaceOrientation _interfaceOrientationFromMask(UIInterfaceOrientationMask value); UIInterfaceOrientationMask _interfaceOrientationToMask(UIInterfaceOrientation value); -@interface AppDelegate() { +@interface AppDelegate() { } // Flutter @@ -84,9 +81,6 @@ @interface AppDelegate() *locationFlutterResults; -// Bluetooth Services -@property (nonatomic) CBPeripheralManager *peripheralManager; -@property (nonatomic) NSMutableSet *bluetoothFlutterResults; @end @implementation AppDelegate @@ -108,10 +102,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( MapViewFactory *factory = [[MapViewFactory alloc] initWithMessenger:registrar.messenger]; [registrar registerViewFactory:factory withId:@"mapview"]; - // Setup ExposurePlugin - [ExposurePlugin registerWithRegistrar:[self registrarForPlugin:@"ExposurePlugin"]]; - - // Setup ExposurePlugin + // Setup GalleryPlugin [GalleryPlugin registerWithRegistrar:[self registrarForPlugin:@"GalleryPlugin"]]; // Setup supported & preffered orientation @@ -242,9 +233,6 @@ - (void)handleFlutterAPIFromCall:(FlutterMethodCall*)call result:(FlutterResult) else if ([call.method isEqualToString:@"location_services_permission"]) { [self handleLocationServicesWithParameters:parameters result:result]; } - else if ([call.method isEqualToString:@"bluetooth_authorization"]) { - [self handleBluetoothAuthorizationWithParameters:parameters result:result]; - } else if ([call.method isEqualToString:@"addToWallet"]) { [self handleAddToWalletWithParameters:parameters result:result]; } @@ -364,20 +352,6 @@ - (void)handleLocationServicesWithParameters:(NSDictionary*)parameters result:(F } } -- (void)handleBluetoothAuthorizationWithParameters:(NSDictionary*)parameters result:(FlutterResult)result { - NSString *method = [parameters inaStringForKey:@"method"]; - if ([method isEqualToString:@"query"]) { - [self queryBluetoothAuthorizationWithFlutterResult:result]; - } - else if ([method isEqualToString:@"request"]) { - [self requestBluetoothAuthorizationWithFlutterResult:result]; - } - else { - result(nil); - } -} - - - (void)handleAddToWalletWithParameters:(NSDictionary*)parameters result:(FlutterResult)result { NSString *base64CardData = [parameters inaStringForKey:@"cardBase64Data"]; NSData *cardData = [[NSData alloc] initWithBase64EncodedString:base64CardData options:0]; @@ -695,46 +669,6 @@ - (void)locationManager:(CLLocationManager*)manager didChangeAuthorizationStatus } } -#pragma mark Bluetooth Authorization - -- (void)queryBluetoothAuthorizationWithFlutterResult:(FlutterResult)flutterResult { - flutterResult(InaBluetoothAuthorizationStatusToString(InaBluetooth.peripheralAuthorizationStatus)); -} - -- (void)requestBluetoothAuthorizationWithFlutterResult:(FlutterResult)flutterResult { - if (InaBluetooth.peripheralAuthorizationStatus == InaBluetoothAuthorizationStatusNotDetermined) { - if (_bluetoothFlutterResults == nil) { - _bluetoothFlutterResults = [[NSMutableSet alloc] init]; - } - [_bluetoothFlutterResults addObject:flutterResult]; - if (_peripheralManager == nil) { - _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil]; - } - } - else { - flutterResult(InaBluetoothAuthorizationStatusToString(InaBluetooth.peripheralAuthorizationStatus)); - } -} - -- (void)didBluetoothServicesPermision { - _peripheralManager.delegate = nil; - _peripheralManager = nil; - - NSSet *flutterResults = _bluetoothFlutterResults; - _bluetoothFlutterResults = nil; - - NSString *status = InaBluetoothAuthorizationStatusToString(InaBluetooth.peripheralAuthorizationStatus); - for (FlutterResult flutterResult in flutterResults) { - flutterResult(status); - } -} - -#pragma mark CBPeripheralManagerDelegate - -- (void)peripheralManagerDidUpdateState:(CBPeripheralManager*)peripheral { - [self didBluetoothServicesPermision]; -} - #pragma mark Deep Links - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { diff --git a/ios/Runner/ExposurePlugin.h b/ios/Runner/ExposurePlugin.h deleted file mode 100644 index b76480a7..00000000 --- a/ios/Runner/ExposurePlugin.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// ExposurePlugin.h -// Runner -// -// Created by Mihail Varbanov on 5/21/20. -// Copyright 2020 Board of Trustees of the University of Illinois. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import -#import - - -@interface ExposurePlugin : NSObject - -@end - diff --git a/ios/Runner/ExposurePlugin.m b/ios/Runner/ExposurePlugin.m deleted file mode 100644 index b574b9bf..00000000 --- a/ios/Runner/ExposurePlugin.m +++ /dev/null @@ -1,1610 +0,0 @@ -// -// ExposurePlugin.m -// Runner -// -// Created by Mihail Varbanov on 5/21/20. -// Copyright 2020 Board of Trustees of the University of Illinois. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "ExposurePlugin.h" -#import -#import -#import -#import -#import - -#import "Bluetooth+InaUtils.h" -#import "NSDictionary+InaTypedValue.h" -#import "CommonCrypto+UIUCUtils.h" -#import "Security+UIUCUtils.h" - -static NSString* const kMethodChanelName = @"edu.illinois.covid/exposure"; -static NSString* const kServiceUuid = @"CD19"; -static NSString* const kCharacteristicUuid = @"1f5bb1de-cdf0-4424-9d43-d8cc81a7f207"; -static NSString* const kRangingBeaconUuid = @"c965bc2c-5f28-4854-b046-7e68e0e60074"; -static NSString* const kLocalNotificationId = @"exposureNotification"; -static NSString* const kExposureTEK1 = @"exposureTEKs"; -static NSString* const kExposureTEK2 = @"exposureTEK2s"; - -static NSString* const kStartMethodName = @"start"; -static NSString* const kStopMethodName = @"stop"; -static NSString* const kTEKsMethodName = @"TEKs"; -static NSString* const kTekRPIsMethodName = @"tekRPIs"; -static NSString* const kExpireTEKMethodName = @"expireTEK"; -static NSString* const kRPILogMethodName = @"exposureRPILog"; -static NSString* const kRSSILogMethodName = @"exposureRSSILog"; -static NSString* const kSettingsParamName = @"settings"; -static NSString* const kTEKParamName = @"tek"; -static NSString* const kTimestampParamName = @"timestamp"; - -static NSString* const kExpUpTimeMethodName = @"exposureUpTime"; -static NSString* const kUpTimeWinParamName = @"upTimeWindow"; - -static NSString* const kTEKNotificationName = @"tek"; -static NSString* const kTEKTimestampParamName = @"timestamp"; -static NSString* const kTEKExpirestampParamName = @"expirestamp"; -static NSString* const kTEKValueParamName = @"tek"; - -static NSString* const kExposureNotificationName = @"exposure"; -static NSString* const kExposureThickNotificationName = @"exposureThick"; -static NSString* const kExposureTimestampParamName = @"timestamp"; -static NSString* const kExposureRPIParamName = @"rpi"; -static NSString* const kExposureDurationParamName = @"duration"; -static NSString* const kExposureRSSIParamName = @"rssi"; - -static NSInteger const kRPIRefreshInterval = (10 * 60); // 10 mins -static NSInteger const kTEKRollingPeriod = (24 * 60 * 60) / kRPIRefreshInterval; // = 144 (kRPIRefreshInterval * kTEKRollingPeriod = 24 hours) - -static NSTimeInterval const kExposureNotifyTickInterval = 1; // 1 sec - -static int const kNoRssi = 127; - -//////////////////////////////////// -// ExposureRecord - -@interface ExposureRecord : NSObject -@property (nonatomic, readonly) NSInteger timestampCreated; -@property (nonatomic, readonly) NSTimeInterval timeUpdated; -@property (nonatomic, readonly) NSInteger duration; -@property (nonatomic, readonly) NSTimeInterval durationInterval; -@property (nonatomic, readonly) int rssi; - -- (instancetype)initWithTimestamp:(NSTimeInterval)timestamp rssi:(int)rssi; -- (void)updateTimestamp:(NSTimeInterval)timestamp rssi:(int)rssi; -@end - -//////////////////////////////////// -// TEKRecord - -@interface TEKRecord : NSObject -@property (nonatomic) NSData* tek; -@property (nonatomic) int expire; - -- (instancetype)initWithTEK:(NSData*)tek expire:(int)expire; - -+ (instancetype)fromJson:(NSDictionary*)json; -- (NSDictionary*)toJson; -@end - -//////////////////////////////////// -// ExposurePlugin - -@interface ExposurePlugin() { - - FlutterMethodChannel* _methodChannel; - FlutterResult _startResult; - - NSData* _rpi; - NSTimer* _rpiTimer; - NSMutableDictionary* _teks; - - CBPeripheralManager* _peripheralManager; - CBMutableService* _peripheralService; - CBMutableCharacteristic* _peripheralCharacteristic; - - CBCentralManager* _centralManager; - - NSMutableDictionary* _peripherals; - NSMutableDictionary* _peripheralRPIs; - NSMutableDictionary* _iosExposures; - NSMutableDictionary* _androidExposures; - - NSMutableDictionary* _exposureUpTime; - NSTimeInterval _exposureStartTime; - - NSTimer* _scanTimer; - NSTimeInterval _lastNotifyExposireThickTime; - - CLLocationManager* _locationManager; - CLBeaconRegion* _beaconRegion; - bool _monitoringLocation; - - AVAudioPlayer* _mutedAudioPlayer; - - NSTimeInterval _exposureTimeoutInterval; - NSTimeInterval _exposurePingInterval; - NSTimeInterval _exposureScanWindowInterval; - NSTimeInterval _exposureScanWaitInterval; - NSTimeInterval _exposureMinDuration; - - int _exposureMinRssi; - int _exposureExpireDays; - int _exposureUptimeExpireInterval; -} -@property (nonatomic, readonly) int exposureMinRssi; -@property (nonatomic) UIBackgroundTaskIdentifier bgTaskId; -@end - -@implementation ExposurePlugin - -static ExposurePlugin *g_Instance = nil; - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:kMethodChanelName binaryMessenger:registrar.messenger]; - ExposurePlugin *instance = [[ExposurePlugin alloc] initWithMethodChannel:channel]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (instancetype)init { - if (self = [super init]) { - if (g_Instance == nil) { - g_Instance = self; - } - _peripherals = [[NSMutableDictionary alloc] init]; - _peripheralRPIs = [[NSMutableDictionary alloc] init]; - _iosExposures = [[NSMutableDictionary alloc] init]; - _androidExposures = [[NSMutableDictionary alloc] init]; - _teks = [self.class loadTEK2sFromStorage]; - _bgTaskId = UIBackgroundTaskInvalid; - } - return self; -} - -- (void)dealloc { - if (g_Instance == self) { - g_Instance = nil; - } -} - -- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)channel { - if (self = [self init]) { - _methodChannel = channel; - } - return self; -} - -+ (instancetype)sharedInstance { - return g_Instance; -} - -#pragma mark MethodCall - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - NSDictionary *parameters = [call.arguments isKindOfClass:[NSDictionary class]] ? call.arguments : nil; - if ([call.method isEqualToString:kStartMethodName]) { - NSDictionary *settings = [parameters inaDictForKey:kSettingsParamName]; - [self startWithSettings:settings flutterResult:result]; - } - else if ([call.method isEqualToString:kStopMethodName]) { - [self stop]; - result([NSNumber numberWithBool:YES]); - } - else if ([call.method isEqualToString:kTEKsMethodName]) { - bool remove = [parameters inaBoolForKey:@"remove"]; - if (remove) { - [self.class saveTEK2sToStorage:nil]; - result(nil); - } - else { - result(self.teksList); - } - } - else if ([call.method isEqualToString:kTekRPIsMethodName]) { - NSString *tekString = [parameters inaStringForKey:kTEKParamName]; - NSData *tek = [[NSData alloc] initWithBase64EncodedString:tekString options:0]; - NSInteger timestamp = [parameters inaIntegerForKey:kTimestampParamName]; - NSInteger expirestamp = [parameters inaIntegerForKey:kTEKExpirestampParamName]; - result([self rpisForTek:tek timestamp:timestamp expirestamp:expirestamp]); - } - else if ([call.method isEqualToString:kExpireTEKMethodName]) { - [self updateTEKExpireTime]; - result(nil); - } - else if ([call.method isEqualToString:kExpUpTimeMethodName]) { - NSInteger upTimeWindow = [parameters inaIntegerForKey:kUpTimeWinParamName]; - result([self exposureUptimeDurationInWindow:upTimeWindow]); - } - else { - result(nil); - } -} - -#pragma mark API - -- (void)startWithSettings:(NSDictionary*)settings flutterResult:(FlutterResult)result { - - if (self.isPeripheralAuthorized && self.isCentralAuthorized && (_peripheralManager == nil) && (_centralManager == nil) && (_startResult == nil)) { - NSLog(@"ExposurePlugin: Start"); - _startResult = result; - [self initSettings:settings]; - [self initRPI]; - [self startPeripheral]; - [self startCentral]; - [self startLocationManager]; - [self startAudioPlayer]; - [self connectAppLiveCycleEvents]; - [self startExposureUptime]; - } - else if (result != nil) { - result([NSNumber numberWithBool:YES]); - } -} - -- (void)checkStarted { - if ((_startResult != nil) && self.isStarted) { - FlutterResult flutterResult = _startResult; - _startResult = nil; - flutterResult([NSNumber numberWithBool:YES]); - } -} - -- (void)startFailed { - if (_startResult != nil) { - FlutterResult flutterResult = _startResult; - _startResult = nil; - flutterResult([NSNumber numberWithBool:NO]); - } - -} - -- (bool)isStarted { - return self.isPeripheralStarted && self.isCentralStarted; -} - -- (void)stop { - NSLog(@"ExposurePlugin: Stop"); - [self stopPeripheral]; - [self stopCentral]; - [self stopLocationManager]; - [self stopAudioPlayer]; - [self clearRPI]; - [self clearExposures]; - [self disconnectAppLiveCycleEvents]; - [self stopExposureUptime]; -} - -#pragma mark Peripheral - -- (void)startPeripheral { - if (self.isPeripheralAuthorized && (_peripheralManager == nil)) { - _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil]; - } - else { - [self startFailed]; - } -} - -- (void)updatePeripheral { - if (self.isPeripheralInitialized && (_rpi != nil) && _peripheralManager.isAdvertising) { - [_peripheralManager updateValue:_rpi forCharacteristic:_peripheralCharacteristic onSubscribedCentrals:nil]; - } -} - -- (void)stopPeripheral { - - if (_peripheralManager != nil) { - if (_peripheralManager.isAdvertising) { - [_peripheralManager stopAdvertising]; - } - - if (_peripheralService != nil) { - [_peripheralManager removeService:_peripheralService]; - _peripheralService = nil; - } - - _peripheralCharacteristic = nil; - - _peripheralManager.delegate = nil; - _peripheralManager = nil; - } -} - -- (bool)isPeripheralAuthorized { - return InaBluetooth.peripheralAuthorizationStatus == InaBluetoothAuthorizationStatusAuthorized; -} - -- (bool)isPeripheralInitialized { - return (_peripheralManager != nil) && (_peripheralManager.state == CBManagerStatePoweredOn) && (_peripheralService != nil) && (_peripheralCharacteristic != nil); -} - -- (bool)isPeripheralStarted { - return self.isPeripheralInitialized && _peripheralManager.isAdvertising; -} - -#pragma mark CBPeripheralManagerDelegate - -- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { - NSLog(@"ExposurePlugin: CBPeripheralManager didUpdateState: %@", @(peripheral.state)); - - if (_peripheralManager.state == CBManagerStatePoweredOn) { - CBUUID *serviceUuid = [CBUUID UUIDWithString:kServiceUuid]; - CBMutableService *service = [[CBMutableService alloc] initWithType:serviceUuid primary:YES]; - - CBUUID *characteristicUuid = [CBUUID UUIDWithString:kCharacteristicUuid]; - CBMutableCharacteristic *characteristic = [[CBMutableCharacteristic alloc] initWithType:characteristicUuid properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable]; - service.characteristics = @[characteristic]; - - [_peripheralManager addService:service]; - } - else { - [self startFailed]; - } -} - -- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error { - NSLog(@"ExposurePlugin: CBPeripheralManager didAddService"); - _peripheralService = [service isKindOfClass:[CBMutableService class]] ? ((CBMutableService*)service) : nil; - _peripheralCharacteristic = [_peripheralService inaMutableCharacteristicWithUUID:[CBUUID UUIDWithString:kCharacteristicUuid]]; - - if (self.isPeripheralInitialized) { - [_peripheralManager startAdvertising: - @{ CBAdvertisementDataServiceUUIDsKey :@[[CBUUID UUIDWithString:kServiceUuid]], - }]; - } - else { - [self startFailed]; - } -} - -- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error { - NSLog(@"ExposurePlugin: CBPeripheralManager peripheralManagerDidStartAdvertising"); - if (self.isPeripheralStarted) { - [self checkStarted]; - } - else { - [self startFailed]; - } -} - -- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request{ - NSLog(@"ExposurePlugin: CBPeripheralManager didReceiveReadRequest"); - request.value = _rpi; - [peripheral respondToRequest:request withResult:CBATTErrorSuccess]; -} - -#pragma mark Central - -- (void)startCentral { - if (self.isCentralAuthorized && (_centralManager == nil)) { - _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{ - CBCentralManagerOptionShowPowerAlertKey : [NSNumber numberWithBool:NO], - }]; - } - else { - [self startFailed]; - } -} - -- (void)stopCentral { - if (_centralManager != nil) { - if (_centralManager.isScanning) { - [_centralManager stopScan]; - [self processExposures]; - } - - _centralManager.delegate = nil; - _centralManager = nil; - } - - if (_scanTimer != nil) { - [_scanTimer invalidate]; - _scanTimer = nil; - } -} - -- (void)startScanning { - if (_centralManager != nil) { - NSLog(@"ExposurePlugin: CBCentralManager scanForPeripheralsWithServices"); - [_centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUuid]] options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }]; - - if (_scanTimer != nil) { - [_scanTimer invalidate]; - } - _scanTimer = [NSTimer scheduledTimerWithTimeInterval:_exposureScanWindowInterval target:self selector:@selector(suspendScannig) userInfo:nil repeats:NO]; - - [self postLocalNotificationRequestIfNeeded]; - } -} - -- (void)suspendScannig { - if (_centralManager != nil) { - NSLog(@"ExposurePlugin: CBCentralManager stopScan"); - if (_centralManager.isScanning) { - [_centralManager stopScan]; - } - - if (_scanTimer != nil) { - [_scanTimer invalidate]; - } - _scanTimer = [NSTimer scheduledTimerWithTimeInterval:_exposureScanWaitInterval target:self selector:@selector(startScanning) userInfo:nil repeats:NO]; - - [self processExposures]; - } -} - -- (bool)isCentralAuthorized { - return InaBluetooth.centralAuthorizationStatus == InaBluetoothAuthorizationStatusAuthorized; -} - -- (bool)isCentralInitialized { - return (_centralManager != nil) && (_centralManager.state == CBManagerStatePoweredOn); -} - -- (bool)isCentralScanning { - return (_centralManager.isScanning || (_scanTimer != nil)); -} - -- (bool)isCentralStarted { - return self.isCentralInitialized && self.isCentralScanning; -} - -- (void)disconnectPeripheralWithUuid:(NSUUID*)peripheralUuid { - [self _disconnectPeripheral:[_peripherals objectForKey:peripheralUuid]]; - [self _removePeripheralWithUuid:peripheralUuid]; -} - -- (void)disconnectPeripheral:(CBPeripheral*)peripheral { - [self _disconnectPeripheral:peripheral]; - [self _removePeripheralWithUuid:peripheral.identifier]; -} - -- (void)_disconnectPeripheral:(CBPeripheral*)peripheral { - if (peripheral != nil) { - peripheral.delegate = nil; - - CBService *service = [peripheral inaServiceWithUUID:[CBUUID UUIDWithString:kServiceUuid]]; - CBCharacteristic *characteristic = [service inaCharacteristicWithUUID:[CBUUID UUIDWithString:kCharacteristicUuid]]; - if (characteristic != nil) { - [peripheral setNotifyValue:NO forCharacteristic:characteristic]; - } - - [_centralManager cancelPeripheralConnection:peripheral]; - } -} - -- (void)_removePeripheralWithUuid:(NSUUID*)peripheralUuid { - if (peripheralUuid != nil) { - [_peripherals removeObjectForKey:peripheralUuid]; - - NSData *rpi = [_peripheralRPIs objectForKey:peripheralUuid]; - if (rpi != nil) { - [_peripheralRPIs removeObjectForKey:peripheralUuid]; - } - - ExposureRecord *record = [_iosExposures objectForKey:peripheralUuid]; - if (record != nil) { - [_iosExposures removeObjectForKey:peripheralUuid]; - } - - if ((rpi != nil) && (record != nil)) { - [self notifyExposure:record rpi:rpi peripheralUuid:peripheralUuid]; - } - } -} - -- (void)_removeAndroidRpi:(NSData*)rpi { - NSUUID *peripheralUuid = [self peripheralUuidForRPI:rpi]; - [self disconnectPeripheralWithUuid:peripheralUuid]; - - ExposureRecord *record = [_androidExposures objectForKey:rpi]; - if (record != nil) { - [_androidExposures removeObjectForKey:rpi]; - } - - if ((rpi != nil) && (record != nil)) { - [self notifyExposure:record rpi:rpi peripheralUuid:nil]; - } -} - -- (NSUUID*)peripheralUuidForRPI:(NSData*)rpi { - for (NSUUID* peripheralUuid in _peripheralRPIs) { - NSData *peripheralRpi = [_peripheralRPIs objectForKey:peripheralUuid]; - if ([peripheralRpi isEqualToData:rpi]) { - return peripheralUuid; - } - } - return nil; -} - -#pragma mark CBCentralManagerDelegate - -- (void)centralManagerDidUpdateState:(CBCentralManager *)central { - NSLog(@"ExposurePlugin: CBCentralManager didUpdateState: %@", @(central.state)); - if (self.isCentralInitialized) { - [self startScanning]; - [self checkStarted]; - } - else { - [self startFailed]; - } -} - -- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { - - CBUUID *serviceUuid = [CBUUID UUIDWithString:kServiceUuid]; - if ([advertisementData inaAdvertisementDataContainsServiceWithUuid:serviceUuid]) { - - NSUUID *peripheralUuid = peripheral.identifier; - if ([_peripherals objectForKey:peripheralUuid] == nil) { - NSLog(@"ExposurePlugin: CBCentralManager didDiscoverPeripheral"); - [_peripherals setObject:peripheral forKey:peripheralUuid]; - [_centralManager connectPeripheral:peripheral options:nil]; - } - - NSDictionary *serviceData = [advertisementData inaDictForKey:CBAdvertisementDataServiceDataKey]; - NSData *rpiData = (serviceData != nil) ? [serviceData objectForKey:serviceUuid] : nil; - if (rpiData != nil) { // Android - if ([_peripheralRPIs objectForKey:peripheralUuid] == nil) { // new record - NSLog(@"ExposurePlugin: New Android peripheral RPI received"); - [_peripheralRPIs setObject:rpiData forKey:peripheralUuid]; - } - else if (![rpiData isEqualToData:[_peripheralRPIs objectForKey:peripheralUuid]]) { // update existing record - NSLog(@"ExposurePlugin: Connected Android peripheral RPI changed"); - NSData * rpi = [_peripheralRPIs objectForKey:peripheralUuid]; - ExposureRecord * record = [_androidExposures objectForKey:rpi]; - if (record != nil) { - [_androidExposures removeObjectForKey:rpi]; - } - if (rpi != nil && record != nil) { - [self notifyExposure:record rpi:rpi peripheralUuid:peripheralUuid]; - } - [_peripheralRPIs setObject:rpiData forKey:peripheralUuid]; - } - [self logAndroidExposure:rpiData rssi:RSSI.intValue]; - } - else { // iOS - [self logiOSExposure:peripheralUuid rssi:RSSI.intValue]; - } - } -} - -- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(nonnull CBPeripheral *)peripheral { - NSLog(@"ExposurePlugin: CBCentralManager didConnectPeripheral"); - peripheral.delegate = self; - [peripheral discoverServices:@[[CBUUID UUIDWithString:kServiceUuid]]]; -} - -- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error { - NSLog(@"ExposurePlugin: CBCentralManager didFailToConnectPeripheral"); -} - -- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { - NSLog(@"ExposurePlugin: CBCentralManager didDisconnectPeripheral"); - [self disconnectPeripheral:peripheral]; -} - -#pragma mark CBPeripheralDelegate - -- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error { - NSLog(@"ExposurePlugin: CBPeripheral didDiscoverServices"); - if (error == nil) { - CBService *service = [peripheral inaServiceWithUUID:[CBUUID UUIDWithString:kServiceUuid]]; - if (service != nil) { - [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:kCharacteristicUuid]] forService:service]; - } - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didModifyServices:(NSArray *)invalidatedServices { - NSLog(@"ExposurePlugin: CBPeripheral didModifyServices"); - CBUUID *serviceUuid = [CBUUID UUIDWithString:kServiceUuid]; - for (CBService *service in invalidatedServices) { - if ([service.UUID isEqual:serviceUuid]) { - [peripheral discoverServices:@[serviceUuid]]; - break; - } - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(nonnull CBService *)service error:(nullable NSError *)error { - NSLog(@"ExposurePlugin: CBPeripheral didDiscoverCharacteristicsForService"); - if (error == nil) { - CBCharacteristic *characteristic = [service inaCharacteristicWithUUID:[CBUUID UUIDWithString:kCharacteristicUuid]]; - if (characteristic != nil) { - [peripheral setNotifyValue:YES forCharacteristic:characteristic]; - [peripheral readValueForCharacteristic:characteristic]; - } - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { - NSLog(@"ExposurePlugin: CBPeripheral didUpdateValueForCharacteristic"); - if ((error == nil) && [characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUuid]] && (characteristic.value != nil)) { - NSUUID *peripheralUuid = peripheral.identifier; - NSData *rpi = [_peripheralRPIs objectForKey:peripheralUuid]; - if (rpi == nil) { - [_peripheralRPIs setObject:characteristic.value forKey:peripheralUuid]; - } - else if (![rpi isEqualToData:characteristic.value]) { - // update existing record - [_peripheralRPIs setObject:characteristic.value forKey:peripheralUuid]; - - NSLog(@"ExposurePlugin: Connected iOS peripheral RPI changed"); - ExposureRecord *record = [_iosExposures objectForKey:peripheralUuid]; - if (record != nil) { - [_iosExposures removeObjectForKey:peripheralUuid]; - } - if ((rpi != nil) && (record != nil)) { - [self notifyExposure:record rpi:rpi peripheralUuid:peripheralUuid]; - } - - NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970]; - record = [[ExposureRecord alloc] initWithTimestamp:currentTimestamp rssi:record.rssi]; - [_iosExposures setObject:record forKey:peripheralUuid]; - } - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error { - NSLog(@"ExposurePlugin: CBPeripheral didReadRSSI"); - if (error == nil) { - [self logiOSExposure:peripheral.identifier rssi:RSSI.intValue]; - } -} - -- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { -} - -#pragma mark Audio Player - -- (void)startAudioPlayer { - if (_mutedAudioPlayer == nil) { - - // Init audio session - AVAudioSession *session = [AVAudioSession sharedInstance]; - [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; - [session setActive:YES error:nil]; - - // Init audio player - NSError *error = nil; -// NSString *audioPath = [[NSBundle mainBundle] pathForResource:@"audio" ofType:@"mp3"]; - NSString *audioPath = [[NSBundle mainBundle] pathForResource:@"silence" ofType:@"mp3"]; - NSURL *audioUrl = [NSURL fileURLWithPath:audioPath]; - _mutedAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioUrl error:&error]; - - if ((_mutedAudioPlayer != nil) && (error == nil)) { - _mutedAudioPlayer.numberOfLoops = -1; - _mutedAudioPlayer.volume = 0.0f; - [_mutedAudioPlayer play]; - } - else { - NSLog(@"ExposurePlugin Audio Player init error: %@", error.localizedDescription); - } - } -} - -- (void)stopAudioPlayer { - if (_mutedAudioPlayer != nil) { - if (_mutedAudioPlayer.playing) { - [_mutedAudioPlayer stop]; - } - _mutedAudioPlayer = nil; - } -} - - -#pragma mark Location Monitor - -- (void)startLocationManager { - if (_locationManager == nil) { - _locationManager = [[CLLocationManager alloc] init]; - _locationManager.delegate = self; - _locationManager.distanceFilter = 1000; - _locationManager.desiredAccuracy = kCLLocationAccuracyKilometer; - _locationManager.pausesLocationUpdatesAutomatically = NO; - _locationManager.allowsBackgroundLocationUpdates = YES; - [self startBeaconRanging]; - [self startLocationMonitor]; - } -} - -- (void)stopLocationManager { - if (_locationManager != nil) { - [self stopBeaconRanging]; - [self stopLocationMonitor]; - _locationManager.delegate = nil; - _locationManager = nil; - } -} - -#pragma mark Beacon Ranging - -// -// http://www.davidgyoungtech.com/2020/05/07/hacking-the-overflow-area -// - -- (void)startBeaconRanging { - if ((_locationManager != nil) && (_beaconRegion == nil) && self.canBeaconRanging) { - _beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:kRangingBeaconUuid] identifier:kRangingBeaconUuid]; - [_locationManager startRangingBeaconsInRegion:_beaconRegion]; - } -} - -- (void)stopBeaconRanging { - if ((_locationManager != nil) && (_beaconRegion != nil)) { - [_locationManager stopRangingBeaconsInRegion:_beaconRegion]; - _beaconRegion = nil; - } -} - -- (bool)isBeaconRangingStarted { - return (_locationManager != nil) && (_beaconRegion != nil); -} - -- (void)updateBeaconRanging { - if (self.canBeaconRanging && !self.isBeaconRangingStarted) { - [self startBeaconRanging]; - } - else if (!self.canBeaconRanging && self.isBeaconRangingStarted) { - [self stopBeaconRanging]; - } -} - -- (bool)canBeaconRanging { - return self.canLocationMonitor && - [CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]]; -} - - -#pragma mark Location & Heading Monitor - -- (void)startLocationMonitor { - if ((_locationManager != nil) && !_monitoringLocation && self.canLocationMonitor) { -// [_locationManager startUpdatingLocation]; -// [_locationManager startUpdatingHeading]; - [_locationManager startMonitoringSignificantLocationChanges]; - _monitoringLocation = YES; - } -} - -- (void)stopLocationMonitor { - if ((_locationManager != nil) && _monitoringLocation) { -// [_locationManager stopUpdatingLocation]; -// [_locationManager stopUpdatingHeading]; - _monitoringLocation = NO; - } -} - -- (bool)isLocationMonitorStarted { - return (_locationManager != nil) && _monitoringLocation; -} - -- (void)updateLocationMonitor { - if (self.canLocationMonitor && !self.isLocationMonitorStarted) { - [self startLocationMonitor]; - } - else if (!self.canLocationMonitor && self.isLocationMonitorStarted) { - [self stopLocationMonitor]; - } -} - -- (bool)canLocationMonitor { - return - [CLLocationManager locationServicesEnabled] && - ( - ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways) || - ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse) - ); -} - -#pragma mark CLLocationManagerDelegate - -- (void)locationManager:(CLLocationManager*)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { - NSLog(@"ExposurePlugin didChangeAuthorizationStatus: %@", @(status)); - [self updateBeaconRanging]; - [self updateLocationMonitor]; -} - -- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray*)beacons inRegion:(CLBeaconRegion *)clBeaconRegion { - //NSLog(@"ExposurePlugin didRangeBeacons:[<%@>] inRegion: %@", @(beacons.count), clBeaconRegion.identifier); -} - -- (void)locationManager:(CLLocationManager *)manager rangingBeaconsDidFailForRegion:(CLBeaconRegion *)clBeaconRegion withError:(NSError *)error { - //NSLog(@"ExposurePlugin rangingBeaconsDidFailForRegion: %@ withError: %@", clBeaconRegion.identifier, error.localizedDescription); -} - -- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { -// UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; -// content.body = @"didUpdateLocations"; -// content.sound = nil; -// -// UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:kLocalNotificationId content:content trigger:nil]; -// [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError* error) { -// }]; -} - -#pragma mark Local Notifications - -- (void)postLocalNotificationRequestIfNeeded { - if ((UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) && - (UIScreen.mainScreen.brightness == 0)) - { - NSLog(@"ExposurePlugin: Posting Exposure Local Notification"); - - __weak typeof(self) weakSelf = self; - [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { - if (settings.authorizationStatus == 2 && settings.lockScreenSetting == 2) { - [weakSelf exposureUptimeHeartBeat]; - } - }]; - - UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; - content.body = @"Exposure Notification system checking"; - content.sound = nil; - - UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:kLocalNotificationId content:content trigger:nil]; - [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError* error) { - //UIScreen.mainScreen.brightness = 0.01; - }]; - } - -} - -#pragma mark Exposure Uptime - -- (void)startExposureUptime { - _exposureStartTime = [[[NSDate alloc] init] timeIntervalSince1970]; - _exposureUpTime = [self loadExposureUptimeFromStorage]; -} - -- (void)stopExposureUptime { - [self exposureUptimeHeartBeat]; - _exposureStartTime = 0.0; - _exposureUpTime = nil; -} - -- (void)exposureUptimeHeartBeat { - if ((0.0 < _exposureStartTime) && (_exposureUpTime != nil)) { - NSTimeInterval upTime = [[[NSDate alloc] init] timeIntervalSince1970] - _exposureStartTime; - [_exposureUpTime setObject:[NSNumber numberWithInteger:upTime] forKey:[NSNumber numberWithInteger:_exposureStartTime]]; - [self saveExposureUptimeToStorage]; - } -} - -- (NSString*)exposureUptimeFilePath { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *documentsDirectory = [paths objectAtIndex:0]; - return [documentsDirectory stringByAppendingPathComponent:@"exposureUpTime.json"]; -} - -- (void)saveExposureUptimeToStorage { - if (_exposureUpTime != nil) { - NSMutableDictionary *storageUpTime = [[NSMutableDictionary alloc] init]; - NSMutableArray *expiredTimeKeys = nil; - - NSInteger currentTimestamp = (NSInteger)[[[NSDate alloc] init] timeIntervalSince1970]; - NSInteger expireTimestamp = currentTimestamp - _exposureUptimeExpireInterval; - - for (NSNumber *timeNum in _exposureUpTime) { - NSInteger time = [timeNum integerValue]; - NSString *timeStr = [timeNum stringValue]; - - NSNumber *durationNum = [_exposureUpTime inaNumberForKey:timeNum]; - NSInteger duration = [durationNum integerValue]; - - if ((time + duration) < expireTimestamp) { - if (expiredTimeKeys == nil) { - expiredTimeKeys = [[NSMutableArray alloc] init]; - } - [expiredTimeKeys addObject:timeNum]; - } - else if ((timeStr != nil) && (durationNum != nil)) { - [storageUpTime setObject:durationNum forKey:timeStr]; - } - } - - if (expiredTimeKeys != nil) { - for (NSNumber *timeKey in expiredTimeKeys) { - [_exposureUpTime removeObjectForKey:timeKey]; - } - } - - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:storageUpTime options:0 error:NULL]; - [jsonData writeToFile:self.exposureUptimeFilePath atomically:YES]; - } -} - -- (NSMutableDictionary*)loadExposureUptimeFromStorage { - NSMutableDictionary *upTimes = [[NSMutableDictionary alloc] init]; - - NSInteger currentTimestamp = (NSInteger)[[[NSDate alloc] init] timeIntervalSince1970]; - NSInteger expireTimestamp = currentTimestamp - _exposureUptimeExpireInterval; - - NSData *jsonData = [NSData dataWithContentsOfFile:self.exposureUptimeFilePath options:0 error:NULL]; - NSDictionary* storedDictionary = (jsonData != nil) ? [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL] : nil; - if ([storedDictionary isKindOfClass:[NSDictionary class]]) { - for (NSString *timeStr in storedDictionary) { - NSInteger time = [timeStr integerValue]; - NSNumber *durationNum = [storedDictionary inaNumberForKey:timeStr]; - NSInteger duration = [durationNum integerValue]; - if ((time + duration) > expireTimestamp) { - [upTimes setObject:durationNum forKey:[NSNumber numberWithInteger:time]]; - } - } - } - - return upTimes; -} - -- (NSNumber*)exposureUptimeDurationInWindow:(NSInteger)timeWindow { - NSInteger durationInWindow = 0; - if (_exposureUpTime != nil) { - NSTimeInterval currentTime = [[[NSDate alloc] init] timeIntervalSince1970]; - NSTimeInterval timeWindowInSeconds = timeWindow * 60 * 60; - NSInteger startTimestamp = (NSInteger)(currentTime - timeWindowInSeconds); - - for (NSNumber *timeNum in _exposureUpTime) { - NSInteger time = [timeNum integerValue]; - NSInteger duration = [_exposureUpTime inaIntegerForKey:timeNum]; - if ((time + duration) >= startTimestamp) { - if (time >= startTimestamp) { - durationInWindow += duration; - } else { - durationInWindow += (duration + time - startTimestamp); - } - } - } - } - return [NSNumber numberWithInteger:durationInWindow]; -} - -#pragma mark App Livecycle Events - -- (void)connectAppLiveCycleEvents { - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - [notificationCenter addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; - [notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; - [notificationCenter addObserver:self selector:@selector(applicationWillResignActive) name:UIApplicationWillResignActiveNotification object:nil]; - [notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; - [notificationCenter addObserver:self selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil]; - [notificationCenter addObserver:self selector:@selector(audioSessionInterruptionNotification:) name:AVAudioSessionInterruptionNotification object:nil]; -} - -- (void)disconnectAppLiveCycleEvents { - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - [notificationCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; - [notificationCenter removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; - [notificationCenter removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; - [notificationCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; - [notificationCenter removeObserver:self name:UIApplicationWillTerminateNotification object:nil]; - [notificationCenter removeObserver:self name:AVAudioSessionInterruptionNotification object:nil]; -} - -- (void)applicationDidEnterBackground { - if (_bgTaskId == UIBackgroundTaskInvalid) { - __weak typeof(self) weakSelf = self; - _bgTaskId = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{ - weakSelf.bgTaskId = UIBackgroundTaskInvalid; - }]; - } -} - -- (void)applicationWillEnterForeground { - if (_bgTaskId != UIBackgroundTaskInvalid) { - [UIApplication.sharedApplication endBackgroundTask:_bgTaskId]; - _bgTaskId = UIBackgroundTaskInvalid; - } -} - -- (void)applicationWillResignActive { - [UIApplication.sharedApplication beginReceivingRemoteControlEvents]; - if ((_locationManager != nil) && self.canLocationMonitor && _monitoringLocation) { -// [_locationManager startUpdatingLocation]; -// [_locationManager startUpdatingHeading]; - [_locationManager startMonitoringSignificantLocationChanges]; - } -} - -- (void)applicationDidBecomeActive { - [UIApplication.sharedApplication endReceivingRemoteControlEvents]; -} - -- (void)applicationWillTerminate { - if (0.0 < _exposureStartTime) { - [self stopExposureUptime]; - } -} - -- (void)audioSessionInterruptionNotification:(NSNotification *)notification { - if (_mutedAudioPlayer != nil) { - int interruptionType = [notification.userInfo inaIntForKey:AVAudioSessionInterruptionTypeKey]; - int interruptionOption = [notification.userInfo inaIntForKey:AVAudioSessionInterruptionOptionKey]; - if ((interruptionType == AVAudioSessionInterruptionTypeBegan) && _mutedAudioPlayer.playing) { - [_mutedAudioPlayer pause]; - } - else if ((interruptionType == AVAudioSessionInterruptionTypeEnded) && (interruptionOption == AVAudioSessionInterruptionOptionShouldResume) && !_mutedAudioPlayer.playing) { - [_mutedAudioPlayer play]; - } - } -} - -#pragma mark Settings - -- (void)initSettings:(NSDictionary*)settings { - _exposureTimeoutInterval = (settings != nil) ? [settings inaDoubleForKey:@"covid19ExposureServiceTimeoutInterval" defaults: 300] : 300; // 5 minutes - _exposurePingInterval = (settings != nil) ? [settings inaDoubleForKey:@"covid19ExposureServicePingInterval" defaults: 60] : 60; // 1 minute - _exposureScanWindowInterval = (settings != nil) ? [settings inaDoubleForKey:@"covid19ExposureServiceScanWindowInterval" defaults: 4] : 4; // 4 seconds of scanning - _exposureScanWaitInterval = (settings != nil) ? [settings inaDoubleForKey:@"covid19ExposureServiceScanWaitInterval" defaults: 150] : 150; // 2.5 minutes of latent period - _exposureMinDuration = (settings != nil) ? [settings inaDoubleForKey:@"covid19ExposureServiceLogMinDuration" defaults: 0] : 0; // 0 seconds - _exposureExpireDays = (settings != nil) ? [settings inaIntForKey: @"covid19ExposureExpireDays" defaults: 14] : 14; // 14 days - _exposureMinRssi = (settings != nil) ? [settings inaIntForKey: @"covid19ExposureServiceMinRSSI" defaults: -90] : -90; - _exposureUptimeExpireInterval = (settings != nil) ? [settings inaIntForKey: @"covid19ExposureServiceUptimeExpireInterval" defaults: 604800] : 604800; // 7 days (168 * 60 * 60) -} - -#pragma mark RPI - -- (void)initRPI { - _rpi = [self generateRPI]; - - if (_rpiTimer == nil) { - _rpiTimer = [NSTimer scheduledTimerWithTimeInterval:kRPIRefreshInterval target:self selector:@selector(refreshRPI) userInfo:nil repeats:YES]; - } -} - -- (void)refreshRPI { - _rpi = [self generateRPI]; - - [self updatePeripheral]; -} - -- (void)clearRPI { - if (_rpi != nil) { - _rpi = nil; - } - if (_rpiTimer != nil) { - [_rpiTimer invalidate]; - _rpiTimer = nil; - } -} - -- (NSData*)generateRPI { - NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970]; - - /* obtain ENInvertalNumber and timestamp i for teks generation */ - uint32_t ENInvertalNumber = currentTimestamp / kRPIRefreshInterval; - - /* _i : time aligned with TEKRollingPeriod */ - uint32_t _i = (ENInvertalNumber / kTEKRollingPeriod) * kTEKRollingPeriod; - uint32_t _iExpire = _i + kTEKRollingPeriod; - - /* if new day, generate a new tek */ - /* if in the rest of the day, using last valid TEK */ - if (_teks != nil) { - NSNumber *iMax = [self maxTEKsI]; - if (iMax != nil) { - TEKRecord *maxRecord = [_teks objectForKey:iMax]; - if (maxRecord.expire == (_iExpire)) { - _i = [iMax intValue]; - } else - { - _i = ENInvertalNumber; - } - } - } - - //NSLog(@"ExposurePlugin: ENIntervalNumber: %d, i: %d", ENInvertalNumber, _i); - - /* generate tek each day, and store 14 of them in a database with timestamp i */ - TEKRecord* tekRecord = [_teks objectForKey:[NSNumber numberWithInt: _i]]; - if (tekRecord == nil) { - UInt8 bytes[16]; - int status = SecRandomCopyBytes(kSecRandomDefault, (sizeof bytes)/(sizeof bytes[0]), &bytes); - NSData *tek = (status == errSecSuccess) ? [NSData dataWithBytes:bytes length:sizeof(bytes)] : nil; - if (tek != nil) { - tekRecord = [[TEKRecord alloc] initWithTEK:tek expire:_iExpire]; - [_teks setObject:tekRecord forKey:[NSNumber numberWithInt: _i]]; - - if (_teks.count > (_exposureExpireDays + 1)) { // [0 - 14] gives 15 entries alltogether - uint32_t thresholdI = _i - _exposureExpireDays * kTEKRollingPeriod; - for (NSNumber *tekI in _teks.allKeys) { - if ([tekI intValue] < thresholdI) { - [_teks removeObjectForKey:tekI]; - } - } - } - - [self.class saveTEK2sToStorage:_teks]; - - NSInteger timestamp = ((NSInteger)_i) * kRPIRefreshInterval * 1000; // in miliseconds - NSInteger expirestamp = ((NSInteger)_iExpire) * kRPIRefreshInterval * 1000; // in miliseconds - [self notifyTEK:tek timestamp:timestamp expirestamp:expirestamp]; - } - else { - //NSLog(@"ExposurePlugin: Failed to generate new tek for i: %d", _i); - } - } - //NSLog(@"ExposurePlugin: Obtain tek {%@}", tek); - - NSData* rpi = [self generateRPIForIntervalNumber:ENInvertalNumber tek:tekRecord.tek]; - [self notifyRPI:rpi tek:tekRecord.tek updateType:(_rpi != nil) ? @"update" : @"init" timestamp:(currentTimestamp * 1000.0) _i:_i ENInvertalNumber:ENInvertalNumber]; - return rpi; -} - -- (NSData*)generateRPIForIntervalNumber:(uint32_t)ENInvertalNumber tek:(NSData*)tek { - //NSLog(@"ExposurePlugin: Refresh TEK"); - - /* generate rpik and aemk based on tek */ - NSData* rpik = [HKDFKit deriveKey:tek info:[@"EN-RPIK" dataUsingEncoding:NSUTF8StringEncoding] salt:nil outputSize:16]; - //NSLog(@"ExposurePlugin: Obtain rpik {%@}", rpik); - - NSData* aemk = [HKDFKit deriveKey:tek info:[@"EN-AEMK" dataUsingEncoding:NSUTF8StringEncoding] salt:nil outputSize:16]; - //NSLog(@"ExposurePlugin: Obtain aemk {%@}", aemk); - - /* generate paddedData for rpi message */ - NSData* paddedData_0_5 = [@"EN-RPI" dataUsingEncoding:NSUTF8StringEncoding]; - - const char char_pd_6_11[6] = "\x00\x00\x00\x00\x00\x00"; - NSData *paddedData_6_11 = [NSData dataWithBytes:char_pd_6_11 length:6]; - - uint32_t reverseENIntervalNumber = 0; - reverseENIntervalNumber = CFSwapInt32HostToBig(ENInvertalNumber); - NSData *paddedData_12_15 = [NSData dataWithBytes: &reverseENIntervalNumber length: 4]; - - NSMutableData* paddedData = [NSMutableData data]; - [paddedData appendData:paddedData_0_5]; - [paddedData appendData:paddedData_6_11]; - [paddedData appendData:paddedData_12_15]; - //NSLog(@"ExposurePlugin: PaddedData {%@}", paddedData); - - /* generate encrypted en_rpi with AES-128 */ - NSError *error = nil; - NSData* en_rpi = uiuc_aes_operation(paddedData, kCCEncrypt, kCCModeECB, kCCAlgorithmAES, ccNoPadding, kCCKeySizeAES128, nil, rpik, &error); - //NSLog(@"ExposurePlugin: RPI_en {%@}", en_rpi); - - /* generate metadata for aem message */ - NSData *metadata = [NSData dataWithBytes:(char[]){0x00,0x00,0x00,0x00} length:4]; - //NSLog(@"ExposurePlugin: metadata {%@}", metadata); - - /* generate encrypted en_aem with AES-128-CTR */ - NSData* en_aem = uiuc_aes_operation(metadata, kCCEncrypt, kCCModeCTR, kCCAlgorithmAES, ccNoPadding, kCCKeySizeAES128, en_rpi, aemk, &error); - //NSLog(@"ExposurePlugin: AEM_en {%@}", en_aem); - - /* contaticate en_rpi and en_aem to form the payload */ - NSMutableData* ble_load = [NSMutableData data]; - [ble_load appendData:en_rpi]; - [ble_load appendData:en_aem]; - //NSLog(@"ExposurePlugin: BLE_Payload {%@}", ble_load); - return ble_load; -} - -- (NSNumber*)maxTEKsI { - NSNumber *result = nil; - if (_teks != nil) { - for (NSNumber *i in _teks) { - if ((result == nil) || ([result intValue] < [i intValue])) { - result = i; - } - } - } - return result; -} - -- (NSArray*)teksList { - NSMutableArray *teksList = [[NSMutableArray alloc] init]; - for (NSNumber *tekKey in _teks) { - NSInteger _i = [tekKey intValue]; - TEKRecord *tekRecord = [_teks objectForKey:tekKey]; - NSInteger timestamp = ((NSInteger)_i) * kRPIRefreshInterval * 1000; // in miliseconds - NSInteger expirestamp = ((NSInteger)tekRecord.expire) * kRPIRefreshInterval * 1000; // in miliseconds - NSString *tekString = [tekRecord.tek base64EncodedStringWithOptions:0]; - [teksList addObject:@{ - kTEKTimestampParamName : [NSNumber numberWithInteger:timestamp], - kTEKExpirestampParamName : [NSNumber numberWithInteger:expirestamp], - kTEKValueParamName: tekString ?: [NSNull null], - }]; - } - return teksList; -} - -- (NSDictionary*)rpisForTek:(NSData*)tek timestamp:(NSInteger)timestamp expirestamp:(NSInteger)expirestamp { - NSTimeInterval timestampInterval = (timestamp / 1000.0); - NSTimeInterval expirestampInterval = (expirestamp / 1000.0); - - /* obtain start/endENInvertalNumber and timestamp i for teks generation */ - uint32_t startENInvertalNumber = timestampInterval / kRPIRefreshInterval; - uint32_t endENInvertalNumber = expirestampInterval / kRPIRefreshInterval; - - /* handle TEKs without expirestamp (0 or -1), default to 1 day later */ - if (endENInvertalNumber < startENInvertalNumber || endENInvertalNumber > startENInvertalNumber + kTEKRollingPeriod) - endENInvertalNumber = startENInvertalNumber + kTEKRollingPeriod; - - NSMutableDictionary *rpis = [[NSMutableDictionary alloc] init]; - for (uint32_t intervalIndex = startENInvertalNumber; intervalIndex <= endENInvertalNumber; intervalIndex++) { - NSData *rpi = [self generateRPIForIntervalNumber:intervalIndex tek:tek]; - NSString *rpiString = [rpi base64EncodedStringWithOptions:0]; - NSInteger interval = (((NSInteger)intervalIndex) * kRPIRefreshInterval * 1000); - [rpis setObject:[NSNumber numberWithInteger:interval] forKey:rpiString]; - } - return rpis; -} - -- (void)updateTEKExpireTime { - if (_teks != nil) { - NSNumber * current_i = [self maxTEKsI]; - TEKRecord* tekRecord = [_teks objectForKey:current_i]; - NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970]; - tekRecord.expire = currentTimestamp / kRPIRefreshInterval; - [self.class saveTEK2sToStorage:_teks]; - } -} - -+ (void)saveTEK1sToStorage:(NSDictionary*)teks { - if (teks != nil) { - NSMutableDictionary *storageTeks = [[NSMutableDictionary alloc] init]; - for (NSNumber *_i in teks) { - NSData *value = [teks objectForKey:_i]; - NSString *storageKey = [_i stringValue]; - NSString *storageValue = [value base64EncodedStringWithOptions:0]; - if ((storageKey != nil) && (storageValue != nil)) { - [storageTeks setObject:storageValue forKey:storageKey]; - } - } - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:storageTeks options:0 error:NULL]; - uiucSecStorageData(kExposureTEK1, kExposureTEK1, jsonData); - } - else { - uiucSecStorageData(kExposureTEK1, kExposureTEK1, [NSNull null]); - } -} - -+ (NSMutableDictionary*)loadTEK1sFromStorage { - NSMutableDictionary* teks = [[NSMutableDictionary alloc] init]; - NSData *jsonData = uiucSecStorageData(kExposureTEK1, kExposureTEK1, nil); - if (jsonData != nil) { - NSDictionary *storageTeks = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL]; - if ([storageTeks isKindOfClass:[NSDictionary class]]) { - for (NSString *storageKey in storageTeks) { - NSString *storageValue = [storageTeks inaStringForKey:storageKey]; - NSData *value = [[NSData alloc] initWithBase64EncodedString:storageValue options:0]; - if (value != nil) { - [teks setObject:value forKey:[NSNumber numberWithInt:[storageKey intValue]]]; - } - } - } - } - return teks; -} - -+ (void)saveTEK2sToStorage:(NSDictionary*)teks { - if (teks != nil) { - NSMutableDictionary *storageTeks = [[NSMutableDictionary alloc] init]; - for (NSNumber *_i in teks) { - TEKRecord *record = [teks objectForKey:_i]; - NSString *storageKey = [_i stringValue]; - NSDictionary *storageValue = record.toJson; - if ((storageKey != nil) && (storageValue != nil)) { - [storageTeks setObject:storageValue forKey:storageKey]; - } - } - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:storageTeks options:0 error:NULL]; - uiucSecStorageData(kExposureTEK2, kExposureTEK2, jsonData); - } - else { - uiucSecStorageData(kExposureTEK2, kExposureTEK2, [NSNull null]); - } -} - -+ (NSMutableDictionary*)loadTEK2sFromStorage { - NSMutableDictionary* teks = [[NSMutableDictionary alloc] init]; - NSData *jsonData = uiucSecStorageData(kExposureTEK2, kExposureTEK2, nil); - if (jsonData != nil) { - NSDictionary *storageTeks = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL]; - if ([storageTeks isKindOfClass:[NSDictionary class]]) { - for (NSString *storageKey in storageTeks) { - NSDictionary *storageValue = [storageTeks inaDictForKey:storageKey]; - TEKRecord *record = [TEKRecord fromJson:storageValue]; - if (record != nil) { - [teks setObject:record forKey:[NSNumber numberWithInt:[storageKey intValue]]]; - } - } - } - } - else { - NSDictionary* teks1 = [self loadTEK1sFromStorage]; - if (teks1 != nil) { - for (NSNumber *i in teks1) { - NSData *tek = [teks1 inaDataForKey:i]; - int expire = [i intValue] + kTEKRollingPeriod; - [teks setObject:[[TEKRecord alloc] initWithTEK:tek expire:expire] forKey:i]; - } - } - } - return teks; -} - -#pragma mark Exposure - -- (void)logiOSExposure:(NSUUID*)peripheralUuid rssi:(int)rssi { - NSLog(@"ExposurePlugin: {%@} / rssi: %@", peripheralUuid, @(rssi)); - - NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970]; - ExposureRecord *record = [_iosExposures objectForKey:peripheralUuid]; - if (record == nil) { - // Create new - NSLog(@"ExposurePlugin: {%@} registred", peripheralUuid); - record = [[ExposureRecord alloc] initWithTimestamp:currentTimestamp rssi:rssi]; - [_iosExposures setObject:record forKey:peripheralUuid]; - } - else { - // Update existing - [record updateTimestamp:currentTimestamp rssi:rssi]; - } - - NSData *rpi = [_peripheralRPIs objectForKey:peripheralUuid]; - [self notifyExposureTick:rpi rssi:rssi peripheralUuid:peripheralUuid]; - [self notifyRSSI:rssi rpi:rpi timestamp:(currentTimestamp * 1000.0) peripheralUuid:peripheralUuid]; -} - -- (void)logAndroidExposure:(NSData*)rpi rssi:(int)rssi { - NSLog(@"ExposurePlugin: {%@} / rssi: %@", rpi, @(rssi)); - - NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970]; - ExposureRecord *record = [_androidExposures objectForKey:rpi]; - if (record == nil) { - // Create new - NSLog(@"ExposurePlugin: {%@} registred", rpi); - record = [[ExposureRecord alloc] initWithTimestamp:currentTimestamp rssi:rssi]; - [_androidExposures setObject:record forKey:rpi]; - } - else { - // Update existing - [record updateTimestamp:currentTimestamp rssi:rssi]; - } - - [self notifyExposureTick:rpi rssi:rssi peripheralUuid:nil]; - [self notifyRSSI:rssi rpi:rpi timestamp:(currentTimestamp * 1000.0) peripheralUuid:nil]; -} - -- (void)processExposures { - - NSLog(@"ExposurePlugin: Processing exposures"); - NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970]; - - // collect all iOS expired records (not updated after _exposureTimeoutInterval) - NSMutableSet *expiredPeripheralUuid = nil; - for (NSUUID *peripheralUuid in _iosExposures) { - ExposureRecord *record = [_iosExposures objectForKey:peripheralUuid]; - NSTimeInterval lastHeardInterval = currentTimestamp - record.timeUpdated; - - if (_exposureTimeoutInterval <= lastHeardInterval) { - NSLog(@"ExposurePlugin: {%@} expired", peripheralUuid); - if (expiredPeripheralUuid == nil) { - expiredPeripheralUuid = [[NSMutableSet alloc] init]; - } - [expiredPeripheralUuid addObject:peripheralUuid]; - } -// ping disabled -// else if (_exposurePingInterval <= lastHeardInterval) { -// NSLog(@"ExposurePlugin: {%@} ping", peripheralUuid); -// CBPeripheral *peripheral = [_peripherals objectForKey:peripheralUuid]; -// [peripheral readRSSI]; -// } - } - - if (expiredPeripheralUuid != nil) { - // remove expired records from _iosExposures - for (NSUUID *peripheralUuid in expiredPeripheralUuid) { - [self disconnectPeripheralWithUuid:peripheralUuid]; - } - } - - // collect all Android expired records (not updated after _exposureTimeoutInterval) - NSMutableSet *expiredRPIs = nil; - for (NSData *rpi in _androidExposures) { - ExposureRecord *record = [_androidExposures objectForKey:rpi]; - NSTimeInterval lastHeardInterval = currentTimestamp - record.timeUpdated; - - if (_exposureTimeoutInterval <= lastHeardInterval) { - NSLog(@"ExposurePlugin: {%@} expired", rpi); - if (expiredRPIs == nil) { - expiredRPIs = [[NSMutableSet alloc] init]; - } - [expiredRPIs addObject:rpi]; - } - else if (_exposurePingInterval <= lastHeardInterval) { - NSLog(@"ExposurePlugin: {%@} ping", rpi); - NSUUID *peripheralUuid = [self peripheralUuidForRPI:rpi]; - CBPeripheral *peripheral = [_peripherals objectForKey:peripheralUuid]; - [peripheral readRSSI]; - } - } - - if (expiredRPIs != nil) { - // remove expired records from _androidExposures - for (NSData *rpi in expiredRPIs) { - [self _removeAndroidRpi:rpi]; - } - } -} - -- (void)clearExposures { - for (NSUUID *peripheralUuid in _iosExposures.allKeys) { - [self disconnectPeripheralWithUuid:peripheralUuid]; - } - for (NSData *rpi in _androidExposures.allKeys) { - [self _removeAndroidRpi:rpi]; - } -} - -#pragma mark Notifications - -- (void)notifyExposure:(ExposureRecord*)record rpi:(NSData*)rpi peripheralUuid:(NSUUID*)peripheralUuid { - if (_exposureMinDuration <= record.durationInterval) { - NSString *rpiString = [rpi base64EncodedStringWithOptions:0]; - NSTimeInterval currentTimeInterval = [[[NSDate alloc] init] timeIntervalSince1970]; - NSLog(@"ExposurePlugin: Report Exposure: rpi: {%@} duration: %@", rpiString, @(record.duration)); - - [_methodChannel invokeMethod:kExposureNotificationName arguments:@{ - kExposureTimestampParamName: [NSNumber numberWithInteger:record.timestampCreated], - kExposureRPIParamName: rpiString ?: [NSNull null], - kExposureDurationParamName: [NSNumber numberWithInteger:record.duration], - - @"peripheralUuid": [peripheralUuid UUIDString] ?: [NSNull null], - @"isiOSRecord": [NSNumber numberWithBool:(peripheralUuid != nil)], - @"endTimestamp": [NSNumber numberWithInteger:currentTimeInterval * 1000.0], - }]; - } -} - -- (void)notifyExposureTick:(NSData*)rpi rssi:(int)rssi peripheralUuid:(NSUUID*)peripheralUuid { - - // Do not allow more than 1 notification per second - NSTimeInterval currentTimeInterval = [[[NSDate alloc] init] timeIntervalSince1970]; - if (kExposureNotifyTickInterval <= (currentTimeInterval - _lastNotifyExposireThickTime)) { - - NSString *rpiString = (rpi != nil) ? [rpi base64EncodedStringWithOptions:0] : nil; - NSInteger currentTimestamp = (NSInteger)(currentTimeInterval * 1000.0); - - [_methodChannel invokeMethod:kExposureThickNotificationName arguments:@{ - kExposureTimestampParamName: [NSNumber numberWithInteger:currentTimestamp], - kExposureRPIParamName: rpiString ?: @"...", - kExposureRSSIParamName: [NSNumber numberWithInteger:rssi], - @"peripheralUuid": [peripheralUuid UUIDString] ?: [NSNull null], - }]; - - _lastNotifyExposireThickTime = currentTimeInterval; - } - -} - -- (void)notifyTEK:(NSData*)tek timestamp:(NSInteger)timestamp expirestamp:(NSInteger)expirestamp { - NSString *tekString = [tek base64EncodedStringWithOptions:0]; - NSLog(@"ExposurePlugin: Report TEK: {%@}", tekString); - [_methodChannel invokeMethod:kTEKNotificationName arguments:@{ - kTEKTimestampParamName: [NSNumber numberWithInteger:timestamp], // in milliseconds - kTEKExpirestampParamName: [NSNumber numberWithInteger:expirestamp], // in milliseconds - kTEKValueParamName: tekString ?: [NSNull null], - }]; -} - -- (void)notifyRPI:(NSData*)rpi tek:(NSData*)tek updateType:(NSString*)updateType timestamp:(NSInteger)timestamp _i:(uint32_t)_i ENInvertalNumber:(uint32_t)ENInvertalNumber { - NSString *rpiString = [rpi base64EncodedStringWithOptions:0]; - NSString *tekString = [tek base64EncodedStringWithOptions:0]; - NSLog(@"ExposurePlugin: Report RPI: {%@}", rpiString); - [_methodChannel invokeMethod:kRPILogMethodName arguments:@{ - kExposureTimestampParamName: [NSNumber numberWithInteger:timestamp], - @"updateType": updateType ?: [NSNull null], - @"rpi": rpiString ?: [NSNull null], - @"tek": tekString ?: [NSNull null], - @"_i": [NSNumber numberWithInteger:_i], - @"ENInvertalNumber": [NSNumber numberWithInteger:ENInvertalNumber], - }]; -} - -- (void)notifyRSSI:(int)rssi rpi:(NSData*)rpi timestamp:(NSInteger)timestamp peripheralUuid:(NSUUID*)peripheralUuid { - NSString *rpiString = [rpi base64EncodedStringWithOptions:0]; - [_methodChannel invokeMethod:kRSSILogMethodName arguments:@{ - kExposureTimestampParamName: [NSNumber numberWithInteger:timestamp], - @"rpi": rpiString ?: [NSNull null], - @"rssi": [NSNumber numberWithInt:rssi], - @"isiOSRecord": [NSNumber numberWithBool:(peripheralUuid != nil)], - @"address": [peripheralUuid UUIDString] ?: [NSNull null], - }]; -} - -@end - -//////////////////////////////////// -// ExposureRecord - -@interface ExposureRecord() { - NSTimeInterval _timeCreated; - NSTimeInterval _timeUpdated; - int _lastRSSI; - NSTimeInterval _durationInterval; -} -@end - -@implementation ExposureRecord - -- (instancetype)initWithTimestamp:(NSTimeInterval)timestamp rssi:(int)rssi { - if (self = [super init]) { - _lastRSSI = rssi; - _durationInterval = 0; - _timeCreated = _timeUpdated = timestamp; - } - return self; -} - -- (void)updateTimestamp:(NSTimeInterval)timestamp rssi:(int)rssi { - if ((ExposurePlugin.sharedInstance.exposureMinRssi <= _lastRSSI) && (_lastRSSI != kNoRssi)) { - _durationInterval += (timestamp - _timeUpdated); - } - _lastRSSI = rssi; - _timeUpdated = timestamp; -} - -- (NSInteger)timestampCreated { - return (NSInteger)(_timeCreated * 1000.0); // in milliseconds -} - -- (NSTimeInterval)timeUpdated { - return _timeUpdated; -} - -- (NSInteger)duration { - return (NSInteger)(_durationInterval * 1000.0); // in milliseconds -} - -- (NSTimeInterval)durationInterval { - return _durationInterval; // in seconds -} - -- (int)rssi { - return _lastRSSI; -} - -@end - -//////////////////////////////////// -// TEKRecord - -@implementation TEKRecord -//@property (nonatomic) int expire; -//@property (nonatomic) NSData* tek; - -- (instancetype)initWithTEK:(NSData*)tek expire:(int)expire { - if (self = [super init]) { - _tek = tek; - _expire = expire; - } - return self; - -} - -+ (instancetype)fromJson:(NSDictionary*)json { - return (json != nil) ? [[TEKRecord alloc] - initWithTEK: [[NSData alloc] initWithBase64EncodedString:[json inaStringForKey:@"tek"] options:0] - expire: [json inaIntForKey:@"expire"]] : nil; -} - -- (NSDictionary*)toJson { - return @{ - @"tek": [_tek base64EncodedStringWithOptions:0] ?: [NSNull null], - @"expire": @(_expire) - }; -} - -@end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4dfdf5ad..d95f9db1 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -36,13 +36,13 @@ + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) LSApplicationQueriesSchemes https http - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS NSAppTransportSecurity @@ -55,9 +55,9 @@ NSAppleMusicUsageDescription Allow access to Media Library. NSBluetoothAlwaysUsageDescription - Bluetooth will be used to enable the COVID-19 exposure notification system. + Allow access to Bluetooth. NSBluetoothPeripheralUsageDescription - Bluetooth will be used to enable the COVID-19 exposure notification system. + Allow access to Bluetooth. NSCalendarsUsageDescription Your calendar will be used to let you set reminders for important events. NSCameraUsageDescription @@ -65,13 +65,13 @@ NSContactsUsageDescription Allow access to Contacts. NSLocationAlwaysAndWhenInUseUsageDescription - No location data will be sent or received by this app. The location services are on so that Bluetooth can run in background. + Your location will be used to provide nearby health test locations. NSLocationAlwaysUsageDescription - No location data will be sent or received by this app. The location services are on so that Bluetooth can run in background. + Your location will be used to provide nearby health test locations. NSLocationUsageDescription - No location data will be sent or received by this app. The location services are on so that Bluetooth can run in background. + Your location will be used to provide nearby health test locations. NSLocationWhenInUseUsageDescription - No location data will be sent or received by this app. The location services are on so that Bluetooth can run in background. + Your location will be used to provide nearby health test locations. NSMicrophoneUsageDescription Allow access to Microphone. NSMotionUsageDescription @@ -86,11 +86,6 @@ Allow sending data to Apple’s speech recognition servers. UIBackgroundModes - audio - bluetooth-central - bluetooth-peripheral - fetch - location remote-notification UILaunchStoryboardName diff --git a/ios/Runner/UIUC/CommonCrypto+UIUCUtils.h b/ios/Runner/UIUC/CommonCrypto+UIUCUtils.h deleted file mode 100644 index 580809b3..00000000 --- a/ios/Runner/UIUC/CommonCrypto+UIUCUtils.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// CommonCrypto+UIUCUtils.h -// UIUCUtils -// -// Created by Mihail Varbanov on 5/9/19. -// Copyright 2020 Board of Trustees of the University of Illinois. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import -#import - -NSData * uiuc_aes_operation(NSData * dataIn, - CCOperation operation, // kCC Encrypt, Decrypt - CCMode mode, // kCCMode ECB, CBC, CFB, CTR, OFB, RC4, CFB8 - CCAlgorithm algorithm, // CCAlgorithm AES DES, 3DES, CAST, RC4, RC2, Blowfish - CCPadding padding, // cc NoPadding, PKCS7Padding - size_t keyLength, // kCCKeySizeAES 128, 192, 256 - NSData * iv, // CBC, CFB, CFB8, OFB, CTR - NSData * key, - NSError ** error); diff --git a/ios/Runner/UIUC/CommonCrypto+UIUCUtils.m b/ios/Runner/UIUC/CommonCrypto+UIUCUtils.m deleted file mode 100644 index 12f1355c..00000000 --- a/ios/Runner/UIUC/CommonCrypto+UIUCUtils.m +++ /dev/null @@ -1,99 +0,0 @@ -// -// CommonCrypto+UIUCUtils.m -// UIUCUtils -// -// Created by Mihail Varbanov on 5/9/19. -// Copyright 2020 Board of Trustees of the University of Illinois. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "CommonCrypto+UIUCUtils.h" - -NSData * uiuc_aes_operation(NSData * dataIn, CCOperation operation, // kCC Encrypt, Decrypt - CCMode mode, // kCCMode ECB, CBC, CFB, CTR, OFB, RC4, CFB8 - CCAlgorithm algorithm, // CCAlgorithm AES DES, 3DES, CAST, RC4, RC2, Blowfish - CCPadding padding, // cc NoPadding, PKCS7Padding - size_t keyLength, // kCCKeySizeAES 128, 192, 256 - NSData * iv, // CBC, CFB, CFB8, OFB, CTR - NSData * key, - NSError ** error) -{ - if (key.length != keyLength) { - NSLog(@"CCCryptorArgument key.length: %lu != keyLength: %zu", (unsigned long)key.length, keyLength); - if (error) { - *error = [NSError errorWithDomain:@"kArgumentError key length" code:key.length userInfo:nil]; - } - return nil; - } - - size_t dataOutMoved = 0; - size_t dataOutMovedTotal = 0; - CCCryptorStatus ccStatus = 0; - CCCryptorRef cryptor = NULL; - - ccStatus = CCCryptorCreateWithMode(operation, mode, algorithm, - padding, - iv.bytes, key.bytes, - keyLength, - NULL, 0, 0, // tweak XTS mode, numRounds - kCCModeOptionCTR_BE, // CCModeOptions - &cryptor); - - if (cryptor == 0 || ccStatus != kCCSuccess) { - NSLog(@"CCCryptorCreate status: %d", ccStatus); - if (error) { - *error = [NSError errorWithDomain:@"kCreateError" code:ccStatus userInfo:nil]; - } - CCCryptorRelease(cryptor); - return nil; - } - - size_t dataOutLength = CCCryptorGetOutputLength(cryptor, dataIn.length, true); - NSMutableData *dataOut = [NSMutableData dataWithLength:dataOutLength]; - char *dataOutPointer = (char *)dataOut.mutableBytes; - - ccStatus = CCCryptorUpdate(cryptor, - dataIn.bytes, dataIn.length, - dataOutPointer, dataOutLength, - &dataOutMoved); - dataOutMovedTotal += dataOutMoved; - - if (ccStatus != kCCSuccess) { - NSLog(@"CCCryptorUpdate status: %d", ccStatus); - if (error) { - *error = [NSError errorWithDomain:@"kUpdateError" code:ccStatus userInfo:nil]; - } - CCCryptorRelease(cryptor); - return nil; - } - - ccStatus = CCCryptorFinal(cryptor, - dataOutPointer + dataOutMoved, dataOutLength - dataOutMoved, - &dataOutMoved); - if (ccStatus != kCCSuccess) { - NSLog(@"CCCryptorFinal status: %d", ccStatus); - if (error) { - *error = [NSError errorWithDomain:@"kFinalError" code:ccStatus userInfo:nil]; - } - CCCryptorRelease(cryptor); - return nil; - } - - CCCryptorRelease(cryptor); - - dataOutMovedTotal += dataOutMoved; - dataOut.length = dataOutMovedTotal; - - return dataOut; -} diff --git a/ios/Runner/Utils/Bluetooth+InaUtils.h b/ios/Runner/Utils/Bluetooth+InaUtils.h deleted file mode 100644 index f3593d24..00000000 --- a/ios/Runner/Utils/Bluetooth+InaUtils.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// CBPeripheral+InaUtils.h -// Runner -// -// Created by Mladen Dryankov on 16.12.19. -// Copyright 2020 Board of Trustees of the University of Illinois. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import -#import - -typedef NS_ENUM(NSInteger, InaBluetoothAuthorizationStatus) { - InaBluetoothAuthorizationStatusNotDetermined = 0, - InaBluetoothAuthorizationStatusRestricted, - InaBluetoothAuthorizationStatusDenied, - InaBluetoothAuthorizationStatusAuthorized, -}; - -NSString* InaBluetoothAuthorizationStatusToString(InaBluetoothAuthorizationStatus value); -InaBluetoothAuthorizationStatus InaBluetoothAuthorizationStatusFromString(NSString *value); - -@interface InaBluetooth : NSObject -@property(nonatomic, class, readonly) InaBluetoothAuthorizationStatus peripheralAuthorizationStatus; -@property(nonatomic, class, readonly) InaBluetoothAuthorizationStatus centralAuthorizationStatus; -@end - -@interface CBPeripheral(InaUtils) -- (CBService*)inaServiceWithUUID:(CBUUID*)uuid; -@end - -@interface CBService(InaUtils) -- (CBCharacteristic*)inaCharacteristicWithUUID:(CBUUID*)uuid; -- (CBMutableCharacteristic*)inaMutableCharacteristicWithUUID:(CBUUID*)uuid; -@end - -@interface NSDictionary(InaBluetoothUtils) -- (bool)inaAdvertisementDataContainsServiceWithUuid:(CBUUID*)serviceUuid; -@end diff --git a/ios/Runner/Utils/Bluetooth+InaUtils.m b/ios/Runner/Utils/Bluetooth+InaUtils.m deleted file mode 100644 index 53944168..00000000 --- a/ios/Runner/Utils/Bluetooth+InaUtils.m +++ /dev/null @@ -1,152 +0,0 @@ -// -// CBPeripheral+InaUtils.m -// Runner -// -// Created by Mladen Dryankov on 16.12.19. -// Copyright 2020 Board of Trustees of the University of Illinois. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import "Bluetooth+InaUtils.h" -#import "NSDictionary+InaTypedValue.h" - -////////////////////////////////////// -// InaBluetooth - -@implementation InaBluetooth - -+ (InaBluetoothAuthorizationStatus)peripheralAuthorizationStatus { - if (@available(iOS 13.1, *)) { - switch(CBPeripheralManager.authorization) { - case CBManagerAuthorizationNotDetermined: return InaBluetoothAuthorizationStatusNotDetermined; - case CBManagerAuthorizationRestricted: return InaBluetoothAuthorizationStatusRestricted; - case CBManagerAuthorizationDenied: return InaBluetoothAuthorizationStatusDenied; - case CBManagerAuthorizationAllowedAlways: return InaBluetoothAuthorizationStatusAuthorized; - } - } else { - switch (CBPeripheralManager.authorizationStatus) { - case CBPeripheralManagerAuthorizationStatusNotDetermined: return InaBluetoothAuthorizationStatusNotDetermined; - case CBPeripheralManagerAuthorizationStatusRestricted: return InaBluetoothAuthorizationStatusRestricted; - case CBPeripheralManagerAuthorizationStatusDenied: return InaBluetoothAuthorizationStatusDenied; - case CBPeripheralManagerAuthorizationStatusAuthorized: return InaBluetoothAuthorizationStatusAuthorized; - } - } -} - -+ (InaBluetoothAuthorizationStatus)centralAuthorizationStatus { - if (@available(iOS 13.1, *)) { - switch(CBCentralManager.authorization) { - case CBManagerAuthorizationNotDetermined: return InaBluetoothAuthorizationStatusNotDetermined; - case CBManagerAuthorizationRestricted: return InaBluetoothAuthorizationStatusRestricted; - case CBManagerAuthorizationDenied: return InaBluetoothAuthorizationStatusDenied; - case CBManagerAuthorizationAllowedAlways: return InaBluetoothAuthorizationStatusAuthorized; - } - } else { - return InaBluetoothAuthorizationStatusAuthorized; - } -} - -@end - -////////////////////////////////////// -// InaBluetoothAuthorizationStatus - -NSString* InaBluetoothAuthorizationStatusToString(InaBluetoothAuthorizationStatus value) { - switch (value) { - case InaBluetoothAuthorizationStatusNotDetermined: return @"not_determined"; - case InaBluetoothAuthorizationStatusRestricted: return @"not_supported"; - case InaBluetoothAuthorizationStatusDenied: return @"denied"; - case InaBluetoothAuthorizationStatusAuthorized: return @"allowed"; - } -} - -InaBluetoothAuthorizationStatus InaBluetoothAuthorizationStatusFromString(NSString *value) { - if ([value isEqualToString:@"not_determined"]) { - return InaBluetoothAuthorizationStatusNotDetermined; - } - else if ([value isEqualToString:@"not_supported"]) { - return InaBluetoothAuthorizationStatusRestricted; - } - else if ([value isEqualToString:@"denied"]) { - return InaBluetoothAuthorizationStatusDenied; - } - else if ([value isEqualToString:@"allowed"]) { - return InaBluetoothAuthorizationStatusAuthorized; - } - else { - return InaBluetoothAuthorizationStatusNotDetermined; - } -} - -////////////////////////////////////// -// CBPeripheral+InaUtils - -@implementation CBPeripheral(InaUtils) - -- (CBService*)inaServiceWithUUID:(CBUUID*)uuid { - for (CBService *service in self.services) { - if([uuid isEqual: service.UUID]){ - return service; - } - } - return nil; -} - -@end - -////////////////////////////////////// -// CBService+InaUtils - -@implementation CBService(InaUtils) - -- (CBCharacteristic*)inaCharacteristicWithUUID:(CBUUID *)uuid { - for(CBCharacteristic *characteristic in self.characteristics){ - if([characteristic.UUID isEqual:uuid]) { - return characteristic; - } - } - return nil; -} - -- (CBMutableCharacteristic*)inaMutableCharacteristicWithUUID:(CBUUID*)uuid; { - CBCharacteristic *characteristic = [self inaCharacteristicWithUUID:uuid]; - return [characteristic isKindOfClass:[CBMutableCharacteristic class]] ? ((CBMutableCharacteristic*)characteristic) : nil; -} - -@end - -////////////////////////////////////// -// NSDictionary+InaBluetoothUtils - -@implementation NSDictionary(InaBluetoothUtils) - -- (bool)inaAdvertisementDataContainsServiceWithUuid:(CBUUID*)serviceUuid { - NSArray *serviceUuids = [self inaArrayForKey:CBAdvertisementDataServiceUUIDsKey]; - for (CBUUID *peripheralServiceUuid in serviceUuids) { - if ([peripheralServiceUuid isEqual:serviceUuid]) { - return true; - } - } - - serviceUuids = [self inaArrayForKey: CBAdvertisementDataOverflowServiceUUIDsKey]; - for (CBUUID *peripheralServiceUuid in serviceUuids) { - if ([peripheralServiceUuid isEqual:serviceUuid]) { - return true; - } - } - - return false; -} - -@end diff --git a/lib/main.dart b/lib/main.dart index 3db2799d..d86c1f71 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:illinois/service/UserProfile.dart'; import 'package:illinois/service/Config.dart'; import 'package:illinois/service/NotificationService.dart'; import 'package:illinois/service/Service.dart'; +import 'package:illinois/ui/onboarding/OnboardingNotificationPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingUpgradePanel.dart'; import 'package:illinois/service/Log.dart'; @@ -119,6 +120,8 @@ class _AppState extends State implements NotificationsListener { String _lastRunVersion; String _upgradeRequiredVersion; String _upgradeAvailableVersion; + Map _configNotification; + DateTime _pausedDateTime; Key key = UniqueKey(); @override @@ -130,9 +133,11 @@ class _AppState extends State implements NotificationsListener { Config.notifyUpgradeAvailable, Config.notifyUpgradeRequired, Config.notifyOnboardingRequired, + Config.notifyNotificationAvailable, Organizations.notifyOrganizationChanged, Organizations.notifyEnvironmentChanged, UserProfile.notifyProfileDeleted, + AppLivecycle.notifyStateChanged, ]); AppLivecycle.instance.ensureBinding(); @@ -140,6 +145,7 @@ class _AppState extends State implements NotificationsListener { _lastRunVersion = Storage().lastRunVersion; _upgradeRequiredVersion = Config().upgradeRequiredVersion; _upgradeAvailableVersion = Config().upgradeAvailableVersion; + _configNotification = _checkConfigNotification(); _checkForceOnboarding(); @@ -188,6 +194,9 @@ class _AppState extends State implements NotificationsListener { else if (_upgradeAvailableVersion != null) { return OnboardingUpgradePanel(availableVersion:_upgradeAvailableVersion); } + else if (_configNotification != null) { + return OnboardingNotificationPanel(notification: _configNotification, onClose: _onConfigNotificationClosed); + } else if (!Storage().onBoardingPassed) { return Onboarding().startPanel; } @@ -222,6 +231,37 @@ class _AppState extends State implements NotificationsListener { return false; } + Map _checkConfigNotification({Map notification, String requiredDisplay}) { + notification = notification ?? Config().notification; + + String notificationId = (notification != null) ? AppJson.stringValue(notification['id']) : null; + String display = (notification != null) ? AppJson.stringValue(notification['display']) : null; + if ((display == 'once') && (notificationId != null)) { + Set reportedNotifications = Storage().reportedConfigNotifictions; + if ((reportedNotifications != null) && reportedNotifications.contains(notificationId)) { + return null; // already displayed + } + } + + if ((requiredDisplay != null) && (requiredDisplay != display)) { + return null; // not match + } + + return notification; + } + + void _onConfigNotificationClosed(Map notification) { + + String notificationId = (notification != null) ? AppJson.stringValue(notification['id']) : null; + if (notificationId != null) { + Storage().reportedConfigNotifiction = notificationId; + } + + setState(() { + _configNotification = null; + }); + } + // NotificationsListener @override @@ -244,6 +284,11 @@ class _AppState extends State implements NotificationsListener { _resetUI(); } } + else if (name == Config.notifyNotificationAvailable) { + setState(() { + _configNotification = _checkConfigNotification(notification: param); + }); + } else if (name == Organizations.notifyOrganizationChanged) { _resetUI(); } @@ -253,5 +298,27 @@ class _AppState extends State implements NotificationsListener { else if (name == UserProfile.notifyProfileDeleted) { _resetUI(); } + else if (name == AppLivecycle.notifyStateChanged) { + _onAppLivecycleStateChanged(param); + } } + + void _onAppLivecycleStateChanged(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + _pausedDateTime = DateTime.now(); + } + else if (state == AppLifecycleState.resumed) { + if (_pausedDateTime != null) { + Duration pausedDuration = DateTime.now().difference(_pausedDateTime); + if (Config().refreshTimeout < pausedDuration.inSeconds) { + if (Storage().onBoardingPassed) { + setState(() { + _configNotification = _checkConfigNotification(requiredDisplay: 'verbose') ?? _configNotification; + }); + } + } + } + } + } + } diff --git a/lib/model/Exposure.dart b/lib/model/Exposure.dart deleted file mode 100644 index 2afa6276..00000000 --- a/lib/model/Exposure.dart +++ /dev/null @@ -1,147 +0,0 @@ - - - -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; - -/////////////////////////////// -// ExposureTEK - -class ExposureTEK { - final String tek; - final int timestamp; - final int expirestamp; - - ExposureTEK({this.tek, this.timestamp, this.expirestamp}); - - factory ExposureTEK.fromJson(Map json) { - return (json != null) ? ExposureTEK( - tek: json['tek'], - timestamp: json['timestamp'], - expirestamp: json['expirestamp'], - ) : null; - } - - Map toJson() { - return { - 'tek': tek, - 'timestamp': timestamp, - 'expirestamp': expirestamp, - }; - } - - DateTime get dateUtc { - return (timestamp != null) ? DateTime.fromMillisecondsSinceEpoch(timestamp, isUtc: true) : null; - } - - DateTime get expireUtc { - return (expirestamp != null) ? DateTime.fromMillisecondsSinceEpoch(expirestamp, isUtc: true) : null; - } - - static List listFromJson(List json) { - List values; - if (json != null) { - values = []; - for (dynamic entry in json) { - ExposureTEK value; - try { value = ExposureTEK.fromJson((entry as Map)?.cast()); } - catch(e) { print(e?.toString()); } - values.add(value); - } - } - return values; - } - - static List listToJson(List values) { - List json; - if (values != null) { - json = []; - for (ExposureTEK value in values) { - json.add(value?.toJson()); - } - } - return json; - } - - static Map mapFromJson(List json) { - Map result; - if (json != null) { - result = Map(); - for (dynamic entry in json) { - ExposureTEK value; - try { value = ExposureTEK.fromJson((entry as Map)?.cast()); } - catch(e) { print(e?.toString()); } - if (value.tek != null) { - result[value.tek] = value; - } - } - } - return result; - } - - static List mapToJson(Map entries) { - List json; - if (entries != null) { - json = []; - for (ExposureTEK value in entries.values) { - json.add(value?.toJson()); - } - } - return json; - } -} - -/////////////////////////////// -// ExposureRecord - -class ExposureRecord { - final int id; - final String rpi; - final int timestamp; - final int duration; - - ExposureRecord({this.id, this.rpi, this.timestamp, this.duration}); - - DateTime get dateUtc { - return (timestamp != null) ? DateTime.fromMillisecondsSinceEpoch(timestamp, isUtc: true) : null; - } - - int get durationInMinutes { - return (duration ~/ 60000); // milliseconds -> minutes - } - - String get durationDisplayString { - int durationInSeconds = (duration != null) ? duration ~/ 1000 : null; - if (durationInSeconds != null) { - if (durationInSeconds < 60) { - return "$durationInSeconds sec" + (1 != durationInSeconds ? "s" : ""); - } - else { - int durationInMinutes = durationInSeconds ~/ 60; - if (durationInMinutes < TimeOfDay.minutesPerHour) { - return "$durationInMinutes min" + (1 != durationInMinutes ? "s" : ""); - } else { - int exposureHours = durationInMinutes ~/ TimeOfDay.minutesPerHour; - return "$exposureHours hr" + (1 != exposureHours ? "s" : ""); - } - - } - } - return null; - } -} diff --git a/lib/model/Health.dart b/lib/model/Health.dart index 16349a49..7a99b66d 100644 --- a/lib/model/Health.dart +++ b/lib/model/Health.dart @@ -328,10 +328,6 @@ class HealthStatusBlob { return (nextStep?.toLowerCase()?.contains("test") ?? false) || (nextStepHtml?.toLowerCase()?.contains("test") ?? false); } - - bool reportsExposures({HealthRulesSet rules}) { - return (rules?.codes[code]?.reportsExposures == true); - } } //////////////////////////////// @@ -513,6 +509,10 @@ class HealthHistory implements Comparable { return (dateUtc != null) ? AppDateTime.midnight(dateUtc.toLocal()) : null; } + DateTime getDateMidnightLocal({int offsetInDays}) { + return (dateUtc != null) ? AppDateTime.midnight(dateUtc.toLocal(), offsetInDays: offsetInDays) : null; + } + bool matchPendingEvent(HealthPendingEvent event) { if (event.isTest) { return this.isTest && @@ -528,7 +528,7 @@ class HealthHistory implements Comparable { (this.dateUtc == event?.blob?.dateUtc) && (this.blob?.provider == event?.provider) && (this.blob?.providerId == event?.providerId) && - (this.blob?.vaccine == event?.blob?.vaccine); + (this.blob?.vaccineStatus == event?.blob?.vaccineStatus); } else if (event.isAction) { return this.isAction && @@ -692,17 +692,26 @@ class HealthHistory implements Comparable { return null; } - static HealthHistory mostRecentVaccine(List history, { String vaccine }) { + static int mostRecentVaccineIndex(List history, { String vaccineStatus }) { if (history != null) { for (int index = 0; index < history.length; index++) { HealthHistory historyEntry = history[index]; - if (historyEntry.isVaccine && ((vaccine == null) || (historyEntry.blob?.vaccine?.toLowerCase() == vaccine?.toLowerCase()))) { - return historyEntry; + if (historyEntry.isVaccine && ((vaccineStatus == null) || (historyEntry.blob?.vaccineStatus?.toLowerCase() == vaccineStatus?.toLowerCase()))) { + return index; } } } return null; } + + static DateTime getVaccineExpireDateLocal({List history, int vaccineIndex, HealthRulesSet rules }) { + HealthHistory vaccine = ((history != null) && (vaccineIndex != null) && (0 <= vaccineIndex) && (vaccineIndex < history.length)) ? history[vaccineIndex] : null; + if (vaccine?.blob?.isVaccineEffective ?? false) { + int vaccineBoosterInterval = rules?.getInterval(HealthRulesSet.VaccineBoosterInterval)?.value(history: history, historyIndex: vaccineIndex, rules: rules); + return ((vaccineBoosterInterval != null) && (vaccineBoosterInterval > 0)) ? vaccine.getDateMidnightLocal(offsetInDays: vaccineBoosterInterval + 1) : null; + } + return null; + } } //////////////////////////////// @@ -722,7 +731,7 @@ class HealthHistoryBlob { final int traceDuration; final String traceTEK; - final String vaccine; + final String vaccineStatus; final String actionType; final dynamic actionTitle; @@ -732,12 +741,13 @@ class HealthHistoryBlob { final List extras; static const String VaccineEffective = "Effective"; + static const String VaccineManifacturer = "Vaccine"; HealthHistoryBlob({ this.provider, this.providerId, this.location, this.locationId, this.countyId, this.testType, this.testResult, this.symptoms, this.traceDuration, this.traceTEK, - this.vaccine, + this.vaccineStatus, this.actionType, this.actionTitle, this.actionText, this.actionParams, this.extras }); @@ -757,7 +767,7 @@ class HealthHistoryBlob { traceDuration: json['trace_duration'], traceTEK: json['trace_tek'], - vaccine: json['vaccine'], + vaccineStatus: json['vaccine'], actionType: json['action_type'], actionTitle: json['action_title'], @@ -784,7 +794,7 @@ class HealthHistoryBlob { 'trace_duration': traceDuration, 'trace_tek': traceTEK, - 'vaccine': vaccine, + 'vaccine': vaccineStatus, 'action_type': actionType, 'action_title': actionTitle, @@ -810,7 +820,7 @@ class HealthHistoryBlob { (o.traceDuration == traceDuration) && (o.traceTEK == traceTEK) && - (o.vaccine == vaccine) && + (o.vaccineStatus == vaccineStatus) && (o.actionType == actionType) && DeepCollectionEquality().equals(o.actionTitle, actionTitle) && @@ -834,7 +844,7 @@ class HealthHistoryBlob { (traceDuration?.hashCode ?? 0) ^ (traceTEK?.hashCode ?? 0) ^ - (vaccine?.hashCode ?? 0) ^ + (vaccineStatus?.hashCode ?? 0) ^ (actionType?.hashCode ?? 0) ^ (DeepCollectionEquality().hash(actionTitle) ?? 0) ^ @@ -856,11 +866,15 @@ class HealthHistoryBlob { } bool get isVaccine { - return (vaccine != null); + return (vaccineStatus != null); } bool get isVaccineEffective { - return (vaccine != null) && (vaccine.toLowerCase() == VaccineEffective.toLowerCase()); + return (vaccineStatus != null) && (vaccineStatus.toLowerCase() == VaccineEffective.toLowerCase()); + } + + String get vaccineManifacturer { + return HealthEventExtra.listEntry(extras, displayName: VaccineManifacturer)?.displayValue; } bool get isAction { @@ -1069,7 +1083,7 @@ class HealthPendingEventBlob { final String testType; final String testResult; - final String vaccine; + final String vaccineStatus; final String actionType; final dynamic actionTitle; @@ -1080,7 +1094,7 @@ class HealthPendingEventBlob { HealthPendingEventBlob({this.dateUtc, this.testType, this.testResult, - this.vaccine, + this.vaccineStatus, this.actionType, this.actionTitle, this.actionText, this.actionParams, this.extras}); @@ -1091,7 +1105,7 @@ class HealthPendingEventBlob { testType: AppJson.stringValue(json['TestName']), testResult: AppJson.stringValue(json['Result']), - vaccine: AppJson.stringValue(json['Vaccine']), + vaccineStatus: AppJson.stringValue(json['Vaccine']), actionType: AppJson.stringValue(json['ActionType']), actionTitle: json['ActionTitle'], @@ -1111,10 +1125,10 @@ class HealthPendingEventBlob { 'Extra': HealthEventExtra.listToJson(extras), }; } - else if (vaccine != null) { + else if (vaccineStatus != null) { return { 'Date': healthDateTimeToString(dateUtc), - 'Vaccine': vaccine, + 'Vaccine': vaccineStatus, 'Extra': HealthEventExtra.listToJson(extras), }; } @@ -1141,7 +1155,7 @@ class HealthPendingEventBlob { } bool get isVaccine { - return (vaccine != null); + return (vaccineStatus != null); } bool get isAction { @@ -1236,6 +1250,18 @@ class HealthEventExtra { } return false; } + + static HealthEventExtra listEntry(List values, { String displayName }) { + if (values != null) { + for (HealthEventExtra value in values) { + if ((displayName != null) && (displayName.toLowerCase() != value?.displayName?.toLowerCase())) { + continue; + } + return value; + } + } + return null; + } } /////////////////////////////// @@ -1627,6 +1653,68 @@ class HealthOSFAuth { } } +/////////////////////////////// +// HealthUserOverride + +class HealthUserOverride { + final int testInterval; + final DateTime testIntervalStartDateUtc; + final DateTime testIntervalEndDateUtc; + final bool vaccinationExempt; + + HealthUserOverride({this.testInterval, this.testIntervalStartDateUtc, this.testIntervalEndDateUtc, this.vaccinationExempt }); + + factory HealthUserOverride.fromJson(Map json) { + return (json != null) ? HealthUserOverride( + testInterval: AppJson.intValue(json['interval']), + testIntervalStartDateUtc: healthDateTimeFromString(AppJson.stringValue(json['activation'])), + testIntervalEndDateUtc: healthDateTimeFromString(AppJson.stringValue(json['expiration'])), + vaccinationExempt: AppJson.boolValue(json['exempt']), + ) : null; + } + + Map toJson() { + return { + 'interval': testInterval, + 'activation': healthDateTimeToString(testIntervalStartDateUtc), + 'expiration': healthDateTimeToString(testIntervalEndDateUtc), + "exempt" : vaccinationExempt + }; + } + + bool operator ==(o) { + return (o is HealthUserOverride) && + (o.testInterval == testInterval) && + (o.testIntervalStartDateUtc == testIntervalStartDateUtc) && + (o.testIntervalEndDateUtc == testIntervalEndDateUtc) && + (o.vaccinationExempt == vaccinationExempt); + } + + int get hashCode => + (testInterval?.hashCode ?? 0) ^ + (testIntervalStartDateUtc?.hashCode ?? 0) ^ + (testIntervalEndDateUtc?.hashCode ?? 0) ^ + (vaccinationExempt?.hashCode ?? 0); + + int get effectiveTestInterval { + if (testInterval == null) { + return null; + } + else { + DateTime nowUtc = ((testIntervalStartDateUtc != null) || (testIntervalEndDateUtc != null)) ? DateTime.now().toUtc() : null; + if ((testIntervalStartDateUtc != null) && testIntervalStartDateUtc.isAfter(nowUtc)) { + return null; + } + if ((testIntervalEndDateUtc != null) && testIntervalEndDateUtc.isBefore(nowUtc)) { + return null; + } + return testInterval; + } + } +} + + + /////////////////////////////// // HealthServiceProvider @@ -2673,6 +2761,7 @@ class HealthRulesSet { static const String UserTestMonitorInterval = 'UserTestMonitorInterval'; + static const String VaccineBoosterInterval = 'VaccineBoosterInterval'; static const String FamilyMemberTestPrice = 'FamilyMemberTestPrice'; HealthRulesSet({this.tests, this.symptoms, this.contactTrace, this.vaccines, this.actions, this.defaults, HealthCodesSet codes, this.statuses, this.intervals, Map constants, Map strings}) : @@ -2736,7 +2825,7 @@ class HealthRulesSet { DeepCollectionEquality().hash(intervals) ^ DeepCollectionEquality().hash(strings); - _HealthRuleInterval _getInterval(String name) { + _HealthRuleInterval getInterval(String name) { return (intervals != null) ? intervals[name] : null; } @@ -2865,9 +2954,8 @@ class HealthCodeData { final String _description; final String _longDescription; final bool visible; - final bool reportsExposures; - HealthCodeData({this.code, String color, String name, String description, String longDescription, this.visible, this.reportsExposures}) : + HealthCodeData({this.code, String color, String name, String description, String longDescription, this.visible,}) : _colorString = color, _color = UiColors.fromHex(color), _name = name, @@ -2882,7 +2970,6 @@ class HealthCodeData { description: json['description'], longDescription: json['long_description'], visible: json['visible'], - reportsExposures: json['reports_exposures'] ) : null; } @@ -2894,7 +2981,6 @@ class HealthCodeData { 'description': _description, 'long_description': _longDescription, 'visible': visible, - 'reports_exposures': reportsExposures }; } @@ -2905,8 +2991,7 @@ class HealthCodeData { (o._name == _name) && (o._description == _description) && (o._longDescription == _longDescription) && - (o.visible == visible) && - (o.reportsExposures == reportsExposures); + (o.visible == visible); int get hashCode => (code?.hashCode ?? 0) ^ @@ -2914,8 +2999,7 @@ class HealthCodeData { (_name?.hashCode ?? 0) ^ (_description?.hashCode ?? 0) ^ (_longDescription?.hashCode ?? 0) ^ - (visible?.hashCode ?? 0) ^ - (reportsExposures?.hashCode ?? 0); + (visible?.hashCode ?? 0); Color get color { return _color; @@ -3420,32 +3504,32 @@ class HealthVaccineRulesSet { // HealthVaccineRule class HealthVaccineRule { - final String vaccine; + final String vaccineStatus; final _HealthRuleStatus status; - HealthVaccineRule({this.vaccine, this.status}); + HealthVaccineRule({this.vaccineStatus, this.status}); factory HealthVaccineRule.fromJson(Map json) { return (json != null) ? HealthVaccineRule( - vaccine: json['vaccine'], + vaccineStatus: json['vaccine_status'], status: _HealthRuleStatus.fromJson(json['status']), ) : null; } Map toJson() { return { - 'vaccine': vaccine, + 'vaccine_status': vaccineStatus, 'status': status?.toJson(), }; } bool operator ==(o) => (o is HealthVaccineRule) && - (o.vaccine == vaccine) && + (o.vaccineStatus == vaccineStatus) && (o.status == status); int get hashCode => - (vaccine?.hashCode ?? 0) ^ + (vaccineStatus?.hashCode ?? 0) ^ (status?.hashCode ?? 0); static List listFromJson(List json) { @@ -3472,7 +3556,7 @@ class HealthVaccineRule { } bool _matchBlob(HealthHistoryBlob blob, {HealthRulesSet rules}) { - return (vaccine != null) && (vaccine.toLowerCase() == blob?.vaccine?.toLowerCase()); + return (vaccineStatus != null) && (vaccineStatus.toLowerCase() == blob?.vaccineStatus?.toLowerCase()); } } @@ -4282,7 +4366,7 @@ class HealthRuleIntervalReference extends _HealthRuleInterval { _HealthRuleInterval _referenceInterval({HealthRulesSet rules, Map params }) { _HealthRuleInterval referenceParamInterval = (params != null) ? _HealthRuleInterval.fromJson(params[_reference]) : null; - return (referenceParamInterval != null) ? referenceParamInterval : rules?._getInterval(_reference); + return (referenceParamInterval != null) ? referenceParamInterval : rules?.getInterval(_reference); } @override @@ -4428,6 +4512,10 @@ abstract class HealthRuleCondition { // true / false / null result = _evalTestInterval(conditionParams, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); } + else if (condition == 'test-vaccine') { + // true / false / null + result = _evalTestVaccine(conditionParams, history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); + } if (result is bool) { return HealthRuleConditionResult(result: result); @@ -4515,8 +4603,8 @@ abstract class HealthRuleCondition { } } else if (_matchValue(historyType, HealthHistoryType.vaccine)) { - dynamic vaccine = (conditionParams != null) ? conditionParams['vaccine'] : null; - if ((vaccine != null) && !_matchStringTarget(source: entry?.blob?.vaccine, target: vaccine)) { + dynamic vaccineStatus = (conditionParams != null) ? conditionParams['status'] : null; + if ((vaccineStatus != null) && !_matchStringTarget(source: entry?.blob?.vaccineStatus, target: vaccineStatus)) { return false; } } @@ -4561,6 +4649,22 @@ abstract class HealthRuleCondition { return interval?.valid(history: history, historyIndex: historyIndex, referenceIndex: referenceIndex, rules: rules, params: params); } + static bool _evalTestVaccine(Map conditionParams, { List history, int historyIndex, int referenceIndex, HealthRulesSet rules, Map params }) { + String manifacturer = (conditionParams != null) ? conditionParams['manifacturer'] : null; + if (history != null) { + int originIndex = ((referenceIndex != null) && (0 <= referenceIndex) && (referenceIndex < history.length)) ? referenceIndex : historyIndex; + HealthHistory originEntry = ((originIndex != null) && (0 <= originIndex) && (originIndex < history.length)) ? history[originIndex] : null; + if (originEntry?.type == HealthHistoryType.vaccine) { + if ((manifacturer != null) && !_matchStringTarget(target: originEntry?.blob?.vaccineManifacturer, source: manifacturer)) { + return false; + } + return true; + } + return false; + } + return null; + } + static bool _evalTestUser(Map conditionParams, { HealthRulesSet rules, Map params }) { dynamic role = (conditionParams != null) ? conditionParams['role'] : null; diff --git a/lib/service/Analytics.dart b/lib/service/Analytics.dart index 1f6058dd..a0576bcf 100644 --- a/lib/service/Analytics.dart +++ b/lib/service/Analytics.dart @@ -193,13 +193,8 @@ class Analytics with Service implements NotificationsListener { static const String LogHealthProviderTestProcessedAction = "provider_test_processed"; static const String LogHealthManualTestSubmittedAction = "manual_test_submitted"; static const String LogHealthSymptomsSubmittedAction = "symptoms_submitted"; - static const String LogHealthContactTraceProcessedAction = "contact_trace_processed"; - static const String LogHealthContactTraceTestAction = "contact_trace_test"; static const String LogHealthActionProcessedAction = "action_processed"; static const String LogHealthVaccinationAction = "vaccination"; - static const String LogHealthReportExposuresAction = "report_exposures"; - static const String LogHealthCheckExposuresAction = "check_exposures"; - static const String LogHealthExposureStatisticsAction = "exposure_statistics"; static const String LogHealthStatusName = "status"; static const String LogHealthPrevStatusName = "previous_status"; static const String LogHealthSettingConsentTestResultsName = "consent_test_results"; @@ -210,28 +205,12 @@ class Analytics with Service implements NotificationsListener { static const String LogHealthTestTypeName = "test_type"; static const String LogHealthTestResultName = "test_result"; static const String LogHealthSymptomsName = "symptoms"; - static const String LogHealthDurationName = "duration"; - static const String LogHealthExposureTimestampName = "exposure_timestamp"; - static const String LogHealthVaccineName = "vaccine"; + static const String LogHealthVaccineStatusName = "vaccine_status"; static const String LogHealthActionTypeName = "action_type"; static const String LogHealthActionTitleName = "action_title"; static const String LogHealthActionTextName = "action_text"; static const String LogHealthActionParamsName = "action_params"; static const String LogHealthActionTimestampName = "action_timestamp"; - static const String LogHealthExposureScoreName = "exposure_score"; - static const String LogRpiSeen6HoursName = "rpi_seen_6_hour"; - static const String LogRpiSeen24HoursName = "rpi_seen_24_hour"; - static const String LogRpiSeen168HoursName = "rpi_seen_168_hour"; - static const String LogRpiMatches6HoursName = "rpi_matches_6_hour"; - static const String LogRpiMatches24HoursName = "rpi_matches_24_hour"; - static const String LogRpiMatches168HoursName = "rpi_matches_168_hour"; - static const String LogExposureUpTime6HoursName = "exposure_uptime_6_hour"; - static const String LogExposureUpTime24HoursName = "exposure_uptime_24_hour"; - static const String LogExposureUpTime168HoursName = "exposure_uptime_168_hour"; - static const String LogTestFrequency168HoursName = "test_frequency_168_hour"; - static const String LogExposureNotification168HoursName = "exposure_notification_168_hour"; - static const String LogTestResult168HoursName = "test_result_168_hour"; - static const String LogEpochName = "epoch"; // Event Attributes static const String LogAttributeUrl = "url"; diff --git a/lib/service/BluetoothServices.dart b/lib/service/BluetoothServices.dart deleted file mode 100644 index 9ecf12ee..00000000 --- a/lib/service/BluetoothServices.dart +++ /dev/null @@ -1,133 +0,0 @@ - -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:illinois/service/AppLivecycle.dart'; -import 'package:illinois/service/NativeCommunicator.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Service.dart'; - -class BluetoothServices with Service implements NotificationsListener { - - static const String notifyStatusChanged = "edu.illinois.rokwire.bluetoothservices.status.changed"; - - BluetoothStatus _status; - - // Singletone Instance - - BluetoothServices._internal(); - static final BluetoothServices _instance = BluetoothServices._internal(); - - factory BluetoothServices() { - return _instance; - } - - static BluetoothServices get instance { - return _instance; - } - - // Iniitlaization - - @override - void createService() { - NotificationService().subscribe(this, [ - AppLivecycle.notifyStateChanged, - ]); - } - - @override - void destroyService() { - NotificationService().unsubscribe(this); - } - - @override - Future initService() async { - _status = await _getStatus(); - } - - // NotificationsListener - - @override - void onNotification(String name, dynamic param) { - if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); - } - } - - void _onAppLivecycleStateChanged(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - _checkStatus(); - } - } - - // API - - BluetoothStatus get status { - return _status; - } - - Future _getStatus() async { - if (Platform.isIOS) { - return _bluetoothStatusFromString(await NativeCommunicator().queryBluetoothAuthorization('query')); - } - else { - return BluetoothStatus.PermissionAllowed; - } - } - - void _checkStatus() { - _getStatus().then((BluetoothStatus status){ - if (_status != status) { - _status = status; - NotificationService().notify(notifyStatusChanged, null); - } - }); - } - - Future requestStatus() async { - if (Platform.isIOS && (_status == BluetoothStatus.PermissionNotDetermined)) { - BluetoothStatus status = _bluetoothStatusFromString(await NativeCommunicator().queryBluetoothAuthorization('request')); - if (_status != status) { - _status = status; - NotificationService().notify(notifyStatusChanged, null); - } - } - return _status; - } -} - -enum BluetoothStatus { - PermissionNotDetermined, - PermissionNotSupported, // iOS Emulator - PermissionDenied, - PermissionAllowed -} - -BluetoothStatus _bluetoothStatusFromString(String value){ - if("not_determined" == value) - return BluetoothStatus.PermissionNotDetermined; - if("not_supported" == value) - return BluetoothStatus.PermissionNotSupported; // iOS Emulator - else if("denied" == value) - return BluetoothStatus.PermissionDenied; - else if("allowed" == value) - return BluetoothStatus.PermissionAllowed; - else - return null; -} diff --git a/lib/service/Config.dart b/lib/service/Config.dart index 5ea61268..f3abfe02 100644 --- a/lib/service/Config.dart +++ b/lib/service/Config.dart @@ -39,10 +39,11 @@ class Config with Service implements NotificationsListener { static const String _configFileName = "config.json"; - static const String notifyUpgradeRequired = "edu.illinois.rokwire.config.upgrade.required"; - static const String notifyUpgradeAvailable = "edu.illinois.rokwire.config.upgrade.available"; - static const String notifyOnboardingRequired = "edu.illinois.rokwire.config.onboarding.required"; - static const String notifyConfigChanged = "edu.illinois.rokwire.config.changed"; + static const String notifyUpgradeRequired = "edu.illinois.rokwire.config.upgrade.required"; + static const String notifyUpgradeAvailable = "edu.illinois.rokwire.config.upgrade.available"; + static const String notifyOnboardingRequired = "edu.illinois.rokwire.config.onboarding.required"; + static const String notifyNotificationAvailable = "edu.illinois.rokwire.config.notification.available"; + static const String notifyConfigChanged = "edu.illinois.rokwire.config.changed"; Map _config; @@ -96,6 +97,7 @@ class Config with Service implements NotificationsListener { if (_config != null) { _checkUpgrade(); _checkOnboarding(); + _checkNotification(); _updateFromNet(); } else if (Organizations().organization != null) { @@ -108,6 +110,7 @@ class Config with Service implements NotificationsListener { _checkUpgrade(); _checkOnboarding(); + _checkNotification(); } } else { @@ -200,6 +203,7 @@ class Config with Service implements NotificationsListener { _checkUpgrade(); _checkOnboarding(); + _checkNotification(); } }); } @@ -312,6 +316,15 @@ class Config with Service implements NotificationsListener { } } + // Notification + + void _checkNotification() { + Map value; + if ((value = this.notification) != null) { + NotificationService().notify(notifyNotificationAvailable, value); + } + } + // Assets cache path Directory get appDocumentsDir { @@ -357,12 +370,12 @@ class Config with Service implements NotificationsListener { Map get settings { return (_config != null) ? (_config['settings'] ?? {}) : {}; } Map get upgradeInfo { return (_config != null) ? (_config['upgrade'] ?? {}) : {}; } Map get onboardingInfo { return (_config != null) ? (_config['onboarding'] ?? {}) : {}; } + Map get notification { return (_config != null) ? _config['notification'] : null; } String get assetsUrl { return otherUniversityServices['assets_url']; } // "https://rokwire-assets.s3.us-east-2.amazonaws.com" String get getHelpUrl { return otherUniversityServices['get_help_url']; } // "https://forms.illinois.edu/sec/4961936" String get iCardUrl { return otherUniversityServices['icard_url']; } // "https://www.icard.uillinois.edu/rest/rw/rwIDData/rwCardInfo" String get privacyPolicyUrl { return otherUniversityServices['privacy_policy_url']; } // "https://www.vpaa.uillinois.edu/resources/web_privacy" - String get exposureLogUrl { return otherUniversityServices['exposure_log_url']; } // "http://ec2-18-191-37-235.us-east-2.compute.amazonaws.com:8003/PostSessionData" String get oidcAuthUrl { return oidc['auth_url']; } // "https://shibboleth.illinois.edu/idp/profile/oidc/authorize" String get oidcTokenUrl { return oidc['token_url']; } // "https://{oidc_client_id}:{oidc_client_secret}@shibboleth.illinois.edu/idp/profile/oidc/token" diff --git a/lib/service/Exposure.dart b/lib/service/Exposure.dart deleted file mode 100644 index bf086ae7..00000000 --- a/lib/service/Exposure.dart +++ /dev/null @@ -1,1276 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart'; -import 'package:illinois/model/Exposure.dart'; -import 'package:illinois/model/Health.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/AppLivecycle.dart'; -import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/BluetoothServices.dart'; -import 'package:illinois/service/Config.dart'; -import 'package:illinois/service/Health.dart'; -import 'package:illinois/service/Log.dart'; -import 'package:illinois/service/Network.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Service.dart'; -import 'package:illinois/service/Storage.dart'; -import 'package:illinois/utils/Utils.dart'; - -import 'package:sqflite/sqflite.dart'; -import 'package:path/path.dart'; - - -class Exposure with Service implements NotificationsListener { - - // Notifications - - static const String notifyStartStop = "edu.illinois.rokwire.exposure.start_stop"; - static const String notifyTEKsUpdated = "edu.illinois.rokwire.exposure.teks.updated"; - static const String notifyExposureUpdated = "edu.illinois.rokwire.exposure.expsure.updated"; - static const String notifyExposureThick = "edu.illinois.rokwire.exposure.expsure.thick"; - - // Native - - static const String _methodChannelName = 'edu.illinois.covid/exposure'; - - static const String _startMethodName = 'start'; - static const String _stopMethodName = 'stop'; - static const String _teksMethodName = 'TEKs'; - static const String _tekRPIsMethodName = 'tekRPIs'; - static const String _expireTEKMethodName = 'expireTEK'; - static const String _exposureRPIMethodName = 'exposureRPILog'; - static const String _exposureRSSIMethodName = 'exposureRSSILog'; - static const String _expUpTimeMethodName = 'exposureUpTime'; - - static const String _settingsParamName = 'settings'; - static const String _tekParamName = 'tek'; - static const String _timestampParamName = 'timestamp'; - static const String _expirestampParamName = 'expirestamp'; - static const String _upTimeWinParamName = 'upTimeWindow'; - - static const String _tecNotificationName = 'tek'; - static const String _exposureNotificationName = 'exposure'; - static const String _exposureThickNotificationName = 'exposureThick'; - - // Database - - static const String _databaseName = "exposures.db"; - static const int _databaseVersion = 1; - - static const String _databaseExposureTable = "Exposures"; - static const String _databaseExposureTimestampField = "Timestamp"; - static const String _databaseExposureRPIField = "RPI"; - static const String _databaseExposureDurationField = "Duration"; - static const String _databaseExposureProcessedField = "Processed"; - - static const String _databaseRpiTable = "ExposureRpi"; - static const String _databaseRpiSessionIdField = "SessionId"; - static const String _databaseRpiTEKField = "TEK"; - static const String _databaseRpiTEKStartTimeField = "TEKStartTime"; - static const String _databaseRpiRPIField = "RPI"; - static const String _databaseRpiRPIStartTimeField = "RPIStartTime"; - static const String _databaseRpiEventField = "Event"; - - static const String _databaseContactTable = "ExposureContact"; - static const String _databaseContactSessionIdField = "SessionId"; - static const String _databaseContactStartTimeField = "StartTime"; - static const String _databaseContactDurationField = "Duration"; - static const String _databaseContactRPIField = "RPI"; - static const String _databaseContactSourceField = "Source"; - static const String _databaseContactAddressField = "Address"; - - static const String _databaseRssiTable = "ExposureRssi"; - static const String _databaseRssiSessionIdField = "SessionId"; - static const String _databaseRssiTimestampField = "Timestamp"; - static const String _databaseRssiRSSIField = "RSSI"; - static const String _databaseRssiRPIField = "RPI"; - static const String _databaseRssiSourceField = "Source"; - static const String _databaseRssiAddressField = "Address"; - - static const String _databaseRowID = "rowid"; - - // Time - static const int _rpiRefreshInterval = (10 * 60 * 1000); // 10 min, in milisconds - static const int _rpiCheckExposureBuffer = (30 * 60 * 1000); // 30 min as buffer time - static const int _millisecondsInDay = 24 * 60 * 60 * 1000; // 1 day, in milliseconds - - // Data - final MethodChannel _methodChannel = const MethodChannel(_methodChannelName); - - Database _database; - - bool _serviceInitialized = false; - - bool _pluginInitialized = false; - bool _isPluginStarted = false; - Map _pluginSettings; - - Timer _exposuresMonitorTimer; - DateTime _pausedDateTime; - - bool _checkingReport; - bool _checkingExposures; - int _exposureMinDuration; - int _reportTargetTimestamp; - int _lastReportTimestamp; - - int _logSessionId; - - // Singletone instance - - static final Exposure _service = Exposure._internal(); - - Exposure._internal() { - _methodChannel.setMethodCallHandler(this._nativeCallback); - } - - factory Exposure() { - return _service; - } - - // Service - - @override - void createService() { - NotificationService().subscribe(this, [ - BluetoothServices.notifyStatusChanged, - Config.notifyConfigChanged, - AppLivecycle.notifyStateChanged, - Health.notifyUserUpdated, - Auth.notifyLoggedOut, - ]); - } - - @override - void destroyService() { - NotificationService().unsubscribe(this); - _closeDatabase(); - _destroyPlugin(); - _stopExposuresMonitor(); - } - - @override - Future initService() async { - _reportTargetTimestamp = Storage().exposureReportTargetTimestamp; - _lastReportTimestamp = Storage().exposureLastReportedTimestamp; - - await _openDatabase(); - - _initializePlugin().then((_) { - _pluginInitialized = true; - }); - - _updateExposureMinDuration(); - - _serviceInitialized = true; - } - - @override - void initServiceUI() { - checkExposures().then((_){ - _startExposuresMonitor(); - }); - checkReport(); - } - - @override - Future clearService() async { - await _clearDatabase(); - - _destroyPlugin(); - _pluginInitialized = _serviceInitialized = false; - - _stopExposuresMonitor(); - - _logSessionId = null; - _exposureMinDuration = null; - _checkingReport = _checkingExposures = null; - _reportTargetTimestamp = _lastReportTimestamp = null; - } - - @override - Set get serviceDependsOn { - return Set.from([Storage(), Config(), Health()]); - } - - // NotificationsListener - - @override - void onNotification(String name, dynamic param) { - if (_serviceInitialized) { - if (name == Config.notifyConfigChanged) { - _updatePlugin(forceRestart: true); - _updateExposuresMonitor(); - _updateExposureMinDuration(); - checkReport(); - } - else if (name == Health.notifyUserUpdated) { - _updatePlugin(); - _updateExposuresMonitor(); - checkReport(); - } - else if (name == Auth.notifyLoggedOut) { - _updatePlugin(); - _updateExposuresMonitor(); - } - else if (name == BluetoothServices.notifyStatusChanged) { - _updatePlugin(); - } - else if (name == AppLivecycle.notifyStateChanged) { - _onAppLivecycleStateChanged(param); - } - } - } - - void _onAppLivecycleStateChanged(AppLifecycleState state) { - if (state == AppLifecycleState.paused) { - _pausedDateTime = DateTime.now(); - _stopExposuresMonitor(); - } - else if (state == AppLifecycleState.resumed) { - Duration pausedDuration = (_pausedDateTime != null) ? DateTime.now().difference(_pausedDateTime) : null; - if ((pausedDuration != null) && (Config().refreshTimeout < pausedDuration.inSeconds)) { - _updatePlugin().then((_) { - checkReport(); - checkExposures().then((_){ - _startExposuresMonitor(); - }); - }); - } - else { - _startExposuresMonitor(); - } - } - } - - // Initialize and Destroy - - bool get _serviceEnabled { - return (Config().settings['covid19ExposureMonitorEnabled'] == true) && - (Health().userConsentExposureNotification == true); - } - - bool get _pluginEnabled { - return (BluetoothServices().status == BluetoothStatus.PermissionAllowed) && _serviceEnabled; - } - - Future _initializePlugin() async { - if (_pluginEnabled && !_isPluginStarted && _wasStarted) { - await _nativeStart(); - } - } - - Future _destroyPlugin() async { - if (_isPluginStarted) { - await _nativeStop(); - } - } - - Future _updatePlugin({bool forceRestart}) async { - if (_pluginInitialized) { - if (_pluginEnabled && _isPluginStarted && (forceRestart == true)) { - await _nativeStop(); - } - if (_pluginEnabled && !_isPluginStarted && _wasStarted) { - await _nativeStart(); - } - else if (!_pluginEnabled && _isPluginStarted) { - await _nativeStop(); - } - } - } - - // Method Channel - - Future _nativeStart({Map settings}) async { - if (settings == null) { - settings = Config().settings; - } - if (await _methodChannel.invokeMethod(_startMethodName, { _settingsParamName: settings })) { - _isPluginStarted = true; - _pluginSettings = settings; - } - } - - Future _nativeStop() async { - await _methodChannel.invokeMethod(_stopMethodName); - _isPluginStarted = false; - _pluginSettings = null; - } - - Future _expireTEK() async { - await _methodChannel.invokeMethod(_expireTEKMethodName); - } - - Future evalExposureUpTime(int hours) async { - dynamic duration = await _methodChannel.invokeMethod(_expUpTimeMethodName, { - _upTimeWinParamName : hours, - }); - - return duration is int ? duration : null; - } - - Future> loadTeks({int minStamp, int maxStamp}) async { - List teks; - List json = await _methodChannel.invokeMethod(_teksMethodName); - if (json != null) { - teks = []; - for (dynamic entry in json) { - ExposureTEK tek; - try { tek = ExposureTEK.fromJson((entry as Map)?.cast()); } - catch(e) { print(e?.toString()); } - if (((minStamp == null) || ((tek.timestamp != null) && (minStamp <= tek.timestamp))) && - ((maxStamp == null) || ((tek.timestamp != null) && (maxStamp >= tek.timestamp)))) - { - teks.add(tek); - } - } - } - return teks; - } - - Future deleteTeks() async { - await _methodChannel.invokeMethod(_teksMethodName, { - 'remove': true, - }); - } - - Future> _loadTekRPIs(ExposureTEK tek) async { - Map result = await _methodChannel.invokeMethod(_tekRPIsMethodName, { - _tekParamName : tek.tek, - _timestampParamName: tek.timestamp, - _expirestampParamName: tek.expirestamp, - }); - return result?.cast(); - } - - Future _nativeCallback(MethodCall call) async { - if (call.method == _tecNotificationName) { - NotificationService().notify(notifyTEKsUpdated, null); - _clearExpiredLocalExposures(); - } - else if (call.method == _exposureNotificationName) { - _storeLocalExposure(call.arguments); - _logContact(call.arguments); - } - else if (call.method == _exposureThickNotificationName) { - NotificationService().notify(notifyExposureThick, call.arguments); - } - else if (call.method == _exposureRPIMethodName) { - _logRpi(call.arguments); - } - else if (call.method == _exposureRSSIMethodName) { - _logRssi(call.arguments); - } - return null; - } - - // Database - - Future _openDatabase() async { - if (_database == null) { - String databasePath = await getDatabasesPath(); - String databaseFile = join(databasePath, _databaseName); - _database = await openDatabase(databaseFile, version: _databaseVersion, onCreate: (db, version) async { - try { await db.execute("CREATE TABLE IF NOT EXISTS $_databaseExposureTable($_databaseExposureTimestampField INTEGER NOT NULL, $_databaseExposureRPIField TEXT NOT NULL, $_databaseExposureDurationField INTEGER NOT NULL, $_databaseExposureProcessedField INTEGER NOT NULL DEFAULT '0')",); } catch(e) { print(e?.toString()); } - try { await db.execute("CREATE TABLE IF NOT EXISTS $_databaseRpiTable($_databaseRpiSessionIdField INTEGER, $_databaseRpiTEKField TEXT, $_databaseRpiTEKStartTimeField INTEGER, $_databaseRpiRPIField TEXT, $_databaseRpiRPIStartTimeField INTEGER, $_databaseRpiEventField TEXT)",); } catch(e) { print(e?.toString()); } - try { await db.execute("CREATE TABLE IF NOT EXISTS $_databaseContactTable($_databaseContactSessionIdField INTEGER, $_databaseContactStartTimeField INTEGER, $_databaseContactDurationField INTEGER, $_databaseContactRPIField TEXT, $_databaseContactSourceField TEXT, $_databaseContactAddressField TEXT)",); } catch(e) { print(e?.toString()); } - try { await db.execute("CREATE TABLE IF NOT EXISTS $_databaseRssiTable($_databaseRssiSessionIdField INTEGER, $_databaseRssiTimestampField INTEGER, $_databaseRssiRSSIField INTEGER, $_databaseRssiRPIField TEXT, $_databaseRssiSourceField TEXT, $_databaseRssiAddressField TEXT)",); } catch(e) { print(e?.toString()); } - }); - } - } - - Future _clearDatabase() async { - if (_database != null) { - try { await _database.execute("UPDATE $_databaseExposureTable SET $_databaseExposureProcessedField = 0",); } catch(e) { print(e?.toString()); } - try { await _database.execute("DELETE FROM $_databaseRpiTable",); } catch(e) { print(e?.toString()); } - try { await _database.execute("DELETE FROM $_databaseContactTable",); } catch(e) { print(e?.toString()); } - try { await _database.execute("DELETE FROM $_databaseRssiTable",); } catch(e) { print(e?.toString()); } - } - } - - void _closeDatabase() { - if (_database != null) { - try { _database.close(); } catch(e) { print(e?.toString()); } - _database = null; - } - } - - // Public API - - void start({ Map settings }) { - if (_pluginEnabled && !_isPluginStarted) { - _nativeStart(settings: settings).then((_) { - _wasStarted = true; - NotificationService().notify(notifyStartStop, null); - }); - } - } - - void stop() { - if (_isPluginStarted) { - _nativeStop().then((_) { - _wasStarted = false; - NotificationService().notify(notifyStartStop, null); - }); - } - } - - Future deleteUser() async { - _stopExposuresMonitor(); - await deleteTeks(); - await clearLocalExposures(); - await _destroyPlugin(); - Storage().exposureStarted = null; - Storage().exposureLastReportedTimestamp = null; - } - - bool get isEnabled { - return _pluginEnabled; - } - - bool get isStarted { - return _isPluginStarted; - } - - Map get startSettings { - return _pluginSettings; - } - - bool get _wasStarted { - return Storage().exposureStarted; - } - - set _wasStarted(bool value) { - Storage().exposureStarted = value; - } - - static int get _currentTimestamp { - return DateTime.now().toUtc().millisecondsSinceEpoch; - } - - static int get thresholdTimestamp { - return getThresholdTimestamp(origin: _currentTimestamp); - } - - static int getThresholdTimestamp({int origin}) { - // Two weeks before origin is standard thresold for checking exposures - int midnightTimestamp = (origin ~/ _millisecondsInDay) * _millisecondsInDay; - int twoWeeksAgoMidnightTimestamp = midnightTimestamp - _exposureExpireInterval; - return twoWeeksAgoMidnightTimestamp; - } - - static int get _exposureExpireInterval { - return (Config().settings['covid19ExposureExpireDays'] ?? 14) * _millisecondsInDay; - } - - static Set get _negativeTestCategories { - //TMP: return Set.from(["antibody.positive", "PCR.negative", "SAR.negative"]); - dynamic list = Config().settings['covid19ExposureNegativeTestCategories']; - return (list is List) ? Set.from(list) : null; - } - - // Local Exposures - - Future> loadLocalExposures({int timestamp, bool processed }) async { - List result; - - String query = "SELECT $_databaseRowID, $_databaseExposureTimestampField, $_databaseExposureRPIField, $_databaseExposureDurationField FROM $_databaseExposureTable"; - - String where = ''; - if (timestamp != null) { - if (where.isNotEmpty) { - where += ' AND '; - } - where += "$_databaseExposureTimestampField >= $timestamp"; - } - if (processed != null) { - if (where.isNotEmpty) { - where += ' AND '; - } - where += "$_databaseExposureProcessedField ${processed ? '<>' : '='} 0"; - } - if (where.isNotEmpty) { - query += " WHERE $where"; - } - - query += " ORDER BY $_databaseExposureTimestampField DESC"; - - List> records; - try { records = (_database != null) ? await _database.rawQuery(query) : null; } catch (e) { print(e?.toString()); } - if (records != null) { - result = []; - for (Map record in records) { - result.add(ExposureRecord( - id: record['$_databaseRowID'], - timestamp: record['$_databaseExposureTimestampField'], - rpi: record['$_databaseExposureRPIField'], - duration: record['$_databaseExposureDurationField'], - )); - } - } - - return result; - } - - Future clearLocalExposures() async { - if (_database != null) { - try { - await _database.execute("DELETE FROM $_databaseExposureTable",); - NotificationService().notify(notifyExposureUpdated, null); - } - catch(e) { print(e?.toString()); } - } - } - - /* clean up recorded RPI exposures picked up more than 14 days ago - * This method should be called strictly in "notifyTEK" - * so that every time there is a TEK update (typically per day), - * old RPI will be pruned. - */ - Future _clearExpiredLocalExposures() async { - if (_database != null) { - try { - int currentTimestamp = DateTime.now().toUtc().millisecondsSinceEpoch; - int expireTimestamp = currentTimestamp - _exposureExpireInterval; - await _database.execute("DELETE FROM $_databaseExposureTable where $_databaseExposureTimestampField < $expireTimestamp",); - } catch (e) { print(e?.toString()); } - } - } - - Future _markLocalExposureProcessed(Set exposureIds) async { - if ((_database != null) && exposureIds.isNotEmpty) { - String exposureIdsList = ''; - for (int exposureId in exposureIds) { - if (exposureIdsList.isNotEmpty) { - exposureIdsList += ', '; - } - exposureIdsList += '$exposureId'; - } - try { - await _database.execute("UPDATE $_databaseExposureTable SET $_databaseExposureProcessedField = 1 WHERE $_databaseRowID IN ($exposureIdsList)",); - NotificationService().notify(notifyExposureUpdated, null); - } - catch(e) { print(e?.toString()); } - } - } - - Future _storeLocalExposure(Map exposure) async { - int result = -1; - String rpi = (exposure != null) ? exposure['rpi'] : null; - int timestamp = (exposure != null) ? exposure['timestamp'] : null; - int duration = (exposure != null) ? exposure['duration'] : null; - Log.d('Exposure: Detected Exposure RPI: {$rpi} / duration: $duration ms'); - - if ((_database != null) && (rpi != null)) { - try { - result = await _database.insert(_databaseExposureTable, { - _databaseExposureTimestampField : timestamp ?? 0, - _databaseExposureRPIField : rpi ?? "", - _databaseExposureDurationField : duration ?? 0, - }); - if (0 <= result) { - NotificationService().notify(notifyExposureUpdated, null); - } - } - catch(e) { print(e?.toString()); } - } - return result; - } - - Future _logContact(Map exposure) async { - if ((_logSessionId != null) && (_database != null) && (exposure != null)) { - String rpi = (exposure != null) ? exposure['rpi'] : null; - int timestamp = (exposure != null) ? exposure['timestamp'] : null; - int duration = (exposure != null) ? exposure['duration'] : null; - bool isiOSRecord = (exposure != null) ? exposure['isiOSRecord'] : null; - String source = (isiOSRecord == true) ? 'iOSRecord' : 'AndroidRecord'; - String peripheralUuid = (exposure != null) ? exposure['peripheralUuid'] : null; - //int endTimestamp = (exposure != null) ? exposure['endTimestamp'] : null; - - try { - await _database.insert(_databaseContactTable, { - _databaseContactSessionIdField: _logSessionId, - _databaseContactStartTimeField: timestamp, - _databaseContactDurationField: duration, - _databaseContactRPIField: rpi, - _databaseContactSourceField: source, - _databaseContactAddressField: peripheralUuid, - }); - } catch (e) { - print(e?.toString()); - } - } - } - - Future _logRpi(Map rpiUpdates) async { - if ((_logSessionId != null) && (_database != null) && (rpiUpdates != null)) { - String rpi = (rpiUpdates != null) ? rpiUpdates['rpi'] : null; - String updateType = (rpiUpdates != null) ? rpiUpdates['updateType'] : null; - int updateTime = (rpiUpdates != null) ? rpiUpdates['timestamp'] : null; - int _i = (rpiUpdates != null) ? rpiUpdates['_i'] : null; - String tekString = (rpiUpdates != null) ? rpiUpdates['tek'] : null; - var _iTimestamp = _i * _rpiRefreshInterval; - - try { - await _database.insert(_databaseRpiTable, { - _databaseRpiSessionIdField: _logSessionId, - _databaseRpiTEKField: tekString, - _databaseRpiTEKStartTimeField: _iTimestamp, - _databaseRpiRPIField: rpi, - _databaseRpiRPIStartTimeField: updateTime, - _databaseRpiEventField: updateType, - }); - } - catch(e) { print(e?.toString()); } - } - } - - Future _logRssi(Map rssi) async { - if ((_logSessionId != null) && (_database != null) && (rssi != null)) { - int timestamp = (rssi != null) ? rssi['timestamp'] : null; - String rpi = (rssi != null) ? rssi['rpi'] : null; - int rssiVal = (rssi != null) ? rssi['rssi'] : null; - String address = (rssi != null) ? rssi['address'] : null; - bool isiOSRecord = (rssi != null) ? rssi['isiOSRecord'] : null; - String source = (isiOSRecord == true) ? 'iOSRecord' : 'AndroidRecord'; - - try { - await _database.insert(_databaseRssiTable, { - _databaseRssiSessionIdField: _logSessionId, - _databaseRssiTimestampField: timestamp, - _databaseRssiRSSIField: rssiVal, - _databaseRssiRPIField: rpi, - _databaseRssiSourceField: source, - _databaseRssiAddressField: address, - }); - } - catch(e) { print(e?.toString()); } - } - } - - // Networking - - Future reportTEKs(List teks) async { - String url = (Config().healthUrl != null) ? "${Config().healthUrl}/covid19/trace/report" : null; - String post = (url != null) ? AppJson.encode(ExposureTEK.listToJson(teks)) : null; - Response response = (url != null) ? await Network().post(url, body: post, auth: Network.AppAuth) : null; - return (response?.statusCode == 200); - } - - Future> loadReportedTEKs({int timestamp, int dateAdded}) async { - String url = (Config().healthUrl != null) ? "${Config().healthUrl}/covid19/trace/exposures" : null; - - String params = ''; - if (timestamp != null) { - if (params.isNotEmpty) { - params += '&'; - } - params += "timestamp=$timestamp"; - } - if (dateAdded != null) { - if (params.isNotEmpty) { - params += '&'; - } - params += "date-added=$dateAdded"; - } - if (params.isNotEmpty) { - url += '?$params'; - } - - Response response = (url != null) ? await Network().get(url, auth: Network.AppAuth) : null; - String responseString = (response?.statusCode == 200) ? response.body : null; - List responseJson = (responseString != null) ? AppJson.decodeList(responseString) : null; - return (responseJson != null) ? ExposureTEK.listFromJson(responseJson) : null; - } - - // Report - - set reportTargetTimestamp(int value) { - if (_reportTargetTimestamp != value) { - Storage().exposureReportTargetTimestamp = _reportTargetTimestamp = value; - checkReport(); - } - } - - Future checkReport() async { - - if (!_serviceEnabled) { - return 0; - } - - dynamic exposureActiveDays = Config().settings['covid19ExposureActiveDays'] ?? 0; - int activeInterval = (exposureActiveDays is int) ? (exposureActiveDays * _millisecondsInDay) : null; - - if (activeInterval != null) { - - if (_reportTargetTimestamp == null) { - return 0; - } - else if ((Health().status?.blob?.reportsExposures(rules: Health().rules) != true) && (_lastReportTimestamp != null) && (_reportTargetTimestamp < _lastReportTimestamp)) { - Storage().exposureReportTargetTimestamp = _reportTargetTimestamp = null; - await _expireTEK(); - return 0; - } - } - else { - if (Health().status?.blob?.reportsExposures(rules: Health().rules) != true) { - return 0; - } - } - - if (_checkingReport == true) { - return null; - } - - Log.d('Exposure: Checking local TEKs to report...'); - _checkingReport = true; - - List history = Health().history; - HealthRulesSet rules = Health().rules; - Set negativeTestCategories = _negativeTestCategories; - - int minTimestamp, maxTimestamp, currentTimestamp = _currentTimestamp; - if (activeInterval != null) { - - minTimestamp = getThresholdTimestamp(origin: _reportTargetTimestamp); // two weeks before the target; - int recentTestTimestamp = _findMostRecentNegativeTestTimestamp(history: history, rules: rules, negativeTestCategories: negativeTestCategories, minTimestamp: minTimestamp, maxTimestamp: _reportTargetTimestamp); - if ((recentTestTimestamp != null) && (minTimestamp < recentTestTimestamp)) { - minTimestamp = recentTestTimestamp; // not earlier than the last negative test result - } - if ((_lastReportTimestamp != null) && (minTimestamp < _lastReportTimestamp)) { - minTimestamp = _lastReportTimestamp; // not earlier since the last report - } - - maxTimestamp = _reportTargetTimestamp + activeInterval; - int earlyTestTimestamp = _findEarlierNegativeTestTimestamp(history: history, rules: rules, negativeTestCategories: negativeTestCategories, minTimestamp: _reportTargetTimestamp, maxTimestamp: maxTimestamp); - if ((earlyTestTimestamp != null) && (earlyTestTimestamp < maxTimestamp)) { - maxTimestamp = earlyTestTimestamp; - } - if (currentTimestamp < maxTimestamp) { - maxTimestamp = currentTimestamp; // not later than now - } - } - else { - minTimestamp = getThresholdTimestamp(origin: currentTimestamp); - int recentTestTimestamp = _findMostRecentNegativeTestTimestamp(history: history, rules: rules, negativeTestCategories: negativeTestCategories, minTimestamp: minTimestamp, maxTimestamp: currentTimestamp); - if ((recentTestTimestamp != null) && (minTimestamp < recentTestTimestamp)) { - minTimestamp = recentTestTimestamp; // not earlier than the last negative test result - } - if ((_lastReportTimestamp != null) && (minTimestamp < _lastReportTimestamp)) { - minTimestamp = _lastReportTimestamp; - } - - maxTimestamp = currentTimestamp; - } - - int result; - List teks = await loadTeks(minStamp: minTimestamp, maxStamp: maxTimestamp); - if (teks == null) { - Log.d('Failed to load local TEKs'); - result = null; - } - else if (teks.isEmpty) { - Log.d('No local TEKs newer than $_reportTargetTimestamp.'); - Storage().exposureLastReportedTimestamp = _lastReportTimestamp = maxTimestamp; - result = 0; - } - else if (!await reportTEKs(teks)) { - Log.d('Failed to report ${teks.length} local TEKs'); - result = null; - } - else { - Log.d('Reported ${teks.length} local TEKs'); - Analytics().logHealth(action: Analytics.LogHealthReportExposuresAction); - Storage().exposureLastReportedTimestamp = _lastReportTimestamp = maxTimestamp; - result = teks.length; - } - - // Check whether to stop processing - if ((activeInterval != null) && (_reportTargetTimestamp != null) && (_lastReportTimestamp != null) && ((_reportTargetTimestamp + activeInterval) <= _lastReportTimestamp)) { - Storage().exposureReportTargetTimestamp = _reportTargetTimestamp = null; - await _expireTEK(); - } - - _checkingReport = null; - return result; - } - - int _findMostRecentNegativeTestTimestamp({List history, HealthRulesSet rules, Set negativeTestCategories, int minTimestamp, int maxTimestamp}) { - if ((history != null) && (rules != null) && (negativeTestCategories != null)) { - // start from newest - for (int index = 0; index < history.length; index++) { - HealthHistory historyEntry = history[index]; - int historyTimestamp = (historyEntry.dateUtc != null) ? historyEntry.dateUtc.millisecondsSinceEpoch : null; - if (historyTimestamp != null) { - if ((maxTimestamp != null) && (maxTimestamp < historyTimestamp)) { - continue; - } - if ((minTimestamp != null) && (historyTimestamp < minTimestamp)) { - break; - } - HealthTestRuleResult testRuleResult = historyEntry.isTestVerified ? rules.tests.matchRuleResult(blob: historyEntry?.blob) : null; - if ((testRuleResult?.category != null) && negativeTestCategories.contains(testRuleResult.category)) { - return historyTimestamp; - } - } - } - } - return null; - } - - int _findEarlierNegativeTestTimestamp({List history, HealthRulesSet rules, Set negativeTestCategories, int minTimestamp, int maxTimestamp}) { - if ((history != null) && (rules != null) && (negativeTestCategories != null)) { - // start from oldest - for (int index = history.length - 1; 0 <= index; index--) { - HealthHistory historyEntry = history[index]; - int historyTimestamp = (historyEntry.dateUtc != null) ? historyEntry.dateUtc.millisecondsSinceEpoch : null; - if (historyTimestamp != null) { - if ((minTimestamp != null) && (historyTimestamp < minTimestamp)) { - continue; - } - if ((maxTimestamp != null) && (maxTimestamp < historyTimestamp)) { - break; - } - HealthTestRuleResult testRuleResult = historyEntry.isTestVerified ? rules.tests.matchRuleResult(blob: historyEntry?.blob) : null; - if ((testRuleResult?.category != null) && negativeTestCategories.contains(testRuleResult.category)) { - return historyTimestamp; - } - } - } - } - return null; - } - - // Monitor - - void _startExposuresMonitor() { - if (_serviceEnabled && (_exposuresMonitorTimer == null)) { - int monitorInterval = Config().settings['covid19ExposureMonitorTimeInterval'] ?? 900; - _exposuresMonitorTimer = Timer(Duration(seconds: monitorInterval), checkExposures); - } - } - - void _stopExposuresMonitor() { - if (_exposuresMonitorTimer != null) { - _exposuresMonitorTimer.cancel(); - _exposuresMonitorTimer = null; - } - } - - void _updateExposuresMonitor() { - if (_serviceEnabled && (_exposuresMonitorTimer == null)) { - checkExposures().then((_){ - _startExposuresMonitor(); - }); - } - else if (!_serviceEnabled && (_exposuresMonitorTimer != null)){ - _stopExposuresMonitor(); - } - } - - int get exposureMinDuration { - return _exposureMinDuration ~/ 1000; - } - - set exposureMinDuration(int value) { - if (value != null) { - _exposureMinDuration = value * 1000; - } - else { - _updateExposureMinDuration(); - } - } - - void _updateExposureMinDuration() { - _exposureMinDuration = (Config().settings['covid19ExposureServiceMinDuration'] ?? 7200) * 1000; - } - - Future checkExposures() async { - if (!_serviceEnabled || (_checkingExposures == true)) { - return null; - } - - Log.d('Exposure: Checking Infected Exposures...'); - - _checkingExposures = true; - int thresholdTimestamp = Exposure.thresholdTimestamp; - - List exposures = await Exposure().loadLocalExposures(timestamp: thresholdTimestamp, processed: false); - if (exposures == null) { - _checkingExposures = null; - Log.d('Failed to load local exposures.'); - return null; - } - else if (exposures.isEmpty) { - _checkingExposures = null; - Log.d('No local exposures for processing.'); - return 0; - } - else { - Log.d('Processing ${exposures.length} exposures newer than $thresholdTimestamp.'); - } - - List reportedTEKs = await loadReportedTEKs(timestamp: thresholdTimestamp); - if (reportedTEKs == null) { - Log.d('Failed to load reported TEKs.'); - _checkingExposures = null; - return null; - } - else if (reportedTEKs.isEmpty) { - Log.d('No TEKs newer than $thresholdTimestamp reported.'); - _checkingExposures = null; - return 0; - } - else { - Log.d('Processing ${reportedTEKs.length} TEKs newer than $thresholdTimestamp reported.'); - } - - Analytics().logHealth(action: Analytics.LogHealthCheckExposuresAction); - - List history = Health().history; - HealthStatus lastHealthStatus = Health().status; - HealthHistory lastTest = HealthHistory.mostRecentTest(history); - DateTime lastTestDateUtc = lastTest?.dateUtc; - int scoringDayThreshold = _evalScoringDayThreshold(lastTestDateUtc: lastTestDateUtc); - - int detected = 0; - List results; - - // Map scoringExposures = new Map; - // key = time interval, value = number of rpis in that time interval - Map> scoringExposures = new Map>(); - - for (ExposureTEK tek in reportedTEKs) { - Map rpisMap = await _loadTekRPIs(tek); - if (rpisMap != null) { - Set rpisSet = Set.from(rpisMap.keys); - Set detectedExposures; - - DateTime exposureDateUtc; - int exposureDuration = 0; - // iterating thru local exposures - for (ExposureRecord exposure in exposures) { - // timing check for rpi - if (rpisSet.contains(exposure.rpi) && - ((exposure.timestamp + _rpiCheckExposureBuffer) >= rpisMap[exposure.rpi]) && - ((exposure.timestamp - _rpiCheckExposureBuffer - _rpiRefreshInterval) < rpisMap[exposure.rpi]) - ) { - DateTime exposureRecordDateUtc = exposure.dateUtc; - if ((exposureRecordDateUtc != null) && ((exposureDateUtc == null) || exposureRecordDateUtc.isBefore(exposureDateUtc))) { - exposureDateUtc = exposureRecordDateUtc; - } - exposureDuration += exposure.duration; - if (detectedExposures == null) { - detectedExposures = Set(); - } - detectedExposures.add(exposure.id); - - // increment the exposure in that time interval - int intervalNum = exposure.timestamp ~/ _rpiRefreshInterval; - if (intervalNum >= scoringDayThreshold) { - // filter out the date before the day threshold - Set durationRPISet = scoringExposures[intervalNum]; - if (durationRPISet == null) { - scoringExposures[intervalNum] = durationRPISet = Set(); - } - durationRPISet.add(exposure.rpi); - } - } - } - - if ((exposureDateUtc != null) && (_exposureMinDuration <= exposureDuration)) { - HealthHistory result; - - HealthHistory historyEntry = HealthHistory.traceInList(history, tek: tek.tek); - if (historyEntry != null) { - if ((historyEntry.dateUtc != null) && historyEntry.dateUtc.isBefore(exposureDateUtc)) { - exposureDateUtc = historyEntry.dateUtc; - } - if (historyEntry.blob?.traceDuration != null) { - exposureDuration += historyEntry.blob?.traceDuration; - } - - result = await Health().updateHistory( - id: historyEntry.id, - dateUtc: exposureDateUtc, - type: HealthHistoryType.contactTrace, - blob: HealthHistoryBlob( - traceDuration: exposureDuration, - traceTEK: tek.tek - )); - } - else { - result = await Health().addHistory( - dateUtc: exposureDateUtc, - type: HealthHistoryType.contactTrace, - blob: HealthHistoryBlob( - traceDuration: exposureDuration, - traceTEK: tek.tek - )); - } - - if (result != null) { - _markLocalExposureProcessed(detectedExposures); - if (results == null) { - results = []; - } - results.add(result); - } - } - } - } - - if (results != null) { - for (HealthHistory result in results) { - Analytics().logHealth( - action: Analytics.LogHealthContactTraceProcessedAction, - status: Health().status?.blob?.code, - prevStatus: lastHealthStatus?.blob?.code, - attributes: { - Analytics.LogHealthDurationName: result.blob.traceDuration, - Analytics.LogHealthExposureTimestampName: result.dateUtc?.toIso8601String(), - }); - } - } - - Log.d('Detected: $detected Processed: ${results?.length ?? 0}'); - - // each time duration = 10mins. cumulative duration = 10 * #people in that interval - // finish searching, try to sum all duration: - // Zero the dose only after 2 zero intervals - // trigger the exposure notification only after there is a does above the threshold. - Log.d("scoringExposures = $scoringExposures"); - int scoringStartTime = -1, scoringEndTime = -1; - bool scoringIsExposured = false; - if (scoringExposures.isNotEmpty) { - - // there is a match - - // sort the time interval in ascending order - List enIntervalNumberList = List.from(scoringExposures.keys); - enIntervalNumberList.sort(); - - scoringStartTime = enIntervalNumberList[0]; - int lastKey = enIntervalNumberList[0] - 1; - int tempSum = 0; - - for (int k in enIntervalNumberList) { - // loop all time interval - if (k - lastKey <= 1) { - // consective time interval - tempSum += scoringExposures[k].length * 10; - } - else { - // Zero the dose only after 2 zero interval - Log.d("tempSum = $tempSum"); - scoringStartTime = k; - tempSum = scoringExposures[k].length * 10; - } - lastKey = k; - - if (tempSum > _exposureMinDuration) { - scoringIsExposured = true; - scoringEndTime = k; - Log.d("Above the threshold! Trigger exposure notification"); - break; - } - } - Log.d("tempSum = $tempSum"); - } - - // if isexposure = false, then scoringStartTime and scoringEndTime are meaningless - Log.d("is exposure = $scoringIsExposured, start = $scoringStartTime, end = $scoringEndTime"); - - _checkingExposures = null; - return detected; - } - - int _evalScoringDayThreshold({DateTime lastTestDateUtc}) { - int scoringDateTimestamp; - if (lastTestDateUtc != null) { - int lastTestTimestamp = lastTestDateUtc.millisecondsSinceEpoch; - scoringDateTimestamp = lastTestTimestamp - _millisecondsInDay; // a day before last test timestamp - } - else { - int currentTimestamp = DateTime.now().toUtc().millisecondsSinceEpoch; - int midnightTimestamp = (currentTimestamp ~/ _millisecondsInDay) * _millisecondsInDay; - scoringDateTimestamp = midnightTimestamp - (5 * _millisecondsInDay); // five days ago midnight timestamp - } - return scoringDateTimestamp ~/ _rpiRefreshInterval; - } - - Future evalTestResultExposureScoring({DateTime previousTestDateUtc}) async { - - if (!_serviceEnabled) { - return null; - } - - int previousTestTimestamp = previousTestDateUtc?.millisecondsSinceEpoch; - - List> futures = [ - loadLocalExposures(timestamp: previousTestTimestamp), - loadReportedTEKs(timestamp: previousTestTimestamp), - ]; - List results = await Future.wait(futures); - - List exposures = ((results != null) && (0 < results.length)) ? results[0] : null; - List reportedTEKs = ((results != null) && (1 < results.length)) ? results[1] : null; - if ((exposures == null) || (reportedTEKs == null)) { - return null; - } - else if (exposures.isEmpty || reportedTEKs.isEmpty) { - // no ContactWithPositive or PassedExposureScoring - return _buildTestResultExposureScoring(hasExposureNotificationsEnabled: Health().userConsentExposureNotification); - } - - bool hasContactWithPositive, hasPassedExposureScoring; - for (ExposureTEK tek in reportedTEKs) { - Map rpisMap = await _loadTekRPIs(tek); - if (rpisMap != null) { - int exposureDuration = 0; - Set rpisSet = Set.from(rpisMap.keys); - for (ExposureRecord exposure in exposures) { - if (rpisSet.contains(exposure.rpi) && - ((exposure.timestamp + _rpiCheckExposureBuffer) >= rpisMap[exposure.rpi]) && - ((exposure.timestamp - _rpiCheckExposureBuffer - _rpiRefreshInterval) < rpisMap[exposure.rpi]) - ) { - exposureDuration += exposure.duration; - hasContactWithPositive = true; - } - } - if (_exposureMinDuration <= exposureDuration) { - hasPassedExposureScoring = true; - } - } - } - - return _buildTestResultExposureScoring( - hasContactWithPositive: hasContactWithPositive, - hasPassedExposureScoring: hasPassedExposureScoring, - hasExposureNotificationsEnabled: Health().userConsentExposureNotification); - } - - static int _buildTestResultExposureScoring({bool hasContactWithPositive, bool hasPassedExposureScoring, bool hasExposureNotificationsEnabled}) { - // converting flags --> int to upload - // bit 0 --> has contact with positive - // bit 1 --> has passed exposure scoring - // bit 2 --> has exposure notifications enabled - - return - (((hasContactWithPositive == true) ? 1 : 0) << 0) | - (((hasPassedExposureScoring == true) ? 1 : 0) << 1) | - (((hasExposureNotificationsEnabled == true) ? 1 : 0) << 2); - } - - Future evalSocialActivity(int prevHours, { DateTime maxDateUtc }) async { - // Returns: [# Rpi seen, # Rpi matches with positive Tek] - - if (!_serviceEnabled) { - return null; - } - if (maxDateUtc == null) { - maxDateUtc = DateTime.now().toUtc(); - } - int startTimestamp = maxDateUtc.subtract(Duration(hours: prevHours))?.millisecondsSinceEpoch; - - List> futures = [ - loadLocalExposures(timestamp: startTimestamp), - loadReportedTEKs(timestamp: startTimestamp), - ]; - List results = await Future.wait(futures); - - List exposures = ((results != null) && (0 < results.length)) ? results[0] : null; - List reportedTEKs = ((results != null) && (1 < results.length)) ? results[1] : null; - - exposures.removeWhere((element) => element.dateUtc.isAfter(maxDateUtc)); - reportedTEKs.removeWhere((element) => element.dateUtc.isAfter(maxDateUtc)); - - if ((exposures == null) || (reportedTEKs == null)) { - return null; - } - int totalRpiSeen = exposures.length; - int totalRpiMatches = 0; - - for (ExposureTEK tek in reportedTEKs) { - Map rpisMap = await _loadTekRPIs(tek); - if (rpisMap != null) { - Set rpisSet = Set.from(rpisMap.keys); - for (ExposureRecord exposure in exposures) { - if (rpisSet.contains(exposure.rpi) && - ((exposure.timestamp + _rpiCheckExposureBuffer) >= rpisMap[exposure.rpi]) && - ((exposure.timestamp - _rpiCheckExposureBuffer - _rpiRefreshInterval) < rpisMap[exposure.rpi]) - ) { - totalRpiMatches++; - } - } - } - } - - return [totalRpiSeen, totalRpiMatches]; - } - - - // Logging - - void startLogSession(int sessionId) { - _logSessionId = sessionId; - } - - void endLogSession(String deviceId, bool isAndroid) { - if (_logSessionId != null) { - _postSessionData(sessionId: _logSessionId, deviceId: deviceId, isAndroid: isAndroid); - _logSessionId = null; - } - } - - Future _postSessionData({int sessionId, String deviceId, bool isAndroid}) async { - if (AppString.isStringNotEmpty(Config().exposureLogUrl)) { - List> recordRssi; - String rssiQuery = "SELECT * FROM $_databaseRssiTable WHERE $_databaseRssiSessionIdField = $sessionId"; - try { recordRssi = (_database != null) ? await _database.rawQuery(rssiQuery) : null; } catch (e) { print(e?.toString()); } - - List> recordContact; - String contactQuery = "SELECT * FROM $_databaseContactTable WHERE $_databaseContactSessionIdField = $sessionId"; - try { recordContact = (_database != null) ? await _database.rawQuery(contactQuery) : null; } catch (e) { print(e?.toString()); } - - List> recordRpi; - String rpiQuery = "SELECT * FROM $_databaseRpiTable WHERE $_databaseRpiSessionIdField = $sessionId"; - try { recordRpi = (_database != null) ? await _database.rawQuery(rpiQuery) : null; } catch (e) { print(e?.toString()); } - - Map upload = { - "deviceID": deviceId, - "isAndroid": isAndroid, - "contact": recordContact, - "rpi": recordRpi, - "rssi": recordRssi - }; - Response response = await Network().post( - Config().exposureLogUrl, - body: AppJson.encode(upload), - auth: Network.AppAuth); - return response?.statusCode == 200; - } - return null; - } - - - -} \ No newline at end of file diff --git a/lib/service/FirebaseMessaging.dart b/lib/service/FirebaseMessaging.dart index 173a9e2d..aa20b5fe 100644 --- a/lib/service/FirebaseMessaging.dart +++ b/lib/service/FirebaseMessaging.dart @@ -38,7 +38,6 @@ import 'package:illinois/service/Organizations.dart'; import 'package:illinois/service/Service.dart'; import 'package:illinois/service/Storage.dart'; import 'package:illinois/service/UserProfile.dart'; -import 'package:illinois/service/LocalNotifications.dart'; import 'package:illinois/utils/Utils.dart'; const String _channelId = "Notifications_Channel_ID"; @@ -46,6 +45,7 @@ const String _channelId = "Notifications_Channel_ID"; class FirebaseMessaging with Service implements NotificationsListener { static const String notifyToken = "edu.illinois.rokwire.firebase.messaging.token"; + static const String notifyForegroundMessage = "edu.illinois.rokwire.firebase.messaging.message.foreground"; static const String notifyPopupMessage = "edu.illinois.rokwire.firebase.messaging.message.popup"; static const String notifyConfigUpdate = "edu.illinois.rokwire.firebase.messaging.config.update"; static const String notifySettingUpdated = "edu.illinois.rokwire.firebase.messaging.setting.updated"; @@ -111,7 +111,6 @@ class FirebaseMessaging with Service implements NotificationsListener { UserProfile.notifyProfileDeleted, Health.notifyUserUpdated, Health.notifyStatusUpdated, - LocalNotifications.notifySelected, AppLivecycle.notifyStateChanged, ]); } @@ -196,9 +195,6 @@ class FirebaseMessaging with Service implements NotificationsListener { else if (name == Health.notifyStatusUpdated) { _updateHealthStatusSubscriptions(status: Health().status); } - else if (name == LocalNotifications.notifySelected) { - _processDataMessage(AppJson.decode(param)); - } else if (name == AppLivecycle.notifyStateChanged) { _onAppLivecycleStateChanged(param); } @@ -342,8 +338,13 @@ class FirebaseMessaging with Service implements NotificationsListener { if (AppString.isStringNotEmpty(title) || AppString.isStringNotEmpty(body)) { Log.d("FCM: Android notification message"); //Explicitly show it only when in foreground - String notificationPayload = (data != null) ? json.encode(data) : null; - LocalNotifications().showNotification(title: title, message: body, payload: notificationPayload); + NotificationService().notify(notifyForegroundMessage, { + "title": title, + "body": body, + "onComplete": (){ + _processDataMessage(message); + } + }); } else if (data != null) { Log.d("FCM: Android data message"); diff --git a/lib/service/Health.dart b/lib/service/Health.dart index c69dd103..8357cebb 100644 --- a/lib/service/Health.dart +++ b/lib/service/Health.dart @@ -23,10 +23,7 @@ import 'package:illinois/model/Health.dart'; import 'package:illinois/service/Analytics.dart'; import 'package:illinois/service/AppLivecycle.dart'; import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/BluetoothServices.dart'; import 'package:illinois/service/Config.dart'; -import 'package:illinois/service/Exposure.dart'; -import 'package:illinois/service/LocationServices.dart'; import 'package:illinois/service/NativeCommunicator.dart'; import 'package:illinois/service/Network.dart'; import 'package:illinois/service/NotificationService.dart'; @@ -46,6 +43,7 @@ class Health with Service implements NotificationsListener { static const String notifyStatusUpdated = "edu.illinois.rokwire.health.status.updated"; static const String notifyHistoryUpdated = "edu.illinois.rokwire.health.history.updated"; static const String notifyUserAccountCanged = "edu.illinois.rokwire.health.user.account.changed"; + static const String notifyUserOverrideChanged = "edu.illinois.rokwire.health.iser.override.changed"; static const String notifyCountyChanged = "edu.illinois.rokwire.health.county.changed"; static const String notifyRulesChanged = "edu.illinois.rokwire.health.rules.changed"; static const String notifyBuildingAccessRulesChanged = "edu.illinois.rokwire.health.building_access_rules.changed"; @@ -56,12 +54,10 @@ class Health with Service implements NotificationsListener { static const String _rulesFileName = "rules.json"; static const String _historyFileName = "history.json"; - static const int _exposureStatisticsLogInterval = 6 * 60 * 60 * 1000; // 6 hours - HealthUser _user; PrivateKey _userPrivateKey; String _userAccountId; - int _userTestMonitorInterval; + HealthUserOverride _userOverride; HealthStatus _status; List _history; @@ -80,7 +76,6 @@ class Health with Service implements NotificationsListener { _RefreshOptions _refreshOptions; Set _pendingNotifications; DateTime _pausedDateTime; - Timer _exposureStatisticsLogTimer; // Singletone Instance @@ -116,7 +111,7 @@ class Health with Service implements NotificationsListener { _user = _loadUserFromStorage(); _userPrivateKey = await _loadUserPrivateKey(); _userAccountId = Storage().healthUserAccountId; - _userTestMonitorInterval = Storage().healthUserTestMonitorInterval; + _userOverride = _loadUserOverrideFromStorage(); _status = _loadStatusFromStorage(); _history = await _loadHistoryFromCache(); @@ -127,14 +122,7 @@ class Health with Service implements NotificationsListener { _familyMembers = _loadFamilyMembersFromStorage(); - _exposureStatisticsLogTimer = Timer.periodic(Duration(milliseconds: _exposureStatisticsLogInterval), (_) { - _logExposureStatistics(); - }); - - _refresh(_RefreshOptions.all()).then((_) { - _logExposureStatistics(); - }); - + _refresh(_RefreshOptions.all()); } @override @@ -146,7 +134,7 @@ class Health with Service implements NotificationsListener { _user = null; _userPrivateKey = null; _userAccountId = null; - _userTestMonitorInterval = null; + _userOverride = null; _servicePublicKey = null; _status = null; @@ -157,9 +145,6 @@ class Health with Service implements NotificationsListener { _buildingAccessRules = null; _familyMembers = null; - - _exposureStatisticsLogTimer.cancel(); - _exposureStatisticsLogTimer = null; } @override @@ -190,9 +175,7 @@ class Health with Service implements NotificationsListener { if (_pausedDateTime != null) { Duration pausedDuration = DateTime.now().difference(_pausedDateTime); if (Config().refreshTimeout < pausedDuration.inSeconds) { - _refresh(_RefreshOptions.all()).then((_) { - _logExposureStatistics(); - }); + _refresh(_RefreshOptions.all()); } } } @@ -202,14 +185,14 @@ class Health with Service implements NotificationsListener { if (this._isUserAuthenticated) { _refreshUserPrivateKey().then((_) { - _refresh(_RefreshOptions.fromList([_RefreshOption.user, _RefreshOption.userInterval, _RefreshOption.history, _RefreshOption.familyMembers])); + _refresh(_RefreshOptions.fromList([_RefreshOption.user, _RefreshOption.userOverride, _RefreshOption.history, _RefreshOption.familyMembers])); }); } else { _userPrivateKey = null; _clearUser(); _clearUserAccountId(); - _clearUserTestMonitorInterval(); + _clearUserOverride(); _clearStatus(); _clearHistory(); _clearFamilyMembers(); @@ -227,11 +210,11 @@ class Health with Service implements NotificationsListener { } Future refreshStatus() async { - return _refresh(_RefreshOptions.fromList([_RefreshOption.userInterval, _RefreshOption.history, _RefreshOption.rules, _RefreshOption.buildingAccessRules])); + return _refresh(_RefreshOptions.fromList([_RefreshOption.userOverride, _RefreshOption.history, _RefreshOption.rules, _RefreshOption.buildingAccessRules])); } Future refreshStatusAndUser() async { - return _refresh(_RefreshOptions.fromList([_RefreshOption.user, _RefreshOption.userInterval, _RefreshOption.history, _RefreshOption.rules, _RefreshOption.buildingAccessRules])); + return _refresh(_RefreshOptions.fromList([_RefreshOption.user, _RefreshOption.userOverride, _RefreshOption.history, _RefreshOption.rules, _RefreshOption.buildingAccessRules])); } Future refreshUser() async { @@ -271,7 +254,7 @@ class Health with Service implements NotificationsListener { options.user ? _refreshUser() : Future.value(), options.userPrivateKey ? _refreshUserPrivateKey() : Future.value(), - options.userInterval ? _refreshUserTestMonitorInterval() : Future.value(), + options.userOverride ? _refreshUserOverride() : Future.value(), options.status ? _refreshStatus() : Future.value(), options.history ? _refreshHistory() : Future.value(), @@ -282,7 +265,7 @@ class Health with Service implements NotificationsListener { options.familyMembers ? _refreshFamilyMembers() : Future.value(), ]); - if (options.history || options.rules || options.userInterval) { + if (options.history || options.rules || options.userOverride) { await _rebuildStatus(); await _logProcessedEvents(); } @@ -328,10 +311,6 @@ class Health with Service implements NotificationsListener { return this._isUserAuthenticated && (_user != null); } - bool get userConsentExposureNotification { - return this.isUserLoggedIn && (_user?.consentExposureNotification ?? false); - } - // User HealthUser get user { @@ -421,7 +400,7 @@ class Health with Service implements NotificationsListener { return false; } - Future loginUser({bool consentTestResults, bool consentVaccineInformation, bool consentExposureNotification, AsymmetricKeyPair keys}) async { + Future loginUser({bool consentTestResults, bool consentVaccineInformation, AsymmetricKeyPair keys}) async { if (!this._isUserAuthenticated) { return null; @@ -483,13 +462,9 @@ class Health with Service implements NotificationsListener { } } - // Consent :Exposure Notification - if (consentExposureNotification != null) { - if (consentExposureNotification != user.consentExposureNotification) { - analyticsSettingsAttributes[Analytics.LogHealthSettingConsentExposureNotifName] = consentExposureNotification; - user.consentExposureNotification = consentExposureNotification; - userUpdated = true; - } + // Consent: Exposure Notification (backward support) + if (user.consentExposureNotification == null) { + user.consentExposureNotification = false; } // Save @@ -517,18 +492,8 @@ class Health with Service implements NotificationsListener { Analytics().logHealth( action: Analytics.LogHealthSettingChangedAction, attributes: analyticsSettingsAttributes, defaultAttributes: Analytics.DefaultAttributes); } - if (consentExposureNotification == true) { - if (BluetoothServices().status == BluetoothStatus.PermissionNotDetermined) { - await BluetoothServices().requestStatus(); - } - - if (await LocationServices().status == LocationServicesStatus.PermissionNotDetermined) { - await LocationServices().requestPermission(); - } - } - if (userReset == true) { - await _refresh(_RefreshOptions.fromList([_RefreshOption.userInterval, _RefreshOption.history])); + await _refresh(_RefreshOptions.fromList([_RefreshOption.userOverride, _RefreshOption.history])); } return user; @@ -540,7 +505,7 @@ class Health with Service implements NotificationsListener { await _clearHistory(); _clearStatus(); _clearUserAccountId(); - _clearUserTestMonitorInterval(); + _clearUserOverride(); _clearFamilyMembers(); return true; } @@ -669,10 +634,10 @@ class Health with Service implements NotificationsListener { _beginNotificationsCache(); _clearStatus(); - _clearUserTestMonitorInterval(); + _clearUserOverride(); _clearFamilyMembers(); await _clearHistory(); - await _refresh(_RefreshOptions.fromList([_RefreshOption.userInterval, _RefreshOption.history, _RefreshOption.familyMembers])); + await _refresh(_RefreshOptions.fromList([_RefreshOption.userOverride, _RefreshOption.history, _RefreshOption.familyMembers])); _endNotificationsCache(); } } @@ -681,37 +646,47 @@ class Health with Service implements NotificationsListener { _applyUserAccount(null); } - // User test monitor interval + // User override - int get userTestMonitorInterval { - return _userTestMonitorInterval; + HealthUserOverride get userOverride { + return _userOverride; } - Future _refreshUserTestMonitorInterval() async { + Future _refreshUserOverride() async { try { - Storage().healthUserTestMonitorInterval = _userTestMonitorInterval = await _loadUserTestMonitorInterval(); + HealthUserOverride userOverride = await _loadUserTestMonitorInterval(); + if (_userOverride != userOverride) { + _saveUserOverrideToStorage(_userOverride = userOverride); + _notify(notifyUserOverrideChanged); + } } catch (e) { print(e?.toString()); } } - Future _loadUserTestMonitorInterval() async { -//TMP: return 8; + Future _loadUserTestMonitorInterval() async { if (this._isUserAuthenticated && (Config().healthUrl != null)) { - String url = "${Config().healthUrl}/covid19/uin-override"; + String url = "${Config().healthUrl}/covid19/v2/uin-override"; Response response = await Network().get(url, auth: Network.HealthUserAuth); if (response?.statusCode == 200) { - Map responseJson = AppJson.decodeMap(response.body); - return (responseJson != null) ? responseJson['interval'] : null; + return HealthUserOverride.fromJson(AppJson.decodeMap(response.body)); } throw Exception("${response?.statusCode ?? '000'} ${response?.body ?? 'Unknown error occured'}"); } throw Exception("User not logged in"); } - void _clearUserTestMonitorInterval() { - Storage().healthUserTestMonitorInterval = _userTestMonitorInterval = null; + void _clearUserOverride() { + Storage().healthUserOverride = _userOverride = null; + } + + static HealthUserOverride _loadUserOverrideFromStorage() { + return HealthUserOverride.fromJson(AppJson.decodeMap(Storage().healthUserOverride)); + } + + static void _saveUserOverrideToStorage(HealthUserOverride userOverride) { + Storage().healthUserOverride = AppJson.encode(userOverride?.toJson()); } // Status @@ -800,20 +775,11 @@ class Health with Service implements NotificationsListener { _notify(notifyStatusUpdated); } _applyBuildingAccessForStatus(status); - _updateExposureReportTarget(status); - } - - void _updateExposureReportTarget(HealthStatus status) { - if ((status?.blob?.reportsExposures(rules: _rules) == true) && - /*status?.blob?.historyBlob?.isTest && */ - (status?.dateUtc != null)) - { - Exposure().reportTargetTimestamp = status.dateUtc.millisecondsSinceEpoch; - } } HealthStatus _evalStatus() { - Map params = (_userTestMonitorInterval != null) ? { HealthRulesSet.UserTestMonitorInterval : _userTestMonitorInterval } : null; + int userTestMonitorInterval = _userOverride?.effectiveTestInterval; + Map params = (userTestMonitorInterval != null) ? { HealthRulesSet.UserTestMonitorInterval : userTestMonitorInterval } : null; return _buildStatus(rules: _rules, history: _history, params: params); } @@ -856,7 +822,7 @@ class Health with Service implements NotificationsListener { } else if (historyEntry.isVaccine) { HealthVaccineRule vaccineRule = rules.vaccines.matchRule(blob: historyEntry?.blob, rules: rules); - ruleStatus = vaccineRule?.status?.eval(history: history, historyIndex: index, rules: rules, params: _buildParams(params, historyEntry.blob?.actionParams)); + ruleStatus = vaccineRule?.status?.eval(history: history, historyIndex: index, rules: rules, params: params); } else if (historyEntry.isAction) { HealthActionRule actionRule = rules.actions.matchRule(blob: historyEntry?.blob, rules: rules); @@ -1158,7 +1124,7 @@ class Health with Service implements NotificationsListener { blob: HealthHistoryBlob( provider: event?.provider, providerId: event?.providerId, - vaccine: event?.blob?.vaccine, + vaccineStatus: event?.blob?.vaccineStatus, extras: event?.blob?.extras, ), publicKey: _user?.publicKey @@ -1195,12 +1161,8 @@ class Health with Service implements NotificationsListener { Future _logProcessedEvents() async { if ((_processedEvents != null) && (0 < _processedEvents.length)) { - int exposureTestReportDays = Config().settings['covid19ExposureTestReportDays']; for (HealthPendingEvent event in _processedEvents) { if (event.isTest) { - HealthHistory previousTest = HealthHistory.mostRecentTest(_history, beforeDateUtc: event.blob?.dateUtc, onPosition: 2); - int score = await Exposure().evalTestResultExposureScoring(previousTestDateUtc: previousTest?.dateUtc); - Analytics().logHealth( action: Analytics.LogHealthProviderTestProcessedAction, status: _status?.blob?.code, @@ -1209,29 +1171,7 @@ class Health with Service implements NotificationsListener { Analytics.LogHealthProviderName: event.provider, Analytics.LogHealthTestTypeName: event.blob?.testType, Analytics.LogHealthTestResultName: event.blob?.testResult, - Analytics.LogHealthExposureScoreName: score, }); - - if (exposureTestReportDays != null) { - DateTime maxDateUtc = event?.blob?.dateUtc; - DateTime minDateUtc = maxDateUtc?.subtract(Duration(days: exposureTestReportDays)); - if ((maxDateUtc != null) && (minDateUtc != null)) { - HealthHistory contactTrace = HealthHistory.mostRecentContactTrace(_history, minDateUtc: minDateUtc, maxDateUtc: maxDateUtc); - if (contactTrace != null) { - Analytics().logHealth( - action: Analytics.LogHealthContactTraceTestAction, - status: _status?.blob?.code, - prevStatus: _previousStatus?.blob?.code, - attributes: { - Analytics.LogHealthExposureTimestampName: contactTrace.dateUtc?.toIso8601String(), - Analytics.LogHealthDurationName: contactTrace.blob?.traceDuration, - Analytics.LogHealthProviderName: event.provider, - Analytics.LogHealthTestTypeName: event.blob?.testType, - Analytics.LogHealthTestResultName: event.blob?.testResult, - }); - } - } - } } else if (event.isVaccine) { Analytics().logHealth( @@ -1239,7 +1179,7 @@ class Health with Service implements NotificationsListener { status: _status?.blob?.code, prevStatus: _previousStatus?.blob?.code, attributes: { - Analytics.LogHealthVaccineName: event.blob?.vaccine + Analytics.LogHealthVaccineStatusName: event.blob?.vaccineStatus }); } else if (event.isAction) { @@ -1410,7 +1350,7 @@ class Health with Service implements NotificationsListener { // Contact Trace - // Used only from debug panel, see Exposure.checkExposures + // Used only from debug panel Future processContactTrace({DateTime dateUtc, int duration}) async { HealthHistory history = await _addHistory(await HealthHistory.encryptedFromBlob( @@ -1425,18 +1365,8 @@ class Health with Service implements NotificationsListener { if (history != null) { _notify(notifyHistoryUpdated); - HealthStatus previousStatus = _status; await _rebuildStatus(); - Analytics().logHealth( - action: Analytics.LogHealthContactTraceProcessedAction, - status: previousStatus?.blob?.code, - prevStatus: _status?.blob?.code, - attributes: { - Analytics.LogHealthDurationName: duration, - Analytics.LogHealthExposureTimestampName: dateUtc?.toIso8601String(), - }); - return true; } return false; @@ -1644,8 +1574,16 @@ class Health with Service implements NotificationsListener { // Vaccination bool get isVaccinated { - HealthHistory vaccine = HealthHistory.mostRecentVaccine(Health().history); - return (vaccine != null) && (vaccine.blob != null) && vaccine.blob.isVaccineEffective && (vaccine.dateUtc != null) && vaccine.dateUtc.isBefore(DateTime.now().toUtc()); + int vaccineIndex = HealthHistory.mostRecentVaccineIndex(_history); + HealthHistory vaccine = ((vaccineIndex != null) && (0 <= vaccineIndex) && (vaccineIndex < _history.length)) ? _history[vaccineIndex] : null; + if (vaccine?.blob?.isVaccineEffective ?? false) { + DateTime now = DateTime.now(); + if (vaccine?.dateUtc?.isBefore(now.toUtc()) ?? false) { + DateTime vaccineExpireDateLocal = HealthHistory.getVaccineExpireDateLocal(history: _history, vaccineIndex: vaccineIndex, rules: _rules); + return (vaccineExpireDateLocal == null) || now.isBefore(vaccineExpireDateLocal); + } + } + return false; } // Current Server Time @@ -1792,75 +1730,6 @@ class Health with Service implements NotificationsListener { Map responseJson = (responseString != null) ? AppJson.decode(responseString) : null; return (responseJson != null) ? HealthServiceLocation.fromJson(responseJson) : null; } - - // Exposure Statistics Log - - Future _logExposureStatistics() async { - String userId = this._userId; - if (AppString.isStringNotEmpty(userId)) { - String storageEntry = "lastEpoch.$userId"; - int lastEpoch = Storage().getInt(storageEntry); - int currEpoch = DateTime.now().toUtc().millisecondsSinceEpoch ~/ _exposureStatisticsLogInterval; - if (currEpoch != lastEpoch) { - // need to send lastEpoch's data - await _sendExposureStatistics(lastEpoch ?? (currEpoch - 1)); - Storage().setInt(storageEntry, currEpoch); - } - } - } - - Future _sendExposureStatistics(int epoch) async { - - DateTime maxDateUtc = DateTime.fromMillisecondsSinceEpoch((epoch + 1) * _exposureStatisticsLogInterval); - - int testFrequency168Hours = HealthHistory.retrieveNumTests(_history, 168, maxDateUtc: maxDateUtc); - - int exposureUpTime6Hours = await Exposure().evalExposureUpTime(6); - int exposureUpTime24Hours = await Exposure().evalExposureUpTime(24); - int exposureUpTime168Hours = await Exposure().evalExposureUpTime(168); - - List socialActivity6Hours = await Exposure().evalSocialActivity(6, maxDateUtc: maxDateUtc); - List socialActivity24Hours = await Exposure().evalSocialActivity(24, maxDateUtc: maxDateUtc); - List socialActivity168Hours = await Exposure().evalSocialActivity(168, maxDateUtc: maxDateUtc); - - bool contactTrace168Hours; - String testResult168Hours; - DateTime cutoffDate = DateTime.now().toUtc().subtract(Duration(hours: 168)); - - HealthHistory mostRecentContactTrace = HealthHistory.mostRecentContactTrace(_history, maxDateUtc: maxDateUtc); - if (mostRecentContactTrace == null || - mostRecentContactTrace.dateUtc.isBefore(cutoffDate)) { - contactTrace168Hours = false; - } else { - contactTrace168Hours = true; - } - - HealthHistory mostRecentTest = HealthHistory.mostRecentTest(_history, beforeDateUtc: maxDateUtc); - if (mostRecentTest == null || - mostRecentTest.dateUtc.isBefore(cutoffDate)) { - testResult168Hours = null; - } else { - testResult168Hours = mostRecentTest.blob?.testResult; - } - Analytics().logHealth( - action: Analytics.LogHealthExposureStatisticsAction, - attributes: { - Analytics.LogTestFrequency168HoursName: testFrequency168Hours, - Analytics.LogRpiSeen6HoursName: (socialActivity6Hours != null) ? socialActivity6Hours[0] : null, - Analytics.LogRpiSeen24HoursName: (socialActivity24Hours != null) ? socialActivity24Hours[0] : null, - Analytics.LogRpiSeen168HoursName: (socialActivity168Hours != null) ? socialActivity168Hours[0] : null, - Analytics.LogRpiMatches6HoursName: (socialActivity6Hours != null) ? socialActivity6Hours[1] : null, - Analytics.LogRpiMatches24HoursName: (socialActivity24Hours != null) ? socialActivity24Hours[1] : null, - Analytics.LogRpiMatches168HoursName: (socialActivity168Hours != null) ? socialActivity168Hours[1] : null, - Analytics.LogExposureUpTime6HoursName : exposureUpTime6Hours, - Analytics.LogExposureUpTime24HoursName : exposureUpTime24Hours, - Analytics.LogExposureUpTime168HoursName : exposureUpTime168Hours, - Analytics.LogExposureNotification168HoursName: contactTrace168Hours, - Analytics.LogTestResult168HoursName: testResult168Hours, - Analytics.LogEpochName: epoch, - } - ); - } } class _RefreshOptions { @@ -1890,7 +1759,7 @@ class _RefreshOptions { bool get user { return options.contains(_RefreshOption.user); } bool get userPrivateKey { return options.contains(_RefreshOption.userPrivateKey); } - bool get userInterval { return options.contains(_RefreshOption.userInterval); } + bool get userOverride { return options.contains(_RefreshOption.userOverride); } bool get status { return options.contains(_RefreshOption.status); } bool get history { return options.contains(_RefreshOption.history); } @@ -1922,7 +1791,7 @@ class _RefreshOptions { enum _RefreshOption { user, userPrivateKey, - userInterval, + userOverride, status, history, diff --git a/lib/service/LocalNotifications.dart b/lib/service/LocalNotifications.dart deleted file mode 100644 index 740a21b5..00000000 --- a/lib/service/LocalNotifications.dart +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:io'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:illinois/service/NativeCommunicator.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Service.dart'; -import 'package:illinois/service/Log.dart'; - -class LocalNotifications with Service { - - static const String notifySelected = "edu.illinois.rokwire.localnotifications.selected"; - - static final LocalNotifications _instance = new LocalNotifications._internal(); - - factory LocalNotifications() { - return _instance; - } - - LocalNotifications._internal(); - - FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin; - - @override - void createService() { - } - - @override - void destroyService() { - } - - @override - Future initService() async { - initPlugin(); - } - - @override - Set get serviceDependsOn { - return Set.from([NativeCommunicator()]); - } - - void initPlugin() { - if (_flutterLocalNotificationsPlugin == null) { - if (Platform.isIOS) { - NativeCommunicator().queryNotificationsAuthorization("query").then((bool notificationsAuthorized) { - if (notificationsAuthorized) { - _initPlugin(); - } - }); - } - else { - _initPlugin(); - } - } - } - - void _initPlugin() { - _flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin(); - var initializationSettingsAndroid = new AndroidInitializationSettings('app_icon'); - var initializationSettingsIOS = new IOSInitializationSettings(onDidReceiveLocalNotification: _onDidReceiveLocalNotification); - var initializationSettings = new InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS); - _flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: _onSelectNotification); - } - - Future _onSelectNotification(String payload) async { - Log.d('Android: on select local notification: ' + payload); - NotificationService().notify(notifySelected, payload); - } - - Future _onDidReceiveLocalNotification(int id, String title, String body, String payload) async { - Log.d('iOS: on did receive local notification: ' + payload); - } - - Future showNotification({String title, String message, String payload = ''}) async { - if (_flutterLocalNotificationsPlugin != null) { - var androidPlatformChannelSpecifics = AndroidNotificationDetails('1000', 'DEFAULT_CHANNEL', 'It is default channel', importance: Importance.max, priority: Priority.high); - var iOSPlatformChannelSpecifics = IOSNotificationDetails(presentAlert: true, presentSound: true,); - var platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics); - await _flutterLocalNotificationsPlugin.show(0, title, message, platformChannelSpecifics, payload: payload, ); - } - } - -} diff --git a/lib/service/NativeCommunicator.dart b/lib/service/NativeCommunicator.dart index 7616b154..8ece31fa 100644 --- a/lib/service/NativeCommunicator.dart +++ b/lib/service/NativeCommunicator.dart @@ -184,16 +184,6 @@ class NativeCommunicator with Service { return result; } - Future queryBluetoothAuthorization(String method) async { - String result; - try { - result = await _platformChannel.invokeMethod('bluetooth_authorization', {"method": method }); - } on PlatformException catch (e) { - print(e.message); - } - return result; - } - Future getDeviceId() async { String result; try { diff --git a/lib/service/Onboarding.dart b/lib/service/Onboarding.dart index 911c6edd..8f714dd6 100644 --- a/lib/service/Onboarding.dart +++ b/lib/service/Onboarding.dart @@ -26,13 +26,10 @@ import 'package:illinois/ui/onboarding/OnboardingHealthFinalPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingHealthHowItWorksPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingHealthIntroPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingHealthQrCodePanel.dart'; -import 'package:illinois/ui/onboarding/OnboardingAuthBluetoothPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingLoginPhoneConfirmPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingGetStartedPanel.dart'; -import 'package:illinois/ui/onboarding/OnboardingAuthLocationPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingLoginNetIdPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingLoginPhonePanel.dart'; -import 'package:illinois/ui/onboarding/OnboardingAuthNotificationsPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingOrganizationsPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingRolesPanel.dart'; import 'package:illinois/ui/onboarding/OnboardingLoginPhoneVerifyPanel.dart'; @@ -154,9 +151,6 @@ class Onboarding extends Service implements NotificationsListener{ switch (name) { case "get_started": return OnboardingGetStartedPanel(onboardingContext: context); case "organization": return OnboardingOrganizationsPanel(onboardingContext: context); - case "notifications_auth": return OnboardingAuthNotificationsPanel(onboardingContext: context); - case "location_auth": return OnboardingAuthLocationPanel(onboardingContext: context); - case "bluetooth_auth": return OnboardingAuthBluetoothPanel(onboardingContext: context); case "roles": return OnboardingRolesPanel(onboardingContext: context); case "login_netid": return OnboardingLoginNetIdPanel(onboardingContext: context); case "login_phone": return OnboardingLoginPhonePanel(onboardingContext: context); @@ -179,15 +173,6 @@ class Onboarding extends Service implements NotificationsListener{ if(panel is OnboardingOrganizationsPanel){ return 'organization'; } - else if (panel is OnboardingAuthNotificationsPanel) { - return 'notifications_auth'; - } - else if (panel is OnboardingAuthLocationPanel) { - return 'location_auth'; - } - else if (panel is OnboardingAuthBluetoothPanel) { - return 'bluetooth_auth'; - } else if (panel is OnboardingRolesPanel) { return 'roles'; } diff --git a/lib/service/Service.dart b/lib/service/Service.dart index ebeabd85..f8752c3f 100644 --- a/lib/service/Service.dart +++ b/lib/service/Service.dart @@ -18,13 +18,11 @@ import 'package:illinois/service/Analytics.dart'; import 'package:illinois/service/AppLivecycle.dart'; import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/BluetoothServices.dart'; import 'package:illinois/service/Config.dart'; import 'package:illinois/service/Connectivity.dart'; import 'package:illinois/service/FirebaseService.dart'; import 'package:illinois/service/FirebaseCrashlytics.dart'; import 'package:illinois/service/DeepLink.dart'; -import 'package:illinois/service/Exposure.dart'; import 'package:illinois/service/FirebaseMessaging.dart'; import 'package:illinois/service/FlexUI.dart'; import 'package:illinois/service/Health.dart'; @@ -36,7 +34,6 @@ import 'package:illinois/service/OSFHealth.dart'; import 'package:illinois/service/Onboarding.dart'; import 'package:illinois/service/Organizations.dart'; import 'package:illinois/service/Storage.dart'; -import 'package:illinois/service/LocalNotifications.dart'; import 'package:illinois/service/Styles.dart'; import 'package:illinois/service/UserProfile.dart'; @@ -87,9 +84,7 @@ class Services { AppLivecycle(), Connectivity(), LocationServices(), - BluetoothServices(), NativeCommunicator(), - LocalNotifications(), DeepLink(), Localization(), @@ -100,7 +95,6 @@ class Services { Analytics(), FirebaseMessaging(), Health(), - Exposure(), OSFHealth(), FlexUI(), diff --git a/lib/service/Storage.dart b/lib/service/Storage.dart index eb0a198c..3883e123 100644 --- a/lib/service/Storage.dart +++ b/lib/service/Storage.dart @@ -275,6 +275,26 @@ class Storage with Service { setString(lastRunVersionKey, value); } + //////////////////////// + // Config Notifications + + static const String reportedConfigNotifictionsKey = 'reported_config_notifications'; + + Set get reportedConfigNotifictions { + List list = getStringList(reportedConfigNotifictionsKey); + return (list != null) ? Set.from(list) : null; + } + + set reportedConfigNotifiction(String notificationId) { + if (notificationId != null) { + Set notifications = reportedConfigNotifictions ?? Set(); + if (!notifications.contains(notificationId)) { + notifications.add(notificationId); + setStringList(reportedConfigNotifictionsKey, notifications.toList()); + } + } + } + //////////////// // Auth @@ -484,14 +504,14 @@ class Storage with Service { setString(_healthUserStatusKey, value); } - static const String _healthUserTestMonitorIntervalKey = 'health_user_test_monitor_interval'; + static const String _healthUserOverrideKey = 'health_user_override'; - int get healthUserTestMonitorInterval { - return getInt(_healthUserTestMonitorIntervalKey, defaultValue: null); + String get healthUserOverride { + return getString(_healthUserOverrideKey, defaultValue: null); } - set healthUserTestMonitorInterval(int value) { - setInt(_healthUserTestMonitorIntervalKey, value); + set healthUserOverride(String value) { + setString(_healthUserOverrideKey, value); } static const String _healthUserAccountIdKey = 'health_user_account_id'; @@ -533,39 +553,6 @@ class Storage with Service { set healthFamilyMembers(String value) { setString(_healthFamilyMembersKey, value); } - ///////////// - // Exposure - - static const String _exposureStartedKey = 'exposure_started'; - - bool get exposureStarted { - return getBool(_exposureStartedKey, defaultValue: true); - } - - set exposureStarted(bool value) { - setBool(_exposureStartedKey, value); - } - - static const String _exposureReportTargetTimestampKey = 'exposure_report_target_timestamp'; - - int get exposureReportTargetTimestamp { - return getInt(_exposureReportTargetTimestampKey, defaultValue: null); - } - - set exposureReportTargetTimestamp(int value) { - setInt(_exposureReportTargetTimestampKey, value); - } - - static const String _exposureLastReportedTimestampKey = 'exposure_last_reported_timestamp'; - - int get exposureLastReportedTimestamp { - return getInt(_exposureLastReportedTimestampKey, defaultValue: null); - } - - set exposureLastReportedTimestamp(int value) { - setInt(_exposureLastReportedTimestampKey, value); - } - ///////////// // Http Proxy diff --git a/lib/ui/RootPanel.dart b/lib/ui/RootPanel.dart index c385e238..3380870c 100644 --- a/lib/ui/RootPanel.dart +++ b/lib/ui/RootPanel.dart @@ -63,6 +63,7 @@ class _RootPanelState extends State with SingleTickerProviderStateMix super.initState(); NotificationService().subscribe(this, [ AppLivecycle.notifyStateChanged, + FirebaseMessaging.notifyForegroundMessage, FirebaseMessaging.notifyPopupMessage, FirebaseMessaging.notifyCovid19Notification, Localization.notifyStringsUpdated, @@ -92,6 +93,9 @@ class _RootPanelState extends State with SingleTickerProviderStateMix if (name == AppLivecycle.notifyStateChanged) { _onAppLivecycleStateChanged(param); } + else if (name == FirebaseMessaging.notifyForegroundMessage){ + _onFirebaseForegroundMessage(param); + } else if (name == FirebaseMessaging.notifyPopupMessage) { _onFirebasePopupMessage(param); } @@ -232,6 +236,16 @@ class _RootPanelState extends State with SingleTickerProviderStateMix ); } + void _onFirebaseForegroundMessage(Map content) { + String body = content["body"]; + Function completion = content["onComplete"]; + AppAlert.showDialogResult(context, body).then((value){ + if(completion != null){ + completion(); + } + }); + } + void _onFirebasePopupMessage(Map content) { String displayText = content["display_text"]; String positiveButtonText = content["positive_button_text"]; diff --git a/lib/ui/debug/DebugExposureLogsPanel.dart b/lib/ui/debug/DebugExposureLogsPanel.dart deleted file mode 100644 index 3fd6314b..00000000 --- a/lib/ui/debug/DebugExposureLogsPanel.dart +++ /dev/null @@ -1,1531 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:convert'; -import 'dart:io'; - -import 'package:device_info/device_info.dart'; -import 'package:http/http.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:illinois/model/Exposure.dart'; -import 'package:illinois/utils/AppDateTime.dart'; -import 'package:illinois/service/Config.dart'; -import 'package:illinois/service/Exposure.dart'; -import 'package:illinois/service/Network.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/ui/widgets/HeaderBar.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/utils/Utils.dart'; -import 'package:url_launcher/url_launcher.dart' as url_launcher; - -class DebugExposureLogsPanel extends StatefulWidget { - DebugExposureLogsPanel(); - - @override - _DebugExposureLogsPanelState createState() => - _DebugExposureLogsPanelState(); -} - -class _DebugExposureLogsPanelState extends State - implements NotificationsListener { - TextEditingController _minDurationSettingController; - FocusNode _minDurationSettingFocusNode; - - TextEditingController _minRSSISettingController; - FocusNode _minRSSISettingFocusNode; - - List _teks; - List _exposures; - - bool _poolingTEKs; - bool _reportingTEKs; - bool _checkingTEKs; - bool _checkingExposures; - String _connection; - - // testing framework variables - static const String Url = - "http://ec2-18-191-37-235.us-east-2.compute.amazonaws.com:8003/"; - static const String QueryUrl = - "http://ec2-18-191-37-235.us-east-2.compute.amazonaws.com:8003/SessionReport?"; - TextEditingController _sessionIDTextController; - TextEditingController _additionalDetailTextController; - TextEditingController _querySessionTextController; - TextEditingController _queryDeviceIndexTextController; - //String _currentTestingSessionID = ""; - bool _processingSessionID = false; // circling animation and on tap event - bool _isInSession = - false; // first row button color, onTap event and endSession button - String _executionStatus = "None"; - int _currentSession; - bool _isAndroid; - String _deviceID; - String _thisDeviceIndex; // always shown as this device Index - - Map _startSettings; - - @override - void initState() { - NotificationService().subscribe(this, [ - Exposure.notifyStartStop, - Exposure.notifyTEKsUpdated, - Exposure.notifyExposureUpdated, - Exposure.notifyExposureThick, - ]); - _startSettings = Map.from(Exposure().startSettings ?? Config().settings); - - //int minDuration = _startSettings['covid19ExposureServiceMinDuration']; - int minDuration = Exposure().exposureMinDuration; - _minDurationSettingController = - TextEditingController(text: minDuration?.toString() ?? ''); - _minDurationSettingFocusNode = FocusNode(); - - int minRssi = _startSettings['covid19ExposureServiceMinRSSI']; - _minRSSISettingController = - TextEditingController(text: minRssi?.toString() ?? ''); - _minRSSISettingFocusNode = FocusNode(); - - _sessionIDTextController = TextEditingController(); - _additionalDetailTextController = TextEditingController(); - _querySessionTextController = TextEditingController(); - _queryDeviceIndexTextController = TextEditingController(); - - _loadTEKs(); - _loadExposures(); - - super.initState(); - } - - @override - void dispose() { - NotificationService().unsubscribe(this); - - _minDurationSettingController.dispose(); - _minDurationSettingFocusNode.dispose(); - - _minRSSISettingController.dispose(); - _minRSSISettingFocusNode.dispose(); - - // testing Framework: dispose text controller - _sessionIDTextController.dispose(); - _additionalDetailTextController.dispose(); - _querySessionTextController.dispose(); - _queryDeviceIndexTextController.dispose(); - - super.dispose(); - } - - // NotificationsListener - - @override - void onNotification(String name, dynamic param) { - if (name == Exposure.notifyStartStop) { - _updateConnection(null); - } else if (name == Exposure.notifyTEKsUpdated) { - _loadTEKs(); - } else if (name == Exposure.notifyExposureUpdated) { - _loadExposures(); - } else if (name == Exposure.notifyExposureThick) { - _updateConnection(param); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: SimpleHeaderBarWithBack( - context: context, - titleWidget: Text( - "COVID-19 Exposure Logs", - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontFamily: Styles().fontFamilies.extraBold), - ), - ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeading(), - Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTEKs(), - Container( - height: 32, - ), - _buildExposures(), - Container( - height: 32, - ), - _buildTesting(), - ]), - ), - ], - ), - ), - ), - ], - ), - backgroundColor: Styles().colors.background, - ); - } - - Widget _buildHeading() { - String status; - bool canStart, canStop, canEdit; - if (!Exposure().isEnabled) { - status = 'disabled'; - canStart = canStop = canEdit = false; - } else { - status = Exposure().isStarted ? 'started' : 'stopped'; - canStart = !Exposure().isStarted; - canStop = Exposure().isStarted; - canEdit = !Exposure().isStarted; - } - - return Container( - color: Colors.white, - child: Padding( - padding: EdgeInsets.all(16), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: < - Widget>[ - Row( - children: [ - Padding( - padding: EdgeInsets.only(right: 4), - child: Text( - 'Status: ', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ), - Text( - status, - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 16, - color: Color(0xff494949), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(right: 4), - child: Text( - 'Conn: ', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ), - Text( - _connection ?? '', - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 16, - color: Color(0xff494949), - ), - ), - ], - ), - Padding( - padding: EdgeInsets.only(top: 8), - child: Row(children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 4), - child: Text( - "Min RSSI", - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 12, - color: canEdit - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor), - ), - ), - TextField( - controller: _minRSSISettingController, - focusNode: _minRSSISettingFocusNode, - enabled: canEdit, - decoration: InputDecoration( - border: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.black, width: 1.0)), - contentPadding: EdgeInsets.symmetric(horizontal: 8)), - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 16, - color: canEdit - ? Styles().colors.textBackground - : Styles().colors.disabledTextColor, - ), - ), - ], - ), - ), - Container( - width: 16, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 4), - child: Text( - "Min Duration", - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 12, - color: canEdit - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor), - ), - ), - TextField( - controller: _minDurationSettingController, - focusNode: _minDurationSettingFocusNode, - enabled: canEdit, - decoration: InputDecoration( - border: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.black, width: 1.0)), - contentPadding: EdgeInsets.symmetric(horizontal: 8)), - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 16, - color: canEdit - ? Styles().colors.textBackground - : Styles().colors.disabledTextColor, - ), - ), - ], - ), - ), - ]), - ), - Padding( - padding: EdgeInsets.only(top: 8), - child: Row( - children: [ - Expanded( - child: RoundedButton( - label: "Start", - textColor: canStart - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: canStart - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColorTwo, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - borderWidth: 2, - height: 42, - onTap: () { - _onStart(); - }), - ), - Container( - width: 16, - ), - Expanded( - child: RoundedButton( - label: "Stop", - textColor: canStop - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: canStop - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColorTwo, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - borderWidth: 2, - height: 42, - onTap: () { - _onStop(); - }), - ) - ], - ), - ), - ]), - ), - ); - } - - Widget _buildTEKs() { - List tekWidgets = []; - if (_teks != null) { - for (ExposureTEK tek in _teks) { - String time = AppDateTime.formatDateTime(tek.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - String expiretime = AppDateTime.formatDateTime(tek.expireUtc, format: 'MM/dd HH:mm:ss UTC'); - tekWidgets.add( - Row( - children: [ - Text( - "${tek.tek} | start: $time | expire: $expiretime", - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 12, - color: Color(0xff494949), - ), - ), - ], - ), - ); - } - } - - if (tekWidgets.isEmpty) { - tekWidgets.add( - Row( - children: [], - ), - ); - } - - List content = []; - content.add( - Text( - 'Local TEKs:', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ); - content.add(Container( - height: 100, - decoration: BoxDecoration( - border: - Border.all(color: Styles().colors.fillColorPrimary, width: 1)), - child: Stack( - children: [ - SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: tekWidgets)), - ), - Align( - alignment: Alignment.topRight, - child: Semantics( - button: true, - label: 'Copy', - child: GestureDetector( - onTap: () { - _onCopyTEKs(); - }, - child: Container( - width: 36, - height: 36, - child: Align( - alignment: Alignment.center, - child: Semantics( - excludeSemantics: true, - child: Image.asset('images/icon-copy.png')), - ), - ), - ), - )), - ], - ), - )); - - int teksCount = _teks?.length ?? 0; - - content.add(Padding( - padding: EdgeInsets.only(top: 8), - child: Row( - children: [ - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Pull', - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - borderWidth: 2, - height: 42, - onTap: () { - _onPullTEKs(); - }), - Visibility( - visible: (_poolingTEKs == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - Container( - width: 8, - ), - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Report', - textColor: (0 < teksCount) - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: (0 < teksCount) - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColor, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - borderWidth: 2, - height: 42, - onTap: () { - _onReportTEKs(); - }), - Visibility( - visible: (_reportingTEKs == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - Container( - width: 8, - ), - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Check', - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - borderWidth: 2, - height: 42, - onTap: () { - _onCheckTEKs(); - }), - Visibility( - visible: (_checkingTEKs == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - ], - ), - )); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: content); - } - - Widget _buildExposures() { - List content = []; - - List exposureWidgets = []; - if (_exposures != null) { - for (ExposureRecord exposure in _exposures) { - String time = AppDateTime.formatDateTime(exposure.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - exposureWidgets.add( - Row( - children: [ - Text( - "RPI: ${exposure.rpi} \nTime: $time | Duration: ${exposure.durationDisplayString}", - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 12, - color: Color(0xff494949), - ), - ), - ], - ), - ); - } - } - if (exposureWidgets.isEmpty) { - exposureWidgets.add( - Row( - children: [], - ), - ); - } - - content.add( - Text( - 'Recorded Exposures:', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ); - content.add(Container( - height: 100, - decoration: BoxDecoration( - border: - Border.all(color: Styles().colors.fillColorPrimary, width: 1)), - child: Stack( - children: [ - SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: exposureWidgets)), - ), - Align( - alignment: Alignment.topRight, - child: Semantics( - button: true, - label: 'Copy', - child: GestureDetector( - onTap: () { - _onCopyExposures(); - }, - child: Container( - width: 36, - height: 36, - child: Align( - alignment: Alignment.center, - child: Semantics( - excludeSemantics: true, - child: Image.asset('images/icon-copy.png')), - ), - ), - ), - )), - ], - ), - )); - - int exposuresCount = _exposures?.length ?? 0; - - content.add(Padding( - padding: EdgeInsets.only(top: 8), - child: Row( - children: [ - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Check', - textColor: (0 < exposuresCount) - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: (0 < exposuresCount) - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColor, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - borderWidth: 2, - height: 42, - onTap: () { - _onCheckExposures(); - }), - Visibility( - visible: (_checkingExposures == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - Container( - width: 16, - ), - Expanded( - child: RoundedButton( - label: 'Clear', - textColor: (0 < exposuresCount) - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: (0 < exposuresCount) - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColor, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - borderWidth: 2, - height: 42, - onTap: () { - _onClearExposures(); - }), - ), - ], - ), - )); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: content); - } - - void _loadTEKs() { - Exposure().loadTeks().then((List teks) { - setState(() { - _teks = teks; - }); - }); - } - - void _loadExposures() { - Exposure() - .loadLocalExposures(timestamp: Exposure.thresholdTimestamp) - .then((List exposures) { - setState(() { - _exposures = exposures; - }); - }); - } - - void _updateConnection(Map exposure) { - String rpi = (exposure != null) ? exposure['rpi'] : null; - int timestamp = (exposure != null) ? exposure['timestamp'] : null; - int rssi = (exposure != null) ? exposure['rssi'] : null; - String time = (timestamp != null) ? AppDateTime.formatDateTime(DateTime.fromMillisecondsSinceEpoch(timestamp, isUtc: true), format: 'MM/dd HH:mm:ss UTC') : null; - setState(() { - _connection = - Exposure().isStarted ? "RPI: $rpi\nTime: $time | RSSI: $rssi" : ''; - }); - } - - void _onStart() { - int minDuration = int.tryParse(_minDurationSettingController.text); - if (minDuration == null) { - AppAlert.showDialogResult( - context, "Please enter an integer minimum duration") - .then((_) { - _minDurationSettingFocusNode.requestFocus(); - }); - return; - } - - int minRssi = int.tryParse(_minRSSISettingController.text); - if (minRssi == null) { - AppAlert.showDialogResult( - context, "Please enter an integer minimum RSSI value") - .then((_) { - _minRSSISettingFocusNode.requestFocus(); - }); - return; - } - - //_startSettings['covid19ExposureServiceMinDuration'] = minDuration; - Exposure().exposureMinDuration = minDuration; - _startSettings['covid19ExposureServiceMinRSSI'] = minRssi; - - Exposure().start(settings: _startSettings); - } - - void _onStop() { - Exposure().stop(); - } - - void _onPullTEKs() { - setState(() { - _poolingTEKs = true; - }); - Exposure() - .loadReportedTEKs(timestamp: Exposure.thresholdTimestamp) - .then((List result) { - setState(() { - _poolingTEKs = false; - }); - - String copy = ""; - int copied; - if (result != null) { - copied = 0; - for (ExposureTEK tek in result) { - String time = AppDateTime.formatDateTime(tek.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - copy += "${tek.tek} | $time\n"; - copied++; - } - Clipboard.setData(ClipboardData(text: copy)); - } - if (copied == null) { - AppAlert.showDialogResult(context, "Failed to pull reported."); - } else { - AppAlert.showDialogResult( - context, - (0 < copied) - ? "$copied entries copied to Clipboard." - : "No entries copied to Clipboard."); - } - }); - } - - void _onReportTEKs() { - setState(() { - _reportingTEKs = true; - }); - Exposure().reportTEKs(_teks).then((bool result) { - setState(() { - _reportingTEKs = false; - }); - _loadTEKs(); - AppAlert.showDialogResult( - context, result ? "Successfully reported" : "Failed to report"); - }); - } - - void _onCheckTEKs() { - setState(() { - _checkingTEKs = true; - }); - Exposure().checkReport().then((int result) { - setState(() { - _checkingTEKs = false; - }); - String message; - if (result == null) { - message = 'Failed to report TEKs'; - } else if (result == 0) { - message = "No TEKs reported"; - } else { - message = "$result ${(1 < result) ? 'TEKs' : 'TEK'} reported"; - } - - AppAlert.showDialogResult(context, message); - }); - } - - void _onCopyTEKs() { - String copy = ""; - int copied = 0; - if (_teks != null) { - for (ExposureTEK tek in _teks) { - String time = AppDateTime.formatDateTime(tek.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - copy += "${tek.tek} | $time\n"; - copied++; - } - Clipboard.setData(ClipboardData(text: copy)); - } - AppAlert.showDialogResult( - context, - (0 < copied) - ? "$copied entries copied to Clipboard" - : "No entries copied to Clipboard."); - } - - void _onCheckExposures() { - setState(() { - _checkingExposures = true; - }); - Exposure().checkExposures().then((int detectedCount) { - setState(() { - _checkingExposures = false; - }); - String message; - if (detectedCount == null) { - message = "Failed to check exposures."; - } else if (detectedCount == 0) { - message = "No exposures detected"; - } else { - message = - "Detected $detectedCount ${(detectedCount != 1) ? 'exposures' : 'exposure'}."; - } - - AppAlert.showDialogResult(context, message); - }); - } - - void _onClearExposures() { - Exposure().clearLocalExposures(); - } - - void _onCopyExposures() { - String copy = ""; - int copied = 0; - if (_exposures != null) { - for (ExposureRecord exposure in _exposures) { - String time = AppDateTime.formatDateTime(exposure.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - copy += - "${exposure.rpi} | Time: $time | Duration: ${exposure.durationDisplayString}\n"; - copied++; - } - Clipboard.setData(ClipboardData(text: copy)); - } - AppAlert.showDialogResult( - context, - (0 < copied) - ? "$copied entries copied to Clipboard" - : "No entries copied to Clipboard."); - } - -// testing related widget builder and functions - Widget _buildTesting() { - List content = []; - - content.add( - Text( - 'Available Session ID:', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ); - content.add(Container( - height: 50, - child: TextField( - controller: _sessionIDTextController, - // keyboardType: TextInputType.multiline, - // maxLines: null, - maxLines: 1, - enabled: _isInSession == false, // can not change once in session - decoration: new InputDecoration.collapsed( - hintText: 'press Get Session ID or Type here'), - ), - )); - - // buttons - content.add(Padding( - padding: EdgeInsets.only(top: 8), - child: Row( - children: [ - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Get Session ID', - backgroundColor: Styles().colors.white, - textColor: (_isInSession != true) - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: (_isInSession != true) - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColor, - fontFamily: Styles().fontFamilies.bold, - fontSize: 10, - borderWidth: 2, - height: 42, - onTap: _isInSession - ? null - : () { - _onSessionGet(); - }), - Visibility( - visible: (_processingSessionID == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - Container( - width: 16, - ), - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Create Session', - backgroundColor: Styles().colors.white, - textColor: (_isInSession != true) - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: (_isInSession != true) - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColor, - fontFamily: Styles().fontFamilies.bold, - fontSize: 10, - borderWidth: 2, - height: 42, - onTap: _isInSession - ? null - : () { - _onSessionCreate(); - }), - Visibility( - visible: (_processingSessionID == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - Container( - width: 16, - ), - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Join Session', - backgroundColor: Styles().colors.white, - textColor: (_isInSession != true) - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: (_isInSession != true) - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColor, - fontFamily: Styles().fontFamilies.bold, - fontSize: 10, - borderWidth: 2, - height: 42, - onTap: _isInSession - ? null - : () { - _onSessionJoin(); - }), - Visibility( - visible: (_processingSessionID == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - Container( - width: 16, - ), - ], - ), - )); - - content.add( - Text( - 'Execution Status: $_executionStatus', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ); - content.add(Container( - height: 32, - )); - // add additional text input field - content.add( - Text( - "additional details", - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ); - content.add(Container( - height: 100, - child: TextField( - controller: _additionalDetailTextController, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.done, - maxLines: null, - // onSubmitted: (String value) async { - // await showDialog( - // context: context, - // builder: (BuildContext context) { - // return AlertDialog( - // title: const Text('Thanks!'), - // content: Text('You typed "$value".'), - // actions: [ - // TextButton( - // onPressed: () { - // Navigator.pop(context); - // }, - // child: const Text('OK'), - // ), - // ], - // ); - // }, - // ); - // }, - decoration: new InputDecoration.collapsed( - hintText: 'add additional detail before ending session'), - ), - )); - - content.add(Padding( - padding: EdgeInsets.only(top: 8), - child: Row(children: [ - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'End Session', - backgroundColor: Styles().colors.white, - textColor: (_isInSession == true) - ? Styles().colors.fillColorPrimary - : Styles().colors.disabledTextColor, - borderColor: (_isInSession == true) - ? Styles().colors.fillColorSecondary - : Styles().colors.disabledTextColor, - fontFamily: Styles().fontFamilies.bold, - fontSize: 10, - borderWidth: 2, - height: 42, - onTap: _isInSession - ? () { - _onEndSession(); - } - : null), - Visibility( - visible: (_processingSessionID == true), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 10.5), - child: Container( - width: 21, - height: 21, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Styles().colors.fillColorSecondary), - strokeWidth: 2, - )), - ), - ), - ), - ]), - ), - ]))); - - content.add( - Container( - height: 70, - ), - ); - - content.add( - Text( - 'Query Session ID: ', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ); - - content.add(Container( - height: 50, - child: TextField( - controller: _querySessionTextController, - // keyboardType: TextInputType.multiline, - // maxLines: null, - decoration: - new InputDecoration.collapsed(hintText: 'query session id '), - ), - )); - - content.add( - Text( - 'Query Device Index: --(note: this device index is: $_thisDeviceIndex)', - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, - color: Styles().colors.fillColorPrimary), - ), - ); - - content.add(Container( - height: 50, - child: TextField( - controller: _queryDeviceIndexTextController, - // keyboardType: TextInputType.multiline, - // maxLines: null, - decoration: - new InputDecoration.collapsed(hintText: 'query device Index'), - ), - )); - content.add(Padding( - padding: EdgeInsets.only(top: 8), - child: Row(children: [ - Expanded( - child: Stack(children: [ - RoundedButton( - label: 'Query Session Report', - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorSecondary, - fontFamily: Styles().fontFamilies.bold, - fontSize: 10, - borderWidth: 2, - height: 42, - onTap: () { - _onSessionReport(); - }, - ) - ]), - ), - ]))); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: content); - } - - // testing framework functions - - void _onSessionGet() async { - if (_processingSessionID) { - return; - } - setState(() { - _processingSessionID = true; // block all buttons - _executionStatus = "querying for session ID"; - }); - - Map headers = {"Content-type": "application/json"}; - - Response response = await Network().post(Url + "GetSessionID", - headers: headers, body: null, auth: Network.AppAuth); - String body = response.body; - if (response.statusCode == HttpStatus.ok) { - // update text field - setState(() { - _executionStatus = "available session id is " + body; - _processingSessionID = false; - _sessionIDTextController.text = body; - _querySessionTextController.text = body; - }); - } else { - setState(() { - _executionStatus = "error "; - _processingSessionID = false; - }); - } - - return; - } - - /* - * _onSessionCreate() async: - * step0: check if there is a sessionID in the textfield - * step1: get device info and save to a struct. - * step2: attempt to upload device info to server - * step3: attempt to create new session - * */ - void _onSessionCreate() async { - if (_processingSessionID) { - return; - } - // step 0: check for sessionID - try { - _currentSession = int.parse(_sessionIDTextController.text); - } catch (e) { - setState(() { - _executionStatus = "failed, please enter a valid session ID or get one"; - }); - return; - } - // step1: get device info and save to a struct. - try { - await _gettingDeviceInfo(); - } catch (e) { - print(e); - return; - } - // step2: attempt to upload device info to server - // step3: and create a new session with this device in it. - setState(() { - _executionStatus = "Uploading Device Info"; - }); - Map headers = {"Content-type": "application/json"}; - String req = - '{"isAndroid": $_isAndroid, "deviceID": "$_deviceID", "sessionID":$_currentSession}'; - print("" + req); - Response response = await Network().post(Url + "CreateSession", - headers: headers, body: req, auth: Network.AppAuth); - - // statusCode: - if (response.statusCode == HttpStatus.ok) { - var parsed = json.decode(response.body); - _isInSession = true; - // display parsed["message"] - // save parsed["deviceIndex"] - _thisDeviceIndex = parsed["deviceIndex"]; - _queryDeviceIndexTextController.text = _thisDeviceIndex; - _querySessionTextController.text = _sessionIDTextController.text; - _executionStatus = - response.statusCode.toString() + "" + parsed["message"]; - } else { - _isInSession = false; - _executionStatus = response.statusCode.toString() + " " + response.body; - } - setState(() { - // should update _executaionStatus and _isInSession - _processingSessionID = false; - }); - - if (_isInSession) { - Exposure().startLogSession(_currentSession); - } - return; - } - - /* - * _onSessinJoin() async: - * step0: check if there is a sessionID in the textfield - * step1: get device info and save to a struct. - * step2: attempt to upload device info to server - * step3: attempt to join - * */ - void _onSessionJoin() async { - // Response response = await Network() - // .post(Url + "getSessionID", body: post, auth: Network.AppAuth); - if (_processingSessionID) { - return; - } - // step 0: check for sessionID - try { - _currentSession = int.parse(_sessionIDTextController.text); - } catch (e) { - setState(() { - _executionStatus = "failed, please enter a valid session ID or get one"; - }); - return; - } - - // step1: get device info and save to a struct. - try { - await _gettingDeviceInfo(); - } catch (e) { - print(e); - return; - } - // step2: attempt to upload device info to server - // step3: and create a new session with this device in it. - setState(() { - _executionStatus = "Uploading Device Info"; - }); - Map headers = {"Content-type": "application/json"}; - String req = - '{"isAndroid": $_isAndroid, "deviceID": "$_deviceID", "sessionID":$_currentSession}'; - print("" + req); - Response response = await Network().post(Url + "JoinSession", - headers: headers, body: req, auth: Network.AppAuth); - // statusCode: - // if ok, parse js - // if not, print the value - if (response.statusCode == HttpStatus.ok) { - var parsed = json.decode(response.body); - _isInSession = true; - // display parsed["message"] - // save parsed["deviceIndex"] - _thisDeviceIndex = parsed["deviceIndex"]; - _queryDeviceIndexTextController.text = _thisDeviceIndex; - _querySessionTextController.text = _sessionIDTextController.text; - _executionStatus = - response.statusCode.toString() + "" + parsed["message"]; - } else { - _isInSession = false; - _executionStatus = response.statusCode.toString() + " " + response.body; - } - setState(() { - _processingSessionID = false; - }); - - if (_isInSession) { - Exposure().startLogSession(_currentSession); - } - return; - } - - /* - _onEndSession: - get current session id - get additional details - - * */ - void _onEndSession() async { - if (_processingSessionID) { - return; - } - setState(() { - _processingSessionID = true; - _executionStatus = "submitting additional detail and ending session"; - }); - - try { - _currentSession = int.parse(_sessionIDTextController.text); - } catch (e) { - setState(() { - _executionStatus = "failed, please enter a valid session ID or get one"; - }); - return; - } - // post sessoin data first!!! - Exposure().endLogSession(_deviceID, _isAndroid); - Map headers = {"Content-type": "application/json"}; - String req = '{"isAndroid": $_isAndroid, "deviceID": "$_deviceID",' - ' "sessionID":$_currentSession, "additionalDetail":"${_additionalDetailTextController.text}" }'; - print("" + req); - Response response = await Network().post(Url + "EndSession", - headers: headers, body: req, auth: Network.AppAuth); - // update text field - setState(() { - if (response.statusCode == HttpStatus.ok) { - _executionStatus = response.statusCode.toString() + " " + response.body; - _processingSessionID = false; - _isInSession = false; - } else { - _executionStatus = response.statusCode.toString() + " " + response.body; - } - }); - return; - } - - Future _gettingDeviceInfo() async { - final DeviceInfoPlugin deviceInfoPlugin = new DeviceInfoPlugin(); - setState(() { - _executionStatus = "Getting Device Info"; - }); - _isAndroid = Platform.isAndroid; - if (_isAndroid) { - var build = await deviceInfoPlugin.androidInfo; - _deviceID = build.androidId; - } else { - var data = await deviceInfoPlugin.iosInfo; - _deviceID = data.identifierForVendor; - } - } - - void _onSessionReport() async { - // construct url for GET method - String url = QueryUrl; - // check for sessionID - String sessionID = _querySessionTextController.text; - String deviceIndex = _queryDeviceIndexTextController.text; - bool a = sessionID.length == 0; - bool b = deviceIndex.length == 0; - - if (a && b) { - // both index and sessionID is empty - } else if (a && (!b)) { - // sessionID is empty and deviceIndex is nonzerok - url += "deviceIndex=$deviceIndex"; - } else if ((!a) && b) { - // sessionID is nonempty and deviceIndex is zero - url += "sessionID=$sessionID"; - } else if ((!a) && (!b)) { - // both sessionId and deviceIndex are specified - url += "sessionID=$sessionID&deviceIndex=$deviceIndex"; - } - if (await url_launcher.canLaunch(url)) { - await url_launcher.launch(url); - } else { - throw 'Could not launch $url'; - } - - // await showDialog( - // context: context, - // builder: (BuildContext context) { - // return AlertDialog( - // title: const Text('Thanks!'), - // content: Text(url), - // actions: [ - // TextButton( - // onPressed: () { - // Navigator.pop(context); - // }, - // child: const Text('OK'), - // ), - // ], - // ); - // }, - // ); // showDialog - } -} diff --git a/lib/ui/debug/DebugExposurePanel.dart b/lib/ui/debug/DebugExposurePanel.dart deleted file mode 100644 index 13de35e4..00000000 --- a/lib/ui/debug/DebugExposurePanel.dart +++ /dev/null @@ -1,606 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:illinois/model/Exposure.dart'; -import 'package:illinois/utils/AppDateTime.dart'; -import 'package:illinois/service/Config.dart'; -import 'package:illinois/service/Exposure.dart'; -import 'package:illinois/service/NotificationService.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/ui/widgets/HeaderBar.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/utils/Utils.dart'; - -class DebugExposurePanel extends StatefulWidget { - - DebugExposurePanel(); - - @override - _DebugExposurePanelState createState() => _DebugExposurePanelState(); -} - -class _DebugExposurePanelState extends State implements NotificationsListener { - - TextEditingController _minDurationSettingController; - FocusNode _minDurationSettingFocusNode; - - TextEditingController _minRSSISettingController; - FocusNode _minRSSISettingFocusNode; - - List _teks; - List _exposures; - - bool _poolingTEKs; - bool _reportingTEKs; - bool _checkingTEKs; - bool _checkingExposures; - String _connection; - - Map _startSettings; - - @override - void initState() { - NotificationService().subscribe(this, [ - Exposure.notifyStartStop, - Exposure.notifyTEKsUpdated, - Exposure.notifyExposureUpdated, - Exposure.notifyExposureThick, - ]); - _startSettings = Map.from(Exposure().startSettings ?? Config().settings); - - //int minDuration = _startSettings['covid19ExposureServiceMinDuration']; - int minDuration = Exposure().exposureMinDuration; - _minDurationSettingController = TextEditingController(text: minDuration?.toString() ?? ''); - _minDurationSettingFocusNode = FocusNode(); - - int minRssi = _startSettings['covid19ExposureServiceMinRSSI']; - _minRSSISettingController = TextEditingController(text: minRssi?.toString() ?? ''); - _minRSSISettingFocusNode = FocusNode(); - - _loadTEKs(); - _loadExposures(); - - super.initState(); - } - - @override - void dispose() { - NotificationService().unsubscribe(this); - - _minDurationSettingController.dispose(); - _minDurationSettingFocusNode.dispose(); - - _minRSSISettingController.dispose(); - _minRSSISettingFocusNode.dispose(); - - super.dispose(); - } - - // NotificationsListener - - @override - void onNotification(String name, dynamic param) { - - if (name == Exposure.notifyStartStop) { - _updateConnection(null); - } - else if (name == Exposure.notifyTEKsUpdated) { - _loadTEKs(); - } - else if (name == Exposure.notifyExposureUpdated) { - _loadExposures(); - } - else if (name == Exposure.notifyExposureThick) { - _updateConnection(param); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: SimpleHeaderBarWithBack( - context: context, - titleWidget: Text("COVID-19 Exposure", style: TextStyle(color: Colors.white, fontSize: 16, fontFamily: Styles().fontFamilies.extraBold),), - ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeading(), - Padding(padding: EdgeInsets.all(16), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTEKs(), - Container(height: 32,), - _buildExposures(), - ]), - ), - ], - ), - ), - ), - ), - ], - ), - backgroundColor: Styles().colors.background, - ); - } - - Widget _buildHeading() { - String status; - bool canStart, canStop, canEdit; - if (!Exposure().isEnabled) { - status = 'disabled'; - canStart = canStop = canEdit = false; - } - else { - status = Exposure().isStarted ? 'started' : 'stopped'; - canStart = !Exposure().isStarted; - canStop = Exposure().isStarted; - canEdit = !Exposure().isStarted; - } - - return Container(color:Colors.white, - child: Padding(padding: EdgeInsets.all(16), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, - children:[ - Row(children: [ - Padding(padding: EdgeInsets.only(right: 4), child: Text('Status: ', style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorPrimary),),), - Text(status, style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Color(0xff494949),),), - ],), - - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: EdgeInsets.only(right: 4), child: Text('Conn: ', style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorPrimary),),), - Text(_connection ?? '', style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: Color(0xff494949),),), - ],), - - Padding(padding: EdgeInsets.only(top: 8), child: - Row(children: [ - Expanded(child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: EdgeInsets.only(bottom: 4), - child: Text("Min RSSI", style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 12, color: canEdit ? Styles().colors.fillColorPrimary : Styles().colors.disabledTextColor),), - ), - TextField( - controller: _minRSSISettingController, - focusNode: _minRSSISettingFocusNode, - enabled: canEdit, - decoration: InputDecoration(border: OutlineInputBorder(borderSide: BorderSide(color: Colors.black, width: 1.0)), contentPadding: EdgeInsets.symmetric(horizontal: 8)), - style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: canEdit ? Styles().colors.textBackground : Styles().colors.disabledTextColor,), - ), - ],), - ), - Container(width: 16,), - Expanded(child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: EdgeInsets.only(bottom: 4), - child: Text("Min Duration", style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 12, color: canEdit ? Styles().colors.fillColorPrimary : Styles().colors.disabledTextColor),), - ), - TextField( - controller: _minDurationSettingController, - focusNode: _minDurationSettingFocusNode, - enabled: canEdit, - decoration: InputDecoration(border: OutlineInputBorder(borderSide: BorderSide(color: Colors.black, width: 1.0)), contentPadding: EdgeInsets.symmetric(horizontal: 8)), - style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color: canEdit ? Styles().colors.textBackground : Styles().colors.disabledTextColor,), - ), - ],), - ), - ]), - ), - - Padding(padding: EdgeInsets.only(top: 8), child: - Row(children: [ - Expanded(child: - RoundedButton(label:"Start", - textColor: canStart ? Styles().colors.fillColorPrimary : Styles().colors.disabledTextColor, - borderColor: canStart ? Styles().colors.fillColorSecondary : Styles().colors.disabledTextColorTwo, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onStart(); } - ), - ), - Container(width: 16,), - Expanded(child: - RoundedButton(label:"Stop", - textColor: canStop ? Styles().colors.fillColorPrimary : Styles().colors.disabledTextColor, - borderColor: canStop ? Styles().colors.fillColorSecondary : Styles().colors.disabledTextColorTwo, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onStop(); } - ), - ) - ],), - ), - - ]), - ), - ); - } - - Widget _buildTEKs() { - - List tekWidgets = []; - if (_teks != null) { - for (ExposureTEK tek in _teks) { - String time = AppDateTime.formatDateTime(tek.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - String expiretime = AppDateTime.formatDateTime(tek.expireUtc, format: 'MM/dd HH:mm:ss UTC'); - tekWidgets.add(Row(children: [Text("${tek.tek} | start: $time | expire: $expiretime", style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 12, color: Color(0xff494949),),),],),); - } - } - - if (tekWidgets.isEmpty) { - tekWidgets.add(Row(children: [],),); - } - - List content = []; - content.add(Text('Local TEKs:', style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorPrimary),),); - content.add(Container(height: 100, decoration: BoxDecoration(border: Border.all(color: Styles().colors.fillColorPrimary, width: 1)), child: - Stack(children: [ - SingleChildScrollView(child: - Padding(padding: EdgeInsets.all(4), child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: tekWidgets) - ), - ), - Align(alignment: Alignment.topRight, - child: Semantics (button: true, label: 'Copy', - child: GestureDetector(onTap: () { _onCopyTEKs(); }, - child: Container(width: 36, height: 36, - child: Align(alignment: Alignment.center, - child: Semantics( excludeSemantics: true, child: Image.asset('images/icon-copy.png')), - ), - ), - ), - )), - ],), - )); - - int teksCount = _teks?.length ?? 0; - - content.add(Padding(padding: EdgeInsets.only(top: 8), child: - Row(children: [ - Expanded(child: - Stack(children: [ - RoundedButton(label: 'Pull', - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onPullTEKs(); } - ), - Visibility(visible: (_poolingTEKs == true), child: - Center(child: - Padding(padding: EdgeInsets.only(top: 10.5), child: - Container(width: 21, height: 21, child: - CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 2,) - ), - ), - ), - ), - ]), - ), - Container(width: 8,), - Expanded(child: - Stack(children: [ - RoundedButton(label: 'Report', - textColor: (0 < teksCount) ? Styles().colors.fillColorPrimary : Styles().colors.disabledTextColor, - borderColor: (0 < teksCount) ? Styles().colors.fillColorSecondary : Styles().colors.disabledTextColor, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onReportTEKs(); } - ), - Visibility(visible: (_reportingTEKs == true), child: - Center(child: - Padding(padding: EdgeInsets.only(top: 10.5), child: - Container(width: 21, height: 21, child: - CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 2,) - ), - ), - ), - ), - ]), - ), - Container(width: 8,), - Expanded(child: - Stack(children: [ - RoundedButton(label: 'Check', - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onCheckTEKs(); } - ), - Visibility(visible: (_checkingTEKs == true), child: - Center(child: - Padding(padding: EdgeInsets.only(top: 10.5), child: - Container(width: 21, height: 21, child: - CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 2,) - ), - ), - ), - ), - ]), - ), - ],), - )); - - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: content); - } - - - Widget _buildExposures() { - List content = []; - - List exposureWidgets = []; - if (_exposures != null) { - for (ExposureRecord exposure in _exposures) { - String time = AppDateTime.formatDateTime(exposure.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - exposureWidgets.add(Row(children: [Text("RPI: ${exposure.rpi} \nTime: $time | Duration: ${exposure.durationDisplayString}", style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 12, color: Color(0xff494949),),),],),); - } - } - if (exposureWidgets.isEmpty) { - exposureWidgets.add(Row(children: [],),); - } - - content.add(Text('Recorded Exposures:', style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 16, color: Styles().colors.fillColorPrimary),),); - content.add(Container(height: 100, decoration: BoxDecoration(border: Border.all(color: Styles().colors.fillColorPrimary, width: 1)), child: - Stack(children: [ - SingleChildScrollView(child: - Padding(padding: EdgeInsets.all(4), child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: exposureWidgets) - ), - ), - Align(alignment: Alignment.topRight, - child: Semantics (button: true, label: 'Copy', - child: GestureDetector(onTap: () { _onCopyExposures(); }, - child: Container(width: 36, height: 36, - child: Align(alignment: Alignment.center, - child: Semantics( excludeSemantics: true, child: Image.asset('images/icon-copy.png')), - ), - ), - ), - )), - ],), - )); - - int exposuresCount = _exposures?.length ?? 0; - - content.add(Padding(padding: EdgeInsets.only(top: 8), child: - Row(children: [ - Expanded(child: - Stack(children: [ - RoundedButton(label: 'Check', - textColor: (0 < exposuresCount) ? Styles().colors.fillColorPrimary : Styles().colors.disabledTextColor, - borderColor: (0 < exposuresCount) ? Styles().colors.fillColorSecondary : Styles().colors.disabledTextColor, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onCheckExposures(); } - ), - Visibility(visible: (_checkingExposures == true), child: - Center(child: - Padding(padding: EdgeInsets.only(top: 10.5), child: - Container(width: 21, height: 21, child: - CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Styles().colors.fillColorSecondary), strokeWidth: 2,) - ), - ), - ), - ), - ]), - ), - Container(width: 16,), - Expanded(child: - RoundedButton(label: 'Clear', - textColor: (0 < exposuresCount) ? Styles().colors.fillColorPrimary : Styles().colors.disabledTextColor, - borderColor: (0 < exposuresCount) ? Styles().colors.fillColorSecondary : Styles().colors.disabledTextColor, - backgroundColor: Styles().colors.white, - fontFamily: Styles().fontFamilies.bold, - fontSize: 16, borderWidth: 2, height: 42, - onTap:() { _onClearExposures(); } - ), - ), - ],), - - )); - - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: content); - } - - void _loadTEKs() { - Exposure().loadTeks().then((List teks) { - setState(() { - _teks = teks; - }); - }); - } - - void _loadExposures() { - Exposure().loadLocalExposures(timestamp: Exposure.thresholdTimestamp).then((List exposures) { - setState(() { - _exposures = exposures; - }); - }); - } - - void _updateConnection(Map exposure) { - - String rpi = (exposure != null) ? exposure['rpi'] : null; - int timestamp = (exposure != null) ? exposure['timestamp'] : null; - int rssi = (exposure != null) ? exposure['rssi'] : null; - String time = (timestamp != null) ? AppDateTime.formatDateTime(DateTime.fromMillisecondsSinceEpoch(timestamp, isUtc: true), format: 'MM/dd HH:mm:ss UTC') : null; - setState(() { - _connection = Exposure().isStarted ? "RPI: $rpi\nTime: $time | RSSI: $rssi" : ''; - }); - - } - - void _onStart() { - - int minDuration = int.tryParse(_minDurationSettingController.text); - if (minDuration == null) { - AppAlert.showDialogResult(context, "Please enter an integer minimum duration").then((_) { - _minDurationSettingFocusNode.requestFocus(); - }); - return; - } - - int minRssi = int.tryParse(_minRSSISettingController.text); - if (minRssi == null) { - AppAlert.showDialogResult(context, "Please enter an integer minimum RSSI value").then((_) { - _minRSSISettingFocusNode.requestFocus(); - }); - return; - } - - //_startSettings['covid19ExposureServiceMinDuration'] = minDuration; - Exposure().exposureMinDuration = minDuration; - _startSettings['covid19ExposureServiceMinRSSI'] = minRssi; - - Exposure().start(settings: _startSettings); - } - - void _onStop() { - Exposure().stop(); - } - - void _onPullTEKs() { - setState(() { - _poolingTEKs = true; - }); - Exposure().loadReportedTEKs(timestamp: Exposure.thresholdTimestamp).then((List result) { - setState(() { - _poolingTEKs = false; - }); - - String copy = ""; - int copied; - if (result != null) { - copied = 0; - for (ExposureTEK tek in result) { - String time = AppDateTime.formatDateTime(tek.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - copy += "${tek.tek} | $time\n"; - copied++; - } - Clipboard.setData(ClipboardData(text: copy)); - } - if (copied == null) { - AppAlert.showDialogResult(context, "Failed to pull reported."); - } else { - AppAlert.showDialogResult(context, (0 < copied) ? "$copied entries copied to Clipboard." : "No entries copied to Clipboard."); - } - }); - } - - void _onReportTEKs() { - setState(() { - _reportingTEKs = true; - }); - Exposure().reportTEKs(_teks).then((bool result) { - setState(() { - _reportingTEKs = false; - }); - _loadTEKs(); - AppAlert.showDialogResult(context, result ? "Successfully reported" : "Failed to report"); - }); - } - - void _onCheckTEKs() { - setState(() { - _checkingTEKs = true; - }); - Exposure().checkReport().then((int result) { - setState(() { - _checkingTEKs = false; - }); - String message; - if (result == null) { - message = 'Failed to report TEKs'; - } - else if (result == 0) { - message = "No TEKs reported"; - } - else { - message = "$result ${(1 < result) ? 'TEKs' : 'TEK'} reported"; - } - - AppAlert.showDialogResult(context, message); - }); - } - - void _onCopyTEKs() { - String copy = ""; - int copied = 0; - if (_teks != null) { - for (ExposureTEK tek in _teks) { - String time = AppDateTime.formatDateTime(tek.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - copy += "${tek.tek} | $time\n"; - copied++; - } - Clipboard.setData(ClipboardData(text: copy)); - } - AppAlert.showDialogResult(context, (0 < copied) ? "$copied entries copied to Clipboard" : "No entries copied to Clipboard."); - } - - void _onCheckExposures() { - setState(() { - _checkingExposures = true; - }); - Exposure().checkExposures().then((int detectedCount) { - setState(() { - _checkingExposures = false; - }); - String message; - if (detectedCount == null) { - message = "Failed to check exposures."; - } - else if (detectedCount == 0) { - message = "No exposures detected"; - } - else { - message = "Detected $detectedCount ${ (detectedCount != 1) ? 'exposures' : 'exposure' }."; - } - - AppAlert.showDialogResult(context, message); - }); - } - - void _onClearExposures() { - Exposure().clearLocalExposures(); - } - - void _onCopyExposures() { - String copy = ""; - int copied = 0; - if (_exposures != null) { - for (ExposureRecord exposure in _exposures) { - String time = AppDateTime.formatDateTime(exposure.dateUtc, format: 'MM/dd HH:mm:ss UTC'); - copy += "${exposure.rpi} | Time: $time | Duration: ${exposure.durationDisplayString}\n"; - copied++; - } - Clipboard.setData(ClipboardData(text: copy)); - } - AppAlert.showDialogResult(context, (0 < copied) ? "$copied entries copied to Clipboard" : "No entries copied to Clipboard."); - } -} diff --git a/lib/ui/debug/DebugHealthRulesPanel.dart b/lib/ui/debug/DebugHealthRulesPanel.dart index 4beb5fd4..b7d488ed 100644 --- a/lib/ui/debug/DebugHealthRulesPanel.dart +++ b/lib/ui/debug/DebugHealthRulesPanel.dart @@ -84,13 +84,14 @@ class _DebugHealthRulesPanelState extends State{ Health().loadRulesJson(countyId: _selectedCountyId).then((Map rules) { if (mounted) { - if ((rules != null) && (Health().userTestMonitorInterval != null)) { + int userTestMonitorInterval = Health().userOverride?.effectiveTestInterval; + if ((rules != null) && (userTestMonitorInterval != null)) { dynamic intervals = rules['intervals']; if (intervals == null) { rules['intervals'] = intervals = {}; } if (intervals is Map) { - intervals[HealthRulesSet.UserTestMonitorInterval] = Health().userTestMonitorInterval; + intervals[HealthRulesSet.UserTestMonitorInterval] = userTestMonitorInterval; } } diff --git a/lib/ui/debug/DebugHomePanel.dart b/lib/ui/debug/DebugHomePanel.dart index 30b5d778..fb5d2d8f 100644 --- a/lib/ui/debug/DebugHomePanel.dart +++ b/lib/ui/debug/DebugHomePanel.dart @@ -31,8 +31,6 @@ import 'package:illinois/service/UserProfile.dart'; import 'package:illinois/service/Storage.dart'; import 'package:illinois/ui/debug/DebugCreateEventPanel.dart'; import 'package:illinois/ui/debug/DebugDirectionsPanel.dart'; -import 'package:illinois/ui/debug/DebugExposureLogsPanel.dart'; -import 'package:illinois/ui/debug/DebugExposurePanel.dart'; import 'package:illinois/ui/debug/DebugHealthKeysPanel.dart'; import 'package:illinois/ui/debug/DebugSymptomsReportPanel.dart'; import 'package:illinois/ui/debug/DebugContactTraceReportPanel.dart'; @@ -257,24 +255,6 @@ class _DebugHomePanelState extends State implements Notification textColor: Styles().colors.fillColorPrimary, borderColor: Styles().colors.fillColorPrimary, onTap: _onTapTraceCovid19Exposure)), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), - child: RoundedButton( - label: "COVID-19 Exposures", - backgroundColor: Styles().colors.background, - fontSize: 16.0, - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorPrimary, - onTap: _onTapCovid19Exposures)), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), - child: RoundedButton( - label: "COVID-19 Exposure Logs", - backgroundColor: Styles().colors.background, - fontSize: 16.0, - textColor: Styles().colors.fillColorPrimary, - borderColor: Styles().colors.fillColorPrimary, - onTap: _onTapCovid19ExposureLogs)), Padding(padding: EdgeInsets.only(top: 10), child: Container()), ]); @@ -418,14 +398,6 @@ class _DebugHomePanelState extends State implements Notification Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugContactTraceReportPanel())); } - void _onTapCovid19Exposures() { - Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugExposurePanel())); - } - - void _onTapCovid19ExposureLogs() { - Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugExposureLogsPanel())); - } - void _onTapCovid19Rules(){ Navigator.push(context, CupertinoPageRoute(builder: (context) => DebugHealthRulesPanel())); } diff --git a/lib/ui/health/HealthHomePanel.dart b/lib/ui/health/HealthHomePanel.dart index 511b9482..b2442aa3 100644 --- a/lib/ui/health/HealthHomePanel.dart +++ b/lib/ui/health/HealthHomePanel.dart @@ -74,6 +74,7 @@ class _HealthHomePanelState extends State implements Notificati Health.notifyStatusUpdated, Health.notifyHistoryUpdated, Health.notifyUserAccountCanged, + Health.notifyUserOverrideChanged, ]); _refresh(); @@ -92,6 +93,7 @@ class _HealthHomePanelState extends State implements Notificati (name == Health.notifyStatusUpdated) || (name == Health.notifyHistoryUpdated) || (name == Health.notifyUserAccountCanged) || + (name == Health.notifyUserOverrideChanged) || (name == FlexUI.notifyChanged)) { if (mounted) { @@ -685,11 +687,20 @@ class _HealthHomePanelState extends State implements Notificati String statusTitleText, statusTitleHtml; String statusDescriptionText, statusDescriptionHtml; String headingTitle = Localization().getStringEx('panel.covid19home.vaccination.heading.title', 'VACCINATION'); - - HealthHistory recentVaccine = getRecentVaccine(); - - if (recentVaccine == null) { - // No vaccine at all - promote it. + bool shouldMakeAppointment; + + bool exemptFromVaccination = (Health().userOverride?.vaccinationExempt == true); + bool vaccinationSuspended = (Health().userOverride?.effectiveTestInterval != null); + int recentVaccineIndex = !exemptFromVaccination ? getRecentVaccineIndex(Health().history) : null; + HealthHistory recentVaccine = ((recentVaccineIndex != null) && (0 <= recentVaccineIndex) && (recentVaccineIndex < Health().history.length)) ? Health().history[recentVaccineIndex] : null; + if (exemptFromVaccination) { + // 2.2. if "Exempt" is true, just to say "You are currently exempt from taking COVID-19 vaccines but are required to continue taking tests." + statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.exempt.title', 'Exempt from vaccination'); + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.exempt.description', 'You are currently exempt from taking COVID-19 vaccines but are required to continue taking tests.'); + } + else if (recentVaccine == null) { + // No vaccined at all - promote it. + shouldMakeAppointment = true; statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.none.title', 'Get a vaccine now'); statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.none.description', """ • COVID-19 vaccines are safe @@ -697,36 +708,53 @@ class _HealthHomePanelState extends State implements Notificati • COVID-19 vaccines allow you to safely do more • COVID-19 vaccines build safer protection"""); } - else { - headingDate = AppDateTime.formatDateTime(recentVaccine.dateUtc?.toLocal(), format:"MMMM dd, yyyy", locale: Localization().currentLocale?.languageCode); - statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.title', 'Vaccinated'); - - if ((recentVaccine.blob?.isVaccineEffective ?? false) && (recentVaccine.dateUtc != null)) { - DateTime now = DateTime.now(); - if (recentVaccine.dateUtc.isBefore(now.toUtc())) { - // 5.2.4 When effective then hide the widget - return null; - } - else { - // Vaccinated, but not effective yet. - int delayInDays = AppDateTime.midnight(recentVaccine.dateUtc.toLocal()).difference(AppDateTime.midnight(now)).inDays; - - if (delayInDays > 1) { - statusDescriptionText = sprintf(Localization().getStringEx('panel.covid19home.vaccination.vaccinated.effective.n.description', 'Your vaccine will be effective after %s days.'), [delayInDays]); - } - else if (delayInDays == 1) { - statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.effective.1.description', 'Your vaccine will be effective tomorrow.'); + else if ((recentVaccine.blob?.isVaccineEffective ?? false) && (recentVaccine.dateUtc != null)) { + DateTime now = DateTime.now(); + if (recentVaccine.dateUtc.isBefore(now.toUtc())) { + // Check if vaccine booster interval has expired + DateTime vaccineExpireDateLocal = HealthHistory.getVaccineExpireDateLocal(history: Health().history, vaccineIndex: recentVaccineIndex, rules: Health().rules); + if ((vaccineExpireDateLocal == null) || now.isBefore(vaccineExpireDateLocal)) { + if (!vaccinationSuspended) { + // 5.2.4 When effective then hide the widget + return null; } else { - statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.effective.0.description', 'Your vaccine will be effective today.'); + // Vaccinated status suspended + statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.suspended.title', 'Vaccination status suspended'); + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.suspended.description', 'You are currently effectively vaccinated but are required to continue taking tests until further notice.'); } } + else { + // Vaccine expired + headingDate = AppDateTime.formatDateTime(vaccineExpireDateLocal, format:"MMMM dd, yyyy", locale: Localization().currentLocale?.languageCode); + statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.expired.title', 'Vaccine Expired'); + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.expired.description', 'Get a booster dose now.'); + } } else { - // Vaccinated, unknown status or date. - statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.description', 'Your vaccination is not effective yet.'); + // Vaccinated, but not effective yet. + headingDate = AppDateTime.formatDateTime(recentVaccine.dateUtc?.toLocal(), format:"MMMM dd, yyyy", locale: Localization().currentLocale?.languageCode); + statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.title', 'Vaccinated'); + + int delayInDays = AppDateTime.midnightsDifferenceInDays(AppDateTime.todayMidnightLocal, recentVaccine.dateMidnightLocal); + + if (delayInDays > 1) { + statusDescriptionText = sprintf(Localization().getStringEx('panel.covid19home.vaccination.vaccinated.effective.n.description', 'Your vaccine will be effective after %s days.'), [delayInDays]); + } + else if (delayInDays == 1) { + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.effective.1.description', 'Your vaccine will be effective tomorrow.'); + } + else { + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.effective.0.description', 'Your vaccine will be effective today.'); + } } } + else { + // Vaccinated, unknown status or date. + headingDate = AppDateTime.formatDateTime(recentVaccine.dateUtc?.toLocal(), format:"MMMM dd, yyyy", locale: Localization().currentLocale?.languageCode); + statusTitleText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.title', 'Vaccinated'); + statusDescriptionText = Localization().getStringEx('panel.covid19home.vaccination.vaccinated.description', 'Your vaccination is not effective yet.'); + } List contentWidgets = [ Row(children: [ @@ -792,7 +820,7 @@ class _HealthHomePanelState extends State implements Notificati ],), ]; - if ((recentVaccine == null) && (Config().vaccinationAppointUrl != null)) { + if ((shouldMakeAppointment == true) && (Config().vaccinationAppointUrl != null)) { contentList.addAll([ Container(margin: EdgeInsets.only(top: 14, bottom: 14), height: 1, color: Styles().colors.fillColorPrimaryTransparent015,), @@ -816,21 +844,22 @@ class _HealthHomePanelState extends State implements Notificati )); } - HealthHistory getRecentVaccine() { - HealthHistory mostRecentVaccine; - if (Health().history != null) { - for (HealthHistory historyEntry in Health().history) { + static int getRecentVaccineIndex(List history) { + int mostRecentVaccineIndex; + if (history != null) { + for (int index = 0; index < history.length; index++) { + HealthHistory historyEntry = history[index]; if (historyEntry.isVaccine) { if (historyEntry.blob?.isVaccineEffective ?? false) { - return historyEntry; + return index; } - else if (mostRecentVaccine == null) { - mostRecentVaccine = historyEntry; + else if (mostRecentVaccineIndex == null) { + mostRecentVaccineIndex = index; } } } } - return mostRecentVaccine; + return mostRecentVaccineIndex; } Widget _buildTileButtons() { diff --git a/lib/ui/health/HealthStatusPanel.dart b/lib/ui/health/HealthStatusPanel.dart index 9c02dd72..de0617e2 100644 --- a/lib/ui/health/HealthStatusPanel.dart +++ b/lib/ui/health/HealthStatusPanel.dart @@ -457,7 +457,7 @@ class _HealthStatusPanelState extends State implements Notifi ), ), ), - Visibility(visible: Health().isVaccinated && FlexUI().hasFeature('vaccination_badge'), child: + Visibility(visible: FlexUI().hasFeature('vaccination_badge') && Health().isVaccinated, child: Container(width: screenWidth, height: _photoSize, child: Align(alignment: Alignment.bottomRight, child: Padding(padding: EdgeInsets.only(right: vaccinatedPaddingWidth), child: diff --git a/lib/ui/onboarding/OnboardingAuthBluetoothPanel.dart b/lib/ui/onboarding/OnboardingAuthBluetoothPanel.dart deleted file mode 100644 index c9d1aae7..00000000 --- a/lib/ui/onboarding/OnboardingAuthBluetoothPanel.dart +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/BluetoothServices.dart'; -import 'package:illinois/service/Onboarding.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/ui/onboarding/OnboardingBackButton.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/ui/widgets/ScalableScrollView.dart'; -import 'package:illinois/ui/widgets/SwipeDetector.dart'; -import 'package:illinois/ui/widgets/TrianglePainter.dart'; - -class OnboardingAuthBluetoothPanel extends StatefulWidget with OnboardingPanel { - - final Map onboardingContext; - - OnboardingAuthBluetoothPanel({this.onboardingContext}); - - _OnboardingAuthBluetoothPanelState createState() => _OnboardingAuthBluetoothPanelState(); - - @override - bool get onboardingCanDisplay { - // Android does not support Bluetooth permisions. - // Bluetooth permisions in iOS should be prompted when "Consent to participate in the Exposure Notification system" is selected. - // return Platform.isIOS && (BluetoothServices().status != BluetoothStatus.PermissionAllowed); - return false; - } -} - -class _OnboardingAuthBluetoothPanelState extends State { - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - String notRightNow = Localization().getStringEx( - 'panel.onboarding.bluetooth.button.dont_allow.title', - 'Not right now'); - return Scaffold( - backgroundColor: Styles().colors.background, - body: SwipeDetector( - onSwipeLeft: () => _goNext(), - onSwipeRight: () => _goBack(), - child: - ScalableScrollView( - scrollableChild: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Stack( - children: [ - Column( - children: [ - Container(height: 90,color: Styles().colors.surface,), - CustomPaint( - painter: InvertedTrianglePainter(painterColor: Styles().colors.surface, left : true, ), - child: Container( - height: 67, - ), - ), - ], - ), - Column( - children: [ - Row( - children: [ - Expanded(child: Image.asset('images/background-onboarding-squares-dark.png', excludeFromSemantics: true,fit: BoxFit.fitWidth,)), - ], - ), - ], - ), - Container(margin: EdgeInsets.only(top: 80, bottom: 20),child: Center(child: Image.asset('images/enable-bluetooth-header.png', excludeFromSemantics: true,))), - Align( - alignment: Alignment.topLeft, - child: OnboardingBackButton(padding: EdgeInsets.only(top: 24, left:12.5, right: 20, bottom: 20), onTap: () => _goBack()), - ) - ], - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Semantics( header: true, hint: Localization().getStringEx("app.common.heading.one.hint","Header 1"), - child:Text(Localization().getStringEx('panel.onboarding.bluetooth.label.title', "Enable Bluetooth"), - textAlign: TextAlign.left, - style: TextStyle(fontFamily: Styles().fontFamilies.bold, fontSize: 28, color:Styles().colors.fillColorPrimary), - )) - ), - Container(height: 12, ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.topCenter, - child: Text( - Localization().getStringEx( - 'panel.onboarding.bluetooth.label.description', - "Use Bluetooth to alert you to potential exposure to COVID-19."), - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 20, - color: Styles().colors.fillColorPrimary), - ), - )) - ]), - bottomNotScrollableWidget: - Container(color: Styles().colors.white, child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16,vertical: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ScalableRoundedButton( - label: Localization().getStringEx( - 'panel.onboarding.bluetooth.button.allow.title', - 'Enable Bluetooth'), - hint: Localization().getStringEx( - 'panel.onboarding.bluetooth.button.allow.hint', - ''), - borderColor: Styles().colors.lightBlue, - backgroundColor: Styles().colors.white, - textColor: Styles().colors.fillColorPrimary, - onTap: () => _requestBluetooth(context), - ), - GestureDetector( - onTap: () { - Analytics.instance.logSelect(target: 'Not right now') ; - return _goNext(); - }, - child: Semantics( - label: notRightNow, - hint: Localization().getStringEx( - 'panel.onboarding.bluetooth.button.dont_allow.hint', - ''), - button: true, - excludeSemantics: true, - child: Padding( - padding: EdgeInsets.only(top: 15), - child: Text( - notRightNow, - style: TextStyle( - fontFamily: Styles().fontFamilies.medium, - fontSize: 16, - color: Styles().colors.fillColorPrimary, - decoration: TextDecoration.underline, - decorationColor: Styles().colors.lightBlue, - decorationThickness: 1, - decorationStyle: - TextDecorationStyle.solid), - ))), - ) - ], - ), - ) - )))); - } - - void _requestBluetooth(BuildContext context) { - - Analytics.instance.logSelect(target: 'Enable Bluetooth') ; - - BluetoothStatus authStatus = BluetoothServices().status; - if (authStatus == BluetoothStatus.PermissionNotDetermined) { - BluetoothServices().requestStatus().then((_){ - _goNext(); - }); - } - else if (authStatus == BluetoothStatus.PermissionDenied) { - String message = Localization().getStringEx('panel.onboarding.bluetooth.label.access_denied', 'You have already denied access to this app.'); - showDialog(context: context, builder: (context) => _buildDialogWidget(context, message: message, pushNext: false)); - } - else if (authStatus == BluetoothStatus.PermissionAllowed) { - String message = Localization().getStringEx('panel.onboarding.bluetooth.label.access_granted', 'You have already granted access to this app.'); - showDialog(context: context, builder: (context) => _buildDialogWidget(context, message: message, pushNext: true)); - } - } - - Widget _buildDialogWidget(BuildContext context, {String message, bool pushNext}) { - String okTitle = Localization().getStringEx('dialog.ok.title', 'OK'); - return Dialog( - child: Padding( - padding: EdgeInsets.all(18), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - Localization().getStringEx('app.title', 'Safer Illinois'), - style: TextStyle(fontSize: 24, color: Colors.black), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 26), - child: Text( - message, - textAlign: TextAlign.left, - style: TextStyle( - fontFamily: Styles().fontFamilies.medium, - fontSize: 16, - color: Colors.black), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Analytics.instance.logAlert(text: message, selection:okTitle); - if (pushNext) { - _goNext(replace : true); - } - else { - _closeDialog(context); - } - }, - child: Text(okTitle)) - ], - ) - ], - ), - ), - ); - } - - void _closeDialog(BuildContext context) { - Navigator.pop(context, true); - } - - void _goNext({bool replace = false}) { - Onboarding().next(context, widget, replace: replace); - } - - void _goBack() { - Analytics.instance.logSelect(target: "Back"); - Navigator.of(context).pop(); - } -} diff --git a/lib/ui/onboarding/OnboardingAuthLocationPanel.dart b/lib/ui/onboarding/OnboardingAuthLocationPanel.dart deleted file mode 100644 index b317948c..00000000 --- a/lib/ui/onboarding/OnboardingAuthLocationPanel.dart +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/LocationServices.dart'; -import 'package:illinois/service/Onboarding.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/ui/onboarding/OnboardingBackButton.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/ui/widgets/ScalableScrollView.dart'; -import 'package:illinois/ui/widgets/SwipeDetector.dart'; - -class OnboardingAuthLocationPanel extends StatelessWidget with OnboardingPanel { - final Map onboardingContext; - OnboardingAuthLocationPanel({this.onboardingContext}); - - Future get onboardingCanDisplayAsync async { - // Location permisions in iOS should be prompted when "Consent to participate in the Exposure Notification system" is selected. - LocationServicesStatus status = await LocationServices.instance.status; - return Platform.isAndroid && (status != LocationServicesStatus.PermissionAllowed); - } - - @override - Widget build(BuildContext context) { - String titleText = Localization().getStringEx('panel.onboarding.location.label.title', "Turn on Location Services"); - String notRightNow = Localization().getStringEx( - 'panel.onboarding.location.button.dont_allow.title', - 'Not right now'); - return Scaffold( - backgroundColor: Styles().colors.background, - body: SwipeDetector( - onSwipeLeft: () => _goNext(context), - onSwipeRight: () => _goBack(context), - child:ScalableScrollView( scrollableChild: - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Stack(children: [ - Image.asset( - 'images/share-location-header.png', - fit: BoxFit.fitWidth, - width: MediaQuery.of(context).size.width, - excludeFromSemantics: true, - ), - OnboardingBackButton( - padding: const EdgeInsets.only(left: 10, top: 30, right: 20, bottom: 20), - onTap:() { - Analytics.instance.logSelect(target: "Back"); - _goBack(context); - }), - ]), - Semantics( - label: titleText, - hint: Localization().getStringEx('panel.onboarding.location.label.title.hint', 'Header 1'), - excludeSemantics: true, - child: - Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.center, - child: Text(titleText, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 32, - color: Styles().colors.fillColorPrimary), - )), - )), - Container( - height: 12, - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.topCenter, - child: Text( - Localization().getStringEx( - 'panel.onboarding.location.label.description', - "Background location is required for Bluetooth-based exposure notification to work on your phone"), - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 20, - color: Styles().colors.fillColorPrimary), - ), - )), - ]), - bottomNotScrollableWidget: - Padding( - padding: EdgeInsets.symmetric(horizontal: 24,vertical: 2), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScalableRoundedButton( - label: Localization().getStringEx( - 'panel.onboarding.location.button.allow.title', - 'Enable Location Services'), - hint: Localization().getStringEx( - 'panel.onboarding.location.button.allow.hint', - ''), - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.background, - textColor: Styles().colors.fillColorPrimary, - onTap: () => _requestLocation(context), - ), - GestureDetector( - onTap: () { - Analytics.instance.logSelect(target: 'Not right now') ; - return _goNext(context); - }, - child: Semantics( - label: notRightNow, - hint: Localization().getStringEx( - 'panel.onboarding.location.button.dont_allow.hint', - ''), - button: true, - excludeSemantics: true, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Text( - notRightNow, - style: TextStyle( - fontFamily: Styles().fontFamilies.medium, - fontSize: 16, - color: Styles().colors.fillColorPrimary, - decoration: TextDecoration.underline, - decorationColor: Styles().colors.fillColorSecondary, - decorationThickness: 1, - decorationStyle: - TextDecorationStyle.solid), - ))), - ) - ], - ), - ) - ))); - } - - void _requestLocation(BuildContext context) async { - Analytics.instance.logSelect(target: 'Share My locaiton') ; - LocationServices.instance.status.then((LocationServicesStatus status){ - if (status == LocationServicesStatus.ServiceDisabled) { - LocationServices.instance.requestService(); - } - else if (status == LocationServicesStatus.PermissionNotDetermined) { - LocationServices.instance.requestPermission().then((LocationServicesStatus status) { - _goNext(context); - }); - } - else if (status == LocationServicesStatus.PermissionDenied) { - String message = Localization().getStringEx('panel.onboarding.location.label.access_denied', 'You have already denied access to this app.'); - showDialog(context: context, builder: (context) => _buildDialogWidget(context, message:message, pushNext : false )); - } - else if (status == LocationServicesStatus.PermissionAllowed) { - String message = Localization().getStringEx('panel.onboarding.location.label.access_granted', 'You have already granted access to this app.'); - showDialog(context: context, builder: (context) => _buildDialogWidget(context, message:message, pushNext : true )); - } - }); - } - - Widget _buildDialogWidget(BuildContext context, {String message, bool pushNext}) { - String okTitle = Localization().getStringEx('dialog.ok.title', 'OK'); - return Dialog( - child: Padding( - padding: EdgeInsets.all(18), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - Localization().getStringEx('app.title', 'Safer Illinois'), - style: TextStyle(fontSize: 24, color: Colors.black), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 26), - child: Text( - message, - textAlign: TextAlign.left, - style: TextStyle( - fontFamily: Styles().fontFamilies.medium, - fontSize: 16, - color: Colors.black), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Analytics.instance.logAlert(text: message, selection:okTitle); - if (pushNext) { - _goNext(context, replace : true); - } - else { - _closeDialog(context); - } - }, - child: Text(okTitle)) - ], - ) - ], - ), - ), - ); - } - - void _closeDialog(BuildContext context) { - Navigator.pop(context, true); - } - - void _goNext(BuildContext context, {bool replace = false}) { - Onboarding().next(context, this, replace: replace); - } - - void _goBack(BuildContext context) { - Navigator.of(context).pop(); - } -} diff --git a/lib/ui/onboarding/OnboardingAuthNotificationsPanel.dart b/lib/ui/onboarding/OnboardingAuthNotificationsPanel.dart deleted file mode 100644 index 5b453522..00000000 --- a/lib/ui/onboarding/OnboardingAuthNotificationsPanel.dart +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2020 Board of Trustees of the University of Illinois. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:illinois/service/Analytics.dart'; -import 'package:illinois/service/Onboarding.dart'; -import 'package:illinois/service/Localization.dart'; -import 'package:illinois/ui/widgets/RoundedButton.dart'; -import 'package:illinois/ui/onboarding/OnboardingBackButton.dart'; -import 'package:illinois/service/Styles.dart'; -import 'package:illinois/service/NativeCommunicator.dart'; -import 'package:illinois/service/LocalNotifications.dart'; -import 'package:illinois/ui/widgets/ScalableScrollView.dart'; -import 'package:illinois/ui/widgets/SwipeDetector.dart'; -import 'dart:io' show Platform; - -class OnboardingAuthNotificationsPanel extends StatelessWidget with OnboardingPanel { - final Map onboardingContext; - OnboardingAuthNotificationsPanel({this.onboardingContext}); - - bool get onboardingCanDisplay { - return Platform.isIOS; - } - - Future get onboardingCanDisplayAsync async { - bool notificationsAuthorized = await NativeCommunicator().queryNotificationsAuthorization("query"); - return !notificationsAuthorized; - } - - @override - Widget build(BuildContext context) { - String titleText = Localization().getStringEx('panel.onboarding.notifications.label.title', 'Info when you need it'); - String notRightNow = Localization().getStringEx( - 'panel.onboarding.notifications.button.dont_allow.title', - 'Not right now'); - return Scaffold( - backgroundColor: Styles().colors.background, - body: SwipeDetector( - onSwipeLeft: () => _goNext(context) , - onSwipeRight: () => _goBack(context), - child: - ScalableScrollView( - scrollableChild: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Stack(children: [ - Image.asset( - 'images/allow-notifications-header.png', - fit: BoxFit.fitWidth, - width: MediaQuery.of(context).size.width, - excludeFromSemantics: true, - ), - OnboardingBackButton( - padding: const EdgeInsets.only(left: 10, top: 30, right: 20, bottom: 20), - onTap:() { - Analytics.instance.logSelect(target: "Back"); - _goBack(context); - }), - ]), - Semantics( - label: titleText, - hint: Localization().getStringEx('panel.onboarding.notifications.label.title.hint', 'Header 1'), - excludeSemantics: true, - child: - Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.center, - child: Text( - titleText, - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.bold, - fontSize: 32, - color: Styles().colors.fillColorPrimary), - ), - ))), - Container(height: 12,), - Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Align( - alignment: Alignment.topCenter, - child: Column(mainAxisAlignment: MainAxisAlignment.start, children: [ - Text( - Localization().getStringEx('panel.onboarding.notifications.label.description1', 'Get notified about COVID-19 info'), - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 20, - color: Styles().colors.fillColorPrimary), - ), - Padding(padding: EdgeInsets.only(top: 10), child: Text( - Localization().getStringEx('panel.onboarding.notifications.label.description2', 'This is required for Exposure Notifications to work in background on your phone'), - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: Styles().fontFamilies.regular, - fontSize: 20, - color: Styles().colors.fillColorPrimary), - ),) - ],)), - ), - ], - ), - bottomNotScrollableWidget: Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScalableRoundedButton( - label: Localization().getStringEx('panel.onboarding.notifications.button.allow.title', 'Enable Notifications'), - hint: Localization().getStringEx('panel.onboarding.notifications.button.allow.hint', ''), - borderColor: Styles().colors.fillColorSecondary, - backgroundColor: Styles().colors.background, - textColor: Styles().colors.fillColorPrimary, - onTap: () => _onReceiveNotifications(context), - ), - GestureDetector( - onTap: () { - Analytics.instance.logSelect(target: 'Not right now') ; - return _goNext(context); - }, - child: Semantics( - label:notRightNow, - hint:Localization().getStringEx('panel.onboarding.notifications.button.dont_allow.hint', ''), - button: true, - excludeSemantics: true, - child:Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Text( - notRightNow, - style: TextStyle( - fontFamily: Styles().fontFamilies.medium, - fontSize: 16, - color: Styles().colors.fillColorPrimary, - decoration: TextDecoration.underline, - decorationColor: Styles().colors.fillColorSecondary, - decorationThickness: 1, - decorationStyle: TextDecorationStyle.solid), - ))), - ) - ], - ), - ), - ), - ) - ); - } - - void _onReceiveNotifications(BuildContext context) { - Analytics.instance.logSelect(target: 'Enable Notifications') ; - - //Android does not need for permission for user notifications - if (Platform.isAndroid) { - _goNext(context); - } else if (Platform.isIOS) { - _requestAuthorization(context); - } - } - -void _requestAuthorization(BuildContext context) async { - bool notificationsAuthorized = await NativeCommunicator().queryNotificationsAuthorization("query"); - if (notificationsAuthorized) { - showDialog(context: context, builder: (context) => _buildDialogWidget(context)); - } else { - bool granted = await NativeCommunicator().queryNotificationsAuthorization("request"); - if (granted) { - LocalNotifications().initPlugin(); - Analytics.instance.updateNotificationServices(); - } - print('Notifications granted: $granted'); - _goNext(context); - } - } - -Widget _buildDialogWidget(BuildContext context) { - return Dialog( - child: Padding( - padding: EdgeInsets.all(18), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - Localization().getStringEx('app.title', 'Safer Illinois'), - style: TextStyle(fontSize: 24, color: Colors.black), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 26), - child: Text( - Localization().getStringEx('panel.onboarding.notifications.label.access_granted', 'Your settings have been changed.'), - textAlign: TextAlign.left, - style: TextStyle( - fontFamily: Styles().fontFamilies.medium, - fontSize: 16, - color: Colors.black), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Analytics.instance.logAlert(text:"Already have access", selection: "Ok"); - _goNext(context, replace : true); - }, - child: Text(Localization().getStringEx('dialog.ok.title', 'OK'))) - ], - ) - ], - ), - ), - ); - } - - void _goNext(BuildContext context, {bool replace = false}) { - Onboarding().next(context, this, replace: replace); - } - - void _goBack(BuildContext context) { - Navigator.of(context).pop(); - } -} diff --git a/lib/ui/onboarding/OnboardingHealthConsentPanel.dart b/lib/ui/onboarding/OnboardingHealthConsentPanel.dart index be0dec3c..54237895 100644 --- a/lib/ui/onboarding/OnboardingHealthConsentPanel.dart +++ b/lib/ui/onboarding/OnboardingHealthConsentPanel.dart @@ -14,17 +14,13 @@ * limitations under the License. */ -import 'dart:io'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:illinois/service/Analytics.dart'; import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/BluetoothServices.dart'; import 'package:illinois/service/Health.dart'; import 'package:illinois/service/Localization.dart'; -import 'package:illinois/service/LocationServices.dart'; import 'package:illinois/service/Onboarding.dart'; import 'package:illinois/service/Styles.dart'; import 'package:illinois/ui/onboarding/OnboardingBackButton.dart'; @@ -50,9 +46,7 @@ class _OnboardingHealthConsentPanelState extends State _requestPermisions() async { - if (BluetoothServices().status == BluetoothStatus.PermissionNotDetermined) { - await BluetoothServices().requestStatus(); - } - - if (await LocationServices().status == LocationServicesStatus.PermissionNotDetermined) { - await LocationServices().requestPermission(); - } - } } typedef void OnWidgetSizeChange(Size size); diff --git a/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart b/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart index 06aedfce..44cce64a 100644 --- a/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart +++ b/lib/ui/onboarding/OnboardingHealthDisclosurePanel.dart @@ -131,15 +131,11 @@ class _OnBoardingHealthDisclosurePanelState extends State[ - _Bullet(), - Expanded( - child: Text( - Localization().getStringEx("panel.health.onboarding.covid19.how_it_works.line5.title", "Allow your phone to send exposure notifications when you’ve been in proximity to people who test positive."), - style: TextStyle(fontFamily: Styles().fontFamilies.regular, fontSize: 16, color:Styles().colors.fillColorPrimary), - ), - ), - ], - ), ], ) ), diff --git a/lib/ui/onboarding/OnboardingNotificationPanel.dart b/lib/ui/onboarding/OnboardingNotificationPanel.dart new file mode 100644 index 00000000..e5f8d396 --- /dev/null +++ b/lib/ui/onboarding/OnboardingNotificationPanel.dart @@ -0,0 +1,195 @@ +/* + * Copyright 2020 Board of Trustees of the University of Illinois. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/style.dart'; +import 'package:illinois/service/Analytics.dart'; +import 'package:illinois/service/Localization.dart'; +import 'package:illinois/service/Styles.dart'; +import 'package:illinois/ui/widgets/RoundedButton.dart'; +import 'package:illinois/utils/Utils.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/* + +"notification":{ + "id":"lorem.ipsum.1", + "title":"Lorem Ipsum" + "text":"Sed elit est, tincidunt quis porttitor nec, convallis eget turpis. Integer pulvinar, purus a mattis aliquam, mauris diam pellentesque est, nec laoreet nisi ligula pellentesque ante. Etiam lacinia aliquet nibh vel laoreet.", + "can_close":true, + "display":"once" | "verbose" | "default" | null, + "buttons":[ + {"title":"Vivamus Aliquam", "url":"https://illinois.edu", "url~android":"market://details?id=edu.illinois.rokwire", "url~ios":"itms-apps://itunes.apple.com/us/app/apple-store/id1476075513"} + ], +} +*/ + +class OnboardingNotificationPanel extends StatefulWidget { + + final Map notification; + final void Function(Map notification) onClose; + OnboardingNotificationPanel({Key key, this.notification, this.onClose}) + : super(key: key); + + @override + _OnboardingNotificationPanelState createState() => _OnboardingNotificationPanelState(); +} + +class _OnboardingNotificationPanelState extends State { + + @override + Widget build(BuildContext context) { + bool canClose = AppJson.boolValue(_notificationEntry('can_close')) ?? false; + + return Scaffold(backgroundColor: Styles().colors.background, body: + SafeArea(child: + Stack(children: [ + Visibility(visible: canClose, child: + Align(alignment: Alignment.topRight, child: + Semantics(button: true, label: Localization().getStringEx("dialog.close.title", "Close"), child: + InkWell(onTap: _onTapClose, child: + Padding(padding: (0 < MediaQuery.of(context).padding.top) ? EdgeInsets.only(right: MediaQuery.of(context).padding.top / 2) : EdgeInsets.only(top: 24, right: 24), child: + Container(width: 48, height: 48, alignment: Alignment.center, child: + Image.asset('images/close-blue.png', excludeFromSemantics: true,) + ), + ), + ), + ), + ), + ), + _buildContent(), + ],), + ), + ); + } + + Widget _buildContent() { + + Widget titleWidget = _titleWidget; + Widget bodyWidget = _bodyWidget; + List buttons = AppJson.listValue(_notificationEntry('buttons')); + + return Padding(padding: EdgeInsets.symmetric(horizontal: 32, vertical: 32), child: + SingleChildScrollView(child: + Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Container(height: MediaQuery.of(context).size.height / 8), + (titleWidget != null) ? Padding(padding: EdgeInsets.only(bottom: 40), child: titleWidget) : Container(), + (bodyWidget != null) ? Padding(padding: EdgeInsets.only(), child: bodyWidget) : Container(), + _buildButtons(buttons), + Container(height: MediaQuery.of(context).size.height / 4), + ],), + ), + ); + } + + Widget get _titleWidget { + String titleText = _notificationText('title'); + String titleHtml = _notificationText('title_html'); + if (AppString.isStringNotEmpty(titleHtml)) { + return Html(data: titleHtml, + onLinkTap: (url) => _onTapLink(url), + style: { "body": Style(color: Styles().colors.fillColorPrimary, fontFamily: Styles().fontFamilies.extraBold, fontSize: FontSize(36), textAlign: TextAlign.center, padding: EdgeInsets.zero, margin: EdgeInsets.zero), },); + } + else if (AppString.isStringNotEmpty(titleText)) { + return Text(titleText ?? '', style: TextStyle(color: Styles().colors.fillColorPrimary, fontFamily: Styles().fontFamilies.extraBold, fontSize: 36, ), textAlign: TextAlign.center); + } + else { + return null; + } + } + + Widget get _bodyWidget { + String bodyText = _notificationText('body'); + String bodyHtml = _notificationText('body_html'); + if (AppString.isStringNotEmpty(bodyHtml)) { + return Html(data: bodyHtml, + onLinkTap: (url) => _onTapLink(url), + style: { "body": Style(color: Styles().colors.textBackground, fontFamily: Styles().fontFamilies.medium, fontSize: FontSize(20), padding: EdgeInsets.zero, margin: EdgeInsets.zero), },); + } + else if (AppString.isStringNotEmpty(bodyText)) { + return Text(bodyText, style: TextStyle(color: Styles().colors.textBackground, fontFamily: Styles().fontFamilies.medium, fontSize: 20, )); + } + else { + return null; + } + } + + Widget _buildButtons(List buttonsJsonContent) { + if (AppCollection.isCollectionEmpty(buttonsJsonContent)) { + return Container(); + } + List buttons = []; + for (Map buttonContent in buttonsJsonContent) { + String title = _platformText(buttonContent, 'title') ; + buttons.add(Row(mainAxisSize: MainAxisSize.min, children: [ + RoundedButton( + label: AppString.getDefaultEmptyString(value: title), + padding: EdgeInsets.symmetric(horizontal: 14), + textColor: Styles().colors.fillColorPrimary, + borderColor: Styles().colors.fillColorSecondary, + backgroundColor: Styles().colors.white, + onTap: () => _onTapButton(buttonContent), + ),], + )); + } + return Padding(padding: EdgeInsets.only(top: 30), child: + Wrap(runSpacing: 8, spacing: 16, children: buttons), + ); + } + + static dynamic _platformEntry(Map json, String key, {String os}) { + return (json != null) ? (json["$key~${os ?? Platform.operatingSystem.toLowerCase()}"] ?? json[key]) : null; + } + + static String _platformText(Map json, String key, {String locale }) { + return AppJson.stringValue(_platformEntry(json, "$key~${locale ?? Platform.localeName.toLowerCase()}") ?? _platformEntry(json, key)); + } + + dynamic _notificationEntry(String key) { + return _platformEntry(widget.notification, key); + } + + String _notificationText(String key) { + return _platformText(widget.notification, key); + } + + void _onTapClose() { + Analytics.instance.logSelect(target: "OnboardingNotificationPanel: Close"); + if (widget.onClose != null) { + widget.onClose(widget.notification); + } + } + + void _onTapButton(Map button) { + String title = _platformText(button, 'title', locale: 'en'); + Analytics.instance.logSelect(target: "OnboardingNotificationPanel: $title"); + + String url = AppJson.stringValue(_platformEntry(button, 'url')); + if (AppString.isStringNotEmpty(url)) { + launch(url); + } + } + + void _onTapLink(String url) { + if (AppString.isStringNotEmpty(url)) { + launch(url); + } + } +} diff --git a/lib/ui/settings/SettingsHomePanel.dart b/lib/ui/settings/SettingsHomePanel.dart index 04d57bbc..6af43810 100644 --- a/lib/ui/settings/SettingsHomePanel.dart +++ b/lib/ui/settings/SettingsHomePanel.dart @@ -14,17 +14,14 @@ * limitations under the License. */ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:illinois/service/AppNavigation.dart'; import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/BluetoothServices.dart'; import 'package:illinois/service/Connectivity.dart'; import 'package:illinois/service/Health.dart'; -import 'package:illinois/service/LocationServices.dart'; import 'package:illinois/service/Organizations.dart'; import 'package:illinois/ui/onboarding/OnboardingLoginPhoneVerifyPanel.dart'; import 'package:illinois/ui/settings/SettingsFamilyMembersPanel.dart'; @@ -77,8 +74,6 @@ class _SettingsHomePanelState extends State implements Notifi bool _scanningHealthUserKeys; bool _resetingHealthUserKeys; - bool _permissionsRequested; - GlobalKey _qrCodeButtonKey = GlobalKey(); Size _qrCodeProgressSize = Size(20, 20); Size _qrCodeButtonSize; @@ -594,11 +589,11 @@ class _SettingsHomePanelState extends State implements Notifi }); } - void _updateHealthUser({bool consentTestResults, bool consentVaccineInformation, bool consentExposureNotification}){ + void _updateHealthUser({bool consentTestResults, bool consentVaccineInformation}){ setState(() { _refreshingHealthUser = true; }); - Health().loginUser(consentTestResults: consentTestResults, consentVaccineInformation: consentVaccineInformation, consentExposureNotification: consentExposureNotification).then((_) { + Health().loginUser(consentTestResults: consentTestResults, consentVaccineInformation: consentVaccineInformation,).then((_) { if (mounted) { setState(() { _refreshingHealthUser = false; @@ -690,16 +685,7 @@ class _SettingsHomePanelState extends State implements Notifi for (int index = 0; index < codes.length; index++) { String code = codes[index]; BorderRadius borderRadius = _borderRadiusFromIndex(index, codes.length); - if (code == 'exposure_notifications') { - contentList.add(ToggleRibbonButton( - height: null, - borderRadius: borderRadius, - label: Localization().getStringEx("panel.settings.home.covid19.exposure_notifications", "Exposure Notifications"), - toggled: (Health().user?.consentExposureNotification == true), - context: context, - onTap: _onConsentExposureNotifications)); - } - else if (code == 'provider_test_result') { + if (code == 'provider_test_result') { contentList.add(ToggleRibbonButton( height: null, borderRadius: borderRadius, @@ -881,35 +867,6 @@ class _SettingsHomePanelState extends State implements Notifi } } - - void _onConsentExposureNotifications() { - if (Connectivity().isNotOffline) { - Analytics.instance.logSelect(target: "Exposure Notifications"); - bool consentExposureNotification = Health().user?.consentExposureNotification ?? false; - if (Platform.isIOS && (consentExposureNotification != true) && (_permissionsRequested != true)) { - _permissionsRequested = true; - _requestPermisions().then((_) { - _updateHealthUser(consentExposureNotification: !consentExposureNotification); - }); - } - else { - _updateHealthUser(consentExposureNotification: !consentExposureNotification); - } - } else { - AppAlert.showOfflineMessage(context); - } - } - - Future _requestPermisions() async { - if (BluetoothServices().status == BluetoothStatus.PermissionNotDetermined) { - await BluetoothServices().requestStatus(); - } - - if (await LocationServices().status == LocationServicesStatus.PermissionNotDetermined) { - await LocationServices().requestPermission(); - } - } - void _onConsentTestResult() { if (Connectivity().isNotOffline) { Analytics.instance.logSelect(target: "Consent Test Results"); diff --git a/lib/ui/settings/SettingsPersonalInfoPanel.dart b/lib/ui/settings/SettingsPersonalInfoPanel.dart index 85f2acf5..979404a8 100644 --- a/lib/ui/settings/SettingsPersonalInfoPanel.dart +++ b/lib/ui/settings/SettingsPersonalInfoPanel.dart @@ -18,7 +18,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:illinois/service/Analytics.dart'; import 'package:illinois/service/Auth.dart'; -import 'package:illinois/service/Exposure.dart'; import 'package:illinois/service/Health.dart'; import 'package:illinois/service/Localization.dart'; import 'package:illinois/service/UserProfile.dart'; @@ -46,7 +45,6 @@ class _SettingsPersonalInfoPanelState extends State { bool piiDeleted = await Auth().deleteUserPiiData(); if(piiDeleted) { await Health().deleteUser(); - await Exposure().deleteUser(); await UserProfile().deleteProfile(); // Auth().logout() - invoked by UserProfile().deleteProfile() } diff --git a/lib/utils/AppDateTime.dart b/lib/utils/AppDateTime.dart index e318cbdd..44f5d3bc 100644 --- a/lib/utils/AppDateTime.dart +++ b/lib/utils/AppDateTime.dart @@ -67,8 +67,8 @@ class AppDateTime { } } - static DateTime midnight(DateTime date) { - return (date != null) ? DateTime(date.year, date.month, date.day) : null; + static DateTime midnight(DateTime date, {int offsetInDays}) { + return (date != null) ? DateTime(date.year, date.month, date.day + (offsetInDays ?? 0)) : null; } static DateTime get todayMidnightLocal { diff --git a/pubspec.lock b/pubspec.lock index 9e35f62b..a80a4f52 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" cookie_jar: dependency: "direct main" description: @@ -175,7 +182,7 @@ packages: name: encrypt url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.0.1" fake_async: dependency: transitive description: @@ -566,7 +573,7 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.3.5" process: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 79bdf4a5..57331613 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Illinois client application. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.11.5+1105 +version: 2.12.5+1205 environment: sdk: ">=2.2.0 <3.0.0" @@ -42,7 +42,7 @@ dependencies: flutter_local_notifications: ^5.0.0+4 sprintf: ^6.0.0 sqflite: ^2.0.0+3 - encrypt: ^5.0.0 + encrypt: ^5.0.1 package_info: ^2.0.0 device_info: ^2.0.0 connectivity: ^3.0.3